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:
agra
2026-06-12 09:31:53 +03:00
parent 515ecebea7
commit d8076b9333
1054 changed files with 6836 additions and 6839 deletions

View File

@@ -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`

View File

@@ -11,4 +11,4 @@
extern g_x : *void;
main :: () -> s32 { 0; }
main :: () -> i32 { 0; }

View File

@@ -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

View File

@@ -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

View File

@@ -83,7 +83,7 @@ caller :: (self: *void, _cmd: *void, scene: *void, b: *void, c: *void) callconv(
}
}
main :: () -> s32 { 0; }
main :: () -> i32 { 0; }
```
Build:

View File

@@ -47,7 +47,7 @@ Foo :: #objc_class("SxFooSelfTest") {
}
}
main :: () -> s32 {
main :: () -> i32 {
inline if OS == .macos {
f := Foo.alloc().init();
result := f.poke();

View File

@@ -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.

View File

@@ -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

View File

@@ -27,7 +27,7 @@ configure :: () {
}
#run configure();
main :: () -> s32 {
main :: () -> i32 {
print("hello from runtime\n");
return 0;
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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.**

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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;
}
```

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -39,7 +39,7 @@ fabricated empty-struct type.
NotAType :: 123;
main :: () -> s32 {
main :: () -> i32 {
v: NotAType = ---;
print("value = {}\n", v);
return 0;

View File

@@ -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

View File

@@ -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`,

View File

@@ -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:

View File

@@ -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:

View File

@@ -54,7 +54,7 @@ work :: () {
defer { cb := () { return; }; }
}
main :: () -> s32 {
main :: () -> i32 {
work();
return 0;
}

View File

@@ -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,

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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");

View File

@@ -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 14 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. 00790082).
@@ -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`.

View File

@@ -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.

View File

@@ -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]]));
}

View File

@@ -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);
}
```

View File

@@ -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);
}
```

View File

@@ -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.

View File

@@ -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).

View File

@@ -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:

View File

@@ -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:

View File

@@ -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`.

View File

@@ -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

View File

@@ -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.

View File

@@ -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();

View File

@@ -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 } }; }

View File

@@ -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
}

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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.

View File

@@ -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-...`).

View File

@@ -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.

View File

@@ -30,7 +30,7 @@ plus a REJECTED-PATTERNS silent first-wins.
```sx
// target.sx
helper :: () -> s64 { 7 }
helper :: () -> i64 { 7 }
```
```sx
// facade.sx

View File

@@ -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

View File

@@ -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 :: () {

View File

@@ -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
}
```

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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;

View File

@@ -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 });

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -44,7 +44,7 @@ f :: () {
out(string.{ ptr = @buf[0], len = 1 });
}
main :: () -> s32 {
main :: () -> i32 {
f();
print("{}\n", 5);
return 0;

View File

@@ -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

View File

@@ -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