DocsLanguage

Native Go integration.

Implement a small, typed Fango boundary with an adjacent Go sidecar.

When Fango code needs an operation that’s implemented in Go, you can write a native sidecar for it. Keep the boundary small, and make sure its behavior matches what the Fango declaration promises.

Add a sidecar.

Save this as Hash.fango:

module Hash exposing (crc32)

crc32 : String -> Int
crc32 = native

Save the implementation next to it as Hash.native.go:

package native

import "hash/crc32"

func Crc32(text string) int64 {
    return int64(crc32.ChecksumIEEE([]byte(text)))
}

The exported Go function has the Fango name with an initial capital. From main.fango, import Hash and call Hash.crc32 "hello". Check and run the entry program as usual, and the compiler includes the sidecar for you. User sidecars may only import packages from the Go standard library.

Boundary types.

Fango Go
Int int64
Float float64
String string
Char rune
Bool bool
Unit argument/result Omitted argument/no result

A string result must be valid UTF-8, and a character result must be a Unicode scalar. If a result isn’t valid, the program panics at the boundary. The ordinary user-sidecar ABI doesn’t support arbitrary records, functions, class constraints, or multiple Go results, and a user sidecar can’t return a Go error as an extra result.

Wrap an opaque handle.

A type from the same module, with one constructor and one supported boundary field, can cross the boundary as its underlying value. Keep the constructor private so callers can’t forge the wrapper. Runtime.Native.Any carries a Go object behind such a wrapper, and the native code is responsible for getting that right.

Mark external resource types with {-# resource #-} and provide cleanup scopes for them. Holding on to a handle doesn’t extend the lifetime of the external resource behind it.

Indexed native storage.

A phantom parameter can tie a native handle to the type of value it stores. For example, Key a = Key Int carries an integer at runtime but keeps a in its checked type.

Native signatures have to preserve that index. A function that turns a Key a into a Key b fails with NATIVE STORAGE. Once a wrapper is used this way, Fango code can’t apply or match its constructor, even in the module that defines it, because unpacking the integer and rebuilding it at a different index would defeat the protection.

Representation-blind storage works differently: it wraps Runtime.Native.Any, and its native functions receive payloads as Go any tokens. They may store and return those tokens, but they must not inspect or invoke them, or substitute a different representation. Unit payloads still cross as tokens. Keep reads and writes at the same type index, and give writes a Unit or Bool result.

This is a feature for building libraries, not a way around Fango’s type checks. Retaining a payload keeps that value alive, but it doesn’t extend the external lifetime of a resource the payload refers to.

Native effect operations.

An effect operation can use a sidecar as its fallback implementation:

effect Clock
    tick : () -> Int = native

The sidecar exports func Tick() int64, with the Unit argument omitted just as in an ordinary native declaration. If a Fango handler for tick is installed, it takes precedence over this fallback, so callers can replace the clock’s behavior without touching any use sites.

Effects on native declarations stay part of their callable types. If a native function does observable external work, put it behind an effect rather than describing it as pure.

FangoHost.

A sidecar gets its process context from the reserved FangoHost, which isn’t necessarily the Go worker’s own standard streams. For instance, a native output implementation can call:

if err := FangoHost.WriteOutput([]byte(text)); err != nil {
    panic(err)
}

The host also provides ReadInputLine, ReadInputBytes, WriteError, Arguments, Environment, WorkingDirectory, Exit, and ExecutionContext. Compiled programs install a system host, and the interpreter installs a proxy to the active session. Native function signatures don’t receive a hidden context argument.

Use the host only during a native call. Don’t replace it, and don’t keep it around for background work. Access to the host’s input buffer and each output write are serialized, and line and byte input share the same buffer.

Build and interpreter lifecycle.

Compiled programs include the sidecar in their generated project. The interpreter and REPL run checked code next to sidecars in a persistent worker. Package globals last as long as that worker session does, but importing a new sidecar can rebuild the worker. Sidecar functions are discovered automatically, so a library with a sidecar needs no registration in the compiler or the REPL.

A sidecar function is invisible to handlers, so code that calls it can’t be tested with a simulated service. If tests need to replace it, expose it through an effect of your own, as the standard library’s services do.

Sidecars are trusted native code. They can’t run in the browser playground, and external native calls aren’t available during compile-time evaluation. Inline native Go templates are syntax bundled with the compiler, not a feature for user modules.