Skip to main content

Modules, imports, and exports

One Reed compilation unit compiles to exactly one WebAssembly module. A unit can span several files: use util.{add}; pulls a name in from another Reed source file, and everything imported is compiled into the same single WebAssembly module. There is still no linking step and no separate compilation — WebAssembly's own unit of code is the module, and cross-module wiring is done by the host at instantiation time, not by the compiler. See splitting a program across files.

Every unit also gets Reed's default core library. It is not named with an import, but its methods are available like ordinary declarations. Today that primarily means numeric postfix methods such as i32.clz, i64.div_u, f64.sqrt, and i64.mul_wide_u. Because the core library shares the same flat function namespace, redeclaring one of those exact qualified names is a duplicate declaration.

Everything below is a declaration. A module is a flat, unordered bag of them.

Splitting a program across files

A program can span several files. Every file in the unit is compiled together into one WebAssembly module.

// util.reed
pub struct Point {
  x: i32,
  y: i32,
}

pub func add(a: i32, b: i32) -> i32 {
  return a + b;
}

pub func distance_sq(p: &Point) -> i32 {
  return square(p->x) + square(p->y);
}

// Not `pub`: callable inside util.reed, invisible to every other file.
func square(v: i32) -> i32 {
  return v * v;
}
// main.reed
use util.{add, distance_sq, Point};

func check() -> i32 {
  let p: &Point = new Point { x: 20, y: 22 };
  if (distance_sq(p) != 884) {
    return 1;
  }
  return add(p->x, p->y);
}

export func check as "check";

check() returns 42. reedc build main.reed compiles both files and emits one .wat. There is nothing to list, configure, or link: the imports are the build graph.

Import forms

use util.{add};                // one name
use util.{add, Point};         // several
use util.*;                    // every pub name
use util.{add as plus};        // under a different name here
use net.http.{get};            // the file net/http.reed

A path is a .-separated run of segments with no quotes and no .reed extension. The last segment is the file and the earlier ones are directories, so net.http is net/http.reed. It is relative to the importing file's directory, not to wherever you ran reedc, so a use means the same thing regardless of the working directory.

There is no brace-less single-name form: use util.add; is rejected, because it cannot be told apart from a path naming one more directory level. Write use util.{add};.

A path segment may be a keyword — use std.array.*; is a real standard library import and array is reserved. A segment names a file, not a declaration, so nothing is ambiguous. An item, which does name a declaration, must be an ordinary identifier.

use and import are different things

KeywordMeaning
usea Reed file import: use util.{add};
importa WebAssembly host import: import "env"."log" as func log(i32) -> ();

They do entirely different things, which is why they are different keywords: a host import is a hole the host fills at instantiation time, while a use is resolved by the compiler and disappears into the merged module. Writing import "util.reed".add; is an error naming the replacement.

Aliasing with as

use util.{add as plus}; makes the declaration available as plus in this file only. It is not a rename: the declaration keeps its own name, the emitted WAT still says $add, and another file may import it as add at the same time.

An alias does not shadow a local. Inside a function body, a name that resolves as a local or parameter still does:

use util.{add as helper};

export func check as "check";

func check() -> i32 {
  let helper: i32 = 5;
  // The call reaches the function; the bare read reaches the local. 5 * 10 + 5.
  return helper(2, 3) * 10 + helper;
}

pub is not export

These are independent, and mixing them up is the easiest mistake to make here:

  • pub makes a declaration visible to other Reed files. It is compile-time only and emits nothing.
  • export makes it visible to the WebAssembly host, and is the only thing that produces a WebAssembly export.
plainexported
plainprivate to its filecallable by the host, not importable by Reed files
pubimportable by Reed files, absent from the wasm exportsboth

If you know Rust, pub is pub and export is #[no_mangle] pub extern. A pub func you never export is a normal internal function that other files may call; it does not widen your module's public ABI by one byte.

One thing pub does affect beyond visibility: it counts as a use for unused-decl. A file whose whole purpose is to expose an API that nothing in its own unit calls — a library root — would otherwise report every one of its declarations as unused, which is a warning with no available action, since deleting the declaration is the opposite of what the author wants. Marking something pub is already the statement that it is the intended surface. A private declaration nothing references is still reported.

// Callable from another Reed file. NOT in the emitted module's export section.
pub func helper(v: i32) -> i32 {
  return v + 1;
}

// Both.
pub func api(v: i32) -> i32 {
  return helper(v);
}

export func api as "api";

pub works on things export cannot touch

export only accepts the five WebAssembly entities (functions, globals, memories, tables, tags). pub additionally accepts struct, array, type, param, and macro — everything that exists only at compile time. That is the point: before pub, a macro could not be shared at all.

// lib.reed
pub macro square {
  ($x:expr) => { ($x) * ($x) };
}

pub param Scale: i32 = 10;
// main.reed
use lib.*;

func check() -> i32 {
  let n: i32 = square!(2 + 3);   // 25
  return n - Scale - 15;         // 0
}

export func check as "check";

An imported macro behaves exactly as if it were written in the importing file, argument grouping included. Nothing about the macro or the param survives into the compiled module.

Rules worth knowing

  • Names must be unique across the whole unit, not just per file. Two files cannot both define add. Reed emits one module with source-derived WAT names ((func $add ...)) and deliberately does not mangle them, because unreadable output would defeat the point of emitting readable WAT. A collision names both files.
  • Importing a non-pub name is an error, and a different one from importing a name that does not exist — the fixes differ, so the messages do too.
  • Imports are not transitive. Importing a file gives you what that file declares pub, never what it imported for its own use. See re-exporting below.
  • Import cycles are allowed. A file is parsed once however many files reach it, and merging is order-independent, so two files may import each other -- which is what makes mutually recursive functions across files possible. The one exception is macros: expansion is a single forward pass, so a macro declared in one file of a cycle cannot be used in another, and the error says so rather than telling you to move a definition (inside a cycle, no placement works). Visibility is not relaxed inside a cycle: pub and the non-transitivity rule apply exactly as elsewhere.
  • A file is compiled once however many files import it, so a diamond is fine and both branches agree on an imported type's identity.
  • A std: path is not a file. use std.math.*; names a standard library module built into the compiler. It follows every rule above — one flat namespace, pub-only visibility, merged once however many files import it — but it is never looked up on disk, which is why it works identically in the CLI, an editor, and the browser playground.

The root file, and lib.reed

A compilation unit is rooted at one file, the one you hand to the compiler; every other file in the unit is there because that file imported it, directly or transitively. Nothing about a file's name affects how it is compiled.

There is one convention, and it belongs to the tooling rather than to the language: reedc build, reedc check, and reedc doc use lib.reed in the current directory when you name no file at all.

reedc build # same as: reedc build lib.reed

That is the whole of it — there is no manifest, no project file, and no directory layout the compiler expects. See tooling.

Declaration order never matters

Name resolution is order-independent for every declaration kind. Exports can precede the things they export, a start can precede its target, and functions can call each other in any direction without forward declarations.

export func check as "check";

start warm_up;

func check() -> i32 {
  if (Ready == 1) {
    return helper(Ready);
  }
  return 1;
}

func warm_up() {
  Ready = 1;
}

global Ready: mut i32 = 0;

func helper(flag: i32) -> i32 {
  return flag - 1;
}

The resolver collects every top-level declaration before checking any function body, so "used before declared" is not a category of error in Reed. The only ordering that survives into the output is WebAssembly's own section ordering, which the compiler handles.

Namespaces

A name must be unique within its namespace, and namespaces are mostly — but not entirely — per declaration kind:

NamespaceContainsDuplicate diagnostic
typesstructs, arrays, and function types, all togetherduplicate type name 'P'
functionsimported and defined functions, togetherduplicate function name 'f'
globalsimported and defined globalsduplicate global name 'G'
memoriesimported and defined memoriesduplicate memory name 'M'
tablesimported and defined tablesduplicate table name 'T'
datadata segmentsduplicate data name 'D'
elemelem segmentsduplicate elem name 'E'
tagsimported and defined tags, togetherduplicate tag name 'Oops'
localsparameters and locals, per function
labelsblock/loop/for labels, lexical

The two merged namespaces are the ones that surprise people. struct P { ... } and type P = func(); collide even though they are different declaration keywords, because both emit a WebAssembly type. An imported function and a defined function collide because WebAssembly gives both a slot in the same function index space — the fact that one has a body and the other doesn't is invisible at every call site.

Namespaces that are not merged genuinely don't interact. A struct named Config and a global named Config coexist, as do a global D and a data segment D. There is no "one name, one meaning" rule; the meaning comes from the syntactic position.

Naming case is part of resolution

KindRequired case
functions, parameters, localssnake_case
globals, structs, arrays, function typesPascalCase

This is enforced, not linted. func Check() fails with function name 'Check' must use snake_case, and global counter: i32 = 0; fails with global name 'counter' must use PascalCase. Both are resolution errors, so they stop the compile.

The reason is that the resolver uses leading-character case to pick a namespace. A bare snake_case identifier in value position resolves only as a local or parameter; a bare PascalCase identifier in value position resolves only as a global. That makes the two sets disjoint by construction:

global Count: mut i32 = 0;

func bump(count: i32) -> i32 {
  Count = Count + count;
  return Count;
}

func check() -> i32 {
  bump(2);
  bump(3);
  if (Count == 5) {
    return 0;
  }
  return 1;
}

export func check as "check";

Count and count are unrelated names in the same body, and no shadowing rule is needed to say which wins — a local can never shadow a global in Reed, because a local can never be spelled like one. The cost is that the convention is not negotiable: you cannot name a helper function parseHeader, and you cannot name a global retry_count.

Imports

type Handler = func(i32) -> i32;

import "env"."double" as func double(i32) -> i32;
import "env"."tick" as global Tick: i32;
import "env"."heap" as memory Heap(1 16);
import "env"."handlers" as table Handlers(4 ?Handler);

func apply(value: i32) -> i32 {
  return double(value) + Tick;
}

export func apply as "apply";
export memory Heap as "heap";
export table Handlers as "handlers";

The two strings are the WebAssembly module name and field name. They are arbitrary strings, not identifiers, and have nothing to do with the source name that follows as.

The module name may drop its quotes when it happens to be a plain identifier, which is the common case — the conventional host module is env, and quoting a bare word to say so is noise:

import env."double" as func double(i32) -> i32;    // same import as "env"."double"

The field name keeps its quotes, and that is what keeps the two import kinds apart: a string after the . means a host import, while a name, {, or * means a Reed file import. A file path is always quoted, so import util.helper; is an error naming the missing quotes rather than a second reading of the same line.

Import kindForm
functionimport "m"."f" as func name(i32, i64) -> i32;
globalimport "m"."f" as global Name: i32; or : mut i32
memoryimport "m"."f" as memory Name(min max);
tableimport "m"."f" as table Name(n ?FuncType);
tagimport "m"."f" as tag Name(i32, i64);

An imported function's parameter list carries types onlydouble(i32), never double(value: i32), which is a syntax error (expected a type, found identifier 'value'). There is no body to bind parameter names into, so a name there would be dead syntax. Defined functions give both, because each parameter name becomes a local.

Once imported, the name behaves exactly like a defined one. double(value) above is an ordinary direct call; nothing at the call site distinguishes it from a local function, and an imported function is a legal start target. Imported tables carry the same type restriction as declared ones: ?NamedFuncType, never ?func.

The compiler cannot check that the host actually provides these. A missing or mistyped import is an instantiation failure at runtime, which is why a module with imports compiles fine but can't be run standalone.

Circular imports

Two files may import each other. A file is parsed once however many files reach it, and merging is order-independent, so a cycle needs no special handling from you:

// parity.reed
use helpers.{is_odd};

pub func is_even(n: i32) -> i32 {
  if (n == 0) {
    return 1;
  }
  return is_odd(n - 1);
}

func check() -> i32 {
  return is_even(10) + is_odd(7);
}

export func check as "check";
// helpers.reed
use parity.{is_even};

pub func is_odd(n: i32) -> i32 {
  if (n == 0) {
    return 0;
  }
  return is_even(n - 1);
}

check() returns 2 -- is_even(10) is 1 and is_odd(7) is 1. Mutual recursion across files is the main reason to want cycles, and it needs no forward declarations because the merged module is a flat, unordered bag of declarations.

Two things a cycle does not change:

  • Visibility. A name imported across the edge that closes a cycle needs pub like any other, and the non-transitivity rule still applies. The check simply runs once the whole graph is loaded rather than at the import line.
  • Macro ordering. Macros are expanded in a single forward pass, so a macro must be declared earlier in the merged module than it is used. Inside a cycle there is no order satisfying both directions, so a macro declared in one file of a cycle cannot be used in another. The error says that specifically, rather than suggesting you move the definition -- inside a cycle no placement works, and the fix is to move the macro to a file outside the cycle.

Re-exporting with pub import

Visibility is per file. A name is visible where it is declared and in files that import it directly — importing a file does not pass on what that file imported. So a chain of three files does not reach through:

// deep.reed
pub func deep_add(a: i32, b: i32) -> i32 {
  return a + b;
}
// mid.reed
use deep.{deep_add};

pub func mid_double(n: i32) -> i32 {
  return deep_add(n, n);
}

A file importing mid.reed sees mid_double and nothing else. deep_add is in the compiled module — the whole unit becomes one WebAssembly module, and it is right there in the output — but it is not visible, and using it is an error naming the file that declares it.

This is worth being explicit about because the two facts sound contradictory: a unit is one flat namespace, and names are not universally reachable within it. The flat namespace is about the emitted module (which is why names must be unique unit-wide); visibility is a compile-time property of each source file.

When a file is meant to be a facade — the surface callers program against, assembled from other files — mark its import pub:

// facade.reed
pub use deep.{deep_add};

pub func facade_own() -> i32 {
  return 7;
}
// main.reed
use facade.{deep_add, facade_own};

func check() -> i32 {
  return deep_add(35, facade_own());
}

export func check as "check";

check() returns 42. At the use site, a re-exported name is indistinguishable from one the facade declared itself.

The glob form re-exports the imported file's whole pub surface:

pub use deep.*;

Three properties follow from "a pub import forwards exactly what it imports":

  • It cannot widen visibility. A declaration that is not pub where it is declared is not importable, so it is not re-exportable either. A glob re-export of a file exposes that file's pub names, not its private ones, and not the names it itself imported through a plain import.
  • Re-exports compose. A name can cross any number of files as long as every edge is a pub import. It stops at the first edge that is not.
  • pub on a WebAssembly import is still an error. That name comes from the host rather than from a file, so there is nothing to re-export.

Exports

type Callback = func() -> i32;

memory Scratch(1);
global Counter: mut i32 = 0;

func increment() -> i32 {
  Counter = Counter + 1;
  return Counter;
}

table Callbacks(1 ?Callback) = { &increment };

func check() -> i32 {
  if (increment() != 1) { return 1; }
  if (Counter != 1) { return 2; }
  return 0;
}

export func increment as "increment";
export func increment as "inc";
export global Counter as "counter";
export memory Scratch as "memory";
export table Callbacks as "callbacks";
export func check as "check";

Exports are kind-qualified. export func and export global look up in different namespaces, so the declaration is unambiguous without the compiler inferring anything. Getting the kind wrong is a resolution error against the wrong namespace, e.g. export refers to unknown Func 'Node'.

PropertyRule
Exportable kindsfunc, global, memory, table, tag
Not exportabletypes (structs, arrays, function types), data segments
One item, many namesallowed
Two items, one namerejected: duplicate WebAssembly export name 'x'
Re-exporting an importallowed
Mutable globalsexportable

Source names and ABI names are fully independent, and there is no implicit export: a function is private unless an export declaration names it. Types are not exportable because WebAssembly has no type export — a GC struct crossing a module boundary is matched structurally by the host, not by name.

Globals

struct Node { value: i32 }

global Limit: i32 = 1024;
global Ratio: f64 = 0.5;
global Origin: ?Node = null;
global Boxed: ?i31 = wasm.ref.i31(7);
global Used: mut i32 = 0;

func reserve(amount: i32) -> i32 {
  if (Used + amount > Limit) {
    return 0 - 1;
  }
  Used = Used + amount;
  return Used;
}

func check() -> i32 {
  if (reserve(1000) != 1000) { return 1; }
  if (reserve(100) != 0 - 1) { return 2; }
  if (Origin is &Node) { return 3; }
  if (Ratio != 0.5) { return 4; }
  if (Boxed is null) { return 5; }
  return 0;
}

export global Limit as "limit";
export global Used as "used";
export func check as "check";

A global's initializer must be a WebAssembly constant expression. Nothing runs between "the module is decoded" and "the global has its value" except the constant-expression evaluator, so the permitted forms are narrow:

Allowed in a global initializerRejected
a contextually typed numeric literala function call
null, in a nullable-reference contexta read of any defined (non-imported) global
a read of an imported immutable globala read of any mutable global
a read of a param's bare name (section 13)a local read
struct.new/struct.new_default/array.new/array.new_default/array.new_fixed, or the dedicated new Name { ... }/new Name[count]{} syntax lowering to one of themany as conversion other than 0 as &i31
0 as &i31 (lowers to ref.i31, same as wasm.ref.i31(0))
extended-const arithmetic: i32.add/i32.sub/i32.mul, i64.add/i64.sub/i64.mul
any other const-eligible wasm op: i32.const, i64.const, f32.const, f64.const, ref.null, ref.func, ref.i31, global.get

Each allocation and arithmetic form above recurses into its own arguments, which must themselves be constant.

The "defined global" row is the trap. Reading Limit — an immutable global declared right above — from another global's initializer fails with a global initializer may only read an imported immutable global. Only imported immutable globals qualify. The reason is that WebAssembly's constant-expression global.get is restricted to the imported global index range: imports are already initialized when the global section is evaluated, and defined globals may not be. Reed reports the restriction rather than working around it by constant-folding, so the emitted WAT stays a direct translation of the source.

Other diagnostics you will hit here: a global initializer cannot call a function and this expression is not a valid constant expression for a global initializer. Note that i32.add/i32.sub/i32.mul/i64.add/i64.sub/i64.mul in a global initializer compile to WebAssembly's extended-const proposal, not plain core constant expressions — see Limitations for what that implies for the runtime evaluating the module.

Mutable globals need mut in the type (global Used: mut i32 = 0;), and are perfectly ordinary to assign from any function body. The restrictions apply to the initializer only.

Memories

memory Scratch(1 4);

func check() -> i32 {
  wasm.i32.store<Scratch, offset=0, align=4>(0, 42);
  if (wasm.i32.load<Scratch, offset=0, align=4>(0) == 42) {
    return 0;
  }
  return 1;
}

export memory Scratch as "memory";
export func check as "check";
RuleDetail
Units64 KiB WebAssembly pages
Formmemory Name(min) — unbounded growth; memory Name(min max) — capped
Constraintsmax >= min, else memory maximum must not be smaller than its initial size
Upper boundmemory32 limits must not exceed 65536 pages (a lowering error)
Addressingi32 only; memory64 is out of the target feature set
Countany number of memories, including zero
Default memorynone — every operation names its target

Nothing in the surface language reads or writes memory. All access goes through the wasm escape hatch, where the memory name is the first immediate and offset/align are the rest. align is a byte count and must be a power of two (so align=4 for an i32, align=1 for a byte load) — it is passed straight through to the WAT, and it is a hint, so an under-aligned value is legal, just slower.

Because there is no default memory, multi-memory modules need no special syntax — every site already says which memory it means:

memory Scratch(1);
memory Cache(1 2);

func stash(address: i32, value: i32) {
  wasm.i32.store<Scratch, offset=0, align=4>(address, value);
  wasm.i32.store<Cache, offset=0, align=4>(address, value);
}

func check() -> i32 {
  stash(0, 7);
  if (wasm.i32.load<Scratch, offset=0, align=4>(0) != 7) { return 1; }
  if (wasm.i32.load<Cache, offset=0, align=4>(0) != 7) { return 2; }
  let scratch_pages: i32 = wasm.memory.size<Scratch>();
  if (scratch_pages != 1) { return 3; }
  return 0;
}

export memory Scratch as "scratch";
export memory Cache as "cache";
export func check as "check";

The let scratch_pages: i32 = ... line is not stylistic. Writing wasm.memory.size<Scratch>() != 1 directly fails with cannot determine the operand type for this operator; add a typed binding — comparison operators infer their operand type from context, and a raw wasm operation does not supply one. Binding to an annotated local is the workaround.

Tables and indirect calls

A table is a sequence of references, and in Reed its type is always a nullable reference to a named function type:

type Callback = func(i32) -> i32;

func twice(x: i32) -> i32 { return x * 2; }
func negate(x: i32) -> i32 { return 0 - x; }

table Callbacks(4 ?Callback) = { &twice, &negate };

func dispatch(slot: i32, value: i32) -> i32 {
  let handler: ?Callback = Callbacks[slot];
  if (handler is &Callback) {
    return handler(value);
  }
  return 0;
}

func check() -> i32 {
  if (#Callbacks != 4) { return 1; }
  if (dispatch(0, 21) != 42) { return 2; }
  if (dispatch(1, 5) != 0 - 5) { return 3; }
  if (dispatch(2, 5) != 0) { return 4; }
  Callbacks[2] = &twice;
  if (dispatch(2, 5) != 10) { return 5; }
  return 0;
}

export table Callbacks as "callbacks";
export func check as "check";

Sharp edges, all of which produce errors rather than surprises:

AttemptResult
table T(1 ?func)rejected: a table's declared type must be a nullable reference to a named function type ('?FunctionType')
table T(1 &Callback)same error — the type must be nullable
table T(1 ?C) = { f }syntax error; an entry is null or &name, never a bare name
&f where f's signature differsfunction 'f' does not exactly match the signature of function type 'C'
more initializer entries than the sizetable initializer has more entries than the declared initial size
calling Callbacks[0]() directlya call postfix is only valid on a function name or a non-null typed function reference, found ?C

Function-reference assignability is invariant: &f matches ?C only if f's parameters and results are identical to C's. No parameter contravariance, no result covariance. This mirrors WebAssembly's call_indirect/call_ref type identity check and is why two structurally identical function types with different names are still interchangeable, while a compatible-but-not-identical signature is not.

Table syntax reuses array syntax:

FormLowers toType
Callbacks[i]table.get?Callback
Callbacks[i] = &htable.set
#Callbackstable.sizei32

Out-of-bounds indexing traps, per WebAssembly's native behaviour. Note that slot 2 above returns null, not a trap — it is in bounds, just empty, which is why dispatch(2, 5) takes the fallback path instead of aborting.

The is &Callback narrowing is mandatory. A ?Callback cannot be called, and the compiler will not insert an implicit null check. Once narrowed, the call lowers to call_ref against the named type — no call_indirect, no table index laundering.

A table is not required for indirect dispatch. &function_name produces a typed function reference anywhere the context supplies a matching named function type, so function references pass as ordinary parameters:

type BinaryOp = func(i32, i32) -> i32;

func add(a: i32, b: i32) -> i32 { return a + b; }
func mul(a: i32, b: i32) -> i32 { return a * b; }

table Ops(2 ?BinaryOp) = { &add, &mul };

func fold(op: &BinaryOp, seed: i32, count: i32) -> i32 {
  var total: i32 = seed;
  for i in 1..count {
    total = op(total, i);
  }
  return total;
}

func fold_slot(slot: i32, seed: i32, count: i32) -> i32 {
  let op: ?BinaryOp = Ops[slot];
  if (op is &BinaryOp) {
    return fold(op, seed, count);
  }
  return 0 - 1;
}

func check() -> i32 {
  if (fold_slot(0, 0, 5) != 10) { return 1; }
  if (fold_slot(1, 1, 5) != 24) { return 2; }
  if (fold_slot(0, 0, 0) != 0) { return 3; }
  return 0;
}

export func check as "check";

fold takes a non-null &BinaryOp, so it needs no narrowing of its own — the null check happens once, at the boundary where the value leaves the table. The table exists here only to give the host an index-addressable entry point; the dispatch itself is a plain call_ref.

Tables also accept an optional maximum, table T(1 4 ?C), and wasm.table.grow is in the catalog.

Data segments

memory Heap(1);

data Header at 0 = "Reed";
data Greeting = "hello";

func byte_at(address: i32) -> i32 {
  return wasm.i32.load8_u<Heap, offset=0, align=1>(address);
}

func check() -> i32 {
  if (byte_at(0) != 87) { return 1; }
  if (byte_at(3) != 84) { return 2; }
  return 0;
}

export memory Heap as "memory";
export func check as "check";

The string literal is decoded to UTF-8 bytes. There are two forms:

  • data Name at offset = "..."; is active: the bytes are copied into memory at instantiation. It requires the module to have exactly one memory, otherwise you get active data segment 'Header' requires exactly one memory in the module, found 2 (or found 0). The restriction comes from the syntax having nowhere to name a memory. In a multi-memory module, write the bytes with raw store operations instead.
  • data Name = "..."; is passive: the bytes go in the module but are never copied anywhere automatically.

A passive segment is not a dead end. Mem.init<SomeData>(dest_offset, src_offset, len) copies bytes out of it into a named memory, and SomeData.drop() permanently marks it dropped; both are dedicated method-postfix syntax (spec section 10) that lower to memory.init/data.drop. Neither opcode has a wasm.<opcode>(...) spelling — the raw escape hatch is genuinely the one way you can't reach them — but that only means you write Heap.init<Greeting>(0, 0, 5) instead of a wasm.* call, not that the segment goes unused. See Segment init and drop for the full syntax and a compiling example (the mechanism is identical for a table/elem segment, via Table.init<...>(...)).

Data names live in their own namespace, so a data segment and a global may share a name.

Elem segments

type Callback = func() -> i32;

func answer() -> i32 {
  return 42;
}

elem Handlers: ?Callback = { &answer, null };

func check() -> i32 {
  return 0;
}

export func check as "check";

elem Name: Type = { ... }; declares a passive WebAssembly element segment — parallel to a data declaration, but always passive; there is no at offset form. Type must be a nullable reference to a named function type (?FunctionType), the same requirement a table declaration's own type carries, and each entry follows the identical grammar a table's own inline initializer uses: null or &function_name, where every &function_name must exactly match the declared function type's signature.

Elem names live in their own namespace, parallel to the data namespace, so an elem segment, a data segment, and a global may all share a name. See Segment init and drop for .init<...>(...)/.drop() usage on both data and elem segments, including a compiling example that consumes a declared elem this way.

Start functions

global Ready: mut i32 = 0;

func initialize() {
  Ready = 1;
}

start initialize;

func check() -> i32 {
  if (Ready == 1) {
    return 0;
  }
  return 1;
}

export func check as "check";

At most one start per module. The target must take no parameters and return (); anything else is start function must have no parameters and result '()'. It runs during instantiation, after globals are initialized and active data/element segments are copied, and before the host can call any export — which is what makes it the right place for the initialization work that a global initializer's constant-expression restriction forbids. An imported function is a valid target.

Tags

tag Name(types...); declares an exception tag — a named, typed signature for a throw. It shares one namespace with every other imported/defined tag (see Namespaces above), just like func/global/memory/table. Unlike those four, a tag has no case-convention requirement.

tag Overflow(i32);

export tag Overflow as "overflow";

func check(x: i32) -> i32 throws (Overflow) {
  if (x < 0) {
    throw Overflow(x);
  }
  return x;
}

export func check as "check";

Checked throwing, catching, and explicit propagation are covered in Exceptions.

An imported tag shares the same namespace as a locally-declared one — import "env"."oops" as tag Oops(i32); and a later tag Oops(i32); in the same module collide with a duplicate tag name 'Oops' resolution error, exactly like any other imported/defined pair:

import "env"."oops" as tag Oops(i32);
tag Oops(i32);
// resolution error: duplicate tag name 'Oops'

An imported tag can be thrown like any other:

import "env"."oops" as tag Oops(i32);

func f() throws (Oops) {
  throw Oops(1);
}

An exported tag is how an embedding host identifies which tag fired when it catches the resulting exception — see the opening example above, which exports Overflow. A tag can be both imported and exported in the same module (re-exporting a host-provided tag under a new name); there is no restriction against it.

A complete module

Everything above in one unit — a memory with active data, a start function that reads it, a table of function references, an indirect call, and four exports under names of their own:

type Reducer = func(i32, i32) -> i32;

memory Heap(1 4);
data Banner at 0 = "Reed";

global Seed: mut i32 = 0;

func sum(a: i32, b: i32) -> i32 {
  return a + b;
}

func larger(a: i32, b: i32) -> i32 {
  if (a > b) {
    return a;
  }
  return b;
}

table Reducers(4 ?Reducer) = { &sum, &larger };

func reduce(slot: i32, count: i32) -> i32 {
  let op: ?Reducer = Reducers[slot];
  if (op is &Reducer) {
    var acc: i32 = Seed;
    for i in 1..count {
      acc = op(acc, i);
    }
    return acc;
  }
  return 0 - 1;
}

func initialize() {
  Seed = wasm.i32.load8_u<Heap, offset=0, align=1>(0);
}

start initialize;

func check() -> i32 {
  if (Seed != 87) { return 1; }
  if (reduce(0, 5) != 97) { return 2; }
  if (reduce(1, 5) != 87) { return 3; }
  if (reduce(3, 5) != 0 - 1) { return 4; }
  return 0;
}

export memory Heap as "memory";
export table Reducers as "reducers";
export global Seed as "seed";
export func check as "check";
export func check as "_start_check";

Seed cannot be initialized from Banner directly — reading memory is not a constant expression — so initialize does it at instantiation, which is why check observes 87 ('W') rather than 0. Slot 3 of the table is null because the initializer listed only two of four entries. check is exported twice, which is legal; two different functions under one name would not be.