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

4.6 KiB

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

#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-...).