diff --git a/examples/0174-types-int-literal-boundaries.sx b/examples/0174-types-int-literal-boundaries.sx new file mode 100644 index 0000000..24725ff --- /dev/null +++ b/examples/0174-types-int-literal-boundaries.sx @@ -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); +} diff --git a/examples/0300-closures-lambda.sx b/examples/0300-closures-lambda.sx index b8ced5b..b77f201 100644 --- a/examples/0300-closures-lambda.sx +++ b/examples/0300-closures-lambda.sx @@ -5,5 +5,5 @@ main :: () { s } - print("{}\n", fx(133)); + print("{}\n", fx(-3)); } diff --git a/examples/1156-diagnostics-int-literal-out-of-range.sx b/examples/1156-diagnostics-int-literal-out-of-range.sx new file mode 100644 index 0000000..cc56168 --- /dev/null +++ b/examples/1156-diagnostics-int-literal-out-of-range.sx @@ -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); +} diff --git a/examples/expected/0174-types-int-literal-boundaries.exit b/examples/expected/0174-types-int-literal-boundaries.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0174-types-int-literal-boundaries.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0174-types-int-literal-boundaries.stderr b/examples/expected/0174-types-int-literal-boundaries.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0174-types-int-literal-boundaries.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0174-types-int-literal-boundaries.stdout b/examples/expected/0174-types-int-literal-boundaries.stdout new file mode 100644 index 0000000..bbd4818 --- /dev/null +++ b/examples/expected/0174-types-int-literal-boundaries.stdout @@ -0,0 +1 @@ +-128 127 0 255 9223372036854775807 4294967295 -32768 44 44 -5 diff --git a/examples/expected/1156-diagnostics-int-literal-out-of-range.exit b/examples/expected/1156-diagnostics-int-literal-out-of-range.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1156-diagnostics-int-literal-out-of-range.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1156-diagnostics-int-literal-out-of-range.stderr b/examples/expected/1156-diagnostics-int-literal-out-of-range.stderr new file mode 100644 index 0000000..dc21a45 --- /dev/null +++ b/examples/expected/1156-diagnostics-int-literal-out-of-range.stderr @@ -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; + | ^^^ diff --git a/examples/expected/1156-diagnostics-int-literal-out-of-range.stdout b/examples/expected/1156-diagnostics-int-literal-out-of-range.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1156-diagnostics-int-literal-out-of-range.stdout @@ -0,0 +1 @@ + diff --git a/issues/0112-int-literal-out-of-range-silent-wrap.md b/issues/0112-int-literal-out-of-range-silent-wrap.md index 730092f..6dfb313 100644 --- a/issues/0112-int-literal-out-of-range-silent-wrap.md +++ b/issues/0112-int-literal-out-of-range-silent-wrap.md @@ -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 diff --git a/issues/0112-int-literal-out-of-range-silent-wrap.sx b/issues/0112-int-literal-out-of-range-silent-wrap.sx deleted file mode 100644 index e90c569..0000000 --- a/issues/0112-int-literal-out-of-range-silent-wrap.sx +++ /dev/null @@ -1,8 +0,0 @@ -#import "modules/std.sx"; - -main :: () { - x : s8 = 300; - print("x: {}\n", x); - y : u8 = 256; - print("y: {}\n", y); -} diff --git a/issues/0113-negative-literal-global-initializer-rejected.md b/issues/0113-negative-literal-global-initializer-rejected.md new file mode 100644 index 0000000..3ebe81c --- /dev/null +++ b/issues/0113-negative-literal-global-initializer-rejected.md @@ -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. diff --git a/issues/0113-negative-literal-global-initializer-rejected.sx b/issues/0113-negative-literal-global-initializer-rejected.sx new file mode 100644 index 0000000..0bcb28c --- /dev/null +++ b/issues/0113-negative-literal-global-initializer-rejected.sx @@ -0,0 +1,4 @@ +#import "modules/std.sx"; +g : s64 = -1; +main :: () { print("{} +", g); } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 5a9a2b0..8635628 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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 diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index fab95ce..df366f4 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -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). diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index a45c399..9be3c8f 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -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),