diff --git a/examples/0173-types-int-literal-default-s64.sx b/examples/0173-types-int-literal-default-s64.sx new file mode 100644 index 0000000..c67c5a1 --- /dev/null +++ b/examples/0173-types-int-literal-default-s64.sx @@ -0,0 +1,42 @@ +// Integer literals default to s64 regardless of context: an unannotated +// `x := ` local stays s64 even inside a function whose return +// type is a narrower integer (the implicit-return target must not type the +// body's declarations), and a large literal initializer keeps its value. +// Also covers destructure decls (`a, b := ...`), which share the same rule. +// Regression (issue 0111): these locals adopted the enclosing fn's return +// type (s32/s8), silently wrapping `big := 3000000000` to -1294967296. + +#import "modules/std.sx"; + +f :: () -> s32 { + x := 0; + print("f.x: {}\n", type_name(type_of(x))); + 0 +} + +g :: () -> s8 { + x := 0; + print("g.x: {}\n", type_name(type_of(x))); + 0 +} + +big_host :: () -> s32 { + big := 3000000000; + print("big: {} = {}\n", type_name(type_of(big)), big); + 0 +} + +d_host :: () -> s32 { + a, b := (1, 2); + print("a: {} b: {}\n", type_name(type_of(a)), type_name(type_of(b))); + 0 +} + +main :: () { + f(); + g(); + big_host(); + d_host(); + x := 0; + print("main.x: {}\n", type_name(type_of(x))); +} diff --git a/examples/expected/0173-types-int-literal-default-s64.exit b/examples/expected/0173-types-int-literal-default-s64.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0173-types-int-literal-default-s64.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0173-types-int-literal-default-s64.stderr b/examples/expected/0173-types-int-literal-default-s64.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0173-types-int-literal-default-s64.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0173-types-int-literal-default-s64.stdout b/examples/expected/0173-types-int-literal-default-s64.stdout new file mode 100644 index 0000000..7475f92 --- /dev/null +++ b/examples/expected/0173-types-int-literal-default-s64.stdout @@ -0,0 +1,5 @@ +f.x: s64 +g.x: s64 +big: s64 = 3000000000 +a: s64 b: s64 +main.x: s64 diff --git a/issues/0111-int-literal-local-adopts-fn-return-type.md b/issues/0111-int-literal-local-adopts-fn-return-type.md new file mode 100644 index 0000000..9cfe990 --- /dev/null +++ b/issues/0111-int-literal-local-adopts-fn-return-type.md @@ -0,0 +1,139 @@ +# RESOLVED — 0111: unannotated int-literal locals adopt the enclosing fn's return type + +**Root cause:** `lowerFunctionBodyInto` sets `self.target_type = ret_ty` for the whole +body (for the implicit trailing return), the `.int_literal` arm adopts any integer +`target_type`, and the unannotated paths of `lowerVarDecl` / `lowerDestructureDecl` +lowered their initializer without clearing it — so `x := 0` in a `-> s32`/`-> s8` +function was typed s32/s8 and `big := 3000000000` silently wrapped. + +**Fix:** both decl paths now save/clear/restore `target_type` around the initializer +`lowerExpr` (`src/ir/lower/stmt.zig`); a declaration without annotation provides no +target, so literals take their spec defaults (s64/f64). Trailing-expression and +`return` coercion to the return type are untouched. + +**Regression test:** `examples/0173-types-int-literal-default-s64.sx` (f.x/g.x/main.x, +destructured a/b all print `s64`; `big` prints `3000000000`). + +**Note (not fixed here):** the `.int_literal` arm still wraps a literal that does not +fit an explicitly-annotated integer target (`x : s8 = 300`) with no diagnostic — +filed separately as issue 0112. + +--- + +# 0111 — unannotated int-literal locals adopt the enclosing fn's return type (silent narrowing + wraparound) + +**Symptom.** A local declared `x := ` (no annotation) inside a +function whose return type is an integer type T gets typed as **T**, not the +spec'd default. `f :: () -> s32 { x := 0; ... }` gives `x: s32`; +`g :: () -> s8 { x := 0; ... }` gives `x: s8`. Expected (specs.md §"Integer +literals default to `s64`", lines 240 / 1428): `s64` in all of these — the +declaration has no target type. Inside a `-> void` function the same decl +correctly infers `s64`. + +Consequences are silent and severe: + +- All arithmetic through such locals wraps at the narrowed width: + `x := 0; x += 3000000000;` inside `main :: () -> s32` prints `-1294967296`. +- A large literal initializer truncates with **no diagnostic**: + `big := 3000000000;` inside a `-> s32` fn binds `big` to s32 = `-1294967296`. +- Blast radius: every `main :: () -> s32 { ... }` in the corpus types every + unannotated int-literal local as s32 — long-running counters/accumulators + in such functions are one overflow away from wrong results. Discovered + because issue 0109's verification loop (`sum` over 1M iterations in a + `-> s32` main) printed the 32-bit-wrapped sum after the segfault was fixed. + +## Reproduction + +```sx +#import "modules/std.sx"; + +f :: () -> s32 { + x := 0; + print("f.x: {}\n", type_name(type_of(x))); + 0 +} + +g :: () -> s8 { + x := 0; + print("g.x: {}\n", type_name(type_of(x))); + 0 +} + +big_host :: () -> s32 { + big := 3000000000; + print("big: {} = {}\n", type_name(type_of(big)), big); + 0 +} + +main :: () { + f(); + g(); + big_host(); + x := 0; + print("main.x: {}\n", type_name(type_of(x))); +} +``` + +- **Observed** (current master): `f.x: s32`, `g.x: s8`, + `big: s32 = -1294967296`, `main.x: s64`. +- **Expected**: `s64` for all four; `big` prints `3000000000`. + +Repro co-located: `issues/0111-int-literal-local-adopts-fn-return-type.sx` +(standalone version of the above; unpinned until fixed). + +## Root cause (traced) + +Three-link chain, all confirmed by reading: + +1. `src/ir/lower/decl.zig` ~2149 (`lowerFunctionBodyInto`): before lowering + the body, `self.target_type = ret_ty` is set for the WHOLE body — intended + for the implicit trailing-return coercion — and only restored after the + body finishes. +2. `src/ir/lower/expr.zig` ~1499 (`.int_literal` arm): an int literal adopts + `self.target_type` whenever it `isIntEx` — with no fits-check, so a + too-big literal wraps silently. +3. `src/ir/lower/stmt.zig` ~335-348 (`lowerVarDecl`, unannotated path): + lowers the initializer with whatever `target_type` is in scope and takes + the decl's type from the lowered ref. The annotated path overwrites + `target_type` with the annotation (which is why `y : s64 = 0` is immune); + the unannotated path inherits the function-return context it should never + see. + +## Investigation prompt (paste into a fresh session) + +> Fix issue 0111: unannotated int-literal locals adopt the enclosing +> function's integer return type instead of the s64 default. Root cause chain +> is in the issue (decl.zig ~2149 body-wide `target_type = ret_ty`; +> expr.zig ~1499 `.int_literal` adopting it; stmt.zig ~335 unannotated +> `lowerVarDecl` not clearing it). Fix shape: in `lowerVarDecl`'s +> unannotated path, save `self.target_type`, set it to `null` around the +> `lowerExpr(val)` of the initializer, restore after — a declaration without +> annotation provides no target. Audit the sibling statement positions that +> also shouldn't inherit the return-type context (e.g. expression statements, +> `lowerMultiAssign` / `lowerDestructureDecl` unannotated paths) and apply the +> same clear where applicable. Do NOT touch the trailing-expression / +> `return` paths — `f :: () -> s32 { 0 }` must keep coercing the tail to the +> return type. +> +> Separately consider (same fix or follow-up per scope): the `.int_literal` +> arm wraps a literal that doesn't fit the adopted integer target with no +> diagnostic — add a fits-check that errors like the float-narrowing rule +> (`floatToIntExact` precedent) instead of silently truncating. +> +> Verify: run `issues/0111-int-literal-local-adopts-fn-return-type.sx` — +> expect `s64` for f.x / g.x / big / main.x and `big = 3000000000`. Then +> `zig build && zig build test && bash tests/run_examples.sh` against +> EXISTING snapshots: any diff means an example was silently relying on +> narrowed locals — review each (the change is user-visible only via +> type_name/overflow behavior). Promote the repro per the resolution flow +> (`examples/01xx-types-...` block) and mark this issue RESOLVED. +> +> Context: this bug BLOCKS the in-flight fix session for issues 0108/0109/ +> 0110 (for-loop codegen resource bugs). The 0109 emitter change (entry-block +> alloca hoisting) is already applied in the working tree, builds, and fixes +> both 0109 repros — but its regression example (1M-iteration `sum` +> accumulation in `main :: () -> s32`) cannot produce the documented expected +> output (`sum=499999500000`) until locals stop narrowing to s32, and +> suite/.ir snapshot regen must not run on a compiler with this live +> miscompile. After 0111 lands, resume: finalize 0109 (suite + .ir review + +> regression example + commit), then 0110, then 0108 per their issue files. diff --git a/issues/0112-int-literal-out-of-range-silent-wrap.md b/issues/0112-int-literal-out-of-range-silent-wrap.md new file mode 100644 index 0000000..730092f --- /dev/null +++ b/issues/0112-int-literal-out-of-range-silent-wrap.md @@ -0,0 +1,66 @@ +# 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-...`). diff --git a/issues/0112-int-literal-out-of-range-silent-wrap.sx b/issues/0112-int-literal-out-of-range-silent-wrap.sx new file mode 100644 index 0000000..e90c569 --- /dev/null +++ b/issues/0112-int-literal-out-of-range-silent-wrap.sx @@ -0,0 +1,8 @@ +#import "modules/std.sx"; + +main :: () { + x : s8 = 300; + print("x: {}\n", x); + y : u8 = 256; + print("y: {}\n", y); +} diff --git a/src/ir/lower/stmt.zig b/src/ir/lower/stmt.zig index f8ac77f..f245570 100644 --- a/src/ir/lower/stmt.zig +++ b/src/ir/lower/stmt.zig @@ -337,9 +337,15 @@ pub fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void { // This is critical for generic calls where the return type is only // known after monomorphization. const saved_fbv = self.force_block_value; + const saved_target = self.target_type; self.force_block_value = true; + // An unannotated decl provides no target type: clear the ambient one + // (the enclosing fn's implicit-return target) so literal initializers + // take their spec defaults (s64/f64) instead of adopting it. + self.target_type = null; const ref = self.lowerExpr(val); self.force_block_value = saved_fbv; + self.target_type = saved_target; const ty = self.builder.getRefType(ref); const slot = self.builder.alloca(ty); self.builder.store(slot, ref); @@ -1189,9 +1195,14 @@ pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void { pub fn lowerDestructureDecl(self: *Lowering, dd: *const ast.DestructureDecl) void { // Lower the RHS expression (must produce a tuple) const saved_fbv = self.force_block_value; + const saved_target = self.target_type; self.force_block_value = true; + // Same as the unannotated var-decl path: the destructure declares new + // bindings, so the ambient target type must not type the RHS literals. + self.target_type = null; const ref = self.lowerExpr(dd.value); self.force_block_value = saved_fbv; + self.target_type = saved_target; const ty = self.builder.getRefType(ref); // Get tuple field info