The numeric-limit accessor intercept (NL.1 integer `.min`/`.max`, NL.2 float `.epsilon`/`.min_positive`/`.true_min`/`.inf`/`.nan`) treated ANY receiver whose text matched a builtin numeric type name as a TYPE receiver, without first checking for 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 (issue 0092). Fix: for `.identifier` receivers, prefer an in-scope value binding (`Scope.lookup`) over the fold — defer to ordinary field lowering when the identifier resolves to a value. `.type_expr` receivers are unambiguous types and are never shadowed, so a bare `f64.epsilon`/`s32.max` still folds even in a scope where `` `f64 `` is bound (the parser classifies a bare builtin name as a `.type_expr`). Mirrored in expr_typer.zig so inference matches lowering (avoids the issue-0083 two-resolver desync). Float-only-on-int and non-numeric-receiver errors are unchanged. - src/ir/lower.zig: value-binding guard in lowerNumericLimit. - src/ir/expr_typer.zig: same guard in the numeric-limit inference arm. - src/ir/expr_typer.test.zig: unit test pinning the two-resolver agreement. - examples/0161-types-numeric-limit-value-shadow.sx: regression — raw `` `f64 ``/`` `s32 ``/`` `u8 `` value reads coexisting with bare folds. - issues/0092: RESOLVED banner. - specs.md / readme.md: receiver-vs-shadowing-value-binding note.
78 lines
3.7 KiB
Markdown
78 lines
3.7 KiB
Markdown
> **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.zig` — `lowerNumericLimit`: 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` / `s32.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 ``/`` `s32 ``/`` `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` / `s32.max`). Expected:
|
|
it prints `12 78` from the `Box` fields.
|
|
|
|
## Reproduction
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
|
|
Box :: struct { epsilon: s64; max: s64; }
|
|
|
|
main :: () -> s32 {
|
|
`f64 := Box.{ epsilon = 12, max = 34 };
|
|
`s32 := Box.{ epsilon = 56, max = 78 };
|
|
print("{} {}\n", `f64.epsilon, `s32.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` / `s32.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`.
|