Files
sx/issues/0192-qualified-import-const-not-comptime.md
agra 501399b1a9 fix: resolve qualified-import-member const as a compile-time constant (issue 0192)
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.
2026-06-26 07:51:27 +03:00

6.4 KiB

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) importm :: #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 dimensionerror: 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:

CAP :: 8;

issues/0192-qualified-import-const-not-comptime.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;
}
./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: <pack>.len, <IntType>.min/.max (via TypeResolver.integerLimitFor), and <struct-const>.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.CAPunresolved '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.