✓
Passing This code compiles and runs correctly.
Code
// PIN: a discharged RECORD FIELD read through a `{{ }}` interpolation escapes
// the use-after-discharge wall.
//
// Both halves work on their own, which is what makes this the two-lowerings
// shape rather than a missing feature:
//
// plain binding + argument -> caught (335_024, 335_025, 335_047)
// plain binding + interpolation -> caught
// record field + argument -> caught ("binding 'r.k' was already
// discharged" — the composite key IS the
// name the checker reports)
// record field + interpolation -> THIS TEST
//
// phantom_semantic_checker.zig's interpolation scanner walks a slot for
// identifiers and skips any segment preceded by `.`, on the stated ground that
// "a trailing `.field` names a field, not a binding". True for an ordinary
// record — and the exact case that misses here, because an obligation carried
// on a record field is disposed under `r.k`, a name the scan never forms.
//
// Found while testing whether a borrow could be tied to its owner by minting
// two obligations and discharging them together (610_007's open question). The
// tie held; this is what leaked out from under it.
import app/holder
import std/io
app/holder:grab(name: "x"): r |> app/holder:drop(h: r.k) |> std/io:print.ln("{{ r.k:s }}")
Must contain:
Use-after-dischargeFlows
flow ~grab click a branch to expand · @labels scroll to their anchor
grab (name: "x")
Imported Files
// An obligation that arrives as a RECORD FIELD, so the discharge is tracked
// under the composite key `r.k` rather than a plain binding name.
const std = @import("std");
var buf: [16]u8 = undefined;
~pub tor grab { name: string } -> { k: string<held!>, n: u32 }
~proc grab|zig {
_ = name;
@memcpy($mod.buf[0..3], "abc");
return .{ .k = $mod.buf[0..3], .n = 7 };
}
~pub tor drop { h: string<!held> }
~proc drop|zig {
_ = h;
}