ref argument beside it (NK2302 on a struct-literal store; rustc “missing lifetime” through a method)Open and answer this issue on GitHub.
bug, ready, wip · opened 2026-10-31 · updated 2026-10-31 · 0 comments
agent: builder
model: opus
max_turns: 90
budget_usd: 18
—
Problem. A function that takes a by-value view-holding struct n: N and a ref Bytes parameter, and returns an N built from n, has its result attributed to the ref parameter, not to n. nikaia check --tethers prints borrowed <result> - from the caller's argument for input; n is not named. Two faces:
out.push(E { key: leaf(e.key, input) }), recursive or not) is refused NK2302: walk keeps input after this call, but input: ref Bytes doesn’t say whose data it points into. input is never viewed.struct H { input: ref Bytes }, called from fn keep(n: N, input: ref Bytes) -> N { let h = H { input: input }; return h.walk(n) }, passes check, and rustc refuses the lowered keep: Missing lifetime specifiers (it cannot tell whether the result borrows from n’s lifetimes or from input).Found by the yaml lane (y31, merge(node: Node, input: ref Bytes) -> Node, #928 step 4). Checked by the architect on target/fast/nikaia at 930ef6eb.
Probe, face 1 (nikaia run p.nika; wanted hello 1):
struct N { raw: String, kids: Vec[E] }
struct E { key: N }
fn leaf(n: N, input: ref Bytes) -> N sync { return n }
fn walk(n: N, input: ref Bytes) -> N sync {
let mut out: Vec[E] = []
for e in n.kids.drain() { out.push(E { key: leaf(e.key, input) }) }
return N { raw: n.raw, kids: out }
}
fn make(s: String) -> N {
let t = s.trim()
return N { raw: t, kids: [E { key: N { raw: t, kids: [] } }] }
}
fn main() {
let text = " hello "
let input = Bytes(text.bytes().collect())
let m = walk(make(text), input)
print(f"{m.raw} {m.kids.len()}")
}
Rows: a the probe (NK2302 at the push); b the call recursive, E { key: walk(e.key, input) } (NK2302); c let w = walk(e.key, input) then out.push(E { key: w }) (NK2302); d kids: Vec[N] with out.push(walk(k, input)), no struct literal (runs today, must stay); e face 2 below.
Probe, face 2 (wanted hello 1; rustc refuses keep today):
struct N { raw: String, tag: String, kids: Vec[E] }
struct E { key: N }
struct H { input: ref Bytes }
impl H {
fn walk(ref self, n: N) -> N sync {
let mut out: Vec[E] = []
for e in n.kids.drain() { out.push(E { key: self.walk(e.key) }) }
return N { raw: n.raw, tag: n.tag, kids: out }
}
}
fn keep(n: N, input: ref Bytes) -> N sync {
let h = H { input: input }
return h.walk(n)
}
fn make(s: String) -> N {
let t = s.trim()
return N { raw: t, tag: t, kids: [E { key: N { raw: t, tag: t, kids: [] } }] }
}
fn main() {
let text = " hello "
let input = Bytes(text.bytes().collect())
let m = keep(make(text), input)
print(f"{m.raw} {m.kids.len()}")
}
Suspected location (medium confidence for face 1, low for face 2).
crates/nikaia-std/src/tools/views.nika, mentions (the Expr::Call / Expr::MethodCall arms count every argument that names a carrier) reached from a_field_built through value_mentions. A call’s argument should count only where the callee’s contract says its result can point into that parameter (FnContract.borrows / returns, ADR-353 D1; kept_by_callee beside it reads keeps the same way). Then check what borrows holds for leaf: if it lists input and not n, the ledger’s result sources are wrong too (tether.nika, tethers_report lists only parameters that are views or lends_in; find where borrows is filled for a by-value view-holding parameter, ADR-277 D17).keep (two lent candidates, n: &N<'_,..> and input: &[u8]). The wanted lowering ties the result to n’s lifetimes only, any lowering rustc accepts.hello 1, d unchanged. Test tests/compiler/tests/call_result_views_its_struct_argument.nika + .stdout, refused by the unfixed binary.hello 1; its row in the same test or a second test beside it.tests/compiler --both-settings green; the view tests (it text_views stored_views tethers views_kept) green.Generated by Claude Code