fix(resolver): type-author-aware + local-safe bare-TYPE gate; R3 model escalated [stdlib E1 attempt-3]

R1 (type-author-aware gate): the bare-TYPE visibility gate now requires a
flat-import-reachable TYPE author (struct/enum/union/error-set/protocol/foreign
class). A same-name flat VALUE/FUNCTION no longer makes a namespaced-only TYPE
bare-visible — the name-only `m.names.contains` check (attempt-2) is replaced by
`moduleAuthorsType` (kind-checked via `RawDeclRef`). Regression 0745.

R2 (no local false-positive): a block-local type clobbers the global type-table
entry for its name (`registerStructDecl`'s findByName-orelse-intern +
updatePreservingKey), so it IS the resolved type — never a namespaced-only leak.
A new `local_type_names` set, populated at both block-local type-decl paths,
exempts such names from the gate. Regression 0746.

readme.md: drop the false "transitively" claim — flat-import bare visibility for
functions and constants is NON-transitive (0706).

R3 (foundational model consistency) is ESCALATED, not resolved here — see the
attempt-3 worker report. Ground truth: making the TYPE gate single-hop (to match
the value/function model) breaks ~19 tests, ~13 of them library-INTERNAL generic
refs (e.g. `List.append`'s `alloc: Allocator`, lowered in the caller's source
context). That needs source-pinning generic instantiation to the template's
defining module — a separate architectural piece beyond E1's leaf-cut scope, and
proven risky (a `monomorphizeFunction` pin broke 4 FFI objc-block tests and did
not even take, since template method bodies lack a reliable `source_file`). The
TYPE gate therefore stays on the (type-author-aware) transitive flat closure for
E1; the non-transitive reconciliation is a routed follow-up.
This commit is contained in:
agra
2026-06-07 17:51:09 +03:00
parent 7188481761
commit 4bd57c857e
13 changed files with 179 additions and 72 deletions

View File

@@ -208,6 +208,15 @@ pub const Lowering = struct {
/// `CAllocator` behind a namespace edge from `main`, so the user-visibility
/// gate would reject it) — so the bare TYPE leaf falls open here (F1).
emitting_default_context: bool = false,
/// Names declared as a BLOCK-LOCAL type (a `Foo :: struct/enum/union/error_set`
/// or bare type-decl statement inside a fn / init body). A local type registers
/// into the global type table and CLOBBERS a same-name top-level entry
/// (`registerStructDecl`'s `findByName … orelse intern` + `updatePreservingKey`),
/// so after it lowers the name IS the local type program-wide (single-author,
/// pre-E2). The source-aware bare-TYPE gate consults this so a legitimately
/// block-local type is never mistaken for a namespaced-only leak — even when a
/// namespaced-only import happens to author a top-level type of the same name.
local_type_names: std.StringHashMap(void) = std.StringHashMap(void).init(std.heap.page_allocator),
struct_defaults_map: std.StringHashMap([]const ?*const Node) = std.StringHashMap([]const ?*const Node).init(std.heap.page_allocator), // struct name → field defaults
struct_instance_bindings: std.StringHashMap(std.StringHashMap(TypeId)) = std.StringHashMap(std.StringHashMap(TypeId)).init(std.heap.page_allocator), // mangled struct name → type param bindings
struct_instance_template: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // mangled struct name → template name
@@ -1779,46 +1788,63 @@ pub const Lowering = struct {
if (name.len > 0 and (name[0] == '[' or name[0] == '*' or name[0] == '?')) {
return .{ .resolved = self.typeResolver().resolveName(name, raw) };
}
// Registered named type — gated on BARE-FLAT visibility (F1, the type
// analog of Phase B's value/function tightening). A namespaced-only type
// is registered GLOBALLY yet is reachable from the querying module only
// over a namespace edge, so without this gate its bare reference leaked
// through the global `findByName` first-match. The gate is the TRANSITIVE
// flat-import reachability `typeBareVisible` — NOT `collectVisibleAuthors`,
// which walks each module's OWN decls single-hop and would false-negative
// a type two flat hops away (e.g. `CAllocator`, reached `main → std.sx →
// allocators.sx` over two flat edges). Single-author (E1): the unique
// `findByName` match IS the one bare-visible author's TypeId, so a
// bare-visible name resolves byte-identically; E2 routes this through the
// collector-selected author's per-source `nominal_id` once same-name type
// shadows register.
// Bare nominal name: select its author over the ONE graph-walk collector
// (`typeBareVisibleAsType` — flat-import reachable TYPE author) plus the
// single-hop collector for the alias path. A bare TYPE name is visible iff
// a flat-import-reachable module authors it AS A TYPE (R1: a same-name
// VALUE/FUNCTION does not count); a namespaced-only TYPE is registered
// GLOBALLY yet reachable only over a namespace edge, so without this gate
// its bare reference leaked through `findByName`'s global first-match.
//
// The TYPE reachability here is the TRANSITIVE flat-import closure, NOT the
// single-hop `collectVisibleAuthors`/`isNameVisible` set the bare VALUE /
// FUNCTION / CONST leaves use. That asymmetry (types transitive, values
// non-transitive — 0706) is the open model-consistency question (R3, E1):
// the value/function model needs the source pin for a library template's
// INTERNAL type refs (`List.append`'s `alloc: Allocator`, instantiated in
// the caller's source context) before the type gate can go single-hop too
// — see the worker report. Until that lands, the transitive type closure
// is the only byte-identical option; the gate stays type-author-aware and
// local-safe regardless of which reachability E1.x settles on.
const name_id = table.internString(name);
if (table.findByName(name_id)) |existing| {
const registered = table.findByName(name_id);
// Registered named type (struct/enum/union/error_set/protocol/foreign
// class) — gated on flat-import TYPE visibility (F1, the type analog of
// Phase B's value/function tightening).
if (registered) |existing| {
// Compiler-synthesized default-Context emission resolves the built-in
// allocator types as infrastructure — fall open (the gate is for
// USER bare references, not compiler internals).
// allocator types as infrastructure — fall open (the gate is for USER
// bare references, not compiler internals).
if (self.emitting_default_context) return .{ .resolved = existing };
// The gate applies ONLY to a TOP-LEVEL type author — a `name` declared
// in some module's raw facts (`module_decls`). A LOCAL type (declared
// inside a fn / init block), a generic type-param, and a fabricated
// empty-struct stub are all findByName-registered yet authored in NO
// `module_decls`; they are not bare cross-module references, so they
// resolve ungated and byte-identically (their own diagnostics —
// unknown-type / value-param — still fire in the dedicated pass).
if (self.nameAuthoredAnywhere(name)) {
if (self.typeBareVisible(name, from)) return .{ .resolved = existing };
// Registered top-level type reachable ONLY through a namespaced
// import: a named type is never a `const`, so the alias path
// cannot apply — return `.not_visible` so the leaf does not leak
// the global match; `resolveNominalLeaf` surfaces the diagnostic
// and the `.unresolved` sentinel (qualify it `ns.Type`, Phase F).
return .not_visible;
}
// A flat-import-reachable TYPE author makes the bare reference visible.
// The author must be a TYPE — a same-name flat VALUE/FUNCTION does NOT
// make a namespaced-only type bare-visible (R1). Single-author (E1): the
// unique `findByName` match IS that author's TypeId, so a bare-visible
// name resolves byte-identically; E2 routes this through the collector-
// selected author's per-source `nominal_id` once same-name type shadows
// register.
if (self.typeBareVisibleAsType(name, from)) return .{ .resolved = existing };
// A block-local type (declared inside a fn / init body) clobbers the
// global entry for its name, so `existing` IS that local type — never a
// namespaced-only leak. Resolve it ungated (R2): a legitimately-scoped
// local must not be rejected just because a namespaced-only import also
// authors a top-level type of the same name.
if (self.local_type_names.contains(name)) return .{ .resolved = existing };
// Registered as a TOP-LEVEL named TYPE in some module, but NOT flat-
// import-reachable from `from` and NOT shadowed by a local → reachable
// only over a namespace edge → leak. Return `.not_visible`;
// `resolveNominalLeaf` surfaces the diagnostic and the `.unresolved`
// sentinel (qualify it `ns.Type`, Phase F).
if (self.nameAuthoredAsTypeAnywhere(name)) return .not_visible;
// Not a top-level type author anywhere — a generic type-param's bound
// or a fabricated empty-struct stub. Not a bare cross-module reference;
// resolve ungated (its own diagnostics still fire in the dedicated pass).
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
// Type alias `A :: B`. Select the alias author over the single-hop
// 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();
@@ -1844,23 +1870,45 @@ pub const Lowering = struct {
return null;
}
/// TRUE iff bare `name` is reachable from `from` over the TRANSITIVE
/// flat-import closure (own decls every transitively flat-imported module's
/// own decls). The correct `.user_bare_flat` reachability for the TYPE leaf
/// (F1): a flat import is transitive for resolution — the global decl list a
/// module lowers against is the FULL transitive flat list — so a type two flat
/// hops away (`CAllocator`, reached `main → std.sx → allocators.sx`) IS
/// bare-visible, while a namespaced-only type (reached solely over a namespace
/// edge, never recorded in `flat_import_graph`) is NOT. The single-hop
/// predicates (`isNameVisible` / `collectVisibleAuthors`, own DIRECT flat
/// deps) would false-negate the transitive case. This closure walk lives in
/// `lower.zig`, NOT `resolver.zig`, so the single-graph-walk invariant (one
/// TRUE iff `raw` declares a NAMED TYPE — struct / enum / union / error-set /
/// protocol / foreign class. A `fn_decl`, a value-or-alias `const_decl`, and a
/// `namespace_decl` are NOT named types (a type alias is a `const_decl` whose
/// `findByName` lookup fails, so it never reaches the named-type gate; it
/// resolves through the alias path keyed by the const author instead).
fn isNamedTypeKind(raw: resolver_mod.RawDeclRef) bool {
return switch (raw) {
.struct_decl, .enum_decl, .union_decl, .error_set_decl, .protocol_decl, .foreign_class_decl => true,
.fn_decl, .const_decl, .namespace_decl => false,
};
}
/// TRUE iff module `path` authors a top-level decl named `name` AS A TYPE
/// (struct/enum/union/error-set/protocol/foreign class). A same-name
/// VALUE/FUNCTION author returns false (R1: the gate is type-author-aware, not
/// name-only) — the per-module leaf of `typeBareVisibleAsType`'s closure walk.
fn moduleAuthorsType(decls: *imports_mod.ModuleDecls, path: []const u8, name: []const u8) bool {
const m = decls.get(path) orelse return false;
const ref = m.names.get(name) orelse return false;
return isNamedTypeKind(ref);
}
/// TRUE iff bare `name` is reachable from `from` AS A TYPE over the flat-import
/// closure (own decls every transitively flat-imported module's own decls).
/// This is the type-author-aware analog of the value/function visibility, but
/// TRANSITIVE rather than single-hop — the open R3 asymmetry (see the gate
/// comment in `selectNominalLeaf`): a library template's INTERNAL type ref
/// (`List.append`'s `alloc: Allocator`, declared in `std.sx` but instantiated
/// in the caller's source context) is two flat hops from the caller, and the
/// value/function source-pin that would let the type gate go single-hop too is
/// not yet wired for generic instantiation. A same-name flat VALUE/FUNCTION does
/// NOT make the name type-visible (R1). The closure walk lives in `lower.zig`,
/// NOT `resolver.zig`, so the single-graph-walk invariant (one
/// `flat_import_graph` iterator in `resolver.zig`) is untouched. Falls open
/// (visible) when the scoping facts are unwired (comptime / registration).
fn typeBareVisible(self: *Lowering, name: []const u8, from: []const u8) bool {
fn typeBareVisibleAsType(self: *Lowering, name: []const u8, from: []const u8) bool {
const decls = self.program_index.module_decls orelse return true;
const graph = self.program_index.flat_import_graph orelse return true;
if (moduleAuthorsName(decls, from, name)) return true;
if (moduleAuthorsType(decls, from, name)) return true;
var visited = std.StringHashMap(void).init(self.alloc);
defer visited.deinit();
var queue = std.ArrayList([]const u8).empty;
@@ -1875,33 +1923,34 @@ pub const Lowering = struct {
const dep = kv.key_ptr.*;
if (visited.contains(dep)) continue;
visited.put(dep, {}) catch continue;
if (moduleAuthorsName(decls, dep, name)) return true;
if (moduleAuthorsType(decls, dep, name)) return true;
queue.append(self.alloc, dep) catch continue;
}
}
return false;
}
/// TRUE iff module `path` authors a top-level decl named `name` (the Phase A
/// raw-fact membership — own decls only, the per-module leaf of the closure
/// walk in `typeBareVisible`).
fn moduleAuthorsName(decls: *imports_mod.ModuleDecls, path: []const u8, name: []const u8) bool {
const m = decls.get(path) orelse return false;
return m.names.contains(name);
}
/// TRUE iff `name` is authored as a TOP-LEVEL decl in ANY module's raw facts.
/// Distinguishes a real cross-module type author (the only thing the bare-flat
/// visibility gate polices) from a LOCAL type / generic-param / fabricated
/// empty-struct stub, which are findByName-registered but authored in no
/// `module_decls`. Unwired facts → false (nothing to gate; resolve ungated).
fn nameAuthoredAnywhere(self: *Lowering, name: []const u8) bool {
/// TRUE iff `name` is authored as a TOP-LEVEL NAMED TYPE in ANY module's raw
/// facts. Distinguishes a real cross-module TYPE author (the only thing the
/// bare-flat visibility gate polices) from a LOCAL type / generic-param /
/// fabricated empty-struct stub (findByName-registered but authored in no
/// `module_decls`) and from a same-name VALUE/FUNCTION author (not a type).
/// Unwired facts → false (nothing to gate; resolve ungated).
fn nameAuthoredAsTypeAnywhere(self: *Lowering, name: []const u8) bool {
const decls = self.program_index.module_decls orelse return false;
var it = decls.valueIterator();
while (it.next()) |m| if (m.names.contains(name)) return true;
while (it.next()) |m| {
if (m.names.get(name)) |ref| if (isNamedTypeKind(ref)) return true;
}
return false;
}
/// Record a name declared as a BLOCK-LOCAL type so the bare-TYPE gate never
/// mistakes it for a namespaced-only leak (see `local_type_names`).
fn recordLocalTypeName(self: *Lowering, name: []const u8) void {
self.local_type_names.put(name, {}) catch {};
}
/// 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
@@ -2697,11 +2746,18 @@ pub const Lowering = struct {
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void);
},
// Block-local type declarations
.struct_decl => |sd| self.registerStructDecl(&sd),
.struct_decl => |sd| {
self.recordLocalTypeName(sd.name);
self.registerStructDecl(&sd);
},
.enum_decl, .union_decl => {
if (node.data.declName()) |dn| self.recordLocalTypeName(dn);
_ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
},
.error_set_decl => self.registerErrorSetDecl(node),
.error_set_decl => {
if (node.data.declName()) |dn| self.recordLocalTypeName(dn);
self.registerErrorSetDecl(node);
},
.ufcs_alias => |ua| {
self.program_index.ufcs_alias_map.put(ua.name, ua.target) catch {};
},
@@ -2865,10 +2921,12 @@ pub const Lowering = struct {
// Handle local type declarations: MyType :: struct/union/enum { ... }
if (cd.value.data == .struct_decl) {
self.recordLocalTypeName(cd.name);
self.registerStructDecl(&cd.value.data.struct_decl);
return;
}
if (cd.value.data == .enum_decl or cd.value.data == .union_decl) {
self.recordLocalTypeName(cd.name);
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
return;
}