Splitting a program across files
A worked example of the module system: a small vector library in one file, used by a program in another, with a macro shared across the boundary. Three files, one WebAssembly module.
The library
vec.reed declares a point type and the operations on it. Everything meant for
other files is marked pub; the helper is not.
// vec.reed
pub struct Vec2 {
x: i32,
y: i32,
}
pub func vec(x: i32, y: i32) -> &Vec2 {
return new Vec2 { x: x, y: y };
}
pub func add(a: &Vec2, b: &Vec2) -> &Vec2 {
return vec(a->x + b->x, a->y + b->y);
}
pub func dot(a: &Vec2, b: &Vec2) -> i32 {
return a->x * b->x + a->y * b->y;
}
// No `pub`: callable inside vec.reed, invisible everywhere else. Marking it `pub`
// would not change the emitted module at all -- only who may call it.
func square(v: i32) -> i32 {
return v * v;
}
pub func length_sq(a: &Vec2) -> i32 {
return square(a->x) + square(a->y);
}
The shared macro
assert.reed holds a macro. This is the case export cannot serve at all: a
macro exists only at compile time, so before pub there was no way to share one.
// assert.reed
// Expands to an expression, not a statement. Reed admits macro invocations in
// declaration and expression position only (see the specification's open
// extensions), so a macro that expanded to an `if` would not parse at its call
// site.
pub macro failed {
($actual:expr, $expected:expr, $code:literal) => {
(($actual) != ($expected)) ? $code : 0
};
}
The program
main.reed imports from both. Note the two import styles: a name list from one
file, a glob from the other.
// main.reed
use vec.{Vec2, vec, add, dot, length_sq};
use assert.*;
func check() -> i32 {
let a: &Vec2 = vec(3, 4);
let b: &Vec2 = vec(1, 2);
let sum: &Vec2 = add(a, b);
// Each `failed!` yields the error code, or 0 when the check passes.
let bad: i32 = failed!(length_sq(a), 25, 1)
+ failed!(dot(a, b), 11, 2)
+ failed!(sum->x, 4, 3)
+ failed!(sum->y, 6, 4);
return bad;
}
export func check as "check";
Build it with the root file. The imports are the build graph; there is nothing else to list:
reedc build main.reed
check() returns 0.
What ends up in the module
One WebAssembly module, with names taken straight from the source:
(type $Vec2 (struct (field $x i32) (field $y i32)))
(func $vec ...)
(func $add ...)
(func $dot ...)
(func $square ...)
(func $length_sq ...)
(func $check ...)
(export "check" (func $check)))
(Imported files come first, so vec.reed's functions precede main.reed's.)
Three things are worth reading off that output:
squareis there, even though no other file may call it.pubcontrols who may refer to a name, not whether it is compiled.- Nothing is exported but
check. Every function invec.reedispub, and none of them appears in the export section, becausepubis notexport. failedis nowhere. A macro is expanded and gone; the fourfailed!invocations became ordinary comparisons inside$check.
Adding a fourth file
Every file imports what it uses. If shapes.reed needs vectors, it imports them
itself:
// shapes.reed
use vec.{Vec2, dot};
pub func is_orthogonal(a: &Vec2, b: &Vec2) -> i32 {
if (dot(a, b) == 0) {
return 1;
}
return 0;
}
The shared file is compiled once regardless of how many paths reach it, so Vec2
means the same type everywhere.
Imports are not transitive
main.reed importing shapes.reed does not give it Vec2 or dot. Those
are visible in shapes.reed because shapes.reed imported them, and nowhere
else:
// rejected.reed
// What main.reed must NOT do -- this does not compile.
use shapes.{is_orthogonal};
func check() -> i32 {
let v: &Vec2 = new Vec2 { x: 1, y: 0 }; // error: 'Vec2' is not visible here
return is_orthogonal(v, v);
}
The declaration really is in the compiled module -- the whole unit becomes one
WebAssembly module, and Vec2 is in it. Visibility is a compile-time property of
each file, so being present in the module is not the same as being usable.
Two fixes, and which one is right is a design decision rather than a detail. If
main.reed genuinely works with vectors, it should say so:
// main-fixed.reed
// A fragment: just main.reed's import lines, with the direct import added.
use shapes.{is_orthogonal};
use vec.{Vec2};
If instead shapes.reed is meant to be the surface a caller programs against --
a facade that happens to be built on vectors -- it re-exports them with
pub import:
// shapes-facade.reed
// A fragment: shapes.reed's import line, with `pub` added to re-export.
pub use vec.{Vec2, dot};
Now use shapes.{Vec2} works, and Vec2 arrives at main.reed
indistinguishably from a name shapes.reed declared itself. A pub import can
only pass on what it imports, which is by definition already pub in the file it
came from, so re-exporting never widens visibility -- it forwards it. Re-exports
compose across any number of files provided every edge is marked, and stop at the
first that is not.
The one rule that catches people: names are unique across the whole unit, not
per file. vec.reed and shapes.reed cannot both define dot. See
limitations for why renaming is not done for you.