feat(resolver): source-aware nominal leaf (Lowering-side) + ns-const tightening [stdlib E1]

Route the Lowering-side bare type leaf through the source-keyed caches (E0):
nominal author via collectVisibleAuthors(.user_bare_flat) + alias via
type_aliases_by_source, instead of the global findByName first-match. The
binding-free resolveAstType path + registration sites stay on the global
compat readers (move later). Single-author resolution byte-identical (no
shadows yet). Folded req #1: a namespaced-only import's const is no longer
bare-visible in array-dim/comptime-scalar position. Adds regression 0742
(ns-only bare const) and 0210 (generics/Vector/type-fn stay legacy).

Salvaged from a worker killed at the wall before commit; manager verified
the gate at ground truth (zig build test exit 0; run_examples 479/0 with
0210+0742 ok, prior 477 byte-identical; m3te ios-sim exit 0; folded fix
confirmed fail-before on master 7ffc0c1 exit 0 / pass-after exit 1).
This commit is contained in:
agra
2026-06-07 15:24:43 +03:00
parent a5d6c940dd
commit 7cd12b0ed5
10 changed files with 212 additions and 7 deletions

View File

@@ -0,0 +1,31 @@
// Resolver E1 lock: the bare-type-leaf cutover to the source-aware
// `selectNominalLeaf` must NOT touch the NON-leaf type heads. A generic-struct
// instantiation (`Box(s32)`), a `Vector(N, T)` builtin, and a type-returning
// function (`Make(3, s64)`) are all resolved by `resolveTypeWithBindings`
// ABOVE the bare-name leaf switch (`resolveParameterizedWithBindings` /
// `resolveTypeCallWithBindings` / the `Vector` builtin path), so they stay on
// the legacy resolution and never reach `selectNominalLeaf`. Parameterized
// protocols share the same `resolveParameterizedWithBindings` pre-leaf path
// (covered by 0204/0206). This example pins that all three still resolve
// identically after the cutover.
#import "modules/std.sx";
Box :: struct($T: Type) {
value: T;
}
Make :: ($K: u32, $T: Type) -> Type { return [K]T; }
N :: 3;
main :: () {
b : Box(s32) = .{ value = 42 };
print("box: {}\n", b.value);
v : Vector(4, f32) = .[1, 2, 3, 4];
print("vec: {} {}\n", v.x, v.w);
a : Make(N, s64) = ---;
a[0] = 10; a[2] = 30;
print("typefn: len={} a0={} a2={}\n", a.len, a[0], a[2]);
}

View File

@@ -0,0 +1,15 @@
// Bare module-const visibility under a NAMESPACED-only import — the const
// sibling of 0736 (folded req #1 of the source-aware resolver, Phase E1).
// `dep.sx` is imported only as `dep :: #import`, so its top-level `DEP_LEN`
// is reachable ONLY as `dep.DEP_LEN`. A BARE `DEP_LEN` in a comptime
// array-dimension position must NOT resolve: bare module-const visibility
// joins over the FLAT import edges (`flat_import_graph`), and a namespaced
// alias is not a flat edge. Before the fix the bare const leaked through the
// global `module_const_map` first-match (and its float-fold fallback) straight
// into the `[VAL]s32` dimension, so the array silently got length 3.
dep :: #import "0742-modules-namespaced-only-bare-const-not-visible/dep.sx";
main :: () -> s32 {
arr : [DEP_LEN]s32 = ---;
0
}

View File

@@ -0,0 +1 @@
DEP_LEN :: 3;

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
box: 42
vec: 1.000000 4.000000
typefn: len=3 a0=10 a2=30

View File

@@ -0,0 +1,5 @@
error: array dimension must be a compile-time integer constant
--> examples/0742-modules-namespaced-only-bare-const-not-visible.sx:13:12
|
13 | arr : [DEP_LEN]s32 = ---;
| ^^^^^^^

View File

@@ -1636,6 +1636,23 @@ pub const Lowering = struct {
materialized: ?FuncId = null,
};
/// Outcome of the source-aware bare TYPE leaf (`selectNominalLeaf`, R5 §E).
/// The type-position analogue of `BareCallee`: the nominal author is selected
/// over the ONE graph-walk collector and resolved against the source-keyed
/// caches, never the global `findByName` first-match / global alias map.
pub const TypeHeadResolution = union(enum) {
/// A builtin primitive, a registered named type, or a resolved alias.
resolved: TypeId,
/// A const author is visible but its alias target is not resolved yet —
/// a forward identifier alias. Routes back into the existing
/// `resolveForwardIdentifierAliases` fixpoint (source-aware in E1.5).
pending,
/// No flat-visible (own flat-import) author declares `name` as a type.
/// E1 keeps the existing empty-struct stub; E3 turns this into the
/// `.unresolved` sentinel + a diagnostic.
undeclared,
};
/// THE plain bare-name call selector (fix-0102c, R5 §C). `resolveBareCallee`'s
/// body verbatim, now over the Phase B author collector
/// (`resolver.collectVisibleAuthors` — the ONE graph-walk) instead of a direct
@@ -1709,6 +1726,99 @@ pub const Lowering = struct {
return .{ .func = .{ .decl = the_one.?, .source = the_source } };
}
/// THE source-aware bare TYPE leaf (R5 §E, E1). The type-position analogue
/// of `selectPlainCallableAuthor`: resolve a bare type name `name` referenced
/// from `from` by selecting its nominal author over the ONE graph-walk
/// collector (`resolver.collectVisibleAuthors`) and reading the alias from the
/// source-keyed cache (`type_aliases_by_source`, E0's write side) keyed by the
/// selected author's OWN source — never the global `findByName` first-match
/// nor the global `type_alias_map`.
///
/// `raw` is the backtick raw-identifier escape (issue 0089): a raw reference
/// bypasses the builtin classifier and resolves only through the nominal
/// author / alias path.
///
/// E1 is single-author: `collectVisibleAuthors` returns ≤1 author, so the
/// selection is unambiguous and resolution is byte-identical to the legacy
/// leaf. Same-name shadows (≥2 authors) and the `.ambiguous` outcome (0105)
/// land in E2; the per-author `nominal_id` TypeId that makes a shadow
/// representable also lands then (today a registered named type resolves to
/// its unique `findByName` match, which IS the single author's TypeId).
/// Generic / parameterized-protocol / Vector / type-function heads never
/// reach this leaf — `resolveTypeWithBindings` owns those above the leaf
/// switch, so they stay legacy.
pub fn selectNominalLeaf(self: *Lowering, name: []const u8, from: []const u8, raw: bool) TypeHeadResolution {
const table = &self.module.types;
// Builtin primitive keyword / arbitrary-width int — unless a raw escape
// routes the literal name straight to nominal resolution.
if (!raw) {
if (TypeResolver.resolveBuiltinName(name, table)) |id| return .{ .resolved = id };
}
// Structural string-forms that reach the leaf as a literal type-expr
// name (`[:0]u8` → string, `[*]T`, `*T`, `?T`) carry NO nominal author —
// they are wrappers, not declarations, so source-keying does not apply.
// Resolve them through the stateless namer exactly as the legacy leaf
// did; only the bare nominal name below cuts over to the collector.
if (name.len > 0 and (name[0] == '[' or name[0] == '*' or name[0] == '?')) {
return .{ .resolved = self.typeResolver().resolveName(name, raw) };
}
// Registered named type. Single-author (E1): its unique registered
// TypeId. `findByName` stays the byte-identical resolver here — it also
// reaches a namespaced-only type referenced bare (the global leak 0719
// relies on); E2 routes this through the collector-selected author's
// per-source `nominal_id` once same-name type shadows register, and E3
// turns a true miss into the `.unresolved` sentinel + a diagnostic.
const name_id = table.internString(name);
if (table.findByName(name_id)) |existing| return .{ .resolved = existing };
// Type alias `A :: B`. Select the alias author over the ONE graph-walk
// collector and read its target from the source-keyed cache, keyed by
// the author's OWN declaring source (E0's write side) — this is where the
// global-alias-leak (0104-F2) fix begins, replacing the global
// `type_alias_map` first-match for a flat-visible alias.
var res = self.resolver();
const set = res.collectVisibleAuthors(name, from, .user_bare_flat);
defer if (set.flat.len > 0) self.alloc.free(set.flat);
if (constAuthor(set)) |author| {
if (self.program_index.type_aliases_by_source.get(author.source)) |inner| {
if (inner.get(name)) |alias_ty| return .{ .resolved = alias_ty };
}
// Const author visible but its alias target is not resolved yet —
// a forward identifier alias → `.pending`, back to the fixpoint.
return .pending;
}
return .undeclared;
}
/// The single `const_decl` (alias-or-value const) author of a collected
/// `AuthorSet`. E1 is single-author (`collectVisibleAuthors` returns ≤1), so
/// own-then-flat picks the one author. E2 adds shadow ambiguity.
fn constAuthor(set: resolver_mod.AuthorSet) ?resolver_mod.RawAuthor {
if (set.own) |o| if (o.raw == .const_decl) return o;
for (set.flat) |fa| if (fa.raw == .const_decl) return fa;
return null;
}
/// Resolve the bare TYPE leaf to a `TypeId` for `resolveTypeWithBindings`.
/// Routes through the source-aware `selectNominalLeaf`; `.pending` /
/// `.undeclared` keep the legacy empty-struct stub (E3 turns these into the
/// `.unresolved` sentinel + a diagnostic). When the source context is unwired
/// (`current_source_file` null — comptime / registration callers), there is
/// no querying module to collect from, so fall open to the legacy namer.
fn resolveNominalLeaf(self: *Lowering, name: []const u8, raw: bool) TypeId {
const from = self.current_source_file orelse
return self.typeResolver().resolveName(name, raw);
return switch (self.selectNominalLeaf(name, from, raw)) {
.resolved => |t| t,
// The legacy empty-struct stub for an as-yet-unregistered / forward
// name — `resolveNamed`'s tail, reproduced for byte-identity. A raw
// or non-raw bare name both land the same struct stub here.
.pending, .undeclared => self.module.types.intern(.{ .@"struct" = .{
.name = self.module.types.internString(name),
.fields = &.{},
} }),
};
}
/// The `*FnDecl` a raw author wraps, or null when the author is not a
/// function — `imports.fnDeclOf` over a `RawDeclRef` so the collector's
/// all-domain authors reproduce `module_fns`' fn-only view (a `const`-wrapped
@@ -12849,6 +12959,7 @@ pub const Lowering = struct {
/// `evalConstIntExpr` delegation inside `evalConstFloatExpr`; this surfaces the
/// non-integral float const so the rule can reject it.
pub fn lookupFloatName(self: *Lowering, name: []const u8) ?f64 {
if (self.moduleConstBareInvisible(name)) return null;
return program_index_mod.moduleConstFloat(&self.program_index.module_const_map, &self.module.types, name);
}
@@ -12859,6 +12970,7 @@ pub const Lowering = struct {
/// value bindings are always integer-valued, so only the module-const table
/// can name a float.
pub fn nameIsFloatTyped(self: *Lowering, name: []const u8) bool {
if (self.moduleConstBareInvisible(name)) return false;
return program_index_mod.moduleConstIsFloatTyped(&self.program_index.module_const_map, &self.module.types, name);
}
@@ -12871,12 +12983,45 @@ pub const Lowering = struct {
if (self.comptime_value_bindings) |cvb| {
if (cvb.get(name)) |v| return v;
}
// Folded req #1: gate the bare module const on source-aware visibility
// before reading the global map (see `moduleConstBareInvisible`).
if (self.moduleConstBareInvisible(name)) return null;
// 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, &self.module.types, name);
}
/// Folded req #1: TRUE iff `name` is a module const that is NOT reachable
/// bare from the querying module — the source-aware gate every Lowering-side
/// comptime `module_const_map` reader (`comptimeIntNamed` / `lookupFloatName`
/// / `nameIsFloatTyped`) consults before the global first-match. A
/// namespaced-only import's const must be qualified (`ns.X`); without this
/// gate a bare reference leaks into a comptime-scalar / array-dim position
/// through the global table (the int folder even falls back to the float
/// reader, so all three must gate). The value itself is still folded over the
/// global map, so a cross-module const CHAIN (`N :: M + 1`, M flat-imported)
/// resolves exactly as before; the stateless `type_bridge` registration path
/// keeps the global reader this step. A main-file body carries a null
/// `current_source_file` (it IS the root), so the querying module is
/// `main_file` there; a fully unwired index (no source at all) falls open.
fn moduleConstBareInvisible(self: *Lowering, name: []const u8) bool {
const from = self.current_source_file orelse self.main_file orelse return false;
var res = self.resolver();
const set = res.collectVisibleAuthors(name, from, .user_bare_flat);
defer if (set.flat.len > 0) self.alloc.free(set.flat);
if (set.own) |o| if (self.sourceHasModuleConst(o.source, name)) return false;
for (set.flat) |fa| if (self.sourceHasModuleConst(fa.source, name)) return false;
return true;
}
/// True iff `source`'s per-source const cache declares `name` (E0's
/// `module_consts_by_source` write side).
fn sourceHasModuleConst(self: *Lowering, source: []const u8, name: []const u8) bool {
const inner = self.program_index.module_consts_by_source.get(source) orelse return false;
return inner.contains(name);
}
/// Resolve a type node, checking type_bindings first for generic type params.
pub fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId {
// Pack-index in a type position: `$<pack>[<lit>]` resolves to the
@@ -12968,14 +13113,15 @@ pub const Lowering = struct {
if (node.data == .type_expr and node.data.type_expr.is_generic) {
return .unresolved;
}
// Bare type names resolve through TypeResolver, which reads the
// canonical alias table directly (`ProgramIndex.type_alias_map`). Other
// node kinds (inline type decls, error types) still route through
// type_bridge, which now takes the alias map as an explicit argument
// (the `TypeTable.aliases` borrow is gone, A2.3).
// Bare type names resolve through the source-aware `selectNominalLeaf`
// (E1): the nominal author is selected over the ONE graph-walk collector
// and resolved against the source-keyed caches, not the global
// `findByName` first-match / global alias map. Other node kinds (inline
// type decls, error types) still route through type_bridge, which reads
// the global compat maps (cut over in a later phase).
switch (node.data) {
.type_expr => |te| return self.typeResolver().resolveName(te.name, te.is_raw),
.identifier => |id| return self.typeResolver().resolveName(id.name, id.is_raw),
.type_expr => |te| return self.resolveNominalLeaf(te.name, te.is_raw),
.identifier => |id| return self.resolveNominalLeaf(id.name, id.is_raw),
// A non-spread tuple literal in a type position is a tuple-type
// literal (`(s32, s32)`); validate its elements are types and reject
// non-type elements loudly (issue 0067).