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.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user