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

@@ -205,6 +205,7 @@ pub const Lowering = struct {
break_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)
suppress_int_fit_check: bool = false, // inside an explicit `xx` cast operand: truncation is requested, skip the literal fits-check
block_counter: u32 = 0,
comptime_counter: u32 = 0,
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;
}
/// 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.
/// custom widths), floats, SIMD vectors, and pointers (pointer
/// arithmetic). `.unresolved` returns true so a type we couldn't infer

View File

@@ -976,7 +976,10 @@ pub fn globalInitValue(self: *Lowering, vd: *const ast.VarDecl, var_ty: TypeId)
return switch (v.data) {
.undef_literal => .zeroinit,
.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 },
// A float initializer at an integer-typed global follows the
// implicit narrowing rule (integral folds, non-integral errors).

View File

@@ -627,7 +627,10 @@ pub fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.
pub fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref {
const val_node = info.value;
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),
.bool_literal => |lit| self.builder.constBool(lit.value),
.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: {
break :blk if (self.isIntEx(tt)) tt else .s64;
} else .s64;
self.checkIntLiteralFits(lit.value, ty, node.span);
return self.builder.constInt(lit.value, ty);
},
.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);
}
}
// 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);
self.suppress_int_fit_check = saved_fit;
break :blk switch (uop.op) {
.negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)),
.not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool),