From 1f9f944ca18dd6f844fe8b555f091165d28f7238 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 4 Jun 2026 09:06:08 +0300 Subject: [PATCH] fix(ir): exhaustive named-const array dims (0083) + nested slice-literal coercion (0085) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the F0.4 fixes exhaustive across every resolution / nesting path. 0083 — named-const array dimension, stateless paths. Attempt 1 fixed the stateful resolver (direct local decls, struct fields, params, returns) but the binding-free registration-time resolver (`type_bridge`, used for type aliases `Arr :: [N]T` and inline union/enum field types) still resolved a named dim with a silent `else 0`, so `Arr :: [N]s64; a : Arr` and `union { a: [N]s64 }` were still miscompiled (garbage / bus error). Thread the module-global const table (`ProgramIndex.module_const_map`) 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 dim on the binding-free path, which the stateful path hard-errors) now bails LOUDLY instead of fabricating a 0 length. 0085 — nested slice-literal elements. `lowerArrayLiteral` lowered each element with the element type as target but appended the raw value. A nested `.[...]` element at a slice element type (`[][]s64`) still lowers to an aggregate array `[N]T`, so the outer aggregate held raw arrays where slice {ptr,len} headers were expected — indexing the inner slice read a garbage pointer and segfaulted. After lowering each element, coerce a same-element array to the slice element type via the existing `array_to_slice` op. The coercion recurses with the nesting, so `[][]T` and deeper materialize at every level — local-bound AND direct-call-argument forms. Regressions (fail-before/pass-after demonstrated on the pre-fix compiler): examples/0140-types-named-const-array-dim.sx — extended with type-alias, nested [N][M]T, and union-field named dims (s64 / string / struct elems) examples/0142-types-nested-slice-literal-elements.sx — [][]s64 + [][]string, local-bound vs direct-arg src/ir/type_bridge.test.zig — named-const dim resolves to literal length Gate: zig build, zig build test, bash tests/run_examples.sh (388 passed). Issues 0083 and 0085 marked RESOLVED. --- examples/0140-types-named-const-array-dim.sx | 47 +++++++- ...142-types-nested-slice-literal-elements.sx | 44 ++++++++ .../0140-types-named-const-array-dim.stdout | 4 + ...2-types-nested-slice-literal-elements.exit | 1 + ...types-nested-slice-literal-elements.stderr | 1 + ...types-nested-slice-literal-elements.stdout | 4 + ...named-const-array-dimension-miscompiled.md | 17 ++- ...sted-slice-literal-elements-not-coerced.md | 55 +++++++++ src/ir/calls.zig | 2 +- src/ir/lower.zig | 72 +++++++----- src/ir/protocols.zig | 10 +- src/ir/type_bridge.test.zig | 60 +++++++--- src/ir/type_bridge.zig | 104 +++++++++++------- 13 files changed, 329 insertions(+), 92 deletions(-) create mode 100644 examples/0142-types-nested-slice-literal-elements.sx create mode 100644 examples/expected/0142-types-nested-slice-literal-elements.exit create mode 100644 examples/expected/0142-types-nested-slice-literal-elements.stderr create mode 100644 examples/expected/0142-types-nested-slice-literal-elements.stdout create mode 100644 issues/0085-nested-slice-literal-elements-not-coerced.md diff --git a/examples/0140-types-named-const-array-dim.sx b/examples/0140-types-named-const-array-dim.sx index a17a519..caf79d0 100644 --- a/examples/0140-types-named-const-array-dim.sx +++ b/examples/0140-types-named-const-array-dim.sx @@ -1,32 +1,69 @@ // A fixed array whose dimension is a module-global named constant // (`N :: 16; [N]T`) has the same layout as a literal-dimension array // (`[16]T`): correct length and element stride for scalar, slice/pointer -// (string), and struct element types. +// (string), and struct element types — on EVERY type-resolution path: +// direct local decls, type aliases (`Arr :: [N]T`), nested fixed arrays +// (`[N][M]T`), and inline union fields. The named dim must resolve to the +// same length whether it flows through the stateful body-lowering resolver +// or the stateless registration-time resolver (type_bridge). // Regression (issue 0083): a named-const dim resolved to length 0, giving a // 0-byte alloca — scalar reads returned garbage and string/struct elements -// bus-errored. +// bus-errored. The alias and union-field paths went through the stateless +// resolver, which had no const table and silently fabricated a 0 length. #import "modules/std.sx"; N :: 4; +M :: 3; P :: struct { x: s64; y: s64; } +// Type aliases whose dimension is the named const N (stateless registration). +Arr :: [N]s64; +SArr :: [N]string; + +// Inline union field with a named-const dimension (stateless registration). +U :: union { a: [N]s64; tag: s64; } + main :: () { - // Scalar elements: store then read back. + // Scalar elements (direct local): store then read back. a : [N]s64 = ---; a[0] = 7; a[3] = 42; print("scalar a0={} a3={}\n", a[0], a[3]); - // Slice/pointer elements (string): used to bus-error. + // Slice/pointer elements (string, direct local): used to bus-error. s : [N]string = ---; s[0] = "hi"; s[1] = "yo"; print("string s0={} s1={}\n", s[0], s[1]); - // Struct elements. + // Struct elements (direct local). ps : [N]P = ---; ps[0] = P.{ x = 1, y = 2 }; ps[2] = P.{ x = 5, y = 6 }; print("struct p0x={} p0y={} p2x={}\n", ps[0].x, ps[0].y, ps[2].x); + + // Type-alias dimension (scalar): same layout as the direct `[N]s64`. + aa : Arr = ---; + aa[0] = 11; + aa[3] = 99; + print("alias a0={} a3={}\n", aa[0], aa[3]); + + // Type-alias dimension (string): no bus error, correct reads. + sa : SArr = ---; + sa[0] = "al"; + sa[2] = "ok"; + print("alias s0={} s2={}\n", sa[0], sa[2]); + + // Nested fixed array `[N][M]s64`: both dimensions are named consts. + grid : [N][M]s64 = ---; + grid[0][0] = 1; + grid[3][2] = 8; + print("nested g00={} g32={}\n", grid[0][0], grid[3][2]); + + // Inline union field with a named-const dimension. + u : U = ---; + u.a[0] = 70; + u.a[3] = 7; + print("union u0={} u3={}\n", u.a[0], u.a[3]); } diff --git a/examples/0142-types-nested-slice-literal-elements.sx b/examples/0142-types-nested-slice-literal-elements.sx new file mode 100644 index 0000000..7a1e088 --- /dev/null +++ b/examples/0142-types-nested-slice-literal-elements.sx @@ -0,0 +1,44 @@ +// A nested array/slice literal (`.[.[1, 2], .[3, 4]]`) at an expected slice-of- +// slices type (`[][]s64`) materializes each inner `[N]T` literal as a real `[]T` +// slice, so indexing the inner slice in the callee reads element contents +// correctly — for both the local-bound form and the direct-call-argument form. +// Regression (issue 0085): inner literals were appended as raw `[N]T` arrays +// under an element type of `[]T`, so the outer aggregate's elements were arrays +// where slice {ptr,len} headers were expected; indexing the inner slice read a +// garbage pointer and segfaulted. The per-element array->slice materialization +// recurses with the nesting, so every level coerces. +#import "modules/std.sx"; + +sum_nested :: (xss: [][]s64) -> s64 { + total := 0; + i := 0; + while i < xss.len { + j := 0; + while j < xss[i].len { total += xss[i][j]; j += 1; } + i += 1; + } + return total; +} + +count_x :: (xss: [][]string) -> s64 { + n := 0; + i := 0; + while i < xss.len { + j := 0; + while j < xss[i].len { if xss[i][j] == "x" { n += 1; } j += 1; } + i += 1; + } + return n; +} + +main :: () { + // numeric [][]s64 — local-bound vs direct-arg both sum to 10. + local : [][]s64 = .[.[1, 2], .[3, 4]]; + print("num local={}\n", sum_nested(local)); + print("num direct={}\n", sum_nested(.[.[1, 2], .[3, 4]])); + + // string [][]string — local-bound vs direct-arg both count 4 "x"s. + slocal : [][]string = .[.["x", "a"], .["b", "x"], .["x", "x"]]; + print("str local={}\n", count_x(slocal)); + print("str direct={}\n", count_x(.[.["x", "a"], .["b", "x"], .["x", "x"]])); +} diff --git a/examples/expected/0140-types-named-const-array-dim.stdout b/examples/expected/0140-types-named-const-array-dim.stdout index 9d6220b..579ec07 100644 --- a/examples/expected/0140-types-named-const-array-dim.stdout +++ b/examples/expected/0140-types-named-const-array-dim.stdout @@ -1,3 +1,7 @@ scalar a0=7 a3=42 string s0=hi s1=yo struct p0x=1 p0y=2 p2x=5 +alias a0=11 a3=99 +alias s0=al s2=ok +nested g00=1 g32=8 +union u0=70 u3=7 diff --git a/examples/expected/0142-types-nested-slice-literal-elements.exit b/examples/expected/0142-types-nested-slice-literal-elements.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0142-types-nested-slice-literal-elements.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0142-types-nested-slice-literal-elements.stderr b/examples/expected/0142-types-nested-slice-literal-elements.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0142-types-nested-slice-literal-elements.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0142-types-nested-slice-literal-elements.stdout b/examples/expected/0142-types-nested-slice-literal-elements.stdout new file mode 100644 index 0000000..8e79f9a --- /dev/null +++ b/examples/expected/0142-types-nested-slice-literal-elements.stdout @@ -0,0 +1,4 @@ +num local=10 +num direct=10 +str local=4 +str direct=4 diff --git a/issues/0083-named-const-array-dimension-miscompiled.md b/issues/0083-named-const-array-dimension-miscompiled.md index 1b75f26..73ab66e 100644 --- a/issues/0083-named-const-array-dimension-miscompiled.md +++ b/issues/0083-named-const-array-dimension-miscompiled.md @@ -9,9 +9,22 @@ > type). The stateful `Lowering.resolveArrayLen` evaluates the dimension as a > compile-time integer across the comptime-constant, generic-value, and > module-global const tables, and emits a diagnostic (no fabricated length) when -> it isn't one. Files: `src/ir/type_resolver.zig`, `src/ir/lower.zig`, +> it isn't one. +> +> **Exhaustive follow-up (attempt 2).** The first fix covered every *stateful* +> 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 +> 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` -> (s64 + string + struct element types). +> (direct + type-alias + nested `[N][M]T` + union-field dims, s64 / string / +> struct element types). ## Symptom A fixed array whose dimension is a module-global integer constant (`N :: 16; diff --git a/issues/0085-nested-slice-literal-elements-not-coerced.md b/issues/0085-nested-slice-literal-elements-not-coerced.md new file mode 100644 index 0000000..6a1a221 --- /dev/null +++ b/issues/0085-nested-slice-literal-elements-not-coerced.md @@ -0,0 +1,55 @@ +# 0085 — nested slice literal elements are stored as raw arrays + +> **RESOLVED.** Root cause: `Lowering.lowerArrayLiteral` lowered each element with +> the element type as `target_type` but appended the returned value directly. For +> a nested `.[...]` element whose expected element type is a slice (`[]T`), the +> inner literal still lowers to an aggregate ARRAY `[N]T` — so the outer aggregate +> (typed array-of-`[]T`) held raw arrays where slice {ptr,len} headers were +> expected; indexing the inner slice read a garbage pointer and segfaulted. Fix: +> after lowering each element, when the element type is a slice and the lowered +> value is a same-element array, coerce it via the existing `array_to_slice` op +> (materialize backing storage + build the header) — identical to the whole- +> 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`, +> 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 +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. + +## Reproduction +```sx +#import "modules/std.sx"; + +sum_nested :: (xss: [][]s64) -> s64 { + return xss[0][1] + xss[1][0]; +} + +main :: () { + local : [][]s64 = .[.[1, 2], .[3, 4]]; + print("local={}\n", sum_nested(local)); + print("direct={}\n", sum_nested(.[.[1, 2], .[3, 4]])); +} +``` +Observed on `flow/sx-foundation/F0.4`: segfault at address `0x9` before either +line prints. Expected output: +```text +local=5 +direct=5 +``` + +## Investigation prompt +Fix nested slice literal materialization. The likely area is +`src/ir/lower.zig` in `Lowering.lowerArrayLiteral`: the outer literal can know +its expected element type is `[]T`, and the loop sets `self.target_type = +elem_ty` while lowering each inner literal, but it appends the returned value +directly. For an inner `.[...]`, that returned value is still an array aggregate +`[N]T`, not the target `[]T` slice. Add per-element coercion/materialization +after lowering each element, using the element source type and expected +`elem_ty` (the existing `array_to_slice` coercion should be reused). Verify the +repro prints `local=5` and `direct=5`, then run `zig build && zig build test && +bash tests/run_examples.sh`. diff --git a/src/ir/calls.zig b/src/ir/calls.zig index 25302b0..1af5687 100644 --- a/src/ir/calls.zig +++ b/src/ir/calls.zig @@ -264,7 +264,7 @@ pub const CallResolver = struct { if (method_fd.body.data == .compiler_expr) { return .{ .kind = .struct_method, - .return_type = if (method_fd.return_type) |rt| type_bridge.resolveAstType(rt, &self.l.module.types, &self.l.program_index.type_alias_map) else .void, + .return_type = if (method_fd.return_type) |rt| type_bridge.resolveAstType(rt, &self.l.module.types, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map) else .void, .target = .{ .named = qualified }, .prepends_receiver = true, .expands_defaults = defaultsFor(method_fd, c.args.len + 1), diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 1558d7f..5045547 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -560,9 +560,9 @@ pub const Lowering = struct { } else if (cd.value.data == .struct_decl) { self.registerStructDecl(&cd.value.data.struct_decl); } else if (cd.value.data == .enum_decl) { - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } else if (cd.value.data == .union_decl) { - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } else if (cd.value.data == .comptime_expr) { self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation); } @@ -574,10 +574,10 @@ pub const Lowering = struct { self.registerStructDecl(&sd); }, .enum_decl => { - _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, .union_decl => { - _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, .error_set_decl => { self.registerErrorSetDecl(decl); @@ -675,10 +675,10 @@ pub const Lowering = struct { self.registerStructDecl(&cd.value.data.struct_decl); } else if (cd.value.data == .enum_decl) { // Register enum/tagged-union types in the type table - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } else if (cd.value.data == .union_decl) { // Register plain union types in the type table - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } else if (cd.value.data == .type_expr or cd.value.data == .pointer_type_expr or cd.value.data == .many_pointer_type_expr or @@ -688,7 +688,7 @@ pub const Lowering = struct { cd.value.data == .function_type_expr) { // Type alias: MyFloat :: f64; Ptr :: *u8; Cb :: (s32) -> s32; - const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map); + const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); self.program_index.type_alias_map.put(cd.name, target_ty) catch {}; } else if (cd.value.data == .identifier) { // Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide; @@ -771,7 +771,7 @@ pub const Lowering = struct { // resolve via type_bridge and register the result // under the alias name so `Vec4` in expression // position can `const_type()`. - const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map); + const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); if (result_ty != .void and result_ty != .unresolved) { self.program_index.type_alias_map.put(cd.name, result_ty) catch {}; } @@ -806,11 +806,11 @@ pub const Lowering = struct { }, .enum_decl => { // Register enum/tagged-union types in the type table - _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, .union_decl => { // Register plain union types in the type table - _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, .error_set_decl => { self.registerErrorSetDecl(decl); @@ -1813,7 +1813,7 @@ pub const Lowering = struct { // Block-local type declarations .struct_decl => |sd| self.registerStructDecl(&sd), .enum_decl, .union_decl => { - _ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, .error_set_decl => self.registerErrorSetDecl(node), .ufcs_alias => |ua| { @@ -1972,7 +1972,7 @@ pub const Lowering = struct { return; } if (cd.value.data == .enum_decl or cd.value.data == .union_decl) { - _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); return; } @@ -2912,7 +2912,7 @@ pub const Lowering = struct { // `t : Type = f64;` store a real TypeId; lets // `t == f64` icmp at runtime against the same TypeId. if (self.isKnownTypeName(te.name)) { - const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map); + const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); break :blk self.builder.constType(ty); } break :blk self.emitError(te.name, node.span); @@ -5357,8 +5357,26 @@ pub const Lowering = struct { for (al.elements) |elem| { const old_tt = self.target_type; self.target_type = elem_ty; - const val = self.lowerExpr(elem); + var val = self.lowerExpr(elem); self.target_type = old_tt; + // A nested `.[...]` element at a slice element type lowers to an + // aggregate array `[N]U` (lowerArrayLiteral always yields an array + // value); materialize it into a `[]U` slice so the element is a real + // {ptr,len} header rather than a raw array the callee would read its + // header off of (issue 0085). This per-element coercion recurses with + // the literal nesting, so `[][]T` and deeper coerce at every level. + if (!elem_ty.isBuiltin()) { + const ei = self.module.types.get(elem_ty); + if (ei == .slice) { + const val_ty = self.builder.getRefType(val); + if (!val_ty.isBuiltin()) { + const vi = self.module.types.get(val_ty); + if (vi == .array and vi.array.element == ei.slice.element) { + val = self.coerceToType(val, val_ty, elem_ty); + } + } + } + } elems.append(self.alloc, val) catch unreachable; } @@ -5401,7 +5419,7 @@ pub const Lowering = struct { const name_id = self.module.types.internString(id.name); return self.module.types.findByName(name_id) orelse .unresolved; }, - .type_expr => return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map), + .type_expr => return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map), .field_access => |fa| { // Module.Type — try to resolve the field as a type name const name_id = self.module.types.internString(fa.field); @@ -6875,7 +6893,7 @@ pub const Lowering = struct { // Check for #compiler free functions if (self.program_index.fn_ast_map.get(func_name)) |fd_check| { if (fd_check.body.data == .compiler_expr) { - const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map) else TypeId.void; + const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) else TypeId.void; return self.builder.compilerCall(func_name, args.items, ret_ty); } } @@ -7230,7 +7248,7 @@ pub const Lowering = struct { if (self.program_index.fn_ast_map.get(qualified)) |method_fd| { if (method_fd.body.data == .compiler_expr) { const ret_ty = if (method_fd.return_type) |rt| - type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map) + type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) else .void; return self.builder.compilerCall(qualified, method_args.items, ret_ty); @@ -7668,7 +7686,7 @@ pub const Lowering = struct { const ret_ty = blk: { if (lam.return_type) |rt| { - break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map); + break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } // Use target closure return type if available — but only when it's // a resolved type. An `.unresolved` ret comes from an unbound @@ -8238,7 +8256,7 @@ pub const Lowering = struct { } fn resolveReturnType2(self: *Lowering, rt: ?*const Node) TypeId { - if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types, &self.program_index.type_alias_map); + if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); return .void; } @@ -9278,8 +9296,8 @@ pub const Lowering = struct { const ret_ty: TypeId = blk: { if (fd.return_type) |rt| { if (rt.data == .type_expr) { - if (type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map) != .unresolved) { - break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map); + if (type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) != .unresolved) { + break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } } } @@ -10551,7 +10569,7 @@ pub const Lowering = struct { return .unresolved; } } - return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map); + return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { @@ -10612,7 +10630,7 @@ pub const Lowering = struct { }, .type_expr => |te| { if (self.program_index.type_alias_map.get(te.name)) |alias_ty| return alias_ty; - return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map); + return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, .call => |cl| { // `type_of(x)` resolves to `inferExprType(x)` at lower @@ -10637,7 +10655,7 @@ pub const Lowering = struct { .slice_type_expr, .optional_type_expr, .function_type_expr, - => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map), + => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map), else => return .unresolved, } } @@ -11764,7 +11782,7 @@ pub const Lowering = struct { // literal (`(s32, s32)`); validate its elements are types and reject // non-type elements loudly (issue 0067). .tuple_literal => return self.resolveTupleLiteralTypeArg(node), - else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map), + else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map), } } @@ -12195,7 +12213,7 @@ pub const Lowering = struct { } return; } - _ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map); + _ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl) void { @@ -12341,7 +12359,7 @@ pub const Lowering = struct { if (const_node.data == .const_decl) { const cd = const_node.data.const_decl; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue; - const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map) else null; + const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map, &self.program_index.module_const_map) else null; self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {}; } } diff --git a/src/ir/protocols.zig b/src/ir/protocols.zig index d85cc1d..c5f719c 100644 --- a/src/ir/protocols.zig +++ b/src/ir/protocols.zig @@ -296,7 +296,7 @@ pub const ProtocolResolver = struct { if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) { break :blk void_ptr_ty; } - break :blk type_bridge.resolveAstType(p, table, &self.l.program_index.type_alias_map); + break :blk type_bridge.resolveAstType(p, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map); }; ptypes.append(self.l.alloc, pty) catch unreachable; } @@ -306,7 +306,7 @@ pub const ProtocolResolver = struct { ret_is_self = true; break :blk void_ptr_ty; } - break :blk type_bridge.resolveAstType(rt, table, &self.l.program_index.type_alias_map); + break :blk type_bridge.resolveAstType(rt, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map); } else .void; method_infos.append(self.l.alloc, .{ .name = method.name, @@ -393,7 +393,7 @@ pub const ProtocolResolver = struct { // Resolve the protocol's type-arg list to concrete TypeIds. var arg_tys = std.ArrayList(TypeId).empty; for (ib.protocol_type_args) |arg_node| { - const t = type_bridge.resolveAstType(arg_node, table, &self.l.program_index.type_alias_map); + const t = type_bridge.resolveAstType(arg_node, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map); arg_tys.append(self.l.alloc, t) catch return; } @@ -401,9 +401,9 @@ pub const ProtocolResolver = struct { // parameterised impls (back-compat `target_type` string is kept for // simple cases but the canonical form is the TypeExpr). const src_ty: TypeId = if (ib.target_type_expr) |te| - type_bridge.resolveAstType(te, table, &self.l.program_index.type_alias_map) + type_bridge.resolveAstType(te, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map) else if (ib.target_type.len > 0) - type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.l.program_index.type_alias_map) + type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map) else return; diff --git a/src/ir/type_bridge.test.zig b/src/ir/type_bridge.test.zig index 571eea4..c2cb411 100644 --- a/src/ir/type_bridge.test.zig +++ b/src/ir/type_bridge.test.zig @@ -3,6 +3,8 @@ const std = @import("std"); const types = @import("types.zig"); const type_bridge = @import("type_bridge.zig"); const ast = @import("../ast.zig"); +const program_index_mod = @import("program_index.zig"); +const ModuleConstInfo = program_index_mod.ModuleConstInfo; const Node = ast.Node; const TypeId = types.TypeId; @@ -18,7 +20,7 @@ test "resolveAstType: primitive type_expr" { defer alloc.destroy(node); node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "f64" } } }; - try std.testing.expectEqual(TypeId.f64, type_bridge.resolveAstType(node, &table, null)); + try std.testing.expectEqual(TypeId.f64, type_bridge.resolveAstType(node, &table, null, null)); } test "resolveAstType: pointer type" { @@ -34,7 +36,7 @@ test "resolveAstType: pointer type" { defer alloc.destroy(node); node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = inner } } }; - const id = type_bridge.resolveAstType(node, &table, null); + const id = type_bridge.resolveAstType(node, &table, null, null); try std.testing.expectEqual(TypeInfo{ .pointer = .{ .pointee = .s32 } }, table.get(id)); } @@ -55,7 +57,7 @@ test "resolveAstType: optional slice" { defer alloc.destroy(opt); opt.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .optional_type_expr = .{ .inner_type = slice } } }; - const id = type_bridge.resolveAstType(opt, &table, null); + const id = type_bridge.resolveAstType(opt, &table, null, null); const info = table.get(id); switch (info) { .optional => |o| { @@ -71,7 +73,7 @@ test "resolveAstType: null surfaces as .unresolved (no silent s64 default)" { var table = TypeTable.init(alloc); defer table.deinit(); - try std.testing.expectEqual(TypeId.unresolved, type_bridge.resolveAstType(null, &table, null)); + try std.testing.expectEqual(TypeId.unresolved, type_bridge.resolveAstType(null, &table, null, null)); } test "resolveAstType: threaded alias_map resolves named alias" { @@ -85,7 +87,7 @@ test "resolveAstType: threaded alias_map resolves named alias" { defer alloc.destroy(sh_node); sh_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "ShaderHandle" } } }; - const empty_stub = type_bridge.resolveAstType(sh_node, &table, null); + const empty_stub = type_bridge.resolveAstType(sh_node, &table, null, null); const empty_info = table.get(empty_stub); try std.testing.expectEqual(@as(std.meta.Tag(TypeInfo), .@"struct"), std.meta.activeTag(empty_info)); try std.testing.expectEqual(@as(usize, 0), empty_info.@"struct".fields.len); @@ -102,7 +104,7 @@ test "resolveAstType: threaded alias_map resolves named alias" { defer alloc.destroy(opaque_node); opaque_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "Opaque" } } }; try aliases.put("Opaque", .u64); - try std.testing.expectEqual(TypeId.u64, type_bridge.resolveAstType(opaque_node, &table, &aliases)); + try std.testing.expectEqual(TypeId.u64, type_bridge.resolveAstType(opaque_node, &table, &aliases, null)); // Compound forms (`*Opaque`, `[]Opaque`, `?Opaque`) route through recursive // helpers that thread the same alias_map at every step. @@ -112,10 +114,42 @@ test "resolveAstType: threaded alias_map resolves named alias" { const ptr_node = try alloc.create(Node); defer alloc.destroy(ptr_node); ptr_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = opaque_inner } } }; - const ptr_id = type_bridge.resolveAstType(ptr_node, &table, &aliases); + const ptr_id = type_bridge.resolveAstType(ptr_node, &table, &aliases, null); try std.testing.expectEqual(TypeInfo{ .pointer = .{ .pointee = .u64 } }, table.get(ptr_id)); } +test "resolveAstType: named-const array dimension resolves to the same length as a literal (issue 0083)" { + const alloc = std.testing.allocator; + var table = TypeTable.init(alloc); + defer table.deinit(); + + // `N :: 4` in the module-const table, value backed by an int-literal node. + const n_val = try alloc.create(Node); + defer alloc.destroy(n_val); + n_val.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 4 } } }; + var consts = std.StringHashMap(ModuleConstInfo).init(alloc); + defer consts.deinit(); + try consts.put("N", .{ .value = n_val, .ty = .s64 }); + + // `[N]s64` — dimension is the named const `N`, not a literal. + const elem = try alloc.create(Node); + defer alloc.destroy(elem); + elem.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64" } } }; + const len_node = try alloc.create(Node); + defer alloc.destroy(len_node); + len_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "N" } } }; + const arr = try alloc.create(Node); + defer alloc.destroy(arr); + arr.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .array_type_expr = .{ .length = len_node, .element_type = elem } } }; + + // With the const table threaded, `[N]s64` lays out identically to `[4]s64`. + const id = type_bridge.resolveAstType(arr, &table, null, &consts); + const info = table.get(id); + try std.testing.expect(info == .array); + try std.testing.expectEqual(TypeId.s64, info.array.element); + try std.testing.expectEqual(@as(u32, 4), info.array.length); +} + test "resolveAstType: error_set_decl registers an error-set type + interns tags" { const alloc = std.testing.allocator; var table = TypeTable.init(alloc); @@ -129,7 +163,7 @@ test "resolveAstType: error_set_decl registers an error-set type + interns tags" .tag_names = &tag_names, } } }; - const id = type_bridge.resolveAstType(node, &table, null); + const id = type_bridge.resolveAstType(node, &table, null, null); const info = table.get(id); try std.testing.expect(info == .error_set); try std.testing.expectEqualStrings("ParseErr", table.getString(info.error_set.name)); @@ -137,7 +171,7 @@ test "resolveAstType: error_set_decl registers an error-set type + interns tags" // Tags were interned into the global pool (round-trip a name through it). try std.testing.expectEqualStrings("BadDigit", table.getTagName(table.internTag("BadDigit"))); // Re-resolving the same decl dedups to the same TypeId. - try std.testing.expectEqual(id, type_bridge.resolveAstType(node, &table, null)); + try std.testing.expectEqual(id, type_bridge.resolveAstType(node, &table, null, null)); } // ── ERR E1.2 — failable-signature error channel resolution ── @@ -154,7 +188,7 @@ test "resolveAstType: `!Named` resolves to the declared error set" { const node = try alloc.create(Node); defer alloc.destroy(node); node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = "ParseErr" } } }; - try std.testing.expectEqual(set, type_bridge.resolveAstType(node, &table, null)); + try std.testing.expectEqual(set, type_bridge.resolveAstType(node, &table, null, null)); } test "resolveAstType: bare `!` resolves to a shared inferred placeholder set" { @@ -169,8 +203,8 @@ test "resolveAstType: bare `!` resolves to a shared inferred placeholder set" { defer alloc.destroy(b); b.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = null } } }; - const ia = type_bridge.resolveAstType(a, &table, null); - const ib = type_bridge.resolveAstType(b, &table, null); + const ia = type_bridge.resolveAstType(a, &table, null, null); + const ib = type_bridge.resolveAstType(b, &table, null, null); try std.testing.expect(table.get(ia) == .error_set); try std.testing.expectEqualStrings("!", table.getString(table.get(ia).error_set.name)); try std.testing.expectEqual(@as(usize, 0), table.get(ia).error_set.tags.len); // empty until E1.4 SCC @@ -198,7 +232,7 @@ test "resolveAstType: `(s32, !Named)` result list is a tuple ending in the error defer alloc.destroy(tuple); tuple.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .tuple_type_expr = .{ .field_types = &fields, .field_names = null } } }; - const id = type_bridge.resolveAstType(tuple, &table, null); + const id = type_bridge.resolveAstType(tuple, &table, null, null); const info = table.get(id); try std.testing.expect(info == .tuple); try std.testing.expectEqual(@as(usize, 2), info.tuple.fields.len); diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index 00711f2..fc05de9 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -8,6 +8,8 @@ const TypeInfo = ir_types.TypeInfo; const TypeTable = ir_types.TypeTable; const StringId = ir_types.StringId; const type_resolver = @import("type_resolver.zig"); +const program_index_mod = @import("program_index.zig"); +const ModuleConstInfo = program_index_mod.ModuleConstInfo; /// The single-source type-alias table (`ProgramIndex.type_alias_map`), threaded /// explicitly through every name-resolving entry point so a bare name like @@ -17,6 +19,15 @@ const type_resolver = @import("type_resolver.zig"); /// `null` for contexts that never see aliases, e.g. unit tests). pub const AliasMap = ?*const std.StringHashMap(TypeId); +/// The module-global constant table (`ProgramIndex.module_const_map`), threaded +/// alongside the alias map so a named-const array dimension (`N :: 16; [N]T`) +/// resolves to the same length as a literal dimension on EVERY registration-time +/// path — type aliases (`Arr :: [N]T`), inline union/enum field types — not just +/// the stateful body-lowering path. Without it the stateless dim resolver had no +/// way to evaluate a named const and silently fabricated a 0 length (issue 0083). +/// `null` for contexts with no const table (e.g. unit tests). +pub const ConstMap = ?*const std.StringHashMap(ModuleConstInfo); + /// Binding-free element-recursion adapter for `TypeResolver.resolveCompound`: /// nested element types resolve through `type_bridge.resolveAstType` (the /// registration-time path — no generic/pack bindings). Lets type_bridge reuse @@ -25,20 +36,35 @@ pub const AliasMap = ?*const std.StringHashMap(TypeId); const StatelessInner = struct { table: *TypeTable, alias_map: AliasMap, + consts: ConstMap, pub fn resolveInner(self: StatelessInner, node: *const Node) TypeId { - return resolveAstType(node, self.table, self.alias_map); + return resolveAstType(node, self.table, self.alias_map, self.consts); } - /// Fixed-array dimension at registration time (no bindings / const tables). - /// Only a literal dimension is knowable here; a named-const dimension - /// (`N :: 16; [N]T`) is resolved by the stateful caller - /// (`Lowering.resolveArrayLen`) before it ever reaches this binding-free - /// path — mirroring how `pack_index_type_expr` is handled stateful-first. + /// Fixed-array dimension at registration time: a literal `[16]T`, or a + /// named module-global const `N :: 16; [N]T` looked up in the const table. + /// Both yield the SAME length — registration-time paths (aliases, inline + /// union/enum fields) must lay out a named-const dim identically to a literal + /// (issue 0083). A dimension that is neither is not resolvable on this + /// binding-free path (it would be a computed/comptime expression, which the + /// stateful body-lowering path diagnoses as a hard error at the storage + /// site); bail LOUDLY rather than fabricating a 0 length that silently gives a + /// 0-byte array and out-of-bounds element access. pub fn resolveArrayLen(self: StatelessInner, len_node: *const Node) u32 { - _ = self; - return switch (len_node.data) { - .int_literal => |lit| @intCast(lit.value), - else => 0, - }; + switch (len_node.data) { + .int_literal => |lit| return @intCast(lit.value), + .identifier => |id| if (self.namedConstLen(id.name)) |n| return n, + .type_expr => |te| if (self.namedConstLen(te.name)) |n| return n, + else => {}, + } + std.debug.print("type_bridge: array dimension is not a literal or named integer constant — cannot resolve length at registration time (computed/comptime dimensions are unsupported here)\n", .{}); + return 0; + } + /// A name that resolves to a module-global integer constant → its value. + fn namedConstLen(self: StatelessInner, name: []const u8) ?u32 { + const consts = self.consts orelse return null; + const ci = consts.get(name) orelse return null; + if (ci.value.data == .int_literal) return @intCast(ci.value.data.int_literal.value); + return null; } }; @@ -46,14 +72,14 @@ const StatelessInner = struct { // Resolve an AST type node into an IR TypeId. Used during lowering when // we only have the parsed AST (no codegen type registry). -pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap) TypeId { +pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { // A null node means a caller reached type resolution without a type node. // Every current caller either passes a non-optional node or handles the // "no type" case itself (returning `.void`), so this is a caller bug — and // `.s64` here would silently fabricate an 8-byte int. Surface it via the // `.unresolved` sentinel (trips the sizeOf/toLLVMType panic at codegen). const n = node orelse return .unresolved; - const si = StatelessInner{ .table = table, .alias_map = alias_map }; + const si = StatelessInner{ .table = table, .alias_map = alias_map, .consts = consts }; return switch (n.data) { .type_expr => |te| resolveTypeName(te.name, table, alias_map), .identifier => |id| resolveTypeName(id.name, table, alias_map), @@ -76,8 +102,8 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap // `Closure(..p)` field type at registration time). These tiny fallbacks // are the only stateless-specific shape code left; the stateful expand // lives in PackResolver. - .closure_type_expr => |ct| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveClosurePackShape(&ct, table, alias_map), - .tuple_type_expr => |tt| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveTupleSpreadShape(&tt, table, alias_map), + .closure_type_expr => |ct| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveClosurePackShape(&ct, table, alias_map, consts), + .tuple_type_expr => |tt| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveTupleSpreadShape(&tt, table, alias_map, consts), .pack_index_type_expr => { // Pack-index `$args[N]` in a type position must be resolved // against an active pack binding — `type_bridge` has no access @@ -90,8 +116,8 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap std.debug.print("type_bridge: pack-index type expression encountered outside a pack-aware context — returning .unresolved\n", .{}); return .unresolved; }, - .tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table, alias_map), - .parameterized_type_expr => |pt| resolveParameterizedType(&pt, table, alias_map), + .tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table, alias_map, consts), + .parameterized_type_expr => |pt| resolveParameterizedType(&pt, table, alias_map, consts), // An unannotated param. Its type must be resolved from context // (contextual closure typing, generic binding, or pack substitution) // *before* reaching here; if it doesn't, returning a plausible `.s64` @@ -101,9 +127,9 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap // turns it into a real diagnostic. .inferred_type => .unresolved, // Inline type declarations (used as field types) - .enum_decl => |ed| resolveInlineEnum(&ed, table, alias_map), - .struct_decl => |sd| resolveInlineStruct(&sd, table, alias_map), - .union_decl => |ud| resolveInlineUnion(&ud, table, alias_map), + .enum_decl => |ed| resolveInlineEnum(&ed, table, alias_map, consts), + .struct_decl => |sd| resolveInlineStruct(&sd, table, alias_map, consts), + .union_decl => |ud| resolveInlineUnion(&ud, table, alias_map, consts), .error_set_decl => |esd| resolveInlineErrorSet(&esd, table), .error_type_expr => |ete| resolveErrorType(&ete, table, alias_map), else => { @@ -137,13 +163,13 @@ pub const resolveTypePrimitive = type_resolver.TypeResolver.resolvePrimitive; /// null). type_bridge can't expand the pack (no state), so it preserves the /// pack SHAPE — a `closureTypePack` whose prefix is the fixed params. The /// stateful expand lives in `PackResolver.resolveClosureTypeWithBindings`. -fn resolveClosurePackShape(ct: *const ast.ClosureTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { +fn resolveClosurePackShape(ct: *const ast.ClosureTypeExpr, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { const alloc = table.alloc; var param_ids = std.ArrayList(TypeId).empty; for (ct.param_types) |pt| { - param_ids.append(alloc, resolveAstType(pt, table, alias_map)) catch unreachable; + param_ids.append(alloc, resolveAstType(pt, table, alias_map, consts)) catch unreachable; } - const ret_id = if (ct.return_type) |rt| resolveAstType(rt, table, alias_map) else TypeId.void; + const ret_id = if (ct.return_type) |rt| resolveAstType(rt, table, alias_map, consts) else TypeId.void; return table.closureTypePack(param_ids.items, ret_id, @intCast(param_ids.items.len)); } @@ -152,11 +178,11 @@ fn resolveClosurePackShape(ct: *const ast.ClosureTypeExpr, table: *TypeTable, al /// each field resolves individually (a spread field is not a type → resolves to /// `.unresolved`). The stateful expand lives in /// `PackResolver.resolveTupleTypeWithBindings`. -fn resolveTupleSpreadShape(tt: *const ast.TupleTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { +fn resolveTupleSpreadShape(tt: *const ast.TupleTypeExpr, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { const alloc = table.alloc; var field_ids = std.ArrayList(TypeId).empty; for (tt.field_types) |ft| { - field_ids.append(alloc, resolveAstType(ft, table, alias_map)) catch unreachable; + field_ids.append(alloc, resolveAstType(ft, table, alias_map, consts)) catch unreachable; } var name_ids: ?[]const StringId = null; if (tt.field_names) |names| { @@ -182,14 +208,14 @@ fn resolveTupleSpreadShape(tt: *const ast.TupleTypeExpr, table: *TypeTable, alia // here, so the valid path below builds the tuple and the invalid path never // reaches it from lowering. The sentinel is the backstop for any other // (binding-free) caller. -fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable, alias_map: AliasMap) TypeId { +fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { const alloc = table.alloc; var field_ids = std.ArrayList(TypeId).empty; var name_ids_list = std.ArrayList(StringId).empty; var any_named = false; for (tl.elements) |el| { if (!isTypeShapedAstNode(el.value, table)) return .unresolved; - field_ids.append(alloc, resolveAstType(el.value, table, alias_map)) catch unreachable; + field_ids.append(alloc, resolveAstType(el.value, table, alias_map, consts)) catch unreachable; if (el.name) |n| { any_named = true; name_ids_list.append(alloc, table.internString(n)) catch unreachable; @@ -233,7 +259,7 @@ pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool { }; } -fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId { +fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { // Strip module prefix (e.g. "std.Vector" → "Vector") const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name; // Vector(N, T) is a built-in parameterized type @@ -243,7 +269,7 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa .int_literal => |lit| @intCast(@as(u64, @bitCast(lit.value))), else => 0, }; - const elem = resolveAstType(pt.args[1], table, alias_map); + const elem = resolveAstType(pt.args[1], table, alias_map, consts); return table.vectorOf(elem, length); } } @@ -254,7 +280,7 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa // ── Inline type declarations ───────────────────────────────────────── -fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap) TypeId { +fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { const alloc = table.alloc; const name_id = table.internString(ed.name); @@ -280,7 +306,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia } else { var sfields = std.ArrayList(TypeInfo.StructInfo.Field).empty; for (sd.field_names, sd.field_types) |fname, ftype_node| { - const fty = resolveAstType(ftype_node, table, alias_map); + const fty = resolveAstType(ftype_node, table, alias_map, consts); sfields.append(alloc, .{ .name = table.internString(fname), .ty = fty, @@ -294,10 +320,10 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia table.update(field_ty, sinfo); } } else { - field_ty = resolveAstType(vt, table, alias_map); + field_ty = resolveAstType(vt, table, alias_map, consts); } } else { - field_ty = resolveAstType(vt, table, alias_map); + field_ty = resolveAstType(vt, table, alias_map, consts); } } } @@ -311,7 +337,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia var backing_type: ?TypeId = null; var tag_type: ?TypeId = null; if (ed.backing_type) |bt| { - const backing_ty = resolveAstType(bt, table, alias_map); + const backing_ty = resolveAstType(bt, table, alias_map, consts); backing_type = backing_ty; // Extract tag type from first field of backing struct const backing_info = table.get(backing_ty); @@ -394,7 +420,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia if (ed.backing_type) |bt| { // Only use simple backing types (u8, u16, u32, etc.), not struct backing (enum struct) if (bt.data != .struct_decl) { - enum_backing = resolveAstType(bt, table, alias_map); + enum_backing = resolveAstType(bt, table, alias_map, consts); } } @@ -410,7 +436,7 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable, alias_map: Alia return id; } -fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: AliasMap) TypeId { +fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { const alloc = table.alloc; const name_id = table.internString(sd.name); @@ -418,7 +444,7 @@ fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty; for (sd.field_names, sd.field_types) |fname, ftype_node| { - const field_ty = resolveAstType(ftype_node, table, alias_map); + const field_ty = resolveAstType(ftype_node, table, alias_map, consts); fields.append(alloc, .{ .name = table.internString(fname), .ty = field_ty, @@ -433,7 +459,7 @@ fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable, alias_map: return id; } -fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap) TypeId { +fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: AliasMap, consts: ConstMap) TypeId { const alloc = table.alloc; const name_id = table.internString(ud.name); @@ -441,7 +467,7 @@ fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable, alias_map: Al var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty; for (ud.field_names, ud.field_types) |fname, ftype_node| { - const field_ty = resolveAstType(ftype_node, table, alias_map); + const field_ty = resolveAstType(ftype_node, table, alias_map, consts); fields.append(alloc, .{ .name = table.internString(fname), .ty = field_ty,