Tutorial: Pointers
This tutorial introduces Metel's regular pointer model. The core idea is simple:
&xtakes the address of a value and produces*T&mut xtakes a mutable address and produces*mut T- pointers are storable and cloneable handles
Taking an address
The syntax uses & as address-of:
fun main() {
let mut value = 42;
let read_ptr: *Int = &value;
let write_ptr: *mut Int = &mut value;
}
*Int is a pointer to Int. *mut Int is a mutable pointer to Int.
Read This As Two Choices
Choose pointer mutability at the address-taking site:
&xfor*T&mut xfor*mut T
Why pointers exist
Pointers are mainly about shared state and indirection. They let multiple places talk about the same value instead of copying it around.
That matters in patterns like:
- graph nodes with back-links
- mutable shared state between closures
- data structures where values point at each other
Shared mutable state
Pointers are useful when multiple parts of a program need to talk about the same value explicitly. That includes shared mutable state between closures:
fun main() {
let mut total = 0;
let cell: *mut Int = &mut total;
let add_one = () -> {
*cell += 1;
};
let add_ten = () -> {
*cell += 10;
};
add_one();
add_ten();
println(total); // 11
}
Both closures mutate the same underlying Int through the same pointer.
Ordinary closure capture is by value. The pointer is what makes the sharing explicit: both closures talk to the same storage location, not independent copies.
Dereferencing
Use *p to read through a pointer, and *p = value to write through a mutable pointer:
fun main() {
let mut n = 1;
let p: *mut Int = &mut n;
*p = 4;
println(*p); // 4
}
*T is read-only. *mut T is readable and writable.
Explicit Where It Matters
Metel keeps pointer reads and writes explicit. You can spot aliasing-sensitive operations by looking for &, &mut, or *.
Auto-deref for fields and methods
Field access and method calls automatically dereference one pointer layer:
struct Counter {
value: Int,
}
impl Counter {
fun increment(&mut self) {
self.value += 1;
}
}
fun main() {
let mut counter = Counter { value: 0 };
let p: *mut Counter = &mut counter;
p.increment();
println(p.value); // 1
}
This is shorthand for (*p).increment() and (*p).value.
Auto-deref is only for field access, method calls, and function-pointer calls. Plain reads, writes, and argument passing still use explicit *p.
Function pointers
Pointers to closures or functions can be called directly:
fun main() {
let f = () -> Int { return 42; };
let ptr: *() -> Int = &f;
println(ptr()); // 42
}
Pointer types and linear values
Regular pointers provide explicit aliasing for non-linear values:
*Tand*mut Tare for non-linear, shared values- linear values are not meant to be addressed with
&x
This keeps pointer aliasing and linear ownership from fighting each other.
Boundary Rule
Regular pointers are for explicit aliasing of non-linear values. If a feature depends on exactly-once ownership, regular *T / *mut T are the wrong tool.
What you learned
&xyields*T.&mut xyields*mut T.*preads and*p = valuewrites through a pointer.- Fields, methods, and function-pointer calls auto-dereference one pointer layer.
- Pointers are for explicit aliasing of non-linear values.
Next: Closures and Capturing — first-class functions, captured values, and shared mutation.