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:
22
examples/0174-types-int-literal-boundaries.sx
Normal file
22
examples/0174-types-int-literal-boundaries.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Boundary and exemption cases for the int-literal fits-check: extreme
|
||||||
|
// in-range values compile (incl. negated literals via the constant fold);
|
||||||
|
// width-64 types accept any representable literal; explicit `xx` / `cast`
|
||||||
|
// still truncate on request; literal call args check against param types.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
clamp_s8 :: (v: s8) -> s8 { v }
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
a : s8 = -128;
|
||||||
|
b : s8 = 127;
|
||||||
|
c : u8 = 0;
|
||||||
|
d : u8 = 255;
|
||||||
|
e : u64 = 0x7FFFFFFFFFFFFFFF;
|
||||||
|
f : u32 = 0xFFFFFFFF;
|
||||||
|
g : s16 = -32768;
|
||||||
|
h : s8 = xx 300; // explicit truncation stays legal
|
||||||
|
i := cast(s8) 300; // cast form too
|
||||||
|
j : s8 = clamp_s8(-5);
|
||||||
|
print("{} {} {} {} {} {} {} {} {} {}\n", a, b, c, d, e, f, g, h, i, j);
|
||||||
|
}
|
||||||
@@ -5,5 +5,5 @@ main :: () {
|
|||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
print("{}\n", fx(133));
|
print("{}\n", fx(-3));
|
||||||
}
|
}
|
||||||
|
|||||||
12
examples/1156-diagnostics-int-literal-out-of-range.sx
Normal file
12
examples/1156-diagnostics-int-literal-out-of-range.sx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// An integer literal that does not fit its integer target type is a
|
||||||
|
// compile error (no silent wrap): both faces diagnosed in one run.
|
||||||
|
// Regression (issue 0112): `x : s8 = 300` bound 44, `y : u8 = 256` bound 0.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
x : s8 = 300;
|
||||||
|
print("x: {}\n", x);
|
||||||
|
y : u8 = 256;
|
||||||
|
print("y: {}\n", y);
|
||||||
|
}
|
||||||
1
examples/expected/0174-types-int-literal-boundaries.exit
Normal file
1
examples/expected/0174-types-int-literal-boundaries.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-128 127 0 255 9223372036854775807 4294967295 -32768 44 44 -5
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
error: integer literal 300 does not fit in s8 (range -128..127) — use an explicit `xx` / `cast` to truncate
|
||||||
|
--> examples/1156-diagnostics-int-literal-out-of-range.sx:8:14
|
||||||
|
|
|
||||||
|
8 | x : s8 = 300;
|
||||||
|
| ^^^
|
||||||
|
|
||||||
|
error: integer literal 256 does not fit in u8 (range 0..255) — use an explicit `xx` / `cast` to truncate
|
||||||
|
--> examples/1156-diagnostics-int-literal-out-of-range.sx:10:14
|
||||||
|
|
|
||||||
|
10 | y : u8 = 256;
|
||||||
|
| ^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
# 0112 — out-of-range int literal silently wraps into a narrower annotated target
|
||||||
|
|
||||||
**Symptom.** An integer literal that does not fit its explicitly-annotated
|
**Symptom.** An integer literal that does not fit its explicitly-annotated
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
#import "modules/std.sx";
|
|
||||||
|
|
||||||
main :: () {
|
|
||||||
x : s8 = 300;
|
|
||||||
print("x: {}\n", x);
|
|
||||||
y : u8 = 256;
|
|
||||||
print("y: {}\n", y);
|
|
||||||
}
|
|
||||||
56
issues/0113-negative-literal-global-initializer-rejected.md
Normal file
56
issues/0113-negative-literal-global-initializer-rejected.md
Normal 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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
g : s64 = -1;
|
||||||
|
main :: () { print("{}
|
||||||
|
", g); }
|
||||||
@@ -205,6 +205,7 @@ pub const Lowering = struct {
|
|||||||
break_target: ?BlockId = null,
|
break_target: ?BlockId = null,
|
||||||
continue_target: ?BlockId = null,
|
continue_target: ?BlockId = null,
|
||||||
loop_defer_base: usize = 0, // defer-stack height at the innermost loop's body start (break/continue drain to here)
|
loop_defer_base: usize = 0, // defer-stack height at the innermost loop's body start (break/continue drain to here)
|
||||||
|
suppress_int_fit_check: bool = false, // inside an explicit `xx` cast operand: truncation is requested, skip the literal fits-check
|
||||||
block_counter: u32 = 0,
|
block_counter: u32 = 0,
|
||||||
comptime_counter: u32 = 0,
|
comptime_counter: u32 = 0,
|
||||||
main_file: ?[]const u8 = null, // path of the main file; imported functions are declared extern
|
main_file: ?[]const u8 = null, // path of the main file; imported functions are declared extern
|
||||||
@@ -1129,6 +1130,74 @@ pub const Lowering = struct {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Value range of an integer type, for literal fits-checks. Null for
|
||||||
|
/// 64-bit types — every i64 literal bit pattern is legal there (a 64-bit
|
||||||
|
/// hex literal wraps negative through the lexer's i64 value, so a
|
||||||
|
/// min/max check would false-positive) — and for non-integers.
|
||||||
|
pub fn intLiteralRange(self: *Lowering, ty: TypeId) ?struct { min: i64, max: i64 } {
|
||||||
|
var width: u8 = 0;
|
||||||
|
var is_signed = false;
|
||||||
|
switch (ty) {
|
||||||
|
.s8 => {
|
||||||
|
width = 8;
|
||||||
|
is_signed = true;
|
||||||
|
},
|
||||||
|
.s16 => {
|
||||||
|
width = 16;
|
||||||
|
is_signed = true;
|
||||||
|
},
|
||||||
|
.s32 => {
|
||||||
|
width = 32;
|
||||||
|
is_signed = true;
|
||||||
|
},
|
||||||
|
.u8 => width = 8,
|
||||||
|
.u16 => width = 16,
|
||||||
|
.u32 => width = 32,
|
||||||
|
else => {
|
||||||
|
if (ty.isBuiltin()) return null; // s64/u64/isize/usize/non-int
|
||||||
|
switch (self.module.types.get(ty)) {
|
||||||
|
.signed => |w| {
|
||||||
|
width = w;
|
||||||
|
is_signed = true;
|
||||||
|
},
|
||||||
|
.unsigned => |w| width = w,
|
||||||
|
else => return null,
|
||||||
|
}
|
||||||
|
if (width >= 64) return null;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (is_signed) {
|
||||||
|
const max = (@as(i64, 1) << @intCast(width - 1)) - 1;
|
||||||
|
return .{ .min = -max - 1, .max = max };
|
||||||
|
}
|
||||||
|
const max = (@as(i64, 1) << @intCast(width)) - 1;
|
||||||
|
return .{ .min = 0, .max = max };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diagnose an integer literal that cannot be represented in `ty`
|
||||||
|
/// (REJECTED PATTERNS: no silent wrap). The constant is still emitted by
|
||||||
|
/// the caller so lowering continues and surfaces further errors.
|
||||||
|
pub fn checkIntLiteralFits(self: *Lowering, value: i64, ty: TypeId, span: ast.Span) void {
|
||||||
|
if (self.suppress_int_fit_check) return;
|
||||||
|
const r = self.intLiteralRange(ty) orelse return;
|
||||||
|
if (value < r.min or value > r.max) {
|
||||||
|
if (self.diagnostics) |d| {
|
||||||
|
// Custom-width ints are structural (unnamed in the type
|
||||||
|
// table) — render them as s{N}/u{N}.
|
||||||
|
var name_buf: [8]u8 = undefined;
|
||||||
|
const tn = blk: {
|
||||||
|
if (ty.isBuiltin()) break :blk self.module.types.typeName(ty);
|
||||||
|
break :blk switch (self.module.types.get(ty)) {
|
||||||
|
.signed => |w| std.fmt.bufPrint(&name_buf, "s{d}", .{w}) catch "integer",
|
||||||
|
.unsigned => |w| std.fmt.bufPrint(&name_buf, "u{d}", .{w}) catch "integer",
|
||||||
|
else => self.module.types.typeName(ty),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
d.addFmt(.err, span, "integer literal {} does not fit in {s} (range {}..{}) — use an explicit `xx` / `cast` to truncate", .{ value, tn, r.min, r.max });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Operands valid for a scalar numeric op (`+ - * / %`): ints (incl.
|
/// Operands valid for a scalar numeric op (`+ - * / %`): ints (incl.
|
||||||
/// custom widths), floats, SIMD vectors, and pointers (pointer
|
/// custom widths), floats, SIMD vectors, and pointers (pointer
|
||||||
/// arithmetic). `.unresolved` returns true so a type we couldn't infer
|
/// arithmetic). `.unresolved` returns true so a type we couldn't infer
|
||||||
|
|||||||
@@ -976,7 +976,10 @@ pub fn globalInitValue(self: *Lowering, vd: *const ast.VarDecl, var_ty: TypeId)
|
|||||||
return switch (v.data) {
|
return switch (v.data) {
|
||||||
.undef_literal => .zeroinit,
|
.undef_literal => .zeroinit,
|
||||||
.null_literal => .null_val,
|
.null_literal => .null_val,
|
||||||
.int_literal => |il| .{ .int = il.value },
|
.int_literal => |il| blk: {
|
||||||
|
self.checkIntLiteralFits(il.value, var_ty, v.span);
|
||||||
|
break :blk .{ .int = il.value };
|
||||||
|
},
|
||||||
.bool_literal => |bl| .{ .boolean = bl.value },
|
.bool_literal => |bl| .{ .boolean = bl.value },
|
||||||
// A float initializer at an integer-typed global follows the
|
// A float initializer at an integer-typed global follows the
|
||||||
// implicit narrowing rule (integral folds, non-integral errors).
|
// implicit narrowing rule (integral folds, non-integral errors).
|
||||||
|
|||||||
@@ -627,7 +627,10 @@ pub fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.
|
|||||||
pub fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref {
|
pub fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref {
|
||||||
const val_node = info.value;
|
const val_node = info.value;
|
||||||
return switch (val_node.data) {
|
return switch (val_node.data) {
|
||||||
.int_literal => |lit| self.builder.constInt(lit.value, info.ty orelse .s64),
|
.int_literal => |lit| blk: {
|
||||||
|
if (info.ty) |t| self.checkIntLiteralFits(lit.value, t, val_node.span);
|
||||||
|
break :blk self.builder.constInt(lit.value, info.ty orelse .s64);
|
||||||
|
},
|
||||||
.float_literal => |lit| self.builder.constFloat(lit.value, info.ty orelse .f64),
|
.float_literal => |lit| self.builder.constFloat(lit.value, info.ty orelse .f64),
|
||||||
.bool_literal => |lit| self.builder.constBool(lit.value),
|
.bool_literal => |lit| self.builder.constBool(lit.value),
|
||||||
.string_literal => |lit| self.builder.constString(self.module.types.internString(lit.raw)),
|
.string_literal => |lit| self.builder.constString(self.module.types.internString(lit.raw)),
|
||||||
@@ -1501,6 +1504,7 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
|
|||||||
const ty = if (self.target_type) |tt| blk: {
|
const ty = if (self.target_type) |tt| blk: {
|
||||||
break :blk if (self.isIntEx(tt)) tt else .s64;
|
break :blk if (self.isIntEx(tt)) tt else .s64;
|
||||||
} else .s64;
|
} else .s64;
|
||||||
|
self.checkIntLiteralFits(lit.value, ty, node.span);
|
||||||
return self.builder.constInt(lit.value, ty);
|
return self.builder.constInt(lit.value, ty);
|
||||||
},
|
},
|
||||||
.float_literal => |lit| {
|
.float_literal => |lit| {
|
||||||
@@ -1777,7 +1781,26 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
|
|||||||
break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty);
|
break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Fold a negated integer literal into one constant: `-128` must
|
||||||
|
// range-check as -128, not as an out-of-range +128 intermediate.
|
||||||
|
if (uop.op == .negate and uop.operand.data == .int_literal) {
|
||||||
|
const lit = uop.operand.data.int_literal;
|
||||||
|
const v = -%lit.value;
|
||||||
|
if (self.target_type) |tt| {
|
||||||
|
if (tt == .f32 or tt == .f64) {
|
||||||
|
break :blk self.builder.constFloat(@floatFromInt(v), tt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nty = if (self.target_type) |tt| (if (self.isIntEx(tt)) tt else TypeId.s64) else TypeId.s64;
|
||||||
|
self.checkIntLiteralFits(v, nty, node.span);
|
||||||
|
break :blk self.builder.constInt(v, nty);
|
||||||
|
}
|
||||||
|
// An explicit `xx` cast requests the conversion, truncation
|
||||||
|
// included — literal operands skip the fits-check.
|
||||||
|
const saved_fit = self.suppress_int_fit_check;
|
||||||
|
if (uop.op == .xx) self.suppress_int_fit_check = true;
|
||||||
const operand = self.lowerExpr(uop.operand);
|
const operand = self.lowerExpr(uop.operand);
|
||||||
|
self.suppress_int_fit_check = saved_fit;
|
||||||
break :blk switch (uop.op) {
|
break :blk switch (uop.op) {
|
||||||
.negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)),
|
.negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)),
|
||||||
.not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool),
|
.not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool),
|
||||||
|
|||||||
Reference in New Issue
Block a user