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: 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.