Wado

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

  1. Value semantics by default: Align with the value semantics WEP
  2. Explicit ownership transfer: Use move for explicit moves
  3. Compiler optimization: Allow automatic move when safe
  4. Wasm GC compatibility: Map naturally to Wasm GC structs
  5. 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:

// 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

  1. Familiar syntax: Rust developers will feel at home
  2. Value semantics: Simple mental model - assignment copies by default
  3. Performance: Compiler can optimize to move when safe
  4. Explicit moves: move keyword makes ownership transfer clear
  5. Wasm GC fit: Structs map naturally to Wasm GC struct types
  6. Drop for resources: Clean up external resources (not memory)
  7. Rich trait ecosystem: Built-in traits cover common use cases

Negative

  1. Hidden copies: an implicit deep copy may surprise developers expecting move semantics
    • Mitigation: Compiler warnings for expensive implicit copies
  2. Orphan rule limitations: Cannot extend external types with external traits
    • Mitigation: Newtype pattern (struct MyString(String))
  3. A type cannot decline the deep copy or define its own
    • Mitigation: a reference (&T) is how a value is shared rather than copied
  4. 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 bool beside impl Take<B> for bool makes f.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> implement IndexValue<i32>, IndexValue<RangeExclusive<i32>> and IndexValue<RangeInclusive<i32>> at once. From likewise 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.

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

References