Issue #1101: A tuple match/if with a null end and a text-literal end is refused NK1104 when the same parameter also takes an owned text

Open and answer this issue on GitHub.

ready, wip · opened 2026-10-31 · updated 2026-10-31 · 0 comments

Found by b20 (#1088, docs/briefs/bugs/b20-1088-tuple-option-part-in-arms.md.report.md). A wrong refusal; checked by the architect on target/fast/nikaia at aa1ca981.

Probe

fn other(t: String) -> (i32, String?) { return (2, t.clone()) }
fn literal(x: i32) -> (i32, String?) { return match x { 0 => (1, null), else => (2, "lit") } }
fn show(r: (i32, String?)) {
    let (a, b) = r
    println(f"{a} {b ?? "none"}")
}
fn main() {
    show(literal(1))
    show(other("t"))
}

Expected 2 lit / 2 t. Got NK1104 You're returning (?, ref String), but the function is declared to return (i32, String?)`` at the match.

Shapes (all with other and show as above): | literal’s body | result | | :— | :— | | return match x { 0 => (1, null), else => (2, "lit") } | NK1104 (?, ref String) | | return if x == 0 { (1, null) } else { (2, "lit") } | NK1104 (?, ref String) | | let n: i32 = 1 / return match x { 0 => (n, null), else => (n, "lit") } | NK1104 (i32, ref String) | | return match x { 0 => (1, "a"), else => (2, "lit") } | passes | | two return statements, (1, null) and (2, "lit") | passes | | the first row without show(other("t")) | passes | | the first row, results bound by let and read by .0 | passes |

So: a null end beside a text-literal end, and the same parameter also fed an owned text.

Suspected location (medium confidence, not traced to the line)

crates/nikaia-std/src/tools/check_core_leaves.nika, the return check (around line 685-729): found is made a view at if lending (line 686) or by lent_arms_handed_back (line 693), then check_core_literal_by_use (line 413) reaches check_texts.nika tuple_literal, whose ends branch (line 404-425) hands every end the joined found (plain) instead of the end’s own type; b20 fixed the same shape in check_overflow_ends_into_nullables by reading each end’s type from state.arm_types. Why the owned caller of show switches it on is the text tiers (text_tiers.nika): find which fact differs between the probe with and without show(other("t")).

To build

  1. Trace the probe with and without show(other("t")) to the fact that differs; fix the refusal at its cause.
  2. Test tests/compiler/tests/tuple_null_end_owned_caller.nika + .stdout: rows 1-3 of the table, each fed to one parameter beside an owned result; fails on the unfixed binary.

Generated by Claude Code

Back to Open Issues