Wado Language Specification
Wado is a programming language targeting Wasm/WASI -- Wasm in plain sight.
Overview
| Item | Description |
|---|---|
| Name | Wado |
| Extension | .wado |
| Paradigm | Imperative, Effect System |
| Typing | Static, Strong, Inferred |
| Target | Wasm/WASI |
See also: Cheatsheet for quick syntax reference.
Design Philosophy
- Wasm only: Zero abstraction to Wasm
- Explicitness: Make intent explicit
- Colorless async: Eliminates async/await "color" problem via Wasm Stack Switching
- Effect System: Side effect tracking and control, swappable via Handlers
Lexical Structure
Whitespace
The lexer recognizes exactly four whitespace characters:
| Code Point | Name |
|---|---|
\u0020 |
Space |
\u000A |
LF |
\u000D |
CR |
\u0009 |
Tab |
The lexer skips whitespace between tokens. Other Unicode whitespace characters (e.g., \u00A0 non-breaking space) are not recognized as whitespace and will cause a lexer error if used outside strings.
Comments
// Line comment (extends to end of line)
/* Block comment */
/*
* Multi-line
* block comment
*/
Block comments do not nest.
TODO: the parser keeps comments in the AST.
Shebang
#!/usr/bin/env wado
export fn run() { ... }
#! at position 0 is a shebang and is ignored. #![ is an inner attribute, not a shebang.
Data Section
The __DATA__ marker separates source code from embedded data. Everything after __DATA__ on its own line is captured as raw text and is not parsed as Wado code.
use {println} from "core:cli";
fn run() with Stdout {
println("Hello!");
}
__DATA__
This is raw data that can be accessed via the compiler API.
It can contain any text, including JSON, YAML, or test expectations.
Syntax Rules:
__DATA__must appear at the start of a line (after any preceding newline)- The line must contain only
__DATA__followed by a newline (no trailing content on the same line) - Everything after the
__DATA__line becomes the data section - The data section is optional; most modules won't have one
Accessing Data:
The data section is accessible via the compiler API through Module::data_section(), which returns Option<&str>. This enables tooling like test frameworks to embed expected results directly in source files.
// Compiler API example
let result = wado_compiler::compile_file(path)?;
if let Some(data) = result.module.data_section() {
// Process the data section content
}
The data section content can be accessed at runtime using the #data compile-time location literal. See Compile-Time Location Literals.
Identifiers
Identifiers match the pattern [a-zA-Z_][a-zA-Z0-9_]*:
foo
foo_bar
fooBar
FooBar
FOO_BAR
_private
name123
Identifiers are case-sensitive.
Contextual Keywords
The following keywords are contextual — they act as keywords only in specific syntactic positions and can be used as variable names, field names, and function parameters elsewhere:
| Keyword | Keyword context | Identifier elsewhere |
|---|---|---|
flags |
flags declaration |
Variable, field, parameter |
type |
type declaration |
Variable, field, parameter |
of |
for let <pattern> of <expr> |
Variable, field, parameter |
from |
use { ... } from "..." |
Variable, field, parameter, type name |
test |
test "name" { ... } block |
Variable, field, parameter |
// 'of' as a variable name
let of = 42;
println(`{of}`);
// 'of' as a struct field
struct Item { of: i32 }
let item = Item { of: 10 };
// 'of' as a for-of binding
let arr: List<i32> = [1, 2, 3];
for let of of arr {
println(`{of}`);
}
Statements and Expressions
expr;makes a statement. A semicolon is required for every statement including the last one.return expr;is necessary for a function to return a value.- Control flow statements do not need to be followed by a semicolon.
Variable Scoping
Variables are scoped to their enclosing block. Variables declared inside control flow bodies (if, while, for, loop) are not accessible outside.
for let mut i = 0; i < 10; i = i + 1 {
let x = i * 2;
}
// i and x are not in scope here
if true {
let y = 42;
}
// y is not in scope here
Shadowing in an inner block creates a new binding:
let x = 1;
if true {
let x = x + 1; // New binding, initialized from outer x
println(`{x}`); // 2
}
println(`{x}`); // 1 (outer x unchanged)
Same-scope shadowing is allowed when the new value is derived from the old one:
let x = 1;
let x = x + 1; // OK: RHS references x
let x = transform(x); // OK: RHS references x
Same-scope redeclaration without referencing the old value is not allowed:
let x = 1;
let x = 2; // Error: cannot redeclare 'x' in the same scope
let x = |x: i32| x + 1; // Error: the x inside is the closure parameter, not the outer variable
Local Item Definitions
struct and type (newtype) may be declared inside a function or method
body, scoped to that function:
fn area(width: i32, height: i32) -> i32 {
struct Size {
width: i32,
height: i32,
}
let s = Size { width, height };
return s.width * s.height;
}
Local items follow let's scoping rules, not a nested block's: a local item
declared anywhere in the function — including inside a nested if/while/
for — is visible for the rest of the function, but must be declared before
its first use (no hoisting, no forward references, including between two
local items). Two unrelated functions may declare same-named local items
without collision. A local item cannot be pub or internal: it is always
private to its enclosing function.
Local structs support their own generic parameters:
fn wrap<T>(value: T) -> i32 {
struct Box<T> {
value: T,
}
let b = Box { value };
return 0;
}
enum/variant/flags declarations, methods on a local type (a local
impl/trait block), and generic local type (newtype) are not yet
supported inside a function body. See
WEP: Local Item Definitions.
Global Variables
Global variables are module-level state that compile directly to WebAssembly globals. Unlike local variables (let), globals have module lifetime and are accessed via global.get/global.set instructions.
// Immutable global
global PI: f64 = 3.14159;
// Mutable global
global mut counter: i32 = 0;
// With visibility
pub global VERSION: i32 = 1;
Any type is supported. Any pure expression (no effects) can be used as an initializer.
Mutability:
Assignment is only allowed for global mut declarations:
global CONSTANT: i32 = 42;
global mut variable: i32 = 0;
fn example() {
variable = 10; // OK: mutable global
CONSTANT = 10; // Error: cannot assign to immutable global
}
Operators
Binary Operators
In order of precedence, lowest to highest:
| Precedence | Operators | Description | Associativity |
|---|---|---|---|
| 1 | =, +=, -=, *=, /=, etc |
Assignment | Right |
| 2 | \|\| |
Logical OR | Left |
| 3 | && |
Logical AND | Left |
| 4 | ==, !=, <, <=, >, >= |
Comparison | Restricted |
| 5 | \| |
Bitwise OR | Left |
| 6 | ^ |
Bitwise XOR | Left |
| 7 | & |
Bitwise AND | Left |
| 8 | <<, >> |
Bitwise shift | Left |
| 9 | +, - |
Additive | Left |
| 10 | *, /, % |
Multiplicative | Left |
Between assignment (1) and logical OR (2), the range operators sit at precedence level 1.5:
| Precedence | Operators | Description | Associativity |
|---|---|---|---|
| 1.5 | ..<, ..= |
Range | Non-associative |
..<creates a half-open range[start, end)—RangeExclusive<T>..=creates an inclusive range[start, end]—RangeInclusive<T>- Non-associative:
a..<b..<cis a compile error - Both operands must have the same type (after literal coercion)
See WEP: Range Object for the full design.
Design Note
Bitwise operators (&, |, ^) have higher precedence than comparison operators, fixing C's well-known design flaw. This means flags & MASK == EXPECTED correctly parses as (flags & MASK) == EXPECTED.
Unary Operators
| Operator | Description |
|---|---|
- |
Negation |
! |
Logical NOT |
~ |
Bitwise NOT |
& |
Reference |
&mut |
Mut ref |
* |
Dereference |
Postfix Operators
| Operator | Description |
|---|---|
. |
Field access |
[] |
Index access |
() |
Function call |
:: |
Namespace access |
matches { pattern } |
Pattern test |
as Type |
Type cast |
? |
Error propagation |
matches and ! binding
These tables group operators by form, not by binding strength. matches binds
looser than the binary operators, as, and the value-producing unary operators
(-, ~, &, &mut, *), but tighter than logical !:
!x matches { Some(_) }is!(x matches { Some(_) })— "xdoes not matchSome(_)".*x matches { "kw" },x as i32 matches { 0 },a + b matches { 10 }, andflags & MASK matches { 0 }need no parentheses. A comparison, range, or assignment scrutinee does:(a == b) matches { true }.
Prohibited Operators
Wado intentionally omits certain operators found in other languages:
- No
++/--: usex += 1andx -= 1instead. These operators cause undefined behavior in C/C++ and add unnecessary complexity. - No
**power operator: use thepow(x, y)function instead.**has counterintuitive precedence in languages that have it (e.g., Python's-1**2 = -1).
Type Cast (as)
The as operator performs explicit type conversion between primitive types:
let i = 42;
let f = i as f64; // i32 to f64
let truncated = 3.14 as i32; // f64 to i32 (truncates to 3)
// Chained casts
let x = 10 as f64 as i32 as f64;
// Cast in expressions
let result = (a as f64) + b;
Parentheses for Grouping
Parentheses () can be used to override operator precedence:
let a = 2 + 3 * 4; // 14 (multiplication first)
let b = (2 + 3) * 4; // 20 (addition first due to parentheses)
let c = 3 | 4 & 6; // 7 (& has higher precedence than |)
let d = (3 | 4) & 6; // 6 (| first due to parentheses)
Comparison Chaining
Wado supports mathematical comparison chaining similar to Python, allowing natural range expressions:
// Valid chains (same direction)
a < b < c // Equivalent to: a < b && b < c
a > b > c // Equivalent to: a > b && b > c
a <= b <= c // Equivalent to: a <= b && b <= c
a >= b >= c // Equivalent to: a >= b && b >= c
a == b == c // Equivalent to: a == b && b == c
0 <= x <= 100 // Natural range check
// Invalid chains (semantic error)
a < b > c // Error: mixed directions
a > b < c // Error: mixed directions
a < b >= c // Error: mixing < and >=
a == b < c // Error: mixing == and inequality
a != b != c // Error: != chaining not allowed
Chaining rules:
- Same-direction inequality:
</<=can only chain with</<=, and>/>=can only chain with>/>= - Equality chaining:
==can only chain with== - No
!=chaining:!=cannot be chained (the meaning ofa != b != cis ambiguous) - No mixing: cannot mix equality operators with inequality operators
See docs/wep-2026-01-11-operator-precedence.md for detailed rationale.
Control Flow
Conditional Statements
if condition {
// then block
} else {
// else block
}
// else-if chains
if x < 0 {
println("negative");
} else if x == 0 {
println("zero");
} else {
println("positive");
}
If Expression:
let abs = if x < 0 { -x } else { x };
let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" };
Trailing semicolons are optional in expression blocks (like trailing commas).
If Let Pattern Matching:
let opt: Option<i32> = Option::<i32>::Some(42);
if let Some(x) = opt {
println(`Got: {x}`);
} else {
println("None");
}
Match Ergonomics: When the scrutinee of if let, match, or matches is a reference type (&T or &mut T), patterns match against the underlying type. Payload bindings become references — e.g. matching &Option<T> with Some(x) gives x: &T, not x: T (Rust-compatible, RFC 2005).
let opt: Option<i32> = Option::<i32>::Some(42);
let ro = &opt;
if let Some(x) = ro { // ro: &Option<i32>, x: &i32
println(`Got: {*x}`); // dereference to use the value
}
While Loop
let mut i = 0;
while i < 10 {
println(`i = {i}`);
i = i + 1;
}
While Let Pattern Matching
while let allows iterating while a pattern matches:
let items: List<i32> = [1, 2, 3];
let mut iter = items.iter();
while let Some(x) = iter.next() {
println(`{x}`);
}
The loop continues as long as the pattern matches. When the pattern fails to match (e.g., iter.next() returns None), the loop exits.
For Loop
C-style for loop with initialization, condition, and update. Parentheses are optional:
for let mut i = 0; i < 10; i = i + 1 {
println(`i = {i}`);
}
// With parentheses (also valid)
for (let mut i = 0; i < 10; i = i + 1) {
println(`i = {i}`);
}
// All parts are optional
for ;; {
// infinite loop
}
Note: continue in a for loop executes the update expression before the next iteration, matching C semantics.
For with Pattern Condition
The condition part of a C-style for loop can use let pattern matching:
let items: List<i32> = [10, 20, 30];
let mut iter = items.iter();
for ; let Some(x) = iter.next(); {
println(`{x}`);
}
// With update expression
let mut count = 0;
for ; let Some(x) = iter.next(); count += 1 {
println(`item {count}: {x}`);
}
The loop continues as long as the pattern matches. This is useful for iterating with additional state (like a counter) alongside pattern matching.
For-Of Loop
For iterating over any type that implements IntoIterator:
let numbers: List<i32> = [1, 2, 3, 4, 5];
for let n of numbers {
println(`{n}`);
}
// With mutable binding
for let mut item of items {
item = item * 2; // Can modify the local binding
println(`{item}`);
}
// Custom types that implement IntoIterator also work
for let x of my_collection {
println(`{x}`);
}
For-of desugaring:
// Source
for let item of collection {
body(item);
}
// Desugars to
scope: {
let mut __iter = collection.into_iter();
loop {
if let Some(__item) = __iter.next() {
let item = __item;
body(item);
} else {
break;
}
}
}
Note: The binding is a copy of each element (value semantics), so modifying it does not affect the original collection. For-of works with any type implementing IntoIterator, not just arrays.
Tuple for-of (compile-time expansion):
When the iterable is a tuple, the loop body is expanded once per element at compile time. Each expansion independently types the binding, enabling heterogeneous iteration with per-element trait dispatch.
let t = [42, "hello", true];
for let v of t {
println(`{v}`); // expanded to three blocks, each with the correct type
}
break and continue are not allowed inside a tuple for-of body because the loop is unrolled at compile time into sequential blocks and these have no natural target. .enumerate() is supported and provides a compile-time index.
Infinite Loop
loop {
// runs forever until break
if should_exit() {
break;
}
}
Break and Continue
break exits the innermost enclosing loop. continue skips to the next iteration.
// break example
let mut i = 0;
while i < 100 {
if i == 10 {
break; // exit the loop
}
i = i + 1;
}
// continue example
for let mut i = 0; i < 10; i = i + 1 {
if i == 5 {
continue; // skip printing 5
}
println(`{i}`);
}
Both break and continue work with while, for, and loop.
Labeled Blocks
Labeled blocks create a new scope for variable bindings. The label is required to avoid syntactic ambiguity with struct literals.
let x = 10;
scope: {
let x = 20; // shadows outer x
println(`x = {x}`); // prints "x = 20"
}
println(`x = {x}`); // prints "x = 10" (outer x unchanged)
Syntax: LABEL: { ... }
- The label must be a valid identifier followed by a colon
- The block creates a new variable scope
- Variables declared inside are not accessible outside
- Shadowing is allowed within the block
Nested Blocks:
outer: {
let a = 1;
inner: {
let b = 2;
let sum = a + b; // a is visible from outer scope
println(`{sum}`);
}
// b is not visible here
println(`{a}`);
}
Design Rationale: The label is mandatory because { field: value } without context could be either a block with a labeled statement or a struct literal. Requiring the label removes this ambiguity.
Match Expression
Match expression provides exhaustive pattern matching on variants and other types.
// Match expression (produces a value)
let result = match opt {
Some(x) => x * 2,
None => 0,
};
// Match with custom variants
let area = match shape {
Circle(r) => 3.14159 * r * r,
Rectangle([w, h]) => w * h,
Point => 0.0,
};
// Match statement (no value produced)
match command {
Start => engine.start(),
Stop => engine.stop(),
}
Pattern Syntax:
| Pattern | Example | Description |
|---|---|---|
| Wildcard | _ |
Matches anything |
| Variable | x |
Binds matched value |
| Mut variable | mut x, Some(mut x) |
Binds as mutable |
| Literal | 0, "hello", true |
Matches exact value |
| Constant | MAX_LEN, i32::MAX |
Matches an immutable global / const by value |
| Variant | Some(x), None |
Matches variant case |
| Tuple | [a, b, c] |
Destructures tuple |
| Nested tuple | [10, Some(x)] |
Literal/variant sub-patterns in tuple |
| Struct | { x, y }, Point { x, y } |
Destructures struct |
| Nested struct | { x: 0, y } |
Literal/variant sub-patterns in struct |
| Or | Red \| Blue |
Matches either pattern |
| Guard | Some(x) && x > 0 |
Pattern with condition |
Exhaustiveness:
Match must cover all possible cases. Use _ wildcard for catch-all:
match color {
Red => "red",
Green => "green",
_ => "other", // Required for exhaustiveness
}
Guard Expressions:
Guards use && to reflect left-to-right evaluation (pattern first, then guard):
match customer {
Premium(years) && years > 5 => 0.3,
Premium(_) => 0.2,
_ => 0.1,
}
Constant Patterns:
A pattern identifier that resolves to an immutable global or an associated constant matches by value, instead of binding a new variable:
global TK_FOO: i32 = 1;
global TK_BAR: i32 = 2;
let kind = match token {
TK_FOO | TK_BAR => "keyword",
i32::MAX => "max",
_ => "other",
};
Or Patterns:
Or patterns match if any alternative matches. All alternatives must bind the same names with the same types:
// Enum or-patterns
match color {
Red | Blue => "cool",
Green => "warm",
}
// Variant or-patterns with bindings
match expr {
Num(n) | Neg(n) => use(n),
Zero => 0,
}
// Literal or-patterns
match n {
1 | 2 | 3 => "low",
_ => "high",
}
// Or patterns in matches operator
if shape matches { Circle(_) | Square(_) } { ... }
Nested Sub-Patterns in Tuple/Struct Destructuring:
Tuple and struct patterns support literal, variant, and enum sub-patterns. These are lowered into guard conditions with appropriate checks:
// Literal sub-patterns in tuples
match [x, y] {
[0, 0] => "origin",
[0, _] => "y-axis",
[_, 0] => "x-axis",
_ => "other",
}
// Variant sub-patterns in tuples
match [a, b] {
[Some(x), Some(y)] => x + y,
[Some(x), None] => x,
[None, _] => 0,
}
// Literal sub-patterns in structs
if let { x: 0, y: 0 } = point { println("origin"); }
// Enum sub-patterns in tuples
match [color, size] {
[Red, Large] => "big red",
[Blue, _] => "blue",
_ => "other",
}
Mutable Bindings in Patterns:
The mut keyword before a binding name makes it mutable inside the pattern body:
if let Some(mut x) = opt {
x += 10; // x is mutable
}
match result {
Ok(mut value) => {
value *= 2;
value
},
Err(_) => 0,
}
Matches Operator
The matches infix operator tests if a value matches a pattern, returning bool.
// Basic usage
let is_some = opt matches { Some(_) };
let is_circle = shape matches { Circle(_) };
// With guard
let is_large = shape matches { Circle(r) && r > 10.0 };
// In conditions
if opt matches { Some(_) } {
println("has value");
}
Scope: Pattern bindings are scoped to the guard only and do not escape:
// Bindings don't escape
if opt matches { Some(x) } && x > 0 { } // ERROR: x not in scope
// Use guard inside the pattern instead
if opt matches { Some(x) && x > 0 } { } // OK
Branch Hints
builtin::cold_path() marks the code path that contains it as cold (rarely
executed). It is a statement with no runtime effect: it changes only code
generation. The branch that syntactically contains the call is annotated with a
Wasm branch hint so the engine lays out the other side as the predicted-taken
path, and the cold path is excluded from the inliner's cost estimate (a small
hot function stays inlinable even when it guards a large error path).
Because it is a plain statement rather than a condition wrapper, cold_path()
works anywhere a branch body does — including an if let or match arm, where
no boolean condition is available:
// Error/abort guard: the taken branch is cold.
fn get(self, i: i32) -> T {
if i >= self.len {
builtin::cold_path();
panic("index out of bounds");
}
return self.data[i];
}
// `match` arm with no boolean condition.
match command {
Command::Run => execute(),
Command::Crash => {
builtin::cold_path();
panic("crashed");
}
}
Placed on the fall-through after a guard whose taken branch diverges, it hints the guard as likely-taken — the guard-clause idiom:
fn lookup(self, key: String) -> i32 {
if let Some(v) = self.fast_path(key) {
return v;
}
builtin::cold_path(); // the slow path below is rarely reached
return self.slow_path(key);
}
Memory Model
Core Principles
- Wasm-GC based: Garbage collection delegated to runtime
- Lifetime inference: No explicit lifetime annotations required
- Value semantics: Every value is deeply copied on assignment, parameter passing, and return — references (
&T,&mut T) are the only types that share state
Value Semantics
See WEP: Value Semantics and Reference Stores.
Assignment, parameter passing, and return all perform a deep copy of the value. Primitives, structs, String, and List<T> all follow this rule uniformly. The only exceptions are reference types (&T, &mut T), which alias the underlying value.
struct Point { x: i32, y: i32 }
let a = Point { x: 1, y: 2 };
let mut b = a; // b is a deep copy of a
b.x = 10; // does not affect a
assert a.x == 1;
In-place mutation through a parameter binding (field writes, method calls, index writes) operates on the callee's local copy and is not visible to the caller. To allow callee-side mutation, pass a reference explicitly:
fn translate(p: &mut Point, dx: i32, dy: i32) {
p.x += dx; // visible to caller (reference)
p.y += dy;
}
Type System
Type Mapping at Component Boundaries
Wado types are stored internally as WebAssembly core and GC types, and lift/lower to Component Model types when crossing component boundaries (the Canonical ABI). The compiler performs this conversion automatically, letting Wado use optimal internal representations (e.g. Wasm GC structs) while interoperating through standard CM types at the boundary.
The table below is the Wado↔CM correspondence, read in both directions: Wado→CM when generating a component's exported interface, and CM→Wado when importing an external component (use { Iface } from "./c.wasm" with { type: "wasm" }, see Wasm Module and Component Imports). CM types are written in their WIT spelling.
| Wado Type | Internal Representation | CM Type at Boundary | Notes |
|---|---|---|---|
bool |
i32 |
bool |
Boolean value |
char |
i32 |
char |
Unicode scalar value |
i8, i16, i32, i64 |
i32, i32, i32, i64 |
s8, s16, s32, s64 |
Signed integers |
u8, u16, u32, u64 |
i32, i32, i32, i64 |
u8, u16, u32, u64 |
Unsigned integers |
i128, u128 |
i64 pair (Wide Arithmetic) |
tuple<s64, s64>, tuple<u64, u64> |
128-bit integers |
f32, f64 |
f32, f64 |
f32, f64 |
Floating point |
String |
GC array i8 (UTF-8) |
string |
UTF-8 string, GC-managed internally |
List<T> |
GC array T |
list<T> |
Dynamic array, GC-managed internally |
[T1, T2, ...] |
GC struct {T1, T2, ...} |
tuple<T1, T2, ...> |
Tuple types |
Option<T> |
GC variant | option<T> |
Optional value |
Result<T, E> |
GC variant | result<T, E> |
Result type; result<ok> and bare result are the payload-elided forms |
struct { ... } |
GC struct |
record { ... } |
Wasm GC struct internally, record at CM boundary |
enum { ... } |
i32 |
enum { ... } |
Enumeration without payloads |
variant { ... } |
GC variant | variant { ... } |
Variant/sum type with payloads |
flags { ... } |
i32/i64 |
flags { ... } |
Bit flags |
resource |
i32 (handle) |
resource |
Resource handle; owned and borrowed handles both map here |
Stream<T> |
CM stream (P3) | stream<T> |
Component Model async stream |
Future<T> |
CM future (P3) | future<T> |
Component Model async future |
The Prelude
The prelude (core:prelude) is automatically imported into every module, providing access to fundamental types without requiring explicit imports:
Automatically Available:
String- UTF-8 string typeList<T>- Dynamic array typeTuple<T1, T2, ...>- Alias for[T1, T2, ...]Option<T>and its variants:Some(x),None(also accessible vianullkeyword)Result<T, E>and its variants:Ok(x),Err(e)Stream<T>- Component Model async streamFuture<T>- Component Model async futurePollable- WASI I/O polling resourcei128,u128- 128-bit integer types
Disabling the Prelude:
#![no_prelude] // At the top of a module
// Now you must explicitly import everything
use {String, List, Tuple, Option, Result, Stream, Future, Pollable} from "core:prelude";
Primitive Types
Wasm primitive types are built into the language (no import required):
// Numeric
i8, i16, i32, i64
u8, u16, u32, u64
f32, f64
// Basic
bool
char
Associated Constants
Associated constants are compile-time constants defined in impl blocks using the const keyword. They are inlined at every use site and cannot be mutated.
impl f64 {
pub const PI: f64 = 3.14159265358979323846;
}
let pi = f64::PI; // inlined as the literal value
Primitive types provide built-in associated constants and static methods. See core:prelude for the full list.
128-bit Integer Types (i128/u128)
Unlike primitive types, i128 and u128 are implemented as structs in the prelude. They can be used like primitives thanks to operator overloading:
let a: u128 = 42; // literal coercion
let b = u128::from_u64(1_000_000); // explicit construction
let sum = a + b; // via Add trait
let cmp = a < b; // via Ord trait
// Access low/high 64-bit parts
let low = a.low();
let high = a.high();
WebAssembly has no native 128-bit integer type, so Wado represents them as pairs of 64-bit values. Addition and subtraction use Wasm Wide Arithmetic instructions (i64.add128, i64.sub128) for efficiency. Other operations (division, bitwise, etc.) use software implementations.
Available operations:
| Category | Operations |
|---|---|
| Arithmetic | +, -, *, /, %, unary - (i128) |
| Comparison | ==, !=, <, <=, >, >= |
| Bitwise | &, \|, ^, ~, <<, >> |
| Conversion | from_u64(), from_i64(), low(), high(), as, TryFrom |
as casts follow Rust semantics in both directions:
let a = 42 as u128; // numeric → wide int
let b = a as f64; // wide int → float, correctly rounded (ties to even)
let c = a as i64; // wide int → int, truncates to the low bits
let d = (-1 as i128) as u128; // i128 ↔ u128 reinterprets the bits (u128::MAX)
Checked conversions are available through TryFrom (e.g. i64::try_from(a), u128::try_from(n)), returning Err when the value is out of range for the target type.
SIMD Types (v128)
See WEP: SIMD v128 for full design and rationale.
Wado exposes WebAssembly SIMD via the core:simd module. A single primitive type v128 represents a 128-bit vector, with 10 newtype aliases providing type-safe interpretations:
| Category | Types |
|---|---|
| Signed | i8x16, i16x8, i32x4, i64x2 |
| Unsigned | u8x16, u16x8, u32x4, u64x2 |
| Float | f32x4, f64x2 |
All SIMD newtypes share the v128 base and can be reinterpreted via as cast (zero-cost). Each type provides splat() construction, extract_lane() access, comparison methods (eq, lt, gt, le, ge, ne), and operator overloading (+, -, *, /, &, |, ^, ~, <<, >>).
Tuple literal coercion is supported via the SequenceLiteral trait:
use { i32x4, f64x2 } from "core:simd";
let v: i32x4 = [1, 2, 3, 4]; // tuple literal coercion
let w = i32x4::splat(10); // broadcast
let sum = v + w; // [11, 12, 13, 14]
let mask = v.lt(&w); // per-lane comparison mask
Beyond basic arithmetic and comparison, types provide specialized operations: saturating arithmetic (add_sat_s/u, sub_sat_s/u), lane narrowing/extension, extended multiplication, pairwise addition, type conversion between integer and float, and bit selection. See the core:simd module documentation for the full API.
Relaxed SIMD
Relaxed SIMD operations trade strict determinism for performance. Edge-case behavior (NaN, out-of-range values) is implementation-defined but consistent within a single runtime. Methods use the relaxed_ prefix on existing newtypes:
- Fused multiply-add:
f32x4/f64x2.relaxed_madd(b, c),relaxed_nmadd(b, c) - Min/Max:
f32x4/f64x2.relaxed_min/max(faster than strictmin/max) - Truncation:
i32x4::relaxed_trunc_f32x4_s/u,relaxed_trunc_f64x2_s/u_zero - Lane select:
relaxed_laneselectoni8x16,i16x8,i32x4,i64x2 - Swizzle:
i8x16.relaxed_swizzle - Dot product:
i16x8.relaxed_dot_i8x16_i7x16_s,i32x4.relaxed_dot_i8x16_i7x16_add_s - Q15 multiply:
i16x8.relaxed_q15mulr_s
Relaxed SIMD is not modeled as an effect because: (1) results are deterministic within an environment, (2) hardware behavior cannot be intercepted, and (3) standard floats already have similar NaN non-determinism.
Reference Types
References in Wado provide indirect access to values. Unlike Rust, Wado uses a GC-based memory model with no borrow checker, enabling simpler semantics at the cost of runtime overhead.
Basic Reference Syntax:
let x = 42;
let r = &x; // Immutable reference
let v = *r; // Dereference
let mut y = 0;
let mr = &mut y; // Mutable reference
*mr = 10; // Assign through reference
Reference to Reference:
References can be nested arbitrarily:
let x = 42;
let r = &x; // &i32
let rr = &r; // &&i32
let val = **rr; // 42 (double dereference)
Automatic Coercion (&mut to &):
Mutable references automatically coerce to immutable references when needed:
fn read_value(r: &i32) -> i32 {
return *r;
}
let mut x = 10;
read_value(&mut x); // OK: &mut i32 coerces to &i32
Key Differences from Rust (GC-Based Memory Model):
| Aspect | Rust | Wado |
|---|---|---|
| Memory management | Ownership + borrow checker | Garbage collection |
| Multiple mutable refs | Not allowed | Allowed |
| Returning local refs | Not allowed (dangling) | Allowed (GC keeps alive) |
| Reference to reference | &&T (rare) |
&&T (fully supported) |
| Lifetime annotations | Required | Not needed |
| Borrow checking | Compile-time | None (runtime GC instead) |
Returning References to Local Variables:
Because Wado uses garbage collection, references to local variables remain valid after the function returns:
fn make_ref() -> &i32 {
let x = 42;
return &x; // OK in Wado (x is GC-managed and stays alive)
}
let r = make_ref();
println(`{*r}`); // Works: prints "42"
This would be a dangling pointer error in Rust, but is safe in Wado due to garbage collection.
Multiple Mutable References:
Wado allows multiple mutable references to the same value:
let mut x = 10;
let r1 = &mut x;
let r2 = &mut x; // OK in Wado (no borrow checker)
*r1 = 20;
*r2 = 30;
Design Trade-offs:
- Simplicity: No lifetime annotations or borrow checker errors
- Flexibility: Can freely share and modify references
- Cost: Runtime overhead from garbage collection
- Safety: Memory safety guaranteed by GC, not compile-time checks
Method Receiver: self by Value is Prohibited
In method definitions, the self parameter must always be a reference (&self or &mut self). Bare self (by value) is a syntax error:
impl Point {
fn sum(&self) -> i32 { ... } // OK: immutable reference
fn reset(&mut self) { ... } // OK: mutable reference
// fn consume(self) -> i32 { ... } // ERROR: `self` by value is not allowed
}
In languages with ownership semantics (e.g., Rust), self by value transfers ownership to the method, preventing subsequent use of the receiver. Wado has no ownership system — there is no concept of "consuming" a value — so self by value serves no purpose. The parser rejects it with a clear error message guiding the user to &self or &mut self.
mut Parameters
A parameter can be declared mut to allow the function body to reassign it:
fn increment(mut n: i32) -> i32 {
n += 1; // mutates the local copy
return n;
}
fn normalize(mut s: String) -> String {
s = s.to_uppercase(); // rebinds local binding
return s;
}
The mut keyword grants write access to the local parameter binding inside the function. Wado uses value semantics for every parameter: every value is deeply copied when passed to a function, except references (&T, &mut T) which share state with the caller. This applies uniformly to primitives, structs, String, and List<T>. Inside the callee, both reassignment (p = new_value) and in-place mutations — field writes (p.x = ...), method calls (s.push_str("!"), arr.push(0)), and index writes (arr[0] = ...) — operate on the callee's local copy and are not visible to the caller. To let the callee mutate the caller's value, declare the parameter as &mut T and pass a &mut-reference at the call site.
fn countdown(mut n: i32) with Stdout {
while n > 0 {
println(`{n}`);
n -= 1; // only modifies the local copy
}
}
let x = 3;
countdown(x);
// x is still 3 — every parameter is passed by value
Closures also support mut parameters:
let add_one = |mut n: i32| {
n += 1;
return n;
};
Without mut, any assignment to a parameter is a compile error:
fn bad(n: i32) {
n = 10; // Error: cannot assign to immutable variable 'n'
}
String Type
String is a built-in type representing UTF-8 encoded text with value semantics and GC management.
Design Principles:
- Value semantics: deep-copied on assignment, parameter passing, and return — passing a
Stringto a function gives the callee its own buffer - Mutable through the local binding: methods like
push_strand operators like+=modify the receiver in place, but never the caller's binding - GC-managed: Memory is automatically managed by Wasm GC
- UTF-8 encoding: Direct mapping to Component Model
string
Semantics and Encoding:
- Semantically, a
Stringis a sequence of Unicode scalar values - Internally represented as a UTF-8 byte array (
List<u8>) - Invalid UTF-8 byte sequences are not allowed; all String values must be valid UTF-8
- This ensures interoperability with Component Model
stringtype and safe string operations
Internal Structure:
// Conceptual representation (not user-visible)
struct String {
data: GcArray<u8>, // UTF-8 bytes
len: i32, // Length in bytes
capacity: i32, // Buffer capacity for += operations
}
Index Access (Prohibited)
Direct index access is prohibited to avoid ambiguity between byte and character indexing:
let s = "Hello世界";
// Prohibited
s[0] // Compile error
s[0..5] // Compile error
Use explicit methods instead:
// Byte-level access
let bytes: List<u8> = s.bytes();
let first_byte = bytes[0];
// Character-level access
let chars: List<char> = s.chars();
let first_char = chars[0];
// Other methods
s.len() -> i32 // Length in bytes
s.is_empty() -> bool // Check if empty
Note: bytes() and chars() return iterator objects (StrUtf8ByteIter and StrCharIter) that implement both Iterator and IntoIterator, so they work with for-of directly:
for let c of "hello".chars() {
println(`{c}`); // h, e, l, l, o
}
for let b of "hello".bytes() {
println(`{b}`); // 104, 101, 108, 108, 111
}
String Building:
The append method provides efficient O(1) amortized string building:
let mut builder = String::with_capacity(20);
builder.push_str("Hello");
builder.push_str(", ");
builder.push_str("World!");
// builder is now "Hello, World!"
// `+` operator for two-string concatenation
let combined = "Hello, " + "World!"; // "Hello, World!"
Concatenation
New String (+ operator):
let s1 = "hello";
let s2 = " world";
let s3 = s1 + s2; // Creates new String
Mutation (+= operator):
The += operator provides efficient in-place concatenation:
let mut s = "hello";
s += " world"; // Efficient: uses internal capacity
s += "!"; // May reallocate if capacity exceeded
Implementation: += desugars to String::add_assign(&mut s, suffix) which manages an internal buffer with amortized O(1) complexity.
Pre-allocation:
// Allocate capacity upfront for efficient building
let mut result = String::with_capacity(1000);
for let item of items {
result += item; // No reallocations if within capacity
}
Operator Consistency
The += operator has special semantics for String:
// For numeric types
x += 5 ≡ x = x + 5 // Exact equivalence
// For String
s += t ≈ s = s + t // Same result, different implementation
// += is more efficient (no intermediate allocation)
This special treatment will be generalized via traits in the future, allowing user types to define their own += behavior.
Performance Guidelines
Efficient patterns:
// 1. Pre-allocate when size is known
let mut s = String::with_capacity(estimated_size);
for let item of items {
s += item;
}
// 2. Use += for repeated concatenation
let mut result = "";
result += "Line 1\n";
result += "Line 2\n";
result += "Line 3\n";
// 3. Join arrays of strings (future)
let parts = ["a", "b", "c"];
let result = parts.join(",");
Inefficient patterns:
// Avoid: creates intermediate String objects
let s = "a" + "b" + "c" + "d";
// Prefer:
let mut s = "a";
s += "b";
s += "c";
s += "d";
See docs/wep-2026-01-15-string-type-design.md for design rationale.
Primitive Literals
Boolean Literals
let active = true;
let disabled = false;
Null Literal
The null keyword is equivalent to None and represents the absence of a value:
let missing: Option<i32> = null; // Same as None
let also_missing = None; // Standard library identifier
// Both are equivalent
assert null == None;
Note: null is a language keyword, while None is an identifier from the prelude (Option::None). They compile to the same instructions.
Character Literals
Character literals use single quotes and represent a Unicode scalar value. While internally represented as a 32-bit value (like u32), char is a distinct type with Unicode semantics—similar to how String differs from List<u8>:
let letter = 'A';
let digit = '9';
let unicode = '\u0041'; // Unicode escape (same as 'A')
let emoji = '😀'; // Direct Unicode character
let newline = '\n';
See Escape Sequences for supported escapes (\' for char, \" for string).
let a = '\u0041'; // 'A' (BMP)
let smiley = '\u{1F600}'; // '😀' (non-BMP)
char Casting and Conversion
char can be cast to any integer type to extract the Unicode scalar value (possibly truncated for smaller types):
let c = 'A';
let code = c as i32; // 65
let ucode = c as u32; // 65
let byte = c as u8; // 65 (truncated to low byte)
u8 as char is allowed because all u8 values (0..255) are valid Unicode scalar values:
let byte: u8 = 65;
let c = byte as char; // 'A'
All other integer-to-char casts are prohibited because not all values are valid Unicode scalar values (surrogates 0xD800..0xDFFF and values > 0x10FFFF are invalid):
let x: i32 = 65;
let c = x as char; // compile error
let y: i8 = 65;
let c = y as char; // compile error (i8 can be negative)
Use checked conversion functions instead:
let c = char::from_u32(65 as u32); // Option<char>: Some('A')
let c = char::from_i32(65); // Option<char>: Some('A')
See core:prelude for the full char API.
Casting char to non-integer types is a compile error:
let c = 'A';
let f = c as f64; // compile error: char can only be cast to integer types
let s = c as String; // compile error: char can only be cast to integer types
Integer Literals
let decimal = 42;
let negative = -17;
let with_separator = 1_000_000; // Underscores for readability
let binary = 0b1010_1100; // Binary
let octal = 0o755; // Octal
let hex = 0xFF_AA_BB; // Hexadecimal
Type coercion: When the target type is known from context (type annotation or function argument), integer literals coerce to any compatible integer type, including i128/u128:
let byte: i8 = 127;
let long: i64 = 9_223_372_036_854_775_807;
let unsigned: u32 = 4_294_967_295;
let big: u128 = 1_000_000_000_000;
fn foo(n: i64) { ... }
foo(100); // literal coerced to i64
Compile-time range checking: The compiler rejects literal coercions whose value falls outside the target type's range. All literal bases (decimal, hex 0x, octal 0o, binary 0b) use strict numeric range: the value must lie within [MIN, MAX] for signed types or [0, MAX] for unsigned types.
To reinterpret a bit pattern as a signed integer, use an explicit as cast.
let a: i8 = 127; // OK: max i8
let b: i8 = 128; // compile error: literal out of range for `i8`: 128
let c: i8 = -128; // OK: min i8
let d: u32 = -1; // compile error: literal out of range for `u32`: -1
let e: i8 = 0xFF; // compile error: literal out of range for `i8`: 0xFF
let f: i8 = 0xFF as i8; // OK: explicit bit-pattern reinterpretation (value: -1)
let g: i32 = 0xFFFF_FFFF; // compile error: literal out of range for `i32`: 0xFFFF_FFFF
let h: i32 = 0xFFFF_FFFF as i32; // OK: explicit bit-pattern reinterpretation (value: -1)
let i: u32 = 0x1_0000_0000; // compile error: 33-bit value does not fit in u32
Type conversion (via as):
let byte: i8 = 127 as i8;
let long: i64 = 9_223_372_036_854_775_807 as i64;
let unsigned: u32 = 4_294_967_295 as u32;
Floating-Point Literals
let pi = 3.14159;
let with_separator = 1_000_000.5;
let scientific = 6.022e23; // 6.022 × 10²³
let negative_exp = 1.6e-19; // 1.6 × 10⁻¹⁹
let explicit_positive = 2.5e+10;
Type coercion: Floating-point literals coerce to either f32 or f64 when the target type is known:
let single: f32 = 3.14;
let double: f64 = 3.14159265358979;
Type conversion (via as):
let single: f32 = 3.14 as f32;
let double: f64 = 3.14159265358979 as f64;
String Literals
String literals create String values.
Regular strings use double quotes:
let name = "Alice"; // Type: String
let path = "path/to/file.txt";
let escaped = "Line 1\nLine 2\tTabbed";
Byte strings use a b prefix and create a constant List<u8>:
let magic: List<u8> = b"\x89PNG\r\n"; // Type: List<u8>, value [137, 80, 78, 71, 13, 10]
The content must be ASCII; each \xNN escape (two hex digits) or source
character contributes one byte, and the standard escapes (\n, \t, \\,
\", \0, \r, \') are also accepted. A byte string lowers directly to a
constant data segment — never an element-by-element builder — so a large blob
costs nothing to optimize. (#include_bytes("path") produces the same
List<u8> from a file.)
Escape Sequences
Escape sequences are shared between character and string literals:
| Escape | Character |
|---|---|
\' |
Single quote (char only) |
\" |
Double quote (string only) |
\\ |
Backslash |
\/ |
Forward slash |
\b |
Backspace |
\f |
Form feed |
\n |
Newline |
\r |
Carriage return |
\t |
Tab |
\0 |
Null |
\{ |
Left brace (literal {) |
\} |
Right brace (literal }) |
\uHHHH |
Unicode BMP (4 hex digits) |
\u{H+} |
Unicode full range |
\{ and \} are useful in template strings to produce literal braces without triggering interpolation.
For characters outside BMP (U+10000 and above), use either:
"\uD83D\uDE00" // Surrogate pair
"\u{1F600}" // Variable-length escape
"😀" // Direct Unicode character
Template strings (interpolation) use backticks:
let name = "Alice";
let greeting = `Hello, {name}!`; // "Hello, Alice!"
let count = 42;
let message = `Count: {count}`; // "Count: 42"
// Format specifiers
let pi = 3.14159;
let formatted = `Pi: {pi:0.2f}`; // "Pi: 3.14"
let hex = `{255:x}`; // "ff"
// Inspect (debug) format — works for any type
let p = Point { x: 10, y: 20 };
let debug = `{p:?}`; // "Point { x: 10, y: 20 }"
let pretty = `{p:#?}`; // pretty-print: "Point {\n x: 10,\n y: 20,\n}"
// `{p}` (Display) needs an `impl Display` for `Point`; use `{p:?}` for debug output.
// Escaped braces — literal { and } without interpolation
let json = `\{"key": "{name}"\}`; // {"key": "Alice"}
See WEP: Template Format Specifiers for the full specifier table, WEP: Format Traits for the trait/Formatter infrastructure, and WEP: Inspect for the :? debug output format.
Multiline strings are supported in both regular and template strings. Literal newlines are preserved:
// Regular multiline string
let poem = "Roses are red,
Violets are blue,
Wado is great,
And so are you!";
// Multiline template string
let name = "Alice";
let message = `Dear {name},
Welcome to Wado!
Best regards`;
Tuple Literals
Bracket syntax [...] creates tuple values by default. This aligns with TypeScript conventions and JSON interoperability.
let pair = [1, "hello"]; // Type: [i32, String]
let triple = [42, "answer", true]; // Type: [i32, String, bool]
let single = [42]; // Type: [i32] (1-tuple)
let empty_tuple: [] = []; // Empty tuple (distinct from unit ())
let trailing = [1, 2, 3,]; // Trailing comma allowed
Tuple Types:
Tuple types use bracket syntax [T1, T2, ...]. Tuple<T1, T2, ...> is available as an alias.
let point: [i32, i32] = [10, 20];
let record: [String, i32, bool] = ["Alice", 30, true];
Tuple Element Access:
Tuple elements are accessed by constant index using dot notation or bracket notation:
let t = [10, "hello", true];
let x = t.0; // 10 - dot notation
let y = t[1]; // "hello" - bracket notation
let z = t.2; // true
// Variable index is not allowed (compile error)
let i = 1;
let w = t[i]; // Error: tuple index must be a constant integer
Unit vs Empty Tuple:
The unit type () and empty tuple [] are distinct:
let unit: () = (); // Unit type/value
let empty: [] = []; // Empty tuple (rarely used)
The never type (!) — bottom type:
never is the bottom type: it is a subtype of every type. An expression of type never never returns — it always diverges (traps). panic() and unreachable() both return !.
Because never is assignable to any type, a never-typed expression may appear in any value position without a type mismatch:
// In a match arm — the None branch panics, so the match has type i32
let opt: Option<i32> = Option::<i32>::Some(5);
let x = match opt {
Some(v) => v,
None => panic("unexpected none"),
};
// In a let binding with explicit type annotation
let y: i32 = panic("unreachable");
// In a binary expression — execution diverges before the addition
let z: i32 = panic("boom") + 1;
The ! type can be written explicitly as a return type:
fn fail(msg: String) -> ! {
panic(msg);
}
List Literals
Arrays require explicit conversion from tuple literals using as or implicit coercion when the target type is known at compile time.
// Explicit conversion with `as`
let numbers = [1, 2, 3, 4, 5] as List<i32>;
// Implicit coercion (target type known)
fn takes_array(a: List<i32>) { ... }
takes_array([1, 2, 3]); // OK - compiler knows List<i32> is expected
// Type annotation
let explicit: List<i32> = [1, 2, 3]; // Coerced to array
Coercion Rules:
- Compile-time: When the target type is known (function parameter, type annotation), implicit coercion is allowed
- Runtime/ambiguous: Explicit
as List<T>is required
let t = [1, 2, 3]; // Tuple [i32, i32, i32] - no context
let a = [1, 2, 3] as List<i32>; // List - explicit conversion
fn process(data: List<i32>) { ... }
process([1, 2, 3]); // OK - implicit coercion
Design Rationale:
This design aligns with TypeScript (primary target audience) and enables intuitive JSON interoperability. JSON arrays are heterogeneous and map naturally to tuples:
{ "point": [10, 20], "mixed": [1, "hello", true] }
// A JSON array maps naturally to a tuple:
let point: [i32, i32] = [10, 20];
let mixed: [i32, String, bool] = [1, "hello", true];
See docs/wep-2026-01-15-tuple-and-array-literals.md for detailed rationale.
List Operations:
Arrays support index-based access and assignment:
List Constructors:
let arr = List::<i32>::with_capacity(10); // empty array with pre-allocated capacity
let bools = List::<bool>::filled(100, true); // array of 100 elements, all true
List Operations:
let mut arr: List<i32> = [1, 2, 3];
// Index access (read)
let first = arr[0]; // 1
// Index assignment (write)
arr[0] = 100; // Requires mutable array
arr[1] = 200;
// List methods
arr.push(4); // Add element to end
let len = arr.len(); // Get length
Index Assignment Rules:
- Requires the array variable to be declared with
let mut - Index must be within bounds (runtime check, traps if out of bounds)
- Works with arrays of any element type
Sorting (stable, O(n log n) worst case):
| Method | Mutates? | Comparator |
|---|---|---|
sort() |
Yes | < (requires T: Ord) |
sort_by() |
Yes | Custom fn(&T, &T) -> Ordering |
sorted() |
No | < (requires T: Ord) |
sorted_by() |
No | Custom fn(&T, &T) -> Ordering |
let mut nums: List<i32> = [5, 3, 8, 1];
nums.sort(); // in-place ascending
let orig: List<i32> = [5, 3, 8, 1];
let asc = orig.sorted(); // returns new sorted array
Collection Literal Coercion
Sequence literals [e0, e1, ...] and key-value literals { k: v, ... } can be
coerced to any collection type by implementing the corresponding builder trait:
| Literal | Trait | Example target |
|---|---|---|
[e0, e1, ...] |
SequenceLiteralBuilder |
List<T> |
{ k: v, ... } |
KeyValueLiteralBuilder |
TreeMap<String, V> |
Builder Traits:
pub trait SequenceLiteralBuilder {
type Element;
type Output;
fn new_literal(capacity: i32) -> Self;
fn push_literal(&mut self, value: Self::Element);
fn build(&self) -> Self::Output;
}
pub trait KeyValueLiteralBuilder {
type Value;
type Output;
fn new_literal(capacity: i32) -> Self;
fn insert_literal(&mut self, key: String, value: Self::Value);
fn build(&self) -> Self::Output;
}
When a type implements SequenceLiteralBuilder<Output = Self> or KeyValueLiteralBuilder<Output = Self>, a blanket impl provides the corresponding SequenceLiteral / KeyValueLiteral trait automatically (self-as-builder pattern).
Usage:
let arr: List<i32> = [1, 2, 3];
use { TreeMap } from "core:collections";
let map: TreeMap<String, i32> = { width: 1920, height: 1080 };
Coercion is literal-only — it does not apply to bound variables. If the target type is a struct with matching fields, it is interpreted as a struct literal and coercion is not attempted.
See docs/wep-2026-01-18-iterator-based-literal-coercion.md
for desugaring rules, the immutable-output (separate builder) pattern, and concrete type validation.
Compile-Time Location Literals
Compile-time location literals provide source location information at compile time. They use the # prefix to clearly signal compile-time evaluation.
| Literal | Type | Value |
|---|---|---|
#file |
String |
Current source file path |
#line |
i32 |
Current line number (1-indexed) |
#function |
String |
Fully specialized function name |
#data |
String |
__DATA__ section content (compile error if none) |
#include_str("path") |
String |
External file content as string |
#include_bytes("path") |
List<u8> |
External file content as bytes |
fn example() {
println(`Error at {#file}:{#line}`);
println(`In function: {#function}`);
}
#data:
Returns the raw text content of the __DATA__ section as a String. This is useful for programs that need to access embedded metadata at runtime (e.g., configuration, test fixtures, embedded documents). Using #data in a file that has no __DATA__ section is a compile error.
export fn run() with Stdout {
let config = #data; // contains the __DATA__ section text
println(config);
}
__DATA__
{"key": "value"}
#include_str and #include_bytes:
#include_str("path") reads an external file at compile time and returns its content as a String. The file must be valid UTF-8; otherwise, a compile error is raised. #include_bytes("path") returns the raw bytes as List<u8> without UTF-8 validation.
The path argument must be a string literal. Paths are resolved relative to the source file containing the expression. See WEP: Compile-Time File Inclusion.
let template = #include_str("./templates/header.html");
let icon: List<u8> = #include_bytes("./assets/logo.png");
#function Format:
Returns the fully specialized name without signature:
| Context | #function value |
|---|---|
| Free function | my_function |
| Method | Point::distance |
| Generic method | List<String>::len |
| Closure | parent_function::{closure} |
Call-site evaluation in default arguments:
As a default argument, #file / #line / #function evaluate at the call site, so a defaulted location parameter reports the caller (cf. Swift's #file/#line defaults, C++'s std::source_location::current()):
pub fn log(msg: String, file: String = #file, line: i32 = #line) { /* ... */ }
log("started"); // file/line report this call, not where `log` is defined
Name resolution in a default otherwise uses the callee's scope; only these three literals are redirected. #data / #include_str / #include_bytes and struct field defaults always report their own defining file. For a nested defaulted call (fn outer(x = loc())), every literal reports the outermost call site (outer(...)).
Closures
Closures are anonymous function expressions with |params| body syntax.
An expression body returns its value implicitly:
let add_one = |x: i32| x + 1;
let make_point = |x: i32, y: i32| Point { x, y };
A block body requires explicit return:
let compute = |x: i32| {
let doubled = x * 2;
return doubled + x * 3;
};
An optional -> Type declares the return type, and is what a ? in the body
resolves against:
let parse = |s: String| -> Result<i32, String> {
let n = to_int(s)?;
return Result::Ok(n + 1);
};
Parameter types are always required (never inferred). A ? in the body needs
the return type known — via -> Type or an expected fn(..) -> R.
Closures auto-capture each free variable by reference; the reference kind is inferred from body usage (&T for read-only, &mut T for mutating). Pure read-only captures keep the closure type at fn; any &mut capture promotes it to fn mut. Calling a fn mut closure requires the root of the callee place to be a mutable binding (mirrors Rust's FnMut rule); this applies whether the closure is called directly (f()) or reached through field access or indexing ((h.f)(), arr[i]()). A temporary root — a call result, a literal — has no binding and is always accepted.
Shared mutable state across closures is automatic — multiple closures referring to the same outer binding share the underlying location, with no explicit reference dance needed:
let mut count = 0;
let mut inc = || count += 1; // captures &mut count; type fn mut() -> ()
let get = || count; // captures &count; type fn() -> i32
inc();
inc();
assert get() == 2;
See docs/wep-2026-01-16-closure-implementation.md for the full design (fn vs fn mut, sub-typing, effect generics, iterator API integration).
Note: stores[...] is a separate concept for declaring that a function stores reference parameters beyond the call. It is not yet implemented. See Reference Storage and docs/wep-2026-01-12-value-semantics-and-stores.md.
Function References
A bare function name is an expression of function type. It evaluates to a value of type fn(P...) -> R [with E...] matching the function's signature — internally a zero-capture closure over the named function.
fn double(n: i32) -> i32 { return n * 2; }
let f = double; // type: fn(i32) -> i32
assert f(21) == 42;
apply(double, 21); // pass directly; no `&` needed
let g: fn(i32) -> i32 = double;
Key points:
- Function values carry no observable identity at the Wado level. The runtime uses a Wasm
refunder the hood, but the language does not let you distinguish "value vs reference to value" — there is no state to observe (Wado has noCopy/Clone/identity comparison forfn). &and&mutapply tofn-typed values like to any other value, with no special-casing:&fhas type&fn(...);&mut f(on a mutable binding) has type&mut fn(...).- These references behave per the Reference Types rules.
&fn(...)is not a synonym forfn(...); passing one where the other is expected is a type error. &mut fn(...)parameters are useful as out-parameters: the callee can reassign the referenced slot via*p = other_fn, and the caller observes the new value through the same binding.
- A
&fn(...)or&mut fn(...)value is directly callable; the call expression auto-derefs to invoke the underlyingfn(...).let r = &double; r(21)works without an explicit*r. - Generic functions taken as values need their type arguments pinned. Two principled forms are supported:
- Turbofish on the name itself:
let f = identity::<i32>;evaluates to afn(i32) -> i32value, and a non-call use likeapply(identity::<i32>, 7)works the same way. - An expected
fn(...)type at the use site:let f: fn(i32) -> i32 = identity;andapply(identity, 7)(whereapply's parameter isfn(i32) -> i32) both pin the type arguments through positional inference against the expected signature. - When neither form applies, the compiler emits a dedicated diagnostic suggesting turbofish or a closure wrapper (
|x| identity(x)). The error replaces the older "unknown function" cascade that used to leak fromUNKNOWNtyping.
- Turbofish on the name itself:
- Functions that appear at the Component Model boundary still cannot carry function-typed values across — see the closure WEP.
Default Arguments
Trailing function parameters may declare default values with = expr. Calls that omit defaulted arguments are expanded at the call site, with no runtime cost:
fn connect(host: String, port: i32 = 8080, timeout: i32 = 30) { ... }
connect("localhost"); // → connect("localhost", 8080, 30)
connect("localhost", 3000); // → connect("localhost", 3000, 30)
connect("localhost", 3000, 60);
Rules:
- All defaulted parameters must come after all non-defaulted parameters.
- Default expressions must be effect-free (validated by the effect system).
- Default expressions may reference earlier parameters in the same function:
fn make_rect(width: f64, height: f64 = width) -> Rect { ... }
make_rect(10.0); // → make_rect(10.0, 10.0)
Restrictions:
selfcannot have a default.- Function types do not carry default information; assigning a function with defaults to a
fn(...)type erases them, and every call site of that variable must supply every argument. - Closures cannot declare defaults: a closure value's arity must match its
fn(...)type. The parser accepts= expron closure parameters for recovery only and the elaborator rejects it. export fncannot declare defaults — exported functions appear in the component's WIT signature where every parameter is required by the CM ABI. Split into a private helper plus a thinexport fnwrapper if defaults are needed.- Trait methods may declare defaults only in the trait definition; implementations receive every parameter and cannot add, remove, or change defaults. Direct
impl Type { ... }methods (not part of any trait) may declare defaults freely.
The same = expr syntax applies to struct fields; see Struct Field Defaults.
Tagged Template Literals
Tagged template literals enable compile-time function execution on string literals, allowing zero-overhead binary encoding, DSL validation, and custom compile-time transformations.
Syntax:
let result = tag`literal string`;
Where tag is an effect-free function that executes at compile time.
Requirements:
- The tag function must have no
withclause (effect-free/pure function) - Function signature:
fn(String) -> TwhereTis any type - The function is executed during compilation
- If the function panics, it becomes a compile error
- Only other effect-free functions can be called within the tag function
Example - Binary Literals:
use {base64, hex} from "core:encoding";
// base64 and hex are standard library functions, not keywords
let embedded_image = base64`iVBORw0KGgoAAAANSUhEUgAAAAUA...`; // Type: List<u8>
let crypto_key = hex`48656c6c6f20576f726c64`; // Type: List<u8>
// Invalid base64 causes compile error
let invalid = base64`!!!invalid!!!`; // Compile error: Invalid base64 encoding
Example - DSL Validation:
// User-defined compile-time validation
fn regex(pattern: String) -> Regex {
match compile_regex(pattern) {
Ok(r) => r,
Err(e) => panic(`Invalid regex pattern: {e}`), // Compile error
}
}
let email_pattern = regex`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`;
// SQL query validation
fn sql(query: String) -> SqlQuery {
match parse_sql(query) {
Ok(q) => q,
Err(e) => panic(`Invalid SQL syntax at {e.position}: {e.message}`),
}
}
let query = sql`SELECT * FROM users WHERE id = ?`; // Validated at compile time
Standard Library Support:
The core:encoding module provides common binary encodings:
use {base64, hex} from "core:encoding";
// Base64 decoding (RFC 4648)
pub fn base64(input: String) -> List<u8> {
match decode_base64_impl(input) {
Ok(data) => data,
Err(e) => panic(`Invalid base64 encoding: {e}`),
}
}
// Hexadecimal decoding
pub fn hex(input: String) -> List<u8> {
match decode_hex_impl(input) {
Ok(data) => data,
Err(e) => panic(`Invalid hex encoding: {e}`),
}
}
Compile-Time Execution Constraints:
Tagged template functions are executed at compile time with the following constraints:
- Effect-free only: Functions with
withclauses cannot be used as tags - Pure computation: Only other effect-free functions can be called
- Deterministic: Execution must be deterministic (guaranteed by Wado's deterministic libm)
- Heap allocation: Allowed via Wasm GC (unlike Rust's
const fn) - Recursion: Allowed with reasonable depth limits
- No I/O: Functions requiring effects (FileSystem, Network, etc.) cannot be called
Design Rationale:
Tagged template literals provide a general mechanism for compile-time computation, avoiding the need for built-in syntax for each use case. This aligns with Wado's philosophy of minimal built-ins and explicit dependencies. See docs/wep-2026-01-10-tagged-template-literals.md for detailed design decisions.
Future Extensions:
Interpolation support may be added in future versions:
// Future: interpolation syntax (not yet implemented)
let id = 42;
let query = sql`SELECT * FROM users WHERE id = ${id}`;
Newtype
type T = U creates a newtype - a distinct type that shares representation with its base type.
type Meters = f64;
type Kilometers = f64;
let m: Meters = 1000.0; // literal coercion
let km: Kilometers = 1.0;
let sum = m + m; // OK: Meters + Meters -> Meters
// let bad = m + km; // ERROR: cannot mix Meters and Kilometers
let raw: f64 = m as f64; // explicit cast required
Properties:
Tis a distinct type fromU(no implicit conversion)Tinherits all methods, operators, and traits fromU- Explicit
ascast required to convert betweenTandU - Zero runtime cost (same Wasm representation)
- Literal coercion to
Twhen type context expectsT
Method Signature Substitution:
When calling inherited methods on a newtype, parameters and return types are substituted:
type Location = Point;
impl Point {
fn distance(&self, other: &Point) -> f64 { ... }
}
let loc1: Location = Point { x: 0, y: 0 } as Location;
let loc2: Location = Point { x: 3, y: 4 } as Location;
loc1.distance(&loc2); // params expect &Location, returns f64
Newtype-Specific Methods:
impl Location {
fn name(&self) -> String { ... } // only on Location, not Point
}
Chained Newtypes:
type A = i32;
type B = A;
type C = B;
let c: C = 1;
let a = c as A; // OK: direct cast through chain
let i = c as i32; // OK: direct cast to ultimate base
For complete type isolation where you want to hide base type methods, use a struct wrapper:
struct Miles { value: i32 }
Literal Types
// String literal types
type Direction = "north" | "south" | "east" | "west";
// Numeric literal types
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
// Object literal types
type Status = {
status: "loading" | "success" | "error",
message: String,
};
Structs
Wado uses struct for structured data types. Internally they are implemented as Wasm-GC structs, and automatically converted to Component Model record at component boundaries.
// Struct definition
struct User {
name: String,
age: i32,
active: bool,
}
// Struct with recursive type (enabled by GC)
struct Node {
value: i32,
next: Option<Node>,
}
// Inline struct type
type UserData = struct {
name: String,
age: i32,
};
Field Visibility:
Struct fields follow the same visibility rules as other declarations (see Visibility). A field without a modifier is private to the defining file; internal widens it to the package; pub exposes it to other Wado packages.
pub struct Config {
pub name: String, // visible to other packages
internal tag: i32, // visible to other files in this package
secret: i32, // private to this file
}
Within the defining module, all fields (including private ones) are accessible for construction, reading, and mutation. From another file in the same package, internal (and pub) fields are accessible; from another package, only pub fields are. Reading, setting, or binding a field beyond its reach produces a compile error — whether through field access (c.secret), a struct literal (Config { secret: ... }), or a destructuring pattern (let Config { secret, .. } = c, match). A non-reachable field may still be omitted from a struct literal in another module when it has a default expression (f: T = expr): the default is evaluated in the defining module, so the field is never read or set across the boundary and encapsulation is preserved. A non-reachable field without a default cannot be satisfied from another module, so such a struct can only be constructed by a function within reach.
Struct Construction:
let user = User { name: "Alice", age: 30, active: true };
// Shorthand (variable name matches field)
let name = "Bob";
let age = 25;
let bob: User = { name, age, active: false };
// Implicit struct literal (requires type annotation)
let user: User = { name: "Alice", age: 30, active: true };
Functional update (..base): a leading ..base fills every field the literal
does not list explicitly from the struct value base (same type). The listed
fields override; base is evaluated once and left unchanged (value semantics).
let u2 = User { ..user, age: 31 }; // every field from `user`, age replaced
The spread is leading and single: a field written before it would be overwritten
and unused, so User { age: 31, ..user }, a second spread, and a bare
User { ..user } (a plain copy) are all errors. A ..base cannot read a field
that is not reachable at the use site, so it never exposes a private field across
a module boundary. See WEP: Literal Spread.
Struct Destructuring:
let p = Point { x: 10, y: 20 };
// Unnamed destructuring (type inferred from RHS)
let { x, y } = p;
// Named destructuring (explicit type)
let Point { x, y } = p;
// Renaming fields
let { x: horizontal, y: vertical } = p;
// Ignore remaining fields with ..
struct Person { name: String, age: i32, email: String }
let { name, .. } = person;
// Mutable destructuring
let mut { x, y } = p;
// Nested destructuring
struct Line { start: Point, end: Point }
let { start: { x: x1, y: y1 }, end: { x: x2, y: y2 } } = line;
// In for-of
for let { x, y } of points {
println(`{x}, {y}`);
}
Auto-derived Traits:
Structs derive Eq (field-wise equality) and Ord (lexicographic comparison by field declaration order) when all fields implement those traits, synthesized on demand rather than for every struct — see Bound-Driven Eq / Ord. A user-provided impl Eq or impl Ord takes precedence.
For generic structs, the auto-derived impls have trait bounds on the type parameters: impl<T: Eq> Eq for Foo<T>, impl<T: Ord> Ord for Foo<T>.
Variants derive Eq only (not Ord) the same on-demand way, when all payload types implement Eq: both values must be the same case, and payloads (if any) are compared. A user-provided impl Eq takes precedence. For generic variants, the auto-derived impls have trait bounds on the type parameters: impl<T: Eq> Eq for Maybe<T>.
Struct Field Defaults
Struct fields may declare a default expression with = expr. Fields with defaults may be omitted at construction sites; fields without defaults are required:
struct ServerConfig {
host: String, // required
port: i32 = 8080, // optional
timeout: i32 = 30, // optional
debug: bool = false, // optional
}
let c = ServerConfig { host: "localhost" };
// Desugars to: ServerConfig { host: "localhost", port: 8080, timeout: 30, debug: false }
let c = ServerConfig { host: "localhost", port: 3000 };
// Desugars to: ServerConfig { host: "localhost", port: 3000, timeout: 30, debug: false }
ServerConfig { port: 3000 }; // compile error: missing required field 'host'
Default expressions are evaluated at the construction site. They must be effect-free (validated by the effect system) and cannot reference other fields. Field shorthand ({ host }) and destructuring are unaffected — destructuring sees every field regardless of defaults.
A non-generic struct whose every field has a default auto-derives Default; see Default Trait.
Generic Type Inference
Wado infers type arguments for generic type constructors (struct literals and variant constructors) using two complementary mechanisms:
Forward inference derives type parameters from the values provided (fields or payload arguments):
struct Box<T> { value: T }
let b = Box { value: 42 }; // Box<i32> — T=i32 from field value
let opt = Option::Some("hello"); // Option<String> — T=String from payload
let opt2 = Option::Some(42); // Option<i32> — T=i32 from payload
Backward inference derives type parameters from an expected type context (variable annotation, function parameter type, or return type):
let none: Option<i32> = Option::None; // T=i32 from annotation
let ok: Result<i32, String> = Result::Ok(42);
// T=i32 from payload (forward), E=String from annotation (backward)
When both mechanisms are available, forward inference takes precedence for type parameters that appear in the payload, and backward inference fills in any remaining parameters.
Scope of inference:
| Constructor kind | Forward | Backward | Status |
|---|---|---|---|
| Struct literals | yes | yes | implemented |
| Variant constructors | yes | yes | implemented |
| Generic function calls | — | — | not yet implemented |
| Generic method calls | — | — | not yet implemented |
For generic function and method calls, explicit turbofish syntax is required:
fn identity<T>(x: T) -> T { return x; }
let x = identity::<i32>(42); // turbofish required (for now)
// let y = identity(42); // not yet supported
A _ inside a turbofish leaves that type-argument slot for inference while the
others stay explicit, reusing the same inference an omitted turbofish uses. The
explicit (non-_) arguments always win; an uninferable _ is the same error as
an omitted turbofish on an uninferable parameter. It is scoped to turbofish
arguments — a _ in a plain type annotation (let xs: Array<_>) is rejected.
let r = Result::<_, MyErr>::Ok(42); // infers Ok payload type, pins the error type
let a = pick::<_, bool>(1, true); // infers the first type argument
Traits
Traits define shared behavior that types can implement. Wado uses static dispatch for trait methods - all calls are resolved at compile time.
// Trait declaration
trait Greet {
fn greet(&self) -> String;
}
// Trait implementation
struct Person {
name: String,
}
impl Greet for Person {
fn greet(&self) -> String {
return `Hello, {self.name}!`;
}
}
// Usage
let p = Person { name: "Alice" };
println(p.greet()); // "Hello, Alice!"
Multiple Traits:
A struct can implement multiple traits:
trait Named {
fn name(&self) -> String;
}
trait Aged {
fn age(&self) -> i32;
}
impl Named for Person {
fn name(&self) -> String { return self.name; }
}
impl Aged for Person {
fn age(&self) -> i32 { return self.age; }
}
Method Resolution:
When a method is called on a value:
- Inherent methods (defined in
impl Type { }) are checked first - Trait methods (defined in
impl Trait for Type { }) are checked if no inherent method matches - If multiple traits define the same method name, it's a compile error
struct Robot { id: i32 }
// Inherent method
impl Robot {
fn greet(&self) -> String { return "Beep boop"; }
}
// Trait method (won't be called because inherent method exists)
impl Greet for Robot {
fn greet(&self) -> String { return "Hello from trait"; }
}
let r = Robot { id: 1 };
r.greet(); // Returns "Beep boop" (inherent method wins)
Default Method Implementations:
Trait methods can have default implementations. Implementors can override them or use the defaults:
trait Summary {
fn title(&self) -> String; // required - must be provided
// Default method - uses self.title()
fn summary(&self) -> String {
return `Title: {self.title()}`;
}
}
struct Article { headline: String }
// Only provides the required method; summary() uses the default
impl Summary for Article {
fn title(&self) -> String { return self.headline; }
}
struct Report { headline: String, body: String }
// Overrides the default summary()
impl Summary for Report {
fn title(&self) -> String { return self.headline; }
fn summary(&self) -> String { return `{self.headline}: {self.body}`; }
}
Default methods can call other trait methods (both required and default), and the calls are resolved against the implementing type.
Associated Types:
Traits can declare associated types - placeholder types that are specified by implementors:
trait Container {
type Item; // Associated type declaration
fn get(&self) -> Self::Item;
fn set(&mut self, value: Self::Item);
}
struct IntBox {
value: i32,
}
impl Container for IntBox {
type Item = i32; // Associated type binding
fn get(&self) -> Self::Item {
return self.value;
}
fn set(&mut self, value: Self::Item) {
self.value = value;
}
}
Within trait methods and implementations, Self::TypeName refers to the associated type. The type is resolved at compile time based on the implementing type.
Bounded Associated Types:
Associated types can have trait bounds that constrain what types can be used as the associated type:
trait Collection {
type Element;
type Builder: CollectionBuilder<Element = Self::Element, Output = Self>;
}
Here Builder must implement CollectionBuilder with matching Element and Output types. The Type = ConcreteType syntax constrains associated types of the bound trait to specific types.
Blanket Implementations:
A blanket impl provides a trait implementation for all types that satisfy a given bound:
// Any type that builds itself satisfies Collection automatically
impl<T: CollectionBuilder<Output = T>> Collection for T {
type Element = T::Element;
type Builder = T;
}
This avoids the need for explicit impl Collection for ... on every self-building type. The compiler resolves T::Element via associated type projection on the type parameter.
Standard Library Traits:
The prelude defines IndexValue, IndexAssign, and Index traits using associated types. See Indexing Traits for full definitions.
Trait Bounds:
Type parameters can have trait bounds that constrain what types can be used:
// Struct with trait bound
struct SortedPair<T: Ord> {
first: T,
second: T,
}
// Multiple bounds with + syntax
struct PrintableOrd<T: Ord + Printable> {
value: T,
}
// Bounds on function type parameters
fn max<T: Ord>(a: T, b: T) -> T {
if a > b { return a; }
return b;
}
// Bounded impl blocks - methods only available when T: Ord
impl List<T: Ord> {
pub fn sort(&mut self) { ... }
pub fn sorted(&self) -> List<T> { ... }
}
// Bounded trait implementations - Pair<T> implements Eq only when T: Eq
impl<T: Eq> Eq for Pair<T> {
fn eq(&self, other: &Self) -> bool {
return self.first == other.first && self.second == other.second;
}
}
Not Yet Implemented:
- Trait objects (
dyn Trait) - Fully qualified syntax for disambiguation (
<Type as Trait>::method()) - Using bounds for method resolution on type parameters (calling
T.method()whereT: Trait)
Coherence and Orphan Rules
Wado enforces coherence: for every (Trait, Type) pair, there is at most one impl Trait for Type that can apply. Coherence is guaranteed by the orphan rules, which restrict where trait implementations may be written.
Package Boundary
The unit of coherence is a package — all source files compiled together from the same wado.toml project. Types and traits are classified relative to that boundary:
| Module source | Classification |
|---|---|
./file.wado (relative path import) |
Local |
| Entry-point file | Local |
core:* (standard library) |
Foreign |
wasi:* (WASI interfaces) |
Foreign |
| Remote URL | Foreign |
The Orphan Rule
For impl<P1..Pn> Trait<A1..Am> for T0, the implementation is valid if and only if at least one of these conditions holds:
Traitis local (defined in the current package), or- The sequence
T0, A1, A2, …, Amcontains a local type at some positioni, and no uncovered type parameter appears at any positionj < i.
Uncovered type parameter: A type parameter Pk is uncovered at position i if the type at position i is literally Pk (bare, not wrapped inside another type constructor). List<Pk> is covered; Pk alone is uncovered.
Fundamental types: &T and &mut T are fundamental — they are looked through when checking positions. impl Trait for &LocalType counts as having LocalType at position T0.
Examples
| Implementation | Verdict | Reason |
|---|---|---|
impl Eq for MyStruct |
Allowed | MyStruct is local (T0 is local) |
impl MyTrait for String |
Allowed | MyTrait is local |
impl<T: Eq> Eq for MyBox<T> |
Allowed | MyBox is local (T0 is local) |
impl From<MyError> for String |
Allowed | MyError (local) at A1, no uncovered param before it |
impl<T> From<MyType<T>> for String |
Allowed | MyType (local head) at A1, no uncovered param before it |
impl<T> From<T> for MyType |
Allowed | MyType is local at T0, reached before T1=T |
impl Eq for String |
Forbidden | Both Eq and String are foreign |
impl Eq for List<i32> |
Forbidden | Eq foreign, List (head of T0) is foreign |
impl<T> Eq for T |
Forbidden | T0 is uncovered type parameter, Eq is foreign |
impl<T> From<T> for String |
Forbidden | T0=String (foreign), T1=T (uncovered) before any local |
impl From<String> for i32 |
Forbidden | T0=i32 (foreign), A1=String (foreign), no local found |
Rationale
The orphan rule prevents two packages from independently providing impl Trait for Type for the same (Trait, Type) pair, which would make method resolution ambiguous when both packages are used together. By requiring at least one of the trait or the self type to be local, every valid implementation is "owned" by exactly one package.
The sequence rule (RFC 2451 style) allows impl From<LocalError> for String — even though String is foreign — because LocalError appears in the trait's type argument at position A1 with no uncovered type parameter before it. This makes it unnecessary to define a mirror Into trait just to work around stricter rules.
Inherent Impls
An inherent impl (impl Type { … }, with no trait) is subject to a simpler
coherence rule: it may only be written in the package that owns the type.
The self type's head constructor must be local.
| Implementation | Verdict | Reason |
|---|---|---|
impl MyStruct { … } |
Allowed | MyStruct is local |
impl<T> MyBox<T> { … } |
Allowed | MyBox (head) is local |
impl i32 { … } |
Forbidden | i32 is foreign |
impl String { … } |
Forbidden | String is foreign |
impl<T> Array<T> { … } |
Forbidden | Array is foreign |
impl List<u8> { … } |
Forbidden | List (head) is foreign — even when fully concrete |
This mirrors the trait-impl rationale: if two packages could each add inherent
methods to the same foreign type, their methods would collide. To extend a
foreign type from another package, define a local trait and implement it for
that type (impl MyExt for String) — the orphan rule above permits this because
the trait is local. The owning package itself (e.g. core for String /
Array<T> / List<T>) is of course free to spread inherent impls across its own
modules.
Iterator Traits
The prelude defines iterator traits for generic iteration over collections.
Iterator - Core Iteration Trait:
/// Types that can yield a sequence of values
pub trait Iterator {
type Item;
/// Advances the iterator and returns the next value.
/// Returns None when iteration is complete.
fn next(&mut self) -> Option<Self::Item>;
}
IntoIterator - Conversion Trait:
/// Types that can be converted into an iterator
pub trait IntoIterator {
type Item;
type Iter; // The iterator type
/// Creates an iterator from a value
fn into_iter(&self) -> Self::Iter;
}
FromIterator - Collection Construction:
/// Types that can be constructed from an iterator
pub trait FromIterator<T> {
type Iter;
fn from_iter(iter: Self::Iter) -> Self;
}
ListIter:
The prelude provides ListIter<T> as the iterator type for List<T>:
/// Iterator over List<T> elements
pub struct ListIter<T> {
// internal fields
}
impl Iterator for ListIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> { ... }
}
impl IntoIterator for List<T> {
type Item = T;
type Iter = ListIter<T>;
fn into_iter(&self) -> ListIter<T> { ... }
}
Usage:
let arr: List<i32> = [1, 2, 3, 4, 5];
// for-of uses IntoIterator automatically
for let x of arr {
println(`{x}`);
}
// Explicit iterator
let mut iter = arr.iter();
while let Some(x) = iter.next() {
println(`{x}`);
}
// Collect remaining elements
let mut iter2 = arr.iter();
iter2.next(); // skip first
let rest = iter2.collect(); // [2, 3, 4, 5]
Value Semantics:
By-value iteration (into_iter(), for let x of list) returns copies of elements. Reference iteration yields references instead: iter() and for let x of &list yield &T, and for let x of &mut list yields &mut T.
&mut iteration enables in-place mutation for element types with an addressable interior — struct, List, String, i128/u128 — whose &mut T is the element's shared GC handle:
for let p of &mut points {
p.x += 1; // mutates the element in place
}
A replace-on-assign element type (primitive, enum, flags, variant, fn) has no addressable interior, so a write through &mut T would be lost. &mut iteration over such a list is a compile error; use indexed access instead:
for let mut i = 0; i < arr.len(); i += 1 {
arr[i] = arr[i] * 2;
}
Custom Iterables:
Any type can be made iterable by implementing IntoIterator:
struct Stack<T> { items: List<T> }
struct StackIter<T> { items: List<T>, index: i32 }
impl Iterator for StackIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> { ... }
}
impl IntoIterator for Stack<T> {
type Item = T;
type Iter = StackIter<T>;
fn into_iter(&self) -> StackIter<T> { ... }
}
// Now for-of works
for let x of stack { ... }
Iterator Combinators:
Iterators support map, filter, and fold for functional-style data processing:
let arr: List<i32> = [1, 2, 3, 4, 5];
// map - transform each element
let doubled = arr.iter().map(|x: i32| x * 2).collect();
// [2, 4, 6, 8, 10]
// filter - keep elements matching predicate
let evens = arr.iter().filter(|x: i32| x % 2 == 0).collect();
// [2, 4]
// fold - reduce to single value
let sum = arr.iter().fold(0, |acc: i32, x: i32| acc + x);
// 15
// Chaining combinators
let result = arr.iter()
.filter(|x: i32| x > 2)
.map(|x: i32| x * 10)
.collect();
// [30, 40, 50]
Builtin Comparison Traits
The prelude defines traits for comparison operators:
Eq - Equality:
/// Types that can be compared for equality
pub trait Eq {
/// Returns true if self equals other
fn eq(&self, other: &Self) -> bool;
}
The == and != operators use Eq::eq:
a == bdesugars toEq::eq(&a, &b)a != bdesugars to!Eq::eq(&a, &b)
Ordering Enum:
/// Result of a three-way comparison
pub enum Ordering {
Less, // first value is less than second
Equal, // values are equal
Greater, // first value is greater than second
}
Ord - Ordering:
/// Types that can be ordered
pub trait Ord {
/// Compares self with other and returns an Ordering
fn cmp(&self, other: &Self) -> Ordering;
}
Comparison operators use Ord::cmp:
a < bdesugars toOrd::cmp(&a, &b) == Ordering::Lessa > bdesugars toOrd::cmp(&a, &b) == Ordering::Greatera <= bdesugars toOrd::cmp(&a, &b) != Ordering::Greatera >= bdesugars toOrd::cmp(&a, &b) != Ordering::Less
Default Implementations:
String and List<T> implement Eq and Ord with lexicographic comparison:
impl Eq for String { ... } // byte-by-byte equality
impl Ord for String { ... } // lexicographic ordering
// Usage
let a = "apple";
let b = "banana";
if a < b { ... } // true
Default Trait
See WEP: Default Trait.
The prelude defines a Default trait providing a uniform "zero value" / "empty value" interface:
pub trait Default {
fn default() -> Self;
}
Standard Library Implementations:
| Type | default() |
|---|---|
i8, i16, i32, i64, u8, u16, u32, u64, i128, u128 |
0 |
f32, f64 |
0.0 |
bool |
false |
char |
'\0' |
String |
"" |
List<T> |
[] |
Option<T> |
null |
TreeMap<K, V> |
{} |
Result<T, E> does not implement Default — there is no obvious choice between Ok and Err.
Usage:
let n = i32::default(); // 0
let s = String::default(); // ""
fn make_default<T: Default>() -> T { return T::default(); }
let x = make_default::<i32>(); // 0
let arr = make_default::<List<String>>(); // []
Auto-Derivation:
Default is auto-derived for a non-generic struct when every field has a declared default expression (f: T = expr), synthesized on demand where a S::default() call, a T: Default bound, or an impl Default for S; marker needs it — not for every eligible struct. See Struct Field Defaults. A user-written impl Default for S overrides the auto-derived one. Generic structs require an explicit impl.
struct Config {
host: String = "localhost",
port: i32 = 8080,
}
let c = Config::default(); // Config { host: "localhost", port: 8080 }
For other types, the user writes the impl manually:
struct Point { x: i32, y: i32 }
impl Default for Point {
fn default() -> Point { return Point { x: 0, y: 0 }; }
}
String Parsing Traits
Two prelude traits parse a value from a String, both returning Result. FromStr is strict; LenientFromStr is forgiving of human input. The built-in scalars (String, char, the integer types, f32/f64, bool) implement both.
i32::from_str(&"42") // Ok(42)
i32::from_str(&"0x2A") // Err — strict rejects the prefix
i32::from_str_lenient(&"0x2A") // Ok(42) — radix prefixes 0x/0o/0b
i32::from_str_lenient(&"1_000") // Ok(1000) — `_` digit separators
bool::from_str_lenient(&"TRUE") // Ok(true) — casing, plus 1/0
f64::from_str_lenient(&"inf") // Ok(f64::INFINITY)
i32::from_str_lenient(&" 1 ") // Err — never trims whitespace
FromStr's fundamental operation is from_str_range(s, start, end) (parse a byte range with no substring allocation); from_str defaults to calling it over the whole string. See WEP: Lenient String Parsing.
Indexing Traits
The prelude defines traits for index-based access:
IndexValue - Value Read:
/// Returns element by value (copy)
pub trait IndexValue<IndexType> {
type Output;
fn index_value(&self, index: IndexType) -> Self::Output;
}
IndexAssign - Value Write:
/// Assigns value to element at index
pub trait IndexAssign<IndexType> {
type Input;
fn index_assign(&mut self, index: IndexType, value: Self::Input);
}
Index - Reference Read:
/// Returns element by reference (for reference-type elements only)
pub trait Index<IndexType> {
type Output;
fn index(&self, index: IndexType) -> &Self::Output;
}
Design Note: IndexValue returns by value because Wasm GC's array.get instruction copies elements. For primitives like i32, you cannot get &i32 from an array element. Index is for containers of reference-type elements where returning a reference is possible.
List<T> implements IndexValue and IndexAssign:
let mut arr: List<i32> = [1, 2, 3];
let x = arr[0]; // IndexValue::index_value
arr[1] = 100; // IndexAssign::index_assign
Enums, Variants, and Flags
Wado follows Component Model's distinction between enums and variants (unlike Rust):
Enums (no payloads - Component Model enum):
// Simple enumeration - all cases have no data
enum Color {
Red,
Green,
Blue,
}
// Construction
let c = Color::Red;
// Pattern matching: match, if let, matches
let name = match c {
Red => "red",
Green => "green",
Blue => "blue",
};
if let Red = c { /* ... */ }
if c matches { Green } { /* ... */ }
// Match with wildcards and guards
match c {
Red => "warm",
_ => "other",
}
Enums auto-derive Display as the bare case name (Red), distinct from Inspect's Color::Red. Eq (discriminant equality) and Ord (declaration order) derive the same on-demand way as for structs — see Auto-derived Traits above.
Enums can have impl blocks:
impl Color {
fn is_warm(&self) -> bool {
return match *self {
Red => true,
_ => false,
};
}
}
Variants (with payloads - Component Model variant):
Wado variants have exactly one payload type per case. Unit cases have no payload, and multiple values require explicit tuple syntax [T, U]:
// Sum type where variants can carry data
variant Shape {
Circle(f64), // single payload (radius)
Rectangle([f64, f64]), // explicit tuple payload (width, height)
Point, // no payload (unit)
}
// Generic variant
variant Maybe<T> {
Just(T),
Nothing,
}
// Construction
let s = Shape::Circle(5.0);
let r = Shape::Rectangle([10.0, 20.0]);
let p = Shape::Point;
// Option construction — type inferred from payload (forward inference)
let opt = Option::Some(42); // Option<i32> inferred
let opt_str = Option::Some("hello"); // Option<String> inferred
// Option construction — type inferred from annotation (backward inference)
let none: Option<i32> = Option::None; // T=i32 from annotation
// Result construction — combined forward and backward inference
let ok: Result<i32, String> = Result::Ok(42); // T from payload, E from annotation
let err: Result<i32, String> = Result::Err("fail"); // E from payload, T from annotation
// Explicit turbofish syntax (always available)
let opt2 = Option::<i32>::Some(42);
if let Some(x) = opt {
println(`Got: {x}`);
}
// Custom variant pattern matching with tuple destructuring
// Note: pattern uses case name only, not Type::CaseName
variant ParseResult {
Fail,
Number([i32, i32]), // start, end positions
}
let result = ParseResult::Number([0, 10]);
if let Number([start, end]) = result {
println(`Got number from {start} to {end}`);
}
if let Fail = result {
println("Failed");
}
match s {
Circle(r) => calculate_circle_area(r),
Rectangle([w, h]) => w * h,
Point => 0.0,
}
Implementation Status:
- Variant declarations and construction: implemented
- Generic variant type inference (forward from payload, backward from annotation): implemented
if letpattern matching forOption<T>: implementedif letpattern matching for non-generic custom variants: implemented- Tuple payload pattern destructuring (
if let Foo([a, b]) = x): implemented matchexpression/statement: implementedmatchesoperator: implemented- Match ergonomics (
&Tscrutinees inif let/match/matches; payload bindings become refs): implemented - Nested sub-patterns in tuple/struct destructuring (literal, variant, enum): implemented
- Generic custom variant pattern matching (e.g.,
Maybe<T>): not yet implemented Result<T, E>pattern matching: not yet implemented
Note: Option<T> and Result<T, E> are declared as variants in core:prelude.
Flags (bit flags - Component Model flags):
// Bit flags - each member is a power-of-two bitmask
pub flags Perms {
Read, // bit 0 → value 1
Write, // bit 1 → value 2
Execute, // bit 2 → value 4
}
// Access members
let r = Perms::Read; // 1
let w = Perms::Write; // 2
// Bitwise combination with |
let rw = r | w; // 3
// Bitwise AND for masking
let masked = rw & Perms::Read; // 1 (Read bit is set)
// Bitwise XOR for toggling
let toggled = rw ^ Perms::Read; // 2 (Read bit cleared)
// Special static methods
let none = Perms::none(); // 0 (no bits set)
let all = Perms::all(); // 7 (all bits set)
// Cast to u32 for numeric comparison
assert rw as u32 == 3;
// Arithmetic operators (+, -, *, /, %) are NOT allowed on flags types
// They produce a compile error; use bitwise operators (|, &, ^) instead
Flags are implemented as newtypes over u32. Member names can carry #[cm("...")] attributes for Component Model name mapping:
pub flags PathFlags {
#[cm("symlink-follow")]
SymlinkFollow,
}
Note: Wado's enum maps to Component Model's enum (simple enumeration), and variant maps to Component Model's variant (tagged union with payloads). This differs from Rust where enum can have payloads.
Object Literals
Object literal syntax supports unquoted keys and shorthand properties.
For struct initialization syntax, see the Structs section.
TreeMap (Insertion-Order Map)
For associative arrays, use TreeMap from core:collections:
use { TreeMap } from "core:collections";
let mut map = TreeMap::<String, i32>::new();
map.insert("x", 10);
map.insert("y", 20);
// Index syntax
map["z"] = 30; // assignment
let v = map["x"]; // panics if key not found
let opt = map.get("x"); // returns Option<V>
// Keys preserve insertion order
let keys = map.keys(); // returns List<K> in insertion order
// Functional-update spread: seed from a base map, then override/add keys
let m2: TreeMap<String, i32> = { ..map, "x": 99, "w": 40 };
A { ..base, "k": v } key-value literal seeds the builder with every entry of
base (same map type) and then applies the explicit keys, so explicit keys
override the base. Like the struct form, the spread is leading and single. See
WEP: Literal Spread.
Access Methods
// Struct: dot notation
user.name
// TreeMap: bracket notation or methods
map["key"] // panics if key not found
map.get("key") // returns Option<V>
Serialization and Deserialization
Wado provides a format-agnostic serialization framework via core:serde and a JSON implementation via core:json. See WEP: Serialization and Deserialization.
Compiler-Synthesized impl
The syntax impl Trait for Type; (semicolon instead of block) signals that the compiler generates the method body. Supported traits: From, Serialize, Deserialize, Eq, Ord, Default, and the debug/alternate format traits (Inspect, InspectAlt, DisplayAlt). For the structurally-checkable traits (Eq / Ord / Default / serde) the marker is also a conformance check — a compile error at its own span if Type is ineligible. An Inspect / InspectAlt / DisplayAlt marker always validates. A Display marker (impl Display for Type;) is rejected — Display is not derivable for an arbitrary type; write a real impl Display { fn fmt … }, or rely on the automatic enum / newtype Display.
use { Serialize, Deserialize } from "core:serde";
struct User {
name: String,
age: i32,
}
impl Serialize for User; // compiler generates serialize method
impl Deserialize for User; // compiler generates deserialize method
The compiler inspects the type definition (struct, enum, variant, or flags) and synthesizes the appropriate method body. This is a compile error if a field or case's type doesn't implement the required trait.
Struct field names are serialized verbatim by default (identity); see Serialization Names for rename / rename_all overrides.
Bound-Driven Serialize / Deserialize
The marker above is optional: a T: Serialize bound is satisfied structurally once every field or case of T satisfies the trait — the same on-demand model Eq / Ord use (below). This is how an anonymous struct, which has no name for a marker, becomes serializable:
use { to_string } from "core:json";
struct Point { x: i32, y: i32 } // no impl marker needed
let json = to_string(&Point { x: 1, y: 2 }); // Ok("{\"x\":1,\"y\":2}")
let anon = to_string(&{ x: 1, y: 2 }); // Ok("{\"x\":1,\"y\":2}") — anonymous struct
An anonymous literal may also compose spread bases: { ..a, ..b, field: v }
builds an anonymous struct whose fields are the union of the bases' and explicit
fields, in source order, last contributor winning on a name collision (and its
type). Each base is a struct value, evaluated once. Unlike a named struct's
leading-single ..base, composition allows spreads in any position and more than
one; a member every one of whose fields is overwritten by a later member is a
dead-write error. See WEP: Literal Spread.
let base = { user_id: 1, ip: "10.0.0.1" };
let event = { ..base, level: "warn" }; // { user_id, ip, level } — auto-Serialize
The explicit marker impl Serialize for T; still works — write it to force the impl with no bound present, or to attach #[serde(rename_all = "...")] customization. Like Eq / Ord's marker (below), it is a conformance check: an ineligible field or case is a compile error at the marker's own span. See WEP: Trait Derivation Policy.
Bound-Driven Eq / Ord
Eq / Ord derive the same on-demand way: the impl for T is synthesized only where a == / < call site, a bound, or an explicit marker needs it — not for every declared type. No existing call site's behavior changes.
The explicit marker impl Eq for T; / impl Ord for T; is a hard guarantee, not just a request: a compile error, with a reason chain, at the marker's own span if any field or case is ineligible:
struct Handler { cb: fn(i32) -> i32 }
impl Eq for Handler;
// compile error: cannot derive `Eq` for `Handler`: not every field/case implements `Eq`
Format Traits
{x:?} / {x:#?} (Inspect / InspectAlt) work for every type — no bound needed.
{x} (Display) uses the type's impl Display. Primitives, String, plain enums (bare case name), and newtypes (inherited from the base type) have one; a struct, variant, or generic container needs a hand-written impl Display, otherwise {x} is a compile error and {x:?} gives its debug form. So T: Display certifies a real string representation — e.g. String::push_display takes any Display. {x:#} (DisplayAlt) follows Display.
fn describe<T>(v: &T) -> String { return `{v:?}`; } // any type
fn label<T: Display>(v: &T) -> String { return `{v}`; } // requires a `Display`
See WEP: Trait Derivation Policy.
JSON Module (core:json)
use { to_string, from_string } from "core:json";
// Serialize to JSON string
let json = to_string::<User>(&user); // Result<String, SerializeError>
// Deserialize from JSON string
let user = from_string::<User>(json); // Result<User, DeserializeError>
JSON serialization returns Err for NaN and Infinity float values. JSON deserialization returns Err for malformed input, missing required fields, type mismatches, and trailing data.
JSON NSD Module (core:json_nsd)
Non-self-describing JSON format. Structs are encoded as positional arrays (field names omitted), unit variants as discriminant integers, and payload variants as [disc, payload].
use { to_string, from_string } from "core:json_nsd";
// Struct as positional array
let json = to_string::<User>(&user); // Result: Ok("[\"Alice\",30]")
// Deserialize from positional array
let user = from_string::<User>(`["Alice",30]`); // Result<User, DeserializeError>
The same Serialize and Deserialize trait impls work with both core:json and core:json_nsd.
Command-Line Arguments (core:args)
core:args is a non-self-describing, parse-only Deserializer over argv. Argument types are ordinary structs with impl Deserialize for T;: fields become --long options, and fields marked #[serde(positional)] are filled from bare tokens in declaration order (required, optional, or variadic). Scalar tokens are converted with LenientFromStr. See WEP: Command-Line Argument Parsing.
use { parse } from "core:args";
struct Cli {
#[serde(positional)] input: String,
jobs: i32 = 1,
verbose: bool = false,
}
impl Deserialize for Cli;
let cli = parse::<Cli>(["in.txt", "--jobs", "4", "--verbose"]);
Module System
Wado uses an ESM-like import syntax with use {...} from "module". This aligns with JavaScript/TypeScript conventions, as JavaScript is a primary host environment for Wado.
Visibility
Visibility has two orthogonal axes: a Wado scope ladder (internal / pub)
and a CM-surface flag (export). See WEP: Visibility — internal / pub /
export.
| Keyword | Axis | Reach |
|---|---|---|
| (none) | scope | The defining file (private) |
internal |
scope | Other files in the same package |
pub |
scope | Other Wado packages — the library API |
export |
CM flag | Also lowered at the CM boundary; CM-representable |
pub is the library boundary (Wado-native, so generics, closures, and traits
may cross it). export is the Component Model boundary and is additive:
export ⟹ pub, and an exported signature must be CM-representable, checked at
the definition site.
// Private to this file (default)
fn helper() { ... }
// Package-internal - accessible from other files in this package
internal fn build_ast() -> Doc { ... }
// Library API - accessible from other Wado packages (Wado-native)
pub fn map<T, U>(f: fn(T) -> U, xs: List<T>) -> List<U> { ... }
// Library API + CM boundary export
export fn run() { ... }
| Declaration | Same file | Same package | Other Wado packages | CM boundary |
|---|---|---|---|---|
fn foo() |
Yes | No | No | No |
internal fn foo() |
Yes | Yes | No | No |
pub fn foo() |
Yes | Yes | Yes | No |
export fn foo() |
Yes | Yes | Yes | Yes |
A pub-only item reaches Wado consumers only (source dependency or
provider-tagged .wasm); a non-Wado CM consumer sees export items only.
The ladder applies to top-level items. Members of an impl block (methods,
associated constants) accept the same file-private / internal / pub ladder;
export on a member is a compile error (a method has no Component Model
boundary). Member visibility is not yet enforced at call sites, so internal on
a member currently records intent (package scope) rather than restricting
access. A struct field accepts pub or internal (only pub widens access
beyond the defining module; internal currently behaves like file-private).
Re-export visibility
A use declaration carrying a visibility modifier re-exports the imported names
as members of the importing module, at the modifier's reach:
| Form | Re-exported reach |
|---|---|
pub use { x } from "M" |
x joins this module's public API |
internal use { x } from "M" |
x is re-exported package-internal |
use { x } from "M" |
file-private import; x is not re-exported |
A re-export's reach is the re-export keyword's, not x's own visibility. So
a pub use publishes x even when x is internal in its defining module —
the "internal implementation, public facade" pattern, where a package's entry
module re-exports its internal submodules' items as the library API:
// foo/impl.wado — package-internal implementation
internal fn compute() -> i32 { ... }
// foo.wado — the package's public entry module
pub use { compute } from "./impl.wado"; // compute is now the library API
You may only re-export a name you can see: pub use { x } from "M" requires x
to be importable here (x is pub, or x is internal and M is in this
package). Re-exporting a file-private name is a visibility error, like any other
import. See Re-export Syntax (pub use).
Module Source Types
| Source Type | Syntax | Example |
|---|---|---|
| WASI standard | "wasi:<package>" |
"wasi:cli", "wasi:filesystem" |
| Core library | "core:<module>" |
"core:cli", "core:json" |
| CM coordinate | "<ns>:<pkg>[@<ver>]" |
"docs:regex", "docs:regex@1.0.0" |
| Library alias | "lib:<nick>" |
"lib:router", "lib:shared" |
| Remote (HTTP) | "https://..." |
"https://example.com/lib.wado" |
| Local file | "./<path>" or "../<path>" |
"./utils.wado", "../config.wado" |
A specifier names a package only — no interface segment; interfaces and members are selected in the use { ... } list. wasi:/core: are bundled coordinates, not a separate scheme. See WEP: Package and Module Specifier Syntax.
Module Path Validation
Relative paths in Wado follow the gitignore / shell convention: a path that refers to a file relative to the current file must begin with ./ (next to me) or ../ (up one). A bare path (foo/bar, utils.wado) is never relative-to-here — it is read as a namespace/coordinate or handed to the host, and is rejected wherever only a relative file path is valid. This rule is uniform across every path literal: module imports (use ... from), #include_str / #include_bytes, and Kiln schema paths (from, generator.inputs, generator.output_dir).
Module paths are validated before loading to provide clear error messages:
Namespace Resolution (a namespace is reserved iff the compiler bundles it):
-
Bundled namespaces
core:/wasi:— resolved from embedded stdlib. -
Open coordinates
<ns>:<pkg>(any other namespace) — resolved from the default registry, or awith/manifest source override. -
Library aliases
lib:<nick>— indirection (rename, short name, multiple majors, or a dependency with no public coordinate), resolved viawado.tomlor an inlinewith. -
Remote modules (
http://orhttps://): Delegated to CompilerHost. -
Local modules (
./or../): Resolved relative to importing module. -
Invalid paths: Paths not matching any pattern are rejected.
- Error:
invalid module path 'xxx'; use './' for local modules or a 'namespace:package' coordinate
- Error:
Bare names ("router") are rejected. See WEP: Package and Module Specifier Syntax for resolution and version rules.
Symbol Notation
A symbol is named MODULE#SYMBOL — the written form used by docs, wado query, and diagnostics. MODULE is the import specifier verbatim (quoted as in use; quotes may be dropped for a scheme or bare name with no whitespace). SYMBOL uses Wado's own operators, so its kind is visible from the separator: :: for static scope, . for an instance method, ^ for a trait-impl member.
core:json#parse # free function / global
core:math#f64::PI # associated const / static fn
core:collections#TreeMap.insert # instance method
core:collections#List<String>::len # generics use Wado angle brackets
core:fmt#Point^Display::fmt # trait-impl member
"./utils.wado"#Helper::new # relative path — must be quoted
See WEP: Symbol Notation.
Import Syntax
// ============================================
// WIT Package = Wado Module
// WIT Interface = Wado interface
// ============================================
// 1. WASI standard modules (wasi:*)
use {Stdout, Stderr} from "wasi:cli";
use {Stdout::{write_via_stream}} from "wasi:cli";
// Interface and its members together
use {Stdout, Stdout::{write_via_stream}} from "wasi:cli";
// 2. Core library (core:*)
use {println, eprintln} from "core:cli";
use {format} from "core:fmt";
// 3. Remote modules (https:)
use {ApiClient} from "https://example.com/api.wado";
// 4. Local files (relative path, extension required)
use {Helper} from "./utils.wado";
use {Config} from "../config.wado";
// 5. CM coordinate (resolved via wado.toml or default registry)
use {Regexp} from "docs:regex";
// 6. Library alias (rename / private / coordinate-less dependency)
use {Router} from "lib:router";
Import Attributes (with)
Use with { ... } to specify import metadata:
// Inline dependency source (single-file scripts; no wado.toml needed).
// Same vocabulary as a [dependencies] value, with an exact version.
use {Regexp} from "docs:regex@1.0.0"; // exact pin via the specifier
use {Router} from "lib:router" with { git = "https://github.com/user/router.git", ref = "v1.0" };
use {Parse} from "lib:rx" with { registry = "oci://ghcr.io/acme", package = "docs:regex", version = "1.0.0" };
// Type attribute (REQUIRED for non-.wado imports)
use {sin, cos} from "./libm.wasm" with { type: "wasm" };
// WIT specification for Wasm imports (optional)
use {foo} from "./external.wasm" with {
type: "wasm",
wit: "./external.wit",
};
An inline with source and a wado.toml entry for the same specifier are mutually exclusive. Version ranges (^/~/=) are allowed only in wado.toml, where a lock file resolves them; the specifier @ver and a single-file with take an exact version — a range there is an error.
Type Attribute Requirement:
| Import Source | type Attribute |
Notes |
|---|---|---|
.wado files |
Optional | Type inferred from Wado source |
.wasm files |
Required | type: "wasm" |
core:*, wasi:* |
Not applicable | Bundled namespace handling |
https: URLs |
Required for non-.wado |
Must specify content type |
CM / lib: deps |
Optional | Type inferred from package |
Rationale: Explicit type annotations prevent ambiguity and make dependencies clear, aligning with Wado's design philosophy of explicit imports.
Schema Imports (Kiln)
A use clause whose source is a non-.wado, non-.wasm schema file (e.g. .g4, .proto, .graphql, .wit) is processed by Kiln — a schema-driven code-generation pipeline that lowers the schema to ordinary Wado source which the compiler then handles like any user-authored module. The with { generator: { ... } } clause specifies which generator to invoke:
// Gale generates a parser from an ANTLR4 grammar
use { Parser } from "./Calc.g4" with {
generator: {
module: "wado:gale@0.1",
},
};
// With supplementary input files (paths relative to the source file)
use { RustParser } from "./Rust.g4" with {
generator: {
module: "wado:gale@0.1",
inputs: ["./RustLexer.g4"],
},
};
with { generator: { ... } } fields:
| Field | Required | Meaning |
|---|---|---|
module |
yes | Generator module — either a <namespace>:<name>[@<version>] reference resolved against [build-dependencies], or a relative ./ path. |
options |
no | Record literal whose shape matches the generator's exported pub struct Options. Omit when every field has a default. |
inputs |
no | Supplementary schema paths the generator cannot discover from the primary alone (e.g. a sibling lexer grammar). |
output_dir |
no | Override for the per-invocation generated-source directory (default build/kiln/<synthesized-id>/). |
Manifest:
Generators are declared in [build-dependencies] of wado.toml (a build-only graph that does not enter the consuming project's runtime dependency graph):
[build-dependencies]
gale = { registry = "wado", package = "gale", version = "0.1" }
A bare use { ... } from "./schema.g4" against a non-.wado schema with no with clause is a hard error (Code::KilnMissingWith). Two use clauses for the same from in the same file collapse to a single invocation if their (module, inputs, options, output_dir) match; mismatched clauses are a duplicate-generator error.
Authoring a generator:
A generator is a normal Wado package whose wado.toml declares a [package].generator entry pointing at a module that exports the core:kiln/generator world:
use { Request, Response, Error } from "core:kiln";
pub struct Options {
namespace: String,
}
export fn generate(req: Request<Options>) -> Result<Response, Error> {
// ... parse req.primary.content, emit Wado source ...
}
The compiler extracts the Options shape from the generator's IR and type-checks every call site against it. Generators run in a deterministic sandbox (no clocks, randomness, network, environment, or ambient filesystem); transitive schema files are picked up via a host-provided read-file import that the compiler logs as part of the cache key. Outputs are persisted under build/kiln/<synthesized-id>/ and stamped with a #![generated(by = "...", sources = [...])] header. Subsequent compiles skip the generator when its content-addressed cache key matches wado.lock.
In hosts that cannot execute generators (today's wasm32-bundled LSP / browser playground), Kiln falls back to consume-only mode: the compiler reads cached generated .wado files from disk and emits a stale-cache warning if hashes do not match. Projects that want a full LSP experience in such hosts commit build/kiln/ and wado.lock to their repository.
Wasm Module and Component Imports
A .wasm / .wat asset is imported directly with with { type: "wasm" | "wat" }. The compiler detects from the binary header whether the file is a core module or a Component Model component — both .wasm shapes use type: "wasm"; the distinction is detected, not declared. A single use may pull several names (functions from a core module, interfaces from a component).
| Imported file | Exposes as | Call style |
|---|---|---|
Core wasm module / .wat |
One free pub fn per export |
helper(x) — plain function |
CM component (.wasm) |
One Wado interface per exported CM interface |
Iface::method(x) — effectful, like WASI |
// Core wasm / wat — exports become free functions.
use { sin, cos } from "./libm.wat" with { type: "wat" };
use { helper } from "./mod.wasm" with { type: "wasm" };
// CM component — each exported interface becomes a Wado `interface`,
// and its functions are called like WASI methods (effectful).
use { Compress, Decompress } from "./brotli.wasm" with { type: "wasm" };
export fn run() with Compress, Decompress {
let packed = Compress::compress(bytes);
let back = Decompress::decompress(packed); // Result<List<u8>, String>
}
Values lower/lift across the CM boundary per Type Mapping at Component Boundaries. The dependency component is statically composed into the output, so the result runs standalone. See WEP: Wasm Module Import for the core-wasm path and WEP: Wasm CM Component Import for the component path.
Namespace Import
Use use name from "..." (without curly braces) to import an entire module as a namespace:
// Import a module as a namespace
use utils from "./utils.wado";
utils::helper_function(); // not utils["helper_function"], as it's analyzed at compile time
A namespace import makes all pub symbols from the source module available. At the source level, symbols are accessed with the ns:: prefix. Internally, the compiler desugars the prefix away during the desugar phase and registers all pub symbols from the source module as named imports:
use geo from "./geo.wado";
// Functions
geo::distance(p1, p2); // → distance(p1, p2)
// Types (structs, enums, variants)
let p: geo::Point = geo::Point::origin(); // → Point, Point::origin()
let c = geo::Color::Red; // → Color::Red
let s = geo::Shape::Circle(3.14); // → Shape::Circle(3.14)
Note: Wado does not support use * as name or default imports.
Import Rules
- Named imports use curly braces:
use {x, y} from "..." - Namespace imports omit curly braces:
use name from "..." - Wildcards prohibited:
use {*} from "..."is not allowed - All imports must be explicit (except the prelude)
- Use
::for effect operation access:Effect::{op1, op2}
// Valid patterns
use {println, eprintln} from "core:cli"; // Named import
use {Stdout, Stdout::{write_via_stream}} from "wasi:cli";
use utils from "./utils.wado"; // Namespace import
// Prohibited patterns
use * from "core:cli"; // Wildcard not allowed
use println from "core:cli"; // Named items require curly braces
Calling Effect Operations
Effect operations use :: syntax:
use {Stdout, Stdout::{write_via_stream}} from "wasi:cli";
fn example() with Stdout {
// With import - direct call
write_via_stream(stream);
// Fully qualified - always works
Stdout::write_via_stream(stream);
}
Notation distinction:
.→ struct fields and methods (user.name,stream.read())::→ effect operations and namespace access (Stdout::write_via_stream())
Renaming Imports
use {Stdout::{write_via_stream as stdout_write}} from "wasi:cli";
use {Stderr::{write_via_stream as stderr_write}} from "wasi:cli";
fn log() with Stdout, Stderr {
stdout_write(out_stream);
stderr_write(err_stream);
}
Re-exports (pub use)
Re-exports make imported symbols available to other modules that import from this module:
// math/internal/trig.wado
pub fn sin(x: f64) -> f64 { ... }
pub fn cos(x: f64) -> f64 { ... }
// math/mod.wado - re-export from internal modules
pub use {sin, cos} from "./internal/trig.wado";
pub use {sin as sine} from "./internal/trig.wado"; // with rename
// user code - import from the facade (a "math" dependency declared in wado.toml)
use {sin, cos, sine} from "lib:math";
Re-export rules:
pub usecombinespubvisibility with import syntax- Can only re-export
pubsymbols from the source module - Re-export chains are resolved transparently (A re-exports from B, B re-exports from C)
- Circular re-exports are prohibited
- Wildcards prohibited:
pub use * from "..."is not allowed
Exception: The Prelude
The prelude is automatically imported into every module, making Option, Result, Stream, Future, and Pollable available without explicit imports.
Standard Library
core # core: namespace for the core library
├── prelude # Automatically imported (Option, Result, Stream, Future, Pollable)
├── cli # CLI helpers (println, eprintln, args, env, exit, ...)
├── serde # Serialization traits (Serialize, Deserialize, Serializer, Deserializer)
├── json # JSON format implementation (to_string, from_string)
├── collections # TreeMap
├── base64 # Base64 encoding/decoding
├── zlib # Compression
├── ...
wasi # wasi: namespace for system interfaces
├── cli
├── filesystem
├── ...
For full API documentation, see:
- Core Standard Library — one reference per module:
prelude,cli,collections,serde,json,json_nsd,value,base64,zlib,simd,url,kiln - WASI Standard Library Reference
Global Functions defined in core:prelude
panic("error"); // traps with a message
unreachable(); // traps with no message
The assert Statement
The assert keyword is used to assert that a condition is true. If the condition is false, the program will panic with messages that includes the source of the condition and related intermediate values (like the power-assert).
// If x is not greater than 0, the program will panic, printing x.
assert x > 0;
// Also assert can take an optional message.
assert x > 0, "x must be checked elsewhere";
To keep a failure readable, each captured operand is rendered with Inspect
(:?), which caps sequence types at a default length (DEFAULT_SEQ_LIMIT = 256):
a String operand is truncated to 256 characters with a ... marker and an
List operand to 256 elements (see WEP: Template Format Specifiers).
Non-sequence operands such as floats keep their natural rendering. The optional
user message is formatted with the user's own template specifiers (typically
Display, which is never capped) — opt into a longer dump by formatting the
value yourself.
Testing
Wado has built-in support for writing and running tests. Test declarations are first-class syntax, and the wado test command provides a test runner similar to cargo test or moon test.
Test Declaration Syntax
Tests are declared using the test keyword followed by an optional name and a block:
// Named test
test "addition works" {
assert 1 + 1 == 2;
}
// Unnamed test (identified by file:line)
test {
let result = compute_something();
assert result > 0;
}
// Test with multiple assertions
test "string operations" {
let s = "hello";
assert s.len() == 5;
assert s + " world" == "hello world";
}
// Expect-trap test: the test passes when the body traps
#[expect_trap]
test "panics on bad input" {
panic("intentional panic");
}
#[expect_trap]
test {
unreachable();
}
// TODO test: marks a test for an unimplemented feature.
// Reported on a separate axis from pass/fail (see Test Outcome Model).
#[TODO]
test "not yet implemented" {
panic("TODO: implement this feature");
}
// Custom timeout: override the default 1000ms limit
#[timeout_ms(5000)]
test "slow computation" {
let result = expensive_computation();
assert result == 42;
}
// Synopsis test: runs like any test; `wado doc` renders its body as the
// module's `## Synopsis` section.
#[synopsis]
test {
let p = Point { x: 3, y: 4 };
assert p.length() == 5.0;
}
Syntax Rules:
testis a contextual keyword (functions namedtestare still allowed)- Test name is an optional string literal
- Test body is a block containing statements
- No return type or effect declarations needed
- Tests can use any effects (side effects are allowed in tests)
- Attributes (e.g.,
#[expect_trap],#[TODO],#[timeout_ms(N)],#[synopsis]) may appear before thetestkeyword
Test Identification:
- Named tests: identified by their string name
- Unnamed tests: identified by
{filename}:{line_number}
Test Semantics
Execution:
- Each test runs in isolation with fresh state
- Test order is deterministic (declaration order within a file)
- A test passes if it completes without panicking or trapping
- A test fails if
assertfails,panicis called, or a trap occurs
#[expect_trap] Attribute:
The #[expect_trap] attribute inverts the pass/fail condition for a test:
- The test passes if the body traps (calls
panic,unreachable, or fails anassert) - The test fails if the body completes normally without trapping
This is useful for verifying that invalid operations are correctly rejected at runtime:
#[expect_trap]
test "panics on null dereference" {
let opt: Option<i32> = null;
// force a trap by accessing None without checking
panic("expected None but got value");
}
#[TODO] Attribute:
The #[TODO] attribute marks a test as a placeholder for a feature not yet implemented. TODO tests are reported on a separate axis from regular pass/fail results (see Test Outcome Model below). When the body traps, the test is reported as pending (expected). When the body completes normally, the test is reported as resolved, which is a hard failure requiring the developer to remove the #[TODO] attribute.
#[timeout_ms(N)] Attribute:
The #[timeout_ms(N)] attribute overrides the default test timeout (1000ms) for a specific test. N is an integer literal specifying the timeout in milliseconds. If a test exceeds its timeout, it is interrupted and reported as failed with a message suggesting the #[timeout_ms(N)] attribute. This is useful for tests that involve expensive computation or I/O:
#[timeout_ms(5000)]
test "large data processing" {
let result = process_large_dataset();
assert result.len() > 0;
}
Test Outcome Model
Test results are classified into two independent axes: the pass/fail axis for regular tests, and the TODO axis for tests marked with #[TODO].
Regular Tests
| Condition | Outcome |
|---|---|
| Body completes normally | pass |
| Body traps (panic, assert failure, unreachable) | fail |
Body traps and #[expect_trap] is present |
pass |
Body completes normally and #[expect_trap] is present |
fail |
TODO Tests
Tests marked with #[TODO] are reported separately from regular tests. They do not contribute to the pass or fail count.
| Condition | Outcome | Action Required |
|---|---|---|
| Body traps | todo (pending) | None — the feature is still unimplemented |
| Body completes normally | todo (resolved) | Remove #[TODO] — the feature now works |
A resolved TODO test is a hard failure (exit code 1). This enforces cleanup: once the underlying feature is implemented, the #[TODO] attribute must be removed so the test joins the regular pass/fail pool.
A pending TODO test never causes a failure. This means fixing a compiler bug cannot increase the failure count — newly-passing TODO tests appear as "resolved" on the TODO axis rather than as unexpected failures on the pass/fail axis.
#![TODO] Modules
The #![TODO] inner attribute applies TODO semantics to an entire module:
- If the module fails to compile, it is reported as a single pending TODO entry.
- If the module compiles successfully, each test block is implicitly treated as
#[TODO]. - If the module compiles and all tests pass (i.e., the feature is implemented), it is reported as resolved — a hard failure.
Output Format
The test runner displays results grouped by file. Regular tests use ✓ (green) and ✗ (red). TODO tests use · (yellow, pending) and ✓ (cyan, resolved):
Running tests in math_test.wado... (compiled in 50ms)
✓ addition (2ms)
✗ division_edge_case (3ms)
assertion failed at line 15
· future_feature # TODO (1ms)
✓ now_works # TODO resolved (2ms)
remove the #[TODO] attribute
At the end of the run, a TODO summary section lists all TODO tests with their status:
TODO tests (3):
· pending math_test.wado — future_feature
✓ resolved math_test.wado — now_works
· pending unimpl_test.wado — #![TODO] module
1 TODO test(s) resolved — remove the #[TODO] attribute
The final summary line reports both axes:
N passed, N failed; N todo (M resolved) (duration)
Exit Codes
| Condition | Exit Code |
|---|---|
| All regular tests pass, no resolved TODOs | 0 |
| One or more regular tests fail | 1 |
| One or more TODO tests resolved | 1 |
Effects:
- Tests implicitly have access to all effects (no
withdeclaration required) - This allows tests to perform I/O, use the filesystem, etc.
No Runtime Overhead:
- Test functions are only included when running
wado test - Regular compilation (
wado compile,wado run) excludes test code via dead code elimination
Test Runner CLI
The wado test command discovers and runs tests:
# Auto-discover every *.wado file under the project root
wado test
# Run tests in specific file(s)
wado test path/to/file.wado
wado test path # walk path with the discovery rules
# Filter discovered files by path (shell wildcard, not regex)
wado test --filter '*addition*'
wado test -f '*string*'
# Show help
wado test --help
Discovery (WEP 2026-05-02):
When no files are specified, wado test walks the project root for every
*.wado file. The walker honours .gitignore, .gitmodules, dot-prefixed
paths, nested wado.toml package boundaries, and the root manifest's
[test].exclude glob list. Symbolic links are followed once each, with
canonical-path cycle detection. Sub-packages (any directory containing its
own wado.toml) are run as separate package contexts and reported with a
=== package: <label> === banner; an === aggregate === block sums the
three axes when more than one package ran.
Files without test blocks are still parsed and compiled; only files with
test blocks register and run tests. Compile failures are tracked on a
separate axis from test failures and produce a non-zero exit.
Filtering:
--filter <pattern> matches discovered file paths using shell wildcards
(*, ?, [...]); regex syntax is not supported. Wrap the term in *s
to match anywhere within a path (e.g. '*foo*').
Output and Exit Codes:
See Test Outcome Model above for the full output format, TODO summary, and
exit code rules. The summary prints three axes — compile, test, todo — and
the run exits non-zero whenever any of compile failed, test failed, or
todo resolved is non-zero.
Test File Conventions
*_test.wado is the recommended convention for files that contain only
tests, but the runner no longer requires the suffix: every *.wado file
discovered under the project root is visited, and any file with test
blocks contributes its tests.
src/
math.wado
math_test.wado # Tests for math.wado (recommended convention)
string.wado
string_test.wado # Tests for string.wado
Tests can also be placed in a separate tests/ directory:
src/
lib.wado
tests/
integration_test.wado
Example Test File
// math_test.wado
use {add, multiply} from "./math.wado";
test "add positive numbers" {
assert add(2, 3) == 5;
assert add(0, 0) == 0;
}
test "add negative numbers" {
assert add(-1, -1) == -2;
assert add(-5, 3) == -2;
}
Concurrency Model
Stack Switching Based (Colorless)
// No async keyword needed in function implementations
fn fetch_user(id: i32) -> Result<User, HttpError> with Http {
let response = Http::get(`users/{id}`)?; // Even if Http::get is async in WIT
let user = response.json()?;
return Ok(user);
}
// Called normally
fn main() with Http {
let user = fetch_user(1);
}
// Concurrent execution
fn load_data() -> Data with Http {
let [users, posts] = join(
|| fetch_users(),
|| fetch_posts(),
);
return Data { users, posts };
}
Note: Wado is fully colorless — the async keyword only appears in world declarations (the Component Model surface) to match WIT's async func signatures for exports. Effect declarations and function implementations never use async.
Effect System
Design Philosophy
The Effect System is equivalent to:
- Tracking access to external resources / global variables
- Implicitly propagating DI (Dependency Injection)
- Direct correspondence with WASI Capabilities
Effect Definition
Effects can be defined in two ways:
1. Effect interfaces (declaring operations as free functions):
// WASI CLI effects (see wasi:cli for real definitions)
interface Stdout {
fn write_via_stream(data: Stream<u8>) -> Result<(), ErrorCode>;
}
interface Stderr {
fn write_via_stream(data: Stream<u8>) -> Result<(), ErrorCode>;
}
interface Environment {
fn get_environment() -> List<[String, String]>;
fn get_arguments() -> List<String>;
fn get_initial_cwd() -> Option<String>;
}
// Custom effect interfaces
interface Http {
fn get(url: String) -> Response;
fn post(url: String, body: String) -> Response;
}
interface Dom {
fn query(selector: String) -> Option<Element>;
fn create_element(tag: String) -> Element;
}
Colorless Async:
- Effect declarations never use the
asynckeyword—Wado is fully colorless - The
asynckeyword only appears in world export declarations (the Component Model surface) - WIT's
async funcis handled transparently via Wasm Stack Switching at runtime
2. Methods with effect requirements:
// Methods can declare required effects
impl TcpStream {
fn read(&mut self, buffer: &mut List<u8>) -> Result<i32, IoError> with Network;
fn write(&mut self, data: &List<u8>) -> Result<i32, IoError> with Network;
fn close(&mut self) with Network;
}
impl TcpListener {
fn accept(&self) -> Result<TcpStream, IoError> with Network;
}
// Free functions can also require effects
fn listen(addr: String) -> Result<TcpListener, IoError> with Network;
This approach makes effect requirements explicit and visible in method signatures, maintaining consistency with the language's design philosophy of being clear and explicit.
Effect Declaration in Functions
// Declare required effects with `with`
fn greet(name: String) with Stdout {
Stdout::write_via_stream(to_stream(`Hello, {name}!\n`));
}
// Multiple effects
fn show_env() with Stdout, Environment {
let args = Environment::get_arguments();
Stdout::write_via_stream(to_stream(`Arguments: {args}\n`));
}
// No effects = pure function
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
Importing Effect Operations
To avoid the verbosity of Effect::operation() calls, you can explicitly import effect operations:
// Import effect operations
use {Stdout::{write_via_stream}} from "wasi:cli";
use {Environment::{get_environment, get_arguments}} from "wasi:cli";
pub fn println(message: String) with Stdout {
// Create stream, start consumer, write data, close stream
// (simplified - see core:cli for full implementation)
write_via_stream(...);
}
pub fn env(name: String) -> Option<String> with Environment {
let vars = get_environment(); // No need for Environment:: prefix
for let [key, value] of vars {
if key == name {
return Some(value);
}
}
return None;
}
Import Rules:
- Effect operations use
::syntax:use {Effect::{op1, op2}} from "..." - Multiple operations can be imported:
Effect::{op1, op2, op3} - Renaming is supported:
use {op as renamed} from "..." - Wildcards are prohibited:
use {Effect::{*}}is not allowed - The
withdeclaration is still required for effect tracking
Name Resolution:
- Imported effect operations can be called directly without the
Effect::prefix - If an operation name is ambiguous, use the fully qualified
Effect::operation()syntax - Non-imported effect operations must always use the
Effect::operation()syntax
// Example with name collision handling
use {Stdout::{write_via_stream}} from "wasi:cli";
use {Stderr::{write_via_stream as stderr_write}} from "wasi:cli";
pub fn log(message: String) with Stdout, Stderr {
write_via_stream(...); // Calls Stdout::write_via_stream
stderr_write(...); // Calls Stderr::write_via_stream (renamed)
}
Effect Propagation
- Local functions: Inferred
- pub functions: Must be explicit
// Local functions are inferred
fn internal() {
callee(); // Automatically inherits callee's effects
}
// Public functions must be explicit
pub fn api_function() with Http, FileSystem {
// ...
}
Generic Effects (Effect Polymorphism)
Use <effect E> to declare a generic effect parameter. E can represent zero or more concrete effects, inferred from function-typed arguments at each call site.
fn wrapper<effect E>(f: fn() with E) with E {
f();
}
fn map<T, U, effect E>(arr: List<T>, f: fn(T) -> U with E) -> List<U> with E {
// ...
}
Effect parameters:
- Are declared with the
effectkeyword in generic parameter lists - At most one effect parameter is allowed per function
- Do not participate in monomorphization (effects are erased at runtime)
- Are inferred from the effects of function-typed arguments at each call site; when multiple function-typed arguments reference the same effect parameter,
Eresolves to the union of all their effects - Can coexist with type parameters:
<T, effect E> - Test functions implicitly have all effects
Variadic Type Packs
Use <..T> to declare a type pack parameter that represents zero or more types. Type packs enable writing functions that operate on tuples of any arity.
fn identity<..T>(x: [..T]) -> [..T] {
return x;
}
fn prepend<A, ..T>(a: A, rest: [..T]) -> [A, ..T] {
return [a, ..rest];
}
Type pack parameters:
- Are declared with
..prefix in generic parameter lists:<..T>,<A, ..T> - At most one type pack parameter is allowed per function
- Cannot be combined with
effect:<effect ..T>is invalid - Appear inside tuple types as
[..T](type pack spread) - Can be mixed with fixed type elements:
[A, ..T],[..T, B] - Type arguments are inferred from tuple argument types at call sites
- Expansion happens at monomorphization time
Lexical note: .. is a single token (DotDot). Writing ... (three dots) is a parse error with the diagnostic "unexpected ...; did you mean ..?".
Value Spread
Value spread [..expr] splices a tuple's elements into an enclosing tuple literal:
let a = [1, "hello"];
let b = [..a, true]; // b: [i32, String, bool]
let c = [42, ..a]; // c: [i32, i32, String]
The spread expression is evaluated exactly once. For non-trivial expressions (e.g., function calls), the compiler introduces a temporary binding to avoid duplicate evaluation:
// make_pair() is called once, not twice
let t = [..make_pair(), 30];
Reference Storage (stores[...])
Not yet implemented. See
docs/wep-2026-01-12-value-semantics-and-stores.mdfor the design.
The stores[...] keyword declares that a function stores reference parameters beyond the function call. This enables compile-time escape analysis and automatic heap promotion.
Syntax: with stores[param1, param2, ...]
// Function that stores a reference parameter
fn register(data: &Data) -> Handle with stores[data] {
registry.push(data); // Stores the reference
return new_handle();
}
// Function that does NOT store (no stores declaration)
fn process(data: &Data) -> Result {
return compute(*data); // Uses but doesn't store
}
// Combined with effects
fn store_and_log(data: &Data) -> Handle with Stdout, stores[data] {
println("Storing data...");
return register(data);
}
Naming Rationale: The keyword is stores (not captures) because:
- "Capture" is established terminology for closures (
let f = || x + 1capturesx) storesdescribes what the function does with the reference—it stores it for later use- This avoids conflating two different concepts: closures capturing variables vs functions storing parameters
Functor types can also declare stores (positional: 0 = first parameter):
fn take_storing(f: fn(&Data) with stores[0]) { ... }
fn take_pure(f: fn(&Data) -> Result) { ... } // cannot store
See docs/wep-2026-01-12-value-semantics-and-stores.md for detailed design rationale.
Handlers
A handler is an impl Effect for Type whose methods may use resume value to deliver a value to the suspended caller. The with E => h do { body } block installs h as the handler for effect E for the duration of body. The => arrow reads as a dispatch binding ("calls to E go to h"); it is not an assignment, since each with pushes onto a per-effect handler chain that is restored on exit.
with Stdin => &mut mock do { ... }
with Stdin => &mut s, Stdout => &mut o do { ... }
with &mut bundle do { ... } // bundled (omits effect name)
See docs/wep-2026-01-27-effect-system-design.md for resource-as-effect and effect propagation design, and docs/wep-2026-04-11-effect-handler.md for handler syntax, dispatch lowering, and MockCM.
World System
What is a World?
A world in Wado corresponds directly to the Component Model's world concept. A world defines the contract between a Wasm component and its environment:
- Imports: Which capabilities the component requires (provided by the host or by other components)
- Exports: Which functions and types the component provides
Worlds are classified into two categories:
- Hosted world: A world that a runtime knows how to instantiate and drive. The runtime provides all imports and invokes the exports according to a defined lifecycle. Examples:
wasi:cli/command(executed bywado run),wasi:http/service(executed bywado serve). Informally called a "well-known world." - Library world: A world that defines a component's public API for composition. It is not directly executed by a runtime; instead, other components import its exports. Example: a
jsonlibrary that exports parsing functions.
This distinction is not part of the Component Model specification — the CM treats all worlds uniformly. In Wado, the distinction matters for tooling: wado run and wado serve select a hosted world, while wado.toml's lib field defines a library world.
World Declaration
world WorldName {
import EffectName {
function_name_1,
function_name_2,
}
import AnotherEffect {
function_name_3,
}
// Use async for exports that map to WIT's "async func" (CM surface only)
export async fn exported_function(arg: Type) -> ReturnType;
export fn synchronous_function() -> i32;
}
TBD: Component/Module Structure The relationship between files, modules, and components is still under discussion. The intended design is "1 file = 1 module, 1 component = multiple modules", but the exact syntax for declaring which modules compose a component has not been finalized.
Note: The async keyword only appears in world export declarations to indicate correspondence with WIT's async func. This is the only place async appears in Wado—effect declarations and function implementations are fully colorless.
WASI CLI World Example
The standard WASI CLI command world in Wado syntax:
// Based on wasi:cli@0.3.0-rc-2025-09-16 command world
// Effect definitions are in "core:cli" (see cli.wado)
world Command {
// Standard I/O streams
import Stdout {
write_via_stream,
}
import Stderr {
write_via_stream,
}
import Stdin {
read_via_stream,
}
// Environment access
import Environment {
get_arguments,
get_environment,
get_initial_cwd,
}
// Process control
import Exit {
exit,
exit_with_code,
}
// Terminal interaction (optional)
import TerminalStdin {
get_terminal_stdin,
}
import TerminalStdout {
get_terminal_stdout,
}
import TerminalStderr {
get_terminal_stderr,
}
// Entry point: maps to WIT's "run: async func() -> result"
// async keyword only appears here (world export) - the CM surface
export async fn run() -> Result<(), ()>;
}
// Declare conformance to the Command hosted world
contract Command;
// Implementation — `export` exposes it at the CM boundary
export fn run() with Stdout {
println("Hello, WASI world!");
}
Multiple Worlds
A single codebase can define multiple worlds for different deployment targets:
world BrowserApp {
import Dom {
query_selector,
create_element,
}
export fn mount(root: String);
}
world CliApp {
import Stdout {
write_via_stream,
}
export fn run() -> Result<(), ()>;
}
// Declare conformance — select world at compile time
contract CliApp; // or: contract BrowserApp;
Design Notes
- Explicit function listing: Unlike WIT's
includedirective, Wado requires listing each imported function explicitly for clarity - Effect-based imports: Imports are organized by effect, which maps to WIT interfaces
- Type signatures on exports: Export declarations include full function signatures
- async keyword: Only appears in world export declarations (the Component Model surface); effect declarations and implementations are fully colorless
- Versioning: Version information (
@0.3.0-rc-2025-09-16) is specified in the effect definitions (e.g.,cli.wado), not in the world declaration
Error Handling
Unrecoverable Errors (Wasm Exceptions)
panic("Fatal error"); // Immediate termination
unreachable(); // Unreachable code
assert condition; // Condition check, panic on failure
These cannot be caught in Wado; the program terminates.
Recoverable Errors (Result Type)
fn parse_int(s: String) -> Result<i32, ParseError> {
// ...
}
fn read_config(path: String) -> Result<Config, ConfigError> with FileSystem {
let content = FileSystem::read(path)
.map_err(|e| ConfigError::Io(e))?;
let config = parse_config(content)?;
return Ok(config);
}
// Handle with pattern matching
match result {
Ok(value) => process(value),
Err(e) => handle_error(e),
}
WASI / Browser Support
Wado targets WASI Preview 3 (0.3.0-rc-2025-09-16), which introduces native stream<T> and future<T> types that map directly to Wado's Stream<T> and Future<T>.
All Wado types map directly to Component Model (WIT) types. See the Type Mapping at Component Boundaries table in the Type System section for the complete mapping reference.
WASI P3 CLI Interfaces
Wado effects map to WASI P3 interfaces:
| Wado Effect | WASI Interface | Key Functions |
|---|---|---|
Stdout |
wasi:cli/stdout |
write-via-stream(stream<u8>) |
Stderr |
wasi:cli/stderr |
write-via-stream(stream<u8>) |
Stdin |
wasi:cli/stdin |
read-via-stream() -> tuple<stream<u8>, future<...>> |
Environment |
wasi:cli/environment |
get-arguments(), get-environment() |
Exit |
wasi:cli/exit |
exit(result), exit-with-code(u8) |
Entry Points
Entry points are integrated in World system.
Each hosted world defines its entry point. Currently supported:
| Hosted World | Entry Point | CLI Command |
|---|---|---|
wasi:cli/command |
export fn run() |
wado run |
wasi:http/service |
export async fn handle(request: Request) -> Result<Response, ErrorCode> |
wado serve |
When no explicit contract declaration is present, the runtime determines the expected world (e.g., wado run expects wasi:cli/command).
task return Statement
task return expr; is a statement valid only inside export async fn bodies. It calls the Component Model task.return instruction, delivering the function's result to the CM runtime without terminating the Wasm function. Execution continues after task return, allowing the function to fulfill outstanding futures (e.g. trailers) or perform cleanup.
Motivation
HTTP handlers return a Response that contains a Future-based trailers channel. With a regular return, the Wasm function exits immediately, making it impossible to write to that channel. task return separates result delivery from function termination:
export async fn handle(request: Request) -> Result<Response, ErrorCode> {
let [trailers_future, trailers_tx] = Future::<Result<Option<Trailers>, ErrorCode>>::new();
let headers = Fields::new();
let [response, _tx_future] = Response::new(headers, null, trailers_future);
task return Result::<Response, ErrorCode>::Ok(response); // deliver result; function continues
trailers_tx.write(Result::<Option<Trailers>, ErrorCode>::Ok(null)); // fulfill trailers
}
Rules
task returnis only valid insideexport async fnbodies.- Regular
returnis forbidden inasync fnbodies — it would exit the Wasm function without notifying the CM runtime. - The
task returnexpression is type-checked against the declared return type of the enclosingexport async fn. - The
asynckeyword on a function declaration has no effect on callers; Wado is fully colorless.asynconly appears inexport async fndeclarations to signal CM async calling convention at the component boundary.
Attribute Syntax for Component Model Linking
Use #[cm(...)] attributes to link Wado definitions to Component Model interfaces:
// Link an effect interface to a CM interface
#[cm("wasi:cli/stdout@0.3.0-rc-2025-09-16")]
pub interface Stdout {
#[cm("wasi:cli/stdout@0.3.0-rc-2025-09-16#write-via-stream")]
fn write_via_stream(data: Stream<u8>) -> Result<(), ErrorCode>;
}
// Link a resource to a CM resource
#[cm("wasi:cli/terminal-output@0.3.0-rc-2025-09-16")]
resource TerminalOutput;
// Link an enum to a CM enum
pub enum ErrorCode { // Maps to WIT: enum error-code
Io, // Maps to WIT: io
IllegalByteSequence, // Maps to WIT: illegal-byte-sequence
Pipe, // Maps to WIT: pipe
}
Compiler Attributes
Wado uses #[...] attributes (item-level) and #![...] inner attributes (module-level) to control compiler behavior.
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"); }
#[benign(E, ...)]
Lets a function perform the listed effects without declaring with E, and stops them from propagating to callers — for effects that are observationally pure (unobservable through the function's interface). Only the named effects are suppressed; others propagate normally, and the world import for each is still required. The compiler cannot verify observational purity, so this is an unchecked assertion that must be audited. See WEP: Effect System and Randomness in Collections.
#[benign(InsecureSeed)]
fn make<K, V>() -> HashMap<K, V> {
let seed = get_insecure_seed(); // performed here, not required of callers
return HashMap { seed, /* ... */ };
}
#[secret]
Hides a struct field from debug/inspect output (the :? format specifier).
struct Foo {
pub name: String,
#[secret]
password: String, // excluded from `{foo:?}` output
}
#[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. Each parameter resolves highest-priority-first: -D NAME=value (alias --define) on the wado invocation, then from_env, then the initializer. Overrides are parsed into the declared scalar type with the LenientFromStr spellings. See WEP: Compile-Time Parameters.
#[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=...
#[expect_trap]
Test block attribute. Marks a test that is expected to trap. The test passes if the body traps, and fails if it completes normally.
#[expect_trap]
test "panics on invalid input" {
panic("bad input");
}
#[TODO]
Test block attribute. Marks a test for an unimplemented feature. TODO tests are reported on a separate axis from regular pass/fail results:
- If the body traps, the test is pending (expected — the feature is still unimplemented).
- If the body completes normally, the test is resolved (hard failure — the
#[TODO]attribute must be removed).
Pending TODO tests never cause the test runner to fail. Resolved TODO tests always cause the test runner to fail with exit code 1.
See Test Outcome Model in the Testing section for the full specification.
#[TODO]
test "not yet implemented" {
panic("TODO: implement this");
}
#[timeout_ms(N)]
Test block attribute. Overrides the default test timeout (1000ms). N is an integer literal specifying the timeout in milliseconds.
#[timeout_ms(5000)]
test "slow computation" {
let result = expensive_computation();
assert result == 42;
}
#[synopsis]
Test block attribute. The test runs like any other, and wado doc renders its body as the module's ## Synopsis section — a compiled, always-current usage example. See WEP: Synopsis Tests.
#[synopsis]
test {
let p = Point { x: 3, y: 4 };
assert p.length() == 5.0;
}
#[serde(rename = "...")] / #[serde(rename_all = "...")] / #[serde(positional)]
Controls serialization/deserialization behavior for struct fields. #[serde(rename = "...")] overrides the wire-form key for a single field; #[serde(rename_all = "...")] (on a struct) renames every field by a convention ("camelCase", "snake_case", "kebab-case", ...). A field is optional on deserialization when it has a default value (f: T = expr), falling back to that expression when absent — the single mechanism for optional fields. #[serde(positional)] marks a field as ordinal: it is resolved by position, never by name (name-only and sequence-only formats ignore the hint), which core:args uses to bind a bare token to the field. See WEP: Serialization and Deserialization and core:serde.
Standard Library Attributes
These attributes are used in the standard library (lib/) to wire Wado code to Wasm and the Component Model. They are not intended for user code.
#![no_prelude]
Module-level inner attribute. Prevents the automatic import of core: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
#![TODO]
Module-level inner attribute. Marks the entire module as TODO for wado test. The source must parse successfully (otherwise the attribute cannot be recognized), but compilation errors are tolerated:
- If compilation fails, the module is reported as a single pending TODO entry.
- If compilation succeeds, all test blocks are implicitly treated as
#[TODO]tests. - If the module compiles and all tests pass, it is reported as resolved (hard failure — the
#![TODO]attribute must be removed).
See Test Outcome Model in the Testing section for the full specification.
#![TODO]
test "not yet implemented" {
panic("TODO");
}
#![generated]
Module-level inner attribute. Indicates that the module contains machine-generated code (e.g. from wado-from-idl or gale). Stored in the AST but not carried into TIR or later compilation stages. Reserved for future tooling use: language services may prohibit editing generated modules, and coverage tools may exclude them.
The attribute accepts optional metadata so that generators can attach provenance information directly to the attribute instead of as free-form comments. Two argument shapes are supported 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"])]
#![wasm_module("name")]
Module-level inner attribute. All items in this module are compiled into a separate Wasm core module with the given name, rather than into the main GC core module.
This is the mechanism by which the compiler produces the multi-module component structure required by the Component Model. Items marked with #![wasm_module] are extracted from the main compilation pipeline and emitted as a standalone core module with its own linear memory.
#![wasm_module("mem")]
#![no_prelude]
global mut __heap_offset: i32 = 1024;
#[export_name("realloc")]
export fn bump_realloc(oldptr: i32, oldsize: i32, align: i32, newsize: i32) -> i32 {
// ...
}
Currently, the only wasm_module in the standard library is "mem", defined in core:allocator. See The "mem" Core Module for details.
#[export_name("name")]
Overrides the Wasm export name of a function within its core module. Without this attribute, the export name is derived from the function's module-qualified path.
#[export_name("realloc")]
export fn bump_realloc(...) -> i32 { ... }
// Exported as "realloc" instead of "bump_realloc"
#[canonical("namespace", "name")]
Declares that a builtin function is imported as a Component Model canonical built-in. Used in core:builtin to map intrinsic declarations to CM imports. Functions without this attribute compile directly to Wasm instructions.
| Namespace | Description |
|---|---|
"wasi" |
CM canonical builtins (streams, futures, tasks) |
"mem" |
Memory operations from the "mem" core module |
"bundled" |
Functions from bundled Wasm modules (e.g., libm) |
#[canonical("wasi", "stream-new")]
fn stream_new() -> i64;
#[canonical("mem", "realloc")]
fn realloc(oldptr: i32, oldsize: i32, align: i32, newsize: i32) -> i32;
#[compiler_item("name")]
Marks a stdlib declaration as the resolution for a compiler-recognized item. The compiler uses these annotations to bind specific stdlib symbols (types, traits, methods, variant/enum cases) to the Rust-side enum CompilerItem so downstream passes (synthesis, lowering, codegen) can look them up by key instead of by hard-coded name. Renaming a stdlib item on the Wado side stays transparent as long as the #[compiler_item("...")] argument is unchanged.
Examples: #[compiler_item("option")] on variant Option, #[compiler_item("option_some")] on the Some case, #[compiler_item("display")] on the Display trait, #[compiler_item("string_push_str")] on the String::push_str method.
The attribute is only valid inside core::* modules; the elaborator rejects it on user code.
#[cm("namespace:pkg/interface@version")] / #[cm_params(...)]
Links Wado definitions (effects, resources, enums) to Component Model interfaces. See Attribute Syntax for Component Model Linking.
The "mem" Core Module
The Component Model requires each component to provide a linear memory and a realloc function. The CM runtime calls realloc whenever it needs guest-side linear memory — for example, stream.read copies bytes from the host into a guest buffer, and string lifting/lowering also goes through it.
Wado's main core module uses Wasm GC (managed heap) and does not have linear memory. The "mem" core module is a separate core module that provides:
- Linear memory — a Wasm linear memory for CM data transfer
realloc— the CM-required allocator function
The "mem" module is defined in core:allocator using #![wasm_module("mem")]. The compiler:
- Extracts all functions and globals from
#![wasm_module("mem")]sources during WIR construction - Emits them as a standalone core module with its own linear memory
- Instantiates the "mem" module as part of the component
- Aliases the
reallocexport for use incanon lift/canon loweroptions - Shares the "mem" instance with the main core module via imports, so that
builtin::realloc(declared with#[canonical("mem", "realloc")]) resolves to the same function
The component structure looks like:
(component
(core module "mem" ;; from #![wasm_module("mem")]
(memory (export "mem") 1)
(func (export "realloc") ...)
(global (export "__heap_offset") (mut i32) (i32.const 1024)))
(core instance "mem" (instantiate "mem"))
(core module "main" ;; GC core module
(import "mem" "realloc" (func ...))
(import "mem" "mem" (memory 1))
...)
(core instance "main" (instantiate "main"
(with "mem" (instance "mem"))))
(canon lower ... (realloc (func "mem" "realloc")) (memory (memory "mem" "mem")))
)
Appendix
Naming Conventions
| Element | Style |
|---|---|
| Project name | kebab-case |
| Module/file name | snake_case |
| Primitive types | lowercase |
| User-defined types | UpperCamelCase |
| Enum/variant cases | UpperCamelCase |
| Functions | snake_case |
| Local variables | snake_case |
Component Model interop: The compiler automatically converts between Wado conventions and WIT conventions (kebab-case) at component boundaries.
Terminology
- Wasm: WebAssembly (not WASM)
- WASI: WebAssembly System Interface
- CM: Wasm Component Model
- module: a Wado file
- project: a collection of modules
- Wado standard library: consists of
core:andwasi: - effect: the concept; e.g., "the
Stdouteffect" - effect interface: the declaration (
effect Stdout { ... }); synonyms in literature: "effect signature", "effect type" - operation: a function in an effect interface; synonym: "effect operation"
- handler: provides implementations for operations
- hosted world: a world that a runtime knows how to instantiate and drive (e.g.,
wasi:cli/commandforwado run,wasi:http/serviceforwado serve); informally called "well-known world" - library world: a world that defines a component's public API for composition with other components, rather than for direct execution by a runtime
