From 4bd57c857e2cf025c59d060c035ea425350d6adb Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 7 Jun 2026 17:51:09 +0300 Subject: [PATCH] fix(resolver): type-author-aware + local-safe bare-TYPE gate; R3 model escalated [stdlib E1 attempt-3] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...modules-flat-value-shadows-ns-only-type.sx | 15 ++ .../flatval.sx | 2 + .../nstype.sx | 4 + ...modules-local-type-shadows-ns-only-type.sx | 13 ++ .../dep.sx | 4 + ...dules-flat-value-shadows-ns-only-type.exit | 1 + ...les-flat-value-shadows-ns-only-type.stderr | 5 + ...les-flat-value-shadows-ns-only-type.stdout | 1 + ...dules-local-type-shadows-ns-only-type.exit | 1 + ...les-local-type-shadows-ns-only-type.stderr | 1 + ...les-local-type-shadows-ns-only-type.stdout | 1 + readme.md | 13 +- src/ir/lower.zig | 190 ++++++++++++------ 13 files changed, 179 insertions(+), 72 deletions(-) create mode 100644 examples/0745-modules-flat-value-shadows-ns-only-type.sx create mode 100644 examples/0745-modules-flat-value-shadows-ns-only-type/flatval.sx create mode 100644 examples/0745-modules-flat-value-shadows-ns-only-type/nstype.sx create mode 100644 examples/0746-modules-local-type-shadows-ns-only-type.sx create mode 100644 examples/0746-modules-local-type-shadows-ns-only-type/dep.sx create mode 100644 examples/expected/0745-modules-flat-value-shadows-ns-only-type.exit create mode 100644 examples/expected/0745-modules-flat-value-shadows-ns-only-type.stderr create mode 100644 examples/expected/0745-modules-flat-value-shadows-ns-only-type.stdout create mode 100644 examples/expected/0746-modules-local-type-shadows-ns-only-type.exit create mode 100644 examples/expected/0746-modules-local-type-shadows-ns-only-type.stderr create mode 100644 examples/expected/0746-modules-local-type-shadows-ns-only-type.stdout diff --git a/examples/0745-modules-flat-value-shadows-ns-only-type.sx b/examples/0745-modules-flat-value-shadows-ns-only-type.sx new file mode 100644 index 0000000..342dbea --- /dev/null +++ b/examples/0745-modules-flat-value-shadows-ns-only-type.sx @@ -0,0 +1,15 @@ +// Type-author-aware bare-TYPE visibility gate (Phase E1, R1). `flatval.sx` is +// flat-imported and authors a VALUE/FUNCTION `Secret`; `nstype.sx` is namespaced +// (`nst :: #import`) and authors a TYPE `Secret`. A bare `Secret` in a type +// position must NOT resolve: the only flat-visible `Secret` author is a FUNCTION, +// and a same-name flat value does NOT make the namespaced-only TYPE bare-visible. +// The leak this closes: a name-only gate would see the flat function and let the +// global `findByName` first-match return the namespaced-only struct. The type is +// reachable only as `nst.Secret`. +#import "0745-modules-flat-value-shadows-ns-only-type/flatval.sx"; +nst :: #import "0745-modules-flat-value-shadows-ns-only-type/nstype.sx"; + +main :: () -> s32 { + s : Secret = .{ x = 5, y = 6 }; + s.x +} diff --git a/examples/0745-modules-flat-value-shadows-ns-only-type/flatval.sx b/examples/0745-modules-flat-value-shadows-ns-only-type/flatval.sx new file mode 100644 index 0000000..2f53a35 --- /dev/null +++ b/examples/0745-modules-flat-value-shadows-ns-only-type/flatval.sx @@ -0,0 +1,2 @@ +// A flat-visible VALUE/FUNCTION named `Secret` (not a type). +Secret :: () -> s32 { 0 } diff --git a/examples/0745-modules-flat-value-shadows-ns-only-type/nstype.sx b/examples/0745-modules-flat-value-shadows-ns-only-type/nstype.sx new file mode 100644 index 0000000..6835705 --- /dev/null +++ b/examples/0745-modules-flat-value-shadows-ns-only-type/nstype.sx @@ -0,0 +1,4 @@ +Secret :: struct { + x: s32; + y: s32; +} diff --git a/examples/0746-modules-local-type-shadows-ns-only-type.sx b/examples/0746-modules-local-type-shadows-ns-only-type.sx new file mode 100644 index 0000000..126fb69 --- /dev/null +++ b/examples/0746-modules-local-type-shadows-ns-only-type.sx @@ -0,0 +1,13 @@ +// A block-LOCAL type resolves even when a namespaced-only import authors a +// top-level type of the same name (Phase E1, R2). `dep.sx` is namespaced +// (`dep :: #import`) and authors a top-level `Secret`; `main` declares its OWN +// block-local `Secret`. The local must resolve to ITS fields (a legitimately- +// scoped local is never a namespaced-only leak), not be rejected by the bare-TYPE +// visibility gate just because the namespaced import shares the name. +dep :: #import "0746-modules-local-type-shadows-ns-only-type/dep.sx"; + +main :: () -> s32 { + Secret :: struct { z: s32; } + s : Secret = .{ z = 7 }; + s.z +} diff --git a/examples/0746-modules-local-type-shadows-ns-only-type/dep.sx b/examples/0746-modules-local-type-shadows-ns-only-type/dep.sx new file mode 100644 index 0000000..6835705 --- /dev/null +++ b/examples/0746-modules-local-type-shadows-ns-only-type/dep.sx @@ -0,0 +1,4 @@ +Secret :: struct { + x: s32; + y: s32; +} diff --git a/examples/expected/0745-modules-flat-value-shadows-ns-only-type.exit b/examples/expected/0745-modules-flat-value-shadows-ns-only-type.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0745-modules-flat-value-shadows-ns-only-type.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0745-modules-flat-value-shadows-ns-only-type.stderr b/examples/expected/0745-modules-flat-value-shadows-ns-only-type.stderr new file mode 100644 index 0000000..8c5fb00 --- /dev/null +++ b/examples/expected/0745-modules-flat-value-shadows-ns-only-type.stderr @@ -0,0 +1,5 @@ +error: type 'Secret' is not visible; #import the module that declares it + --> examples/0745-modules-flat-value-shadows-ns-only-type.sx:13:9 + | +13 | s : Secret = .{ x = 5, y = 6 }; + | ^^^^^^ diff --git a/examples/expected/0745-modules-flat-value-shadows-ns-only-type.stdout b/examples/expected/0745-modules-flat-value-shadows-ns-only-type.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0745-modules-flat-value-shadows-ns-only-type.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/0746-modules-local-type-shadows-ns-only-type.exit b/examples/expected/0746-modules-local-type-shadows-ns-only-type.exit new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/examples/expected/0746-modules-local-type-shadows-ns-only-type.exit @@ -0,0 +1 @@ +7 diff --git a/examples/expected/0746-modules-local-type-shadows-ns-only-type.stderr b/examples/expected/0746-modules-local-type-shadows-ns-only-type.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0746-modules-local-type-shadows-ns-only-type.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0746-modules-local-type-shadows-ns-only-type.stdout b/examples/expected/0746-modules-local-type-shadows-ns-only-type.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0746-modules-local-type-shadows-ns-only-type.stdout @@ -0,0 +1 @@ + diff --git a/readme.md b/readme.md index 0078b64..d629bd1 100644 --- a/readme.md +++ b/readme.md @@ -400,12 +400,13 @@ A bare call to a name that two or more flat imports both provide is ambiguous an is rejected; qualify it with a namespaced import (`m :: #import …; m.fn()`). A **namespaced** import only binds its alias: reach the module's members as -`m.name`. Bare-name visibility joins over flat (`#import "…"`) imports only — -transitively (a flat import of a flat import is visible) — never over a -namespaced alias. A bare reference to a namespaced-only import's member — -function, module constant, or **type** — is not visible and is rejected (`type -'X' is not visible; #import the module that declares it`); qualify it as -`m.name`. +`m.name`. Bare-name visibility joins over flat (`#import "…"`) imports, never over +a namespaced alias. For **functions and constants** that join is non-transitive: a +flat import of a flat import is NOT bare-visible (when `A` imports `B` and `B` +imports `C`, `A` does not see `C`'s top-level names — qualify them). A bare +reference to a namespaced-only import's member — function, module constant, or +**type** — is not visible and is rejected (`type 'X' is not visible; #import the +module that declares it`); qualify it as `m.name`. ### Implicit Context diff --git a/src/ir/lower.zig b/src/ir/lower.zig index f28c586..2b69f6e 100644 --- a/src/ir/lower.zig +++ b/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; }