One clause, two worlds.
The landing page shows a compiler being strict. This shows something else: a signature becoming a fact about the compiled artifact, which a host reads and acts on before any code runs.
Two functions
Identical, except for one line. They are
demo/read_only.deed and
demo/read_write.deed in the compiler's repository, with
the comment above each one left off here; the playground links at the
bottom carry the whole files.
read_only
module demo/read_only
fn process(files: Dir, name: String) -> Result<String, String>
uses
Io.read,
{
Io.read(files, name)
}
read_write
module demo/read_write
fn process(files: Dir, name: String) -> Result<String, String>
uses
Io.read,
Io.save,
{
match Io.read(files, name) {
err(why) => err(why),
ok(text) => match Io.save(files, name, text) {
err(why) => err(why),
ok(_) => ok(text),
},
}
}
Two worlds
Compiled to WebAssembly components, the import section of each module is its world. It lists what the module asks a host for, and it is readable before a single instruction runs.
read_only.wasm imports
deed:io read
read_write.wasm imports
deed:io read
deed:io save
One extra clause in the signature. One extra line in the import section. Nobody wrote that line: it is what the effect row compiled to. There is no manifest to keep in step with the code, because the code is the manifest.
What the host does with it
A host that offers Io.read and not Io.save
runs the first module and refuses the second. The refusal is not a
permission check inside the program, and there is nothing to switch
off: the module asked for an operation the host does not have, and
the host saw that in the import list.
There is no command to type for this, because a component has no
main to enter through. Running one is a host's job, so
the demonstration is a test:
// crates/deed-driver/tests/demo.rs
let stopped = call(&module, "process", &[Value::I64(0), Value::I64(0)])
.expect_err("read_write needs a host to provide save");
let Trap::NeedsAHost(what) = stopped else {
panic!("it should say what it wanted, not {stopped}");
};
NeedsAHost carries the name of the operation the module
asked for, and it is reached before any instruction of
process executes.
None of this is checked by eye
The import sections, the host's refusal and the listing printed further up this page are all pinned in the compiler's repository, and they run on every commit:
$ cargo test -p deed-driver --test demo
running 5 tests
test read_only_imports_read_and_nothing_else ... ok
test running_read_only_without_a_host_names_read ... ok
test read_write_imports_read_and_save ... ok
test running_read_write_without_save_names_what_it_wanted ... ok
test the_readme_lists_the_imports_the_modules_actually_have ... ok
The last one exists because the listing on this page and in demo/README.md was typed, and a new effect in either signature would change the import section with nothing to notice the prose no longer matching.
Try them
Both programs open in the playground, running the same compiler this
page is describing. Add Io.save to the first one's
uses clause, or take it out of the second one's, and
watch what the compiler says.