Ownership Gets a Route Map
Talk now follows owned values through loops, fields, borrows, receivers, and bytecode with sharper rules.
Today's Talk release is about keeping track of who owns what, even when control flow takes the scenic road. Breaks, continues, borrowed parameters, consuming receivers, and field moves now leave clearer footprints for the compiler and runtime.
The lead item: owned locals inside loops are now cleaned up when control leaves by break or spins onward by continue. The changed lowering tests look for real frees before the loop exit or next loop head.
That makes early loop control behave like ordinary scope exit instead of slipping past owned values with its pockets full.
func f() -> Int { // the loop creates a nested // lifetime for its body loop { // s is the owned String whose // storage must not leak let s = "a" + "b" // break takes the early exit // path that now drops s break } 0 } f()
func f() -> Int { // keep_going drives the continue // branch let keep_going = true loop { // s is created before the branch // and must be handled on both // exits let s = "a" + "b" if keep_going { // continue jumps to the loop // head after dropping owned // loop locals continue } break } 0 } f()
Talk now handles the case where a value is moved on one branch but not the other. The lowering tests expect a runtime drop flag, so the final cleanup drops only when the value is still live.
The practical result is that a conditional move no longer forces the compiler into either leaking or double-dropping.
func maybe(flag: Bool) -> Int { // s starts live before the if let s = "hi" + "!" if flag { // the then branch moves s into t let t = s t.length } else { // the else branch leaves s live, // so cleanup must be conditional 2 } 0 } maybe(false)
Fewer mystery moves, fewer forgotten drops.- The day's release, in one line
Owned struct fields now get more precise drop treatment. Moving one field leaves its siblings available, while the moved path is tracked separately for cleanup.
Assignment to a moved field also reopens that field for ordinary use. The release tests check both the open-field drop and the flag reset after replacement.
struct Person { let name: String let title: String let age: Int } // person owns multiple fields with // different drop needs func f() -> Int { let person = Person( name: "Pat" + "", title: "Dr" + "", age: 41 ) // name moves out of person.name let name = person.name // person.title and person.age // remain usable after the field // move name.length + person.title.length + person.age } f()
struct Person { let name: String let title: String } func f() -> Int { let person = Person( name: "Pat" + "", title: "Dr" + "" ) // old receives the original // person.name let old = person.name // assigning person.name // reinitializes that field person.name = "Sue" + "" // the old value, new field value, // and sibling title are all read old.length + person.name.length + person.title.length } f()
func f() -> Int { loop { // s owns the concatenated String // inside the loop body let s = "a" + "b" // break now exits only after the // owned loop local is dropped break } 0 } f()
Borrow types are now part of the surface language: &T for shared access and &mut T for exclusive access. Calls can auto-borrow owned arguments for shared parameters without stamping the argument expression itself as borrowed.
Methods also gained clearer receiver modes. Plain methods receive shared self, mut func receives mutable self, and attempts to assign through shared self are diagnosed.
// no-core struct String { let length: Int } // len accepts a shared borrow of // String func len(s: &String) -> Int { s.length } // s is passed to len by // auto-borrow let s = String(length: 4) // s remains usable after the // borrowed call let y = len(s) let z = s.length
// no-core struct Counter { let n: Int // mut func gives bump mutable // access to self mut func bump() -> () { // self.n assignment is allowed // only through the mutable // receiver self.n = 2 () } }
The parser, type checker, formatter, and highlighter now understand consuming func. Tests updated protocol and method examples where the receiver is intentionally taken by value.
That matters for generic methods and protocol dispatch: a consuming requirement says the call may use up the receiver, while an ordinary method reads through a shared self.
struct Wrapper<T> { let wrapped: T // getWrapped is declared // consuming, so self is taken by // value consuming func getWrapped() { // self.wrapped can be returned // from the consumed wrapper self.wrapped } } // the same consuming generic // method works for Float too Wrapper(wrapped: 123).getWrapped() Wrapper(wrapped: 1.23).getWrapped()
protocol Addy { associated RHS associated Ret // the protocol requirement marks // addy as consuming consuming func addy(rhs: RHS) -> Ret } extend Int: Addy { // Int's conformance matches the // consuming receiver mode consuming func addy(rhs: Int) -> Int { self } } // the member call dispatches // through that consuming // requirement 1.addy(2)
Several tools got more direct: talk bytecode dumps the scheduled VM module, talk build can emit bytecode or link a standalone executable, and run-bytecode runs a serialized image.
Formatting also gained a width option for wrapping comments, and talk llm now prints the compact language reference used by tool-facing helpers.