fix(0112): out-of-range int literals error instead of silently wrapping

checkIntLiteralFits range-checks a literal against its integer target
(builtins + custom widths via intLiteralRange; width-64 types skip —
every representable literal is a legal bit pattern there) and diagnoses
with the type's range and an xx/cast hint. Wired into the .int_literal
arm (covers decls, assignments, call args, struct-literal fields),
lowerStructConstant, and globalInitValue.

A negated literal now folds to a single constant so -128 range-checks
as -128 rather than as an out-of-range +128 intermediate. An explicit
xx operand skips the check — truncation stays available on request
(cast(T) was already exempt: its value arg lowers without the target).

examples/0300-closures-lambda.sx pinned 133 wrapping to -3 through an
s3 param — the exact class this outlaws; updated to a fitting value.

Found during the fix and filed separately: issue 0113 (negated-literal
global initializers rejected as non-constant; pre-existing).

Regressions: examples/1156-diagnostics-int-literal-out-of-range.sx,
examples/0174-types-int-literal-boundaries.sx.
This commit is contained in:
agra
2026-06-10 22:28:24 +03:00
parent fea5617e4e
commit 67313e1dad
16 changed files with 240 additions and 11 deletions

View File

@@ -1,3 +1,35 @@
# RESOLVED — 0112: out-of-range int literal silently wraps into a narrower annotated target
**Root cause:** the `.int_literal` arm adopted an integer `target_type` with no
fits-check, truncating at emission width; `globalInitValue` serialized literal
global initializers raw the same way.
**Fix:** `Lowering.checkIntLiteralFits` (src/ir/lower.zig) range-checks a
literal against its integer target (`intLiteralRange`: builtins + custom
widths; width-64 types skip — every representable literal is legal there) and
diagnoses `integer literal N does not fit in T (range lo..hi) — use an
explicit `xx` / `cast` to truncate`. Wired into the `.int_literal` arm,
`lowerStructConstant`, and `globalInitValue`. A negated literal now folds to
one constant (`-128` checks as -128, not as an out-of-range +128
intermediate), and an explicit `xx` operand skips the check
(`suppress_int_fit_check`) — truncation stays available on request;
`cast(T)` was already exempt (its value arg lowers without the target).
Coverage via the shared arm: decls, assignments, call args, struct-literal
fields, struct constants, globals.
**Behavior change:** `examples/0300-closures-lambda.sx` passed `133` to an
`s3` param and pinned the wrapped `-3`; updated to a fitting value.
**Regression tests:** `examples/1156-diagnostics-int-literal-out-of-range.sx`
(both faces diagnosed in one run) and
`examples/0174-types-int-literal-boundaries.sx` (extreme in-range values,
width-64 types, `xx`/`cast` escapes, call args).
**Found during the fix:** negated-literal GLOBAL initializers (`g : s64 = -1;`)
are rejected as non-constant — pre-existing gap, filed as issue 0113.
---
# 0112 — out-of-range int literal silently wraps into a narrower annotated target
**Symptom.** An integer literal that does not fit its explicitly-annotated

View File

@@ -1,8 +0,0 @@
#import "modules/std.sx";
main :: () {
x : s8 = 300;
print("x: {}\n", x);
y : u8 = 256;
print("y: {}\n", y);
}

View File

@@ -0,0 +1,56 @@
# 0113 — negative-literal global initializer rejected as "not a compile-time constant"
**Symptom.** A top-level global initialized with a negated literal fails to
compile: `g : s64 = -1;` errors
`global 'g' must be initialized by a compile-time constant`. Expected: a
negated literal is a compile-time constant; the global serializes to -1.
Positive literals work (`g : s64 = 1;`). Locals are unaffected
(`x : s64 = -1;` inside a function is fine — lowerExpr folds the negate).
## Reproduction
```sx
#import "modules/std.sx";
g : s64 = -1;
main :: () {
print("{}\n", g);
}
```
- **Observed**: `error: global 'g' must be initialized by a compile-time
constant` at the initializer.
- **Expected**: compiles; prints `-1`.
Repro co-located: `issues/0113-negative-literal-global-initializer-rejected.sx`.
## Root cause (suspected area)
`src/ir/lower/decl.zig` `globalInitValue` (~973): the initializer switch has
arms for `.int_literal` / `.float_literal` / `.bool_literal` / etc., but a
negated literal is a `.unary_op` node, which falls into the catch-all
`else => "must be initialized by a compile-time constant"`. The identifier
arm already routes module-const values through `constExprValue` (~1013) —
the direct `.unary_op` / `.binary_op` initializer shapes never get that
chance.
## Investigation prompt (paste into a fresh session)
> Fix issue 0113: `g : s64 = -1;` (and const-expression initializers like
> `g : s64 = 2 + 3;`) are rejected as non-constant globals. In
> `src/ir/lower/decl.zig` `globalInitValue`, route `.unary_op` and
> `.binary_op` initializers through the same const-expression evaluation the
> `.identifier` arm uses (`constExprValue`, or the
> `program_index.evalConstFloatExpr`-family used by `typedConstInitFits`
> ~878) before falling into the catch-all diagnostic. Apply the int-literal
> fits-check (`checkIntLiteralFits`) to the folded value against the
> global's type — `g : s8 = -300;` must produce the range diagnostic, not a
> wrap and not "non-constant". Negative bounds in `typedConstInitFits`
> already admit unary_op shapes; keep both checks consistent.
>
> Verify: the repro prints -1; `g2 : s8 = -300;` errors with the range
> message; `g3 : s32 = 2 + 3;` initializes to 5 (or, if expression globals
> are deliberately unsupported, keeps a SPECIFIC diagnostic saying so).
> `zig build && zig build test && bash tests/run_examples.sh`. Promote the
> repro per the resolution flow.

View File

@@ -0,0 +1,4 @@
#import "modules/std.sx";
g : s64 = -1;
main :: () { print("{}
", g); }