From 501399b1a93e1a72ddbf0daaccdc021f0c8bceb1 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 26 Jun 2026 07:51:27 +0300 Subject: [PATCH] fix: resolve qualified-import-member const as a compile-time constant (issue 0192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A namespaced import's const (`m :: #import "lib.sx"; … m.CAP`) only ever resolved as a runtime value — the const folders in program_index.zig had no namespace-member arm, so a qualified const was rejected as an array dimension / Vector lane / generic value-param and could not seed another const, while the flat-import form worked everywhere. Add a `lookupQualifiedConst` (+ float / float-typed twins) ctx hook: resolve the alias via `namespaceAliasVerdictFrom` to its target module, then fold the member from that module's per-source const cache (`foldQualifiedConstInt` in lower/comptime.zig), pinned to the target source so nested const RHSs fold there. Wire it into evalConstIntExpr / evalConstFloatExpr / isFloatValuedExpr — both the expression-position field_access arm (`[m.CAP]T`) and the type-argument dotted-name arm (`Vector(m.LANES, …)`, generic value-params). Implemented on the source-aware ctxs (Lowering / SourceConstCtx); the namespace-blind ModuleConstCtx / StatelessInner return null, so a qualified-const dim reached only via the stateless type-alias path stays a clean unresolved-dim diagnostic, never a fabricated length. Resolves correctly for array dims, arithmetic, integral-float dims, Vector lanes, generic value-params, inline-for bounds, and struct fields. Regression: examples/modules/0842-modules-qualified-import-const-comptime.sx. --- current/CHECKPOINT-FIBERS.md | 26 ++++ ...modules-qualified-import-const-comptime.sx | 27 ++++ .../layout.sx | 6 + ...dules-qualified-import-const-comptime.exit | 1 + ...les-qualified-import-const-comptime.stderr | 1 + ...les-qualified-import-const-comptime.stdout | 1 + ...192-qualified-import-const-not-comptime.md | 127 ++++++++++++++++++ src/ir/lower.zig | 25 ++++ src/ir/lower/comptime.zig | 66 +++++++++ src/ir/program_index.test.zig | 11 ++ src/ir/program_index.zig | 60 ++++++++- src/ir/type_bridge.zig | 16 +++ 12 files changed, 361 insertions(+), 6 deletions(-) create mode 100644 examples/modules/0842-modules-qualified-import-const-comptime.sx create mode 100644 examples/modules/0842-modules-qualified-import-const-comptime/layout.sx create mode 100644 examples/modules/expected/0842-modules-qualified-import-const-comptime.exit create mode 100644 examples/modules/expected/0842-modules-qualified-import-const-comptime.stderr create mode 100644 examples/modules/expected/0842-modules-qualified-import-const-comptime.stdout create mode 100644 issues/0192-qualified-import-const-not-comptime.md diff --git a/current/CHECKPOINT-FIBERS.md b/current/CHECKPOINT-FIBERS.md index fea98593..19f0c952 100644 --- a/current/CHECKPOINT-FIBERS.md +++ b/current/CHECKPOINT-FIBERS.md @@ -518,6 +518,32 @@ non-unification: virtual-time timers and real kqueue timeouts are NOT merged — timer before ever blocking on kqueue (a program uses `sleep` OR fds); a true "fd-or-real-timeout" wants a kqueue `EVFILT_TIMER`, future work. +> **✅ issue 0192 FIXED (2026-06-26) — epoll work UNBLOCKED.** A qualified-import-member const +> (`m.EV_SIZE`) now folds as a compile-time constant in every position the bare/flat form does +> (array dim, arithmetic, Vector lane, generic value-param, inline-for) — so the clean +> `[MAXEV * ep.EV_SIZE]u8` event buffer the bindings want will work. Fix: a `lookupQualifiedConst` +> ctx hook resolving the namespace alias → target module's per-source const, wired into the int/float +> const folders (`src/ir/program_index.zig` + `src/ir/lower/comptime.zig`). Regression: +> `examples/modules/0842-modules-qualified-import-const-comptime.sx`. The hint stands for the rebuild: +> **a struct-per-arch `EpollEvent` (arch-branched u32 fields, 12 B x86_64 / 16 B aarch64) beat raw +> byte access** — idiomatic field reads, no issue-0155 scalar-pointer indexing, no unaligned u64. +> Resume: rebuild `std/net/epoll.sx`, branch `std.event.Loop` on `inline if OS`, lock with a darwin run +> + ir-only linux example. + +> **⛔ (HISTORICAL) BLOCKED on issue 0192 (filed 2026-06-26).** Started the epoll work: chose the `std.event.Loop` +> backend (pure sx + libc externs, zero compiler change — per "do this in sx as much as possible") as +> the first deliverable, since event.sx already names epoll as its linux backend and it's runnable +> (darwin via kqueue) + ir-only-verifiable (linux). De-risked four landmines by probe — arch-dependent +> layout const via module-scope `inline if ARCH` (folds + validates in linux IR), slice-based byte access +> (sidesteps issue 0155), no unaligned u64 (store the 32-bit fd in epoll `data`), and comptime-dead linux +> externs don't break the darwin corpus (just an unreferenced `declare`). Then hit a compiler bug while +> sizing the event buffer: a **qualified-import-member const is not a compile-time constant** — +> `[m.CAP]u8` / `A :: m.CAP` fail (a *flat*-imported const works). Root cause located: +> `evalConstIntExpr` (`src/ir/program_index.zig:325`) has no namespace-member-const arm. Per the STOP +> rule the half-built `std/net/epoll.sx` (which used a struct-based layout to route around the bug) was +> **removed**, not landed — the unblock session rebuilds it cleanly with the fix in hand. Repro + +> investigation prompt: `issues/0192-qualified-import-const-not-comptime.{md,sx}`. + Design note carried forward: an event-loop `Io` needs a current-`Scheduler` handle. `sched.*` methods thread it via `self`/the `Task`; if B1.4c wants the capability-threaded `context.io` form it'll need an ambient current-scheduler accessor in sched.sx (still deferred — the `sched.*`-method form diff --git a/examples/modules/0842-modules-qualified-import-const-comptime.sx b/examples/modules/0842-modules-qualified-import-const-comptime.sx new file mode 100644 index 00000000..b74bc71e --- /dev/null +++ b/examples/modules/0842-modules-qualified-import-const-comptime.sx @@ -0,0 +1,27 @@ +// A qualified-import-member const (`m.CAP`) folds as a compile-time constant in +// every comptime position the bare-import form already supported: array +// dimensions, const arithmetic, an integral-float dimension, a Vector lane +// count, and a generic value-param argument. +// +// Regression (issue 0192): a namespaced import's const reached the const +// folders only as a runtime value — `evalConstIntExpr`'s field_access arm had +// no namespace-member arm, and a qualified ref in type-argument position +// (`Vector(m.LANES, …)`) arrives as a single dotted name. Both now resolve +// through the namespace alias to the target module's per-source const. +#import "modules/std.sx"; +m :: #import "0842-modules-qualified-import-const-comptime/layout.sx"; + +Box :: struct($N: i64) { data: [N]u8; } + +main :: () -> i64 { + a : [m.CAP]u8 = ---; // array dimension → 8 + b : [m.CAP * 2 + 1]u8 = ---; // arithmetic over a qualified const → 17 + c : [m.FOURF]u8 = ---; // integral float const dim → 4 + v : Vector(m.LANES, f32) = ---; // Vector lane count → 4 lanes + bx : Box(m.CAP) = ---; // generic value-param arg → 8 + iter := 0; + inline for 0..m.CAP (i) { iter += 1; } // inline-for bound → 8 + + print("a={} b={} c={} box={} lanes-iter={}\n", a.len, b.len, c.len, bx.data.len, iter); + return a.len + b.len + c.len + bx.data.len + iter; // 8+17+4+8+8 = 45 +} diff --git a/examples/modules/0842-modules-qualified-import-const-comptime/layout.sx b/examples/modules/0842-modules-qualified-import-const-comptime/layout.sx new file mode 100644 index 00000000..2cbcb893 --- /dev/null +++ b/examples/modules/0842-modules-qualified-import-const-comptime/layout.sx @@ -0,0 +1,6 @@ +// A namespaced-import module exporting compile-time consts of each shape the +// regression exercises: a plain int, an expression-RHS int, and a float. +BASE :: 4; +CAP :: BASE + 4; // 8 — nested const RHS, folded in THIS module's scope +LANES :: 4; +FOURF : f64 : 4.0; // integral float const diff --git a/examples/modules/expected/0842-modules-qualified-import-const-comptime.exit b/examples/modules/expected/0842-modules-qualified-import-const-comptime.exit new file mode 100644 index 00000000..ea90ee31 --- /dev/null +++ b/examples/modules/expected/0842-modules-qualified-import-const-comptime.exit @@ -0,0 +1 @@ +45 diff --git a/examples/modules/expected/0842-modules-qualified-import-const-comptime.stderr b/examples/modules/expected/0842-modules-qualified-import-const-comptime.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/modules/expected/0842-modules-qualified-import-const-comptime.stderr @@ -0,0 +1 @@ + diff --git a/examples/modules/expected/0842-modules-qualified-import-const-comptime.stdout b/examples/modules/expected/0842-modules-qualified-import-const-comptime.stdout new file mode 100644 index 00000000..03732ac4 --- /dev/null +++ b/examples/modules/expected/0842-modules-qualified-import-const-comptime.stdout @@ -0,0 +1 @@ +a=8 b=17 c=4 box=8 lanes-iter=8 diff --git a/issues/0192-qualified-import-const-not-comptime.md b/issues/0192-qualified-import-const-not-comptime.md new file mode 100644 index 00000000..ba54fcc8 --- /dev/null +++ b/issues/0192-qualified-import-const-not-comptime.md @@ -0,0 +1,127 @@ +# Issue 0192 — qualified-import-member const is not a compile-time constant + +> **✅ RESOLVED.** Root cause: the const folders in `src/ir/program_index.zig` +> had no namespace-member arm — `evalConstIntExpr` resolved a bare/flat const +> leaf (`lookupDimName`) but not a qualified `m.CAP`. Fix: a `lookupQualifiedConst` +> (+ float / float-typed twins) ctx hook that resolves the alias `m` via +> `Lowering.namespaceAliasVerdict` to its target module and folds `CAP` from +> that module's per-source const cache (`foldQualifiedConstInt` in +> `src/ir/lower/comptime.zig`), pinned to the target source so nested const RHSs +> fold there. Wired into `evalConstIntExpr` / `evalConstFloatExpr` / +> `isFloatValuedExpr` — both the EXPRESSION-position `field_access` arm (`[m.CAP]T`) +> and the TYPE-argument dotted-name arm (`Vector(m.LANES, …)`, generic +> value-params). Implemented on the source-aware ctxs (`Lowering` / +> `SourceConstCtx`); the namespace-blind `ModuleConstCtx` / `StatelessInner` +> return null (documented — a qualified-const dim reached ONLY via the stateless +> type-alias path stays a clean unresolved-dim diagnostic, never a fabricated +> length). Regression: `examples/modules/0842-modules-qualified-import-const-comptime.sx` +> (qualified const as array dim, arithmetic, integral-float dim, Vector lane, +> generic value-param, inline-for bound). Suite green 816/0. +> +> NOTE: the writeup's secondary symptom `A :: m.CAP` (a const aliasing a +> qualified const) is NOT part of this bug — `A :: B` (aliasing a *bare* local +> const) fails identically, so const-aliasing-a-single-name is a separate +> pre-existing limitation (`N :: M + 1` expression-RHS works). Out of scope here. + +## Symptom + +A constant reached through a **namespaced (qualified) import** — `m :: #import +"lib.sx"; … m.CAP` — is not recognized as a compile-time constant. It works +fine as a *runtime* value, but the moment a comptime context needs it the +compiler either rejects it or fails to resolve it: + +- as an **array dimension** → `error: array dimension must be a compile-time + integer constant` +- seeding **another const** (`A :: m.CAP;`) → `error: unresolved 'A'` + +Expected: a qualified-import const should fold to a compile-time constant +everywhere a flat-imported const does — array dimensions, `Vector` lanes, +const initializers, value-param args. (A **flat** `#import` of the same const +works in all those positions; only the qualified `m.CONST` form fails.) + +## Reproduction + +Two files (the bug requires a *qualified* import, which needs a second module). + +`issues/0192-qualified-import-const-not-comptime/lib.sx`: +```sx +CAP :: 8; +``` + +`issues/0192-qualified-import-const-not-comptime.sx`: +```sx +m :: #import "0192-qualified-import-const-not-comptime/lib.sx"; + +main :: () -> i64 { + buf : [m.CAP]u8 = ---; // ← error: array dimension must be a compile-time integer constant + return buf.len; +} +``` + +```sh +./zig-out/bin/sx run issues/0192-qualified-import-const-not-comptime.sx +``` + +Scoping probes (all run against the same `CAP :: 8`): + +| Form | Result | +|------|--------| +| `#import "lib.sx"; [CAP]u8` (flat, as dim) | ✅ works | +| `m :: #import "lib.sx"; [m.CAP]u8` (qualified, as dim) | ❌ "array dimension must be a compile-time integer constant" | +| `m :: #import …; A :: m.CAP;` (qualified, seeding a const) | ❌ "unresolved 'A'" | +| `m :: #import …; x := m.CAP;` (qualified, runtime value) | ✅ works (prints 8) | + +So the value *is* reachable; only its **compile-time folding through the +qualified member-access path** is missing. + +## Investigation prompt + +The compile-time integer folder is `evalConstIntExpr` in +`src/ir/program_index.zig` (≈ line 318). Its `.identifier` arm resolves a +flat-scope const via `ctx.lookupDimName(id.name)` — that is why a flat import +works. Its `.field_access` arm (≈ line 325) only handles three shapes: +`.len`, `.min`/`.max` (via `TypeResolver.integerLimitFor`), and +`.field` (via `ctx.lookupConstStructField`). There is **no arm +that resolves a namespace-member const** — for `m.CAP`, `obj_name == "m"`, +`fa.field == "CAP"`, none of the three match, and it falls through to `null` → +`.not_const` → the array-dim diagnostic. The const-init path +(`A :: m.CAP` → `unresolved 'A'`) is the same gap one layer up: the const +initializer can't fold `m.CAP` either. + +The fix likely needs a new `ctx` hook — e.g. `lookupQualifiedConst(namespace, +name) -> ?i64` — that follows the namespace edge (the import-alias `m` → +its module, via `namespace_edges` / `module_decls` in `ProgramIndex`) to the +imported module and returns the named const's folded integer value. Wire it +into `evalConstIntExpr`'s `.field_access` arm: when `obj_name` names a known +import namespace (not a pack / type / struct-const), look the field up as a +module-level const in that namespace's module. The same resolution should make +`A :: m.CAP` fold (whatever const-init folding path also routes through, or a +sibling of, `evalConstIntExpr`). + +Mirror the existing `lookupConstStructField` plumbing — it already threads a +"resolve a name's const value from another scope" capability through the +`ModuleConstCtx` / `Lowering` ctx; the qualified-namespace case is the analogous +"resolve a const from an imported module by alias" lookup. Watch the +float sibling `evalConstFloatExpr` (≈ line 443) and `isFloatValuedExpr` +(≈ line 264) — a qualified float const (`m.PI`) has the identical gap, so fix +the cluster consistently (per the comment at line 332 about keeping the const +cluster in agreement). + +## Verification step + +After the fix, the reproduction above should compile and run, printing exit +code `8` (`buf.len`). Add a regression example exercising a qualified-import +const as (a) an array dimension and (b) a const initializer. Then unblock the +**linux epoll** work (CHECKPOINT-FIBERS): `library/modules/std/net/epoll.sx` +wants `[N * ep.EV_SIZE]u8` event buffers sized from a qualified-import layout +const — the cleanest expression of the arch-dependent `epoll_event` stride. + +## Discovered by + +Building `library/modules/std/net/epoll.sx` (the linux epoll twin of +`std/net/kqueue.sx`, CHECKPOINT-FIBERS deferred follow-up). The epoll event +buffer wants to be sized `[MAXEV * ep.EV_SIZE]u8` from the bindings module's +arch-dependent stride const; `ep.EV_SIZE` as an array dimension hit this bug. +A struct-based layout (`EpollEvent` with arch-branched u32 fields) sidesteps +it, but per the project's STOP rule the workaround is not landed — the bindings +work is paused pending this fix. diff --git a/src/ir/lower.zig b/src/ir/lower.zig index b3f23d81..ac4b90fe 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -119,6 +119,15 @@ pub const SourceConstCtx = struct { pub fn lookupConstStructField(self: SourceConstCtx, name: []const u8, field: []const u8) ?i64 { return self.lowering.foldConstStructField(name, field, self.frame); } + pub fn lookupQualifiedConst(self: SourceConstCtx, ns: []const u8, field: []const u8) ?i64 { + return self.lowering.foldQualifiedConstInt(ns, field, self.frame); + } + pub fn lookupQualifiedConstFloat(self: SourceConstCtx, ns: []const u8, field: []const u8) ?f64 { + return self.lowering.foldQualifiedConstFloat(ns, field, self.frame); + } + pub fn qualifiedNameIsFloatTyped(self: SourceConstCtx, ns: []const u8, field: []const u8) bool { + return self.lowering.qualifiedConstIsFloatTyped(ns, field, self.frame); + } }; // ── Scope ─────────────────────────────────────────────────────────────── @@ -884,6 +893,18 @@ pub const Lowering = struct { pub fn lookupConstStructField(self: *Lowering, name: []const u8, field: []const u8) ?i64 { return self.foldConstStructField(name, field, null); } + /// Qualified-import-member const leaf (`m.CAP`, issue 0192) for the shared + /// dimension evaluator — resolves the namespace alias `ns` to its target + /// module and folds its `field` const there. + pub fn lookupQualifiedConst(self: *Lowering, ns: []const u8, field: []const u8) ?i64 { + return self.foldQualifiedConstInt(ns, field, null); + } + pub fn lookupQualifiedConstFloat(self: *Lowering, ns: []const u8, field: []const u8) ?f64 { + return self.foldQualifiedConstFloat(ns, field, null); + } + pub fn qualifiedNameIsFloatTyped(self: *Lowering, ns: []const u8, field: []const u8) bool { + return self.qualifiedConstIsFloatTyped(ns, field, null); + } /// Resolve a type node, checking type_bindings first for generic type params. pub fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId { @@ -1752,6 +1773,10 @@ pub const Lowering = struct { pub const foldSourceConstInt = lower_comptime.foldSourceConstInt; pub const foldSourceConstFloat = lower_comptime.foldSourceConstFloat; pub const sourceConstIsFloatTyped = lower_comptime.sourceConstIsFloatTyped; + pub const selectQualifiedConst = lower_comptime.selectQualifiedConst; + pub const foldQualifiedConstInt = lower_comptime.foldQualifiedConstInt; + pub const foldQualifiedConstFloat = lower_comptime.foldQualifiedConstFloat; + pub const qualifiedConstIsFloatTyped = lower_comptime.qualifiedConstIsFloatTyped; pub const comptimeIntNamed = lower_comptime.comptimeIntNamed; pub const selectModuleConst = lower_comptime.selectModuleConst; pub const GlobalAuthor = lower_comptime.GlobalAuthor; diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index 8db1af0b..6393c60f 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -1293,6 +1293,72 @@ pub fn foldSourceConstInt(self: *Lowering, name: []const u8, frame: ?*const Cons }; } +/// Resolve a QUALIFIED module const `ns.field` (a namespaced-import member — +/// `m :: #import "lib.sx"; … m.CAP`) to its authoring source + info (issue +/// 0192). The alias `ns` is resolved in the CURRENT source context — the file +/// that wrote `ns.field`, since an alias binds in its declaring file, not the +/// use site — then `field` is read from that target module's per-source const +/// cache (`module_consts_by_source`). Null when `ns` is not a visible namespace +/// alias, is ambiguous, or names no such const there. Diagnostic-free: a +/// speculative const fold must not emit (a real "name resolves nowhere" error is +/// the reference site's job), so the ambiguous-alias case folds to null here. +pub fn selectQualifiedConst(self: *Lowering, ns: []const u8, field: []const u8) ?SelectedConst { + // Resolve the alias from the use-site source — the file that wrote `ns.field` + // (where `ns` binds). A main-file body carries a null `current_source_file` + // (it IS the root), so fall back to `main_file`, matching `selectModuleConst`. + const from = self.current_source_file orelse self.main_file orelse return null; + const target = switch (self.namespaceAliasVerdictFrom(ns, from)) { + .target => |t| t, + .ambiguous, .none => return null, + }; + const src = target.target_module_path; + const ci = self.sourceModuleConst(src, field) orelse return null; + return .{ .info = ci, .source = src }; +} + +/// Source-aware INTEGER fold of a qualified const `ns.field` (issue 0192): the +/// qualified twin of `foldSourceConstInt`. Resolve the namespace member to its +/// authoring source, then fold ITS RHS PINNED to that source so nested const +/// leaves (`CAP :: BASE + 1`, `BASE` authored in the target module) re-select +/// against the target module, not the use site. `frame` (keyed by name + +/// author-source) cycle-guards a const whose value references another const. +pub fn foldQualifiedConstInt(self: *Lowering, ns: []const u8, field: []const u8, frame: ?*const ConstFoldFrame) ?i64 { + const sel = self.selectQualifiedConst(ns, field) orelse return null; + if (constFoldFrameContains(frame, field, sel.source)) return null; + if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null; + var f = ConstFoldFrame{ .name = field, .source = sel.source, .parent = frame }; + const restore = self.pinConstAuthorSource(sel.source); + defer restore.unpin(); + return program_index_mod.evalConstIntExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); +} + +/// FLOAT counterpart of `foldQualifiedConstInt` (issue 0192) — the qualified +/// twin of `foldSourceConstFloat`, so a qualified non-integral float const +/// (`m.PI`) folds the same way its bare-name sibling does. +pub fn foldQualifiedConstFloat(self: *Lowering, ns: []const u8, field: []const u8, frame: ?*const ConstFoldFrame) ?f64 { + const sel = self.selectQualifiedConst(ns, field) orelse return null; + if (constFoldFrameContains(frame, field, sel.source)) return null; + if (!program_index_mod.isCountableConstType(&self.module.types, sel.info.ty)) return null; + var f = ConstFoldFrame{ .name = field, .source = sel.source, .parent = frame }; + const restore = self.pinConstAuthorSource(sel.source); + defer restore.unpin(); + return program_index_mod.evalConstFloatExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); +} + +/// "Is the qualified const `ns.field` FLOAT-valued" (issue 0192) — the +/// qualified twin of `sourceConstIsFloatTyped`, consulted by the int folder's +/// division guard so `m.K / 3` (with `m.K : f64`) is recognised as float +/// division exactly as a bare `K / 3` is. +pub fn qualifiedConstIsFloatTyped(self: *Lowering, ns: []const u8, field: []const u8, frame: ?*const ConstFoldFrame) bool { + const sel = self.selectQualifiedConst(ns, field) orelse return false; + if (constFoldFrameContains(frame, field, sel.source)) return false; + if (program_index_mod.isFloatConstType(sel.info.ty)) return true; + var f = ConstFoldFrame{ .name = field, .source = sel.source, .parent = frame }; + const restore = self.pinConstAuthorSource(sel.source); + defer restore.unpin(); + return program_index_mod.isFloatValuedExpr(sel.info.value, SourceConstCtx{ .lowering = self, .frame = &f }); +} + /// Float counterpart of `foldSourceConstInt` (E2/F2/R1). pub fn foldSourceConstFloat(self: *Lowering, name: []const u8, frame: ?*const ConstFoldFrame) ?f64 { return switch (self.selectModuleConst(name)) { diff --git a/src/ir/program_index.test.zig b/src/ir/program_index.test.zig index 29f30741..32308f44 100644 --- a/src/ir/program_index.test.zig +++ b/src/ir/program_index.test.zig @@ -188,6 +188,17 @@ const DimCtx = struct { pub fn nameIsFloatTyped(_: DimCtx, name: []const u8) bool { return std.mem.eql(u8, name, "F") or std.mem.eql(u8, name, "K"); } + // This test ctx models no namespace imports — qualified-member consts + // (`m.CAP`, issue 0192) are exercised end-to-end by the corpus, not here. + pub fn lookupQualifiedConst(_: DimCtx, _: []const u8, _: []const u8) ?i64 { + return null; + } + pub fn lookupQualifiedConstFloat(_: DimCtx, _: []const u8, _: []const u8) ?f64 { + return null; + } + pub fn qualifiedNameIsFloatTyped(_: DimCtx, _: []const u8, _: []const u8) bool { + return false; + } }; fn nLit(v: i64) ast.Node { diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index 36e96740..e79701f4 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -125,6 +125,20 @@ const ModuleConstCtx = struct { pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 { return null; } + // The GLOBAL-map fold carries no namespace-import facts (no `namespace_edges` + // / per-source const cache), so a qualified-member const `m.CAP` can only be + // resolved by the SOURCE-AWARE path (`SourceConstCtx` / `Lowering`). Null + // here (issue 0192). A qualified const used inside another module const's RHS + // folds through `SourceConstCtx`, not this ctx, so this is not a live gap. + pub fn lookupQualifiedConst(_: ModuleConstCtx, _: []const u8, _: []const u8) ?i64 { + return null; + } + pub fn lookupQualifiedConstFloat(_: ModuleConstCtx, _: []const u8, _: []const u8) ?f64 { + return null; + } + pub fn qualifiedNameIsFloatTyped(_: ModuleConstCtx, _: []const u8, _: []const u8) bool { + return false; + } /// Float counterpart of `lookupDimName`, so `evalConstFloatExpr` resolves a /// float-const leaf whose value references another const /// (`G : f64 : 2.0; F : f64 : G + 0.5`) recursively through the SAME @@ -261,8 +275,8 @@ pub fn isFloatValuedExpr(node: *const Node, ctx: anytype) bool { return switch (node.data) { .float_literal => true, .int_literal => false, - .identifier => |id| ctx.nameIsFloatTyped(id.name), - .type_expr => |te| ctx.nameIsFloatTyped(te.name), + .identifier => |id| ctx.nameIsFloatTyped(id.name) or qualifiedDottedIsFloat(id.name, ctx), + .type_expr => |te| ctx.nameIsFloatTyped(te.name) or qualifiedDottedIsFloat(te.name, ctx), .field_access => |fa| blk: { // A backtick RAW receiver (`` `f64.epsilon ``) is an ordinary field // READ on a value whose spelling shadows a builtin type, NOT the @@ -274,6 +288,10 @@ pub fn isFloatValuedExpr(node: *const Node, ctx: anytype) bool { }; if (obj_name) |on| { if (type_resolver.TypeResolver.floatLimitFor(on, fa.field) != null) break :blk true; + // A QUALIFIED-import-member float const (`m.PI`, issue 0192): so + // the int folder's division guard classifies `m.K / 3` as float + // division exactly as it does a bare `K / 3`. + if (ctx.qualifiedNameIsFloatTyped(on, fa.field)) break :blk true; } break :blk false; }, @@ -283,6 +301,28 @@ pub fn isFloatValuedExpr(node: *const Node, ctx: anytype) bool { }; } +/// A namespace-qualified const written in TYPE-argument position (`Vector(m.N, +/// f32)`, a generic value-param `Vec(m.N, …)`) reaches the const folders as a +/// SINGLE dotted name — a `type_expr` / `identifier` whose `name` is `"m.N"` — +/// not the `field_access` node the EXPRESSION position (`[m.N]T`) produces. +/// Split on the first `.` and resolve the tail as a const in namespace `m`'s +/// target module (issue 0192). Null for an unqualified name (no `.`), so an +/// ordinary leaf is unaffected. (sx identifiers carry no `.`, so a dotted name +/// is always a namespace qualification; a single-level alias yields exactly one +/// `.`, and a stray multi-dot tail simply finds no const and folds to null.) +fn qualifiedDottedInt(name: []const u8, ctx: anytype) ?i64 { + const dot = std.mem.indexOfScalar(u8, name, '.') orelse return null; + return ctx.lookupQualifiedConst(name[0..dot], name[dot + 1 ..]); +} +fn qualifiedDottedFloat(name: []const u8, ctx: anytype) ?f64 { + const dot = std.mem.indexOfScalar(u8, name, '.') orelse return null; + return ctx.lookupQualifiedConstFloat(name[0..dot], name[dot + 1 ..]); +} +fn qualifiedDottedIsFloat(name: []const u8, ctx: anytype) bool { + const dot = std.mem.indexOfScalar(u8, name, '.') orelse return false; + return ctx.qualifiedNameIsFloatTyped(name[0..dot], name[dot + 1 ..]); +} + /// Evaluate a constant integer expression to its value. THE single /// integer-expression folder for the compiler — array dimensions (`[N]T`, /// `[M + 1]T`), Vector lane counts (`Vector(N, f32)`), generic value-param @@ -320,8 +360,8 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 { .int_literal => |lit| lit.value, // An integral float literal (`[4.0]T`) folds to its integer; `4.5` → null. .float_literal => |lit| floatToIntExact(lit.value), - .identifier => |id| ctx.lookupDimName(id.name), - .type_expr => |te| ctx.lookupDimName(te.name), + .identifier => |id| ctx.lookupDimName(id.name) orelse qualifiedDottedInt(id.name, ctx), + .type_expr => |te| ctx.lookupDimName(te.name) orelse qualifiedDottedInt(te.name, ctx), .field_access => |fa| blk: { // A backtick RAW receiver (`` `i64.max ``, `` `f64.epsilon ``) is an // ordinary field READ on a value whose spelling shadows a builtin @@ -352,6 +392,11 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 { // A struct const's integer field (`LIT.r`) folds to the // SELECTED author's field value. if (ctx.lookupConstStructField(on, fa.field)) |v| break :blk v; + // A QUALIFIED-import-member const (`m.CAP`, issue 0192): `on` + // names a namespace alias and `fa.field` a const in its target + // module. Tried last so a same-named struct-const / numeric-limit + // receiver keeps its existing meaning. + if (ctx.lookupQualifiedConst(on, fa.field)) |v| break :blk v; } // Any other field access is not a compile-time integer leaf. break :blk null; @@ -438,8 +483,8 @@ pub fn evalConstFloatExpr(node: *const Node, ctx: anytype) ?f64 { .float_literal => |lit| lit.value, // A name bound to a numeric module const whose value is a non-integral // float (the integral / integer cases were caught by the int delegation). - .identifier => |id| ctx.lookupFloatName(id.name), - .type_expr => |te| ctx.lookupFloatName(te.name), + .identifier => |id| ctx.lookupFloatName(id.name) orelse qualifiedDottedFloat(id.name, ctx), + .type_expr => |te| ctx.lookupFloatName(te.name) orelse qualifiedDottedFloat(te.name, ctx), .field_access => |fa| blk: { // A numeric-limit accessor on a builtin FLOAT type (`f64.true_min`, // `f32.epsilon`, `f64.max`, …) is a compile-time float leaf — the @@ -461,6 +506,9 @@ pub fn evalConstFloatExpr(node: *const Node, ctx: anytype) ?f64 { }; if (obj_name) |on| { if (type_resolver.TypeResolver.floatLimitFor(on, fa.field)) |v| break :blk v; + // A QUALIFIED-import-member float const (`m.PI`, issue 0192) — + // the float twin of the int folder's qualified-const arm. + if (ctx.lookupQualifiedConstFloat(on, fa.field)) |v| break :blk v; } break :blk null; }, diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index c5643ebd..ace5bc74 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -94,6 +94,22 @@ const StatelessInner = struct { pub fn lookupPackLen(_: StatelessInner, _: []const u8) ?i64 { return null; } + // The registration-time path holds only the flat global const map — no + // namespace-import facts (`namespace_edges` / per-source cache) — so a + // qualified-member const `m.CAP` is not a compile-time leaf here (issue + // 0192). It resolves on the stateful body-lowering path (`Lowering`); a + // qualified-const dimension reached ONLY through this path (e.g. a type + // alias `Arr :: [m.CAP]T`) stays unresolved and surfaces the clean dim + // diagnostic rather than a fabricated length. + pub fn lookupQualifiedConst(_: StatelessInner, _: []const u8, _: []const u8) ?i64 { + return null; + } + pub fn lookupQualifiedConstFloat(_: StatelessInner, _: []const u8, _: []const u8) ?f64 { + return null; + } + pub fn qualifiedNameIsFloatTyped(_: StatelessInner, _: []const u8, _: []const u8) bool { + return false; + } /// Float-valued leaf for the shared float-expression evaluator — the FLOAT /// twin of `lookupDimName`, routed through the SAME `program_index.moduleConstFloat` /// the stateful body-lowering path uses, so a float-const-leaf dimension