10. The host boundary
10.1 External functions
@external("module", "function")
name: (T1, …, Tn) -> R This declares a function that the host gives. It is the Wasm import function in the import module module, encoded as in Appendix B. The full type must be written. The type has one shape, its parameter data has positions only, and its parameters and value must be types that the host can take (§10.3). It has no need, or exactly <- (Fail(E)) with an E that the host can take; the host’s error is then raised through the caller’s Fail. Every call of an external function is done when it is reached, like all calls. An external function should have no side effects that i code may see; work with side effects on the host uses host services (§9.7).
10.1.1 JavaScript value declarations
The form is
@js("expression") name : T, where T expands to one opaque type (§4.3). No holes, type variables, unions, functions or constructed
resource types. The annotation is required. It creates no i constructor or
cast between labelled types. i/js.Value = Opaque is one ordinary use; packages can
define Element = Opaque or other opaque types. JS is specific to this attribute;
it is not a target-neutral foreign language feature.
For example, an opaque type defined by a package needs no library rule:
Element = Opaque
@js("globalThis.document.body")
body : Element Each retained declaration becomes a zero-argument value import returning
externref, with a cache in the loader, rather than an imported mutable
global. Its first and only initialization call is emitted in ilang_start,
in §2.5 dependency/module/source order alongside ordinary initializers.
The host parses the text as one parenthesized expression in strict ESM scope.
It may name globals but not generated loader locals, other declarations or i
bindings. The compiler does not type-check JS. Syntax failure is a loader
error before startup; an initializer exception stops startup and no entry
runs. Initializers may have host side effects. Use function expressions to defer work to an explicit Js
call. A retained declaration is available only on js and nodejs. On any other target, it is a compile error under §10.1.2.
No initializer or synchronous bridge call may re-enter i. Callback re-entry is not defined.
10.1.2 Foreign linking
This rule defines acceptance and foreign initializer execution. Run it after ordinary type checking, before optimization, with the same result at every optimization level. There is no per-input specialization, constructor propagation, constant folding, Boolean evaluation, type-directed branch elimination or implementation-chosen widening. There is exactly one branch-pruning rule below.
The finite graph has one node for each ordinary binding’s initializer, each source function body (each branch of a chain is one), and each foreign declaration. References are resolved lexical definition identities. Start with all ordinary top-level initializers and emitted entries, exports and tests. Scanning an initializer keeps §2.5: making a function does not scan its body. A reference to an existing binding, including a name bound to another name or a function passed as data, follows its definition and activates all function bodies syntactically contained in that definition. Scanning an activated body visits all expressions and guards, both sides of Boolean operators, and every branch except this section’s sole pruning case. A function reached in an activated body is itself activated, even if it is never called. A non-direct initializer scans its nested function bodies too, since it may execute them. Reads of foreign declarations retain them.
Iterate these fixed reference edges to the least fixed point. Every source body has one node shared by all callers; a second call adds no specialized copy. Recursion terminates by marking nodes once. References to bindings of data activate all functions in their definitions, not only a selected key. Higher-order parameters add no inferred callee set: all supplied functions are already activated where they are referenced, stored, returned or passed. This closed program cannot receive an i function from the host (§10.3). So an unknown function parameter cannot hide an unvisited source function. No function-value analysis or optimizer discovery changes this graph.
The only pruning case. The platform provides Target (§9.7) at every entry, holding the member of Target’s data for the target being built. A subject is certified only when it is the name bound by target : Target in the service pattern of an emitted entry, or a lexical name made solely by alias = certified_name (a group does not change this). The entry binding must have no source value reference anywhere in the compiled program, including as a higher-order value or recursive call; its imports, exports and entry configuration are declarations, not references. So no i call can provide another Target to that body. A parameter, a function result, a key read, a constructor expression, a service pattern of any other function, and a Target provided by i code are unknown. There is no propagation through arguments or results.
A function’s direct lexical capture of a certified name or alias remains certified. A newly bound or covering name is a different definition and gains certification only through the alias rule. A provide of another Target does not change a name already bound; later functions that take Target from their caller see the provided value and are unknown. keep (§9.4) activates its argument by ordinary reference reachability and grants no certification to the services it keeps. These rules use lexical identity, not run-time analysis.
For the source form certified_name |> chain (allowing a one-expression { } group around the subject), omit a branch’s guard and body only if its pattern is Target(C(…)) or Target(C) for a resolved constructor C that is not the build’s member, allowing patterns for the data of C and conjunctions with names. For alternatives, this must hold for every alternative; any other alternative prevents pruning. Compare resolved constructor identities, not names or target labels. js and nodejs have different members. A catch-all, a type test or any other pattern is retained. A guarded branch with the build’s member is always possibly executed, even if its guard is literally False; later branches are retained too. No first-match or coverage reasoning removes any other branch. Calling a named chain, or passing the subject to a function, gives no pruning. Type-check all branches first. Suppressing an impossible branch’s references does not remove the pattern-failure Assert behavior of a one-branch match, or any other source meaning. This rule changes no inferred need. The compiler keeps this source match until the analysis is done; matches introduced by an optimizer cannot gain certification.
Every retained foreign declaration is initialized exactly once in §10.1.1 order, even if an optimizer removes its reads. An unretained declaration is never initialized. On an unavailable target a retained declaration is a compile error at its declaration and a requiring reference, never null or a late trap. Ordinary top-level forced = foreign always retains it. Two retained @js("({})") declarations produce distinct objects; reads of one keep its identity within its opaque type (§4.1, §11.4).
Passing Target to a helper, taking it in a helper’s service pattern, or making the matching constructor also gives no pruning. An uncalled function referenced in retained code still retains its foreign reads. This loss is deliberate and predictable. A direct entry match remains expressible under the certified-entry rule.
The generated artifact and loader metadata are specified in §B.6.1. Host exports always use the fixed ABI conversions, never a dynamically replaced Js converter.
10.2 Entry points
These functions are entry points:
- The program entry: the selected top-level function of the compiled package (§2.6). The default is
maininsrc/main.i; the namemainin any other module is ordinary. Withi run NAMEori build NAME, use that[entries]target instead. Its parameter group is(), and its result is unused. The binding may be an alias or an expression that computes a function value; its inferred function type determines the parameter group. After ordinary module initialization, the entry invokes that initialized value through the ordinary call rules (§6.6). A missing default means no program entry. - Tests: the entries of the collection named by
[test] collectionini.toml(§14.5).std/testprovides ordinary imported item macrostest,skip,only,fails,suite, andeach, which contribute private functions and display metadata tostd/test.tests. For example,(testlib = (test) = std/test)makestest("addition", () { assert(1 + 1 == 2) })available. These names have no special language meaning. Each selected function has the parameter group()and givesNilorNever. A test passes when it givesNil. It fails on a stop (§11.7) or an error that reaches the platform’sFail.failsreverses that expectation;skipreports an entry without running it, andonlyselects focused entries.suiteprefixes names and provides the services written in it to its test bodies;eachcontributes one test for each written case. The runner compiles the collection once and runs each selected test in a fresh started instance (§B.1). With no configured collection, no tests run. - Host exports: the functions named in
[wasm].exports(§2.6) of the package that is compiled. The[wasm]of other packages is not used. They must be in the exports of their module. A host export has one shape, its parameter data has positions only, and its parameters and value must be types that the host can take (§10.3).
Platform services. At an entry the platform of the target provides its services (§9.7): the host services of its table, Assert, Scope, Root, Owner, Await, Runner, Scheduler, Target, and a top-level Fail. For the program entry and tests, that Fail takes any error, and an error that reaches it stops the program or fails the test. For a host export, it takes only an error type that the host can take, and the error is given to the host (Appendix B).
The need of an entry, after what the entry’s own code provides, may contain only services that the platform of the target provides. Otherwise it is the compile error “T is not provided by the target t”, also when the program catches every error of the operations that need T. Checking with no target checks the entry’s shape and types, but not its need.
On targets using the asynchronous entry protocol, an entry is async exactly when its need has a host service whose declaration is "async", or Runner. Await, Owner and Root alone do not make it async. A requested synchronous export that is async is a compile error (§2.6.2). The host waits through Appendix B’s protocol. WASI instead keeps blocking imports, synchronous metadata and no async hand-off (§B.7). When the need of an entry has Scheduler, Runner, Await, Owner or Root, the entry runs in a fresh task run (§9.9) that provides them.
The entry context is opened before the entry runs, and closed on every exit, after structured cleanup and before the result or error is delivered. A stop runs no i finals. A synchronous context cannot hand off: a low-level wait needing hand-off or poll stops with request: no active entry. An async entry may return status 0 on its first call; scheduler switches alone do not yield to the host.
10.3 Types the host can take
Nil, Bool, Int, Float, String, BitArray, and opaque types (§4.3). Values of opaque types go to the host as opaque references. No other type may go to the host: data, labelled types with data, and functions do not cross. An error type that the host can take is a union of Nil, Bool, Int, Float, String, and BitArray, with no two the same.
Where i code gives a value to the host, an Int or String member counts as Int or String, whatever its value set (§4.2): a literal type, a union of literal types of one type constructor (also through a name), or all the values but a finite set, such as Int - 0. Where the host gives the value (the value of an external function, the value and errors of a host-service function, and the parameters of a host export), an Int or String member must have all its values, because the host’s values are checked only for their form (§B.2).
@external("app", "mode")
mode : () -> { "on" | "off" } // compile error: the host gives the value
@external("app", "log")
log : ("info" | "warn") -> Nil // correct: i code gives the value The outer Request and optional Result in a request-mode host declaration describe how work is obtained; they are not additional boundary types. Requests, tickets, tasks, groups, channels and events cannot cross the interface. IO types over Int and constructed Thrown and Mismatch values are not boundary types; their libraries pass Int or an opaque outcome and decode in i. JavaScript value declarations do not change the value and error lists above. No library-specific exception is added.