Foreign interoperability

A package reaches code the compiler did not compile through a facade: a module marked foreign after the module keyword. In a facade only signatures are accepted — no bodies, no infix declarations, no type declarations. A signature may be a function's or, as below, a constant's. Only a subset of the Zelkova types may appear as a parameter or return type in one of those signatures.

This is the only way out of Zelkova. There is no privileged escape hatch the standard library may use and a user's package may not: a standard-library module that needs a foreign primitive declares a facade in the same syntax any user package would.

The idea is close to TypeScript's type definitions, with a stricter rule about which types a signature may name.

A facade is only usable from inside the package that declares it. The module name is not exposed to any other package.

A facade declares an effect unless it says otherwise. Its result type is Task (Result Failure a), its companion may do anything, and the compiler wraps the call (An effectful facade). Writing unsafe before a signature removes the Task and leaves a plain function type, which its author promises is pure and total (An unsafe facade).

expect=ok
module foreign Core.Prim exposing
  ( fdiv
  , idiv
  )

unsafe fdiv : Float -> Float -> Float
unsafe idiv : Int -> Int -> Int

Two signatures rather than one, because a facade names the types the code behind it really handles: the division behind fdiv divides, the one behind idiv truncates.

A facade names a boundary, not a backend

Zelkova compiles to more than one target, and a facade is written once for all of them. What varies is the companion: a file of the same base name, sitting beside the facade in the same directory, holding the code the signatures describe. There is one companion per target.

src/
  Core/
    Prim.zel     module foreign Core.Prim
    Prim.mjs     the JavaScript behind it
    Prim.wasm    the WebAssembly component behind it

A build reads the companion for the target it is building and ignores the others. Neither the facade nor anything importing it names a target, so a module written above a facade — Basics, say — is compiled for every target unchanged.

One signature list serves every target. Both companions above export fdiv and idiv, with the types Core.Prim declares for them. A facade whose exports cannot be provided on some target is not one facade: it splits into one facade per target, each shipping the companion it has.

A facade with no companion for the target being built is an error, and the error names the facade and the target. A facade that ships one companion is a package that builds for one target — a facade over a browser API, say.

The JavaScript companion

An ES module. Its exports take a plain parameter list: a Zelkova function of two arguments is backed by a JavaScript function of two arguments, called directly. Currying is the compiler's business.

The WebAssembly companion

A component, whose exported interface is written in WIT. Its exports take a plain parameter list on the same terms, and each is declared with the WIT spelling of the Zelkova type in its signature.

Which types may cross the boundary

A type may be named in a facade signature when every target can hold the code behind the facade to it. Two targets means two mechanisms, and a type is admitted when it has both.

A value that fails a check becomes Err (Malformed ..) out of an effectful facade, and aborts the program out of an unsafe one, which has no result type to carry it.

A facade has no body the compiler can read, so its annotation is enforced by the target rather than while compiling. Four of the six forms a type expression can take have both mechanisms:

TypeWhat its JavaScript predicate decidesWhat its WIT spelling is
Intthe value is a bigint the 64-bit range holdss64
Floatthe value is a numberf64
Boolthe value is a booleanbool
Charthe value is a string holding one Unicode scalar valuechar
Stringthe value is a string with no surrogate outside a pairstring
()the value is undefined; a result is discardednothing: no parameter, and no result
A tuplethe value is an array of the tuple's length, each element satisfying its component's predicatetuple of its components' spellings
A recordthe value is an object with exactly the record's fields, each field satisfying its own predicatea record of the same fields
A listthe value is an array, every element of which satisfies the element type's predicatelist of the element's spelling
A union type, applied to admitted typesthe value carries one of that type's constructor names, and arguments satisfying that constructor's predicatesa variant, one case per constructor

An Int crosses as a BigInt and not as a number: a JavaScript number is a binary64, exact on integers only to 2^53, and Int's range is wider. A companion holding a number converts before returning it, and one taking an Int is handed a BigInt.

A JavaScript string is a sequence of UTF-16 units and may hold a surrogate that is half of no pair. Such a string is no String, and a lone surrogate no Char, so both fail their checks.

Known gap: a string holding a lone surrogate should fail the String check, and a lone surrogate the Char check. Both pass today, and Comparable orders the value above every character up to U+FFFF. docs/tickets/lang-91.md tracks it.

The first five rows are the scalar types, which the compiler knows by the qualified name of each declaration. They are admitted by identity and not by spelling: a module declaring its own Int names an ordinary union in a facade signature, admitted — if at all — under the union row.

A signature may name any of those, in any position an argument or a result may take.

expect=ok
module foreign Core.Colour exposing
  ( rgb
  , luminance
  )

unsafe rgb : Int -> (Int, Int, Int)
unsafe luminance : (Int, Int, Int) -> Float

The two mechanisms cost different things. A predicate walks the whole value, so a facade returning a list of a thousand tuples checks a thousand tuples on the way out, once per crossing; the same facade on a WebAssembly target checks nothing at the call, because the interface was checked when the component loaded.

Not implemented: lists have no literal production (LANG-44), so that row is about a type a program cannot write today. Records and Lists each specify their construct, and neither chapter publishes an encoding: what a record and a list look like across either boundary belongs to code generation. Nothing reads a WIT interface either, since no WebAssembly is produced (GEN-15).

A union crosses as a tagged value

A companion may receive a union value and construct one, so the two sides have to agree on its shape.

In JavaScript a union value is an object carrying the name of its constructor in a $ field, with that constructor's arguments in declaration order in fields named a, b, c and so on. Red is {$: "Red"}; Rgb 255 0 0 is {$: "Rgb", a: 255n, b: 0n, c: 0n}. The predicate reads $, checks it against the declaration's constructors, and checks each argument against the predicate of the type that constructor declares for it.

In WebAssembly it is a variant with one case per constructor, named after it, carrying that constructor's arguments as its payload — a tuple of them where there is more than one.

expect=okpackage=union
module Palette exposing (Swatch(..))

type Swatch
  = Named Int
  | Rgb Int Int Int
expect=okpackage=union
module foreign Core.Palette exposing
  ( toHex
  )

import Palette exposing (Swatch(..))

unsafe toHex : Swatch -> Int

A constructor's name is part of what a companion depends on, on either target, so renaming one breaks every companion that mentions it.

A union may be recursive, and the JavaScript predicate follows it: the walk descends into each argument and terminates because a Zelkova value is immutable and can hold no cycle.

Not implemented: no WebAssembly is produced and no interface is read (GEN-15).

The unit value crosses as undefined

() has one value, and in JavaScript it is undefined (DEC-21).

A result of () is discarded, not checked. A facade whose result is () — unsafe f : X -> (), or the payload of Task (Result Failure ()) — ignores whatever its companion returns and yields undefined.

A parameter of () keeps its slot. A companion is handed undefined where the signature declares a () parameter, so its positions match the plain parameter list the signature describes. A companion may leave a trailing () parameter undeclared, since JavaScript reads an omitted trailing argument as undefined.

Nested, the check is strict. A () inside a tuple, a record, a list or a union argument a companion returns must be undefined, checked like any other field or element. A record field of type () must be present, as { f: undefined }.

What a facade signature may not name

Two of the six forms can be enforced by neither target, and a facade signature may name neither.

A type variable excludes no value, so there is nothing for a predicate to decide, and WIT has no variable to spell it with. A facade is therefore monomorphic; polymorphism lives in ordinary Zelkova above it.

A function type is rejected wherever it appears. typeof x === 'function' decides that a value is some function, not that it is the one declared, and a component's interface has no function type to name either.

expect=canonical-error:FacadeTypeNotAdmitted
module foreign Core.Utils exposing
  ( equal
  )

equal : a -> a -> Bool
expect=canonical-error:FacadeTypeNotAdmitted
module foreign Core.List exposing
  ( count
  )

count : (Int -> Bool) -> Int -> Int

A class constraint is rejected on the same grounds, Comparable a => a being a signature over a. A constrained function is specialised, and a facade has no body to specialise. Type classes is the chapter.

An effectful facade

A clock, a file and a socket are what a program reaches foreign code for, and none of them is a function of its arguments. So a facade declares an effect by default: its companion may read, write, wait and fail.

A facade signature declares an effect, and its result type must be Task (Result Failure a). Any other result type is an error unless the signature is marked unsafe, so a facade cannot declare its foreign code to be infallible by saying less. Failure is the error type every one of them names, a being the only part its author chooses. It is not one of the default imports, so a facade naming it imports it.

expect=unimplemented
module foreign Core.File exposing (read)

import Task exposing (Failure)

read : String -> Task (Result Failure String)

A Task never crosses the boundary, and neither does the Result. The companion takes the arguments the signature names and returns the payload — a string, for read:

export async function read(path) {
  return await fs.promises.readFile(path, "utf8");
}

That companion throws when the file is missing, and it is a correct companion. The Result is built on the Zelkova side, by the wrapper the compiler puts around the call: it catches what the companion throws, checks what the companion returns against a, and yields Ok for a value that passes, Err (Threw ..) for a failure the companion raised, and Err (Malformed ..) for a value that does not match.

What counts as raising a failure is the target's own word for it: a thrown exception or a rejected promise in JavaScript, a trap in WebAssembly. Both arrive as Err (Threw ..).

a is the type the companion really hands back: it is checked at the boundary like any other returned value, and it may be neither a type variable nor a function type.

A companion whose result is not ready at once returns it the way its target returns a value later, and the check runs on the value that arrives. A result that never arrives is a Task that never produces a value, which is the second of the two outcomes rather than a failure.

A facade constant may name a Task too, and it is the one constant whose JavaScript companion is a function: the effect has to happen each time the Task is run, and a value exported once happens once.

expect=unimplemented
module foreign Core.Time exposing (now)

import Task exposing (Failure)

now : Task (Result Failure Int)
export function now() {
  return BigInt(Date.now());
}

An effectful facade is a facade, so it is not importable outside the package that declares it. A package offers an effect to its dependents the way it offers any other foreign-backed value: an ordinary module imports the facade and re-declares what it offers.

Task may appear only as the whole of a result type. Not as an argument; and not nested inside another type.

A failure the caller is meant to tell apart from a broken companion goes in the payload. Failure reports that the foreign code broke and says nothing about what the effect was for. A read separating a missing file from a broken companion declares String -> Task (Result Failure (Result IoError String)) — two Results, collapsed into one by the module that publishes read to other packages.

Not implemented: neither block above compiles under this harness, which compiles a block against a stand-in Basics and nothing else, so their import Task finds no module even though zelkova-core declares Task and Failure.

An unsafe facade

unsafe before a signature declares a function rather than an effect. The result type is then any admitted type, the Task and its Result are gone, and no wrapper stands between the caller and the companion.

expect=ok
module foreign Core.Basics exposing (idiv)

unsafe idiv : Int -> Int -> Int

The word is a promise its author makes to the compiler, covering the two things the compiler cannot check and does not try to:

A companion that breaks the first computes wrong answers and nothing reports it. One that breaks the second aborts the program.

unsafe removes the Task, not the check. The return value is held to its declared type exactly as any other crossing is, and a value that fails aborts.

Not implemented: a companion that throws does not abort naming its export — the check is only run over a returned value, so a throw propagates as the companion's own exception instead (GEN-23).

Facade constants

A facade signature may also declare a constant — a type with no arrow, taking no arguments.

expect=ok
module foreign Core.Basics exposing
  ( pi
  , e
  )

unsafe pi : Float
unsafe e : Float

The plain-parameter-list rule above has a zero-argument case, and the two targets spell it differently. A JavaScript companion exports the value itself — unless the constant names a Task, which is the one case where it exports a function:

export const pi = Math.PI;
export const e = Math.E;

A component exports only functions, so a WebAssembly companion exports one of no arguments returning the value, and its WIT declares that.

Evaluation is strict, and an ordinary parameterless binding is placed in an evaluation order that reads off which declarations it depends on — see A binding with no parameters is evaluated once. A facade constant has no Zelkova body to place in that order: it names a foreign binding directly, so it is evaluated on whatever schedule the target gives that — when the .mjs module exporting it is evaluated, or when the component's export is called — and nothing here promises it happens once, lazily, or at any particular point relative to the rest of the program.

Testing a companion

A companion is target code, and so is its test: a JavaScript assertion about a JavaScript function. It reaches Zelkova the way any other foreign code does — as a facade — and sits under tests/, the root a dependent never compiles.

src/
  Core/
    Prim.zel        module foreign Core.Prim
    Prim.mjs
    Prim.wasm
tests/
  Core/
    PrimChecks.zel  module foreign Core.PrimChecks
    PrimChecks.mjs
    PrimChecks.wasm
    PrimTest.zel    the Test values a runner finds

Everything a facade is holds of one under tests/: signatures and no bodies, one companion per target, the admitted types in every position, and the same error when the target being built has no companion. The two roots share one set of module names, so a facade that checks Core.Prim takes a different name.

A test facade declares an effect, and is never unsafe. A failed assertion is a failure the companion raised, and the wrapper around an effectful call turns that into Err (Threw ..), which the module above reports as a failing test. The same assertion behind an unsafe signature aborts the program instead of failing one test.

expect=unimplemented
module foreign Core.PrimChecks exposing
  ( idivTruncates
  , idivRefusesAFraction
  )

import Task exposing (Failure)

idivTruncates : Task (Result Failure ())
idivRefusesAFraction : Task (Result Failure ())

Each is a facade constant naming a Task, so its JavaScript companion exports a function, and the target's own assertion library raises:

import assert from "node:assert/strict";
import { idiv } from "../../src/Core/Prim.mjs";

export function idivTruncates() {
  assert.equal(idiv(7, 2), 3);
}

export function idivRefusesAFraction() {
  assert.throws(() => idiv(7.5, 2));
}

A test companion may import the companion it checks as a module of the target, which is what the second export above does: Core.Prim is reached as a .mjs file, with no boundary between them. A test companion must name that file by the shortest relative path between the two in the package's source, out of tests/ and into src/; that import reaches the same companion however a build places the two files. A value crossing a boundary is checked against the type its signature declares, so a test written above Core.Prim can hand idiv only what Int admits. What a companion does with a value outside that set is a question only the target can ask it.

A runner finds a value of type Test a module under tests/ exposes, so Core.PrimTest imports the facade and exposes one per check.

Not implemented: the spec harness compiles a block against a stand-in Basics only, so the zel block does not compile there.