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:
190
src/ir/lower.zig
190
src/ir/lower.zig
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user