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:
dead_code: an unused or test-only free function or global (see Thedead_codeLint).literal_cast: a cast that types a literal a suffix could type (see Theliteral_castLint).self_comparison: a comparison of an expression with itself (see Theself_comparisonLint).shadowed_name: a binder that takes a name already reaching a known symbol (see Theshadowed_nameLint).undecided_effects: a trait head that writes nowithclause (see The Trait Head).
#[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
puborexportitem. Aninternalitem is not a root, because nothing outside the package can reach it. - A function whose name a world export names.
- A method. A method is not itself reported, and a free function that only a method calls counts as used.
- A struct field default, an associated constant's value, and the default body
of an
interfaceorresourceoperation.
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:
- Reached from a
testblock: "only used by tests". Compiling for the test world omits this warning, since there those tests are what the item is for. - Reached from nothing: "never used".
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:
name = "..."is the name-Dsets. It defaults to the global's own name.from_env = "..."names an environment variable read at compile time. It is independent ofname.
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:
-D NAME=value(alias--define) on thewadoinvocation.- The
from_envvariable, where one is named and set. - The tool's own default for the parameter, such as the log level
wado testsets forcore:log. The user did not write a tool default, so one that matches no parameter, or does not convert, is dropped silently. - 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 never satisfies a trait requirement, so an
implwhose method is#[unavailable]has not implemented the trait's method. - On a trait it is not a requirement: no impl owes it. Every type implementing the trait answers to the name, through the type or through a bound.
- A reserved name answers only where no method of its kind (instance or static) does. Where another trait of the type has such a method, the call reaches that method. A method whose trait is not imported at the call site does not displace the reservation.
- Through a bound a call does not tell the kinds apart, so a reserved static beside an instance method of the same name is ambiguous, as two methods are.
wado docdoes not render it.
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 = "...")]sets the wire name of one field or case, exactly as written.#[wire(name_policy = "...")]on a type converts the source name of every field, case or member that has nonameof its own. On a newtype, which has no members, it applies to the type's own name. The policies are"camelCase","snake_case","SCREAMING_SNAKE_CASE","PascalCase","kebab-case"and"SCREAMING-KEBAB-CASE", and any other string is an error. A policy reads any source casing, so aPascalCasecase and asnake_casefield both convert.
#[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:
- Scalar
key = "value"pairs (e.g.by = "wado-from-idl"). - List
key = ["v1", "v2", ...]pairs whose values are a comma-separated list of string literals (e.g.sources = ["a.wit", "b.wit"]).
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
&mutparameter is written through, and the write may reach everything the referent reaches, as far as theArray<T>rule below and anoutsiderange (see Trap Conditions) allow. - A declaration returning
!never returns: every call ends in a trap. That is nottrap, which says a call that returns may instead trap. - An
Array<T>parameter, by value or by reference, is reached as the array: its length and its element slots. What its elements reach is not read or written, unless the declaration iscopies_args, which reads all of it, oropaquein either attribute.
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:
- A body-less declaration missing either attribute, unless it carries
#[cm(...)]or is#[unavailable], which is never called and so has no facts to state. - Either attribute on a function with a body, on a
traitorinterfacemethod requirement, or on a declaration carrying#[cm(...)]. - A second
#[storage]or#[side_effect]on one declaration. - A repeated key, or an unknown value, identifier or key.
none,opaqueorblack_boxbeside anything else in#[side_effect], but foropaquebesidesuspend.suspendbesidenone,hintorblack_box.- A condition key without
trap. atorcountwithoutoutside, oroutsideandatarrays of different lengths.- A name in a key that is not a parameter of the declaration.
unsetnaming an array that is not inoutside.outsideorunsetnaming a parameter that is not an array, orat,countornegativenaming one that is not an integer.lenin#[storage]beside a value other thanfresh,holds_argsorcopies_args, on a declaration that does not return an array, or naming a parameter that is not an integer.stores_argson a declaration without exactly one&mutparameter.fresh,part_of_args,holds_argsorcopies_argson a declaration that returns()or!.part_of_args,holds_args,copies_argsorstores_argson a declaration with no parameter that can carry storage. The&mutparameterstores_argsstores into does not count.noneon a declaration whose result can carry storage.
Rationale: WEP: Builtin Storage and Side-Effect Attributes.
