fix(stdlib/E4): kind-aware type-fn head gate (non-fn must not vouch)

The type-fn head visibility check (`headFnLeak`) used the module-scope
NAME predicate `isNameVisible`, so a same-name 1-hop NON-function (a value
const `Make :: 123`) reported the name "visible" and let the global
`fn_ast_map` type-fn — whose real author is 2 flat hops away — silently
instantiate. `size_of(Make(s64))` printed 8 at exit 0 instead of a
visibility diagnostic.

Decide visibility from the ELIGIBLE FUNCTION authors directly reachable
from the use site (`flatFnAuthorVisible`, mirroring `flatFnAuthorAmbiguous`'s
fn-only author view): visible iff the own author or a 1-hop flat-import
author is a `fn_decl`. A non-function does not vouch. Guarded to fall open
when the import facts aren't wired (comptime / directory imports), mirroring
`headTypeGate`. Own / scope-local / 1-hop / directly-imported type-fn heads
still resolve; 0769 ambiguity unchanged.

Regression: examples/0770-modules-type-fn-head-non-transitive (main → b
[`Make :: 123` + flat-imports c] → c [`Make :: ($T) -> Type`]); the bare
`Make(s64)` head emits "type 'Make' is not visible", exit 1.
This commit is contained in:
agra
2026-06-08 16:03:23 +03:00
parent cb9ef381b5
commit 94c3cd7507
7 changed files with 86 additions and 12 deletions

View File

@@ -0,0 +1,23 @@
// 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 a
// NON-function. `main` flat-imports `b.sx`; `b.sx` declares `Make :: 123` (a
// value const, 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
// `Make :: 123` const must NOT vouch for it.
//
// Regression (Phase E4 attempt-7, finding E4-type-fn-head-hidden-by-visible-
// nonfn): before `headFnLeak` decided visibility from the ELIGIBLE FUNCTION
// authors it used the module-scope NAME predicate (`isNameVisible`), which the
// visible non-fn `Make :: 123` satisfied — so the global `fn_ast_map` type-fn
// silently instantiated and `size_of(Make(s64))` printed 8 at exit 0 instead of
// the visibility diagnostic.
#import "modules/std.sx";
#import "0770-modules-type-fn-head-non-transitive/b.sx";
main :: () -> s32 {
print("size={}\n", size_of(Make(s64)));
0
}

View File

@@ -0,0 +1,6 @@
// The directly-imported (1-hop) author of the NAME `Make` — but as a value
// const, 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
// non-function must not vouch for the 2-hop type-fn head.
#import "c.sx";
Make :: 123;

View File

@@ -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; };
}

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: type 'Make' is not visible; #import the module that declares it
--> examples/0770-modules-type-fn-head-non-transitive.sx:21:32
|
21 | print("size={}\n", size_of(Make(s64)));
| ^^^^

View File

@@ -14030,31 +14030,42 @@ pub const Lowering = struct {
/// Single-hop non-transitive visibility + ambiguity gate for an UNQUALIFIED
/// type-returning FUNCTION head used as a type (`Make(N, T)` where
/// `Make :: ($K, $T) -> Type`). A type-fn is a `fn_decl`, so its visibility is
/// FUNCTION visibility (`isNameVisible`, the single-hop flat name set) — NOT the
/// type-author model. Returns TRUE (loud diagnostic already emitted) when the
/// head is AMBIGUOUS (≥2 distinct direct flat same-name type-fn authors, no own
/// author — consistent with the parameterized struct / protocol heads and the
/// leaf, 0755/0767, never a silent `fn_ast_map` first-/last-wins pick) or
/// NOT-VISIBLE (authored somewhere but unreachable from the use site, a
/// 2-flat-hop leak). A scope-local (mangled) type-fn or the querying source's
/// OWN author wins outright (own-wins) and is exempt; falls open when unwired /
/// `Make :: ($K, $T) -> Type`). A type-fn is a `fn_decl`, so visibility is
/// decided from the ELIGIBLE FUNCTION authors directly reachable from the use
/// site (`flatFnAuthorVisible`) — NOT the module-scope NAME predicate
/// (`isNameVisible`), which a same-name NON-function (a value const, a named
/// type) would wrongly vouch for. Returns TRUE (loud diagnostic already
/// emitted) when the head is AMBIGUOUS (≥2 distinct direct flat same-name
/// type-fn authors, no own author — consistent with the parameterized struct /
/// protocol heads and the leaf, 0755/0767, never a silent `fn_ast_map`
/// first-/last-wins pick) or NOT-VISIBLE (its only directly-visible same-name
/// author is a non-function and the real type-fn author is ≥2 flat hops away).
/// A scope-local (mangled) type-fn or the querying source's OWN function author
/// wins outright (own-wins) and is exempt; falls open when unwired /
/// default-context. Diagnostic mirrors the type form (the head IS used as a type
/// here).
fn headFnLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool {
if (self.emitting_default_context) return false;
const from = self.current_source_file orelse return false;
if (self.scope) |s| if (s.lookupFn(name) != null) return false;
// Fall open when the import facts aren't wired (comptime callers,
// directory imports without a main file): the author collector would
// otherwise return an empty set and wrongly report a genuinely-visible
// type-fn as not-visible. Mirrors `headTypeGate`'s guard.
if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return false;
// ≥2 distinct direct flat type-fn authors with no own author — a genuine
// collision the source cannot disambiguate. Diagnose loudly BEFORE the
// `isNameVisible` short-circuit, which would otherwise report "visible" and
// let the single `fn_ast_map[name]` author silently win.
// visibility short-circuit, which would otherwise let the single
// `fn_ast_map[name]` author silently win.
if (self.flatFnAuthorAmbiguous(name, from)) {
if (self.diagnostics) |d|
d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name});
return true;
}
if (self.isNameVisible(name)) return false;
// 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).
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});
return true;
@@ -14081,6 +14092,28 @@ pub const Lowering = struct {
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
/// 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).
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;
}
for (set.flat) |fa| {
if (fnDeclOfRaw(fa.raw) != null) return true;
}
return false;
}
/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)).
fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId {
// A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is