Wado

Compiler Attributes

An attribute #[...] applies to the item it precedes, and an inner attribute #![...] to the whole module. This chapter lists them in three groups: those any program writes, those valid anywhere that mostly the standard library and generated code write, and those valid only in the standard library.

User-Facing Attributes

These attributes are part of the language surface and can be used in any Wado source file.

#[inline] / #[inline(always)] / #[inline(never)]

Inlining hints for the optimizer. Applies to functions.

#[inline]              // hint: prefer inlining
fn small_helper() -> i32 { return 42; }

#[inline(always)]      // always inline (ignores threshold)
fn critical_path() -> i32 { return 1; }

#[inline(never)]       // never inline
fn error_handler() { panic("error"); }

test {
    assert small_helper() + critical_path() == 43;
}

#[benign(E, ...)]

Lets a function perform the listed effects without declaring with E, and stops them from propagating to callers. On a global, it lets the initializer perform them. It is meant for effects that no caller can observe through the function's interface, or through the global's value. Only the named effects are admitted. Any other effect a function performs propagates as usual, and in an initializer it is an error. The world import for each named effect is still required. The compiler cannot verify that they are unobservable, so this is an unchecked assertion that must be audited.

An argument names an effect the way a with clause does, by the name the function's module gives it, an import alias included. A name that reaches no effect there is an error. An effect of the same name declared in another module is a different effect, and stays required.

pub struct HashIndex {
    seed: u64, // private, and iteration order never reads it
}

impl HashIndex {
    #[benign(InsecureSeed)]
    pub fn new() -> HashIndex {
        let [seed, _] = InsecureSeed::get_insecure_seed(); // not required of callers
        return HashIndex { seed };
    }
}

fn fresh() -> HashIndex {   // declares no effect, and needs none
    return HashIndex::new();
}

On a global, the listed effects are admitted in its initializer:

use { println, Stdout } from "core:cli";

fn announce() -> i32 with Stdout {
    println("initialized");
    return 7;
}

#[benign(Stdout)]
global SEVEN: i32 = announce();

test {
    assert SEVEN == 7;
}

Rationale: WEP: Effect System and Randomness in Collections and WEP: HashMap.

#[ambient]

Exempts a function's body from effect checking. Ambient Functions states the rule.

#[secret]

Hides a struct field from debug/inspect output (the :? format specifier).

struct Foo {
    pub name: String,
    #[secret]
    password: String, // excluded from `${foo:?}` output
}

test {
    let foo = Foo { name: "ann", password: "hunter2" };
    assert `${foo:?}` == "Foo { name: \"ann\", .. }";
}

#[allow(...)]

Waives a lint on the item carrying it. As the module inner attribute #![allow(...)] it waives the lint for every item in the file. There is no #[deny(...)]. The lints are:

#[allow(dead_code)]
fn scaffolding() -> i32 {  // no "function `scaffolding` is never used"
    return 0;
}

fn used() -> i32 {         // reached, so never reported
    return 1;
}

test {
    assert used() == 1;
}

The dead_code Lint

The dead_code lint warns about a free function or a global that the program does not use. An item is used when one of these roots reaches it:

A trait's default body counts as reached only where a call lands on it, so a function that only an unreached default body calls is unused.

An item the roots do not reach is reported one of two ways:

An item in the standard library or in a #![generated] module is never reported.

Rationale: WEP: Unused Diagnostics.

#[param] / #[param(from_env = "...")] / #[param(name = "...")]

Marks a global as a compile-time build input. The type annotation gives the type, the initializer is the fallback, and read sites are ordinary global references.

Built with -D API_URL=https://example.com and PORT=80 in the environment:

#[param]
global API_URL: String = "http://localhost";   // -D API_URL=...

#[param(from_env = "PORT")]
global PORT: i32 = 8080;                        // read from an env var

#[param(name = "build.id")]
global BUILD_ID: String = "dev";                // -D build.id=...

test {
    assert API_URL == "https://example.com" && PORT == 80;
    assert BUILD_ID == "dev";                   // nothing set it
}

It takes two optional arguments:

Each is a non-empty string, and any other argument is an error. #[param] on a global mut is an error. The global may be pub.

The declared type must be a built-in scalar: String, char, bool, f32, f64, or an integer type from i8 to u128. Any other type is an error.

Resolution

Each parameter takes the first of these that supplies a value:

  1. -D NAME=value (alias --define) on the wado invocation.
  2. The from_env variable, where one is named and set.
  3. The tool's own default for the parameter, such as the log level wado test sets for core:log. The user did not write a tool default, so one that matches no parameter, or does not convert, is dropped silently.
  4. The initializer, type-checked as written.

A supplied value is trimmed of surrounding whitespace, then converted to the declared type with the spellings LenientFromStr accepts. So a String or char parameter cannot carry surrounding whitespace.

Parameter names share one namespace across the whole compilation, dependencies included. Same-named parameters in two packages answer to the same -D, so a library prefixes its names.

Resolution Failures

The invocation sets a level for each of three failures. error fails the build, warn reports and falls back to the initializer, and ignore falls back silently.

Failure Flag Default
A -D name that matches no #[param] --param-unknown error
A value that does not convert to the type --param-invalid error
No value supplied, so the initializer holds --param-missing ignore

Rationale: WEP: Compile-Time Parameters.

#[unavailable("reason")]

Declares a name that is deliberately not offered, on a declaration with no body. A call to it is an error that reports the reason. The reason is the only argument, and a removal writes its version into it. A missing or empty reason is an error.

impl File {
    #[unavailable("write `open_with(Options::default())` instead")]
    pub fn open(&self);

    #[unavailable("removed in 0.5.0; use `open_with`")]
    pub fn open_timeout(&self);
}

So f.open() is rejected with the reason:

let f = File { path: "a.txt" };
f.open();

The declaration reserves a name rather than a signature. Its parameters and return type are parsed but never resolved or type-checked, and may be left empty. self is the exception: it decides whether the instance name or the static name is reserved, since those are two names on one type.

A call that reaches the declaration is an error, and its arguments are never counted or checked against the parameters. A fault inside an argument is still reported. Naming the declaration without calling it is the same error. A declaration that nothing names is not an error.

The declaration takes part in name resolution and nothing else:

It goes on a module function, an impl method, or a trait method. Anywhere else is an error. export is refused with it, since there is no function to lower at the component boundary.

Rationale: WEP: Declared Absence.

#[expect_trap]

Test block attribute. Marks a test that passes only if its body traps. #[expect_trap] Attribute states the rule.

#[TODO]

Test block attribute. Marks a test for an unimplemented feature, reported apart from pass/fail. #[TODO] Attribute states the rule.

#![TODO]

Module-level inner attribute. Marks every test in the module as #[TODO], and tolerates a module that fails to compile. The source must still parse, since otherwise the attribute cannot be recognized. #![TODO] Modules states the outcomes.

#[timeout_ms(N)]

Test block attribute. Overrides the default test timeout. #[timeout_ms(N)] Attribute states the rule.

#[synopsis]

Test block attribute. The test runs like any other, and wado doc renders its body as the module's ## Synopsis section, a usage example that is compiled and so stays current.

#[synopsis]
test {
    let p = Point { x: 3, y: 4 };
    assert p.length() == 5.0;
}

A synopsis documents the module that holds it, not one item. wado test counts it in the ordinary pass/fail total, and it combines with #[expect_trap], #[TODO] and #[timeout_ms]. It is conventionally unnamed, since it is the example rather than one case among several.

wado doc places the ## Synopsis section after the module's //! doc and before its items. The code is the test body exactly as written between the outer braces, with its indentation removed, so nothing hidden sets it up. Each #[synopsis] test in the module is one code block, in source order. A module with none has no section. wado doc runs nothing.

Rationale: WEP: Synopsis Tests.

#[coverage(off)]

On a fn, an impl or a module (#![coverage(off)]), leaves that code out of what wado test --coverage measures. It is for code no test can reach, such as an entry point only another world calls. off is its only argument, since measuring is the default.

#[wire(...)]

Controls how a declaration is serialized and deserialized. The framework it customizes is in Serialization, and the library API in core:serde.

Rationale: WEP: Serialization and Deserialization.

Each declaration reads its own keys:

Declaration Keys
struct, enum, variant, flags, newtype name_policy
Struct field name, positional
Enum case, variant case name

A key the declaration does not read is an error, and so is #[wire] anywhere else, a flags member included. The keys may be split across several #[wire] attributes on one declaration, but each key is written once.

Wire Names

A name is written as in the source unless one of these keys changes it (see Serialization Names).

#[wire(name_policy = "camelCase")]
struct Event {
    created_at: String,     // "createdAt"
    #[wire(name = "type")]
    event_type: String,     // "type"
}

test {
    let e = Event { created_at: "now", event_type: "click" };
    assert to_string(&e).unwrap() == "{\"createdAt\":\"now\",\"type\":\"click\"}";
}

Positional Fields

#[wire(positional)] marks a struct field as ordinal: when deserializing, it is filled by position and never matched by name. A format that resolves fields by name, such as core:json, never fills it, so the field takes its default or is reported missing. A sequence-only format reads it in order like any other field. Serializing is unaffected. core:args fills positional fields from bare tokens (see Command-Line Arguments).

Optional Fields

No #[wire] key makes a field optional: a field default does. Missing, Repeated, and Unknown Fields states the rule, #[wire(default)] included.

Module and Boundary Attributes

These are valid in any module. Most programs never write them: the standard library and generated code do.

#![no_prelude]

Module-level inner attribute. Turns off the automatic import of the prelude. Used by low-level modules that define the prelude itself or that operate below the prelude layer.

#![no_prelude]
// This module does not import core:prelude

use { Option } from "core:prelude";

test {
    let x: Option<i32> = Option::Some(1);
    assert x matches { Some(1) };
}

#![generated]

Module-level inner attribute. Indicates that the module contains machine-generated code (e.g. from wado-from-idl or gale). It does not change how the module compiles. Tools read it; Kiln's use is in Authoring a generator.

The attribute may carry metadata, so a generator records where a file came from in the attribute rather than in a comment. Two argument shapes are accepted inside the parentheses:

Conventional keys are by (the tool that produced the file) and sources (the list of source paths it was generated from). Unknown keys are tolerated, so generators can introduce additional metadata without requiring a spec change.

#![generated]

#![generated(by = "wado-from-idl", sources = ["deps/random.wit"])]

#![generated(by = "wado-from-idl", sources = ["cli.wit", "clocks.wit"])]

fn roll() -> i32 { return 4; }

test {
    assert roll() == 4;   // the attribute does not change how it compiles
}

#![stdlib("path")]

Module-level inner attribute. Names the bundled stdlib module a file is, such as #![stdlib("core:cbor")]. The file is that module however it was loaded, so a file an editor opens directly is the same module an import reaches. It must name a module bundled in the compiler, and naming any other is an error.

#![wasm_module("mem")]
#![no_prelude]
#![stdlib("core:allocator")]
#![stdlib("core:bogus.wado")]

#[cm("namespace:pkg/interface@version")] / #[cm_params(...)]

Links Wado definitions (interfaces, worlds, resources, enums) to their Component Model names. See Attribute Syntax for Component Model Linking.

Standard Library Attributes

These attributes wire the standard library to Wasm and the Component Model. They are valid only in the standard library: a core:* or wasi:* module, or the declarations the compiler writes for an imported Wasm asset. Anywhere else each is an error.

#[canonical("mem", "realloc")]
fn realloc(oldptr: i32, oldsize: i32, align: i32, newsize: i32) -> i32;

#![wasm_module("name")]

Module-level inner attribute. All items in this module are compiled into a separate Wasm core module with the given name, which owns its own linear memory.

The Component Model requires a component to provide a linear memory and a realloc function for data crossing the boundary. core:allocator provides both as the core module "mem", the only wasm_module in the standard library. Its header is quoted under #![stdlib].

#[allocator("name")]

Marks a function in a wasm_module as the realloc implementation named name. The world selects which one the component uses: bump for CLI programs, freelist for HTTP services, and debug for the test world. debug never reuses freed memory and fills it with 0xFF.

#[allocator("bump")]
export fn bump_realloc(oldptr: i32, oldsize: i32, align: i32, newsize: i32) -> i32 {
    // Free: rewind if this was the most recent allocation
    if newsize == 0 {
        if (oldptr + oldsize) as u32 == frontier() {
            set_frontier(oldptr as u32);
        }
        return 0;
    }
    return bump(align as u32, newsize as u32) as i32;
}

#[export_name("name")]

Overrides the name a function is exported under from its core module.

#[canonical("namespace", "name")]

Declares that a bodyless function is imported rather than defined. Used in core:builtin to map intrinsic declarations to their imports.

Namespace Description
"wasi" CM canonical builtins (streams, futures, tasks)
"mem" Exports of the "mem" core module (realloc)
"wasm:<path>" Exports of an imported core-wasm asset (e.g. the bundled libm)
#[canonical("wasi", "stream-new")]
#[storage(none)]
#[side_effect(opaque)]
pub fn stream_new() -> i64;
#[canonical("mem", "realloc")]
#[storage(none)]
#[side_effect(opaque)]
pub fn realloc(oldptr: i32, oldsize: i32, align: i32, newsize: i32) -> i32;

#[compiler_item("name")]

Binds a stdlib declaration to the language item of that name, such as #[compiler_item("option")] on variant Option or #[compiler_item("display")] on the Display trait.

#[immediate(...)]

Names a parameter that becomes a Wasm immediate: the argument's literal value is encoded into the instruction itself.

#[immediate(value)]
#[storage(none)]
#[side_effect(none)]
pub fn v128_const(value: i128) -> v128;

It names one parameter, unquoted, and repeats for a second. Like #[storage(...)], it belongs to a declaration with no body, because a body is called rather than encoded as one instruction. A trait or interface method requirement is an error for the same reason: it reaches an impl, which is called. So is a declaration carrying #[cm(...)], which its adapter calls.

#[storage(...)]

States what a call to a declaration with no body shares with its arguments and what it keeps of them. Its body cannot be read for these facts, so the attribute is their only source. Reference Retention states what keeping is, and that no function type carries it.

#[storage(part_of_args)]
#[side_effect(trap, outside = [arr], at = [idx], unset = arr)]
pub fn array_get_ref<T>(arr: &Array<T>, idx: i32) -> &T;
#[storage(stores_args)]
#[side_effect(trap, outside = [dst, src], at = [dst_offset, src_offset], count = len)]
pub fn array_copy<T>(dst: &mut Array<T>, dst_offset: i32, src: &Array<T>, src_offset: i32, len: i32);

The first argument is one of seven values:

Value Meaning
none The call shares no storage and keeps none
fresh The result is new storage and holds nothing it was handed
part_of_args The result is an argument's storage, or part of it
holds_args The result is new storage that holds what the arguments hold
copies_args The result is new storage that holds copies of what the args hold
stores_args The call stores what the arguments hold into the &mut argument
opaque The optimizer assumes nothing about what the call shares or keeps

A holds_args result holds the very objects it was handed, so a write through one is seen through the other. A copies_args result holds copies made as a value copy makes them, so making it reads everything the arguments reach. A reference among them still names the object it named, so the result holds what the arguments hold, as a holds_args one does.

#[storage(copies_args)]
#[side_effect(none)]
pub fn array_clone<T>(src: &Array<T>) -> Array<T>;

The attribute names no parameter, since the types say which ones it means. Only a parameter whose type can carry storage counts, and a type parameter does, since some instantiation carries storage. A by-value parameter contributes itself: builtin::select hands back one of its two operands, so its part_of_args result may be part of either. A reference parameter contributes what it points to, so array_copy stores the elements of src. The destination of stores_args is the one &mut parameter.

len = p says the returned array holds p elements. It goes with fresh, holds_args and copies_args, the values whose result is new storage.

#[side_effect(...)]

States what a call to a declaration with no body does besides computing its result.

Identifier Meaning
none No effect beyond what the signature states
trap The call may trap
read The call reads linear memory
write The call writes linear memory
opaque The optimizer assumes nothing the signature does not state
hint The call computes nothing, but its position is what it means
black_box The optimizer may assume nothing about the operand or the result
suspend Other tasks may run before the call returns
#[storage(none)]
#[side_effect(read, trap)]
pub fn i32_load(addr: i32) -> i32;

The signature states facts of its own, so no identifier lists them:

A fact the attribute leaves out is a fact the call does not have: a call with no trap never traps. none, opaque and black_box each stand alone, except that opaque takes suspend.

The tasks that run while a call is suspended may read and write anything held elsewhere: a global, and whatever a global or another task can reach. A call without suspend, opaque included, reaches only what its arguments reach, linear memory and the host. none, hint and black_box return without suspending, so none of them takes suspend.

#[canonical("wasi", "waitable-set-wait")]
#[storage(none)]
#[side_effect(opaque, suspend)]
pub fn waitable_set_wait(ws: i32, outptr: i32) -> i32;

A hint call is never deleted, moved or merged on its own. It goes when the code that contains it goes, and it never keeps that code alive.

A black_box call is never deleted, moved or merged, and its result is never computed from its operand. A test or a benchmark uses builtin::black_box to keep the work it measures from being folded away.

Trap Conditions

trap alone means the call may trap at any time. Condition keys narrow it: with any of them, the conditions listed are the only ones under which the call traps.

Key Form Meaning
outside [a, …] Traps unless the range lies within array a
at [p, …] Paired with outside: the range starts at p
count p Every range in outside has p elements
unset a Traps if the element of a at its at is unset
negative p Traps if p is below zero
#[storage(fresh, len = len)]
#[side_effect(trap, negative = len)]
pub fn array_new<T>(len: i32) -> Array<T>;

outside and at are arrays of the same length, and the i-th entries pair up. Without at, every range starts at 0. Without count, every range has 1 element. A range lies within a only when its start and its count are both non-negative and they end at or before a's length. outside = [a] also says the call does not replace a, and a write through a then reaches only the elements of a in that range.

A call that states outside = [a], at = [i] without count and returns a value is an element accessor: it reaches the one element of a at i. It writes that element when it returns &mut, and reads it otherwise. An element is unset when array_new left a reference element empty; a primitive element always holds a value.

What the Attributes Assume

The attributes state what a call does when the host behaves as specified. Running out of memory is assumed never to happen, and no attribute states it. The same holds for any other failure of the host.

Imports

A function imported from a core .wasm / .wat asset exchanges only scalars, and the compiler sees none of its body. The declaration the compiler writes for it carries #[storage(none)] and #[side_effect(opaque)].

A Component Model import carries neither attribute. Its adapter lowers every argument to scalars and linear memory and lifts the result back, so the import call it makes shares no storage, is opaque and may suspend, the same for every import.

Errors

Each of these is an error, and a malformed attribute is never read as some other fact:

Rationale: WEP: Builtin Storage and Side-Effect Attributes.