References, Moves & Ownership
NG uses a value semantics model by default — variables are copied on assignment — with explicit references and moves for shared ownership and efficient transfers.
Value Semantics (Default)
By default, bindings are independent copies:
val a = [1, 2, 3];
val b = a; // b is a copy of a
b[0] := 99;
print(a[0]); // 1 (a is unchanged)This applies to function arguments and return values too:
fun modify(arr: [i32]) {
arr[0] := 99; // modifies the local copy only
}
val arr = [1, 2, 3];
modify(arr);
print(arr[0]); // 1 (unchanged)Reference Types: ref<T>
Create a reference to share access to a value:
val x = 42;
val r: ref<i32> = ref x; // r refers to x
x = 43;
print(*r); // 43 (reads through the reference)Creating References
Use the ref operator on a mutable place:
val x = 1;
val r = ref x; // reference to xReading Through References
Dereference with *:
print(*r); // reads the valueWriting Through References
Use deref assignment :=:
*r := 10; // writes through the reference
print(x); // 10Reference Parameters
Functions can accept references:
fun swap<T>(a: ref<T>, b: ref<T>) {
val tmp = move *a;
*a := move *b;
*b := move tmp;
}
val x = 1;
val y = 2;
swap(ref x, ref y);
print(x, y); // 2, 1Move Semantics
move transfers ownership of a value, invalidating the original location:
val a = [1, 2, 3];
val b = move a; // a is now invalid
// print(a[0]); // ERROR: use after move
print(b[0]); // 1Move Dereference
Combine move and * to move a value out of a reference:
fun take<T>(dest: ref<T>, src: ref<T>) {
*dest := move *src; // moves value from src into dest
}Runtime Move Checking
The interpreter tracks moved values at runtime and throws an error if you try to read a moved location before reassigning it:
val x = move a; // a is now "moved"
// print(a); // Runtime error: use after move
a = [4, 5, 6]; // reassign — OK now
print(a); // OKReferences to Object Properties
References can point to object fields:
type Point { x: i32; y: i32; }
val p = new Point { x: 10, y: 20 };
val rx = ref p.x;
*rx := 99;
print(p.x); // 99Partial Moves
You can move individual fields from an object while leaving others accessible:
type Person {
name: string;
age: i32;
}
val p = new Person { name: "Alice", age: 30 };
val name = move p.name; // p.name is moved
// print(p.name); // ERROR: field was partially moved
print(p.age); // OK: age is still accessibleRestoration After Partial Move
Writing to a partially-moved field restores full access:
p.name = "Bob"; // restores p.name
print(p.name); // OK nowPartial Move Tracking in Objects
The type checker tracks which fields have been partially moved and prevents reads from moved fields. This extends to nested objects and tuple fields:
val nested = new Wrapper { inner: new Inner { x: 1, y: 2 } };
val x = move nested.inner.x;
// print(nested.inner.x); // ERROR: partially moved
print(nested.inner.y); // OKReferences and Aliasing
Direct Ref Aliasing
A ref creates a borrow that remains active within its lexical scope. While a direct ref is alive, neither the original value nor the ref can be invalidated by moves:
val x = 1;
val r = ref x;
// val y = move x; // ERROR: can't move while ref is active
print(*r); // OK — ref is still usableRef Borrow Ends at Scope Boundary
val x = 1;
{
val r = ref x; // borrow starts
print(*r);
} // borrow ends
val y = move x; // OK — ref is goneHeap-Allocated Objects (new)
Objects created with new are heap-allocated and alias by reference:
val a = new Point { x: 1, y: 2 };
val b = a; // b aliases the same heap object
a.x = 10;
print(b.x); // 10 (shared)What's Next?
Continue to Traits to learn about NG's trait system for abstraction and polymorphism.
Try it:
example/22.ref_move_swap.ng— Reference swap Try it:example/23.ref_places.ng— References to places Try it:example/24.move_value_semantics.ng— Move semantics Try it:example/50.partial_move.ng— Partial moves Try it:example/51.partial_move_drop.ng— Partial moves with Drop