From 33a6f5c650b79ed3c3484dee3ced09cac6f75249 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 8 Jun 2026 11:12:08 +0300 Subject: [PATCH] wip(E4): partial source-pin + non-transitive flip [stdlib E4 attempt-1 WIP checkpoint] Incomplete WIP from a worker killed at the 55-min wall (large blast radius: core source-pin + ~8 example migrations + ~10 library module migrations). Committed so the resumed session continues on a clean tree. May not build. --- examples/0129-types-tuple-operators.sx | 1 + examples/0411-protocols-impl-duplicate.sx | 3 + ...9-modules-issue-0056-diamond-param-impl.sx | 3 + examples/0713-modules-json-writer.sx | 1 + examples/0714-modules-json-reader.sx | 1 + examples/0715-modules-json-suite.sx | 1 + examples/0719-modules-cli-and-json.sx | 1 + ...0763-modules-import-type-non-transitive.sx | 20 +++ .../b.sx | 7 + .../c.sx | 1 + .../0807-memory-xx-recover-then-dispatch.sx | 1 + ...-value-routes-through-context-allocator.sx | 1 + .../0411-protocols-impl-duplicate.stderr | 4 +- ...63-modules-import-type-non-transitive.exit | 1 + ...-modules-import-type-non-transitive.stderr | 5 + ...-modules-import-type-non-transitive.stdout | 0 library/modules/gpu/gles3.sx | 1 + library/modules/gpu/metal.sx | 1 + library/modules/std/json.sx | 3 + library/modules/std/objc_block.sx | 6 + library/modules/ui/dock.sx | 1 + library/modules/ui/glyph_cache.sx | 1 + library/modules/ui/pipeline.sx | 1 + library/modules/ui/state.sx | 1 + readme.md | 17 +- src/ast.zig | 5 + src/imports.zig | 25 +++ src/ir/lower.zig | 147 ++++++++++++------ 28 files changed, 202 insertions(+), 58 deletions(-) create mode 100644 examples/0763-modules-import-type-non-transitive.sx create mode 100644 examples/0763-modules-import-type-non-transitive/b.sx create mode 100644 examples/0763-modules-import-type-non-transitive/c.sx create mode 100644 examples/expected/0763-modules-import-type-non-transitive.exit create mode 100644 examples/expected/0763-modules-import-type-non-transitive.stderr create mode 100644 examples/expected/0763-modules-import-type-non-transitive.stdout diff --git a/examples/0129-types-tuple-operators.sx b/examples/0129-types-tuple-operators.sx index e75f7f0..eebbd85 100644 --- a/examples/0129-types-tuple-operators.sx +++ b/examples/0129-types-tuple-operators.sx @@ -1,4 +1,5 @@ #import "modules/std.sx"; +#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it. #import "modules/math/math.sx"; #import "modules/compiler.sx"; #import "modules/test.sx"; diff --git a/examples/0411-protocols-impl-duplicate.sx b/examples/0411-protocols-impl-duplicate.sx index c532b05..52fd198 100644 --- a/examples/0411-protocols-impl-duplicate.sx +++ b/examples/0411-protocols-impl-duplicate.sx @@ -13,6 +13,9 @@ // both impl modules. #import "modules/std.sx"; +// `Wrap` is declared in the shared types module; bare-import visibility is +// non-transitive, so naming it here means importing it here (not via impl-a/b). +#import "./0411-protocols-impl-duplicate-types.sx"; #import "./0411-protocols-impl-duplicate-impl-a.sx"; #import "./0411-protocols-impl-duplicate-impl-b.sx"; diff --git a/examples/0709-modules-issue-0056-diamond-param-impl.sx b/examples/0709-modules-issue-0056-diamond-param-impl.sx index 5435dbd..ffcf6f9 100644 --- a/examples/0709-modules-issue-0056-diamond-param-impl.sx +++ b/examples/0709-modules-issue-0056-diamond-param-impl.sx @@ -9,6 +9,9 @@ // registration tripped: `duplicate impl 'Into' for source 's64'`. Now the flat // decl list also dedups by node identity, so this builds and prints 7. #import "modules/std.sx"; +// `Wrapped` lives in the shared `common.sx`; bare-import visibility is +// non-transitive, so naming it here means importing it here (not via mid_a/b). +#import "0709-modules-issue-0056/common.sx"; #import "0709-modules-issue-0056/mid_a.sx"; #import "0709-modules-issue-0056/mid_b.sx"; diff --git a/examples/0713-modules-json-writer.sx b/examples/0713-modules-json-writer.sx index 36c7adc..cbb50c4 100644 --- a/examples/0713-modules-json-writer.sx +++ b/examples/0713-modules-json-writer.sx @@ -18,6 +18,7 @@ // and freed in one `deinit`; the writer path allocates nothing. #import "modules/std.sx"; +#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it. #import "modules/std/json.sx"; #import "modules/fs.sx"; diff --git a/examples/0714-modules-json-reader.sx b/examples/0714-modules-json-reader.sx index 4305b44..9f3c4d5 100644 --- a/examples/0714-modules-json-reader.sx +++ b/examples/0714-modules-json-reader.sx @@ -20,6 +20,7 @@ // `JsonParseError` variant on the error channel, never a bogus value. #import "modules/std.sx"; +#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it. #import "modules/std/json.sx"; // Canonical document: no insignificant whitespace, escapes in the writer's diff --git a/examples/0715-modules-json-suite.sx b/examples/0715-modules-json-suite.sx index 508a100..bb1c19f 100644 --- a/examples/0715-modules-json-suite.sx +++ b/examples/0715-modules-json-suite.sx @@ -23,6 +23,7 @@ // and decoded strings go through `alloc`, and the writer allocates nothing. #import "modules/std.sx"; +#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it. #import "modules/std/json.sx"; // The writer's EXACT output for the PART A document (insertion order, diff --git a/examples/0719-modules-cli-and-json.sx b/examples/0719-modules-cli-and-json.sx index 5810b2b..956e355 100644 --- a/examples/0719-modules-cli-and-json.sx +++ b/examples/0719-modules-cli-and-json.sx @@ -12,6 +12,7 @@ // independent identities. #import "modules/std.sx"; +#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it. // `cli` is imported BOTH flat (so its types — `FlagSpec` / `Command` / `Diag` — // are bare-visible) AND namespaced (so the same-name `cli.parse` stays a // distinct qualified identity from `json.parse`). Post-E1 a bare reference to a diff --git a/examples/0763-modules-import-type-non-transitive.sx b/examples/0763-modules-import-type-non-transitive.sx new file mode 100644 index 0000000..9699b0a --- /dev/null +++ b/examples/0763-modules-import-type-non-transitive.sx @@ -0,0 +1,20 @@ +// `#import` is non-transitive for TYPES, exactly like values/functions (0706): +// when A imports B and B imports C, A must NOT see C's top-level TYPE names. +// This file imports `b.sx` (which in turn imports `c.sx`) and then references +// C's type `COnly` directly — the compiler rejects it with a +// "type ... is not visible; #import the module that declares it" diagnostic. +// +// `b.sx` ↔ `c.sx` together still compile: `b_make`'s return type `COnly` +// resolves because b.sx directly imports c.sx. +// +// Regression (Phase E4): before the bare-TYPE gate went single-hop this +// 2-flat-hop type was wrongly visible (the interim transitive closure). + +#import "modules/std.sx"; +#import "0763-modules-import-type-non-transitive/b.sx"; + +main :: () -> s32 { + x : COnly = .{ v = 5 }; + print("{}\n", x.v); + 0 +} diff --git a/examples/0763-modules-import-type-non-transitive/b.sx b/examples/0763-modules-import-type-non-transitive/b.sx new file mode 100644 index 0000000..2d52546 --- /dev/null +++ b/examples/0763-modules-import-type-non-transitive/b.sx @@ -0,0 +1,7 @@ +#import "c.sx"; + +// b.sx directly imports c.sx, so it CAN name `COnly` — proving the type is +// only one flat hop away here, two hops away from a file that imports b.sx. +b_make :: () -> COnly { + .{ v = 99 } +} diff --git a/examples/0763-modules-import-type-non-transitive/c.sx b/examples/0763-modules-import-type-non-transitive/c.sx new file mode 100644 index 0000000..3d28f1f --- /dev/null +++ b/examples/0763-modules-import-type-non-transitive/c.sx @@ -0,0 +1 @@ +COnly :: struct { v: s64 = 0; } diff --git a/examples/0807-memory-xx-recover-then-dispatch.sx b/examples/0807-memory-xx-recover-then-dispatch.sx index 2bfe669..ca81c08 100644 --- a/examples/0807-memory-xx-recover-then-dispatch.sx +++ b/examples/0807-memory-xx-recover-then-dispatch.sx @@ -4,6 +4,7 @@ // whether the recovery happens BEFORE or AFTER the first dispatch. #import "modules/std.sx"; +#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it. main :: () -> s32 { gpa := GPA.init(); diff --git a/examples/0808-memory-xx-value-routes-through-context-allocator.sx b/examples/0808-memory-xx-value-routes-through-context-allocator.sx index 0d58952..dfaac73 100644 --- a/examples/0808-memory-xx-value-routes-through-context-allocator.sx +++ b/examples/0808-memory-xx-value-routes-through-context-allocator.sx @@ -9,6 +9,7 @@ // the operand's storage, so it never allocates and never reaches this // path. See specs.md §3 — Protocol value ownership and lifetime. #import "modules/std.sx"; +#import "modules/allocators.sx"; // `Allocator` is non-transitive: name it, import it. Tracer :: struct { count: s64; diff --git a/examples/expected/0411-protocols-impl-duplicate.stderr b/examples/expected/0411-protocols-impl-duplicate.stderr index 9d7feb8..3c92113 100644 --- a/examples/expected/0411-protocols-impl-duplicate.stderr +++ b/examples/expected/0411-protocols-impl-duplicate.stderr @@ -1,5 +1,5 @@ error: duplicate xx conversion from 's64' to 'Wrap': impls in /Users/agra/projects/sx/examples/./0411-protocols-impl-duplicate-impl-a.sx and /Users/agra/projects/sx/examples/./0411-protocols-impl-duplicate-impl-b.sx - --> /Users/agra/projects/sx/examples/0411-protocols-impl-duplicate.sx:20:17 + --> /Users/agra/projects/sx/examples/0411-protocols-impl-duplicate.sx:23:17 | -20 | w : Wrap = xx 7; +23 | w : Wrap = xx 7; | ^ diff --git a/examples/expected/0763-modules-import-type-non-transitive.exit b/examples/expected/0763-modules-import-type-non-transitive.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0763-modules-import-type-non-transitive.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0763-modules-import-type-non-transitive.stderr b/examples/expected/0763-modules-import-type-non-transitive.stderr new file mode 100644 index 0000000..4402aed --- /dev/null +++ b/examples/expected/0763-modules-import-type-non-transitive.stderr @@ -0,0 +1,5 @@ +error: type 'COnly' is not visible; #import the module that declares it + --> /Users/agra/projects/sx/examples/0763-modules-import-type-non-transitive.sx:17:9 + | +17 | x : COnly = .{ v = 5 }; + | ^^^^^ diff --git a/examples/expected/0763-modules-import-type-non-transitive.stdout b/examples/expected/0763-modules-import-type-non-transitive.stdout new file mode 100644 index 0000000..e69de29 diff --git a/library/modules/gpu/gles3.sx b/library/modules/gpu/gles3.sx index f029647..a0aae44 100644 --- a/library/modules/gpu/gles3.sx +++ b/library/modules/gpu/gles3.sx @@ -21,6 +21,7 @@ // the Metal backend takes — caller branches on OS). #import "modules/std.sx"; +#import "modules/allocators.sx"; #import "modules/compiler.sx"; #import "modules/opengl.sx"; #import "modules/gpu/types.sx"; diff --git a/library/modules/gpu/metal.sx b/library/modules/gpu/metal.sx index dcf39e6..29ea5f9 100644 --- a/library/modules/gpu/metal.sx +++ b/library/modules/gpu/metal.sx @@ -7,6 +7,7 @@ // non-iOS targets don't reach the Metal-touching code paths. #import "modules/std.sx"; +#import "modules/allocators.sx"; #import "modules/std/objc.sx"; #import "modules/compiler.sx"; #import "modules/gpu/types.sx"; diff --git a/library/modules/std/json.sx b/library/modules/std/json.sx index 8b537c7..d9bef38 100644 --- a/library/modules/std/json.sx +++ b/library/modules/std/json.sx @@ -58,6 +58,9 @@ // ===================================================================== #import "modules/std.sx"; +// `Array`/`Object` methods take an explicit `alloc: Allocator`; bare-import +// visibility is non-transitive, so the module that names the type imports it. +#import "modules/allocators.sx"; #import "modules/fs.sx"; // The writer's failure contract: a too-small caller buffer (Overflow) or diff --git a/library/modules/std/objc_block.sx b/library/modules/std/objc_block.sx index d9aaeb2..2033f19 100644 --- a/library/modules/std/objc_block.sx +++ b/library/modules/std/objc_block.sx @@ -22,6 +22,12 @@ // caller returns. If you need that, ship a `Block_copy`-backed sibling // API and use it instead. +// `build_block_convert` (below) is a comptime metaprogram that emits sx source +// with `concat` / `int_to_string`; those live in std.sx. A metaprogram body +// resolves its bare names in its OWN module (issue 0106), so this module must +// import std.sx itself rather than relying on the call site's visibility. +#import "modules/std.sx"; + // Standard 32-byte block header plus two trailing slots for the sx closure // it wraps. Total = 48 bytes. Block :: struct { diff --git a/library/modules/ui/dock.sx b/library/modules/ui/dock.sx index e86871c..a94b30c 100755 --- a/library/modules/ui/dock.sx +++ b/library/modules/ui/dock.sx @@ -1,4 +1,5 @@ #import "modules/std.sx"; +#import "modules/allocators.sx"; #import "modules/math"; #import "modules/ui/types.sx"; #import "modules/ui/render.sx"; diff --git a/library/modules/ui/glyph_cache.sx b/library/modules/ui/glyph_cache.sx index 190f013..ef852e6 100755 --- a/library/modules/ui/glyph_cache.sx +++ b/library/modules/ui/glyph_cache.sx @@ -1,4 +1,5 @@ #import "modules/std.sx"; +#import "modules/allocators.sx"; #import "modules/opengl.sx"; #import "modules/gpu/types.sx"; #import "modules/gpu/api.sx"; diff --git a/library/modules/ui/pipeline.sx b/library/modules/ui/pipeline.sx index 747e0a5..e33ceee 100755 --- a/library/modules/ui/pipeline.sx +++ b/library/modules/ui/pipeline.sx @@ -1,5 +1,6 @@ #import "modules/std.sx"; #import "modules/allocators.sx"; +#import "modules/ui/glyph_cache.sx"; // `font: GlyphCache` — name it, import it (non-transitive). #import "modules/opengl.sx"; #import "modules/gpu/api.sx"; #import "modules/ui/types.sx"; diff --git a/library/modules/ui/state.sx b/library/modules/ui/state.sx index 3abe5e5..ee58dc6 100755 --- a/library/modules/ui/state.sx +++ b/library/modules/ui/state.sx @@ -1,4 +1,5 @@ #import "modules/std.sx"; +#import "modules/allocators.sx"; // --- State(T) — a handle to persistent storage --- diff --git a/readme.md b/readme.md index d629bd1..e6fb5bd 100644 --- a/readme.md +++ b/readme.md @@ -401,12 +401,17 @@ 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, 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`. +a namespaced alias. That join is **non-transitive for every bare member kind — +functions, constants, AND types alike**: 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 — including its types — so qualify them, or `#import "C"` directly +if you reference them). A bare reference to a namespaced-only import's member — +function, module constant, or **type** — is likewise not visible and is rejected +(`type 'X' is not visible; #import the module that declares it`); qualify it as +`m.name`. (A library's own *internal* type references still resolve: a generic +struct / pack fn / protocol body is instantiated in the module that defines it, so +e.g. `List(T).append`'s `alloc: Allocator` is visible there regardless of the call +site.) ### Implicit Context diff --git a/src/ast.zig b/src/ast.zig index 9122a10..f194559 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -783,6 +783,11 @@ pub const ProtocolDecl = struct { /// True when the declared NAME was a backtick raw identifier — exempt from /// the reserved-type-name decl check (issue 0089). is_raw: bool = false, + /// Defining module path (stamped by `resolveImports`), so a parameterized + /// protocol instantiated cross-module resolves its method signature types in + /// the module that declares it (E4 — the protocol analog of + /// `StructTemplate.source_file`). Null for a synthesized/sourceless decl. + source_file: ?[]const u8 = null, }; pub const ForeignRuntime = enum { diff --git a/src/imports.zig b/src/imports.zig index 9717093..dc5c177 100644 --- a/src/imports.zig +++ b/src/imports.zig @@ -661,14 +661,39 @@ fn reportDuplicateName(diagnostics: ?*errors.DiagnosticList, added: bool, name: fn stampFnBodySource(decl: *Node, file_path: []const u8) void { switch (decl.data) { .fn_decl => |fd| fd.body.source_file = file_path, + .struct_decl => |sd| stampStructMethodSources(sd, file_path), + // A parameterized protocol is instantiated cross-module; record its + // defining path so the instantiation resolves method-signature types in + // this module (E4). + .protocol_decl => decl.data.protocol_decl.source_file = file_path, .const_decl => |cd| switch (cd.value.data) { .fn_decl => |fd| fd.body.source_file = file_path, + // `List :: struct { … append :: (…) { … } }` — the methods of a + // (possibly generic) struct are monomorphized in their template's + // OWN module (issue 0106 + the E4 instantiation source-pin), so their + // bodies need the defining path stamped just like a top-level fn. + .struct_decl => |sd| stampStructMethodSources(sd, file_path), + .protocol_decl => cd.value.data.protocol_decl.source_file = file_path, else => {}, }, else => {}, } } +/// Stamp the defining module path onto every method (and struct-level fn +/// constant) body of a struct decl, so a generic-struct method monomorphized at +/// a cross-module call site still pins to the module that declares it. +fn stampStructMethodSources(sd: ast.StructDecl, file_path: []const u8) void { + for (sd.methods) |m| { + if (m.data == .fn_decl) m.data.fn_decl.body.source_file = file_path; + } + for (sd.constants) |c| { + if (c.data == .const_decl and c.data.const_decl.value.data == .fn_decl) { + c.data.const_decl.value.data.fn_decl.body.source_file = file_path; + } + } +} + /// `reportDuplicateName` keyed off a node whose `declName()` carries the name /// (the regular authored-decl sites; an `import_decl` has no `declName`, so a /// namespace alias must use `reportDuplicateName` with the alias directly). diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 28604c0..545c24f 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1778,9 +1778,10 @@ pub const Lowering = struct { /// the legacy stub there and defers the diagnostic to the checker. undeclared, /// `name` IS a registered named type, but it is reachable from the - /// querying module ONLY through a namespaced import — not bare-visible - /// over the transitive flat-import closure (the type analog of Phase B's - /// bare-call tightening, F1). The user must qualify it (`ns.Type`). + /// querying module ONLY through a namespaced import (or over more than one + /// flat hop) — not bare-visible over the single-hop direct flat-import set + /// (the type analog of Phase B's bare-call tightening, F1). The user must + /// qualify it (`ns.Type`) or `#import` the declaring module directly. /// `resolveNominalLeaf` surfaces the "not visible" diagnostic and returns /// the `.unresolved` poison sentinel — NEVER the global `findByName` match /// (which would leak the type) and NEVER a silent empty-struct stub (which @@ -1915,17 +1916,15 @@ pub const Lowering = struct { // `module_consts_by_source`, never in `type_aliases_by_source`, so it is // correctly excluded too. // - // 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, - // sequenced as E4 per Agra): 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. 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 E4 - // settles on. + // The TYPE reachability here is SINGLE-HOP — `from`'s own author plus its + // DIRECT flat-import edges (`flatTypeAuthorCount`), the same non-transitive + // set the bare VALUE / FUNCTION / CONST leaves use (E4, consistent with + // 0706). A library template's INTERNAL type refs (`List.append`'s + // `alloc: Allocator`) still resolve because every instantiation kind + // (generic struct / fn / pack fn / param protocol / type fn) is + // source-pinned to the template's defining module, so the query + // originates THERE — where the type is a direct flat import — not at the + // cross-module call site. const name_id = table.internString(name); const registered = table.findByName(name_id); @@ -1956,10 +1955,10 @@ pub const Lowering = struct { // `internNamedTypeDecl` adopting that stub when the type registers. // // The querying source's OWN author wins outright (own-wins, 0105 case - // 3); otherwise the transitive flat-import closure is searched, and ≥2 - // DISTINCT flat-visible authors → `.ambiguous` (0105 case 4). Single- - // author (E1) keeps ≤1 author across the closure, so this stays byte- - // identical to the legacy leaf. + // 3); otherwise the single-hop direct flat-import set is searched, and + // ≥2 DISTINCT flat-visible authors → `.ambiguous` (0105 case 4). Single- + // author keeps ≤1 author across that set, so this stays byte-identical + // to the legacy leaf. if (self.moduleTypeAuthor(from, name)) |author| switch (author) { .alias => |tid| return .{ .resolved = tid }, .named => |ref| { @@ -2122,10 +2121,12 @@ pub const Lowering = struct { }; } - /// What bare `name`'s type authors look like across the TRANSITIVE - /// flat-import closure of `from` (the querying source's OWN author is consulted - /// by `selectNominalLeaf` first — own-wins — so this surveys only the - /// cross-module flat authors): + /// What bare `name`'s type authors look like across the SINGLE-HOP flat-import + /// set of `from` — its DIRECT bare `#import` edges only, NOT the transitive + /// closure (E4: consistent with the bare VALUE/FUNCTION/CONST leaves and + /// example 0706; the interim transitive closure E1 shipped is gone). The + /// querying source's OWN author is consulted by `selectNominalLeaf` first + /// (own-wins), so this surveys only the cross-module direct-flat authors: /// - `.ambiguous` — ≥2 DISTINCT resolved TypeIds (issue 0105 case 4); /// - `.one` — exactly one distinct resolved TypeId; /// - `.unregistered` — ≥1 flat author found but none resolves to a TypeId @@ -2135,37 +2136,29 @@ pub const Lowering = struct { /// local / leak / forward-alias arms. /// Distinctness is BY TypeId: each distinct author holds a distinct /// `nominal_id` TypeId, while a diamond import of the SAME module yields the - /// same TypeId, so byte-identical de-dup falls out. The closure walk lives in - /// `lower.zig`, NOT `resolver.zig` — the single-graph-walk invariant (one - /// `flat_import_graph` iterator in `resolver.zig`) is untouched. + /// same TypeId, so byte-identical de-dup falls out. A library template's + /// INTERNAL bare-TYPE refs (a 2-flat-hop type like `List(T).append`'s + /// `alloc: Allocator`) stay resolvable because instantiation is source-pinned + /// to the template's defining module (E4 #1), so the query originates THERE — + /// where the type is a direct flat import — not at the cross-module call site. + /// The walk lives in `lower.zig`, NOT `resolver.zig` — the single-graph-walk + /// invariant (one `flat_import_graph` iterator in `resolver.zig`) is untouched. const FlatTypeAuthorCount = union(enum) { none, one: TypeId, ambiguous, unregistered }; fn flatTypeAuthorCount(self: *Lowering, name: []const u8, from: []const u8) FlatTypeAuthorCount { const graph = self.program_index.flat_import_graph orelse return .none; + const direct = graph.get(from) orelse return .none; var found: ?TypeId = null; var saw_author = false; - var visited = std.StringHashMap(void).init(self.alloc); - defer visited.deinit(); - var queue = std.ArrayList([]const u8).empty; - defer queue.deinit(self.alloc); - visited.put(from, {}) catch return .none; - queue.append(self.alloc, from) catch return .none; - var i: usize = 0; - while (i < queue.items.len) : (i += 1) { - const deps = graph.get(queue.items[i]) orelse continue; - var it = deps.iterator(); - while (it.next()) |kv| { - const dep = kv.key_ptr.*; - if (visited.contains(dep)) continue; - visited.put(dep, {}) catch continue; - if (self.moduleTypeAuthor(dep, name) != null) { - saw_author = true; - if (self.moduleTypeAuthorTid(dep, name)) |tid| { - if (found) |f| { - if (tid != f) return .ambiguous; - } else found = tid; - } + var it = direct.iterator(); + while (it.next()) |kv| { + const dep = kv.key_ptr.*; + if (self.moduleTypeAuthor(dep, name) != null) { + saw_author = true; + if (self.moduleTypeAuthorTid(dep, name)) |tid| { + if (found) |f| { + if (tid != f) return .ambiguous; + } else found = tid; } - queue.append(self.alloc, dep) catch continue; } } if (found) |t| return .{ .one = t }; @@ -2238,6 +2231,9 @@ pub const Lowering = struct { fn resolveNominalLeaf(self: *Lowering, name: []const u8, raw: bool, span: ?ast.Span) TypeId { const from = self.current_source_file orelse return self.typeResolver().resolveName(name, raw); + if (std.mem.eql(u8, name, "BOOL")) { + std.debug.print("[BOOLLEAF] from={s} -> {s}\n", .{ from, @tagName(self.selectNominalLeaf(name, from, raw)) }); + } return switch (self.selectNominalLeaf(name, from, raw)) { .resolved => |t| t, // A forward alias (`.pending`) or a forward / not-yet-interned named @@ -11551,6 +11547,14 @@ pub const Lowering = struct { self.pack_arg_types = pre_pat; self.pack_constraint = if (pack_proto != null) pre_pcon else null; + // Resolve the declared return + fixed-prefix param types in the pack fn's + // OWN module (E4), so a 2-flat-hop library type named in the signature is + // bare-visible — mirrors the body pin further down and the + // `monomorphizeFunction` pin. The comptime call-site args below are + // lowered AFTER this restore, in the caller's context (issue 0106). + const saved_sig_src = self.current_source_file; + if (fd.body.source_file) |src| self.setCurrentSourceFile(src); + const declared_is_generic_ret = blk: { const rt = fd.return_type orelse break :blk false; if (rt.data != .type_expr) break :blk false; @@ -11590,6 +11594,7 @@ pub const Lowering = struct { .ty = ty, }) catch return; } + self.setCurrentSourceFile(saved_sig_src); const name_id = self.module.types.internString(owned_name); _ = self.builder.beginFunction(name_id, params.items, ret_ty); @@ -11740,6 +11745,19 @@ pub const Lowering = struct { // Install type bindings self.type_bindings = bindings.*; + // Pin to the template's defining module for the whole monomorphization + // (return type, param types, body), so a library-internal bare TYPE ref + // — e.g. `List(T).append`'s `alloc: Allocator` default-param type, or a + // body reference to a type visible only in the template's module — + // resolves where it is visible, not at the (possibly cross-module) call + // site. This is the issue-0100-F1 plain-fn pin extended to generic + // instantiation; without it the non-transitive bare-TYPE gate (E4) would + // reject a 2-flat-hop library type the call site cannot see directly. + // A synthesized / sourceless body keeps the caller's context. + const saved_source_mono = self.current_source_file; + defer self.setCurrentSourceFile(saved_source_mono); + if (fd.body.source_file) |src| self.setCurrentSourceFile(src); + // Resolve return type with type bindings active. The body's tail // expression inherits this as its target_type so bare `.{...}` // literals resolve to the monomorphised return type instead of @@ -12969,7 +12987,7 @@ pub const Lowering = struct { if (fd.params.len > 0) { var types_list = std.ArrayList(TypeId).empty; for (fd.params[1..]) |p| { - types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; + types_list.append(self.alloc, self.resolveParamTypeInSource(fd.body.source_file, &p)) catch unreachable; } return types_list.items; } @@ -12987,7 +13005,7 @@ pub const Lowering = struct { } var types_list = std.ArrayList(TypeId).empty; for (fd.params[1..]) |p| { - types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable; + types_list.append(self.alloc, self.resolveParamTypeInSource(fd.body.source_file, &p)) catch unreachable; } self.type_bindings = saved_bindings; return types_list.items; @@ -13376,6 +13394,20 @@ pub const Lowering = struct { return self.resolveType(type_ann); } + /// `resolveParamType` with the visibility context pinned to `src`, the + /// DEFINING module of the param's function. An imported method's + /// default-param type (`alloc: Allocator`) is bare-visible only inside its + /// own module, so typing a cross-module call's args against it must resolve + /// in that module's context, not the call site's (E4 — the param analog of + /// `resolveTypeInSource`). `src == null` falls back unchanged. + fn resolveParamTypeInSource(self: *Lowering, src: ?[]const u8, p: *const ast.Param) TypeId { + const pinned = src orelse return self.resolveParamType(p); + const saved = self.current_source_file; + defer self.setCurrentSourceFile(saved); + self.setCurrentSourceFile(pinned); + return self.resolveParamType(p); + } + /// Construct a `TypeResolver` view over the current lowering state (borrows /// only; cheap by-value, reflects current `diagnostics` / `program_index`). fn typeResolver(self: *Lowering) TypeResolver { @@ -14074,6 +14106,14 @@ pub const Lowering = struct { self.comptime_value_bindings = saved_value_bindings; } + // Resolve the type fn's body (inline struct/union fields, or the returned + // type expression) in its OWN module (E4), so a 2-flat-hop library type + // named there is bare-visible — not the cross-module call site. The arg + // exprs above were already resolved in the caller's context. + const saved_tf_src = self.current_source_file; + defer self.setCurrentSourceFile(saved_tf_src); + if (fd.body.source_file) |src| self.setCurrentSourceFile(src); + // Determine if alias_name is a real alias (e.g., "Foo" for "Complex(u32)") // or just the template name itself (inline use like "Sx(f32)") const has_alias = !std.mem.eql(u8, alias_name, template_name); @@ -14689,9 +14729,16 @@ pub const Lowering = struct { const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info); table.updatePreservingKey(id, struct_info); - // Method infos resolved with the type-arg binding (T → s64). + // Method infos resolved with the type-arg binding (T → s64), pinned to + // the protocol's OWN module (E4) so a method-signature type visible only + // there resolves correctly when instantiated cross-module. `Self` and the + // bound type-args short-circuit before the leaf; a concrete library type + // in a signature is the case this pin protects. const saved_tb = self.type_bindings; self.type_bindings = tb; + const saved_pp_src = self.current_source_file; + defer self.setCurrentSourceFile(saved_pp_src); + if (pd.source_file) |src| self.setCurrentSourceFile(src); var method_infos = std.ArrayList(ProtocolMethodInfo).empty; for (pd.methods) |method| { var ptypes = std.ArrayList(TypeId).empty;