Generics
Reed has no type parameters, and structurally cannot get them: a compilation unit becomes
one WebAssembly GC module with nominal, source-derived type names and no mangling. What it
has instead is a comptime func: a compile-time function whose parameters are kinded
(a type, a name, a number) and whose body is a template of declarations. Instantiating it
writes those declarations into your module, as if you had typed them.
comptime func box_of(T: type, Name: ident, prefix: ident) {
pub struct $Name { value: mut $T }
pub func [<new_ $prefix>](v: $T) -> &$Name {
return new $Name { value: v };
}
pub func $Name.get(self: &$Name) -> $T { return self->value; }
pub func $Name.set(self: &$Name, v: $T) { self->value = v; }
}
comptime box_of(i32, IntBox, int_box);
comptime box_of(f64, FloatBox, float_box);
func check() -> i32 {
let i: &IntBox = new_int_box(20);
i.set(i.get() + 2);
let f: &FloatBox = new_float_box(0.5);
if (i.get() != 22) { return 1; }
if (f.get() != 0.5) { return 2; }
return 0;
}
export func check as "check";
IntBox and FloatBox are two genuinely different WebAssembly GC types, with unboxed
fields and static dispatch. This is monomorphization, done in the source language rather
than by a type checker.
Defining a generator
comptime func name(param: kind, ...) { declaration* }
Each parameter names what its argument must be:
| Kind | The argument must be | Used in the body as |
|---|---|---|
type | a complete type: i32, &Point, ?any | $T, in any type position |
ident | exactly one identifier | $Name, and inside [<...>] to build new names |
i32, i64 | a compile-time-known integer | $N, and by bare name in comptime if/comptime for |
bool | true/false, or a comptime expression | as above |
string | a string literal | str!(Name), [<...>] (never at runtime) |
The body is a template. $T and $Name substitute an argument's tokens, [<a $b>] pastes
fragments into one new identifier, and str!(x) builds a string literal — the same three
forms comptime for/comptime if use, because this is the same expansion
pass.
A generator's name must be snake_case, and, like a macro, it must be
defined before it is instantiated: expansion is a single forward pass.
Instantiating
comptime name(argument, ...);
Declaration position only — a generator produces declarations, so there is nothing an expression-position instantiation could mean.
The declarations it produces are ordinary declarations from that point on. They occupy the same flat namespace, obey the same naming rules, and are exported, referenced, and emitted exactly like ones you wrote out. Your editor's outline lists them; go-to-definition finds them.
Why two names, Name and prefix
Reed's naming convention is enforced, not cosmetic: a type must be PascalCase and a
function must be snake_case, and a PascalCase name may not contain an underscore at
all. So a generator cannot derive new_IntBox from IntBox — that is a resolution error,
not a style nit. Generators that declare both a type and functions therefore take one name
of each kind.
Why not a macro?
Macros can also generate declarations, and for token-shaped work they remain
the right tool — $(...) repetition, arbitrary tt splicing, and expression-position
expansion are all things a comptime func deliberately does not do. Three things a macro
cannot do are exactly what a container generator needs:
Arity is checked. A macro's expr fragment swallows commas, so a one-capture pattern
matches a two-argument call; a macro physically cannot reject a mis-arity invocation. A
generator names what it wanted:
comptime func 'box_of' takes 3 arguments (T: type, Name: ident, prefix: ident),
but 2 were supplied
A type argument is really parsed. A macro's $T:ty is matched by shape — it
accepts ?nonsense and the failure surfaces somewhere inside the expansion. A type
parameter runs the argument through the real type grammar, at the call:
comptime func 'box_of' parameter 'T' expects a type, but '?' is not one
Diagnostics name parameters. A failed macro invocation reports why each arm did not match. A failed instantiation reports which parameter rejected which argument.
Composition
A generator body may contain anything a module can, including comptime for, comptime if, a macro invocation, or another instantiation. A generator forwarding its own
parameters to another is the ordinary way to build a bigger one out of smaller pieces:
comptime func slot_of(T: type, Name: ident, prefix: ident) {
pub struct $Name { value: mut $T }
pub func [<new_ $prefix>](v: $T) -> &$Name { return new $Name { value: v }; }
pub func $Name.get(self: &$Name) -> $T { return self->value; }
}
comptime func two_slots_of(T: type, Base: ident, prefix: ident) {
comptime slot_of($T, [<$Base A>], [<$prefix _a>]);
comptime slot_of($T, [<$Base B>], [<$prefix _b>]);
}
comptime two_slots_of(i32, Reg, reg);
func check() -> i32 {
let a: &RegA = new_reg_a(30);
let b: &RegB = new_reg_b(12);
return a.get() - b.get() - 18;
}
export func check as "check";
A value parameter is readable by bare name in a nested comptime if/comptime for, so a
generator can vary what it declares by its arguments, not just what types they use:
comptime func counters(N: i32, Base: ident, prefix: ident) {
comptime for i in 0..N {
global [<$Base $i>]: mut i32 = $i;
pub func [<$prefix _read $i>]() -> i32 { return [<$Base $i>]; }
}
comptime if (N > 2) {
pub func [<$prefix _is_wide>]() -> i32 { return 1; }
} else {
pub func [<$prefix _is_wide>]() -> i32 { return 0; }
}
}
comptime counters(3, Slot, slot);
func check() -> i32 {
return slot_read0() + slot_read1() + slot_read2() + slot_is_wide() - 4;
}
export func check as "check";
Instantiating the same thing twice is fine
Requesting an instantiation that already happened, with the same arguments, does nothing — it is not a duplicate-declaration error. This matters more than it looks: a compilation unit is merged into one module, so a library that instantiates a container on your behalf and your own instantiation of the same one would otherwise collide, with neither side able to see the other.
Two instantiations with different arguments that declare the same name do still collide, as they must — they really are two different declarations of one name.
A generator's extra declarations are not your problem
A generator typically declares more than any one caller uses. Those extras are not reported as unused, and the ones nothing reaches are not emitted into your module. You cannot edit someone else's generator to silence a warning, so the compiler does not raise one.
The standard library's generic containers
std.vec is this feature applied to the obvious case:
use std.vec.*;
comptime vec_of(i32, IntVec, int_vec);
comptime vec_search_of(i32, IntVec);
func check() -> i32 {
let v: &IntVec = new_int_vec();
for i in 0..25 { v.push(i * 2); }
if (v.len() != 25) { return 1; }
if (v.at(3) != 6) { return 2; }
if (v.index_of(8) != 4) { return 3; }
v.reverse();
if (v.at(0) != 48) { return 4; }
return 0;
}
export func check as "check";
Limits
- A
vec_ofelement type must be defaultable — a numeric type, or a nullable?Foo. Growth allocates a default-initialized array, and a non-null&Foohas no default value. A container of structs iscomptime vec_of(?Point, PointVec, point_vec);, andatreturns?Point, which you narrow withis. - A generator cannot recurse, directly or through a chain of others, and a module is limited to 1,000 instantiations.
- A
///comment inside a generator body does not reach the generated declaration. Comments are trivia the expansion pass does not carry. Document the generator itself; its doc comment shows on hover over both the definition and every instantiation. - Renaming a generator is refused by the language server. The declarations it produced are indistinguishable from hand-written ones by the time renaming could run, so a rename would be unable to tell what it should touch.