Ownership Gets Its Timing Right
Talk now does a sharper job of moving, borrowing, dropping, and showing those facts at the right moment.
Today's release tightens the route from source ownership to lowered code and editor feedback. The big story is timing: owned locals drop when their scopes end, moved values stay moved, and shared compiler paths stay shared instead of multiplying behind the curtain.
The lowering tests now insist that owned values made inside if branches are freed before control rejoins the common tail. In the checked IR, both branch-local strings are freed, and the final continuation appears only once.
That means Talk can keep cleanup precise without duplicating the rest of the function for every branch.
func f(a: Bool, b: Bool) -> Int { if a { // Local s is owned only by this // branch let s = "a" + "b" } else { // Local t is the matching owned // value from the other branch let t = "c" + "d" } if b {} else { } // The final value is reached // after branch cleanup rejoins 123 } f(true, false)
A function that computes a tail value after creating an owned string now lowers through a drop_scope wrapper. The changed test checks that the string is freed before the return continuation receives the final integer.
The visible promise is simple: returning a value does not skip the owned locals waiting at the door.
func f() -> Int { // Owned local s needs cleanup at // function scope exit let s = "a" + "b" // The tail value must survive // while cleanup runs first 123 } // Calling f exercises the lowered // return path f()
Drops happen where the story says they should.- The day's release, in one line
func f(a: Bool, b: Bool) -> Int { if a { // The then branch creates owned // local s inside a short scope let s = "a" + "b" } else { // The else branch creates owned // local t with the same cleanup // problem let t = "c" + "d" } // Both branches rejoin before the // later if and final result if b {} else { } 123 } f(true, false)
Sequential statement-level ifs now share their lowered tail instead of copying it again and again. The changed lowering test counts occurrences of the final continuation and expects only one or two, not a little parade of duplicates.
This is not a new syntax feature, but it is a compiler invariant users can feel in steadier generated IR.
func f(a: Bool, b: Bool, c: Bool, d: Bool) -> Int { // The first empty branch should // not clone the whole remaining // tail if a {} else { } if b {} else { } if c {} else { } // Four sequential branches stress // the join sharing if d {} else { } // The single final result is the // tail being shared 123 } f(true, false, true, false)
Type checking now keeps auto-borrowing at the immediate call boundary instead of letting it leak under nested type constructors or contravariant function parameters. Tests reject borrowed returns where owned values are required, while still allowing a mutable borrow return to downgrade to a shared borrow return.
The result is a clearer split: calls may borrow an argument for the call, but nested boxes and function slots must still match their real ownership shape.
// no-core struct String { let length: Int } // The function promises a shared // borrow result func as_shared(s: &mut String) -> &String { // Returning s downgrades the // mutable borrow to shared s } // A function value returning &mut // String fits a shared-return slot let f: (&mut String) -> &String = func(s: &mut String) -> &mut String { s }
Hover output and LSP inlay hints now read from ownership facts, showing moves, borrows, and drops in the editor. Tests check for labels such as borrow, move, and drop, and for hover text that marks owned values.
Diagnostics also keep going file by file: ownership errors can survive beside unrelated type errors in another document.