Memory Gets a New Constitution
Talk moves ownership into the type system, opens heap-allocated reference graphs, and makes leak checks part of the release bargain.
Today's Talk release is a memory edition from the grammar down to the evaluator.
Ownership
now lives in type permissions and grades, with a focused flow pass for path-sensitive questions, while
'heap structs bring mutable aliased structures without lifetime annotations or manual
frees.
The headline change is that ownership classification is no longer something users
spell
over and over. Declaration grades sit in tick-suffix position, so struct Token 'linear
creates a must-consume type, while structural rules decide when ordinary user types may opt into
Copy.
CheapClone fills the practical middle. Strings, arrays, storage buffers, and byte storage
can be
silently cloned when an owned value is needed from a borrow, because the operation is just an O(1)
retain.
Unique values written as *T go the other way: they always move, never silently clone, and
can be
mutated in place.
struct Token 'linear { let text: String } struct Point { let x: Int let y: Int } extend Point: Copy {} struct Person { let name: String } func borrow_name(person: &Person) -> String { // String is CheapClone, so the // borrowed field read can retain // the buffer instead of moving it person.name }
The new Deinit protocol gives owned values a consuming destructor that
runs
at scope exit. Drops run in reverse declaration order, partial moves use drop flags, and enum payloads,
by-value arguments, match binders, tail branches, and existentials all participate in cleanup.
Borrowing also gets quieter and stricter. Borrows end at last use with no lifetime annotations, returned borrows are inferred interprocedurally, and attempts to return a borrow of function-owned storage still fail. Closure captures are inferred per variable, global borrows make their owners unassignable, and editor hovers now name values as owned, copy, borrowed view, linear, or cheap to clone.
No lifetime annotations, no manual frees.- The day's release, in one line
struct Handle { let name: String } extend Handle: Deinit { consuming func deinit() { // cleanup runs when Handle drops () } } func open_two() -> Int { let first = Handle(name: "one") let second = Handle(name: "two") // second drops before first 0 }
struct Node 'heap now means reference semantics. Assignments
alias,
mutation through one reference is visible through the others, and cycles are allowed because the runtime
places heap values in inferred regions. Linking values merges their regions; when the last binding into
a
region leaves scope, destructors run and the region is bulk-freed.
The first public uses are practical: core gains a string-keyed Dict<Value> built on
a heap
node chain, and HttpServer is now heap-backed with routes in a growable chain instead of a
four-route ceiling. The release also draws v1 lines around the feature: escaping closure captures,
any P packing, raw-storage containers such as Array<Node>, heap plus
Copy, and auto-derived Showable are compile errors.
struct Node 'heap { let name: String let next: Optional<Node> } func make_cycle() -> Int { let a = Node(name: "a", next: Optional.none) let b = Node(name: "b", next: Optional.some(a)) // Linking b back to a merges the // region. The cycle still tears // down at scope exit. a.next = Optional.some(b) 0 }
The fixed list is aimed at real programs that were close but not quite ordinary. A
for x in xs { x in ... } body block with its own argument now type checks instead of
creating a
second never-reseeded symbol. Variant patterns over borrowed iterator elements also look through the
borrow
once the element type resolves.
Borrowed enum matches stop double-freeing payloads, as expressions now erase at the HIR
boundary
and run, and the formatter's multiline labeled calls re-parse after formatting. Even the small
stuff is
tidier: typealias Target = Response now keeps spaces around the equals sign.
enum Event { case a(Int) case b } func score(items: Array<Event>) -> Int { for e in items { match e { .a(x) -> x .b -> 0 } } 0 } typealias Target = Response let answer = 41 as any Number
The compiler internals section reads like a strangler arc made concrete. A new
desugar
phase handles for-loops, operators, function-to-let lowering, method self, and expression
if before name resolution, so the resolver goes back to binding names.
HIR is now Core-shaped: literals merge, as and parentheses disappear at build,
stored-field
reads and variant constructions split from generic member and call syntax, and type-directed decisions
happen
once. Flow analysis is pure CFG dataflow, MIR statements are evaluation units, and lower/
is
decomposed by concern instead of carrying one large mixed responsibility.
The suite now treats leak detection as policy. Every program-running test uses the allocation-tracking evaluator and expects allocations and heap objects to balance to zero at exit, except for the single greppable container-element teardown fence that documents the remaining known deficit.
A new real-program corpus backs that rule with small, complete Talk programs: iterate-and-match examples, string builders, conditional moves in loops, effect handlers, and heap graphs run on both engines with matching stdout. In other words, the new memory model is not just checked by unit cases; it is asked to survive ordinary programs.