Go interop

Status: the Rust compiler (rust/, cargo build --release -p sky) is the primary Sky compiler; the Haskell compiler is preserved under legacy-haskell-compiler/. Verified by the example sweep + compiler test suite (cargo test + xtask gates). See ../compiler/versions.md for the changelog.

Sky imports any Go package and uses it with full type safety.

See also: boundary-philosophy.md — why every FFI call returns Result Error T, when to reach for FFI vs Sky's stdlib, and the Result-vs-Task distinction.

The promise

Adding a package

sky add github.com/google/uuid

This:

  1. Runs go get inside sky-out/ to fetch the module.
  2. Runs tools/sky-ffi-inspect to emit a JSON description of every public function, type, and struct field.
  3. Generates .skycache/ffi/<slug>.{skyi,kernel.json} and .skycache/go/<slug>_bindings.go.
  4. Adds the dep to sky.toml under [go.dependencies].

sky install (or any subsequent sky build) regenerates missing bindings idempotently.

Using a package

import Github.Com.Google.Uuid as Uuid
import Sky.Core.Result as Result
import Sky.Core.Error as Error
import Std.Log exposing (println)


main =
    -- Every FFI call returns Result Error T — pattern match or
    -- use Result.withDefault to get the value out.
    case Uuid.newString () of
        Ok id ->
            println id

        Err e ->
            println ("uuid failed: " ++ Error.toString e)

Zero-arg Go FFI functions are called with () in Sky — the inspector emits a () -> R Sky signature for every zero-param Go function so the call site stays explicit. (Sky-side kernel zero-arity bindings like Uuid.v4 are different — those are kernel-registered and called as bare values; the () rule is only for FFI-generated wrappers.)

Module name mapping:

Go pathSky module
github.com/google/uuidGithub.Com.Google.Uuid
github.com/stripe/stripe-go/v84Github.Com.Stripe.StripeGo.V84
net/httpNet.Http
fyne.io/fyne/v2/appFyne.Io.Fyne.V2.App

Hyphens are dropped, next character upper-cased. Non-alphanumerics become _.

Return type mapping

Every Go FFI call returns Result Error T. This is intentional — the FFI boundary is a trust boundary — same discipline as a typed-airlock FFI. See boundary-philosophy.md for the full reasoning.

The wrapping shape depends on what the Go function returns:

Go returnSky type
T (single, no error)Result Error T
*T (single pointer, no error)Result Error T (opaque; nil-deref panic → Err via recover)
(T, error)Result Error T
errorResult Error ()
(T, bool) (comma-ok)Result Error (Maybe T)
(T, *NamedErr) where NamedErr implements errorResult Error T
(T, U) (neither error nor bool)Result Error (T, U)
(T, U, error)Result Error (T, U)
(T, U, V)Result Error (T, U, V)
[]TResult Error (List T)
map[string]VResult Error (Dict String V)
*pkg.Struct (opaque)Result Error Struct (with generated getters/setters)
interface{} / anyResult Error any (boxed)
void (no return)Result Error ()

Element-type mapping (used inside the wrappers above):

GoSky
stringString
int, int64, int32Int
float64Float
boolBool

Notes:

Opaque struct pattern (Sky's builder convention)

Go structs are opaque — you build them via generated constructors and pipeline setters. Every step returns Result Error T (constructor, every setter, every getter), so chains use Result.andThen:

params =
    Stripe.newCheckoutSessionParams ()
        |> Result.andThen (Stripe.checkoutSessionParamsSetMode "payment")
        |> Result.andThen (Stripe.checkoutSessionParamsSetSuccessURL "https://example.com/success")
        |> Result.andThen (Stripe.checkoutSessionParamsSetLineItems [ lineItem ])

Naming rules:

Setters take the value first and the struct second — so they pipe naturally via |> + Result.andThen. The Result wrap covers the boundary failure modes (nil receiver, panic, type mismatch); a successful chain returns Ok params.

Pointer fields are auto-wrapped. For Mode *string, you pass a plain String and Sky wraps &v on the Go side.

Callbacks (Go function values)

import Net.Http as Http
import Github.Com.Gorilla.Mux as Mux


handler : Http.ResponseWriter -> Http.Request -> Task Error ()
handler w req =
    Http.writeString w "Hello!"


main =
    let
        router = Mux.newRouter ()
        _ = Mux.routerHandleFunc router "/" handler
    in
        Http.listenAndServe ":8000" router

Sky handles the func(ResponseWriter, *Request) signature by wrapping the Sky closure in a Go adapter.

Large packages (Stripe SDK, Fyne, Firestore)

The FFI generator emits typed and reflect-typed variants per function. Unused bindings are stripped at build time by dceFfiWrappers:

You pay for what you use.

When Sky can't type a Go symbol

Some Go shapes don't have a compile-time-expressible type:

For these, Sky emits a reflect-typed wrapper (Go_X_y(arg0 any) any) that works at runtime via reflect.Value.Call. You lose Go-side static type checking but the code still compiles and runs.

See ffi-design.md for the classification algorithm.