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:
23
examples/0770-modules-type-fn-head-non-transitive.sx
Normal file
23
examples/0770-modules-type-fn-head-non-transitive.sx
Normal 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
|
||||
}
|
||||
6
examples/0770-modules-type-fn-head-non-transitive/b.sx
Normal file
6
examples/0770-modules-type-fn-head-non-transitive/b.sx
Normal 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;
|
||||
5
examples/0770-modules-type-fn-head-non-transitive/c.sx
Normal file
5
examples/0770-modules-type-fn-head-non-transitive/c.sx
Normal 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; };
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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)));
|
||||
| ^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user