From 566de9682139972d874ceed5d167f14be51ac89a Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 8 Jun 2026 16:43:01 +0300 Subject: [PATCH] fix(stdlib/E4): type-fn head gate selects the TYPE-FUNCTION author (ordinary fn must not vouch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit attempt-7 made the type-fn head gate kind-aware (a non-function no longer vouches), but it still accepted ANY function author: a directly-visible ORDINARY function (`Make :: () -> s32`, zero `$`-params) authorized a hidden 2-flat-hop type-function head (`Make :: ($T) -> Type`), so `size_of(Make(s64))` silently instantiated the 2-hop type-fn and printed `size=8` at exit 0. Narrow the author view from "any fn_decl" to "a TYPE-FUNCTION" via a new `typeFnAuthor` predicate (`fnDeclOfRaw` + `type_params.len > 0`), the same discriminator every instantiation site uses to recognize a type-fn head. Both `flatFnAuthorVisible` and `flatFnAuthorAmbiguous` now count only type-fn authors, so a same-name ordinary function — which cannot be the type head being instantiated — does not vouch for a 2-hop type-fn head. Regression 0771: main -> b (`Make :: () -> s32` ordinary fn + flat-imports c) -> c (`Make :: ($T) -> Type`); `size_of(Make(s64))` -> "type 'Make' is not visible", exit 1 (fail-before on 94c3cd7: size=8 exit 0). 0770 (non-fn vouch), 0769 (type-fn ambiguity), 0768/0767/0766-0763, 0208/0210 (valid type-fn heads), 0544/0706/0105 and FFI all green & byte-identical. --- ...dules-type-fn-head-ordinary-fn-no-vouch.sx | 25 +++++++++++ .../b.sx | 7 +++ .../c.sx | 5 +++ ...les-type-fn-head-ordinary-fn-no-vouch.exit | 1 + ...s-type-fn-head-ordinary-fn-no-vouch.stderr | 5 +++ ...s-type-fn-head-ordinary-fn-no-vouch.stdout | 1 + src/ir/lower.zig | 43 +++++++++++++------ 7 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 examples/0771-modules-type-fn-head-ordinary-fn-no-vouch.sx create mode 100644 examples/0771-modules-type-fn-head-ordinary-fn-no-vouch/b.sx create mode 100644 examples/0771-modules-type-fn-head-ordinary-fn-no-vouch/c.sx create mode 100644 examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.exit create mode 100644 examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.stderr create mode 100644 examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.stdout diff --git a/examples/0771-modules-type-fn-head-ordinary-fn-no-vouch.sx b/examples/0771-modules-type-fn-head-ordinary-fn-no-vouch.sx new file mode 100644 index 0000000..1e45410 --- /dev/null +++ b/examples/0771-modules-type-fn-head-ordinary-fn-no-vouch.sx @@ -0,0 +1,25 @@ +// A type-returning FUNCTION head (`Make(s64)` where `Make :: ($T) -> Type`) is +// NON-transitive even when a DIRECT flat import authors the same name as an +// ORDINARY (non-type-returning) function. `main` flat-imports `b.sx`; `b.sx` +// declares `Make :: () -> s32` (a plain function, NOT a type-fn) AND flat-imports +// `c.sx`, whose `Make` IS the type-returning function. The only TYPE-FN author of +// `Make` is two flat hops away (main → b → c), so the bare `Make(s64)` head must +// emit the "type 'Make' is not visible" diagnostic and poison — the visible 1-hop +// ordinary `Make :: () -> s32` must NOT vouch for it. +// +// Regression (Phase E4 attempt-8, finding E4-type-fn-head-hidden-by-visible- +// nontypefn): attempt-7's `headFnLeak` decided visibility from any FUNCTION +// author (`fnDeclOfRaw != null`), so the visible ordinary `Make` function (which +// CANNOT be the type head being instantiated) still vouched — the global +// `fn_ast_map` type-fn silently instantiated and `size_of(Make(s64))` printed 8 +// at exit 0 instead of the visibility diagnostic. The fix narrows the author view +// to TYPE-FUNCTIONS (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param), the same +// discriminator every instantiation site uses to recognize a type-fn head. + +#import "modules/std.sx"; +#import "0771-modules-type-fn-head-ordinary-fn-no-vouch/b.sx"; + +main :: () -> s32 { + print("size={}\n", size_of(Make(s64))); + 0 +} diff --git a/examples/0771-modules-type-fn-head-ordinary-fn-no-vouch/b.sx b/examples/0771-modules-type-fn-head-ordinary-fn-no-vouch/b.sx new file mode 100644 index 0000000..91e5229 --- /dev/null +++ b/examples/0771-modules-type-fn-head-ordinary-fn-no-vouch/b.sx @@ -0,0 +1,7 @@ +// The directly-imported (1-hop) author of the NAME `Make` — but as an ORDINARY +// function (`() -> s32`), NOT a type-returning function. It flat-imports c.sx +// (where the real `Make` type-fn lives, two hops from a file that imports b.sx). +// A same-name ordinary function must not vouch for the 2-hop type-fn head: it has +// zero `$`-params, so it cannot be the type head `Make(s64)` is instantiating. +#import "c.sx"; +Make :: () -> s32 { return 7; } diff --git a/examples/0771-modules-type-fn-head-ordinary-fn-no-vouch/c.sx b/examples/0771-modules-type-fn-head-ordinary-fn-no-vouch/c.sx new file mode 100644 index 0000000..77a8935 --- /dev/null +++ b/examples/0771-modules-type-fn-head-ordinary-fn-no-vouch/c.sx @@ -0,0 +1,5 @@ +// The real type-returning function `Make`, two flat hops from a file that +// imports b.sx. A file importing only b.sx must NOT see this head. +Make :: ($T: Type) -> Type { + return struct { x: T; }; +} diff --git a/examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.exit b/examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.stderr b/examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.stderr new file mode 100644 index 0000000..7cfbbda --- /dev/null +++ b/examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.stderr @@ -0,0 +1,5 @@ +error: type 'Make' is not visible; #import the module that declares it + --> examples/0771-modules-type-fn-head-ordinary-fn-no-vouch.sx:23:32 + | +23 | print("size={}\n", size_of(Make(s64))); + | ^^^^ diff --git a/examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.stdout b/examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0771-modules-type-fn-head-ordinary-fn-no-vouch.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/lower.zig b/src/ir/lower.zig index bc92109..eaba1a4 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -2329,6 +2329,19 @@ pub const Lowering = struct { }; } + /// TRUE iff `ref` is a TYPE-FUNCTION head author — a `fn_decl` (or const- + /// wrapped fn) declaring at least one `$`-parameter, i.e. instantiable as a + /// bare type head (`Make(s64)` where `Make :: ($T) -> Type`). Mirrors the + /// `fd.type_params.len > 0` gate every instantiation site uses to recognize a + /// type-fn head, so an ORDINARY same-name function (`Make :: () -> s32`, zero + /// type params) is NOT a type-fn author and does NOT vouch for a hidden 2-flat- + /// hop type-fn head (E4 attempt-8: a `fn_decl != null` author view let any + /// visible function — type-fn or not — authorize a type head). + fn typeFnAuthor(ref: resolver_mod.RawDeclRef) bool { + const fd = fnDeclOfRaw(ref) orelse return false; + return fd.type_params.len > 0; + } + /// Materialize (lower-on-demand) the FuncId for a selected bare-call author, /// caching into `sf.materialized`. Shadow-only: the winner owns the /// name-keyed slot and lowers through the lazy path, so @@ -14063,8 +14076,9 @@ pub const Lowering = struct { return true; } // KIND-AWARE: visible iff a directly-reachable (own or 1-hop flat) author - // is a FUNCTION. A same-name 1-hop non-function does NOT vouch for a - // type-fn head whose real author is 2 flat hops away (E4 attempt-7). + // is itself a TYPE-FUNCTION. A same-name 1-hop non-function (attempt-7) OR + // ordinary non-type function (attempt-8) does NOT vouch for a type-fn head + // whose real author is 2 flat hops away. if (self.flatFnAuthorVisible(name, from)) return false; if (self.diagnostics) |d| d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name}); @@ -14072,8 +14086,9 @@ pub const Lowering = struct { } /// TRUE iff bare `name` has ≥2 DISTINCT direct flat-import authors that are - /// type-returning FUNCTIONS (`fn_decl`s — `fnDeclOfRaw` unwraps a const-wrapped - /// fn) and the querying source authors NONE itself. The querying source's OWN + /// TYPE-FUNCTIONS (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param — an ordinary + /// same-name function does not count) and the querying source authors NONE + /// itself. The querying source's OWN /// author wins outright (own-wins), so an own author short-circuits to "not /// ambiguous" — the existing single-author path instantiates it. Diamond /// imports of the SAME author collapse in `collectVisibleAuthors`'s @@ -14087,29 +14102,31 @@ pub const Lowering = struct { if (set.own != null) return false; // own-wins var fn_authors: usize = 0; for (set.flat) |fa| { - if (fnDeclOfRaw(fa.raw) != null) fn_authors += 1; + if (typeFnAuthor(fa.raw)) fn_authors += 1; } return fn_authors >= 2; } /// TRUE iff bare `name` has at least one DIRECTLY-visible author — the /// querying source's OWN author or a 1-hop flat-import author — that is a - /// FUNCTION decl (`fnDeclOfRaw` unwraps a const-wrapped fn). The KIND-AWARE + /// TYPE-FUNCTION (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param). The KIND-AWARE /// analogue of `isNameVisible` for a type-fn head: a same-name 1-hop - /// NON-function (a value const `Make :: 123`, a named type) is NOT a function - /// author and does NOT vouch, so a type-fn whose only directly-visible - /// same-name author is a non-fn — its real author 2 flat hops away — is - /// correctly invisible. Mirrors `flatFnAuthorAmbiguous`'s fn-only author view - /// (E4 attempt-7: closes the `isNameVisible` name-only short-circuit leak). + /// NON-function (a value const `Make :: 123`, a named type) does NOT vouch + /// (attempt-7), and — crucially — neither does a same-name 1-hop ORDINARY + /// function (`Make :: () -> s32`, zero `$`-params), which cannot be the type + /// head being instantiated (attempt-8). So a type-fn whose only directly- + /// visible same-name author is a non-fn OR a non-type-fn — its real author 2 + /// flat hops away — is correctly invisible. Mirrors `flatFnAuthorAmbiguous`'s + /// type-fn-only author view. fn flatFnAuthorVisible(self: *Lowering, name: []const u8, from: []const u8) bool { var res = self.resolver(); const set = res.collectVisibleAuthors(name, from, .user_bare_flat); defer if (set.flat.len > 0) self.alloc.free(set.flat); if (set.own) |own| { - if (fnDeclOfRaw(own.raw) != null) return true; + if (typeFnAuthor(own.raw)) return true; } for (set.flat) |fa| { - if (fnDeclOfRaw(fa.raw) != null) return true; + if (typeFnAuthor(fa.raw)) return true; } return false; }