lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
This commit is contained in:
@@ -39,7 +39,7 @@ sx-defined globals shared across sx modules.
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
extern g_x : *void; // want: a reference to a global defined elsewhere
|
||||
main :: () -> s32 { 0; }
|
||||
main :: () -> i32 { 0; }
|
||||
```
|
||||
|
||||
`./zig-out/bin/sx run …` → `error: expected '::', ':=', or ':' after identifier`
|
||||
|
||||
@@ -11,4 +11,4 @@
|
||||
|
||||
extern g_x : *void;
|
||||
|
||||
main :: () -> s32 { 0; }
|
||||
main :: () -> i32 { 0; }
|
||||
|
||||
@@ -42,7 +42,7 @@ worth pinning down because:
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
n := size_of(*u8); // error: unexpected token in expression
|
||||
print("{}\n", n);
|
||||
0;
|
||||
@@ -56,7 +56,7 @@ Also fails on the alias form:
|
||||
|
||||
Ptr :: *u8; // error: unexpected token in expression
|
||||
|
||||
main :: () -> s32 { 0; }
|
||||
main :: () -> i32 { 0; }
|
||||
```
|
||||
|
||||
Both `sx run` and `sx build` reject identically.
|
||||
@@ -104,7 +104,7 @@ class of "ptr type as type-expression value" is unsupported.
|
||||
> handles `*` prefix. Today it likely returns a `unary_op
|
||||
> { op = deref, operand = … }` AST node.
|
||||
> - Look at how lower.zig's `resolveTypeArg` consumes the AST node
|
||||
> for `size_of(s32)` — what AST shape does it expect for a type
|
||||
> for `size_of(i32)` — what AST shape does it expect for a type
|
||||
> literal? Probably an `identifier` whose name resolves to a type.
|
||||
> - The fix should extend `resolveTypeArg` to also accept a
|
||||
> `unary_op { op = deref, ... }` and treat it as "pointer to
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# issue-0042 — Const-decl type aliases (`MyInt :: s32;`) silently return `.s64` from `size_of` / `align_of`
|
||||
# issue-0042 — Const-decl type aliases (`MyInt :: i32;`) silently return `.i64` from `size_of` / `align_of`
|
||||
|
||||
**FIXED.** `MyInt :: s32; size_of(MyInt)` now returns `4`
|
||||
**FIXED.** `MyInt :: i32; size_of(MyInt)` now returns `4`
|
||||
correctly. The `resolveTypeArg` `.identifier` branch consults
|
||||
`type_alias_map` before falling through. The fix landed
|
||||
alongside the broader alias-resolution work tracked in
|
||||
@@ -14,28 +14,28 @@ Below preserved as a record of the original problem.
|
||||
A type alias declared via `Foo :: SomeType;` is registered in the
|
||||
lowering's `type_alias_map` but is **never consulted** when the alias
|
||||
name is later used as a type argument to `size_of` / `align_of`. The
|
||||
fallback returns `.s64` (8 bytes) — which coincidentally produces a
|
||||
fallback returns `.i64` (8 bytes) — which coincidentally produces a
|
||||
correct result for any alias whose underlying type is 8 bytes
|
||||
(`*T`, `f64`, function pointers, `s64`, `u64`), silently masking the
|
||||
(`*T`, `f64`, function pointers, `i64`, `u64`), silently masking the
|
||||
bug for years.
|
||||
|
||||
Observed:
|
||||
```
|
||||
size_of(s32) = 4 ← direct, correct
|
||||
size_of(i32) = 4 ← direct, correct
|
||||
size_of(MyInt) = 8 ← via alias, WRONG (expected 4)
|
||||
```
|
||||
|
||||
Where `MyInt :: s32;`.
|
||||
Where `MyInt :: i32;`.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
MyInt :: s32;
|
||||
MyInt :: i32;
|
||||
|
||||
main :: () -> s32 {
|
||||
print("direct: {}\n", size_of(s32)); // 4
|
||||
main :: () -> i32 {
|
||||
print("direct: {}\n", size_of(i32)); // 4
|
||||
print("alias: {}\n", size_of(MyInt)); // 8 — should be 4
|
||||
0;
|
||||
}
|
||||
@@ -53,9 +53,9 @@ alias: 8
|
||||
issue-0041 work extends the const-decl alias path to register
|
||||
pointer, optional, array, slice, many-pointer, and function-type
|
||||
aliases (`Ptr :: *u8;`, `Maybe :: ?u8;`, `Arr :: [3]u8;`,
|
||||
`Cb :: (s32) -> s32;`). Every one of those aliases ends up in
|
||||
`Cb :: (i32) -> i32;`). Every one of those aliases ends up in
|
||||
`type_alias_map`, then `size_of(<alias>)` falls through the same
|
||||
`.identifier` branch that ignores the map — returning `.s64` (8).
|
||||
`.identifier` branch that ignores the map — returning `.i64` (8).
|
||||
For pointer and function-type aliases this is coincidentally right
|
||||
(8 bytes). For optional, array, etc. it produces silently-wrong
|
||||
sizes (`size_of(Maybe) = 8` instead of 2;
|
||||
@@ -77,7 +77,7 @@ would ship subtly broken.
|
||||
> if (tb.get(id.name)) |ty| return ty;
|
||||
> }
|
||||
> const name_id = self.module.types.internString(id.name);
|
||||
> return self.module.types.findByName(name_id) orelse .s64;
|
||||
> return self.module.types.findByName(name_id) orelse .i64;
|
||||
> },
|
||||
> ```
|
||||
>
|
||||
@@ -96,7 +96,7 @@ would ship subtly broken.
|
||||
>
|
||||
> Why two branches: an `.identifier` AST node is what parsePrimary
|
||||
> emits for non-keyword names; `.type_expr` is what it emits for
|
||||
> built-in primitive names recognised by `Type.fromName` (`s32`,
|
||||
> built-in primitive names recognised by `Type.fromName` (`i32`,
|
||||
> `u8`, etc.) and for the `f32`/`f64`/`Type` keywords. User-defined
|
||||
> alias names like `MyInt` and `Ptr` flow through `.identifier`.
|
||||
>
|
||||
@@ -111,7 +111,7 @@ would ship subtly broken.
|
||||
> }
|
||||
> if (self.type_alias_map.get(id.name)) |alias_ty| return alias_ty;
|
||||
> const name_id = self.module.types.internString(id.name);
|
||||
> return self.module.types.findByName(name_id) orelse .s64;
|
||||
> return self.module.types.findByName(name_id) orelse .i64;
|
||||
> },
|
||||
> ```
|
||||
>
|
||||
@@ -125,7 +125,7 @@ would ship subtly broken.
|
||||
>
|
||||
> **Possible adjacency:** the issue may extend to `align_of`
|
||||
> (likely same call path) and to type-alias chains
|
||||
> (`A :: s32; B :: A;` — does B resolve through A's alias entry?).
|
||||
> (`A :: i32; B :: A;` — does B resolve through A's alias entry?).
|
||||
> Worth pinning down with a test once the primary fix lands.
|
||||
|
||||
## Plan-level impact
|
||||
|
||||
@@ -83,7 +83,7 @@ caller :: (self: *void, _cmd: *void, scene: *void, b: *void, c: *void) callconv(
|
||||
}
|
||||
}
|
||||
|
||||
main :: () -> s32 { 0; }
|
||||
main :: () -> i32 { 0; }
|
||||
```
|
||||
|
||||
Build:
|
||||
|
||||
@@ -47,7 +47,7 @@ Foo :: #objc_class("SxFooSelfTest") {
|
||||
}
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
inline if OS == .macos {
|
||||
f := Foo.alloc().init();
|
||||
result := f.poke();
|
||||
|
||||
@@ -27,10 +27,10 @@ than corrupt IR.
|
||||
# Reproduction
|
||||
|
||||
```sx
|
||||
foo :: (..$args) -> s64 { return 42; }
|
||||
foo :: (..$args) -> i64 { return 42; }
|
||||
|
||||
main :: () -> s32 {
|
||||
n : s64 = foo();
|
||||
main :: () -> i32 {
|
||||
n : i64 = foo();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -58,7 +58,7 @@ at first use. But the existing monomorphisation machinery binds a
|
||||
single TypeId per `$T` name — it has no notion of a *pack* (a
|
||||
variable-length list of TypeIds bound positionally). When the
|
||||
call site tries to monomorphise with the call's args, the body's
|
||||
`args` parameter gets resolved to a single (probably default `.s64`)
|
||||
`args` parameter gets resolved to a single (probably default `.i64`)
|
||||
TypeId, but the call-site arg-packing path (`packVariadicCallArgs`)
|
||||
treats it as a regular `..T` slice — the two views disagree and
|
||||
the emitted IR is malformed.
|
||||
|
||||
@@ -11,7 +11,7 @@ and tripped a null pointer store at `storeAtRawPtr`.
|
||||
The pack-fn face of this bug (filed as face 2) was fixed
|
||||
incidentally by step 2b's mono refactor — pack-fn calls
|
||||
bypass the inline-return-slot setup entirely. Plain
|
||||
`($x: s32)` comptime fns stay on the inline path; the
|
||||
`($x: i32)` comptime fns stay on the inline path; the
|
||||
`createComptimeFunction` save/restore fix covers that path.
|
||||
|
||||
Regression test:
|
||||
@@ -25,8 +25,8 @@ two shapes depending on the comptime-param flavour:
|
||||
|
||||
| Outer fn shape | Failure |
|
||||
|---|---|
|
||||
| `helper :: ($x: s32) -> s64 { print("inside\n"); return 42; }` (plain comptime) | Panic: `cast causes pointer to be null` at `src/ir/interp.zig:207 storeAtRawPtr`. |
|
||||
| `dump :: (..$args) -> s64 { n := args[0]; print("got {}\n", n); return n; }` (pack-fn) | Compile error: `unresolved 'result'` at fake span `1:5` (inside the inserted code). |
|
||||
| `helper :: ($x: i32) -> i64 { print("inside\n"); return 42; }` (plain comptime) | Panic: `cast causes pointer to be null` at `src/ir/interp.zig:207 storeAtRawPtr`. |
|
||||
| `dump :: (..$args) -> i64 { n := args[0]; print("got {}\n", n); return n; }` (pack-fn) | Compile error: `unresolved 'result'` at fake span `1:5` (inside the inserted code). |
|
||||
|
||||
Both vanish if you remove either the nested `print(...)` OR the
|
||||
`return X;` statement:
|
||||
@@ -56,19 +56,19 @@ and it isn't.
|
||||
#import "modules/std.sx";
|
||||
|
||||
// Face 1 — interp panic:
|
||||
helper :: ($x: s32) -> s64 {
|
||||
helper :: ($x: i32) -> i64 {
|
||||
print("inside\n");
|
||||
return 42;
|
||||
}
|
||||
|
||||
// Face 2 — "unresolved 'result'":
|
||||
dump :: (..$args) -> s64 {
|
||||
n : s64 = args[0];
|
||||
dump :: (..$args) -> i64 {
|
||||
n : i64 = args[0];
|
||||
print("got {}\n", n);
|
||||
return n;
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
n := helper(7); // ← panic in interp
|
||||
print("{}\n", dump(7)); // ← "unresolved 'result'"
|
||||
return 0;
|
||||
@@ -84,7 +84,7 @@ in the same program.
|
||||
same pattern hit a different fatal stage (LLVM verifier) before
|
||||
the fix. The fix exposed it; it didn't create it.
|
||||
- Not caused by step 2a (pack typed indexing, commit `cd36784`):
|
||||
Face 1 reproduces with a plain `($x: s32)` comptime fn, no
|
||||
Face 1 reproduces with a plain `($x: i32)` comptime fn, no
|
||||
pack involved.
|
||||
- Not exercised by any test in the suite today. `format`/`print`
|
||||
use arrow form or `#insert`-only bodies — no `return` in a
|
||||
|
||||
@@ -27,7 +27,7 @@ configure :: () {
|
||||
}
|
||||
#run configure();
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
print("hello from runtime\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -28,14 +28,14 @@ Block literal source. With this bug, the builder receives an
|
||||
empty slice and emits the empty-pack source for every call shape,
|
||||
silently producing wrong block trampolines.
|
||||
|
||||
Baseline regression check (not affected): a hand-built `[]s64`
|
||||
Baseline regression check (not affected): a hand-built `[]i64`
|
||||
slice round-trips correctly across the same kind of call:
|
||||
|
||||
```sx
|
||||
walk :: (xs: []s64) -> s64 { return xs.len; }
|
||||
walk :: (xs: []i64) -> i64 { return xs.len; }
|
||||
main :: () {
|
||||
arr : [3]s64 = .{10, 20, 30};
|
||||
sl : []s64 = arr;
|
||||
arr : [3]i64 = .{10, 20, 30};
|
||||
sl : []i64 = arr;
|
||||
print("call: {}\n", walk(sl)); // prints 3 — works
|
||||
}
|
||||
```
|
||||
@@ -71,7 +71,7 @@ Cleaner repro that contrasts inline vs callee:
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
walk :: (args: []Any) -> s64 { return args.len; }
|
||||
walk :: (args: []Any) -> i64 { return args.len; }
|
||||
|
||||
probe :: (..$args) -> string {
|
||||
inline_list := $args;
|
||||
@@ -105,7 +105,7 @@ Suspected area:
|
||||
|
||||
- `src/ir/lower.zig` — `buildPackSliceValue` / `materialisePackSlice`.
|
||||
- Whether the slice aggregate it returns is the same shape sx uses
|
||||
for an ordinary slice — `{ ptr: *T, len: s64 }` in field order
|
||||
for an ordinary slice — `{ ptr: *T, len: i64 }` in field order
|
||||
used by `.len` reads at consumer sites.
|
||||
- Whether the slice survives the function-call ABI: the callee
|
||||
reads the slice fields from its frame's slot for the argument;
|
||||
@@ -123,7 +123,7 @@ What to check first:
|
||||
the slice aggregate produced by `buildPackSliceValue` — not at
|
||||
the underlying `alloca [N x Any]` (which would be the data
|
||||
pointer, not the slice).
|
||||
3. Compare with the `[]s64` round-trip path that works — what's
|
||||
3. Compare with the `[]i64` round-trip path that works — what's
|
||||
different about how the slice is bound at the call site?
|
||||
|
||||
Verification step after fix:
|
||||
|
||||
@@ -17,14 +17,14 @@ Bus error at address 0x3fff
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
Show :: protocol { show :: () -> string; }
|
||||
A :: struct { x: s64; }
|
||||
A :: struct { x: i64; }
|
||||
impl Show for A { show :: (self: *A) -> string => "A"; }
|
||||
|
||||
each :: (..xs: []Show) -> void {
|
||||
i := 0;
|
||||
while i < xs.len { print("{}\n", xs[i].show()); i = i + 1; }
|
||||
}
|
||||
main :: () -> s32 { each(A.{ x = 1 }, A.{ x = 2 }); 0; }
|
||||
main :: () -> i32 { each(A.{ x = 1 }, A.{ x = 2 }); 0; }
|
||||
```
|
||||
|
||||
# Root cause
|
||||
|
||||
@@ -23,9 +23,9 @@ materialising a single `[]Any` slice for the one `items` parameter.
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
log_count :: (items: []Any) -> s64 { return items.len; }
|
||||
forward :: (..$args) -> s64 { return log_count(..args); }
|
||||
main :: () -> s32 { print("{}\n", forward(1, "hi", 2.5)); return 0; }
|
||||
log_count :: (items: []Any) -> i64 { return items.len; }
|
||||
forward :: (..$args) -> i64 { return log_count(..args); }
|
||||
main :: () -> i32 { print("{}\n", forward(1, "hi", 2.5)); return 0; }
|
||||
```
|
||||
|
||||
Expected: `3` (the pack spreads into the `[]Any` slice, like calling
|
||||
@@ -38,7 +38,7 @@ the cleaner spelling is an **`xx` cast**, which already means "erase/convert to
|
||||
the expected type":
|
||||
|
||||
```sx
|
||||
forward :: (..$args) -> s64 { return log_count(xx args); } // target: []Any
|
||||
forward :: (..$args) -> i64 { return log_count(xx args); } // target: []Any
|
||||
```
|
||||
|
||||
`xx args` (target-typed) should materialize the pack into the expected slice:
|
||||
@@ -62,7 +62,7 @@ Declare the forwarder as the **slice** variadic instead of a pack — then it's
|
||||
already a runtime `[]Any` and forwards directly:
|
||||
|
||||
```sx
|
||||
forward :: (..args: []Any) -> s64 { return log_count(args); } // works -> 3
|
||||
forward :: (..args: []Any) -> i64 { return log_count(args); } // works -> 3
|
||||
```
|
||||
|
||||
This is what `examples/162-pack-bare-args.sx` demonstrates.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
was a general pre-existing bug — `self.x` failed on *any* generic-struct
|
||||
impl method.)
|
||||
2. `createProtocolThunk` monomorphizes the template method for a generic-struct
|
||||
instance (`Combined.get` → `Combined__s64_s64.get` with the instance
|
||||
instance (`Combined.get` → `Combined__i64_i64.get` with the instance
|
||||
bindings), so the erasure vtable dispatches instead of an `unreachable` thunk.
|
||||
|
||||
`xx c` (Combined → VL($R)) now dispatches correctly. The *full* canonical `map`
|
||||
@@ -27,18 +27,18 @@ This is the last piece of the canonical `map` (`return xx c;`).
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
VL :: protocol(T: Type) { get :: () -> T; }
|
||||
IntCell :: struct { v: s64; }
|
||||
impl VL(s64) for IntCell { get :: (self: *IntCell) -> s64 => self.v; }
|
||||
IntCell :: struct { v: i64; }
|
||||
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
|
||||
Combined :: struct($R: Type, ..$Ts: []Type) { sources: (..VL(Ts)); value: $R; }
|
||||
impl VL($R) for Combined($R, ..$Ts) { get :: (self: *Combined) -> $R => self.value; }
|
||||
|
||||
make :: (..sources: VL) -> VL(s64) {
|
||||
c : Combined(s64, ..sources.T) = ---;
|
||||
make :: (..sources: VL) -> VL(i64) {
|
||||
c : Combined(i64, ..sources.T) = ---;
|
||||
c.value = 99;
|
||||
c.sources = (..sources);
|
||||
return xx c; // Combined__s64_s64 -> VL(s64)
|
||||
return xx c; // Combined__i64_i64 -> VL(i64)
|
||||
}
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
r := make(IntCell.{ v = 1 });
|
||||
print("{}\n", r.get()); // expect 99; instead traps
|
||||
0;
|
||||
@@ -53,11 +53,11 @@ diagnostic — so an impl *was* matched), but the JIT traps on `r.get()`.
|
||||
`param_impl_map` is keyed by **concrete** `(protocol, target_args_mangled,
|
||||
source_mangled)`. The impl `impl VL($R) for Combined($R, ..$Ts)` is generic on
|
||||
both sides — its source mangles to a generic `Combined` (with `$R`/`$Ts`), not
|
||||
the concrete `Combined__s64_s64`. Erasing `Combined__s64_s64 → VL(s64)` looks up
|
||||
`(VL, s64, Combined__s64_s64)`, which doesn't key-match the generic impl; some
|
||||
the concrete `Combined__i64_i64`. Erasing `Combined__i64_i64 → VL(i64)` looks up
|
||||
`(VL, i64, Combined__i64_i64)`, which doesn't key-match the generic impl; some
|
||||
looser path still produces a protocol value, but its vtable slot for `get`
|
||||
isn't bound to the monomorphized `Combined__s64_s64.get` (which returns
|
||||
`self.value` as `$R`=s64). Calling through it traps.
|
||||
isn't bound to the monomorphized `Combined__i64_i64.get` (which returns
|
||||
`self.value` as `$R`=i64). Calling through it traps.
|
||||
|
||||
The fix needs generic-impl matching + per-instance monomorphization for protocol
|
||||
erasure: when erasing a concrete generic-struct instance to a parameterized
|
||||
@@ -75,7 +75,7 @@ and fill the vtable with the resulting fn-ptrs. Compare:
|
||||
# Verification
|
||||
|
||||
The reproduction should print `99`. Plain (non-generic) struct → parameterized
|
||||
protocol erasure already works (`examples/206`: `xx IntCell -> VL(s64)`); the gap
|
||||
protocol erasure already works (`examples/206`: `xx IntCell -> VL(i64)`); the gap
|
||||
is specifically a *generic-struct* source matched via a *generic* impl.
|
||||
|
||||
# Status
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 0055 — binary arithmetic accepts mismatched operand types (`s64 + string`)
|
||||
# 0055 — binary arithmetic accepts mismatched operand types (`i64 + string`)
|
||||
|
||||
**FIXED** (`examples/214-binop-operand-type-check.sx`). `lowerBinaryOp` in
|
||||
[src/ir/lower.zig](../src/ir/lower.zig) now checks operand-type
|
||||
@@ -7,13 +7,13 @@ predicates that pass `.unresolved` through (so a type we couldn't infer is
|
||||
never falsely diagnosed) but reject a concretely incompatible operand:
|
||||
|
||||
- **arithmetic** `+ - * / %` → `isArithOperand` (numeric / vector /
|
||||
pointer). Without it `s64 + string` lowered as `add : s64` and
|
||||
pointer). Without it `i64 + string` lowered as `add : i64` and
|
||||
reinterpreted the string's bytes — garbage.
|
||||
- **ordering** `< <= > >=` → `isOrderingOperand` (numeric / enum / pointer
|
||||
/ bool / vector). Without it `s64 < string` fed mismatched LLVM types to
|
||||
/ bool / vector). Without it `i64 < string` fed mismatched LLVM types to
|
||||
`icmp` and tripped the verifier.
|
||||
- **bitwise / shift** `& | ^ << >>` → `isBitwiseOperand` (integer / enum /
|
||||
bool / vector). Without it `s64 & string` reinterpreted the bytes.
|
||||
bool / vector). Without it `i64 & string` reinterpreted the bytes.
|
||||
|
||||
On mismatch it emits `cannot apply '<op>' to operands of type '<lhs>' and
|
||||
'<rhs>'` and returns a placeholder sentinel instead of the corrupting op.
|
||||
@@ -25,27 +25,27 @@ check. Legitimate mixes — flags-enum bitwise (`.read | .write`,
|
||||
|
||||
Still NOT covered (left deliberately): equality `==` / `!=`, whose path is
|
||||
heavily special-cased (string `str_eq`, `Any` unbox, `optional == null`).
|
||||
`s64 == string` still slips through. Folding a compatibility check into
|
||||
`i64 == string` still slips through. Folding a compatibility check into
|
||||
that path without regressing the special cases is a separate change — open
|
||||
a fresh issue if it bites.
|
||||
|
||||
## Symptom
|
||||
|
||||
Binary arithmetic operators (`+`, `-`, `*`, `/`, `%`) perform **no
|
||||
operand-type compatibility check**. `s64 + string` compiles cleanly and
|
||||
operand-type compatibility check**. `i64 + string` compiles cleanly and
|
||||
runs, reinterpreting the `string` operand's bytes (pointer/len) as an
|
||||
integer.
|
||||
|
||||
- **Observed:** `a + c` where `a: s64`, `c: string` compiles and prints a
|
||||
- **Observed:** `a + c` where `a: i64`, `c: string` compiles and prints a
|
||||
garbage number (e.g. `4346102832` — `40 + <string data pointer>`).
|
||||
- **Expected:** a type-error diagnostic, e.g.
|
||||
`cannot apply '+' to operands of type 's64' and 'string'`.
|
||||
`cannot apply '+' to operands of type 'i64' and 'string'`.
|
||||
|
||||
Surfaced in `examples/213-canonical-map.sx`: a third source `v3: StrCell`
|
||||
(`VL(string)`) was added so the mapper became `(a, b, c) => a + b + c`
|
||||
with `a, b: s64` and `c: string`. The canonical `map` infers the closure
|
||||
with `a, b: i64` and `c: string`. The canonical `map` infers the closure
|
||||
params from the projected pack element types, so `a + b + c` is
|
||||
`s64 + s64 + string` — which should reject. Instead `r.get()` prints
|
||||
`i64 + i64 + string` — which should reject. Instead `r.get()` prints
|
||||
garbage (`4312977714`).
|
||||
|
||||
> Note: the working-tree copy of `examples/213` also contains a *separate*
|
||||
@@ -62,10 +62,10 @@ Minimal, standalone (only `modules/std.sx`):
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
a : s64 = 40;
|
||||
main :: () -> i32 {
|
||||
a : i64 = 40;
|
||||
c : string = "it should error";
|
||||
r := a + c; // expected: type error (s64 + string)
|
||||
r := a + c; // expected: type error (i64 + string)
|
||||
print("{}\n", r); // actual: prints garbage, e.g. 4346102832
|
||||
0;
|
||||
}
|
||||
@@ -84,8 +84,8 @@ $ ./zig-out/bin/sx run repro.sx
|
||||
> (`var ty = lhs_ty;` ~L2680), then at the final `switch (bop.op)` (~L2735)
|
||||
> emits `.add => self.builder.add(lhs, rhs, ty)` (and `.sub/.mul/.div/.mod`)
|
||||
> with that LHS-derived `ty` — never verifying `rhs_ty` is compatible. For
|
||||
> `s64 + string`, `ty = .s64` and the `string` rhs Ref is fed to an
|
||||
> `add : s64`, reinterpreting its bytes as an integer.
|
||||
> `i64 + string`, `ty = .i64` and the `string` rhs Ref is fed to an
|
||||
> `add : i64`, reinterpreting its bytes as an integer.
|
||||
>
|
||||
> The fix: before the arithmetic `switch` arms, for the arithmetic ops
|
||||
> (`add/sub/mul/div/mod`) check that `lhs_ty` and `rhs_ty` are
|
||||
|
||||
@@ -14,13 +14,13 @@ not be imported through more than one path. Under a diamond —
|
||||
|
||||
```
|
||||
main ─┬─ mid_a ─┐
|
||||
└─ mid_b ─┴─ common (holds `impl Into(Wrapped) for s64`)
|
||||
└─ mid_b ─┴─ common (holds `impl Into(Wrapped) for i64`)
|
||||
```
|
||||
|
||||
— compilation failed with:
|
||||
|
||||
```
|
||||
error: duplicate impl 'Into' for source 's64' in .../common.sx
|
||||
error: duplicate impl 'Into' for source 'i64' in .../common.sx
|
||||
```
|
||||
|
||||
This bit the moment `modules/std/objc.sx` (imported by `main.sx`,
|
||||
@@ -48,10 +48,10 @@ imported through any diamond.
|
||||
`examples/issue-0056/common.sx`:
|
||||
|
||||
```sx
|
||||
Wrapped :: struct { v: s64; }
|
||||
Wrapped :: struct { v: i64; }
|
||||
|
||||
impl Into(Wrapped) for s64 {
|
||||
convert :: (self: s64) -> Wrapped {
|
||||
impl Into(Wrapped) for i64 {
|
||||
convert :: (self: i64) -> Wrapped {
|
||||
return .{ v = self };
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,8 @@ impl Into(Wrapped) for s64 {
|
||||
#import "issue-0056/mid_a.sx";
|
||||
#import "issue-0056/mid_b.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
w : Wrapped = xx 7; // pre-fix: duplicate impl 'Into' for source 's64'
|
||||
main :: () -> i32 {
|
||||
w : Wrapped = xx 7; // pre-fix: duplicate impl 'Into' for source 'i64'
|
||||
print("{}\n", w.v); // post-fix: prints 7
|
||||
0;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ module**. The identical code works (a) inline in the main file, and (b) in an
|
||||
imported module if the arg is passed *without* `xx` (auto-boxed).
|
||||
|
||||
- **Observed:** `Segmentation fault at address 0x1...` in `__platform_memmove`,
|
||||
via `runJITFromObject` (target.zig:244). Crashes for any int width (s32, u64).
|
||||
via `runJITFromObject` (target.zig:244). Crashes for any int width (i32, u64).
|
||||
- **Expected:** prints the formatted string, same as the auto-boxed / inline
|
||||
forms.
|
||||
|
||||
@@ -38,9 +38,9 @@ imported module if the arg is passed *without* `xx` (auto-boxed).
|
||||
```sx
|
||||
#import "std.sx";
|
||||
|
||||
build :: (n: s32) -> string {
|
||||
build :: (n: i32) -> string {
|
||||
result := "x:\n";
|
||||
i : s32 = 0;
|
||||
i : i32 = 0;
|
||||
while i < n {
|
||||
line := format(" item {}\n", xx i); // <-- xx cast to Any is the trigger
|
||||
result = concat(result, line);
|
||||
@@ -55,7 +55,7 @@ Driver (e.g. `.sx-tmp/repro.sx`):
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
m :: #import "modules/zz_repro.sx";
|
||||
main :: () -> s32 { print("[{}]", m.build(2)); return 0; }
|
||||
main :: () -> i32 { print("[{}]", m.build(2)); return 0; }
|
||||
```
|
||||
|
||||
Run: `./zig-out/bin/sx run .sx-tmp/repro.sx` → segfault.
|
||||
@@ -65,7 +65,7 @@ Run: `./zig-out/bin/sx run .sx-tmp/repro.sx` → segfault.
|
||||
- **Auto-box works:** change `xx i` → `i` in the module → prints fine.
|
||||
- **Inline works:** put the same `build` body (with `xx i`) directly in the
|
||||
driver's `main` (no import) → prints fine.
|
||||
- **Width-independent:** `xx` on an `s32` or a `u64` both crash.
|
||||
- **Width-independent:** `xx` on an `i32` or a `u64` both crash.
|
||||
- So the trigger is specifically: **explicit `xx <int>` → Any as a variadic
|
||||
`format`/`print` arg, in a function that lives in an imported module.**
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> **✅ RESOLVED.** Root cause: `resolveReturnType` ([src/ir/lower.zig]) infers a
|
||||
> no-annotation function's return type from its body, but the body references the
|
||||
> function's own params — which weren't in `self.scope` yet (they're bound later,
|
||||
> at body lowering). So `inferExprType` couldn't resolve `x` in `(x: s32) => x * 2`
|
||||
> at body lowering). So `inferExprType` couldn't resolve `x` in `(x: i32) => x * 2`
|
||||
> and returned `.unresolved`, which reached LLVM emission. It only slipped through
|
||||
> when a same-named binding happened to linger in scope from earlier lowering.
|
||||
> Fix: bind the function's plain annotated value params into a temporary scope
|
||||
@@ -28,7 +28,7 @@ thread … panic: unresolved type reached LLVM emission — a type resolution fa
|
||||
|
||||
- **Observed:** the lambda's `func.ret` is `.unresolved` when `declareFunction`
|
||||
runs, so emission panics (SIGABRT, exit 134).
|
||||
- **Expected:** the inferred return type (`s32` here) is resolved before
|
||||
- **Expected:** the inferred return type (`i32` here) is resolved before
|
||||
emission; the program prints `14` and exits 0.
|
||||
|
||||
## Reproduction
|
||||
@@ -38,7 +38,7 @@ thread … panic: unresolved type reached LLVM emission — a type resolution fa
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
f :: (x: s32) => x * 2; // inferred return type
|
||||
f :: (x: i32) => x * 2; // inferred return type
|
||||
|
||||
main :: () {
|
||||
print("{}\n", f(7)); // want: 14
|
||||
@@ -50,25 +50,25 @@ main :: () {
|
||||
|
||||
### Key contrasts (narrowing clues)
|
||||
|
||||
- **Explicit return type works:** `f :: (x: s32) -> s32 => x * 2;` → prints `14`,
|
||||
- **Explicit return type works:** `f :: (x: i32) -> i32 => x * 2;` → prints `14`,
|
||||
exit 0.
|
||||
- **The same lambda inside a large program works:** the old monolithic
|
||||
`50-smoke.sx` contained `double :: (x: s32) => x * 2;` as a local const and ran
|
||||
`50-smoke.sx` contained `double :: (x: i32) => x * 2;` as a local const and ran
|
||||
clean (exit 0). The panic only appears when the expr-bodied inferred-return
|
||||
lambda is compiled in a *small* module (minimal file, or as the first/only such
|
||||
function). This strongly suggests the return-type inference for `=>` lambdas is
|
||||
triggered as a side effect of some other pass that the large file happens to run
|
||||
and the minimal file does not — rather than being driven unconditionally for
|
||||
every expr-bodied lambda.
|
||||
- Both the **top-level** form (`f :: (x: s32) => x * 2;`) and the **local-const**
|
||||
- Both the **top-level** form (`f :: (x: i32) => x * 2;`) and the **local-const**
|
||||
form (inside `main`) panic identically.
|
||||
|
||||
## Investigation prompt (paste into a fresh session)
|
||||
|
||||
> An expression-bodied lambda `f :: (x: s32) => x * 2;` with an inferred return
|
||||
> An expression-bodied lambda `f :: (x: i32) => x * 2;` with an inferred return
|
||||
> type panics at `src/ir/emit_llvm.zig:4594` ("unresolved type reached LLVM
|
||||
> emission") because `func.ret` is still `.unresolved` when `declareFunction`
|
||||
> (emit_llvm.zig:1658) emits it. Adding an explicit `-> s32` fixes it, and the
|
||||
> (emit_llvm.zig:1658) emits it. Adding an explicit `-> i32` fixes it, and the
|
||||
> same lambda compiled inside a large module (the old 50-smoke.sx) resolves fine
|
||||
> — so the inferred-return resolution for `=>` lambdas is running only
|
||||
> conditionally.
|
||||
@@ -95,7 +95,7 @@ main :: () {
|
||||
|
||||
Blocks the `50-smoke.sx` split (test-layout migration, Phase 2): the
|
||||
**functions** section exercises exactly this construct
|
||||
(`double :: (x: s32) => x * 2;`), so it cannot be extracted into a standalone
|
||||
(`double :: (x: i32) => x * 2;`), so it cannot be extracted into a standalone
|
||||
example until this is fixed. Working around it (adding an explicit return type)
|
||||
would stop testing inferred-return lambdas and hide the bug, so per the project's
|
||||
impassable rule the split is paused here.
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
> arrow, direct + `Closure(...)` param).
|
||||
>
|
||||
> **Remaining E5.1 follow-up (not 0060):** calling a **bare** failable
|
||||
> function-type param (`cb: (s64) -> (s64, !E)`) resolves the call result as
|
||||
> `unresolved` (the idiomatic `Closure(s64) -> (s64, !E)` form works); the
|
||||
> function-type param (`cb: (i64) -> (i64, !E)`) resolves the call result as
|
||||
> `unresolved` (the idiomatic `Closure(i64) -> (i64, !E)` form works); the
|
||||
> non-failable→failable widening adapter is currently *rejected* rather than
|
||||
> generated; and the program-wide SCC union per closure shape is unimplemented.
|
||||
|
||||
@@ -39,10 +39,10 @@ non-failable closures miscompile too.
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
apply :: (f: (s64) -> s64) -> s64 { return f(5); }
|
||||
apply :: (f: (i64) -> i64) -> i64 { return f(5); }
|
||||
main :: () {
|
||||
print("block={}\n", apply(closure((x: s64) -> s64 { return x * 2; }))); // want 10
|
||||
print("arrow={}\n", apply(closure((x: s64) -> s64 => x * 2))); // want 10
|
||||
print("block={}\n", apply(closure((x: i64) -> i64 { return x * 2; }))); // want 10
|
||||
print("arrow={}\n", apply(closure((x: i64) -> i64 => x * 2))); // want 10
|
||||
}
|
||||
```
|
||||
|
||||
@@ -50,7 +50,7 @@ main :: () {
|
||||
- **Actual:** `block=192`, `arrow=20` (exit 0 — silent miscompile, no diagnostic).
|
||||
|
||||
**Working contrast:** `examples/0302-closures-closures.sx` —
|
||||
`apply :: (f, x) -> s64 { return f(x); }` called as `apply(closure(... => ...), 10)`
|
||||
`apply :: (f, x) -> i64 { return f(x); }` called as `apply(closure(... => ...), 10)`
|
||||
works. There the piped value arrives as a *separate* argument; here the callee
|
||||
calls the closure param with a *literal*, and the literal/closure-env marshalling
|
||||
is wrong. Likely an env/arg-slot mixup when a closure literal is materialized as a
|
||||
@@ -77,7 +77,7 @@ Two further gaps sit on top of 0060:
|
||||
called** ones work end-to-end (success / `catch` / `or` all correct).
|
||||
|
||||
2. **Arrow-body failable closures miscompile.** After the parser patch,
|
||||
`n := closure((x: s64) -> (s64, !E) => x + 1); n(40) catch e 0` returns `0`
|
||||
`n := closure((x: i64) -> (i64, !E) => x + 1); n(40) catch e 0` returns `0`
|
||||
instead of `41` — the value slot reads as undef/0. Block-body equivalents
|
||||
are correct, so it's an arrow-body (`=>`) failable-closure lowering bug
|
||||
(the expression-body return isn't assembled into the `{value, error}` tuple
|
||||
@@ -88,8 +88,8 @@ Two further gaps sit on top of 0060:
|
||||
## Investigation prompt (paste into a fresh session)
|
||||
|
||||
> Closure literals passed as a function-type argument miscompile when the callee
|
||||
> calls them: `apply :: (f: (s64)->s64) -> s64 { return f(5); }` then
|
||||
> `apply(closure((x: s64) -> s64 { return x*2; }))` prints 192 (want 10); the
|
||||
> calls them: `apply :: (f: (i64)->i64) -> i64 { return f(5); }` then
|
||||
> `apply(closure((x: i64) -> i64 { return x*2; }))` prints 192 (want 10); the
|
||||
> arrow form prints 20. The working pattern (examples/0302) passes the value as a
|
||||
> separate arg. Suspect the closure-literal-as-call-argument lowering: the
|
||||
> closure env / the inner call's constant argument is marshalled into the wrong
|
||||
|
||||
@@ -28,7 +28,7 @@ Expected: the dead statements are dropped (unreachable); the program compiles
|
||||
and runs.
|
||||
|
||||
This blocked ERR E5.1: the canonical failable-closure form from the plan,
|
||||
`closure((x) -> (s32, !) { raise error.X; return x; })`, has a dead `return x;`
|
||||
`closure((x) -> (i32, !) { raise error.X; return x; })`, has a dead `return x;`
|
||||
after the unconditional `raise` and tripped the verifier.
|
||||
|
||||
## Reproduction
|
||||
@@ -37,7 +37,7 @@ Minimal (non-failable — the bug is general, not error-specific):
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
main :: () -> s32 { return 0; print("dead\n"); }
|
||||
main :: () -> i32 { return 0; print("dead\n"); }
|
||||
```
|
||||
|
||||
Failable facet (the form that blocked E5.1):
|
||||
@@ -45,8 +45,8 @@ Failable facet (the form that blocked E5.1):
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
E :: error { Neg }
|
||||
top :: (x: s64) -> (s64, !E) { raise error.Neg; return x; }
|
||||
main :: () -> s32 { print("r={}\n", top(5) catch e 0); return 0; }
|
||||
top :: (x: i64) -> (i64, !E) { raise error.Neg; return x; }
|
||||
main :: () -> i32 { print("r={}\n", top(5) catch e 0); return 0; }
|
||||
```
|
||||
|
||||
Both abort with "Terminator found in the middle of a basic block". A
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
>
|
||||
> ```sx
|
||||
> wrap :: ($T: Type, f: Closure() -> (T, !E)) -> (T, !E) { return try f(); }
|
||||
> wrap(s32, closure(() -> (s32, !E) { return 7; })) catch e -1 // 7
|
||||
> r, err := wrap(s32, closure(() -> (s32, !E) { return 9; })) // r=9
|
||||
> wrap(s32, closure(() -> (s32, !E) { raise error.Bad; })) catch e -1 // -1
|
||||
> wrap(i32, closure(() -> (i32, !E) { return 7; })) catch e -1 // 7
|
||||
> r, err := wrap(i32, closure(() -> (i32, !E) { return 9; })) // r=9
|
||||
> wrap(i32, closure(() -> (i32, !E) { raise error.Bad; })) catch e -1 // -1
|
||||
> ```
|
||||
>
|
||||
> The only real (separate, orthogonal) defect found: a NON-`$` `T: Type` function
|
||||
@@ -30,7 +30,7 @@ failable return tuple during monomorphization. Observed two ways:
|
||||
- Consumed via destructure: the success value renders as `T{}` (the literal
|
||||
generic type name) instead of the concrete value, and the error slot is wrong.
|
||||
|
||||
Expected: `T` is bound to the concrete monomorphization type (`s32`), the success
|
||||
Expected: `T` is bound to the concrete monomorphization type (`i32`), the success
|
||||
value flows through as `7`, and the error slot is `0` on success.
|
||||
|
||||
## Reproduction
|
||||
@@ -40,9 +40,9 @@ value flows through as `7`, and the error slot is `0` on success.
|
||||
E :: error { Bad }
|
||||
wrap :: (T: type, f: Closure() -> (T, !E)) -> (T, !E) { return try f(); }
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
// catch form → LLVM phi type mismatch:
|
||||
r := wrap(s32, closure(() -> (s32, !E) { return 7; })) catch e -1;
|
||||
r := wrap(i32, closure(() -> (i32, !E) { return 7; })) catch e -1;
|
||||
print("{}\n", r); // want 7
|
||||
return 0;
|
||||
}
|
||||
@@ -51,8 +51,8 @@ main :: () -> s32 {
|
||||
Destructure form (same root cause, different surfacing):
|
||||
|
||||
```sx
|
||||
r, err := wrap(s32, closure(() -> (s32, !E) { return 7; }));
|
||||
print("{} {}\n", r, xx err); // prints "T{} s64"; want "7 0"
|
||||
r, err := wrap(i32, closure(() -> (i32, !E) { return 7; }));
|
||||
print("{} {}\n", r, xx err); // prints "T{} i64"; want "7 0"
|
||||
```
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
@@ -27,7 +27,7 @@ The UFCS auto-address-of (`p.bump()` → `bump(@p)`) does not kick in for free
|
||||
functions; the receiver is loaded by value instead of having its address taken.
|
||||
The same method defined **inside** the struct works fine — so this is specific
|
||||
to free-function UFCS, not method calls in general. Not failable-specific (the
|
||||
repro is a plain `-> s32`), so this is orthogonal to ERR.
|
||||
repro is a plain `-> i32`), so this is orthogonal to ERR.
|
||||
|
||||
Expected: `p.bump()` on a `*Parser`-first-param free function takes `@p`'s
|
||||
address, matching the in-struct method behavior.
|
||||
@@ -36,17 +36,17 @@ address, matching the in-struct method behavior.
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
Parser :: struct { pos: s32; }
|
||||
bump :: (p: *Parser) -> s32 { p.pos += 1; return p.pos; } // FREE fn, pointer first param
|
||||
Parser :: struct { pos: i32; }
|
||||
bump :: (p: *Parser) -> i32 { p.pos += 1; return p.pos; } // FREE fn, pointer first param
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
p := Parser.{ pos = 0 };
|
||||
print("{}\n", p.bump()); // LLVM signature mismatch
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Control (works): move `bump` inside `Parser :: struct { … bump :: (p: *Parser) -> s32 { … } }`.
|
||||
Control (works): move `bump` inside `Parser :: struct { … bump :: (p: *Parser) -> i32 { … } }`.
|
||||
Also fails with an explicit `bump(@p)` — so the explicit address-of of a local
|
||||
struct into a pointer param is the underlying miscompile, not just the UFCS sugar.
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ produces garbage (the value renders as `T{}`), with no diagnostic.
|
||||
|
||||
```sx
|
||||
idwrap :: (T: Type, f: Closure() -> T) -> T { return f(); }
|
||||
main :: () -> s32 {
|
||||
print("{}\n", idwrap(s32, closure(() -> s32 { return 7; }))); // prints "T{}", want 7
|
||||
main :: () -> i32 {
|
||||
print("{}\n", idwrap(i32, closure(() -> i32 { return 7; }))); // prints "T{}", want 7
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,7 +39,7 @@ in binding position doesn't parse" note in `current/CHECKPOINT-ERR.md`
|
||||
#import "modules/std.sx";
|
||||
|
||||
E :: error { Bad }
|
||||
val :: () -> (s32, !E) { return 5; }
|
||||
val :: () -> (i32, !E) { return 5; }
|
||||
|
||||
f :: () -> !E {
|
||||
defer {
|
||||
@@ -49,7 +49,7 @@ f :: () -> !E {
|
||||
return;
|
||||
}
|
||||
|
||||
main :: () -> s32 { return 0; }
|
||||
main :: () -> i32 { return 0; }
|
||||
```
|
||||
|
||||
Also reproduces with no `defer`, as a plain value block:
|
||||
|
||||
@@ -31,19 +31,19 @@ regression example (0040); the example sidesteps it with positive arm values.
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
classify :: (n: s32) -> s32 {
|
||||
classify :: (n: i32) -> i32 {
|
||||
if n == {
|
||||
case 0: 100;
|
||||
case 1: 10;
|
||||
else: -1; // ← negated literal arm → phi width mismatch
|
||||
}
|
||||
}
|
||||
main :: () -> s32 { classify(1) }
|
||||
main :: () -> i32 { classify(1) }
|
||||
```
|
||||
|
||||
Expected: compiles, `classify(1)` returns 10. Actual: LLVM verification failure.
|
||||
|
||||
Workaround: give the arm an explicitly-typed value (`else: { x : s32 = -1; x }`)
|
||||
Workaround: give the arm an explicitly-typed value (`else: { x : i32 = -1; x }`)
|
||||
or avoid the negation.
|
||||
|
||||
## Investigation prompt
|
||||
@@ -52,7 +52,7 @@ Look at the match-as-value lowering in `src/ir/lower.zig` (`lowerMatch`, the
|
||||
`has_value_merge` path ~line 4500, and the arm `result_type` inference
|
||||
~line 11698). The arm value is lowered via `lowerBlockValue(arm.body)` then
|
||||
`coerceToType(v, v_ty, result_type)`. For a negated-literal arm the value's
|
||||
type comes out narrower than `result_type` (s64 here), and the coercion path
|
||||
type comes out narrower than `result_type` (i64 here), and the coercion path
|
||||
that should widen it before the `br merge_bb, {v}` either runs against the wrong
|
||||
target or is skipped — so the phi gets an i32 operand under an i64 result. Make
|
||||
the arm value coerce to the merge's `result_type` consistently (mirror how the
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
> **RESOLVED** (2026-06-02).
|
||||
> **Root cause:** `type_bridge.resolveTupleLiteralAsType` treated a tuple literal
|
||||
> as a tuple TYPE and, for any element that wasn't type-shaped, emitted a
|
||||
> `std.debug.print` and substituted `.s64` for that field — a silent fabricated
|
||||
> `std.debug.print` and substituted `.i64` for that field — a silent fabricated
|
||||
> type (the forbidden silent-fallback pattern). The stateful caller
|
||||
> (`Lowering.resolveTypeArg`, used by `size_of`) delegated `.tuple_literal`
|
||||
> straight to that path, so `size_of((s32, 1))` compiled and printed `16`.
|
||||
> straight to that path, so `size_of((i32, 1))` compiled and printed `16`.
|
||||
> **Fix:**
|
||||
> - `type_bridge.resolveTupleLiteralAsType` now returns `.unresolved` (no `.s64`,
|
||||
> - `type_bridge.resolveTupleLiteralAsType` now returns `.unresolved` (no `.i64`,
|
||||
> no debug print) when any element is not type-shaped — it refuses to fabricate
|
||||
> a tuple. (type_bridge is stateless, so this is the binding-free backstop.)
|
||||
> - New stateful `Lowering.resolveTupleLiteralTypeArg` validates each element via
|
||||
@@ -17,14 +17,14 @@
|
||||
> `resolveTypeArg` (size_of/align_of/…) and the `resolveTypeWithBindings`
|
||||
> name-fallback; type_bridge builds the tuple only after validation passes.
|
||||
> **Regression test:** `examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx`
|
||||
> (exit 1 + diagnostic). Valid `(s32, s32)` still works
|
||||
> (exit 1 + diagnostic). Valid `(i32, i32)` still works
|
||||
> (`examples/0115-types-compound-type-in-expression.sx`). Suite 351/0.
|
||||
|
||||
## Symptom
|
||||
|
||||
`size_of((s32, 1))` treats the tuple literal as a tuple TYPE even though `1` is
|
||||
`size_of((i32, 1))` treats the tuple literal as a tuple TYPE even though `1` is
|
||||
not a type. The compiler prints an internal `type_bridge` debug line, then
|
||||
silently substitutes `.s64` for that slot and compiles successfully.
|
||||
silently substitutes `.i64` for that slot and compiles successfully.
|
||||
|
||||
Observed:
|
||||
|
||||
@@ -41,8 +41,8 @@ with no fabricated tuple type and no successful run.
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
print("bad tuple type size = {}\n", size_of((s32, 1)));
|
||||
main :: () -> i32 {
|
||||
print("bad tuple type size = {}\n", size_of((i32, 1)));
|
||||
0
|
||||
}
|
||||
```
|
||||
@@ -59,27 +59,27 @@ scratch file under `.sx-tmp/`.
|
||||
## Investigation prompt
|
||||
|
||||
Fix issue 0067: tuple literals reinterpreted as tuple types must reject non-type
|
||||
elements instead of silently fabricating `.s64` fields.
|
||||
elements instead of silently fabricating `.i64` fields.
|
||||
|
||||
Suspected area:
|
||||
- `src/ir/type_bridge.zig`, `resolveTupleLiteralAsType`
|
||||
- The current non-type branch does `std.debug.print(...)` and
|
||||
`field_ids.append(alloc, .s64)`, which violates the compiler fallback rules.
|
||||
`field_ids.append(alloc, .i64)`, which violates the compiler fallback rules.
|
||||
- Related callers: `type_bridge.resolveAstType` for `.tuple_literal`, and
|
||||
`Lowering.resolveTypeWithBindings` fallback paths that reach `type_bridge`.
|
||||
|
||||
Likely fix:
|
||||
- Replace the `.s64` substitution with a real diagnostic path and an
|
||||
- Replace the `.i64` substitution with a real diagnostic path and an
|
||||
unmistakable failure result (`.unresolved`, or a nullable/result return that
|
||||
forces callers to handle the failure).
|
||||
- Make the diagnostic user-facing via the lowering diagnostics plumbing, not
|
||||
`std.debug.print`.
|
||||
- Preserve the valid behavior pinned by `examples/0115-types-compound-type-in-expression.sx`,
|
||||
where `(s32, s32)` in a type-demanding site resolves as a tuple type.
|
||||
where `(i32, i32)` in a type-demanding site resolves as a tuple type.
|
||||
|
||||
Verification:
|
||||
- Add a focused diagnostics example in the `11xx` block for
|
||||
`size_of((s32, 1))` expecting exit 1 and a clear diagnostic.
|
||||
`size_of((i32, 1))` expecting exit 1 and a clear diagnostic.
|
||||
- Run:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -39,7 +39,7 @@ fabricated empty-struct type.
|
||||
|
||||
NotAType :: 123;
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
v: NotAType = ---;
|
||||
print("value = {}\n", v);
|
||||
return 0;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> **RESOLVED.** Root cause: `Lowering.scanDecls`' `.identifier` alias branch only
|
||||
> registered `A :: B` into `ProgramIndex.type_alias_map` when `B` was already
|
||||
> known (in `type_alias_map` or the `TypeTable`). A forward target declared later
|
||||
> (`MyChain :: MyInt; MyInt :: s32;`) was never present during the single forward
|
||||
> (`MyChain :: MyInt; MyInt :: i32;`) was never present during the single forward
|
||||
> scan, so the alias name went unregistered and the A2.4 unknown-type pass — which
|
||||
> treats `type_alias_map` keys as declared types — flagged its uses.
|
||||
> Fix: added a fixpoint post-pass `resolveForwardIdentifierAliases` at the end of
|
||||
@@ -20,17 +20,17 @@ though the same alias chain works when ordered after its target.
|
||||
|
||||
Observed: `MyChain` is diagnosed as an unknown type.
|
||||
|
||||
Expected: `MyChain :: MyInt; MyInt :: s32;` should resolve `MyChain` to `s32`
|
||||
Expected: `MyChain :: MyInt; MyInt :: i32;` should resolve `MyChain` to `i32`
|
||||
when used in a type annotation, matching the existing ordered-chain behavior
|
||||
(`MyInt :: s32; MyChain :: MyInt;`).
|
||||
(`MyInt :: i32; MyChain :: MyInt;`).
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
MyChain :: MyInt;
|
||||
MyInt :: s32;
|
||||
MyInt :: i32;
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
v: MyChain = 7;
|
||||
return v;
|
||||
}
|
||||
@@ -64,11 +64,11 @@ Context:
|
||||
- This surfaced while re-reviewing `8770145`, the issue-0068 fix. That fix
|
||||
correctly stopped arbitrary value consts (`NotAType :: 123`) from satisfying
|
||||
the unknown-type check.
|
||||
- Ordered identifier aliases still work: `MyInt :: s32; MyChain :: MyInt;`.
|
||||
- Ordered identifier aliases still work: `MyInt :: i32; MyChain :: MyInt;`.
|
||||
- `.call` type aliases still work: `Vec3 :: Vec(3, f32);` and
|
||||
`Foo :: Complex(u32);`.
|
||||
- The failing shape is specifically a forward identifier alias:
|
||||
`MyChain :: MyInt; MyInt :: s32;`.
|
||||
`MyChain :: MyInt; MyInt :: i32;`.
|
||||
|
||||
Suspected area:
|
||||
- `src/ir/lower.zig`, `Lowering.scanDecls`, especially the `.identifier` alias
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
> fixpoint runs at the END of `Lowering.scanDecls`, but the same scan loop
|
||||
> resolved top-level `var_decl` global annotations (and typed module-constant
|
||||
> annotations) via `self.resolveType(ta)` BEFORE that fixpoint ran — so a forward
|
||||
> alias (`A :: B; B :: s32; g : A = 7;`) was still absent from
|
||||
> alias (`A :: B; B :: i32; g : A = 7;`) was still absent from
|
||||
> `type_alias_map`, `resolveType` fabricated an empty-struct stub, and the global
|
||||
> got a type mismatching its initializer at LLVM verification (the typed-const
|
||||
> path silently mistyped the constant instead).
|
||||
@@ -30,18 +30,18 @@ LLVM verification failed: Global variable initializer type does not match global
|
||||
ptr @g
|
||||
```
|
||||
|
||||
Expected: `A :: B; B :: s32; g : A = 7;` should type `g` as `s32` and compile/run
|
||||
Expected: `A :: B; B :: i32; g : A = 7;` should type `g` as `i32` and compile/run
|
||||
the same way as the ordered alias form.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
A :: B;
|
||||
B :: s32;
|
||||
B :: i32;
|
||||
|
||||
g : A = 7;
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
return g;
|
||||
}
|
||||
```
|
||||
@@ -63,7 +63,7 @@ annotation must resolve before that global's type is registered.
|
||||
Context:
|
||||
- Issue 0069 (`49a383d`) added `Lowering.resolveForwardIdentifierAliases`, a
|
||||
fixpoint post-pass at the end of `scanDecls`, to resolve top-level
|
||||
identifier-RHS aliases like `A :: B; B :: s32;`.
|
||||
identifier-RHS aliases like `A :: B; B :: i32;`.
|
||||
- That works for aliases used later in function bodies because the A2.4
|
||||
unknown-type pass and body lowering run after `scanDecls`.
|
||||
- But top-level `var_decl` annotations are resolved inside the same `scanDecls`
|
||||
@@ -92,9 +92,9 @@ Verification:
|
||||
|
||||
```sx
|
||||
A :: B;
|
||||
B :: s32;
|
||||
B :: i32;
|
||||
g : A = 7;
|
||||
main :: () -> s32 { return g; }
|
||||
main :: () -> i32 { return g; }
|
||||
```
|
||||
|
||||
- Keep `examples/0132-types-forward-type-alias.sx`,
|
||||
|
||||
@@ -35,12 +35,12 @@ supported.
|
||||
#import "modules/std.sx";
|
||||
|
||||
A :: B;
|
||||
B :: s32;
|
||||
B :: i32;
|
||||
|
||||
K : A : 42;
|
||||
g : A = K;
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
print("g={}\n", g);
|
||||
return g;
|
||||
}
|
||||
@@ -82,7 +82,7 @@ Likely fix:
|
||||
the global's declared type.
|
||||
- If some initializer shape cannot be represented as a global constant yet, emit
|
||||
a diagnostic instead of returning `null` / zero-initializing.
|
||||
- Do not regress issue 0070: `A :: B; B :: s32; g : A = 7;` and
|
||||
- Do not regress issue 0070: `A :: B; B :: i32; g : A = 7;` and
|
||||
`K : A : 35;` must still resolve through the converged alias map.
|
||||
- Preserve literal, array literal, struct literal, and foreign-global behavior.
|
||||
|
||||
@@ -92,10 +92,10 @@ Verification:
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
A :: B;
|
||||
B :: s32;
|
||||
B :: i32;
|
||||
K : A : 42;
|
||||
g : A = K;
|
||||
main :: () -> s32 { print("g={}\n", g); return g; }
|
||||
main :: () -> i32 { print("g={}\n", g); return g; }
|
||||
```
|
||||
|
||||
- Keep these green:
|
||||
|
||||
@@ -35,14 +35,14 @@ initializer loudly if field-access global constants are not supported yet.
|
||||
#import "modules/std.sx";
|
||||
|
||||
Point :: struct {
|
||||
x: s32;
|
||||
y: s32;
|
||||
x: i32;
|
||||
y: i32;
|
||||
}
|
||||
|
||||
K : Point : Point.{ x = 9, y = 4 };
|
||||
g : s32 = K.x;
|
||||
g : i32 = K.x;
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
print("g={}\n", g);
|
||||
return g;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ Context:
|
||||
(`K : A : 42; g : A = K;`) and diagnoses identifiers that are not usable
|
||||
constants.
|
||||
- A remaining non-identifier expression shape still falls through silently:
|
||||
`K : Point : Point.{ x = 9, y = 4 }; g : s32 = K.x;` emits a null global
|
||||
`K : Point : Point.{ x = 9, y = 4 }; g : i32 = K.x;` emits a null global
|
||||
initializer payload and runs as `g=0`.
|
||||
|
||||
Suspected area:
|
||||
|
||||
@@ -54,7 +54,7 @@ work :: () {
|
||||
defer { cb := () { return; }; }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
work();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
> `abiCoerceParamType` → `coerceArg` treat as a legitimate (void-typed) foreign
|
||||
> argument, corrupting the call ABI with no diagnostic. Fix: one shared resolver
|
||||
> `LLVMEmitter.argIRTypeOrFail` ([src/ir/emit_llvm.zig]) returns the dedicated
|
||||
> `.unresolved` sentinel on a failed lookup — never `.void`/`.s64` — so the failure
|
||||
> `.unresolved` sentinel on a failed lookup — never `.void`/`.i64` — so the failure
|
||||
> cannot masquerade as a real type and trips `toLLVMType`'s existing hard `@panic`
|
||||
> tripwire at the call site. All four sites
|
||||
> ([src/ir/emit_llvm.zig] JNI constructor; [src/backend/llvm/ops.zig] objc_msgSend,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
> **RESOLVED** (2026-06-03)
|
||||
> **Root cause:** the `type_name` / `type_eq` reflection builtins resolved their
|
||||
> `Type` arg's IR type with `getRefIRType(...) orelse TypeId.s64`, then gated `== .any`
|
||||
> — so a failed must-succeed lookup silently became "bare i64" (`.s64 != .any`),
|
||||
> `Type` arg's IR type with `getRefIRType(...) orelse TypeId.i64`, then gated `== .any`
|
||||
> — so a failed must-succeed lookup silently became "bare i64" (`.i64 != .any`),
|
||||
> reading the wrong value with no diagnostic.
|
||||
> **Fix:** added the sibling classifier `LLVMEmitter.reflectArgRepr`
|
||||
> (`src/ir/emit_llvm.zig`) which routes the lookup through `argIRTypeOrFail` →
|
||||
@@ -16,18 +16,18 @@
|
||||
> `.void` (null-ptr / undef-i64). Left in place with an invariant comment.
|
||||
> **Regression test:** `src/ir/emit_llvm.test.zig` — "emit: reflectArgRepr surfaces
|
||||
> .unresolved for an unresolvable reflection arg ref (issue 0075)" (fail-before with
|
||||
> `orelse .s64` → `.bare`; pass-after → `.unresolved`).
|
||||
> `orelse .i64` → `.bare`; pass-after → `.unresolved`).
|
||||
|
||||
# 0075 — silent `getRefIRType(...) orelse TypeId.s64` fallback in reflection builtins
|
||||
# 0075 — silent `getRefIRType(...) orelse TypeId.i64` fallback in reflection builtins
|
||||
|
||||
## Symptom
|
||||
**One-line:** The `type_name` and `type_eq` reflection builtins resolve their Type
|
||||
argument's IR type via `getRefIRType(...) orelse TypeId.s64` — the forbidden
|
||||
silent-type-lookup fallback (`.s64` is the exact issue-0042 sentinel the project
|
||||
argument's IR type via `getRefIRType(...) orelse TypeId.i64` — the forbidden
|
||||
silent-type-lookup fallback (`.i64` is the exact issue-0042 sentinel the project
|
||||
rules name) — so a failed must-succeed lookup silently decides "not boxed (`!= .any`)"
|
||||
and mis-handles the value with no diagnostic.
|
||||
|
||||
**Observed (primary — must fix):** `self.e.getRefIRType(...) orelse TypeId.s64` at:
|
||||
**Observed (primary — must fix):** `self.e.getRefIRType(...) orelse TypeId.i64` at:
|
||||
- `src/backend/llvm/ops.zig:1023` (`.type_name` builtin — `arg_ir_ty`, gates the
|
||||
`== .any` boxed-extract vs bare-i64 decision)
|
||||
- `src/backend/llvm/ops.zig:1049` (`.type_eq` builtin — first operand)
|
||||
@@ -35,8 +35,8 @@ and mis-handles the value with no diagnostic.
|
||||
|
||||
`getRefIRType` (`src/ir/emit_llvm.zig:2229`, `?TypeId`) returns `null` only when a ref
|
||||
is neither a function param nor a block instruction result — a must-not-happen case
|
||||
for a real builtin argument. On `null` the code defaults to `.s64`, then tests
|
||||
`arg_ir_ty == .any`; the `.s64` default silently means "treat as a bare TypeId index,
|
||||
for a real builtin argument. On `null` the code defaults to `.i64`, then tests
|
||||
`arg_ir_ty == .any`; the `.i64` default silently means "treat as a bare TypeId index,
|
||||
not a boxed `Any`", so a genuinely-boxed arg whose lookup failed would skip the
|
||||
`ExtractValue` and use the wrong value — silent miscompile, no diagnostic.
|
||||
|
||||
@@ -55,36 +55,36 @@ issue 0074), never a real-type default.
|
||||
stating the invariant.
|
||||
|
||||
## Audited — intentional language defaults (NO action; documented so they aren't re-flagged)
|
||||
- `src/ir/lower.zig:4855` — `int_literal => constInt(lit.value, info.ty orelse .s64)`:
|
||||
an untyped integer literal defaulting to `s64` is standard language semantics, not a
|
||||
- `src/ir/lower.zig:4855` — `int_literal => constInt(lit.value, info.ty orelse .i64)`:
|
||||
an untyped integer literal defaulting to `i64` is standard language semantics, not a
|
||||
lookup failure.
|
||||
- `src/ir/lower.zig:4856` — `float_literal => constFloat(..., info.ty orelse .f64)`:
|
||||
untyped float literal defaults to `f64` — language semantics.
|
||||
- `src/ir/type_bridge.zig:334` — `.tag_type = tag_type orelse .s64`: documented
|
||||
- `src/ir/type_bridge.zig:334` — `.tag_type = tag_type orelse .i64`: documented
|
||||
("enum unions are always tagged (default i64)") — an intentional default tag type,
|
||||
not a swallowed lookup.
|
||||
|
||||
## Provenance / scope
|
||||
Pre-existing, NOT introduced by the arch-refactor. Discovered during the **issue-0074
|
||||
fix** (the fix worker surfaced the reflection `.s64` fallbacks as a separate pattern
|
||||
fix** (the fix worker surfaced the reflection `.i64` fallbacks as a separate pattern
|
||||
outside 0074's FFI-arg scope) and confirmed by a manager sweep
|
||||
(`rg "orelse \.(s64|void|...)" src`). Filed per the IMPASSIBLE RULE (existing
|
||||
(`rg "orelse \.(i64|void|...)" src`). Filed per the IMPASSIBLE RULE (existing
|
||||
default-returns that swallow a lookup failure → file, don't fix in place).
|
||||
|
||||
## Reproduction
|
||||
Latent / static (same nature as 0074): well-formed IR always gives a builtin arg a
|
||||
resolvable type, so the `.s64` default can't be driven at runtime today — which is why
|
||||
resolvable type, so the `.i64` default can't be driven at runtime today — which is why
|
||||
it's dangerous (a future IR change would silently miscompile `type_name`/`type_eq`).
|
||||
Exercised by the comptime/reflection examples; the fix must keep the suite at 361/0.
|
||||
|
||||
## Investigation prompt (ready to paste into a fresh session)
|
||||
> In `/Users/agra/projects/sx`, the `.type_name` and `.type_eq` reflection builtins in
|
||||
> `src/backend/llvm/ops.zig` (lines 1023, 1049, 1055) resolve a Type argument's IR type
|
||||
> with the forbidden silent fallback `getRefIRType(...) orelse TypeId.s64`, used to gate
|
||||
> with the forbidden silent fallback `getRefIRType(...) orelse TypeId.i64`, used to gate
|
||||
> a `== .any` boxed-vs-bare decision. Issue 0074 already added the shared resolver
|
||||
> `LLVMEmitter.argIRTypeOrFail` (`src/ir/emit_llvm.zig`) returning the dedicated
|
||||
> `.unresolved` sentinel on a failed lookup. Route these three sites through that helper
|
||||
> (or a sibling) so a failed lookup yields `.unresolved` — never `.s64`; then `==.any`
|
||||
> (or a sibling) so a failed lookup yields `.unresolved` — never `.i64`; then `==.any`
|
||||
> is false for `.unresolved` AND you must make the unresolved case loud (diagnostic via
|
||||
> `self.diagnostics.addFmt(.err, span, ...)` or a hard tripwire), not silently "bare
|
||||
> i64". Also resolve the borderline `lower.zig:2527/2528` `target_type orelse .void`
|
||||
@@ -94,4 +94,4 @@ Exercised by the comptime/reflection examples; the fix must keep the suite at 36
|
||||
> bash tests/run_examples.sh` stays 361/0; add a `*.test.zig` regression test asserting
|
||||
> the loud `.unresolved` path for a `type_name`/`type_eq` arg with an unresolvable ref
|
||||
> (fail-before/pass-after). Expected new behavior: an unresolved reflection-builtin arg
|
||||
> type surfaces loudly, never silently defaults to `.s64`.
|
||||
> type surfaces loudly, never silently defaults to `.i64`.
|
||||
@@ -46,9 +46,9 @@
|
||||
> `Param.name_span`.
|
||||
>
|
||||
> **Regression tests:**
|
||||
> - `examples/0125-types-type-named-var-rejected.sx` — `:=` form (`s2`) rejected.
|
||||
> - `examples/0125-types-type-named-var-rejected.sx` — `:=` form (`i2`) rejected.
|
||||
> - `examples/1119-diagnostics-reserved-type-name-as-identifier.sx` — parameter
|
||||
> (`u8`), typed-local (`s64`, `bool`), and `:=` (`string`) forms rejected.
|
||||
> (`u8`), typed-local (`i64`, `bool`), and `:=` (`string`) forms rejected.
|
||||
> - `examples/1121-diagnostics-reserved-name-control-flow.sx` — destructure name,
|
||||
> `if` / `while` optional bindings, `for` capture + index names, match-arm
|
||||
> capture.
|
||||
@@ -65,12 +65,12 @@
|
||||
> streaming with non-reserved names (`hasher`, `ctx`) accumulates correctly via
|
||||
> both `update(@h, …)` and `h.update(…)`.
|
||||
>
|
||||
> Pre-existing example `examples/0904-...` declared locals `s1`/`s2` (incidental
|
||||
> Pre-existing example `examples/0904-...` declared locals `i1`/`i2` (incidental
|
||||
> names); renamed to `filled`/`empty`.
|
||||
>
|
||||
> **Coverage extension (issue 0077).** The first landing scoped the binding
|
||||
> check to main-file decls (matching the unknown-type check's trusted-imports
|
||||
> convention); an imported module could still declare `s2 := …` and hit the
|
||||
> convention); an imported module could still declare `i2 := …` and hit the
|
||||
> original LLVM verifier abort. The reserved-name binding diagnostic now runs
|
||||
> over EVERY compiled module — imported user modules (descending the
|
||||
> `namespace_decl` an `mod :: #import` wraps) AND the stdlib `library/` — and
|
||||
@@ -81,11 +81,11 @@
|
||||
|
||||
## Symptom (how it first surfaced)
|
||||
|
||||
A local variable whose name is lexically a type — e.g. `s2` (the `sN`
|
||||
arbitrary-width signed-int syntax: `Type.fromName("s2")` → `s(2)`), or `u8`,
|
||||
`s64`, etc. — is accepted as a variable. Because such a name parses as a
|
||||
A local variable whose name is lexically a type — e.g. `i2` (the `sN`
|
||||
arbitrary-width signed-int syntax: `Type.fromName("i2")` → `s(2)`), or `u8`,
|
||||
`i64`, etc. — is accepted as a variable. Because such a name parses as a
|
||||
`.type_expr` (not `.identifier`), the address-of family of lowering sites
|
||||
(`@s2`, the autoref `s2.update(...)` receiver, a bare `f(s2)` at a `*T` param,
|
||||
(`@i2`, the autoref `i2.update(...)` receiver, a bare `f(i2)` at a `*T` param,
|
||||
global function-pointer args) does NOT recognize it as a scoped local and falls
|
||||
through to value lowering — loading the whole aggregate and passing it **by
|
||||
value** to a `ptr` parameter:
|
||||
@@ -104,7 +104,7 @@ any of this — the `.identifier` paths already work correctly.
|
||||
|
||||
The language is **accepting reserved/builtin type names as identifiers** in the
|
||||
first place. `sN`/`uN` (arbitrary-width ints) and the named builtins
|
||||
(`bool`, `string`, `void`, `f32`, `f64`, `s8`/`s16`/`s32`/`s64`,
|
||||
(`bool`, `string`, `void`, `f32`, `f64`, `i8`/`i16`/`i32`/`i64`,
|
||||
`u8`/`u16`/`u32`/`u64`, …) are reserved type names; declaring a variable with
|
||||
such a name is meaningless and produces the mis-lowering above. Patching each
|
||||
address-of site to tolerate the name (the rejected `bareVarName` approach) is
|
||||
@@ -135,14 +135,14 @@ type-shaped name can reach them).
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
Sha256 :: struct { h:[8]u64; block:[64]u8; block_len:s64=0; total_len:u64=0; }
|
||||
Sha256 :: struct { h:[8]u64; block:[64]u8; block_len:i64=0; total_len:u64=0; }
|
||||
init :: () -> Sha256 { s:Sha256=---; s.block_len=0; s.total_len=0; s }
|
||||
update :: (self:*Sha256, data:string) { self.total_len += data.len; }
|
||||
main :: () -> s32 { s2 := init(); update(@s2, "."); print("total_len={}\n", s2.total_len); return 0; }
|
||||
main :: () -> i32 { i2 := init(); update(@i2, "."); print("total_len={}\n", i2.total_len); return 0; }
|
||||
```
|
||||
|
||||
`./zig-out/bin/sx run <file>` today → LLVM verifier abort.
|
||||
**Expected after fix:** a clean compile-time diagnostic that `s2` is a reserved
|
||||
**Expected after fix:** a clean compile-time diagnostic that `i2` is a reserved
|
||||
type name and cannot be an identifier (exit non-zero, readable error — NOT an
|
||||
LLVM abort, NOT a silent copy). The same program with a non-reserved name
|
||||
(`hasher := init(); update(@hasher, ".")`) must compile and print `total_len=1`.
|
||||
@@ -150,7 +150,7 @@ LLVM abort, NOT a silent copy). The same program with a non-reserved name
|
||||
## Verification
|
||||
|
||||
1. Pinned diagnostics test(s) asserting the error for representative reserved
|
||||
names used as identifiers: `s2`, `u8`, `s64`, `bool`, `string` (declaration
|
||||
names used as identifiers: `i2`, `u8`, `i64`, `bool`, `string` (declaration
|
||||
forms: `:=`, typed local, and a parameter name). Capture the diagnostic text
|
||||
in `expected/`.
|
||||
2. A positive test: the same `*self` streaming pattern with NON-reserved names
|
||||
@@ -165,6 +165,6 @@ LLVM abort, NOT a silent copy). The same program with a non-reserved name
|
||||
## Provenance
|
||||
|
||||
Discovered by the `distribution` flow (P1.2 pure-sx SHA-256), whose minimal repro
|
||||
happened to name a local `s2`. Real SHA-256 code with names like `hasher`/`ctx`
|
||||
happened to name a local `i2`. Real SHA-256 code with names like `hasher`/`ctx`
|
||||
is unaffected on the current compiler — so the P1.2 "blocker" was a
|
||||
naming artifact, and this issue is really a missing-diagnostic correctness bug.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
>
|
||||
> **Root cause:** the reserved-name binding diagnostic (issue 0076) only ran
|
||||
> over main-file decls (`UnknownTypeChecker.run`'s `main_file` filter). An
|
||||
> imported module's `s2 := …` was never checked and reached lowering, where the
|
||||
> imported module's `i2 := …` was never checked and reached lowering, where the
|
||||
> address-of family loaded the whole aggregate and passed it by value to a
|
||||
> `*Box` param — LLVM verifier abort.
|
||||
>
|
||||
@@ -27,7 +27,7 @@
|
||||
>
|
||||
> **Regression test:** `examples/1120-diagnostics-imported-reserved-type-name.sx`
|
||||
> (+ companion `1120-diagnostics-imported-reserved-type-name/mod.sx`) — an
|
||||
> imported module declaring `s2 := …` now emits the clean diagnostic at the
|
||||
> imported module declaring `i2 := …` now emits the clean diagnostic at the
|
||||
> import's declaration site (exit 1), not an LLVM abort.
|
||||
|
||||
## Symptom
|
||||
@@ -36,7 +36,7 @@ An imported module can still declare a parameter or `var` binding whose name is
|
||||
reserved/builtin type name. Observed: the imported-module repro below reaches
|
||||
lowering and fails LLVM verification by passing a loaded struct value to a
|
||||
`*Box` parameter. Expected: the same declaration-site diagnostic used for
|
||||
main-file issue 0076 should reject the imported module's `s2` binding before
|
||||
main-file issue 0076 should reject the imported module's `i2` binding before
|
||||
lowering.
|
||||
|
||||
## Reproduction
|
||||
@@ -49,18 +49,18 @@ Create these two files under the repo root, then run
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct { total: s64 = 0; count: s64 = 0; }
|
||||
Box :: struct { total: i64 = 0; count: i64 = 0; }
|
||||
|
||||
update :: (self: *Box, n: s64) {
|
||||
update :: (self: *Box, n: i64) {
|
||||
self.total += n;
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
run_imported_reserved_name :: () -> s32 {
|
||||
s2 := Box.{ total = 0, count = 0 };
|
||||
update(@s2, 5);
|
||||
s2.update(7);
|
||||
print("imported s2 total={} count={}\n", s2.total, s2.count);
|
||||
run_imported_reserved_name :: () -> i32 {
|
||||
i2 := Box.{ total = 0, count = 0 };
|
||||
update(@i2, 5);
|
||||
i2.update(7);
|
||||
print("imported i2 total={} count={}\n", i2.total, i2.count);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -71,7 +71,7 @@ run_imported_reserved_name :: () -> s32 {
|
||||
#import "modules/std.sx";
|
||||
mod :: #import ".sx-tmp/issue0077_mod.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
return mod.run_imported_reserved_name();
|
||||
}
|
||||
```
|
||||
@@ -99,5 +99,5 @@ reserved-name bindings in `library/` (for example `u1 := ...` in
|
||||
newly illegal under the corrected rule.
|
||||
|
||||
Verification: run the two-file repro above and expect a clean diagnostic at the
|
||||
imported module's `s2 := ...` declaration, not LLVM verification failure. Then
|
||||
imported module's `i2 := ...` declaration, not LLVM verification failure. Then
|
||||
run `zig build`, `zig build test`, and `bash tests/run_examples.sh`.
|
||||
|
||||
@@ -46,11 +46,11 @@ global[0] via fn=10 (want 111)
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
g : [3]s64 = .[10, 20, 30];
|
||||
write_global :: (i: s64, v: s64) { g[i] = v; }
|
||||
g : [3]i64 = .[10, 20, 30];
|
||||
write_global :: (i: i64, v: i64) { g[i] = v; }
|
||||
|
||||
main :: () {
|
||||
loc : [3]s64 = .[10, 20, 30];
|
||||
loc : [3]i64 = .[10, 20, 30];
|
||||
loc[1] = 222;
|
||||
print("local[1]={}\n", loc[1]); // 222 (correct)
|
||||
g[1] = 222;
|
||||
|
||||
@@ -43,13 +43,13 @@ constant shape is unsupported.
|
||||
#import "modules/std.sx";
|
||||
|
||||
Pair :: struct {
|
||||
a: s64;
|
||||
b: s64;
|
||||
a: i64;
|
||||
b: i64;
|
||||
}
|
||||
|
||||
pairs : [2]Pair = .[ .{ a = 1, b = 2 }, .{ a = 3, b = 4 } ];
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
print("pairs[0]={},{}\n", pairs[0].a, pairs[0].b);
|
||||
print("pairs[1]={},{}\n", pairs[1].a, pairs[1].b);
|
||||
if pairs[0].a == 1 and pairs[0].b == 2 and pairs[1].a == 3 and pairs[1].b == 4 {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
> `constStructLiteral` / `constArrayLiteral` and made the whole aggregate look
|
||||
> non-constant, so `globalInitValue` rejected it with "must be initialized by a
|
||||
> compile-time constant". A `null` is a compile-time constant (the zero
|
||||
> pointer) and a top-level scalar pointer global (`p : *s64 = null;`) already
|
||||
> pointer) and a top-level scalar pointer global (`p : *i64 = null;`) already
|
||||
> serialized fine — only the nested-aggregate path was wrong.
|
||||
> **Fix:** add `.null_literal => .null_val` to `constExprValue` so a null leaf
|
||||
> serializes to a constant zero pointer. Made the LLVM constant emitters
|
||||
@@ -39,8 +39,8 @@ as a constant zero pointer the same way a top-level pointer global does.
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct {
|
||||
p: *s64;
|
||||
marker: s64;
|
||||
p: *i64;
|
||||
marker: i64;
|
||||
}
|
||||
|
||||
boxes : [2]Box = .[
|
||||
@@ -48,7 +48,7 @@ boxes : [2]Box = .[
|
||||
.{ p = null, marker = 22 },
|
||||
];
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
print("ptrs={} {} markers={} {}\n",
|
||||
boxes[0].p == null,
|
||||
boxes[1].p == null,
|
||||
|
||||
@@ -48,7 +48,7 @@ Color :: enum u8 { red; green; blue; }
|
||||
|
||||
chosen : Color = .green;
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
print("chosen={}\n", chosen);
|
||||
if chosen == .green {
|
||||
print("PASS\n");
|
||||
|
||||
@@ -15,21 +15,21 @@
|
||||
> resolution path (direct local decls, struct fields, function params/returns),
|
||||
> but the *stateless* registration-time resolver (`type_bridge`, used for type
|
||||
> aliases `Arr :: [N]T` and inline union/enum field types) still resolved the
|
||||
> named dim with a silent `else 0` — so `Arr :: [N]s64; a : Arr` and
|
||||
> `union { a: [N]s64 }` were still miscompiled. Fix: the module-global const
|
||||
> named dim with a silent `else 0` — so `Arr :: [N]i64; a : Arr` and
|
||||
> `union { a: [N]i64 }` were still miscompiled. Fix: the module-global const
|
||||
> table (`ProgramIndex.module_const_map`) is now threaded into `type_bridge`
|
||||
> alongside the alias map, so `StatelessInner.resolveArrayLen` resolves a named
|
||||
> module-const dim to the same length everywhere. The remaining unresolvable case
|
||||
> (a computed/comptime dimension on the binding-free path) bails LOUDLY instead of
|
||||
> fabricating a 0 length. Files: `src/ir/type_resolver.zig`, `src/ir/lower.zig`,
|
||||
> `src/ir/type_bridge.zig`. Regression: `examples/0140-types-named-const-array-dim.sx`
|
||||
> (direct + type-alias + nested `[N][M]T` + union-field dims, s64 / string /
|
||||
> (direct + type-alias + nested `[N][M]T` + union-field dims, i64 / string /
|
||||
> struct element types).
|
||||
>
|
||||
> **Root-cause close-out (attempt 3).** Attempt 2 threaded the const map into
|
||||
> `type_bridge` but the map wasn't fully populated when an alias resolved its
|
||||
> dimension: type aliases (`Arr :: [N]T`) resolve EAGERLY in scanDecls pass 1,
|
||||
> while TYPED consts (`N : s64 : 16`) register only in pass 2 and a
|
||||
> while TYPED consts (`N : i64 : 16`) register only in pass 2 and a
|
||||
> forward-declared untyped const (`Arr :: [N]T; N :: 16`) hadn't registered yet
|
||||
> either — so the stateless resolver saw an empty table, printed a non-fatal
|
||||
> warning, fabricated length 0, and CONTINUED to garbage / a segfault. Three
|
||||
@@ -46,7 +46,7 @@
|
||||
> `src/ir/program_index.zig`, `src/ir/lower.zig`, `src/ir/type_bridge.zig`,
|
||||
> `src/ir/type_resolver.zig`. Regressions:
|
||||
> `examples/0143-types-typed-const-array-dim.sx` (typed-const dim direct + via
|
||||
> alias for s64/string/struct, forward-ref alias, nested) and
|
||||
> alias for i64/string/struct, forward-ref alias, nested) and
|
||||
> `examples/1129-diagnostics-array-dim-not-const.sx` (an unresolvable computed dim
|
||||
> halts with a clean diagnostic + non-zero exit, not a fabricated 0-length array).
|
||||
>
|
||||
@@ -70,7 +70,7 @@
|
||||
> `src/ir/lower.zig`, `src/ir/type_bridge.zig`. Regression:
|
||||
> `examples/0144-types-const-expr-array-dim.sx` (every expression form, direct vs
|
||||
> alias, scalar / string / struct element types); `1129` re-pointed at a genuinely
|
||||
> non-const dimension (`[get()]s64`, a runtime call) so it still proves the
|
||||
> non-const dimension (`[get()]i64`, a runtime call) so it still proves the
|
||||
> stateless clean-halt.
|
||||
>
|
||||
> **Unified comptime-int evaluator (attempt 5).** Attempts 1–4 fixed the array
|
||||
@@ -106,7 +106,7 @@
|
||||
>
|
||||
> **Value-param type functions + oversized guard (attempt 6).** Two remaining
|
||||
> siblings in the comptime-int path. (1) A type-RETURNING function with a value
|
||||
> param used as a TYPE annotation (`b : Make(N, s64)` where `Make :: ($K: u32,
|
||||
> param used as a TYPE annotation (`b : Make(N, i64)` where `Make :: ($K: u32,
|
||||
> $T: Type) -> Type { return [K]T; }`) was rejected "unknown type 'N'" because
|
||||
> the unknown-type checker walked the value-param position as a type name, AND the
|
||||
> parameterized-type-annotation path never routed to `instantiateTypeFunction`
|
||||
@@ -116,8 +116,8 @@
|
||||
> binder's value/type classification); `resolveParameterizedWithBindings` routes
|
||||
> a type-returning-function name to `instantiateTypeFunction`; and that binder
|
||||
> resolves a general return-type expression (`return [K]T`) with bindings active.
|
||||
> `Make(N, s64)`, `Make(M + 1, s64)`, and `Make(3, s64)` now resolve to one
|
||||
> `[3]s64`. (2) Oversized dim/lane folds (`[5_000_000_000]`) panicked the
|
||||
> `Make(N, i64)`, `Make(M + 1, i64)`, and `Make(3, i64)` now resolve to one
|
||||
> `[3]i64`. (2) Oversized dim/lane folds (`[5_000_000_000]`) panicked the
|
||||
> compiler — fixed under issue 0087 via the shared range-checked
|
||||
> `program_index.foldDimU32` gate. Files: `src/ir/semantic_diagnostics.zig`,
|
||||
> `src/ir/lower.zig`, `src/ir/program_index.zig`, `src/ir/type_bridge.zig`.
|
||||
@@ -125,10 +125,10 @@
|
||||
>
|
||||
> **Diagnostic-accuracy parity (attempt 7).** The fold + layout were correct, but
|
||||
> the two paths still DIVERGED on the error MESSAGE for an oversized dim. The
|
||||
> direct form (`a : [5_000_000_000]s64`) reported the accurate "array dimension
|
||||
> direct form (`a : [5_000_000_000]i64`) reported the accurate "array dimension
|
||||
> 5000000000 does not fit in u32" (from the stateful `resolveArrayLen`, which
|
||||
> branches on `foldDimU32`'s `.too_large` / `.below_min` / `.not_const` variants),
|
||||
> but the type-ALIAS form (`Big :: [5_000_000_000]s64`) reported a FALSE "an array
|
||||
> but the type-ALIAS form (`Big :: [5_000_000_000]i64`) reported a FALSE "an array
|
||||
> dimension is not a compile-time integer constant" — because the stateless
|
||||
> `resolveArrayLen` collapsed every non-`.ok` `DimU32` to `null`, so the
|
||||
> alias-registration site had only one generic message to emit. Fix: a single
|
||||
@@ -147,7 +147,7 @@
|
||||
> **Integral-float counts + value-param range gate (attempt 8, Agra ruling).**
|
||||
> Two finishing items on the shared count path. (1) An *integral* compile-time
|
||||
> FLOAT used as a count (array dim, Vector lane, value-param, `inline for` bound)
|
||||
> was wrongly rejected — `N : f64 : 4.0`, `N :: 4.0`, and `[4.0]s64` all said
|
||||
> was wrongly rejected — `N : f64 : 4.0`, `N :: 4.0`, and `[4.0]i64` all said
|
||||
> "must be a compile-time integer constant". The shared evaluator now folds an
|
||||
> integral float to its integer at the single leaf
|
||||
> (`program_index.floatToIntExact`, used by both the `.float_literal` arm of
|
||||
@@ -175,7 +175,7 @@
|
||||
> `Box(5_000_000_000)` with `$K: Count` compiled and bound a truncated value. The
|
||||
> gate (`Lowering.resolveValueParamArg`, shared by BOTH binders — struct +
|
||||
> type-fn) now resolves the constraint to its underlying builtin
|
||||
> (`canonicalIntConstraintName`: `Count` → u32, `Small` → s8) before
|
||||
> (`canonicalIntConstraintName`: `Count` → u32, `Small` → i8) before
|
||||
> range-checking, so an aliased integer constraint behaves exactly like the
|
||||
> builtin it names. (2) A named const with an EXPRESSION RHS (`M :: 2; N :: M + 1`)
|
||||
> did not fold as a count — `program_index.moduleConstInt` read only a LITERAL RHS
|
||||
@@ -192,14 +192,14 @@
|
||||
> `src/ir/lower.zig`. Regressions: `examples/0146-types-comptime-count-matrix.sx`
|
||||
> (the full positive matrix — every consumer × representative leaf form),
|
||||
> `examples/1135-diagnostics-value-param-alias-constraint-overflow.sx` (aliased
|
||||
> u32 + s8 overflow), `examples/1136-diagnostics-array-dim-nonconst-direct-no-crash.sx`
|
||||
> u32 + i8 overflow), `examples/1136-diagnostics-array-dim-nonconst-direct-no-crash.sx`
|
||||
> (direct non-const dim halts cleanly, no fabrication / panic); the cascade
|
||||
> cleanup also tightened `examples/1502`/`1503` to one diagnostic each.
|
||||
>
|
||||
> **Final convergence — type-fn binder parity (attempt 10).** One last cell of
|
||||
> the count surface still diverged from the struct binder. A FAILED value-param
|
||||
> bind on a type-RETURNING FUNCTION (`MakeC :: ($K: Count, $T: Type) -> Type
|
||||
> { return [K]T; }`; `a : MakeC(5_000_000_000, s64)`) emitted its correct range
|
||||
> { return [K]T; }`; `a : MakeC(5_000_000_000, i64)`) emitted its correct range
|
||||
> diagnostic, but `instantiateTypeFunction` then returned `null`, so
|
||||
> `resolveParameterizedWithBindings` fell through to the empty-struct *placeholder*
|
||||
> named `MakeC`. The binding `a` got that placeholder type, so a downstream
|
||||
@@ -216,7 +216,7 @@
|
||||
## Symptom
|
||||
A fixed array whose dimension is a module-global integer constant (`N :: 16;
|
||||
a : [N]T`) miscompiles element access: reads/writes compute a wrong address.
|
||||
With `s64` elements `a[0]` returns GARBAGE (silent); with slice/pointer element
|
||||
With `i64` elements `a[0]` returns GARBAGE (silent); with slice/pointer element
|
||||
types (`[N]string`) it Bus-errors. The identical program with a LITERAL dimension
|
||||
(`a : [16]T`) is correct. Silent-miscompile class (cf. 0079–0082).
|
||||
|
||||
@@ -224,7 +224,7 @@ types (`[N]string`) it Bus-errors. The identical program with a LITERAL dimensio
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
N :: 16;
|
||||
main :: () { a : [N]s64 = ---; a[0] = 7; print("a0={}\n", a[0]); }
|
||||
main :: () { a : [N]i64 = ---; a[0] = 7; print("a0={}\n", a[0]); }
|
||||
```
|
||||
`./zig-out/bin/sx run` prints `a0=8472789232` (garbage); want `a0=7`. Replacing
|
||||
`[N]` with `[16]` prints `7`.
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
> path (`coerceCallArgs` → `coerceToType` → `CoercionResolver.classify`) had no
|
||||
> array→slice arm, so `classify([N]T, []T)` returned `.none` and the raw array
|
||||
> value was passed where a slice was expected — the callee read its {ptr,len}
|
||||
> header off the wrong bytes (returned 0 / garbage, segfaulted for `[]s64`). Fix:
|
||||
> header off the wrong bytes (returned 0 / garbage, segfaulted for `[]i64`). Fix:
|
||||
> `classify` now returns a new `.array_to_slice` plan for `[N]T → []T` (same
|
||||
> element type), and `coerceToType` emits the existing `array_to_slice` op, which
|
||||
> materializes the array into addressable storage and builds the slice header —
|
||||
> identical to the local-bound path. Files: `src/ir/conversions.zig`,
|
||||
> `src/ir/lower.zig`. Regression: `examples/0141-types-slice-literal-direct-call-arg.sx`
|
||||
> (string + numeric `[]s64`, direct vs local-bound).
|
||||
> (string + numeric `[]i64`, direct vs local-bound).
|
||||
|
||||
## Symptom
|
||||
A `.[...]` array/slice literal passed DIRECTLY as a call argument yields a slice
|
||||
@@ -23,7 +23,7 @@ passing the local is correct.
|
||||
## Reproduction
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
show :: (xs: []string) -> s64 { n:=0; i:=0; while i<xs.len { if xs[i]=="nope" {n+=1;} i+=1; } return n; }
|
||||
show :: (xs: []string) -> i64 { n:=0; i:=0; while i<xs.len { if xs[i]=="nope" {n+=1;} i+=1; } return n; }
|
||||
main :: () {
|
||||
print("direct={}\n", show(.["a","nope","b","nope"])); // prints 0 (WRONG)
|
||||
local : []string = .["a","nope","b","nope"]; print("local={}\n", show(local)); // prints 2 (correct)
|
||||
@@ -40,4 +40,4 @@ storage backs the slice). Look at how a literal aggregate argument is lowered at
|
||||
call site (materialize the literal into addressable storage whose lifetime spans
|
||||
the call, then pass a slice/pointer to it) vs the local-bound path. Fix so a
|
||||
directly-passed literal arg behaves identically to a local-bound one. Verify with
|
||||
the repro (both `2`) + a numeric `[]s64` case, gate green.
|
||||
the repro (both `2`) + a numeric `[]i64` case, gate green.
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
> literal coercion the var-decl / call-arg paths already run. The coercion
|
||||
> recurses with the nesting, so `[][]T` and deeper materialize at every level.
|
||||
> Files: `src/ir/lower.zig` (`lowerArrayLiteral`). Regression:
|
||||
> `examples/0142-types-nested-slice-literal-elements.sx` (`[][]s64` + `[][]string`,
|
||||
> `examples/0142-types-nested-slice-literal-elements.sx` (`[][]i64` + `[][]string`,
|
||||
> local-bound AND direct-call-argument forms).
|
||||
|
||||
## Symptom
|
||||
Nested array/slice literals such as `.[.[1, 2], .[3, 4]]` miscompile when the
|
||||
expected element type is a slice (`[][]s64`). Observed: both the local-bound and
|
||||
expected element type is a slice (`[][]i64`). Observed: both the local-bound and
|
||||
direct-call forms segfault while indexing the inner slice. Expected: both forms
|
||||
materialize each inner `[N]T` literal as a `[]T` slice and print the same value.
|
||||
|
||||
@@ -25,12 +25,12 @@ materialize each inner `[N]T` literal as a `[]T` slice and print the same value.
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
sum_nested :: (xss: [][]s64) -> s64 {
|
||||
sum_nested :: (xss: [][]i64) -> i64 {
|
||||
return xss[0][1] + xss[1][0];
|
||||
}
|
||||
|
||||
main :: () {
|
||||
local : [][]s64 = .[.[1, 2], .[3, 4]];
|
||||
local : [][]i64 = .[.[1, 2], .[3, 4]];
|
||||
print("local={}\n", sum_nested(local));
|
||||
print("direct={}\n", sum_nested(.[.[1, 2], .[3, 4]]));
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ not fit in u32" / "Vector lane count must fit in u32", and no compiler panic.
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () {
|
||||
a : [5000000000]s64 = ---;
|
||||
a : [5000000000]i64 = ---;
|
||||
print("{}\n", a.len);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
> **RESOLVED (F0.7)** — A typed module-level constant whose initializer does not
|
||||
> match its annotation is now rejected at the declaration with a clear
|
||||
> `type mismatch` diagnostic, killing both symptoms (the `print(N)` segfault and
|
||||
> the `[N]s64` → 4 fold).
|
||||
> the `[N]i64` → 4 fold).
|
||||
>
|
||||
> **Root cause.** `registerTypedModuleConst` (`src/ir/lower.zig`) stored the
|
||||
> annotation type on the const but never checked the initializer literal against
|
||||
@@ -9,7 +9,7 @@
|
||||
> `emitModuleConst` then stamped the `int_literal` with the `string` type (a
|
||||
> bogus pointer → segfault at the use site), and `program_index.moduleConstInt`
|
||||
> folded the const into an integer COUNT by inspecting the `int_literal` node
|
||||
> alone, ignoring `ModuleConstInfo.ty` (so `[N]s64` folded to 4).
|
||||
> alone, ignoring `ModuleConstInfo.ty` (so `[N]i64` folded to 4).
|
||||
>
|
||||
> Both LITERAL initializers (`N : string : 4`) and const-EXPRESSION initializers
|
||||
> (`M :: 2; N : string : M + 2`, `V : string : -M`) are rejected — the validation
|
||||
@@ -17,18 +17,18 @@
|
||||
>
|
||||
> **Mixed-numeric escape closed at the type-system root (attempt 3).** The
|
||||
> type-based validation reuses `inferExprType`, which inferred a non-comparison
|
||||
> binary op from its LHS alone — so `BAD : s64 : M + 0.5` (s64 + f64) inferred
|
||||
> `s64` and was accepted+truncated, while `0.5 + M` inferred `f64` and was
|
||||
> binary op from its LHS alone — so `BAD : i64 : M + 0.5` (i64 + f64) inferred
|
||||
> `i64` and was accepted+truncated, while `0.5 + M` inferred `f64` and was
|
||||
> rejected: operand-order-dependent. The fix is in the binary-op arm of
|
||||
> `ExprTyper.inferType` (`src/ir/expr_typer.zig`): arithmetic / bitwise / shift
|
||||
> ops now infer the PROMOTED result of `(lhs, rhs)` via `Lowering.arithResultType`
|
||||
> — the same int×float → float rule `lowerBinaryOp` already applied for the
|
||||
> value (extracted from its inline block into a shared helper, so the two can't
|
||||
> diverge). `M + 0.5` now infers `f64` in either operand order, so the typed-const
|
||||
> validation rejects it against an `s64` annotation with no special-casing in the
|
||||
> validation rejects it against an `i64` annotation with no special-casing in the
|
||||
> validation logic itself. This was a pre-existing inference bug broader than
|
||||
> typed consts (it also mis-formatted `print("{}", M + 0.5)` as a truncated int);
|
||||
> the typed-LOCAL `y : s64 = 1.5` → 1 narrowing is a SEPARATE assignment-coercion
|
||||
> the typed-LOCAL `y : i64 = 1.5` → 1 narrowing is a SEPARATE assignment-coercion
|
||||
> bug tracked as issue 0095.
|
||||
>
|
||||
> **Fix per file.**
|
||||
@@ -56,13 +56,13 @@
|
||||
>
|
||||
> **Regression tests.**
|
||||
> - `examples/1143-diagnostics-typed-module-const-mismatch.sx` — negative: eight
|
||||
> mismatch shapes — four literal (`int→string`, `string→s64`, `bool→s64`,
|
||||
> `float→s64`), two const-expression (`M + 2 → string`, `-M → string`), and two
|
||||
> mixed-numeric (`s64 : M + 0.5` and `s64 : 0.5 + M`, rejected in BOTH operand
|
||||
> mismatch shapes — four literal (`int→string`, `string→i64`, `bool→i64`,
|
||||
> `float→i64`), two const-expression (`M + 2 → string`, `-M → string`), and two
|
||||
> mixed-numeric (`i64 : M + 0.5` and `i64 : 0.5 + M`, rejected in BOTH operand
|
||||
> orders) — each emit a `type mismatch` diagnostic, exit 1.
|
||||
> - `examples/0162-types-typed-module-const-roundtrip.sx` — positive: valid typed
|
||||
> consts (`s64` as count + printed, `f32` from int, `f32` float, `string`,
|
||||
> `*void` null, const-expression `s64 : M + 2` used as a count + printed,
|
||||
> consts (`i64` as count + printed, `f32` from int, `f32` float, `string`,
|
||||
> `*void` null, const-expression `i64 : M + 2` used as a count + printed,
|
||||
> `f32 : M + 2`, plus mixed-numeric `f64 : M + 0.5` and `f64 : 0.5 + M` folding
|
||||
> to 2.5 in both orders) compile, fold, and print correctly.
|
||||
> - `examples/0163-types-mixed-numeric-promotion.sx` — positive: pins the
|
||||
@@ -105,7 +105,7 @@ Related count-surface manifestation:
|
||||
N : string : 4;
|
||||
|
||||
main :: () {
|
||||
a : [N]s64 = ---;
|
||||
a : [N]i64 = ---;
|
||||
print("{}\n", a.len);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
> never the reserved keyword/type." The backtick is never part of the name's text.
|
||||
>
|
||||
> 1. **Backtick raw identifier.** The lexer recognises a leading backtick
|
||||
> (`` `s2 ``) and emits an `.identifier` token whose span excludes the backtick,
|
||||
> (`` `i2 ``) and emits an `.identifier` token whose span excludes the backtick,
|
||||
> carrying a `Token.is_raw` flag ([src/lexer.zig], [src/token.zig]). The flag
|
||||
> threads through `ast.Identifier`, `ast.TypeExpr`, and EVERY binding / capture /
|
||||
> declaration node ([src/ast.zig]): `VarDecl` / `ConstDecl` / `Param` / `FnDecl`
|
||||
@@ -19,19 +19,19 @@
|
||||
> `NamespaceDecl` / `ImportDecl` / `CImportDecl` / `LibraryDecl`.
|
||||
>
|
||||
> - **Value position.** The parser skips `Type.fromName` for a raw identifier
|
||||
> in expression position ([src/parser.zig] `parsePrimary`), so `` `s2 `` is a
|
||||
> in expression position ([src/parser.zig] `parsePrimary`), so `` `i2 `` is a
|
||||
> value identifier; a later bare reference resolves to the binding.
|
||||
> - **Type position.** `parseTypeExpr` sets the raw flag on the type ATOM and
|
||||
> lets it flow through the SAME continuations as a bare name (attempt 5), so a
|
||||
> raw reference parameterizes a reserved-spelled template (`` `s2(s64) ``) and
|
||||
> raw reference parameterizes a reserved-spelled template (`` `i2(i64) ``) and
|
||||
> composes under the pointer / optional / slice wrappers; `ParameterizedTypeExpr`
|
||||
> carries `is_raw` and `resolveParameterizedWithBindings` skips the `Vector`
|
||||
> intrinsic when raw. Resolution skips the builtin classifier
|
||||
> (`TypeResolver.resolveNamed`'s `skip_builtin`, threaded from `te.is_raw` in
|
||||
> [src/ir/lower.zig] and [src/ir/type_bridge.zig]) and looks up a
|
||||
> `` `s2 ``-declared type (struct / enum / union / alias), else a NORMAL
|
||||
> "unknown type 's2'" error (`UnknownTypeChecker.reportIfUnknownType` skips the
|
||||
> builtin-name exemption when raw). A bare `s2` in type position is still the
|
||||
> `` `i2 ``-declared type (struct / enum / union / alias), else a NORMAL
|
||||
> "unknown type 'i2'" error (`UnknownTypeChecker.reportIfUnknownType` skips the
|
||||
> builtin-name exemption when raw). A bare `i2` in type position is still the
|
||||
> builtin int. The SECOND (editor/LSP) classifier in [src/sema.zig]
|
||||
> (`Type.fromTypeExpr` / `resolveTypeNode` / `resolveTypeNameStr`) honors
|
||||
> `is_raw` too, so a backtick reserved-name annotation resolves to the user type
|
||||
@@ -40,9 +40,9 @@
|
||||
> `PointerTypeInfo` / `OptionalTypeInfo` / `SliceTypeInfo` / `ManyPointerTypeInfo`
|
||||
> / `ArrayTypeInfo` each store a REQUIRED `is_raw` ([src/types.zig], no default,
|
||||
> so a future construction site cannot drop it) that every `resolveTypeNameStr`
|
||||
> call passes as its `skip_builtin` — so `` *`s2 ``, `` ?`s2 ``, `` [N]`s2 ``,
|
||||
> `` []`s2 ``, `` [*]`s2 `` field-access / unwrap / index / deref in the editor
|
||||
> index all reach the user type instead of reclassifying the inner `s2` to the
|
||||
> call passes as its `skip_builtin` — so `` *`i2 ``, `` ?`i2 ``, `` [N]`i2 ``,
|
||||
> `` []`i2 ``, `` [*]`i2 `` field-access / unwrap / index / deref in the editor
|
||||
> index all reach the user type instead of reclassifying the inner `i2` to the
|
||||
> builtin (the divergence the DIRECT-only attempt left for compound forms).
|
||||
> - **Declaration position.** A bare reserved-name declaration of EVERY kind
|
||||
> still errors (issue 0076 preserved); the backtick form is exempt. The check
|
||||
@@ -54,7 +54,7 @@
|
||||
> symmetry is enforced structurally for the bug-prone node: `ConstDecl`'s
|
||||
> `name_span` + `is_raw` carry NO default (attempt 5), so the compiler rejects
|
||||
> any construction site — including the two struct-body const forms (untyped
|
||||
> `` `s2 :: 5 `` and typed `` `s2 : T : v ``) that previously dropped both —
|
||||
> `` `i2 :: 5 `` and typed `` `i2 : T : v ``) that previously dropped both —
|
||||
> that omits them. `FnDecl` is built at every parser site through `parseFnDecl`,
|
||||
> whose `name_is_raw` is a REQUIRED parameter (the equivalent guarantee); the
|
||||
> type decls likewise route through parse-functions taking `name_is_raw`.
|
||||
@@ -66,31 +66,31 @@
|
||||
> / `union_decl` / `enum_decl` / `protocol_decl` arms
|
||||
> ([src/ir/semantic_diagnostics.zig]) check only the *type* name (and method
|
||||
> *params*), not field / tag / variant / method-signature names. The backtick
|
||||
> is optional there (`obj.s2` and `` obj.`s2 `` resolve to the same member).
|
||||
> is optional there (`obj.i2` and `` obj.`i2 `` resolve to the same member).
|
||||
> This bare member-name exemption covers only the **identifier-classified**
|
||||
> reserved spellings — `s1`..`s64`, `u1`..`u64`, `bool`, `string`, `void`,
|
||||
> reserved spellings — `i1`..`i64`, `u1`..`u64`, `bool`, `string`, `void`,
|
||||
> `usize`, `isize`, `Any` — which all lex as ordinary identifiers. The two
|
||||
> **keyword-classified** spellings, `f32` and `f64`, are lexer keywords
|
||||
> ([src/token.zig]), and a member-name slot requires an identifier token
|
||||
> ([src/parser.zig]); a bare `f32` / `f64` is therefore rejected at parse
|
||||
> (`expected field name in struct`) even in a member position, and still needs
|
||||
> the backtick there too — `` struct { `f32: s64; } `` / `` union { `f64: … } ``
|
||||
> / `` protocol { `f32 :: () -> s64; } `` work as field / tag / method names.
|
||||
> the backtick there too — `` struct { `f32: i64; } `` / `` union { `f64: … } ``
|
||||
> / `` protocol { `f32 :: () -> i64; } `` work as field / tag / method names.
|
||||
> The exemption stops at member *definitions*: an `impl` method is a real
|
||||
> function reached through the `impl_block` → `fn_decl` arm, so a
|
||||
> reserved-spelled impl method needs the backtick (`` `s2 :: (self) ``), no
|
||||
> reserved-spelled impl method needs the backtick (`` `i2 :: (self) ``), no
|
||||
> more exempt than a free function (cf. `examples/1122`). Pinned by
|
||||
> `examples/0158-types-reserved-name-member-exempt.sx`.
|
||||
> 2. **`#import c` foreign-name exemption.** `c_import.zig` synthesizes foreign
|
||||
> `#foreign` decls with `Param.is_raw = true` (and the synthesized `FnDecl`
|
||||
> `is_raw = true`), so generated C names that collide with reserved type names
|
||||
> (`s1`, `s2`) import unedited and a reserved-name foreign fn is bare-callable.
|
||||
> (`i1`, `i2`) import unedited and a reserved-name foreign fn is bare-callable.
|
||||
>
|
||||
> **Bare-callable foreign / backtick fn.** `lowerCall` rewrites a `.type_expr`
|
||||
> callee to an identifier when a function **of RAW provenance** of that name is in
|
||||
> scope ([src/ir/lower.zig]) — scoped to the callee `FnDecl`'s `is_raw` flag, so it
|
||||
> only ever fires for a backtick / `#import c` foreign fn (the decl check guarantees
|
||||
> no bare reserved-name fn exists). `s2(4)` resolves to the function (`TypeName(val)`
|
||||
> no bare reserved-name fn exists). `i2(4)` resolves to the function (`TypeName(val)`
|
||||
> is not a cast).
|
||||
>
|
||||
> **Regression tests.** `examples/0151-types-backtick-raw-identifier.sx` (every
|
||||
@@ -98,7 +98,7 @@
|
||||
> control-flow / capture form), `examples/0153-types-backtick-const-fn-decl.sx`
|
||||
> (backtick `::` const + fn decl, bare + backtick call),
|
||||
> `examples/0154-types-backtick-raw-type-reference.sx` (raw in TYPE position —
|
||||
> struct / enum / union / alias decl + reference; bare `s2` still the int),
|
||||
> struct / enum / union / alias decl + reference; bare `i2` still the int),
|
||||
> `examples/0155-types-backtick-typed-const-union-tag.sx` (typed const + union tag),
|
||||
> `examples/0156-types-backtick-struct-const.sx` (struct-body const, untyped + typed),
|
||||
> `examples/0157-types-backtick-parameterized-raw-type.sx` (raw parameterized type +
|
||||
@@ -117,8 +117,8 @@
|
||||
> caret on the name). Backtick lexer + `resolveNamed(skip_builtin)` unit tests in
|
||||
> `src/lexer.zig` / `src/ir/type_resolver.test.zig`; the editor/LSP raw-type
|
||||
> resolution (the second classifier) is pinned in `src/sema.test.zig` — the direct
|
||||
> case plus raw provenance through every compound shape (`` *`s2 `` field access,
|
||||
> `` ?`s2 `` unwrap, `` [N]`s2 `` index, parameterized `` `s2(s64) ``), each with a
|
||||
> case plus raw provenance through every compound shape (`` *`i2 `` field access,
|
||||
> `` ?`i2 `` unwrap, `` [N]`i2 `` index, parameterized `` `i2(i64) ``), each with a
|
||||
> bare-spelling control that stays the builtin (fail-before verified).
|
||||
>
|
||||
> The original report is preserved below.
|
||||
@@ -129,12 +129,12 @@
|
||||
|
||||
Importing non-sx source whose names collide with sx reserved type names is
|
||||
rejected. `library/modules/stb_truetype.sx` is a `#import c { ... }` block over a
|
||||
vendored C header (`vendors/stb_truetype/stb_truetype.h`); C identifiers `s1`,
|
||||
`s2` (which collide with sx's signed-int type keywords `s1`..`sN`) produce:
|
||||
vendored C header (`vendors/stb_truetype/stb_truetype.h`); C identifiers `i1`,
|
||||
`i2` (which collide with sx's signed-int type keywords `i1`..`sN`) produce:
|
||||
|
||||
```
|
||||
error: 's1' is a reserved type name and cannot be used as an identifier
|
||||
error: 's2' is a reserved type name and cannot be used as an identifier
|
||||
error: 'i1' is a reserved type name and cannot be used as an identifier
|
||||
error: 'i2' is a reserved type name and cannot be used as an identifier
|
||||
```
|
||||
|
||||
The user cannot hand-edit these — they are generated from the vendored C header.
|
||||
@@ -143,13 +143,13 @@ identifier even when it wants to.
|
||||
|
||||
## Root cause
|
||||
|
||||
The parser classifies any reserved-type-name spelling (`s2`, `u8`, `f64`, …) as a
|
||||
The parser classifies any reserved-type-name spelling (`i2`, `u8`, `f64`, …) as a
|
||||
`.type_expr` via `name_class.Type.fromName`, never as an `.identifier`. The F0.1 /
|
||||
issue-0076 fix added `UnknownTypeChecker.checkBindingName`
|
||||
(`src/ir/semantic_diagnostics.zig`) to reject a value binding / param spelled as
|
||||
a reserved type name (the `.type_expr`-vs-`.identifier` mismatch otherwise breaks
|
||||
address-of / autoref lowering). F0.1 deliberately extended this check to imported
|
||||
declarations — which is what now fires on the C-imported `s1`/`s2`.
|
||||
declarations — which is what now fires on the C-imported `i1`/`i2`.
|
||||
|
||||
## Desired behaviour (Agra ruling)
|
||||
|
||||
@@ -164,12 +164,12 @@ mechanisms:
|
||||
reserved-name rule:
|
||||
|
||||
```sx
|
||||
`s2 := 2.5; // OK — identifier "s2", distinct from the s2 signed-int type
|
||||
s2 := 2.5; // ERROR — bare s2 is still the reserved type name
|
||||
`i2 := 2.5; // OK — identifier "i2", distinct from the i2 signed-int type
|
||||
i2 := 2.5; // ERROR — bare i2 is still the reserved type name
|
||||
```
|
||||
|
||||
Prefix form (single leading backtick on the identifier). The raw identifier's
|
||||
TEXT is `s2` (the backtick is not part of the name). A bare `s2` used as a TYPE
|
||||
TEXT is `i2` (the backtick is not part of the name). A bare `i2` used as a TYPE
|
||||
remains the signed-int type.
|
||||
|
||||
## Reproduction
|
||||
@@ -179,10 +179,10 @@ sx-side (minimal):
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
main :: () {
|
||||
`s2 := 2.5; // must compile: identifier s2 = 2.5
|
||||
print("{}\n", `s2); // 2.5
|
||||
`i2 := 2.5; // must compile: identifier i2 = 2.5
|
||||
print("{}\n", `i2); // 2.5
|
||||
}
|
||||
```
|
||||
|
||||
Import-side: a `#import c` block over a header declaring `int s1, s2;` (or
|
||||
Import-side: a `#import c` block over a header declaring `int i1, i2;` (or
|
||||
`stb_truetype.sx`) must NOT emit the reserved-type-name error.
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# 0090 — integer formatter can't render i64::MIN or unsigned all-ones
|
||||
|
||||
> STATUS: RESOLVED (F0.8). Both extremes now render correctly:
|
||||
> `s64.min` → `-9223372036854775808`, `u64.max` → `18446744073709551615`.
|
||||
> `i64.min` → `-9223372036854775808`, `u64.max` → `18446744073709551615`.
|
||||
>
|
||||
> **Root cause.**
|
||||
> - Symptom 1 (i64::MIN): `std.int_to_string` computed the magnitude as
|
||||
> `0 - n`, which overflows for `s64::MIN` (its magnitude is
|
||||
> unrepresentable as a positive s64) — the value stayed negative, the
|
||||
> `0 - n`, which overflows for `i64::MIN` (its magnitude is
|
||||
> unrepresentable as a positive i64) — the value stayed negative, the
|
||||
> `while v > 0` loop ran zero times, and only the `-` was emitted.
|
||||
> - Symptom 2 (unsigned all-ones): `any_to_string`'s `case int:` arm
|
||||
> formatted every integer as s64 (`int_to_string(xx val)`); there was no
|
||||
> way to tell a `u64` from an `s64`, so an all-ones u64 printed as `-1`.
|
||||
> formatted every integer as i64 (`int_to_string(xx val)`); there was no
|
||||
> way to tell a `u64` from an `i64`, so an all-ones u64 printed as `-1`.
|
||||
>
|
||||
> **Fix per file.**
|
||||
> - `library/modules/std.sx` — `int_to_string` now extracts digits straight
|
||||
> from `n` (taking `|n % 10|` per digit, `n` truncates toward zero) so it
|
||||
> never negates `s64::MIN`. Added `uint_to_string` (unsigned decimal via
|
||||
> never negates `i64::MIN`. Added `uint_to_string` (unsigned decimal via
|
||||
> long-division-by-10 over four 16-bit limbs) and `decompose_u16x4` (the
|
||||
> shared 16-bit-limb split, now reused by `int_to_hex_string` too).
|
||||
> `any_to_string`'s `case int:` routes through the new
|
||||
@@ -35,7 +35,7 @@
|
||||
> table built from `isUnsignedInt`; runtime arm GEPs in at the TypeId.
|
||||
>
|
||||
> **Regression test.** `examples/0046-basic-int-formatter-extremes.sx`
|
||||
> pins both extremes plus a width spread (s8/s16/s32 + u8/u16/u32/u64,
|
||||
> pins both extremes plus a width spread (i8/i16/i32 + u8/u16/u32/u64,
|
||||
> mins/maxes, 0, ordinary values). Unit tests: `isUnsignedInt` in
|
||||
> `src/ir/types.test.zig`.
|
||||
>
|
||||
@@ -67,13 +67,13 @@
|
||||
> accepts an `Any` argument (the formatter passes `val: Any`), but the dynamic
|
||||
> `type_name` / `type_is_unsigned` path still read the Any's payload as a
|
||||
> TypeId index unconditionally — correct only when the Any holds a *Type
|
||||
> value*. For an Any holding a *value* (`av : Any = 6`, runtime tag `s64`,
|
||||
> value*. For an Any holding a *value* (`av : Any = 6`, runtime tag `i64`,
|
||||
> payload `6`) it reported `types[6]` (`u8`): `type_name(av)` → `"u8"`,
|
||||
> `type_is_unsigned(av)` → `true`. Per Agra's ruling ("Any is a type AND a
|
||||
> value, so it's expected to work"), both builtins now branch on the Any's
|
||||
> runtime tag: tag `== .any` → the box is a Type value, use the payload as the
|
||||
> TypeId; otherwise the tag IS the held value's type. So `type_name(av)` →
|
||||
> `"s64"`, `type_is_unsigned(av)` → `false`, while `type_name(type_of(x))`
|
||||
> `"i64"`, `type_is_unsigned(av)` → `false`, while `type_name(type_of(x))`
|
||||
> still names the held type. The formatter is unchanged (it already passed
|
||||
> `type_of(val)`, a proper Type value).
|
||||
> - `src/ir/interp.zig` — shared `Value.reflectTypeId` (the tag-branching
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
## Symptom
|
||||
|
||||
`print("{}", x)` mis-renders the integer extremes the s64-based formatter can't
|
||||
`print("{}", x)` mis-renders the integer extremes the i64-based formatter can't
|
||||
represent:
|
||||
- `i64::MIN` (`-9223372036854775808`) prints a bare `-` (the minus sign with NO
|
||||
digits).
|
||||
@@ -115,8 +115,8 @@ root reason.
|
||||
|
||||
## Root cause (suspected)
|
||||
|
||||
The integer-to-string path is `s64`-based (`std.int_to_string` / the `{}` formatter
|
||||
takes `s64`): it negates the value to print the sign, but `-i64::MIN` overflows, and
|
||||
The integer-to-string path is `i64`-based (`std.int_to_string` / the `{}` formatter
|
||||
takes `i64`): it negates the value to print the sign, but `-i64::MIN` overflows, and
|
||||
it has no unsigned-aware path so an all-ones u64 is read as `-1`. Needs a width/
|
||||
signedness-aware integer formatter (format by the value's actual integer TYPE:
|
||||
unsigned types print the unsigned decimal; signed `MIN` is handled without negating).
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
> guard so inferred types match lowering (avoids the issue-0083 two-resolver
|
||||
> desync).
|
||||
>
|
||||
> Bare `f64.epsilon` / `s32.max` (no shadowing binding) still fold — the parser
|
||||
> Bare `f64.epsilon` / `i32.max` (no shadowing binding) still fold — the parser
|
||||
> classifies a bare builtin name as a `.type_expr` (parser.zig:2743), so the
|
||||
> bare receiver is never value-shadowed even in a scope where `` `f64 `` is
|
||||
> bound. Float-only-on-int and non-numeric-receiver errors are unchanged.
|
||||
>
|
||||
> Regression: `examples/0161-types-numeric-limit-value-shadow.sx` (raw
|
||||
> `` `f64 ``/`` `s32 ``/`` `u8 `` value reads coexisting with bare folds) +
|
||||
> `` `f64 ``/`` `i32 ``/`` `u8 `` value reads coexisting with bare folds) +
|
||||
> unit test in `src/ir/expr_typer.test.zig`. NL.1 (`examples/0148`) / NL.2
|
||||
> (`examples/0159`, `examples/0160`) unregressed.
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
Field access on a raw reserved-spelled value binding is interpreted as a builtin
|
||||
type numeric-limit access instead of an ordinary value field access. Observed:
|
||||
the repro prints `0.000000 2147483647` (`f64.epsilon` / `s32.max`). Expected:
|
||||
the repro prints `0.000000 2147483647` (`f64.epsilon` / `i32.max`). Expected:
|
||||
it prints `12 78` from the `Box` fields.
|
||||
|
||||
## Reproduction
|
||||
@@ -41,12 +41,12 @@ it prints `12 78` from the `Box` fields.
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct { epsilon: s64; max: s64; }
|
||||
Box :: struct { epsilon: i64; max: i64; }
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
`f64 := Box.{ epsilon = 12, max = 34 };
|
||||
`s32 := Box.{ epsilon = 56, max = 78 };
|
||||
print("{} {}\n", `f64.epsilon, `s32.max);
|
||||
`i32 := Box.{ epsilon = 56, max = 78 };
|
||||
print("{} {}\n", `f64.epsilon, `i32.max);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -68,7 +68,7 @@ intercept for actual type receivers (`.type_expr`, and bare reserved integer
|
||||
names with no value binding). If raw provenance is available in the AST, using it
|
||||
to disambiguate is also acceptable, but the observable rule must be that
|
||||
`` `f64.epsilon `` reads the value field when `` `f64 `` is a value binding,
|
||||
while bare `f64.epsilon` / `s32.max` still fold as numeric limits.
|
||||
while bare `f64.epsilon` / `i32.max` still fold as numeric limits.
|
||||
|
||||
Verification: pin a regression test from the repro above. It should print
|
||||
`12 78`. Also verify the existing numeric-limit examples still pass:
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
> - `src/ir/expr_typer.zig` — numeric-limit inference arm: the `shadowed`
|
||||
> check now calls the same helper.
|
||||
>
|
||||
> A bare `f64.epsilon` / `s32.max` (a `.type_expr` receiver, never an
|
||||
> A bare `f64.epsilon` / `i32.max` (a `.type_expr` receiver, never an
|
||||
> `.identifier`) still folds, even when a global or module-const raw value of
|
||||
> the same spelling exists — the bare receiver is never value-shadowed.
|
||||
> Float-only-on-int and non-numeric-receiver errors are unchanged.
|
||||
>
|
||||
> Regression: `examples/0161-types-numeric-limit-value-shadow.sx` now exercises
|
||||
> all three binding kinds — a GLOBAL `` `f32 ``, a MODULE-CONST `` `s16 ``, and
|
||||
> LOCAL `` `f64 ``/`` `s32 ``/`` `u8 `` — each reading its field while the bare
|
||||
> all three binding kinds — a GLOBAL `` `f32 ``, a MODULE-CONST `` `i16 ``, and
|
||||
> LOCAL `` `f64 ``/`` `i32 ``/`` `u8 `` — each reading its field while the bare
|
||||
> spelling still folds. Unit test `src/ir/expr_typer.test.zig` pins the global
|
||||
> + module-const sources. NL.1 (`examples/0148`) / NL.2 (`examples/0159`,
|
||||
> `examples/0160`) unregressed.
|
||||
@@ -40,20 +40,20 @@
|
||||
Field access on a **global** raw reserved-spelled value binding is interpreted as
|
||||
a builtin type numeric-limit access instead of an ordinary value field access.
|
||||
Observed: the repro prints `0.000000 2147483647` (`f64.epsilon` /
|
||||
`s32.max`). Expected: it prints `12 78` from the `Box` fields.
|
||||
`i32.max`). Expected: it prints `12 78` from the `Box` fields.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
Box :: struct { epsilon: s64; max: s64; }
|
||||
Box :: struct { epsilon: i64; max: i64; }
|
||||
|
||||
`f64 := Box.{ epsilon = 12, max = 34 };
|
||||
`s32 := Box.{ epsilon = 56, max = 78 };
|
||||
`i32 := Box.{ epsilon = 56, max = 78 };
|
||||
|
||||
main :: () -> s32 {
|
||||
print("{} {}\n", `f64.epsilon, `s32.max);
|
||||
main :: () -> i32 {
|
||||
print("{} {}\n", `f64.epsilon, `i32.max);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -66,7 +66,7 @@ at `Lowering.lowerNumericLimit` and the new issue-0092 guard around
|
||||
`Scope.lookup`. That guard returns `null` for a shadowing local, but global raw
|
||||
bindings are registered in `ProgramIndex.global_names` (and module constants in
|
||||
`ProgramIndex.module_const_map`), not in `Scope`, so an `.identifier` receiver
|
||||
whose text is `f64` / `s32` still folds to a numeric limit before ordinary
|
||||
whose text is `f64` / `i32` still folds to a numeric limit before ordinary
|
||||
global field lowering can read the value. Mirror the same rule in
|
||||
`src/ir/expr_typer.zig` so inferred types match lowering.
|
||||
|
||||
@@ -74,7 +74,7 @@ Likely fix: for `.identifier` numeric-limit receivers, prefer any in-scope value
|
||||
binding source over the builtin-type fold: lexical `Scope.lookup`, global values
|
||||
(`program_index.global_names`), and module value constants where applicable.
|
||||
Keep `.type_expr` receivers folding as type receivers, so bare `f64.epsilon` and
|
||||
`s32.max` still fold even when a raw global value of the same spelling exists.
|
||||
`i32.max` still fold even when a raw global value of the same spelling exists.
|
||||
|
||||
Verification: pin the repro above as a regression. It should print `12 78`.
|
||||
Also verify the existing numeric-limit examples still pass:
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
> so a pointer-to-`.unresolved` reached LLVM emission and tripped the
|
||||
> `src/backend/llvm/types.zig` tripwire. The nested lvalue-pointer path
|
||||
> (`Lowering.lowerExprAsPtr`'s `.field_access` fallback) had the sibling defect:
|
||||
> on a miss it returned `structGepTyped(obj_ptr, 0, .s64, obj_ty)` — a silent
|
||||
> field-0/`.s64` default.
|
||||
> on a miss it returned `structGepTyped(obj_ptr, 0, .i64, obj_ty)` — a silent
|
||||
> field-0/`.i64` default.
|
||||
>
|
||||
> **Fix (`src/ir/lower.zig`):** all three lvalue field-store sites — single
|
||||
> assignment, address-of, and multi-target assignment — route field resolution
|
||||
> through one shared helper, `fieldLvaluePtr(obj_ptr, obj_ty, field)`, which
|
||||
> resolves struct fields, union/tagged-union direct fields, promoted
|
||||
> anonymous-struct union members, tuple elements, and vector lanes (reusing
|
||||
> `vectorLaneIndex`), and returns `null` (no field 0 / `.unresolved` /`.s64`
|
||||
> `vectorLaneIndex`), and returns `null` (no field 0 / `.unresolved` /`.i64`
|
||||
> default) when nothing matches. Each caller emits the read path's
|
||||
> field-not-found diagnostic (`emitFieldError`) on a `null` result:
|
||||
> 1. `lowerAssignment` `.field_access` target — delegates to `fieldLvaluePtr`;
|
||||
@@ -23,7 +23,7 @@
|
||||
> deleted (issue-0083 two-resolver divergence removed).
|
||||
> 2. `lowerExprAsPtr` `.field_access` — delegates to `fieldLvaluePtr`, so the
|
||||
> address-of path resolves promoted union members (`@v.x`) — not only direct
|
||||
> union fields — and a genuine miss errors. The `.s64` sentinel is gone.
|
||||
> union fields — and a genuine miss errors. The `.i64` sentinel is gone.
|
||||
> 3. `lowerMultiAssign` `.field_access` target — replaced its struct-only loop
|
||||
> (which defaulted `field_idx 0` / `field_ty .unresolved` on a miss, silently
|
||||
> storing into field 0 — `p.q, y = 2, 3` printed `x=2 y=3`) with the shared
|
||||
@@ -65,7 +65,7 @@ Expected: a normal compile error like `field 'q' not found on type 'Point'`, mat
|
||||
|
||||
## Reproduction
|
||||
```sx
|
||||
Point :: struct { x: s64; }
|
||||
Point :: struct { x: i64; }
|
||||
|
||||
main :: () {
|
||||
p := Point.{ x = 1 };
|
||||
@@ -81,6 +81,6 @@ panic: unresolved type reached LLVM emission — a type resolution failure was n
|
||||
## Investigation prompt
|
||||
Fix issue 0094 in the sx compiler: assigning to a missing struct field (`p.q = 2`) panics with `.unresolved` reaching LLVM emission instead of emitting a field-not-found diagnostic.
|
||||
|
||||
Suspected area: `src/ir/lower.zig`, especially `Lowering.lowerAssignment`'s `.field_access` target path around the struct-field lookup (`field_ty` starts as `.unresolved`, no matched field diagnoses, then `ptrTo(field_ty)` is stored) and the related `Lowering.lowerExprAsPtr` field-access fallback that returns `structGepTyped(obj_ptr, 0, .s64, obj_ty)` on lookup failure. The fix should make failed lvalue field lookup loud, reusing `emitFieldError(obj_ty, field, span)` or equivalent, and should not use `.s64`, `.void`, or any real type as a sentinel.
|
||||
Suspected area: `src/ir/lower.zig`, especially `Lowering.lowerAssignment`'s `.field_access` target path around the struct-field lookup (`field_ty` starts as `.unresolved`, no matched field diagnoses, then `ptrTo(field_ty)` is stored) and the related `Lowering.lowerExprAsPtr` field-access fallback that returns `structGepTyped(obj_ptr, 0, .i64, obj_ty)` on lookup failure. The fix should make failed lvalue field lookup loud, reusing `emitFieldError(obj_ty, field, span)` or equivalent, and should not use `.i64`, `.void`, or any real type as a sentinel.
|
||||
|
||||
Verification: run the repro and expect exit 1 with a source diagnostic `field 'q' not found on type 'Point'`; no LLVM panic. Then run `zig build`, `zig build test`, and `bash tests/run_examples.sh`.
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
>
|
||||
> **Completion (F0.11 attempt 2)** — the direct-`const_float` coerce arm only
|
||||
> caught a float LITERAL; a non-integral const-folded float EXPRESSION
|
||||
> (`local/field/param : s64 = M + 0.5`) still truncated silently. Closed by:
|
||||
> (`local/field/param : i64 = M + 0.5`) still truncated silently. Closed by:
|
||||
> - New `program_index.evalConstFloatExpr` — the f64 counterpart to
|
||||
> `evalConstIntExpr`, delegating every integer subtree back to it (no parallel
|
||||
> integer logic), adding only the float literal / negate / `+ - * /` arms.
|
||||
@@ -44,7 +44,7 @@
|
||||
>
|
||||
> **Completion (F0.11 attempt 3)** — attempt 2 resolved INT-const-expr leaves
|
||||
> (`M + 0.5`, `M :: 2`), but a non-integral result via a FLOAT-const leaf
|
||||
> (`F : f64 : 2.5; y : s64 = F + 0.25` = 2.75) still truncated silently:
|
||||
> (`F : f64 : 2.5; y : i64 = F + 0.25` = 2.75) still truncated silently:
|
||||
> `evalConstFloatExpr` delegated only integer leaves to `evalConstIntExpr` and had
|
||||
> no float-const leaf arm. Closed by completing the evaluator:
|
||||
> - `program_index.moduleConstFloat` — the f64 twin of `moduleConstInt` (same
|
||||
@@ -60,11 +60,11 @@
|
||||
> `floatToIntExact` (the SAME facility `foldComptimeFloatInit` uses) instead of
|
||||
> the int-only `evalComptimeInt`, which folded leaf-by-leaf in `i64` and so
|
||||
> rejected an integral SUM built from a non-integral float leaf
|
||||
> (`K : s64 : F + 1.5` = 4.0). Integral float-const-leaf consts now FOLD;
|
||||
> (`K : i64 : F + 1.5` = 4.0). Integral float-const-leaf consts now FOLD;
|
||||
> non-integral ones still error with the unified wording.
|
||||
> - Out of scope (consistent with the int evaluator): a LOCAL `::` const leaf is
|
||||
> resolved as a scope ref, not through the const tables, so neither
|
||||
> `evalConstIntExpr` nor `evalConstFloatExpr` folds it — a local `M : s64 : 2`
|
||||
> `evalConstIntExpr` nor `evalConstFloatExpr` folds it — a local `M : i64 : 2`
|
||||
> in `M + 0.5` and a local `F : f64 : 2.5` in `F + 0.25` both still truncate
|
||||
> identically. Float now matches int exactly at that boundary.
|
||||
>
|
||||
@@ -73,7 +73,7 @@
|
||||
> DIMENSION / count path still diverged: it folded a DIRECT integral float literal
|
||||
> (`[4.0]`, `[N]` with `N : f64 : 4.0`) yet rejected an INTEGRAL expression built
|
||||
> from a non-integral float-const leaf (`[F + 1.5]` = 4.0, or `[K]` with
|
||||
> `K : s64 : F + 1.5`) as "must be a compile-time integer constant" — because the
|
||||
> `K : i64 : F + 1.5`) as "must be a compile-time integer constant" — because the
|
||||
> dim fold used the int-only `evalConstIntExpr`, never the float-aware path. Closed
|
||||
> by routing the count fold through the SAME facility the other four sites use:
|
||||
> - New `program_index.foldCountI64` — the single int-or-integral-float count fold:
|
||||
@@ -87,8 +87,8 @@
|
||||
> integer constant" — the cast-escape advice the binding sites give does not apply
|
||||
> in a dimension position, so the dim wording omits it. `reportDimError`, the
|
||||
> Vector-lane resolver, and the top-level array-alias diagnostic all handle the
|
||||
> new variant, so the DIRECT (`a : [F + 0.25]s64`) and type-ALIAS
|
||||
> (`Arr :: [F + 0.25]s64`) forms emit the identical message.
|
||||
> new variant, so the DIRECT (`a : [F + 0.25]i64`) and type-ALIAS
|
||||
> (`Arr :: [F + 0.25]i64`) forms emit the identical message.
|
||||
> - `type_bridge.StatelessInner.lookupFloatName` (routed through `moduleConstFloat`)
|
||||
> is the float twin of its `lookupDimName`, so the registration-time alias path
|
||||
> folds a float-const-leaf dimension to the SAME count as the stateful direct
|
||||
@@ -99,7 +99,7 @@
|
||||
> literal / int-const-expr / float-const-leaf forms, but `evalConstFloatExpr` still
|
||||
> LAGGED `evalConstIntExpr`: the int evaluator resolves a numeric-limit field-access
|
||||
> leaf (`f64.true_min`, `f64.max`) via `type_resolver.integerLimitFor`, but the
|
||||
> float evaluator had no parallel arm, so `y : s64 = f64.true_min + 0.5` (= 0.5)
|
||||
> float evaluator had no parallel arm, so `y : i64 = f64.true_min + 0.5` (= 0.5)
|
||||
> truncated silently to 0 (the direct `f64.true_min` already errored via the IR-level
|
||||
> `constFloatInfo` path, but the *expression* form escaped). Closed by bringing the
|
||||
> two evaluators to PARITY:
|
||||
@@ -109,7 +109,7 @@
|
||||
> `integerLimitFor` arm. Integer limits / `<pack>.len` are still resolved by the
|
||||
> int delegation, so only the float-limit case lands here.
|
||||
> - The audit also surfaced a missing `%` arm: the int evaluator folds `.mod` but
|
||||
> the float one did not, so `y : s64 = 5.5 % 2.0` (= 1.5) truncated silently to 1.
|
||||
> the float one did not, so `y : i64 = 5.5 % 2.0` (= 1.5) truncated silently to 1.
|
||||
> `evalConstFloatExpr` now handles `.mod` via `@rem` (matching `evalConstIntExpr`
|
||||
> and codegen's `frem`; `6.0 % 4.0` folds to 2 via the int delegation, `5.5 % 2.0`
|
||||
> = 1.5 is rejected). The two evaluators are now at full leaf/operator parity, so
|
||||
@@ -127,9 +127,9 @@
|
||||
> `examples/0162-types-typed-module-const-roundtrip.sx`, and the aligned const
|
||||
> diagnostic in `examples/1143-diagnostics-typed-module-const-mismatch.sx`
|
||||
> (G / BAD / BAD2 stay errors with the new wording). The array-dimension site is
|
||||
> pinned in the same two examples: 0168 adds `[F + 1.5]s64`, `[KF]s64`
|
||||
> (`KF : s64 : F + 1.5`), and a type-alias `ArrFE :: [F + 1.5]s64` all folding to
|
||||
> len 4; 1146 adds `[F + 0.25]s64` erroring; `examples/1132` now expects the
|
||||
> pinned in the same two examples: 0168 adds `[F + 1.5]i64`, `[KF]i64`
|
||||
> (`KF : i64 : F + 1.5`), and a type-alias `ArrFE :: [F + 1.5]i64` all folding to
|
||||
> len 4; 1146 adds `[F + 0.25]i64` erroring; `examples/1132` now expects the
|
||||
> precise non-integral-float dim wording. Unit:
|
||||
> `program_index.test.zig` "evalConstFloatExpr folds comptime float expressions"
|
||||
> (covers the float-const leaf: `F` → 2.5, `F + 0.25` → 2.75, `F + 1.5` → 4.0;
|
||||
@@ -138,7 +138,7 @@
|
||||
> → 1.5 / `% 0.0` → null) and "foldCountI64 / foldDimU32 fold an integral float
|
||||
> count, reject a non-integral one" (the count fold + the `non_integral_float` /
|
||||
> `below_min` distinction). Attempt 5 also extends 0168 (positive: `f64.max -
|
||||
> f64.max` → 0, `6.0 % 4.0` → 2, integer-limit `s8.max`/`[u8.max]` unregressed,
|
||||
> f64.max` → 0, `6.0 % 4.0` → 2, integer-limit `i8.max`/`[u8.max]` unregressed,
|
||||
> `xx` escapes for both new forms) and 1146 (negative: `f64.true_min + 0.5` and
|
||||
> `5.5 % 2.0` error at a binding site).
|
||||
>
|
||||
@@ -174,7 +174,7 @@
|
||||
> `evalConstFloatExpr` / `evalConstIntExpr` did NOT — so once the read flowed into
|
||||
> an integer binding, the float folder returned the BUILTIN `f64.epsilon`
|
||||
> (2.22e-16) and the rule wrongly errored ("narrow non-integral float
|
||||
> '0.0000…0002220446049250313'"), and the integer folder turned `` `s8.max `` as an
|
||||
> '0.0000…0002220446049250313'"), and the integer folder turned `` `i8.max `` as an
|
||||
> array dimension into the builtin `127` (a fabricated 127-element array) instead
|
||||
> of an ordinary runtime field read. Closed at the single root: both evaluators'
|
||||
> field-access arms now mirror `isFloatValuedExpr`'s `is_raw` guard — a raw
|
||||
@@ -182,11 +182,11 @@
|
||||
> falls through to the ordinary runtime field read. A raw value-shadow is a
|
||||
> mutable-local field (a subsequent `` `f64.epsilon = 4.0 `` is observable), so it
|
||||
> is genuinely runtime and must not be const-folded: it now behaves EXACTLY like a
|
||||
> plainly-named field read — `` `f64.epsilon `` narrowing into `s64` truncates to
|
||||
> plainly-named field read — `` `f64.epsilon `` narrowing into `i64` truncates to
|
||||
> its field value (`11.5` → `11`, identical to `b.epsilon`, NOT a non-integral
|
||||
> error on the builtin limit), and `` `s8.max `` as an array dimension is rejected
|
||||
> error on the builtin limit), and `` `i8.max `` as an array dimension is rejected
|
||||
> as a non-constant count (identical to `b.max`). The bare builtin path is
|
||||
> unchanged (`f64.epsilon`, `s8.max`, `[u8.max]` still fold). Regression:
|
||||
> unchanged (`f64.epsilon`, `i8.max`, `[u8.max]` still fold). Regression:
|
||||
> `examples/0169-types-value-shadow-field-narrowing.sx` (positive — raw float-field
|
||||
> read narrows/truncates, mutation proves runtime, bare limit still folds),
|
||||
> `examples/1148-diagnostics-value-shadow-field-dim-not-const.sx` (negative — raw
|
||||
@@ -198,18 +198,18 @@ A typed LOCAL (and likely typed param/field) silently truncates a floating-point
|
||||
initializer to an integer annotation instead of rejecting or requiring an explicit cast.
|
||||
|
||||
Observed:
|
||||
- `y : s64 = 1.5;` → y == 1 (float literal truncated, no diagnostic)
|
||||
- `y : s64 = 2 + 0.5;` → y == 2 (float-valued expr truncated, no diagnostic)
|
||||
- `y : i64 = 1.5;` → y == 1 (float literal truncated, no diagnostic)
|
||||
- `y : i64 = 2 + 0.5;` → y == 2 (float-valued expr truncated, no diagnostic)
|
||||
|
||||
Expected: a type-mismatch / narrowing diagnostic (consistent with typed MODULE CONSTS,
|
||||
which after F0.7 reject `N : s64 : 1.5` and `N : s64 : M + 0.5`). Today consts are strict
|
||||
which after F0.7 reject `N : i64 : 1.5` and `N : i64 : M + 0.5`). Today consts are strict
|
||||
but locals are lenient — an inconsistency.
|
||||
|
||||
## Reproduction
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
main :: () {
|
||||
y : s64 = 1.5;
|
||||
y : i64 = 1.5;
|
||||
print("{}\n", y); // prints 1
|
||||
}
|
||||
```
|
||||
@@ -221,7 +221,7 @@ registerTypedModuleConst + typedConstInitFits/constExprInitFits). Make typed-loc
|
||||
assignment-coercion consistent: either reject a non-integral float→int initializer with a
|
||||
diagnostic (matching the const path) or require an explicit `xx`/cast. Suspected area: the
|
||||
assignment / typed-binding coercion path (coerceToType ladder, specs.md §"coercion") in
|
||||
src/ir/lower.zig. Verify `y : s64 = 1.5` errors (or requires a cast); confirm integral-float
|
||||
src/ir/lower.zig. Verify `y : i64 = 1.5` errors (or requires a cast); confirm integral-float
|
||||
folding rules (specs.md: `4.0`→4 ok, `4.5` rejected) stay consistent. Then gate.
|
||||
|
||||
## Disposition
|
||||
|
||||
@@ -18,7 +18,7 @@ value resolves against `failableSuccessType(ret_ty)` (the value type / value-tup
|
||||
literal gets its real ordinal and the success-return path appends the `0` error slot; an
|
||||
**explicit full failable tuple** literal (`return (v..., e)`, arity == full-tuple field count)
|
||||
keeps the full-tuple target so its trailing error element resolves against the error set and is
|
||||
forwarded as-is. The s32 case was already correct because integer literals don't resolve variants
|
||||
forwarded as-is. The i32 case was already correct because integer literals don't resolve variants
|
||||
against `target_type`.
|
||||
|
||||
Two follow-up defects from the first cut of this fix were corrected (attempt-2 review):
|
||||
@@ -50,7 +50,7 @@ Below preserved as a record of the original problem.
|
||||
|
||||
A value-failable function `-> (EnumType, !ErrSet)` writes a **garbage nonzero tag into the error
|
||||
slot on the SUCCESS path**. Per specs.md the error channel must be `0` on success ("0 in the
|
||||
error slot means no error"). Every **runtime read** of the slot on success (`cast(s64) err`, bare
|
||||
error slot means no error"). Every **runtime read** of the slot on success (`cast(i64) err`, bare
|
||||
`if err`, `err == error.X`, and therefore `error_tag_name(err)`) reports a false error. Only the
|
||||
path-sensitive compile-time proof `if !err` reads correctly (it is tied to the SSA value, not a
|
||||
runtime load of the slot), which is why it masks the bug.
|
||||
@@ -71,12 +71,12 @@ pick :: (s: string) -> (Color, !E) {
|
||||
raise error.Nope;
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
c, e := pick("red"); // SUCCESS -> error slot MUST be 0
|
||||
print("error e (int) = {}\n", cast(s64) e); // EXPECT 0 ; BUG prints 1
|
||||
print("error e (int) = {}\n", cast(i64) e); // EXPECT 0 ; BUG prints 1
|
||||
if e { print("bare-if e: ERROR (WRONG)\n"); } else { print("bare-if e: ok\n"); }
|
||||
if e == error.Nope { print("e == Nope (WRONG)\n"); } else { print("e != Nope (ok)\n"); }
|
||||
if !e { print("guard !e: value c (int) = {}\n", cast(s64) c); } // c = 0 = .red (CORRECT)
|
||||
if !e { print("guard !e: value c (int) = {}\n", cast(i64) c); } // c = 0 = .red (CORRECT)
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
@@ -96,14 +96,14 @@ e != Nope (ok)
|
||||
guard !e: value c (int) = 0
|
||||
```
|
||||
|
||||
## Contrast — the IDENTICAL shape with an s32 value is CORRECT
|
||||
## Contrast — the IDENTICAL shape with an i32 value is CORRECT
|
||||
|
||||
```sx
|
||||
pick :: (n: s32) -> (s32, !E) { if n > 0 { return n; } raise error.Nope; }
|
||||
pick :: (n: i32) -> (i32, !E) { if n > 0 { return n; } raise error.Nope; }
|
||||
// v, e := pick(5); → error slot = 0 (correct); bare-if e: ok
|
||||
```
|
||||
The split is **enum-value-specific** because only an enum literal (`return .variant`) resolves its
|
||||
tag against `target_type`. An integer literal does not, so the s32 path never got mis-stamped with
|
||||
tag against `target_type`. An integer literal does not, so the i32 path never got mis-stamped with
|
||||
the failable-tuple type and never took the false forwarding branch.
|
||||
|
||||
## Root cause (confirmed at ground truth)
|
||||
@@ -115,6 +115,6 @@ failable tuple). The LLVM IR on the success path was:
|
||||
ret { i64, i32 } { i64 0, i32 undef } ; error slot UNDEF, not 0 (.blue gave i64 0 too — value lost)
|
||||
```
|
||||
|
||||
vs. the s32 case which already produced `ret { i32, i32 } { i32 7, i32 0 }`. After narrowing the
|
||||
vs. the i32 case which already produced `ret { i32, i32 } { i32 7, i32 0 }`. After narrowing the
|
||||
return target to the value type, the enum success path produces `ret { i64, i32 } zeroinitializer`
|
||||
(value 0 = `.red`, error slot 0), and `.blue` correctly carries ordinal 2.
|
||||
|
||||
@@ -50,7 +50,7 @@ from **its own module's flat import** was rejected:
|
||||
|
||||
```
|
||||
m :: #import "m.sx"; // m.sx: `#import "helper.sx"; foo :: () { helper() }`
|
||||
main :: () -> s32 { print("{}\n", m.foo()); 0 } // → 'helper' is not visible
|
||||
main :: () -> i32 { print("{}\n", m.foo()); 0 } // → 'helper' is not visible
|
||||
```
|
||||
|
||||
**Fix** (`src/ir/program_index.zig`, `src/ir/lower.zig`):
|
||||
@@ -81,8 +81,8 @@ lowering and the caller's own trailing statements / `return` were treated as
|
||||
dead-after-terminator:
|
||||
|
||||
```
|
||||
m :: #import "m.sx"; // m.sx: `#import "helper.sx"; foo :: () -> s64 { if true { return helper(); } return 0; }`
|
||||
main :: () -> s32 {
|
||||
m :: #import "m.sx"; // m.sx: `#import "helper.sx"; foo :: () -> i64 { if true { return helper(); } return 0; }`
|
||||
main :: () -> i32 {
|
||||
x := m.foo();
|
||||
print("after\n"); // dropped
|
||||
return 0; // → error: body produces no value
|
||||
@@ -132,7 +132,7 @@ exits 0 after. 0719 and 0720 stay green.
|
||||
cli :: #import "modules/std/cli.sx";
|
||||
json :: #import "modules/std/json.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
gpa := GPA.init();
|
||||
arena := Arena.init(xx gpa, 8192);
|
||||
defer arena.deinit();
|
||||
|
||||
@@ -10,7 +10,7 @@ bug: `opt!.method()` failed to resolve the method at all (`error: unresolved
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
S :: struct { id: string; n: s64; }
|
||||
S :: struct { id: string; n: i64; }
|
||||
mk :: () -> ?S { return S.{ id = "hello", n = 42 }; }
|
||||
main :: () {
|
||||
print("chained: {}\n", mk()!.id); // observed: garbage (e.g. 8362783136)
|
||||
@@ -28,8 +28,8 @@ typed Ref into a slot and `v.field` reads it back. But the *chained* form never
|
||||
materializes a slot: `lowerFieldAccess` re-derives the receiver type via
|
||||
`inferExprType(fa.object)` (= `inferExprType(mk()!)`), got `.unresolved`, and
|
||||
the struct-field lookup on `.unresolved` failed — `mk()!.id` was typed
|
||||
`.unresolved`/`s64` and its value emitted as `undef` (the print monomorphized
|
||||
`pack_s64` with `i64 undef`, surfacing as a stale stack address). The method
|
||||
`.unresolved`/`i64` and its value emitted as `undef` (the print monomorphized
|
||||
`pack_i64` with `i64 undef`, surfacing as a stale stack address). The method
|
||||
chain failed for the same reason: receiver typing returned `.unresolved`, so
|
||||
method resolution found nothing.
|
||||
|
||||
@@ -56,9 +56,9 @@ One arm fixes all of them.
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
Inner :: struct { tag: string; k: s64; }
|
||||
Inner :: struct { tag: string; k: i64; }
|
||||
S :: struct {
|
||||
id: string; n: s64; inner: Inner;
|
||||
id: string; n: i64; inner: Inner;
|
||||
greet :: (self: *S) -> string { return self.id; }
|
||||
}
|
||||
mk :: () -> ?S { return S.{ id = "hello", n = 42, inner = Inner.{ tag = "deep", k = 7 } }; }
|
||||
|
||||
@@ -78,13 +78,13 @@ byte-identical requirement cannot hold until this bug is fixed.
|
||||
|
||||
```sx
|
||||
// m.sx
|
||||
secret :: () -> s64 { 7 }
|
||||
secret :: () -> i64 { 7 }
|
||||
```
|
||||
|
||||
```sx
|
||||
// main.sx
|
||||
m :: #import "m.sx";
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
x := secret(); // bare; `secret` is only namespaced-imported as `m.secret`
|
||||
0
|
||||
}
|
||||
@@ -103,7 +103,7 @@ main :: () -> s32 {
|
||||
```sx
|
||||
// main.sx
|
||||
std :: #import "modules/std.sx";
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
std.print("hello\n"); // legit qualified call
|
||||
0
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ all share `lowerBreak`/`lowerContinue`.
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
for 0..3: (i) {
|
||||
defer print("cleanup {}\n", i);
|
||||
if i == 1 { break; }
|
||||
|
||||
@@ -34,7 +34,7 @@ iterations; stack usage is static per frame regardless of trip count.
|
||||
|
||||
This hits three shapes, all confirmed:
|
||||
|
||||
1. user locals declared in a loop body (`buf : [128]s64 = ---;`),
|
||||
1. user locals declared in a loop body (`buf : [128]i64 = ---;`),
|
||||
2. nested loops (inner `for`'s `idx_slot` alloca sits in the outer body),
|
||||
3. compiler temporaries spilled in the body (e.g. `index_get`'s `ig.tmp` —
|
||||
see issue 0110 for the for-over-array case specifically).
|
||||
@@ -46,10 +46,10 @@ Repro A — body local (`issues/0109-loop-body-alloca-stack-growth.sx`):
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
sum := 0;
|
||||
for 0..1000000: (i) {
|
||||
buf : [128]s64 = ---;
|
||||
buf : [128]i64 = ---;
|
||||
buf[0] = i;
|
||||
sum += buf[0];
|
||||
}
|
||||
@@ -68,7 +68,7 @@ Repro B — pure nested loops, zero user locals:
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
n := 0;
|
||||
for 0..3000000: (i) {
|
||||
for 0..1: (j) { n += 1; }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
**Root cause:** `lowerFor`'s by-value element fetch emitted `index_get` on the
|
||||
array *value*; the emitter realizes that as spill-whole-array-to-temp + GEP one
|
||||
element, per iteration — O(N²) bytes copied (and pre-0109, per-iteration stack
|
||||
growth that segfaulted a `[4096]s64` loop).
|
||||
growth that segfaulted a `[4096]i64` loop).
|
||||
|
||||
**Fix:** in `src/ir/lower/control_flow.zig` `lowerFor`, when the iterable is an
|
||||
array with addressable storage (`getExprAlloca` hit, and the iterable was not
|
||||
@@ -25,8 +25,8 @@ mutation leaves the array untouched).
|
||||
lowers the element fetch as `index_get` on the array *value*, which the LLVM
|
||||
emitter realizes as: load the whole array as an SSA value, spill it to a
|
||||
fresh `ig.tmp` alloca, GEP one element. Per iteration. Observed: a `for` over
|
||||
a `[4096]s64` array segfaults (4096 iterations × 32KB spill = ~134MB of stack
|
||||
— see issue 0109 for why body allocas never unwind); a `[256]s64` version
|
||||
a `[4096]i64` array segfaults (4096 iterations × 32KB spill = ~134MB of stack
|
||||
— see issue 0109 for why body allocas never unwind); a `[256]i64` version
|
||||
completes but copies 256 × 2KB = 512KB to read 2KB of data. Expected: O(1)
|
||||
stack, O(N) total work — GEP into the array's existing storage and load the
|
||||
single element.
|
||||
@@ -42,8 +42,8 @@ copying. By-value should reuse the same base and just add a load.
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
arr : [4096]s64 = ---;
|
||||
main :: () -> i32 {
|
||||
arr : [4096]i64 = ---;
|
||||
i := 0;
|
||||
while i < 4096 { arr[i] = i; i += 1; }
|
||||
sum := 0;
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
**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.
|
||||
lowered their initializer without clearing it — so `x := 0` in a `-> i32`/`-> i8`
|
||||
function was typed i32/i8 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
|
||||
target, so literals take their spec defaults (i64/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`).
|
||||
**Regression test:** `examples/0173-types-int-literal-default-i64.sx` (f.x/g.x/main.x,
|
||||
destructured a/b all print `i64`; `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 —
|
||||
fit an explicitly-annotated integer target (`x : i8 = 300`) with no diagnostic —
|
||||
filed separately as issue 0112.
|
||||
|
||||
---
|
||||
@@ -24,42 +24,42 @@ filed separately as issue 0112.
|
||||
|
||||
**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
|
||||
spec'd default. `f :: () -> i32 { x := 0; ... }` gives `x: i32`;
|
||||
`g :: () -> i8 { x := 0; ... }` gives `x: i8`. Expected (specs.md §"Integer
|
||||
literals default to `i64`", lines 240 / 1428): `i64` in all of these — the
|
||||
declaration has no target type. Inside a `-> void` function the same decl
|
||||
correctly infers `s64`.
|
||||
correctly infers `i64`.
|
||||
|
||||
Consequences are silent and severe:
|
||||
|
||||
- All arithmetic through such locals wraps at the narrowed width:
|
||||
`x := 0; x += 3000000000;` inside `main :: () -> s32` prints `-1294967296`.
|
||||
`x := 0; x += 3000000000;` inside `main :: () -> i32` 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
|
||||
`big := 3000000000;` inside a `-> i32` fn binds `big` to i32 = `-1294967296`.
|
||||
- Blast radius: every `main :: () -> i32 { ... }` in the corpus types every
|
||||
unannotated int-literal local as i32 — 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.
|
||||
`-> i32` main) printed the 32-bit-wrapped sum after the segfault was fixed.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
f :: () -> s32 {
|
||||
f :: () -> i32 {
|
||||
x := 0;
|
||||
print("f.x: {}\n", type_name(type_of(x)));
|
||||
0
|
||||
}
|
||||
|
||||
g :: () -> s8 {
|
||||
g :: () -> i8 {
|
||||
x := 0;
|
||||
print("g.x: {}\n", type_name(type_of(x)));
|
||||
0
|
||||
}
|
||||
|
||||
big_host :: () -> s32 {
|
||||
big_host :: () -> i32 {
|
||||
big := 3000000000;
|
||||
print("big: {} = {}\n", type_name(type_of(big)), big);
|
||||
0
|
||||
@@ -74,9 +74,9 @@ main :: () {
|
||||
}
|
||||
```
|
||||
|
||||
- **Observed** (current master): `f.x: s32`, `g.x: s8`,
|
||||
`big: s32 = -1294967296`, `main.x: s64`.
|
||||
- **Expected**: `s64` for all four; `big` prints `3000000000`.
|
||||
- **Observed** (current master): `f.x: i32`, `g.x: i8`,
|
||||
`big: i32 = -1294967296`, `main.x: i64`.
|
||||
- **Expected**: `i64` 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).
|
||||
@@ -95,14 +95,14 @@ Three-link chain, all confirmed by reading:
|
||||
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);
|
||||
`target_type` with the annotation (which is why `y : i64 = 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
|
||||
> function's integer return type instead of the i64 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
|
||||
@@ -112,7 +112,7 @@ Three-link chain, all confirmed by reading:
|
||||
> 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` paths — `f :: () -> i32 { 0 }` must keep coercing the tail to the
|
||||
> return type.
|
||||
>
|
||||
> Separately consider (same fix or follow-up per scope): the `.int_literal`
|
||||
@@ -121,7 +121,7 @@ Three-link chain, all confirmed by reading:
|
||||
> (`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
|
||||
> expect `i64` 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
|
||||
@@ -132,8 +132,8 @@ Three-link chain, all confirmed by reading:
|
||||
> 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
|
||||
> accumulation in `main :: () -> i32`) cannot produce the documented expected
|
||||
> output (`sum=499999500000`) until locals stop narrowing to i32, 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.
|
||||
|
||||
@@ -18,14 +18,14 @@ 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.
|
||||
`i3` 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;`)
|
||||
**Found during the fix:** negated-literal GLOBAL initializers (`g : i64 = -1;`)
|
||||
are rejected as non-constant — pre-existing gap, filed as issue 0113.
|
||||
|
||||
---
|
||||
@@ -33,10 +33,10 @@ 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
|
||||
integer target truncates with no diagnostic: `x : s8 = 300;` binds 44,
|
||||
integer target truncates with no diagnostic: `x : i8 = 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`).
|
||||
narrowing rule, which errors on non-exact `y : i64 = 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
|
||||
@@ -48,7 +48,7 @@ annotation path keeps wrapping).
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () {
|
||||
x : s8 = 300;
|
||||
x : i8 = 300;
|
||||
print("x: {}\n", x);
|
||||
y : u8 = 256;
|
||||
print("y: {}\n", y);
|
||||
@@ -58,7 +58,7 @@ main :: () {
|
||||
- **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
|
||||
`integer literal 300 does not fit in i8 (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`
|
||||
@@ -72,7 +72,7 @@ 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
|
||||
lvalues (`b = 300` where `b: i8`) reach the same arm via `lowerAssignment`'s
|
||||
LHS-derived target and likely need the same check.
|
||||
|
||||
## Investigation prompt (paste into a fresh session)
|
||||
@@ -84,15 +84,15 @@ LHS-derived target and likely need the same check.
|
||||
> 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:
|
||||
> type, and its range — do NOT silently fall back to i64 (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).
|
||||
> two diagnostics (i8/300, u8/256); boundary values still compile
|
||||
> (`x : i8 = -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-...`).
|
||||
|
||||
@@ -7,10 +7,10 @@ compile-time constant" — even though `constExprValue` already folds
|
||||
|
||||
**Fix:** a `.unary_op` arm routes the initializer through `constExprValue`;
|
||||
the folded value follows the direct-literal rules — `checkIntLiteralFits` on
|
||||
ints (`g : s8 = -300;` gets the range diagnostic, not "non-constant"), and a
|
||||
ints (`g : i8 = -300;` gets the range diagnostic, not "non-constant"), and a
|
||||
negated float at an integer global narrows only when integral
|
||||
(`g : s64 = -4.0;` → -4; `-4.5` errors). Binary-op initializers
|
||||
(`g : s32 = 2 + 3;`) remain unsupported and keep the specific
|
||||
(`g : i64 = -4.0;` → -4; `-4.5` errors). Binary-op initializers
|
||||
(`g : i32 = 2 + 3;`) remain unsupported and keep the specific
|
||||
"must be initialized by a compile-time constant" diagnostic — const-expr
|
||||
folding for those is a separate feature if ever wanted.
|
||||
|
||||
@@ -22,18 +22,18 @@ folding for those is a separate feature if ever wanted.
|
||||
# 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
|
||||
compile: `g : i64 = -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).
|
||||
Positive literals work (`g : i64 = 1;`). Locals are unaffected
|
||||
(`x : i64 = -1;` inside a function is fine — lowerExpr folds the negate).
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
g : s64 = -1;
|
||||
g : i64 = -1;
|
||||
|
||||
main :: () {
|
||||
print("{}\n", g);
|
||||
@@ -58,20 +58,20 @@ 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
|
||||
> Fix issue 0113: `g : i64 = -1;` (and const-expression initializers like
|
||||
> `g : i64 = 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
|
||||
> global's type — `g : i8 = -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
|
||||
> Verify: the repro prints -1; `g2 : i8 = -300;` errors with the range
|
||||
> message; `g3 : i32 = 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.
|
||||
|
||||
@@ -30,7 +30,7 @@ plus a REJECTED-PATTERNS silent first-wins.
|
||||
|
||||
```sx
|
||||
// target.sx
|
||||
helper :: () -> s64 { 7 }
|
||||
helper :: () -> i64 { 7 }
|
||||
```
|
||||
```sx
|
||||
// facade.sx
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
> the full std namespace tail is enabled on top.
|
||||
|
||||
**Symptom.** When two modules in one program declare a same-named module
|
||||
const with DIFFERENT shapes (scalar `K : s64 : 4` vs array
|
||||
`K : [4]s64 : .[...]`), resolution conflates them instead of selecting
|
||||
const with DIFFERENT shapes (scalar `K : i64 : 4` vs array
|
||||
`K : [4]i64 : .[...]`), resolution conflates them instead of selecting
|
||||
per-author:
|
||||
|
||||
- **Observed (minimal repro below)**: compiler PANIC — `unresolved type
|
||||
@@ -30,7 +30,7 @@ per-author:
|
||||
own scalar `K` reads as the other module's array global (prints the
|
||||
array's address or the whole array). Seen corpus-wide when
|
||||
`hash :: #import "modules/std/hash.sx"` (hash.sx declares the SHA-256
|
||||
`K : [64]s64` table) is added to the std.sx namespace tail: examples
|
||||
`K : [64]i64` table) is added to the std.sx namespace tail: examples
|
||||
0786/0787/0788/0789/0791/0793/0794 (same-name-const family), 0162, 0168
|
||||
all read hash's `K` instead of their own.
|
||||
- **Expected**: own-wins / per-author const selection (the documented F2
|
||||
@@ -45,15 +45,15 @@ consts are robust across every module pulled into every program.
|
||||
|
||||
```sx
|
||||
// h.sx
|
||||
K : [4]s64 : .[11, 22, 33, 44];
|
||||
use_k :: () -> s64 { K[2] }
|
||||
K : [4]i64 : .[11, 22, 33, 44];
|
||||
use_k :: () -> i64 { K[2] }
|
||||
```
|
||||
|
||||
```sx
|
||||
// main.sx
|
||||
#import "modules/std.sx";
|
||||
h :: #import "h.sx";
|
||||
K : s64 : 4;
|
||||
K : i64 : 4;
|
||||
main :: () { print("K={} h.use_k={}\n", K, h.use_k()); }
|
||||
```
|
||||
|
||||
@@ -100,7 +100,7 @@ flat-imports std.sx. The panic variant above is the minimal entry point.)
|
||||
|
||||
Same-name module consts are selected own-wins via `selectModuleConst`
|
||||
(F2, src/ir/lower/expr.zig ~1641) over `module_const_map` — but ARRAY
|
||||
consts lower as GLOBALS (`@K = internal global [4 x s64]`), registered in
|
||||
consts lower as GLOBALS (`@K = internal global [4 x i64]`), registered in
|
||||
a different, still last-wins registry (find it: grep the lowering for
|
||||
where a top-level array const becomes a module global — likely
|
||||
`lowerGlobalDecl` / the global-var map in src/ir/lower/decl.zig). The
|
||||
|
||||
@@ -24,7 +24,7 @@ program crashes at runtime.
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
Color :: struct { r, g, b: s64; }
|
||||
Color :: struct { r, g, b: i64; }
|
||||
WHITE :: Color.{ r = 255, g = 255, b = 255 };
|
||||
|
||||
main :: () {
|
||||
|
||||
@@ -36,8 +36,8 @@ pointer-to-array.
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () {
|
||||
k : [4]s64 = .[11, 22, 33, 44];
|
||||
p := @k; // *[4]s64
|
||||
k : [4]i64 = .[11, 22, 33, 44];
|
||||
p := @k; // *[4]i64
|
||||
print("{}\n", p[2]); // expected 33; panics at emission today
|
||||
}
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
> `lowerExpr`'s catch-all `unknown_expr` error. Fixed by giving the six
|
||||
> compound type-expr shapes (`*T`, `[*]T`, `[]T`, `?T`, `[N]T`, fn types) a
|
||||
> first-class `const_type` lowering arm in `src/ir/lower/expr.zig`,
|
||||
> mirroring named types (`t : Type = *s64;` now works like `t : Type =
|
||||
> mirroring named types (`t : Type = *i64;` now works like `t : Type =
|
||||
> f64;`). (2) The cast handler's private static-type gate only accepted
|
||||
> bare names — replaced with the canonical `isStaticTypeArg`
|
||||
> (`src/ir/lower/call.zig`), so static compound casts route through
|
||||
@@ -17,26 +17,26 @@
|
||||
|
||||
## Symptom
|
||||
|
||||
`cast(T) expr` with a COMPOUND static type argument (`*s64`, `[]u8`, `?s32`,
|
||||
`[*]s64`, `[4]s64`, …) fails to compile with a junk diagnostic pointing at the
|
||||
`cast(T) expr` with a COMPOUND static type argument (`*i64`, `[]u8`, `?i32`,
|
||||
`[*]i64`, `[4]i64`, …) fails to compile with a junk diagnostic pointing at the
|
||||
type argument. Observed:
|
||||
|
||||
```
|
||||
error: unresolved 'unknown_expr' (in probe.sx fn main)
|
||||
--> probe.sx:5:21
|
||||
|
|
||||
5 | q : *s64 = cast(*s64) p;
|
||||
5 | q : *i64 = cast(*i64) p;
|
||||
| ^^^^
|
||||
```
|
||||
|
||||
Expected: the cast resolves the type argument statically and routes through
|
||||
`coerceExplicit` (for `cast(*s64) p` where `p : *s64`, a no-op), exactly as it
|
||||
does for bare names (`cast(s32) 3.14` works). The spec places no scalar-only
|
||||
`coerceExplicit` (for `cast(*i64) p` where `p : *i64`, a no-op), exactly as it
|
||||
does for bare names (`cast(i32) 3.14` works). The spec places no scalar-only
|
||||
restriction on `cast(Type)` (specs.md "cast(Type) expr — prefix operator that
|
||||
converts expr to Type").
|
||||
|
||||
Pre-existing on master (verified on a clean build of 679653f) — independent of
|
||||
the in-flight const-pointer work; plain `*s64` reproduces it.
|
||||
the in-flight const-pointer work; plain `*i64` reproduces it.
|
||||
|
||||
## Reproduction
|
||||
|
||||
@@ -45,8 +45,8 @@ the in-flight const-pointer work; plain `*s64` reproduces it.
|
||||
|
||||
main :: () {
|
||||
x := 42;
|
||||
p : *s64 = @x;
|
||||
q : *s64 = cast(*s64) p; // error: unresolved 'unknown_expr'
|
||||
p : *i64 = @x;
|
||||
q : *i64 = cast(*i64) p; // error: unresolved 'unknown_expr'
|
||||
print("{}\n", q.*); // expected: 42
|
||||
}
|
||||
```
|
||||
@@ -64,8 +64,8 @@ builtin path, which cannot resolve it and surfaces the catch-all
|
||||
|
||||
The codebase already has the canonical gate: `Lowering.isStaticTypeArg`
|
||||
(`src/ir/lower/generic.zig:206`), which `type_name` / `type_eq` use — it
|
||||
accepts the full compound-shape set (this is why `type_name(*s64)` folds
|
||||
fine while `cast(*s64)` dies). The fix likely: replace the private
|
||||
accepts the full compound-shape set (this is why `type_name(*i64)` folds
|
||||
fine while `cast(*i64)` dies). The fix likely: replace the private
|
||||
`is_static_type` block with `self.isStaticTypeArg(type_arg)` (keeping the
|
||||
scope-shadow semantics: an identifier bound to a runtime `Type` variable must
|
||||
still route to the runtime-dispatch path used by `case`-arm `cast(type)` —
|
||||
@@ -74,7 +74,7 @@ dispatch"). Then `resolveTypeArg` already handles the compound shapes.
|
||||
|
||||
Verification:
|
||||
1. Run the repro above — expect `42`, exit 0.
|
||||
2. Sanity: `cast([]u8)`, `cast(?s32)`, `cast([*]s64)` forms resolve.
|
||||
2. Sanity: `cast([]u8)`, `cast(?i32)`, `cast([*]i64)` forms resolve.
|
||||
3. `bash tests/run_examples.sh` — the `case`-arm runtime-dispatch examples
|
||||
(any_to_string formatting suite) must stay green, proving the
|
||||
runtime-`Type`-variable path still falls through to the builtin.
|
||||
|
||||
@@ -32,7 +32,7 @@ Observed (one probe, all three failures):
|
||||
|
||||
- `xs.sum_all()` (concrete fn, slice receiver) → **works**
|
||||
- `xs.first_of()` (generic `[]$T` fn, slice receiver) → `unresolved 'first_of'`
|
||||
- `p.pick(s32)` (generic `$T: Type` fn, struct receiver) → `unresolved 'pick'`
|
||||
- `p.pick(i32)` (generic `$T: Type` fn, struct receiver) → `unresolved 'pick'`
|
||||
- `a.create(Session)` (generic fn, protocol-value receiver) → `unresolved 'create'`
|
||||
|
||||
Expected: specs.md §UFCS promises the rewrite unconditionally ("When
|
||||
@@ -53,7 +53,7 @@ first_of :: (xs: []$T) -> T { xs[0] }
|
||||
|
||||
main :: () {
|
||||
arr := .[1, 2, 3];
|
||||
xs : []s64 = arr;
|
||||
xs : []i64 = arr;
|
||||
print("{}\n", first_of(xs)); // 1 — direct call works
|
||||
print("{}\n", xs.first_of()); // error: unresolved 'first_of'
|
||||
}
|
||||
@@ -84,7 +84,7 @@ concrete fns.
|
||||
|
||||
Verification:
|
||||
1. The repro above prints `1` twice, exit 0.
|
||||
2. Matrix probe: generic-on-struct (`p.pick(s32)`), generic-on-slice
|
||||
2. Matrix probe: generic-on-struct (`p.pick(i32)`), generic-on-slice
|
||||
(`xs.first_of()`), generic-on-protocol-value
|
||||
(`a.create(Session)` with `create :: (a: Allocator, $T: Type) -> *T`)
|
||||
all dispatch; concrete UFCS unchanged.
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
`Alias :: Box;` where `Box` is a generic struct (`struct ($T: Type)`)
|
||||
lowers without any diagnostic, and instantiating through the alias
|
||||
(`Alias(s64).{ ... }`) reaches LLVM emission with an `.unresolved`
|
||||
(`Alias(i64).{ ... }`) reaches LLVM emission with an `.unresolved`
|
||||
type — the backend tripwire panics:
|
||||
|
||||
```
|
||||
@@ -81,7 +81,7 @@ Box :: struct ($T: Type) {
|
||||
BoxAlias :: Box;
|
||||
|
||||
main :: () {
|
||||
b := BoxAlias(s64).{ item = 3 };
|
||||
b := BoxAlias(i64).{ item = 3 };
|
||||
print("{}\n", b.item);
|
||||
}
|
||||
```
|
||||
@@ -99,14 +99,14 @@ Box :: struct ($T: Type) {
|
||||
BoxAlias :: Box;
|
||||
|
||||
main :: () {
|
||||
b := BoxAlias(s64).{ item = 3 };
|
||||
b := BoxAlias(i64).{ item = 3 };
|
||||
print("{}\n", b.get());
|
||||
}
|
||||
```
|
||||
|
||||
Cross-module variant (`rich.sx` declares `Box`; `facade.sx` has
|
||||
`r :: #import "rich.sx"; Box :: r.Box;`; a consumer flat-importing
|
||||
facade.sx gets `type 'Box' is not visible` at `Box(s64).{ ... }`).
|
||||
facade.sx gets `type 'Box' is not visible` at `Box(i64).{ ... }`).
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
@@ -139,8 +139,8 @@ declaring module with ordinary own-decl visibility so one-level
|
||||
flat-import carry works (mirror whatever makes `Thing :: r.Thing;`
|
||||
re-export correctly today). Mind collision semantics (own-wins /
|
||||
ambiguity) and that the alias must also work as a plain type head in
|
||||
annotations (`x: BoxAlias(s64)`), nested generics
|
||||
(`List(BoxAlias(s64))` if applicable), and method/UFCS dispatch on
|
||||
annotations (`x: BoxAlias(i64)`), nested generics
|
||||
(`List(BoxAlias(i64))` if applicable), and method/UFCS dispatch on
|
||||
instantiations through the alias.
|
||||
|
||||
Motivating context: the std.sx-as-pure-re-exports restructure wants
|
||||
|
||||
@@ -51,7 +51,7 @@ deep.)
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
pack_sum :: (..$args) -> s64 {
|
||||
pack_sum :: (..$args) -> i64 {
|
||||
args[0] + args[1]
|
||||
}
|
||||
sum_alias :: pack_sum;
|
||||
|
||||
@@ -44,7 +44,7 @@ Against the pre-fix compiler with the re-export std.sx:
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
Point :: struct { x, y: s32; }
|
||||
Point :: struct { x, y: i32; }
|
||||
main :: () {
|
||||
f := closure((p: Point) -> Point => Point.{ x = p.x + 1, y = p.y });
|
||||
r := f(Point.{ x = 1, y = 2 });
|
||||
|
||||
@@ -41,12 +41,12 @@ Both directions are broken, on every plain dispatch path probed:
|
||||
|
||||
- too MANY args, bare call: `concat("a", "b", "c")` (std's `concat`
|
||||
takes 2 strings) → LLVM verifier failure.
|
||||
- too FEW args, bare call: `add2(1)` with `add2 :: (a: s64, b: s64)`
|
||||
- too FEW args, bare call: `add2(1)` with `add2 :: (a: i64, b: i64)`
|
||||
→ same.
|
||||
- methods / ufcs dot-calls: same shape, receiver included. Worse:
|
||||
a trailing-default param on a plain struct method or a ufcs fn is
|
||||
never filled on the dot-call path (`p.scaled()` with
|
||||
`scaled :: (self: Point, k: s64 = 2)` emits a 1-arg call to a
|
||||
`scaled :: (self: Point, k: i64 = 2)` emits a 1-arg call to a
|
||||
2-param fn — bare calls fill defaults via `expandCallDefaults`,
|
||||
the method/ufcs sites never run `appendDefaultArgs`).
|
||||
|
||||
@@ -62,7 +62,7 @@ C variadics, `#compiler` / `#builtin` bodies.
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
s := concat("a", "b", "c"); // concat takes (a: string, b: string)
|
||||
out(s);
|
||||
return 0;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
> (`any_to_string`'s per-array-type arms pass the array by value — any
|
||||
> 64K+ array type + any `{}` print still crashes).
|
||||
> Regression test: `examples/0055-basic-large-stack-array.sx`
|
||||
> ([65536]u8 write/read loops + [131072]s64 first/last — `sx build`
|
||||
> ([65536]u8 write/read loops + [131072]i64 first/last — `sx build`
|
||||
> segfaulted pre-fix). 22 `.ir` snapshots re-pinned (removed undef
|
||||
> stores / `ig.tmp` spills → in-place gep+load; reviewed
|
||||
> instruction-shape-only). Gates: zig build test 426/426, suite
|
||||
@@ -59,12 +59,12 @@ replaced by in-place access the module compiles.
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
f :: (fd: s32) {
|
||||
f :: (fd: i32) {
|
||||
buf : [65536]u8 = ---;
|
||||
if buf[0] > 0 { out("x\n"); }
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
f(1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ f :: () {
|
||||
out(string.{ ptr = @buf[0], len = 1 });
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
main :: () -> i32 {
|
||||
f();
|
||||
print("{}\n", 5);
|
||||
return 0;
|
||||
|
||||
@@ -30,11 +30,11 @@ during emission instead of compiling (or diagnosing).
|
||||
(src/backend/llvm/types.zig:175, via `emitIndexGet` in the
|
||||
monomorphized body).
|
||||
- **Expected**: the array coerces to a slice at the `[]T` param — the
|
||||
same promotion a CONCRETE `[]s64` param (and a `[]s64`-annotated
|
||||
same promotion a CONCRETE `[]i64` param (and a `[]i64`-annotated
|
||||
local) already performs — so `T` binds from the array's element type
|
||||
and the call compiles.
|
||||
|
||||
Passing an actual slice works (`s : []s64 = a; first(s)` prints the
|
||||
Passing an actual slice works (`s : []i64 = a; first(s)` prints the
|
||||
element); only the direct array spelling breaks, and only for generic
|
||||
slice params.
|
||||
|
||||
@@ -47,8 +47,8 @@ first :: (xs: []$T) -> T {
|
||||
return xs[0];
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
a : [3]s64 = ---;
|
||||
main :: () -> i32 {
|
||||
a : [3]i64 = ---;
|
||||
a[0] = 7; a[1] = 8; a[2] = 9;
|
||||
v := first(a);
|
||||
print("{}\n", v);
|
||||
@@ -56,7 +56,7 @@ main :: () -> s32 {
|
||||
}
|
||||
```
|
||||
|
||||
Observed at master 837b5d3: the panic above. With `s : []s64 = a;
|
||||
Observed at master 837b5d3: the panic above. With `s : []i64 = a;
|
||||
first(s)` it prints `7`.
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
> the bare-identifier path routes generics through
|
||||
> `inferGenericReturnType`. Lowering dispatched the right mono (the
|
||||
> value was correct); only the planned result type was wrong, so
|
||||
> pack-fn callers (print's Any boxing) mis-tagged it — and a non-s64
|
||||
> pack-fn callers (print's Any boxing) mis-tagged it — and a non-i64
|
||||
> binding (f64) failed LLVM verification outright, the pack being
|
||||
> monomorphized for the stub while the call returned `double`. Fix:
|
||||
> both arms now classify a `type_params.len > 0` callee as
|
||||
> `.generic_fn` and infer the return type through the call's bindings,
|
||||
> mirroring the flat path. Regression test:
|
||||
> `examples/0213-generics-namespaced-call-result.sx` (s64 + f64
|
||||
> `examples/0213-generics-namespaced-call-result.sx` (i64 + f64
|
||||
> bindings via print, concrete type flowing into arithmetic; pre-fix:
|
||||
> `T{}` boxing / LLVM verification failure — both demonstrated).
|
||||
> Gates: zig build test 426/426, suite 595/595, distribution repo
|
||||
|
||||
Reference in New Issue
Block a user