Skip to main content

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:

  • square is there, even though no other file may call it. pub controls who may refer to a name, not whether it is compiled.
  • Nothing is exported but check. Every function in vec.reed is pub, and none of them appears in the export section, because pub is not export.
  • failed is nowhere. A macro is expanded and gone; the four failed! 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.