Files
sx/issues/0092-raw-reserved-value-numeric-limit-shadow.md
agra d8076b9333 lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.

Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).

Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.

zig build test: 426/426; examples suite: 595/595.
2026-06-12 09:31:53 +03:00

3.7 KiB

RESOLVED (NL.2 attempt 3). Root cause: the numeric-limit accessor intercept treated ANY receiver whose text matched a builtin numeric type name as a TYPE receiver, without first checking whether that identifier resolved to an in-scope VALUE binding. An F0.6 backtick raw identifier (`f64 := …) binds a local under the stripped name f64; field access on it (`f64.epsilon) parses as an .identifier receiver, which the intercept silently folded to the type's numeric limit — a silent-wrong-value bug.

Fix (value-binding precedence for .identifier receivers; .type_expr receivers are unambiguous types and never shadowed):

  • src/ir/lower.ziglowerNumericLimit: after confirming the receiver is a builtin numeric type name and the field is a limit accessor, return null (defer to ordinary field lowering) when fa.object is an .identifier that Scope.lookup resolves to a value binding.
  • src/ir/expr_typer.zig — numeric-limit inference arm: mirror the same guard so inferred types match lowering (avoids the issue-0083 two-resolver desync).

Bare f64.epsilon / i32.max (no shadowing binding) still fold — the parser classifies a bare builtin name as a .type_expr (parser.zig:2743), so the bare receiver is never value-shadowed even in a scope where `f64 is bound. Float-only-on-int and non-numeric-receiver errors are unchanged.

Regression: examples/0161-types-numeric-limit-value-shadow.sx (raw `f64/`i32/`u8 value reads coexisting with bare folds) + unit test in src/ir/expr_typer.test.zig. NL.1 (examples/0148) / NL.2 (examples/0159, examples/0160) unregressed.

0092 — numeric-limit intercept hijacks raw reserved-spelled value receivers

Symptom

Field access on a raw reserved-spelled value binding is interpreted as a builtin type numeric-limit access instead of an ordinary value field access. Observed: the repro prints 0.000000 2147483647 (f64.epsilon / i32.max). Expected: it prints 12 78 from the Box fields.

Reproduction

#import "modules/std.sx";

Box :: struct { epsilon: i64; max: i64; }

main :: () -> i32 {
    `f64 := Box.{ epsilon = 12, max = 34 };
    `i32 := Box.{ epsilon = 56, max = 78 };
    print("{} {}\n", `f64.epsilon, `i32.max);
    return 0;
}

Investigation prompt

Investigate issue 0092: raw reserved-spelled value receivers are being captured by the numeric-limit accessor intercept. In src/ir/lower.zig, start at Lowering.lowerFieldAccess and Lowering.lowerNumericLimit (currently around lower.zig:4826 / lower.zig:4923). The intercept treats any .identifier/.type_expr receiver whose text is a builtin type name as a type receiver, without first checking whether the identifier resolves to a value binding in the current lexical scope. Mirror the fix in src/ir/expr_typer.zig around the numeric-limit inference arm so inferred types match lowering.

Likely fix: for .identifier receivers, prefer an in-scope value binding (Scope.lookup) over the builtin-type numeric-limit intercept; keep the intercept for actual type receivers (.type_expr, and bare reserved integer names with no value binding). If raw provenance is available in the AST, using it to disambiguate is also acceptable, but the observable rule must be that `f64.epsilon reads the value field when `f64 is a value binding, while bare f64.epsilon / i32.max still fold as numeric limits.

Verification: pin a regression test from the repro above. It should print 12 78. Also verify the existing numeric-limit examples still pass: examples/0148-types-int-numeric-limits.sx, examples/0159-types-float-numeric-limits.sx, and the negative diagnostics in examples/0160-types-float-numeric-limits-errors.sx.