Issue #1094: A call result built from a by-value view-holding argument is charged to a 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:

  1. Its call stored into a field of a view-holding struct literal (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.
  2. The same walk as a method of 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).

To build

  1. Face 1 rows a-c print hello 1, d unchanged. Test tests/compiler/tests/call_result_views_its_struct_argument.nika + .stdout, refused by the unfixed binary.
  2. Face 2 prints hello 1; its row in the same test or a second test beside it.
  3. tests/compiler --both-settings green; the view tests (it text_views stored_views tethers views_kept) green.

Generated by Claude Code

Back to Open Issues