From 156edf8e28f08755cf0a21e6712c585f6d1f8946 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 5 Jun 2026 07:17:20 +0300 Subject: [PATCH] fix(ir): reject typed module const whose initializer mismatches annotation [F0.7] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A typed module-level constant whose initializer did not match its annotation was silently accepted: `N : string : 4` compiled, then `print(N)` segfaulted (an integer emitted as a `string` const → a bogus pointer) and `[N]s64` folded `N` to 4 as an integer count. Issue 0088. Root cause: `registerTypedModuleConst` stored the annotation type but never validated the initializer literal against it, and `program_index.moduleConstInt` folded a const into a count by inspecting the initializer node alone, ignoring `ModuleConstInfo.ty`. Fix at the declaration (kills both symptoms): - lower.zig: `registerTypedModuleConst` now validates the initializer via `typedConstInitFits` (arms mirror `emitModuleConst`'s faithful-emit precondition: int→int/float, float→float, bool→bool, string→string, null→pointer/optional, `---`→any). A mismatch emits a `type mismatch` diagnostic at the initializer span and does not register the const (also evicting the pass-0 placeholder). Not routed through `coercionResolver().classify`: that runtime-coercion planner is unsound here (null's natural type is void → false-rejects `*T`; bool is 1 bit → false-accepts s64). - program_index.zig: `moduleConstInt` now takes the `TypeTable` and gates the fold on `isCountableConstType(ci.ty)` (integer of any width, or a float), so a non-numeric typed const can never fold into a count off its initializer node. Callers in lower.zig and type_bridge.zig updated. Regression: - examples/1143-diagnostics-typed-module-const-mismatch.sx (negative, exit 1) - examples/0162-types-typed-module-const-roundtrip.sx (positive) - program_index.test.zig: gate-on-declared-type unit test Docs: specs.md §3 Constant Binding + readme.md note the compatibility rule. --- ...0162-types-typed-module-const-roundtrip.sx | 35 +++++++ ...diagnostics-typed-module-const-mismatch.sx | 20 ++++ ...62-types-typed-module-const-roundtrip.exit | 1 + ...-types-typed-module-const-roundtrip.stderr | 1 + ...-types-typed-module-const-roundtrip.stdout | 4 + ...agnostics-typed-module-const-mismatch.exit | 1 + ...nostics-typed-module-const-mismatch.stderr | 23 +++++ ...nostics-typed-module-const-mismatch.stdout | 1 + ...-typed-module-const-annotation-mismatch.md | 93 +++++++++++++++++++ readme.md | 5 + specs.md | 7 ++ src/ir/lower.zig | 79 +++++++++++++++- src/ir/program_index.test.zig | 40 ++++++-- src/ir/program_index.zig | 30 +++++- src/ir/type_bridge.zig | 2 +- 15 files changed, 326 insertions(+), 16 deletions(-) create mode 100644 examples/0162-types-typed-module-const-roundtrip.sx create mode 100644 examples/1143-diagnostics-typed-module-const-mismatch.sx create mode 100644 examples/expected/0162-types-typed-module-const-roundtrip.exit create mode 100644 examples/expected/0162-types-typed-module-const-roundtrip.stderr create mode 100644 examples/expected/0162-types-typed-module-const-roundtrip.stdout create mode 100644 examples/expected/1143-diagnostics-typed-module-const-mismatch.exit create mode 100644 examples/expected/1143-diagnostics-typed-module-const-mismatch.stderr create mode 100644 examples/expected/1143-diagnostics-typed-module-const-mismatch.stdout create mode 100644 issues/0088-typed-module-const-annotation-mismatch.md diff --git a/examples/0162-types-typed-module-const-roundtrip.sx b/examples/0162-types-typed-module-const-roundtrip.sx new file mode 100644 index 0000000..d899733 --- /dev/null +++ b/examples/0162-types-typed-module-const-roundtrip.sx @@ -0,0 +1,35 @@ +// Valid typed module-level constants compile, fold, and print correctly across +// every initializer/annotation pairing the registrar accepts: +// - integer → integer (`K : s64 : 4`) — usable as an array count too +// - integer → float (`W : f32 : 800`) +// - float → float (`PI : f32 : 3.14159`) +// - string → string (`S : string : "hi"`) +// - null → pointer (`P : *void : null`) +// +// Companion to the negative example 1143: the issue-0088 fix rejects a typed +// const whose initializer mismatches its annotation, and these correctly-typed +// consts must keep working (no over-rejection). +#import "modules/std.sx"; + +K : s64 : 4; +W : f32 : 800; +PI : f32 : 3.14159; +S : string : "hi"; +P : *void : null; + +main :: () { + // Integer const: prints AND drives an array dimension (len 4). + a : [K]s64 = ---; + a[0] = 10; + a[3] = 40; + print("K={} len={} a0={} a3={}\n", K, a.len, a[0], a[3]); + + // Integer-into-float and float consts print as floats. + print("W={} PI={}\n", W, PI); + + // String const prints its text. + print("S={}\n", S); + + // Null pointer const is null. + print("P_is_null={}\n", P == null); +} diff --git a/examples/1143-diagnostics-typed-module-const-mismatch.sx b/examples/1143-diagnostics-typed-module-const-mismatch.sx new file mode 100644 index 0000000..afbb2ed --- /dev/null +++ b/examples/1143-diagnostics-typed-module-const-mismatch.sx @@ -0,0 +1,20 @@ +// A typed module-level constant whose initializer does not match its +// annotation is a compile-time type error — not a silently-accepted const. +// Each declaration below pairs a literal with an incompatible annotation, so +// the compiler must emit a `type mismatch` diagnostic at the initializer and +// abort (exit 1) rather than registering a usable const. +// +// Regression (issue 0088): `N : string : 4` was accepted; `print(N)` then +// segfaulted (an integer emitted as a `string` const → a bogus pointer) and +// `[N]s64` folded `N` to 4. The fix rejects the declaration at the root. + +#import "modules/std.sx"; + +N : string : 4; // integer literal where a string is annotated +F : s64 : "x"; // string literal where an integer is annotated +B : s64 : true; // boolean literal where an integer is annotated +G : s64 : 1.5; // float literal where an integer is annotated + +main :: () { + print("unreachable\n"); +} diff --git a/examples/expected/0162-types-typed-module-const-roundtrip.exit b/examples/expected/0162-types-typed-module-const-roundtrip.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0162-types-typed-module-const-roundtrip.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0162-types-typed-module-const-roundtrip.stderr b/examples/expected/0162-types-typed-module-const-roundtrip.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0162-types-typed-module-const-roundtrip.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0162-types-typed-module-const-roundtrip.stdout b/examples/expected/0162-types-typed-module-const-roundtrip.stdout new file mode 100644 index 0000000..379f4df --- /dev/null +++ b/examples/expected/0162-types-typed-module-const-roundtrip.stdout @@ -0,0 +1,4 @@ +K=4 len=4 a0=10 a3=40 +W=800.000000 PI=3.141590 +S=hi +P_is_null=true diff --git a/examples/expected/1143-diagnostics-typed-module-const-mismatch.exit b/examples/expected/1143-diagnostics-typed-module-const-mismatch.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1143-diagnostics-typed-module-const-mismatch.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1143-diagnostics-typed-module-const-mismatch.stderr b/examples/expected/1143-diagnostics-typed-module-const-mismatch.stderr new file mode 100644 index 0000000..dbb4b57 --- /dev/null +++ b/examples/expected/1143-diagnostics-typed-module-const-mismatch.stderr @@ -0,0 +1,23 @@ +error: type mismatch: constant 'N' is declared 'string' but its initializer is an integer literal + --> examples/1143-diagnostics-typed-module-const-mismatch.sx:13:14 + | +13 | N : string : 4; // integer literal where a string is annotated + | ^ + +error: type mismatch: constant 'F' is declared 's64' but its initializer is a string literal + --> examples/1143-diagnostics-typed-module-const-mismatch.sx:14:14 + | +14 | F : s64 : "x"; // string literal where an integer is annotated + | ^^^ + +error: type mismatch: constant 'B' is declared 's64' but its initializer is a boolean literal + --> examples/1143-diagnostics-typed-module-const-mismatch.sx:15:14 + | +15 | B : s64 : true; // boolean literal where an integer is annotated + | ^^^^ + +error: type mismatch: constant 'G' is declared 's64' but its initializer is a float literal + --> examples/1143-diagnostics-typed-module-const-mismatch.sx:16:14 + | +16 | G : s64 : 1.5; // float literal where an integer is annotated + | ^^^ diff --git a/examples/expected/1143-diagnostics-typed-module-const-mismatch.stdout b/examples/expected/1143-diagnostics-typed-module-const-mismatch.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1143-diagnostics-typed-module-const-mismatch.stdout @@ -0,0 +1 @@ + diff --git a/issues/0088-typed-module-const-annotation-mismatch.md b/issues/0088-typed-module-const-annotation-mismatch.md new file mode 100644 index 0000000..be8948b --- /dev/null +++ b/issues/0088-typed-module-const-annotation-mismatch.md @@ -0,0 +1,93 @@ +> **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). +> +> **Root cause.** `registerTypedModuleConst` (`src/ir/lower.zig`) stored the +> annotation type on the const but never checked the initializer literal against +> it, so `N : string : 4` registered as `{value = int 4, ty = string}`. +> `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). +> +> **Fix per file.** +> - `src/ir/lower.zig` — `registerTypedModuleConst` now validates the +> initializer against the resolved annotation via the new `typedConstInitFits` +> (arms mirror `emitModuleConst`'s faithful-emit precondition: int → int/float, +> float → float, bool → bool, string → string, null → pointer/optional, +> `---` → any). A mismatch emits `type mismatch: constant '' is declared +> '' but its initializer is ` at the initializer span and does NOT +> register the const (it also evicts the pass-0 placeholder so a count use +> can't still fold it). `literalKindName` names the literal kind for the +> message. +> - `src/ir/program_index.zig` — `moduleConstInt` / `moduleConstIntFramed` now +> take the `TypeTable` and gate the fold on `isCountableConstType(ci.ty)` +> (integer of any width, or a float), so a non-numeric typed const can never be +> folded into a count off its initializer node. Callers in `lower.zig` and +> `type_bridge.zig` updated. +> +> **Regression tests.** +> - `examples/1143-diagnostics-typed-module-const-mismatch.sx` — negative: four +> mismatch shapes (`int→string`, `string→s64`, `bool→s64`, `float→s64`) 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) compile, fold, and print correctly. +> - `src/ir/program_index.test.zig` — `moduleConstInt gates the fold on the +> declared type, not the initializer node`. + +# 0088 — Typed module const annotation mismatch is accepted + +## Symptom + +A module-level typed constant whose initializer does not match its annotation is +accepted. Observed: `N : string : 4` compiles; printing `N` segfaults, and using +`N` as an array dimension folds it as `4`. Expected: the const declaration emits +a type-mismatch diagnostic and no downstream use treats it as a valid string or +integer count. + +## Reproduction + +```sx +#import "modules/std.sx"; + +N : string : 4; + +main :: () { + print("N={}\n", N); +} +``` + +Related count-surface manifestation: + +```sx +#import "modules/std.sx"; + +N : string : 4; + +main :: () { + a : [N]s64 = ---; + print("{}\n", a.len); +} +``` + +Observed on `flow/sx-foundation/F0.4` attempt 10: the first repro segfaults in +the generated program; the second prints `4`. + +## Investigation prompt + +Fix issue 0088: typed module constants must validate/coerce their initializer +against the explicit annotation before being registered or used. Suspected area: +`src/ir/lower.zig`, especially `registerTypedModuleConst`, `lowerExpr`'s +module-const identifier path, and any const-declaration lowering that stores +`ProgramIndex.module_const_map` entries. `src/ir/program_index.zig`'s +`moduleConstInt` currently folds by inspecting the initializer node and ignores +`ModuleConstInfo.ty`; after the declaration is diagnosed or represented +correctly, a non-integer typed const such as `N : string : 4` must not become a +valid count. Likely fix: add a typed-const validation path that emits a clear +diagnostic for incompatible initializer/annotation pairs, and ensure the +module-const count lookup only accepts constants whose declared/inferred type is +numeric and integral-compatible. Verify by running the two repros above: expect +a non-zero compile with a type-mismatch diagnostic for `N : string : 4`, no +runtime segfault, and no `[N]` length of `4`. diff --git a/readme.md b/readme.md index e1846ec..6c31334 100644 --- a/readme.md +++ b/readme.md @@ -114,6 +114,11 @@ y : s32 = 0; // explicit type z : s32 = ---; // uninitialized ``` +A typed constant's initializer must be compatible with its annotation — an +integer literal fits any integer or float, a float a float type, a string +`string`, `null` a pointer/optional. A mismatch like `N : string : 4` is a +compile-time `type mismatch` error, not a silently-accepted constant. + Builtin type names (`s2`, `u8`, `bool`, `string`, …) are reserved and a *bare* spelling can't be used as an identifier at a **value-binding or declaration-name** site — a value binding (`:=` / typed local / parameter), a `::` constant or diff --git a/specs.md b/specs.md index d78ed4d..bdcd3a7 100644 --- a/specs.md +++ b/specs.md @@ -1458,6 +1458,13 @@ SOME_FUNC :: () => 42; // () -> s32 SOME_TYPE :: f64; // type alias ``` +With an explicit annotation, the initializer literal must be compatible with the +annotated type, or the declaration is a compile-time `type mismatch` error: an +integer literal fits any integer or float type (`W : f32 : 800`), a float literal +a float type, a boolean `bool`, a string literal `string`, `null` a pointer or +optional, and `---` any type. A mismatch such as `N : string : 4` is rejected at +the declaration — it does not register a usable constant. + ### Variable Binding (mutable) ```sx diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 1558a7e..23a6789 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -920,12 +920,75 @@ pub const Lowering = struct { switch (cd.value.data) { .int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal => { const ty = self.resolveType(ta); + // An unresolvable annotation is already diagnosed by the type + // resolver; don't pile a bogus type-mismatch on top, and don't + // leave the pass-0 placeholder (registered off the initializer + // literal) behind as a usable const. + if (ty == .unresolved) { + _ = self.program_index.module_const_map.remove(cd.name); + return; + } + // Validate the initializer literal against the explicit + // annotation. A mismatch (`N : string : 4`) is a type error, not + // a silently-accepted const — registering it would let + // `emitModuleConst` stamp the literal with the wrong IR type + // (an int emitted as a `string` const → a bogus pointer that + // segfaults at the use site) and let the count path fold it + // (`[N]s64` → 4). Issue 0088. + if (!self.typedConstInitFits(cd.value, ty)) { + if (self.diagnostics) |d| { + d.addFmt(.err, cd.value.span, "type mismatch: constant '{s}' is declared '{s}' but its initializer is {s}", .{ + cd.name, self.formatTypeName(ty), literalKindName(cd.value), + }); + } + // Evict the pass-0 placeholder (`N : string : 4` was + // pre-registered as `.s64` off its `int_literal` in + // scanDecls pass 0); leaving it would let a count use still + // fold `N` to 4. + _ = self.program_index.module_const_map.remove(cd.name); + return; + } self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {}; }, else => {}, } } + /// True iff a literal initializer of `value`'s kind is faithfully + /// representable at the declared `dst_ty` — the precondition + /// `emitModuleConst` relies on when it materialises the constant. The arms + /// match `emitModuleConst`'s arms exactly, using the same type-kind + /// predicates (`isIntEx` / `isFloat` / the `module.types.get` tag) the rest + /// of lowering uses. + /// + /// Deliberately NOT routed through `coercionResolver().classify` + /// (conversions.zig): that planner judges RUNTIME value coercions and is + /// unsound as a compile-time literal-representability oracle here — a `null` + /// literal's natural type is `.void`, so `classify(.void, *T)` yields `.none` + /// and would reject the valid `P : *void : null`; `bool` is 1 bit wide, so + /// `classify(.bool, s64)` yields `.widen` and would accept the bogus + /// `B : s64 : true`. + fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool { + return switch (value.data) { + // `---` zero-inits at any type. + .undef_literal => true, + // Integer literal → any integer (incl. custom widths) or float + // (`WIDTH : f32 : 800`). + .int_literal => self.isIntEx(dst_ty) or isFloat(dst_ty), + // Float literal → a float type only (the float arm emits `constFloat`). + .float_literal => isFloat(dst_ty), + .bool_literal => dst_ty == .bool, + .string_literal => dst_ty == .string, + // `null` → a pointer or optional. + .null_literal => !dst_ty.isBuiltin() and switch (self.module.types.get(dst_ty)) { + .pointer, .many_pointer, .optional => true, + else => false, + }, + // Only the literal kinds the caller's switch admits reach here. + else => true, + }; + } + /// Register a top-level mutable global (e.g., `context : Context = ---;`). /// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias /// in the type annotation (`A :: B; B :: s32; g : A = 7;`) resolves to its @@ -11876,7 +11939,7 @@ pub const Lowering = struct { // The module-const branch is shared verbatim with the stateless // registration-time resolver (`type_bridge`) so a `[N]T` dimension // resolves to the same length on both paths (issue 0083). - return program_index_mod.moduleConstInt(&self.program_index.module_const_map, name); + return program_index_mod.moduleConstInt(&self.program_index.module_const_map, &self.module.types, name); } /// Resolve a type node, checking type_bindings first for generic type params. @@ -15519,6 +15582,20 @@ pub const Lowering = struct { return self.closureShapeKey(params, self.returnValuePart(ret)); } + /// Human-readable name for a literal initializer kind, used in the typed + /// module-const type-mismatch diagnostic. + fn literalKindName(node: *const Node) []const u8 { + return switch (node.data) { + .int_literal => "an integer literal", + .float_literal => "a float literal", + .bool_literal => "a boolean literal", + .string_literal => "a string literal", + .null_literal => "null", + .undef_literal => "'---'", + else => "a value", + }; + } + fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 { return switch (op) { .add => "+", diff --git a/src/ir/program_index.test.zig b/src/ir/program_index.test.zig index fc3013e..00eee7f 100644 --- a/src/ir/program_index.test.zig +++ b/src/ir/program_index.test.zig @@ -215,6 +215,8 @@ test "floatToIntExact accepts integral floats, rejects the rest" { } test "moduleConstInt folds expression-RHS consts and rejects cycles" { + var table = types.TypeTable.init(std.testing.allocator); + defer table.deinit(); var map = std.StringHashMap(pi.ModuleConstInfo).init(std.testing.allocator); defer map.deinit(); @@ -236,12 +238,12 @@ test "moduleConstInt folds expression-RHS consts and rejects cycles" { try map.put("F", .{ .value = &f_val, .ty = .f64 }); try map.put("G", .{ .value = &g_val, .ty = .f64 }); - try std.testing.expectEqual(@as(?i64, 2), pi.moduleConstInt(&map, "M")); - try std.testing.expectEqual(@as(?i64, 3), pi.moduleConstInt(&map, "N")); - try std.testing.expectEqual(@as(?i64, 6), pi.moduleConstInt(&map, "P")); - try std.testing.expectEqual(@as(?i64, 4), pi.moduleConstInt(&map, "F")); - try std.testing.expect(pi.moduleConstInt(&map, "G") == null); - try std.testing.expect(pi.moduleConstInt(&map, "absent") == null); + try std.testing.expectEqual(@as(?i64, 2), pi.moduleConstInt(&map, &table, "M")); + try std.testing.expectEqual(@as(?i64, 3), pi.moduleConstInt(&map, &table, "N")); + try std.testing.expectEqual(@as(?i64, 6), pi.moduleConstInt(&map, &table, "P")); + try std.testing.expectEqual(@as(?i64, 4), pi.moduleConstInt(&map, &table, "F")); + try std.testing.expect(pi.moduleConstInt(&map, &table, "G") == null); + try std.testing.expect(pi.moduleConstInt(&map, &table, "absent") == null); // A cyclic const has no compile-time integer value, and folding it must not // recurse forever: mutual `A :: B + 0; B :: A + 0` and self `C :: C + 0` all @@ -256,9 +258,29 @@ test "moduleConstInt folds expression-RHS consts and rejects cycles" { try map.put("A", .{ .value = &a_val, .ty = .s64 }); try map.put("B", .{ .value = &b_val, .ty = .s64 }); try map.put("C", .{ .value = &c_val, .ty = .s64 }); - try std.testing.expect(pi.moduleConstInt(&map, "A") == null); - try std.testing.expect(pi.moduleConstInt(&map, "B") == null); - try std.testing.expect(pi.moduleConstInt(&map, "C") == null); + try std.testing.expect(pi.moduleConstInt(&map, &table, "A") == null); + try std.testing.expect(pi.moduleConstInt(&map, &table, "B") == null); + try std.testing.expect(pi.moduleConstInt(&map, &table, "C") == null); +} + +test "moduleConstInt gates the fold on the declared type, not the initializer node" { + var table = types.TypeTable.init(std.testing.allocator); + defer table.deinit(); + var map = std.StringHashMap(pi.ModuleConstInfo).init(std.testing.allocator); + defer map.deinit(); + + // An `int_literal` value node folds to an integer ONLY when the declared + // type is numeric. A `string`/`bool`-typed const carrying an integer-looking + // initializer must never be folded into a count (issue 0088): the count path + // consults `ModuleConstInfo.ty`, not just the node shape. + var int_val = nLit(4); + try map.put("OK", .{ .value = &int_val, .ty = .s64 }); + try map.put("STR", .{ .value = &int_val, .ty = .string }); + try map.put("BOOLEAN", .{ .value = &int_val, .ty = .bool }); + + try std.testing.expectEqual(@as(?i64, 4), pi.moduleConstInt(&map, &table, "OK")); + try std.testing.expect(pi.moduleConstInt(&map, &table, "STR") == null); + try std.testing.expect(pi.moduleConstInt(&map, &table, "BOOLEAN") == null); } test "evalConstIntExpr folds an integral float literal, halts on a fractional one" { diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index e629189..b7a6657 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -91,20 +91,40 @@ fn moduleConstFrameContains(frame: ?*const ModuleConstFrame, name: []const u8) b /// scope, so `lookupPackLen` is always null. const ModuleConstCtx = struct { consts: *const std.StringHashMap(ModuleConstInfo), + table: *const types.TypeTable, frame: ?*const ModuleConstFrame, pub fn lookupDimName(self: ModuleConstCtx, name: []const u8) ?i64 { - return moduleConstIntFramed(self.consts, name, self.frame); + return moduleConstIntFramed(self.consts, self.table, name, self.frame); } pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 { return null; } }; -fn moduleConstIntFramed(consts: *const std.StringHashMap(ModuleConstInfo), name: []const u8, parent: ?*const ModuleConstFrame) ?i64 { +/// A module const may serve as an integer COUNT only when its DECLARED type is +/// numeric — an integer of any width or a float (an integral float folds to its +/// int via `floatToIntExact`). `moduleConstIntFramed` consults this so a count +/// is gated on `ModuleConstInfo.ty`, not just the shape of the initializer node: +/// a `string`/`bool`/pointer/struct-typed const can never be folded into a count +/// off an integer-looking initializer (issue 0088 — the second symptom, where +/// `N : string : 4` folded `[N]s64` to 4 by reading the `int_literal` node and +/// ignoring the `string` annotation). +fn isCountableConstType(table: *const types.TypeTable, ty: TypeId) bool { + return switch (ty) { + .s8, .s16, .s32, .s64, .u8, .u16, .u32, .u64, .usize, .isize, .f32, .f64 => true, + else => if (ty.isBuiltin()) false else switch (table.get(ty)) { + .signed, .unsigned => true, + else => false, + }, + }; +} + +fn moduleConstIntFramed(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8, parent: ?*const ModuleConstFrame) ?i64 { if (moduleConstFrameContains(parent, name)) return null; const ci = consts.get(name) orelse return null; + if (!isCountableConstType(table, ci.ty)) return null; var frame = ModuleConstFrame{ .name = name, .parent = parent }; - return evalConstIntExpr(ci.value, ModuleConstCtx{ .consts = consts, .frame = &frame }); + return evalConstIntExpr(ci.value, ModuleConstCtx{ .consts = consts, .table = table, .frame = &frame }); } /// A name bound to a module-global integer constant → its value, else null. @@ -120,8 +140,8 @@ fn moduleConstIntFramed(consts: *const std.StringHashMap(ModuleConstInfo), name: /// RHS over other consts (`M :: 2; N :: M + 1` → 3) all resolve identically and /// everywhere a count is accepted. Cyclic consts fold to null (see /// `ModuleConstCtx`). -pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), name: []const u8) ?i64 { - return moduleConstIntFramed(consts, name, null); +pub fn moduleConstInt(consts: *const std.StringHashMap(ModuleConstInfo), table: *const types.TypeTable, name: []const u8) ?i64 { + return moduleConstIntFramed(consts, table, name, null); } /// Evaluate a constant integer expression to its value. THE single diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index 3d87458..96a85b2 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -70,7 +70,7 @@ const StatelessInner = struct { /// operand may legitimately be negative. pub fn lookupDimName(self: StatelessInner, name: []const u8) ?i64 { const consts = self.consts orelse return null; - return program_index_mod.moduleConstInt(consts, name); + return program_index_mod.moduleConstInt(consts, self.table, name); } /// Pack-length leaf for the shared integer-expression evaluator. The /// registration-time path has no pack-arity information (packs are bound