2. Modules
2.1 Packages and modules
A package is a directory with an i.toml file in it (§2.6). Every .i or .is file under its src/ or test/ directory is a module of the package.
The path of a module is the name of its package (§2.6), then /, then its path under src/ or test/, without .i or .is, with / between the parts. In the package named app, the path of src/users/admin.i is app/users/admin. Every part of a path must be a name. Two modules of one package with the same path are a compile error.
Modules in test/ are checked when their package is checked (§2.6), and compiled as test entries when its tests are run. They may import modules in src/, but not the other way.
A test module may be imported only by test modules of the same package. Its exports alone do not make its bindings used (§2.4.1).
In a module, the first part of a module path says which package the module is in. It must be one of these:
i, for the library modules (§9, §10, §13, §14, §B.7.1) and kept paths named at the start of this document. How it gives them is not fixed: the operations that cannot be written in i may be built into the compiler, and they are not host imports (§10.1);- the
nameof the module’s own package; - a dependency name of the module’s own package (§2.6); in a test module, also a dev-dependency name.
A module may only import modules that are named in this way. Then the rest of the path is the path of the module in that package, as that package names it. So with the dependency json, json/decode is the module src/decode.i of that package.
Each package gives its own names to the packages it uses, so there is no space of package names that all packages share. All modules of the packages that a program uses, directly or through other packages, are compiled together. A type or a value is named by its package (§2.7), the path of its module in that package, and its name. So two definitions with the same name in different modules or packages are different, and two packages that give one dependency different names still use the same types.
If modules import one another in a circle, it is a compile error.
2.2 Form of a file
File = [ Imports ] [ Exports ] { Item } .
Imports = "(" ImportItem { "," ImportItem } [ "," ] ")" .
Exports = "(" ExportItem { "," ExportItem } [ "," ] ")" .
Item = { Attribute } ( TypeDef | Binding | Declaration | MacroItem ) .
Attribute = "@" [ name "." ] name [ Call ] .
MacroItem = [ name "." ] name Call . Call is the call of §6.1, so the arguments of an attribute and of a MacroItem are read as expressions. A ( on the same line right after the name of an attribute is always the start of the arguments of that attribute (§1.7). So when an attribute has no arguments and the item after it starts with (, as the binding (even, odd) = … does, the item begins on the next line:
@m
(even, odd) = ((n) { n == 0 || odd(n - 1) }, (n) { n != 0 && even(n - 1) }) On one line, @m (even, odd) = … is the attribute @m(even, odd) followed by = …, which is a compile error.
At the start of a file, a group in ( ) that is not followed by = is not an item:
- When an item in it has a module path (§2.3), which always has a
/, the group is the imports. - When not, it is the exports: every item in it is a
name, or anUpNamewith or without(..).
A file has at most one imports group and at most one exports group. They come first, the imports before the exports. Neither may be empty: a file with nothing to import or export leaves the group out. Attributes may not come before the imports or the exports: a group in ( ) with an attribute before it is an item. A group in ( ) that is followed by = is a binding with a data pattern, as in (even, odd) = …. Any other group in ( ) at the top level, with or without attributes before it, is a compile error.
An item that is a name, then : and a type, with no = after the type, is a declaration, whatever its attributes. Any other item that starts with an UpName is a type definition (§4.3). An item that is a call, m(args) or a.m(args), with no = or : after its ), is a MacroItem. Every other item is a binding (§2.5).
A MacroItem is a macro call in the position of an item (§14.5). When its name is not a macro, it is a compile error.
A declaration requires @external or @js (§10.1), or is the signature of a typed macro, with @macro in a .is file and a binding of the same name (§14.8). Any other declaration is a compile error, unless a macro before it replaces it.
In one module, every top-level name is defined one time only. Two bindings may not bind the same name, and two type definitions may not define the same type. Each labelled type defines one constructor with its name (§4.3). A constructor and a top-level binding may not have the same name either, because they are in one space of names (§2.3).
These are the attributes. An attribute whose name is one of them, with no module name before it, is that attribute. Its arguments must be string literals, given by position, with no labels,. Any other argument, such as @external("env" <> "", "f"), @external(module : "env", "f"), is a compile error. Any other attribute, such as @m, @m(args), or @a.m(args), is a macro before an item (§14.5); @macro marks a macro definition in a .is file (§14.5). An attribute in another place, or one attribute given two times on one item, is a compile error.
@js("expression"): on a declaration of a closed opaque type (§10.1.1). A declaration cannot have both@jsand@external.@external("module", "function"): on a declaration (§10.1). The module may not start withilang:, which is kept for the runtime (§B.4).@host("namespace")and@host("namespace", "async"): on a type definition (§9.7). The namespace may not start withilang:.@deprecated("note"): on any item. Using the marked item from another module gives a compile warning.
2.3 Imports
ImportItem = [ name "=" ] [ "(" ImportName { "," ImportName } [ "," ] ")" "=" ] ModulePath .
ImportName = name | UpName .
ModulePath = name "/" name { "/" name } . Header groups are not data. A colon does not introduce an import alias.
The imports and the exports are each a parenthesised group of items. In each group, the items have , or a break (§1.7) between them:
(
i/fail
decode = json/decode
opt = (Option, Some) = std/option
)
(
main
User(..)
) Every item gives a name to a module:
- With no
=, the name is the last part of the path. alias = pathgives the namealias.(A, b) = pathalso puts the exported namesAandbinto this module. They may then be used with no module name before them.- Both may be used together:
opt = (Option, Some) = std/option.
Importing an UpName puts in the type and the constructor with that name, if they are there. Only exported names may be imported. Two imports may not put in the same name. An imported name may not be the same as a top-level name of this module.
Module names, value names, and constructor names are in one space of names. So a module name may not be the same as a top-level name or another imported name of the module, and a local name may cover a module name. In a.b, when a is a value, .b reads a key (§6.7); when a is a module, a.b is a member of the module. Type names are in a different space, so a type and a value may have the same name.
2.4 Exports
ExportItem = name | UpName [ "(" ".." ")" ] . - A top-level name that is not in the exports may only be used in its module.
- For a labelled type
T(§4.3),Texports the type but not its constructor: other modules may use the type, but may not make its values, take them apart, read their keys, or spread them.T(..)exports the type and its constructor. - For a name
A(§4.3),Aexports the name.A(..)exports the name and every member of its expansion with(..); every member must be a labelled type defined in this module, or it is a compile error.(..)on a labelled type exports only that type and its constructor, also when its data is a union. - Every exported name must be defined in this module.
- If the type of an exported definition, or the expansion of an exported name, uses a type defined in this module, that type must be exported too. If not, it is a compile error. A type that a macro brings in from another module must be exported by that module.
A type bound by an alias pattern is a name with no constructors of its own; exporting it with (..) has the same effect as exporting it alone. An exported name or annotation that depends on a value is checked after its type is complete and must not expose a private labelled type. Its source value need not be exported. Reading from another module never fixes that source’s weak type; a new permanent read requires a wholly closed completed source, or the consumer uses a closed name exported by the source module (§4.7).
2.4.1 Unused top-level bindings
A top-level value name bound by a binding (§2.5) gives warning W0504 when it is not used. Names starting with _ do not give this warning (§1.3). The warning does not change initialization or make i check fail.
A name is used when it is referenced in its module, exported from a module outside test/, an entry of its package (§10.2), a test, or named in [wasm].exports. References inside any binding, including unused bindings, function bodies, patterns, and written types, count. Merely importing a name does not use it. In test modules, an export is used when referenced by a module of the same package. Each name in a binding is checked separately; using one name does not make its other names used. A name starting with _ suppresses its own warning.
This warning does not apply to type definitions, names, service definitions, constructors, or declarations (@external and @js). References in their written types count as uses. For an unused function in a test module, whose parameter group is () and whose result type is Nil, the warning adds: “did you forget to register this function with test?” The hint is not given outside test modules.
2.5 Top-level bindings
Binding = Pattern "=" Expr .
Declaration = name ":" Annot . Type parameters of a function are written on the function (§6.8) or on its written type (§4.7): id = <A>(x : A) -> A { x }, id : <A>(A) -> A = (x) { x }, and the declaration inspect : <A>(A) -> String. Declarations read Annot after : (§3), just like pattern annotations. A lowercase name followed by ( is not a pattern (§7.1), so an item id(A) = … is a compile error.
An item that is a call, m(args) or a.m(args), and has no = or : after its ), is not a binding. It is a macro call (§2.2, §14.5).
An item name : Annot with no = after the type is a declaration, whatever its attributes. A declaration requires @external or @js, or expansion by a macro (§2.2). A @js declaration has the closed opaque type and foreign link semantics of §10.1. It is initialized once during start only if retained by the target link pass; ordinary bindings retain this section’s initialization rules.
A top-level binding has the same rules as a binding in a block (§6.10). Only these things are different, and they come from its place:
-
Its pattern may not begin with an
UpName, because a top-level item that begins with anUpNameis a type definition (§2.2). -
Its names may be used anywhere in the module, before or after it. So top-level functions may use one another.
-
Its value is worked out once, when the program starts, before any entry point runs (§B.1). The order is below.
-
It may not have a need (§9.2), because nothing is there to provide it. So its pattern may not fail, because a pattern that may fail needs
Assert(§4.8 rule 10). -
If working out a value stops the program, no entry point runs.
Order. Working out a direct value (§6.10) runs no code, so it only reads the names outside the function bodies in it. Working out any other value may call every function it can get to, so it may read every name in it, also inside function bodies. From this:
-
The full dependencies of a binding are all the top-level names of this module in its value, also inside function bodies, and the full dependencies of their bindings.
-
The dependencies of a binding with a direct value are the top-level names of this module outside the function bodies in its value, and the dependencies of their bindings. The dependencies of any other binding are its full dependencies.
A binding may not be among its own dependencies. If it is, it is a compile error: x = [x] and x = f(), where the body of f uses x, are compile errors, but f = (n) { f(n - 1) } is not. The values are worked out one at a time, with a module after the modules it imports. In a module, the next value worked out is always the first binding, in the order of the source, whose dependencies are all worked out, as in Go. So in a = (1, z) y = p("y") z = p("z"), the order is y, z, a.
Type dependencies. A type may name a value (§3). This does not work out that value or add it to dependencies or full dependencies. It only orders type inference: a binding whose annotations use the type of a value of its module, directly or through aliases, is typed after that value’s group (§4.7). A type dependency within one group, including on itself, or a cycle between groups is a compile error. The message gives the chain of dependencies.
2.6 The package file
The file i.toml is in TOML. These keys and the provider keys below have a meaning; other root keys are not used. Unknown keys inside a provider plan are errors (§2.6.1):
name = "app" # required: the first part of the paths of this package's modules
[dependencies] # packages that this package uses
json = "github.com/alice/json@1.4.0" # a location, "@", and the lowest version needed
text = "codeberg.org/bob/tools//text@0.3.2" # a package in a directory of a repository
util = { path = "../util" } # a directory on this computer, relative to this file
[dev-dependencies] # packages that only the tests of this package use
check = "github.com/carol/check@2.1.0"
[wasm]
exports = ["app/math.add"] # host exports (§10.2): module path, ".", function name namemust be aname(§1.3), and noti. It is only used inside the package. A package may have anyname, and the packages that use it give it their own dependency names.- The key of a dependency is its dependency name. It must be a
name, noti, not thenameof this package, and not the same as another dependency name or dev-dependency name of this package. - A location is the address of a git repository, without
https://, for examplegithub.com/alice/json. The repository is got with git fromhttps://and the location. A location may end with//and a directory path, for a package that is in that directory of the repository. Any git host may be used; there is no central list of packages. - A version is
major.minor.patch, three decimal numbers, as in Semantic Versioning. { path = "…" }is a package in a directory on this computer. Only the package that is compiled, and the packages that it gets by path, may have path dependencies. A package that is got by location may not have them.- The packages that a package uses, directly or through other packages, may not be in a circle.
- The
dev-dependenciesof a package are only used when its own tests are compiled. Thedev-dependenciesof other packages are not used.
The optional [entries] table maps entry names (ordinary names) to
strings "module.path.function". The module path is relative to this package;
its components name directories below src or test, followed by the function. Targets must be top-level functions of this package;
they need not be exported. Each has the parameter group (). All declarations are checked even when unselected.
Names are unique as TOML keys; several names may select the same function.
main in src/main.i is the default entry, if present; [entries] does
not replace it. A package without that function has no default entry.
Entry declarations and main functions in dependencies have no effect.
i run and i build select the default entry when no entry is named.
i run NAME and i build NAME select a key of [entries]; an unknown key is an error listing the declared names. Program arguments to i run follow --.
Entry selection changes only the program entry, not module visibility or
host exports. The selected function uses the entry checking,
platform services and calling rules of §10.2. An unused-binding warning must
exclude both the default entry and every declared entry.
2.6.1 Target provider plans
A target provider plan maps resolved service type identities to ordinary i factory functions or host registrations. Infer source types first. For each entry, supply every service left in its inferred environment and all dependencies of the chosen factories. Supply whole services, never selected fields. Missing required services and dependency cycles are compile errors naming the target, service and requiring entry. Catching operation errors, including unsupported, does not remove a requirement. A target with no default for a standard capability and no explicit provider is therefore a compile error even when all its operation errors are caught.
Every factory has an ordinary checked type, returns its service and has no uncaught typed errors. Construction may stop, but cannot add an error to the source entry’s ABI error row. All source factories are type-checked; only the chosen factories’ needs are provisioned. A reference to an unchosen factory in executable source still follows §10.1.2. Construct each graph node once per entry invocation, after top-level initialization. Captured services do not change when another service is later substituted (§9.2).
This is the full version-1 schema; string cells are its nullary shorthand. A service key or factory value is a fully qualified module path, a dot and its type or binding name, resolved through the package graph. The package plan is data, not executable macro code. The root package selects target cells over the shipped plan; dependency packages cannot override its selections. A package may declare default cells for its own nominal service constructors; these retain that package’s authority, and root cells override them. A dependency row for another package’s service is an error. There is no wildcard or implicit service discovery. The shipped capability registry is plan data: a registered row without a cell for this target is unsupplied, including user-registered capabilities. An absent root cell inherits the shipped cell when one exists. A plan is resolved with its declaring package’s authority: it may name that package’s private bindings; references into dependencies must be exported. Inherited cells keep their original declaring authority. Copying a dependency’s private path into a root cell is an access error. This gives every package the same plan-only access, without making a private factory callable from ordinary outside source. It preserves private root-runner construction. The rule does not change source visibility (§2.4). Unknown keys, duplicate resolved rows/cells, unknown targets, ill-kinded type arguments and unresolved names are errors. The five columns are js, nodejs, native, wasi and host; ilang is a CLI alias only.
providers."<resolved service type constructor>":
role = "none" | "scheduler" | "runner" # default none
adapter = "<accessible ordinary function>" # iff role != none
discriminant = {js=C, nodejs=C, native=C, wasi=C, host=C} # optional
<target> = "<factory>" # compact form
| {factory="<factory>", arguments=[...], seed_required=false}
| {host=true}
provider_adapters."<accessible ordinary function>":
supplies = ["<closed service type>", ...]
requires = ["<closed service type>", ...]
wasm.exports = ["<exported entry>", ...]
wasm.synchronous = ["<member of wasm.exports>", ...] Exactly one variant occupies a target cell. arguments defaults to []; its
only elements are "seed" and "entry-root-key", each at most once, in the
factory’s argument order. A string is {factory=..., arguments=[]}.
seed_required defaults to false and is legal only with a seed argument.
A host cell is legal only for a declared host service; §9.7’s open
custom-host registration is its implicit cell. A registered ordinary row
never falls through to host registration. A discriminant row may omit cells;
its default cell constructs discriminant[current_target], provisioning
constructor fields as services. Each constructor must belong to that row’s
type, and each field type must be a closed valid service type. Constructor
aliases resolve before validation; maps must cover all five targets but need
not be injective. An explicit cell replaces generation and disables §10.1.2’s
certification for that row. Generated payloads are dependency edges too.
A row key is a nominal service constructor identity, not its spelling. A
parameterized row’s factory scheme is instantiated at each required set of
type arguments, by ordinary type checking; no holes or free variables may
remain in a chosen instance. Distinct Cell(Int)/Cell(String) nodes stay
distinct. Each factory invocation binds only its own checked environment in
its lexical scope. It does not union incompatible instantiations into one
environment; §9.2’s same-constructor restrictions still apply to each
actual function environment. This changes no source inference.
A factory for service S has type (generated arguments) <- R -> S,
with R its ordinary inferred need and no uncaught Fail requirement (§9.3).
Key(Nil) is an invariant identity key (§9.8). A request Root holds such a
key and identifies a request run in an entry context; an Owner holds its own
key and its Root and identifies a cancellation group. Both are ordinary
portable service values; Await supplies the function that waits for a request
to settle (§9.11). Scheduler and Runner are ordinary service values for a
structured task run and its scheduling and random-generator context (§9.9).
The primitive entry registry and any required initial root key exist before
factories run; ordinary factories construct the chosen services from their arguments
and dependencies. The request and task contracts are defined in §9.9 and
§9.11. This plan gives none of these services a host-reference representation.
The selected arguments have these exact types and meanings:
| Argument | Type | Value and dependency |
|---|---|---|
| seed | List(S) | [] outside the adapter; inside it, [s] when supplies contains S, otherwise []; a nonempty list depends on the adapter entering |
| entry-root-key | Key(Nil) | The initial registered root key of this entry’s primitive context, available before factories; no service dependency |
seed_required=true requires exactly [s] at this construction site, or the
plan is rejected; it is not a runtime missing-service fallback. A seed is an
ordinary value; the factory decides how to use it. It may return it, wrap it,
or ignore it. Even with a seed, the selected factory is called and its
result is the chosen S. A nullary override receives no seed and still replaces
the chosen value. This prevents an adapter from silently undoing an Await,
Root or user-service override. Supplying a seed is not a second provider row.
The root-key argument serializes the narrow initial-root bootstrap: open the primitive entry registry, register its initial key when this argument is present in the graph, then let an ordinary factory construct the nominal Root value. A graph needing no key registers no initial root. It is not a public unscoped root allocator. Any ordinary factory with this argument type can receive the key; no rule recognizes Root or its module. All uses in one entry receive the same initial key. The registry exists for synchronous fake-only entries too; only entries classified async by §10.2 have async admission. The primitive opening is fixed, dependency-free and constructs no replaceable service value. On every exit it retires the entry’s registered roots/groups, even after a stop.
The role belongs to the row. A root-package target cell replaces only the provider cell; the inherited role, adapter and discriminant are unchanged. Redeclaring inherited metadata identically is allowed; changing it is an error. New application rows may declare any role with the same rules. A row with a role must name a declared adapter, and a row with role none must not. An override must still type-check as a provider of S; supplying a pure fake does not erase its role. Request mode likewise remains the declared host mode. All row/adapter declarations are schema-checked in a target plan; only chosen factory dependencies need supplies. Source-only check does no plan validation.
An adapter declaration names an accessible ordinary generalized function with this checked type (schema notation, R and S are the declared service unions):
adapter(A,E,N): (body: () -> A ^ E ! N) -> A ^ E ! R | N - S It is generalized over arbitrary A, E and N; no extra result/error restriction or requirement is permitted. The declared closed supplies and requires lists each form a valid ordinary environment. They may overlap across lists: the required outer Runner and supplied inner Runner, for example, are different lexical values. A candidate’s inferred scheme must satisfy this entire type, not only the current entry instantiation. There is no rank-two field or new function kind: the plan names an ordinary top-level generalized function. The adapter supplies S by calling body under ordinary environment bindings; those bindings are captured as seeds by the generated continuation. It must not assume its seeds are the final services used by the source entry.
The continuation has entry lifetime. Generated guards allow exactly one
invocation, wholly within the adapter invocation’s dynamic extent. Re-entry
or a second invocation stops with provider: body already called; invocation
after adapter exit stops with provider: body is not active; returning normally
without a completed invocation stops with provider: body was not completed.
A stop may abandon the invocation; it runs no i finals and the primitive
context still retires. Suspension is not exit. Adapters may use ordinary
structured control and resource.ensure around body; they cannot detach it.
The guard is required for every adapter, not trusted library code. Ordinary
service values can escape normally; their later operations obey their own
lifetime rules. This is not a new linear type system.
Resolve and validate the graph as follows, before linking or optimization:
- Resolve body providers transitively from the entry’s full inferred needs.
Each service uses its one target cell. Include all factory requirements,
generated-constructor payloads and declared request modes, whether used at
runtime or not. Choose roles from these rows, not their factories’ names.
Expansion must be finite: on any dependency path within one scope,
revisiting a service constructor is rejected even at different type
arguments (
provider: recursive dependency). This conservative rule also rejects a finitely decreasing chain such as Box(Box(Config)) -> Box(Config) -> Config. Independent sibling instances remain legal. It prevents an expanding Cell(A) -> Cell(List(A)) plan from hanging the compiler, without adding type-level termination analysis. Completed nodes may be shared; sharing must not hide a repeated constructor on any graph path. - If a role is reached, include its adapter and resolve that adapter’s required services in an outer scope using the same target cells. Include all roles and requirements reached there too. Repeat until closed. No adapter means no outer scope. More than one resolved adapter identity is an error naming the entry and both originating service rows and their adapter names. Repeated references to the same adapter are shared. Adapter references must resolve to a top-level function binding, allowing only direct lexical binding aliases; arbitrary computed function values cannot be plan adapter names. There is no nested composition.
- Make one node per (outer/body scope, fully instantiated service). An edge to each required service stays in that scope. An adapter-enter node depends on all outer requires. Seed nodes depend on adapter-enter; a body factory with a seed argument depends on the appropriate seed if one is supplied. Outer factories always receive []; an unsatisfied required seed is an error. The primitive context/key is a leaf, never a service-provider dependency. Two scopes do not share provider instances, even if their factories match.
- Check all supplied/required types, missing cells and cycles over this one graph, including adapter and seed edges. A factory cannot obtain an inner seed while constructing an outer prerequisite. Such a requirement is a plan error, not permission for a temporary default service. Source factories and adapters have already undergone ordinary type checking.
- Execute a stable topological order. First materialize primitive arguments and host-registration leaves in canonical order; these run no i factory. Then, among ready service nodes, order lexicographically by canonical package/module/type path and normalized type arguments, then outer before body. Expand aliases and sort/deduplicate union members recursively when serializing closed type arguments; never sort by allocated TypeIds. Prefer all ready provider nodes to adapter-enter. Enter the adapter once its prerequisites are ready; finish the same order in its guarded continuation, then invoke the source entry with its chosen body services. No provider may execute before primitive admission is opened when async.
This ordering may construct a body service independent of seeds before adapter entry; it is then an ordinary lexical capture in the continuation. It constructs seed-dependent providers after entry. This is still one topological order, not Root/Owner/Await followed by another sort. No second provider is substituted to break a cycle. Classification includes both scopes and the adapter’s requirements; linkage includes the adapter and all chosen factory bodies. A generated wrapper reference is not a source reference in §10.1.2’s certification.
Shipped task rows use the same schema: scheduler/runner roles select one ordinary adapter with the structured run type and supplied Scheduler/Runner/Await/Owner/Root, requiring outer Runner. Seed-aware ordinary factories return the supplied values inside that adapter; outside it they construct the accepted default Runner through its private ordinary constructor and the direct context services. The plan names that constructor with its own package authority, never via a public root-runner API. The outer Runner factory has no required seed and no dependency cycle. Root’s direct factory takes entry-root-key; Owner’s direct factory needs Root. Seed-aware factories still declare their full ordinary environments on both paths; those requirements are provisioned even when a seed is present. The seeded default run remains FIFO, path [], seed 0, with no entropy draw. No special factory elision, role erasure or hidden context provider is permitted.
2.6.2 Synchronous exports and commands
wasm.synchronous is a list of host exports and must be a subset of wasm.exports. A listed export that is async (§10.2) is a compile error showing the service of its need that makes it async. Otherwise the caller uses the async entry protocol when the metadata says async, even if the first call returns status 0. No source type promises synchronous host calling, and no implicit blocking adapter changes that contract. WASI retains §B.7’s blocking ABI.
The targets are js, nodejs, native, wasi and host. --target ilang is an alias of --target host. Host artifacts keep Appendix B’s format. i run and i test check against the native platform and run in the compiler’s process.
| Command | Target | Checking and execution |
|---|---|---|
i run | native | Check the needs of the selected entry against the platform; run in process |
i test | native | Check the needs of the tests against the platform; run each test in a fresh instance |
i build | the selected target; default native | Check the needs of its entries against that platform; write its artifacts (below) |
i check | none | Parse, resolve and infer all source, including test bodies; check no need against a platform |
i check --target t | t, with ilang read as host | Check source and the needs of that target’s main, configured exports and eligible tests; write and run nothing |
WASI has no compiled test entries under §B.7: its check still checks test source, but builds no test entry. Targeted checking checks the needs of entries against the platform, synchronous contracts and §10.1.2’s foreign availability. Untargeted checking promises none of these: success does not promise that any target can build or run the program. Neither check form promises actual host grants, engine features, successful initializers, or runtime results. Malformed manifest syntax is still a package-load error. The target-independent source checker keeps the entry shape and interface checks, but does not reject an entry merely for needing a service. A package with only host exports uses host (or its ilang alias).
i build [NAME] [--target t] writes the artifacts of the selected target for the selected entry:
native, the default: one executable,build/<name>(below). Native builds requiremainor a named entry.wasi: the WASI preview 1 command modulebuild/<name>.wasm(§B.7).jsandnodejs:build/<name>.wasmwith its loadername.mjsandname.d.ts(§B.6.1).host, also writtenilang: the modulebuild/<name>.wasmof Appendix B, for an embedding host.
A plain i build writes no .wasm module.
Native packages. i build with the native target writes one executable file, build/<name>, where <name> is the package name or the entry NAME of i build NAME (--out chooses another path). It is the native runner — Wasmtime’s runtime without a compiler, the standard host services and the reactor of i run — followed by a payload: a manifest, the program’s ilang.* metadata and its code compiled ahead of time, sealed by a SHA-256 digest. The manifest records the contract the code was compiled for (Wasmtime version, target triple, CPU feature baseline, collector and engine settings), the digest of the module and its trusted binding plan; the runner loads nothing else, and refuses a missing, truncated or damaged payload and a payload whose contract is not its own. The executable runs the program entry with its arguments, environment and standard streams, and exits as i run does. On Linux the executable is statically linked and needs no shared library; on macOS it depends only on system libraries. Its code is compiled for the runner’s target triple (on Linux the musl triple of the building machine’s architecture); cross-target builds and signing are not specified. Building fails, with the namespace and label, for each required import the runner cannot supply, including a declaration it does not grant (§9.7); a service replaced by ordinary i code needs no import.
2.7 Versions
A version of a package with a location is a git tag vmajor.minor.patch in its repository, for example v1.4.0. The package is the files of the repository at that tag, or the files of the directory after //, with its i.toml at the top.
A package in a program is a location and a major version, or a directory for a path dependency. So github.com/alice/json@1.4.0 and github.com/alice/json@2.0.1 are two different packages, and their types are different. A program may use both, with two dependency names. Inside one major version, a newer version must be able to take the place of an older one. A published macro interface includes whether it is a syntax or typed macro, its argument-data shapes and explicit labels, and its compile-time contract. Without a stronger declared contract, a tool conservatively treats any change to the source bytes of its defining module or its transitive imported modules (including initializers) as a changed macro interface, even when its ordinary function type is unchanged. Removing or changing that contract requires a new major version; adding a macro requires a new minor version.
A program uses one version of each package. It is chosen by minimal version selection:
- Start with the dependencies of the package that is compiled, and, when its tests are compiled, its dev-dependencies. Each one is a package and a lowest version.
- For each package and version that is reached, read its
i.tomlat that version. Its dependencies are reached too, each with its lowest version. - For each package, the version used is the highest version that is reached.
So the versions used change only when an i.toml changes. A newer version that is put on a repository is not used until a package asks for it.
2.8 The lock file
The file i.lock is next to the i.toml of the package that is compiled. It has one line for every version of a package with a location whose manifest minimal version selection reads (§2.7), in either the configuration with tests or the configuration without tests, including versions superseded by a higher reached version, in the order of locations and then versions as UTF-8 bytes:
github.com/alice/json 1.4.0 9c3e1f0a5b7d2c4e8f6a1b3d5c7e9f0a2b4c6d8e sha256-6d2f… The parts are the location, the version, the git commit of the tag, and the hash of the package, with one space between them. The hash is sha256- and the lowercase hex SHA-256 of these bytes: for each file of the package, in the order of their paths as UTF-8 bytes: the path from the package directory, with / between the parts; a zero byte; the size of the file as a decimal number; a zero byte; the bytes of the file. The files of the package are the files in the git tree of the commit under the package directory, with their bytes as git keeps them, before any change that a checkout makes to line ends. A symbolic link is a file whose bytes are its target. For a package got by location, i.toml, src, test, and every path below src or test must not be symbolic links. Source loading does not follow such links, even when their targets are inside the package. A tool cannot publish a package whose committed manifest or source paths contain such a link. Submodules are not part of the package.
- When the files that are got for a package do not have the commit and the hash in
i.lock, it is an error, and nothing is compiled. So a tag that is moved, or files that are changed on the way, are found. - When
i.lockhas no line for a reached version, the line is added. Lines for versions whose manifests are no longer read in either configuration are taken out. Raising a selected version does not remove a lower version that is still reached. Compilation still uses only the selected version of each package and major version (§2.7). - A tool may get the files from the repository or from any copy of it, because the hash makes certain they are the same files. Each use verifies the actual file set and bytes against the committed package and the lock. A stored hash alone does not verify a copy. Compilation consumes the verified bytes, without reopening mutable paths.