Files
sx/issues/0112-int-literal-out-of-range-silent-wrap.md
agra 67313e1dad 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.
2026-06-10 22:28:24 +03:00

99 lines
4.6 KiB
Markdown

# 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
integer target truncates with no diagnostic: `x : s8 = 300;` binds 44,
`y : u8 = 256;` binds 0. Expected: a compile-time error (the value is known
exactly at compile time; this is the integer analogue of the float→int
narrowing rule, which errors on non-exact `y : s64 = 1.5`).
Split from issue 0111 (whose fix removed the *implicit* narrowing — an
unannotated `x := 0` no longer adopts the fn return type — but the explicit
annotation path keeps wrapping).
## Reproduction
```sx
#import "modules/std.sx";
main :: () {
x : s8 = 300;
print("x: {}\n", x);
y : u8 = 256;
print("y: {}\n", y);
}
```
- **Observed** (current master): prints `x: 44` / `y: 0`, exit 0, no
diagnostic.
- **Expected**: compile error per literal, e.g.
`integer literal 300 does not fit in s8 (range -128..127)`, and the analog
for `256` / `u8 (range 0..255)`.
Repro co-located: `issues/0112-int-literal-out-of-range-silent-wrap.sx`
(unpinned until fixed).
## Root cause (suspected area)
`src/ir/lower/expr.zig` `.int_literal` arm (~1499): when `target_type` is an
integer type, it emits `constInt(lit.value, tt)` with no fits-check — the
value truncates at LLVM emission width. The annotated-decl path
(`lowerVarDecl` with `type_annotation`, `src/ir/lower/stmt.zig` ~255) sets
`target_type` to the annotation before lowering the initializer, so every
annotated narrow decl funnels through this arm. Assignments to narrow
lvalues (`b = 300` where `b: s8`) reach the same arm via `lowerAssignment`'s
LHS-derived target and likely need the same check.
## Investigation prompt (paste into a fresh session)
> Fix issue 0112: an int literal that does not fit its integer target type
> silently wraps. In the `.int_literal` arm of `lowerExpr`
> (`src/ir/lower/expr.zig` ~1499), before adopting an integer `target_type`,
> range-check `lit.value` against the target's signedness/width (the type
> table knows both; mirror the bounds logic used by
> `TypeResolver.integerLimitFor`). On overflow emit a diagnostic via
> `self.diagnostics.addFmt(.err, node.span, ...)` naming the literal, the
> type, and its range — do NOT silently fall back to s64 (REJECTED PATTERNS:
> no silent fallback defaults); still return a `constInt` of the target type
> so lowering continues to surface further errors. Audit sibling literal
> sinks that bypass this arm (comptime folds, `lowerStructConstant`, global
> initializers) for the same check.
>
> Verify: `issues/0112-int-literal-out-of-range-silent-wrap.sx` errors with
> two diagnostics (s8/300, u8/256); boundary values still compile
> (`x : s8 = -128` / `127`, `y : u8 = 0` / `255`, `m : u64` large literals).
> `zig build && zig build test && bash tests/run_examples.sh` — any example
> that relied on silent wrapping must be reviewed individually. Promote the
> repro per the resolution flow (likely `examples/11xx-diagnostics-...`).