Memory Model
This chapter covers how values are copied and shared: value semantics, references, and how parameters and receivers hold their arguments. Memory is garbage-collected, so a program writes no lifetimes and frees nothing.
Value Semantics
Assignment, parameter passing, and return all perform a deep copy of the value. Primitives, structs, variants, String, and List<T> all follow this rule.
The copy stops at a reference. A &T or &mut T, wherever it appears, is copied as a reference, and the copy aliases the same value. So a Slice<T> or a StrSlice, which is a struct holding a reference to its backing array, shares its elements with the value it views.
A resource is a handle to something the host owns, not a Wado value, so it is never deep-copied. An affine resource is move-only: assignment, parameter passing, and return move it, and the source is unusable afterwards (see Resource Ownership).
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;
}
test {
let mut p = Point { x: 1, y: 2 };
translate(&mut p, 10, 20);
assert p.x == 11 && p.y == 22;
}
These semantics are as-if. A program may rely on the value each expression denotes; it may not rely on the number of copies performed to produce it.
A program never chooses where a value lives. There is no stack or heap to pick between. A value needs no annotation to outlive its scope, because the garbage collector keeps alive every value a reference can reach.
A closure that outlives the scope that declared it needs no rule of its own: the collector keeps what it captured alive. How a closure copies is in Closures Are Values.
A call across a component boundary copies its arguments and its result, so the two components never share a value's storage. A resource crosses as a handle (Type Mapping at Component Boundaries).
Rationale: WEP: Value Semantics and Reference Retention.
Reference Types
A reference gives indirect access to a value. Unlike Rust, Wado has no borrow checker: the garbage collector keeps every referent alive, so a reference never dangles.
Basic Reference Syntax
let x = 42;
let r = &x; // Immutable reference
let v = *r; // Dereference
assert v == 42;
let mut y = 0;
let mr = &mut y; // Mutable reference
*mr = 10; // Assign through reference
assert y == 10;
A reference is never null, and there is no arithmetic on it. A reference that
may be absent is written Option<&T>.
Reference to Reference
References can be nested arbitrarily:
let x = 42;
let r = &x; // &i32
let rr = &r; // &&i32
let val = **rr; // double dereference
assert val == 42;
Automatic Coercion (&mut to &)
Mutable references automatically coerce to immutable references when needed:
fn read_value(r: &i32) -> i32 {
return *r;
}
test {
let mut x = 10;
assert read_value(&mut x) == 10; // OK: &mut i32 coerces to &i32
}
Returning References to Local Variables
A reference to a local variable stays valid after the function returns:
fn make_ref() -> &i32 {
let x = 42;
return &x; // OK in Wado (x is GC-managed and stays alive)
}
test {
let r = make_ref();
assert *r == 42;
}
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;
assert x == 30;
Reference Retention
A function may keep a reference parameter past its return: it may store it in a
global, or write it through a &mut parameter the caller still holds. It may
also return a reference into a parameter's storage. Each is safe, because the
referent is GC-managed and cannot dangle.
A function with a body declares nothing about what it keeps: that is inferred
from the body. A function type carries no such declaration, and neither does a
closure, so a function value is called the same way whatever the function
keeps. Retention is not an effect either. It grants no authority and no handler
intercepts it, so it has no place in a with clause.
A declaration with no body has nothing to infer from, so it states what it keeps with
#[storage(...)].
Rationale: WEP: Value Semantics and Reference Retention.
Mutable References to Fields and Elements
A primitive, enum, flags or fn field or element keeps nothing that a write
through a &mut could reach, since assignment replaces it. So taking &mut of
one is an error. That holds whether it is written &mut x.f or &mut xs[i], or
taken implicitly by a &mut self receiver. A &mut of a local is fine, since it
writes to the variable itself.
A variant field or element admits &mut. The reference points to a copy of
the variant that shares its payload with the original. So a mutation reached
through match ergonomics, such as
if let A(b) = r { b.n = 1; }, lands. Replacing the whole value through the
reference would not, so it is an error.
These rules for references to replace-on-assign values are not settled. They are being relaxed toward what Rust allows, so expect them to change.
Reference Identity
== and != on two references compare the values they point to, as in Rust
(see Eq). The prelude function
ref_eq(a: &T, b: &T) -> bool compares identity instead: whether the two
references point to one place. A place is where a value is stored: a variable,
a field, an element.
Identity is guaranteed in one direction only. Two references to one place are
always ref_eq: &x taken twice of one variable, or a reference and a copy of
it. References to distinct places may also be ref_eq, because the copies value
semantics promise are as-if: the implementation may store equal content once,
by eliding a copy or by interning a constant String or List. Whether it
does is unspecified, so a ref_eq that
is true only by such sharing is unpredictable. Java's == on
strings behaves the same way.
let mut xs: List<i32> = [1, 2, 3];
let ys: List<i32> = [1, 2, 3];
let zs = xs;
assert &xs == &ys; // equal values
assert ref_eq(&xs, &xs); // always
ref_eq(&xs, &ys); // false or true: the two may be stored once
ref_eq(&xs, &zs); // false or true: the copy may be elided
A & to a List element or a struct field of a type that assignment replaces
(a primitive, enum, flags, variant or fn) points to a copy of the value,
taken where the & is written. It does not see a later assignment to the
element or field, and two such references are two places:
let r = &xs[0];
xs[0] = 9;
assert *r == 1;
ref_eq(&xs[0], &xs[0]); // false or true: each `&` takes its own copy
A closure's identity stays unobservable. ref_eq takes references only, and a
reference to a closure points to the place holding it, not to the closure:
let f = || 1;
let g = f;
assert ref_eq(&f, &f); // always
ref_eq(&f, &g); // false or true: two places holding one closure
Parameters
Method Receiver: self by Value
A method receiver is &self or &mut self. Bare self (by value) is allowed only on a resource, on an aggregate that holds one, or on a generic type, since its type arguments may be resources (Option<T>::unwrap(self)):
impl Point {
fn sum(&self) -> i32 { return self.x + self.y; } // OK: immutable reference
fn reset(&mut self) { self.x = 0; self.y = 0; } // OK: mutable reference
}
test {
let mut p = Point { x: 1, y: 2 };
assert p.sum() == 3;
p.reset();
assert p.sum() == 0;
}
Point holds no resource, so self by value on it is an error:
impl Point {
fn consume(self) -> i32 { return self.x; }
}
A by-value self is passed as any parameter is. A receiver holding an affine resource moves into the method, so the caller's binding cannot be used afterward, which is how the resource is consumed (see Resource Ownership). Any other receiver is copied, so Option<i32>::unwrap leaves its binding usable.
mut Parameters
A parameter declared mut may be written in the body, as a let mut binding
may (Variable Mutability):
fn increment(mut n: i32) -> i32 {
n += 1; // mutates the local copy
return n;
}
fn normalize(mut s: String) -> String {
s = s.to_ascii_uppercase(); // rebinds local binding
return s;
}
test {
assert increment(1) == 2 && normalize("ab") == "AB";
}
Writing a mut parameter changes only the callee's copy (Value Semantics). A &mut T parameter holds a copy of the reference, so a write through it (*p = v) reaches the referent.
fn countdown(mut n: i32) with Stdout {
while n > 0 {
println(`${n}`);
n -= 1; // only modifies the local copy
}
}
test {
let x = 3;
countdown(x);
assert x == 3; // every parameter is passed by value
}
Closures also support mut parameters:
let add_one = |mut n: i32| {
n += 1;
return n;
};
assert add_one(1) == 2;
Without mut, any assignment to a parameter is a compile error:
fn bad(n: i32) {
n = 10;
}
