fix(0111): unannotated decl literals no longer adopt the fn return type
lowerVarDecl (unannotated) and lowerDestructureDecl now clear target_type around the initializer lowering: a declaration without annotation provides no target, so int/float literals take their spec defaults (s64/f64) instead of the enclosing function's implicit-return type (x := 0 in a -> s8 fn was s8; big := 3000000000 in -> s32 silently wrapped to -1294967296). Regression: examples/0173-types-int-literal-default-s64.sx. The remaining explicit-annotation wrap (x : s8 = 300) is filed as issue 0112.
This commit is contained in:
42
examples/0173-types-int-literal-default-s64.sx
Normal file
42
examples/0173-types-int-literal-default-s64.sx
Normal file
@@ -0,0 +1,42 @@
|
||||
// Integer literals default to s64 regardless of context: an unannotated
|
||||
// `x := <int literal>` 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)));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
f.x: s64
|
||||
g.x: s64
|
||||
big: s64 = 3000000000
|
||||
a: s64 b: s64
|
||||
main.x: s64
|
||||
139
issues/0111-int-literal-local-adopts-fn-return-type.md
Normal file
139
issues/0111-int-literal-local-adopts-fn-return-type.md
Normal file
@@ -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 := <int literal>` (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.
|
||||
66
issues/0112-int-literal-out-of-range-silent-wrap.md
Normal file
66
issues/0112-int-literal-out-of-range-silent-wrap.md
Normal file
@@ -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-...`).
|
||||
8
issues/0112-int-literal-out-of-range-silent-wrap.sx
Normal file
8
issues/0112-int-literal-out-of-range-silent-wrap.sx
Normal file
@@ -0,0 +1,8 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () {
|
||||
x : s8 = 300;
|
||||
print("x: {}\n", x);
|
||||
y : u8 = 256;
|
||||
print("y: {}\n", y);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user