talk_dev's Journal

Talk of the Town

Today's Typecheck
talk_dev posted a new release

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.

Ownership Moves Into the Type System

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
}
Destructors and Borrows Keep Score

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
Landing in This Release
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
}
Heap Structs Open the Graph Room

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
}
Iterator Matches and Ascriptions Get Repairs

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 Middle End Gets a Small Core

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.

Tests Put Memory on the Balance Sheet

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.

permalinkrecent entriesarchive
The Daily Talk - What's New in Talk - Release Notes for July 3, 2026