Issue #929: A struct that holds a text read at run time and hands out views of it: the checker accepts, rustc refuses; ref String field refuses the text (blocks a pull parser, #928)

Open and answer this issue on GitHub.

wip · opened 2026-10-28 · updated 2026-10-29 · 4 comments

Found while

Checking what the yaml package (#928) needs from the language: a pull / event parser is a struct that holds the input and a position, and whose next returns views into the input. With target/fast/nikaia on develop (2026-10-28):

Probe 1: field input: String, the text read at run time

use std::fs

enum Value {
    Str(String),
    Seq(Vec[Value]),
}

struct Cursor {
    input: String,
    pos: i64,
}

fn next_item(mut c: Cursor) -> String? {
    let rest = c.input.lines().skip(c.pos).collect()
    if rest.len() == 0 {
        return null
    }
    c.pos += 1
    return rest[0].trim()
}

fn parse(input: String) -> Value {
    let mut items: Vec[Value] = []
    for l in input.lines() {
        items.push(Value::Str(l.trim()))
    }
    return Value::Seq(items)
}

fn main() throws {
    let text = fs::read_to_string("in.yaml", fs::Root::Anywhere)
    let v = parse(text)
    let mut c = Cursor { input: text, pos: 0 }
    while true {
        let s = next_item(c)
        if s == null { break }
        println(f"pull: {s}")
    }
    match v {
        Value::Seq(items) => {
            for it in items {
                match it {
                    Value::Str(s) => println(s),
                    else => println("?"),
                }
            }
        },
        else => println("?"),
    }
}

nikaia check accepts it; nikaia run stops in rustc:

error: Cannot move out of `text` because it is borrowed.   --> 33: let mut c = Cursor { input: text, pos: 0 }
error: Lifetime may not live long enough.                  --> 19: return rest[0].trim()

Lowered: struct Cursor { input: String, .. } and fn next_item(c: &mut Cursor) -> Option<&'static str>. parse alone is fine (fn parse(input: &str) -> Value<'_>, enum Value<'a> { Str(&'a str), .. }): a zero-copy tree works; a struct that holds the text and hands out views of it does not.

Expected (ADR-353, ADR-283 D25): next_item’s result points into c; giving text to Cursor while v views it moves the buffer to the keep; either the program runs, or the checker refuses it with a Nikaia diagnostic. A program the checker accepts never reaches rustc’s errors.

Probe 2: the same with input: ref String

warning[NK1261]: `ref` is redundant here: a field written `ref String` says what `String` already says.
error[NK1106]: `Cursor.input` holds `ref String`, but you're giving it `String`.   --> Cursor { input: text, pos: 0 }

The warning says ref changes nothing; the error says it does. With a literal (let input = "…") the ref String form runs (Held pattern, examples/1brc.nika).

To build

  1. Probe 1 runs (the view next_item returns points into c.input; Cursor lowers with a lifetime or the keep), or is refused before rustc with a diagnostic naming the view, as a test in tests/compiler/.
  2. A ref String field takes a String local read at run time, as it takes a literal; or NK1261 no longer calls ref redundant where it changes what is accepted. Test beside 1.

The commit that builds the last step says Fixes #N.

Comments

keywan-ghadami, 2026-10-28

23336d8 (0.0.1261) builds both steps for the pull-parser shape:

  1. A view of the text in a mut struct parameter is a view of that parameter (rsrc_a_source, views.nika): fn next_item(mut c: Cursor) -> String? now has ref(c) in the ledger and lowers to -> Option<&str> instead of &'static str. Test a_mut_struct_parameter_hands_out_a_view_of_its_text.
  2. Held { input: text } with input: ref String lends text, as ref text does (check_core_places.nika), no more NK1106. Test text_given_to_a_ref_string_field_is_lent.

Still open (why the issue stays open): probe 1 as written also gives text to Cursor while v = parse(text) still views it. That stops in rustc (Cannot move out of text because it is borrowed), not in the checker. ADR-353 says the buffer moves to the keep there; until that is built, the checker should at least refuse it with a Nikaia diagnostic. The same holds for let h = Held { input: text }; take(text); use(h) and its ref text spelling, which behaved the same before this change.

To build (left):

  1. Giving text to a new owner while a view of it lives: the buffer moves to the keep (ADR-353, ADR-283 D25), or the checker refuses it before rustc. Test with probe 1 from above.

Generated by Claude Code

keywan-ghadami, 2026-10-28

The same contradiction in a grammar rule’s result type

Found while trying a YAML line scanner as a grammar (#928), target/fast/nikaia at 0.0.1261:

grammar Lines {
    rule WS = "" { }
    rule Line -> (String, String) = ind:text(" "*) body:until(line_ending) line_ending { (ind, body) }
    entry rule doc -> Vec[(String, String)] = ls:Line* eof { ls }
}

fn main() throws {
    let text = "a:\n  b: 1\n  c: two\n"
    for (ind, body) in Lines::doc(text) {
        println(f"[{ind}] {body}")
    }
}
error[NK1104]: This action builds `(ref String, ref String)`, but its rule returns `(String, String)`.
   --> 3:90
    = help: Make it a `(String, String)`, or change the declaration to `(ref String, ref String)`.

With (ref String, ref String) it runs and prints [] a:, [ ] b: 1, [ ] c: two, but every ref String then gets NK1261 (ref is redundant here … says what String already says). Where the spec says a String result means what ref String means (NK1261, ADR-282 D2), a rule declared -> (String, String) should take the views text(…) and until(…) yield, as a plain -> String rule does; the compiler holds it to the view.

To build (added):

  1. A grammar rule whose result type holds String (in a tuple, a struct, a Vec) takes the views its action builds, as NK1261 says it means the same; test with the program above, no ref written.

Generated by Claude Code

keywan-ghadami, 2026-10-29

Stand an 0.0.1276:


Generated by Claude Code

keywan-ghadami, 2026-10-29

Schritt 4 ist auf develop (2f73c24, 0.0.1279). Ein Tupel aus String im Ergebnis einer Grammatikregel oder Funktion nimmt Views an, ohne ref. Struct, Enum, Vec und T? konnten das schon. Schritt 3 ist mit #948 im selben Commit erledigt. Der Commit sagt Fixes #948, #929.

Noch offen, aber nicht Teil dieses Issues: Wird ein solches Tupel in einem pub-Feld gehalten, kommt NK1104, und dessen Hilfe schlägt noch ref vor. Das widerspricht ADR-282 D2.


Generated by Claude Code

Back to Open Issues