WEP: Struct and Trait System
Context
Wado needs a type system for defining custom data types and shared behavior. This WEP defines the syntax and semantics for structs (product types) and traits (behavior abstraction), building on the value semantics established in wep-2026-01-12-value-semantics-and-retention.md.
Design Goals
- Value semantics by default: Align with the value semantics WEP
- Explicit ownership transfer: Use
movefor explicit moves - Compiler optimization: Allow automatic move when safe
- Wasm GC compatibility: Map naturally to Wasm GC structs
- Familiar syntax: Rust-like syntax for ease of learning
Decision
1. Struct Definition
Structs define product types with named fields:
struct Point {
x: i32,
y: i32,
}
// With visibility modifiers
pub struct User {
pub name: String,
pub age: i32,
active: bool, // private by default
}
// Generic struct
struct Pair<T, U> {
first: T,
second: U,
}
// Recursive struct (enabled by Wasm GC)
struct Node<T> {
value: T,
next: Option<Node<T>>,
}
2. Struct Instantiation
let p = Point { x: 10, y: 20 };
// Shorthand when variable name matches field
let x = 10;
let y = 20;
let p = Point { x, y };
// Struct update syntax
let p2 = Point { x: 100, ..p }; // copies y from p
3. Method Blocks (impl)
Methods are defined in impl blocks:
impl Point {
// Associated function (no self) - called via Point::new()
pub fn new(x: i32, y: i32) -> Point {
return Point { x, y };
}
// Method with immutable self reference
pub fn magnitude(&self) -> f64 {
return ((self.x * self.x + self.y * self.y) as f64).sqrt();
}
// Method with mutable self reference
pub fn translate(&mut self, dx: i32, dy: i32) {
self.x += dx;
self.y += dy;
}
// Method that consumes self (takes ownership)
pub fn into_tuple(self) -> [i32, i32] {
return [self.x, self.y];
}
}
// Usage
let p = Point::new(3, 4);
let mag = p.magnitude(); // 5.0
let mut p2 = Point::new(0, 0);
p2.translate(10, 20);
let tuple = p.into_tuple(); // p is consumed
4. Methods with Effects
Methods can declare effect requirements:
impl User {
pub fn save(&self) with FileSystem {
FileSystem::write(`users/${self.name}.json`, self.to_json());
}
pub fn load(name: String) -> Result<User, IoError> with FileSystem {
let data = FileSystem::read(`users/${name}.json`)?;
return User::from_json(data);
}
}
5. Trait Definition
Traits define shared behavior:
trait Display {
fn display(&self) -> String;
}
trait Default {
fn default() -> Self;
}
// Trait with default implementation
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
return `Hello, ${self.name()}!`;
}
}
// Trait with associated type
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
// Trait with effect requirements
trait Loadable {
fn load(path: String) -> Result<Self, IoError> with FileSystem;
}
6. Trait Implementation
impl Display for Point {
fn display(&self) -> String {
return `Point(${self.x}, ${self.y})`;
}
}
impl Default for Point {
fn default() -> Point {
return Point { x: 0, y: 0 };
}
}
// Generic impl
impl<T: Display> Display for Pair<T, T> {
fn display(&self) -> String {
return `(${self.first.display()}, ${self.second.display()})`;
}
}
An impl defines every method its trait declares without a body. A trait method written with a body is a default, and an impl may take it or replace it.
An impl may also declare a method the trait does not. It is a helper its own
bodies call on self, reachable through that impl alone. This is where methods
part from associated types, which an impl binds one-for-one with the
declaration.
7. Trait Bounds
// Single bound
fn print_all<T: Display>(items: List<T>) with Stdout {
for let item of items {
println(item.display());
}
}
// Multiple bounds with +
fn process<T: Display + Default>(value: Option<T>) -> String {
match value {
Some(v) => v.display(),
None => T::default().display(),
}
}
// Several bounded parameters
fn merge<T: Display, U: Display>(a: T, b: U) -> String {
return `${a.display()} and ${b.display()}`;
}
8. Trait Objects (Dynamic Dispatch)
// Trait object type with &dyn
fn log_item(item: &dyn Display) with Stdout {
println(item.display());
}
// In collections
let items: List<&dyn Display> = [&point, &user];
// Owned trait object with Box
let boxed: Box<dyn Display> = Box::new(point);
Implementation Note: dyn trait objects map to Wasm GC reference types with a vtable struct.
9. Copy Semantics
Building on the value semantics WEP: every value is deeply copied when assigned or passed, so there is no opt-in trait to request a copy and no way to ask for a shallow one. A primitive copies bitwise; an aggregate copies its fields.
let a: i32 = 42;
let b = a; // both valid
struct User {
name: String,
age: i32,
}
let u1 = User { name: "Alice", age: 30 };
let u2 = u1; // deep copy — `u1` stays valid
Compiler Copy Elision
The copy is semantic, not mandatory work: the compiler elides it wherever the source is not read again.
| Pattern | Compiler Action |
|---|---|
| Last use before assignment | Move |
| Used after assignment | Copy |
| Passed to function (last use) | Move |
| Returned from function | Move (RVO) |
10. Built-in Traits
Wado provides several built-in traits in the prelude:
Drop
For types that need cleanup when going out of scope:
trait Drop {
fn drop(&mut self);
}
// Example: file handle cleanup
impl Drop for FileHandle {
fn drop(&mut self) {
self.close();
}
}
Note: Unlike Rust, Wado runs on Wasm GC, so Drop is primarily for releasing external resources (file handles, network connections), not memory deallocation.
Default
For types with a default value:
trait Default {
fn default() -> Self;
}
impl Default for i32 {
fn default() -> i32 { return 0; }
}
impl Default for String {
fn default() -> String { return ""; }
}
impl<T> Default for List<T> {
fn default() -> List<T> { return []; }
}
impl<T> Default for Option<T> {
fn default() -> Option<T> { return None; }
}
Display
For human-readable string representation:
trait Display {
fn display(&self) -> String;
}
// Used by template strings
let p = Point { x: 1, y: 2 };
let s = `Point is: ${p}`; // calls p.display()
Debug
For debug/developer-oriented string representation:
trait Debug {
fn debug(&self) -> String;
}
// Used by debug printing
debug_println(value); // calls value.debug()
Eq
For equality comparison (== and !=):
trait Eq {
fn eq(&self, other: &Self) -> bool;
}
All primitive types implement Eq. Custom types implement Eq to enable == and != operators.
Ord
For ordering comparison (<, <=, >, >=):
variant Ordering {
Less,
Equal,
Greater,
}
trait Ord {
fn cmp(&self, other: &Self) -> Ordering;
}
All primitive types implement Ord. Custom types implement Ord to enable comparison operators.
Hash
For hashable types:
trait Hash {
fn hash(&self) -> u64;
}
Note: TreeMap (the default associative array in Wado) uses Ord for key ordering, not Hash.
Iterator
For iterable types:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// Default implementations for common operations
fn map<U, F: Fn(Self::Item) -> U>(self, f: F) -> Map<Self, F> { ... }
fn filter<F: Fn(&Self::Item) -> bool>(self, f: F) -> Filter<Self, F> { ... }
fn fold<U, F: Fn(U, Self::Item) -> U>(self, init: U, f: F) -> U { ... }
fn collect<C: FromIterator<Self::Item>>(self) -> C { ... }
}
trait IntoIterator {
type Item;
type Iter: Iterator<Item = Self::Item>;
fn into_iter(self) -> Self::Iter;
}
FromIterator
For collecting iterators into containers:
trait FromIterator<T> {
fn from_iter<I: Iterator<Item = T>>(iter: I) -> Self;
}
impl<T> FromIterator<T> for List<T> { ... }
11. Orphan Rules
Wado enforces orphan rules to prevent conflicting implementations:
- You can implement your trait for any type
- You can implement any trait for your type
- You cannot implement an external trait for an external type
// OK: your trait for external type
impl MyTrait for String { ... }
// OK: external trait for your type
impl Display for MyStruct { ... }
// ERROR: external trait for external type
impl Display for String { ... } // Not allowed
12. Marker Traits
Some traits have no methods and serve as compile-time markers:
// Sized is implicit for most types
trait Sized {}
Consequences
Positive
- Familiar syntax: Rust developers will feel at home
- Value semantics: Simple mental model - assignment copies by default
- Performance: Compiler can optimize to move when safe
- Explicit moves:
movekeyword makes ownership transfer clear - Wasm GC fit: Structs map naturally to Wasm GC struct types
- Drop for resources: Clean up external resources (not memory)
- Rich trait ecosystem: Built-in traits cover common use cases
Negative
- Hidden copies: an implicit deep copy may surprise developers expecting move semantics
- Mitigation: Compiler warnings for expensive implicit copies
- Orphan rule limitations: Cannot extend external types with external traits
- Mitigation: Newtype pattern (
struct MyString(String))
- Mitigation: Newtype pattern (
- A type cannot decline the deep copy or define its own
- Mitigation: a reference (
&T) is how a value is shared rather than copied
- Mitigation: a reference (
- One trait per receiver at one argument list: a call names only the method, and
method resolution reads neither the argument nor the expected type, so
impl Take<A> for boolbesideimpl Take<B> for boolmakesf.take(..)ambiguous. Selecting on either would be overload resolution: arguments are elaborated against the resolved signature, so choosing by argument type means typing the arguments first, independently of any candidate.- Mitigation: the ambiguity is reported at the call site, naming both traits.
Operators are unaffected — they select by operand type on their own path, which is
what lets
List<T>implementIndexValue<i32>,IndexValue<RangeExclusive<i32>>andIndexValue<RangeInclusive<i32>>at once.Fromlikewise has a dedicated resolution path (WEP 2026-03-16). - Resolution: WEP: Overload Resolution generalizes the operand-type selection to named method calls within one trait's argument lists, and adds the trait-qualified call form for cross-trait collisions.
- Mitigation: the ambiguity is reported at the call site, naming both traits.
Operators are unaffected — they select by operand type on their own path, which is
what lets
Comparison with Rust
| Aspect | Rust | Wado |
|---|---|---|
| Default on assignment | Move | Deep copy (value semantics) |
| Opting out of the copy | (the default) | move, or share via &T |
| Copying a value | Copy / Clone traits |
Automatic, and deep |
| Memory cleanup | Drop trait |
GC (Drop for resources) |
| Trait syntax | Same | Same |
| impl blocks | Same | Same |
Examples
Complete Example
use {println} from "core:cli";
struct Point {
x: i32,
y: i32,
}
impl Point {
pub fn new(x: i32, y: i32) -> Point {
return Point { x, y };
}
pub fn magnitude(&self) -> f64 {
return ((self.x * self.x + self.y * self.y) as f64).sqrt();
}
pub fn translate(&mut self, dx: i32, dy: i32) {
self.x += dx;
self.y += dy;
}
}
impl Default for Point {
fn default() -> Point {
return Point { x: 0, y: 0 };
}
}
impl Display for Point {
fn display(&self) -> String {
return `(${self.x}, ${self.y})`;
}
}
fn run() with Stdout {
let p1 = Point::new(3, 4);
let p2 = p1; // deep copy (both valid)
println(`p1 = ${p1}`); // p1 = (3, 4)
println(`p2 = ${p2}`); // p2 = (3, 4)
println(`magnitude = ${p1.magnitude()}`); // magnitude = 5.0
let mut p3 = Point::default();
p3.translate(10, 20);
println(`p3 = ${p3}`); // p3 = (10, 20)
// Explicit move
let p4 = move p3;
// println(p3); // ERROR: p3 has been moved
}
Generic Container with Traits
struct Stack<T> {
items: List<T>,
}
impl<T> Stack<T> {
pub fn new() -> Stack<T> {
return Stack { items: [] };
}
pub fn push(&mut self, item: T) {
self.items.push(item);
}
pub fn pop(&mut self) -> Option<T> {
return self.items.pop();
}
pub fn is_empty(&self) -> bool {
return self.items.len() == 0;
}
}
impl<T: Display> Display for Stack<T> {
fn display(&self) -> String {
let parts = self.items.iter().map(|x| x.display()).collect::<List<String>>();
return `Stack[${parts.join(", ")}]`;
}
}
Known gaps
Selfin a field type names the struct being declared, but code generation panics on it.struct Node { next: Option<Self> }reaches WIR asOption<unknown>, which is not registered.
