fix(stdlib/E4): gate unqualified parameterized type heads non-transitively
attempt-3: extend the E4 single-hop bare-TYPE gate to parameterized type
HEADS (the constructor-head analog of the bare-leaf gate). Before this, the
head lookup hit the global struct_template_map / protocol_ast_map /
fn_ast_map *before* any source-aware visibility check, so a 2-flat-hop
imported generic struct/protocol/type-fn remained bare-visible (e.g.
`Box(s64)` when main imports only b.sx and b.sx imports c.sx).
- headTypeLeak: generic-struct / parameterized-protocol heads use the same
type-author single-hop model as the bare-leaf gate (moduleTypeAuthor +
flatTypeAuthorCount + localTypeInSource + nameAuthoredAsTypeAnywhere).
- headFnLeak: type-returning-function heads use single-hop function
visibility (isNameVisible), exempting scope-local mangled type-fns.
- Gated at every unqualified head site: resolveParameterizedWithBindings,
resolveTypeCallWithBindings, the scanDecls alias-decl dispatch (poisoning
the alias with .unresolved on leak), resolveArrayLiteralType, and the
generic-static-method call path. Namespaced (`ns.Box(..)`) heads are an
explicit qualified reach and stay exempt. Source-pinned instantiation
(E3/E4) is preserved, so library-internal heads still resolve where they
are visible.
Regression: examples/0764-modules-import-generic-head-non-transitive
(2-hop `Box(s64)` -> "type 'Box' is not visible", exit 1; direct #import
resolves). Fails-before on a250964 (printed 3), passes-after.
README: note the non-transitive rule covers parameterized type heads.
Gate: zig build 0, zig build test 0 (LSP 522, 423/423), run_examples
505/0, FFI 12xx/13xx/14xx green, 0706/0763/0544/0105 green & byte-identical,
m3te ios-sim build+launch exit 0.
This commit is contained in:
@@ -1019,8 +1019,19 @@ pub const Lowering = struct {
|
||||
.field_access => |fa| fa.field,
|
||||
else => "",
|
||||
};
|
||||
// A namespaced callee (`ns.Box(..)`) is an explicit qualified
|
||||
// reach, exempt from the bare-head visibility gate (E4).
|
||||
const head_qualified = call_data.callee.data == .field_access;
|
||||
if (callee_name.len > 0) {
|
||||
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
|
||||
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| reg: {
|
||||
// 2-hop generic-struct head leak: poison the alias with
|
||||
// `.unresolved` (suppressed downstream) so the use site
|
||||
// sees no fabricated-stub cascade, only the loud
|
||||
// "not visible" diagnostic the gate already emitted.
|
||||
if (!head_qualified and self.headTypeLeak(callee_name, call_data.callee.span)) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
|
||||
break :reg;
|
||||
}
|
||||
const inst_id = self.instantiateGenericStruct(tmpl, call_data.args);
|
||||
// Register under the alias name
|
||||
const alias_name_id = self.module.types.internString(cd.name);
|
||||
@@ -1055,7 +1066,9 @@ pub const Lowering = struct {
|
||||
} else if (self.program_index.fn_ast_map.get(callee_name)) |fd| {
|
||||
// Type-returning function: Foo :: Complex(u32)
|
||||
if (fd.type_params.len > 0) {
|
||||
if (self.instantiateTypeFunction(cd.name, callee_name, fd, call_data.args)) |result_ty| {
|
||||
if (!head_qualified and self.headFnLeak(callee_name, call_data.callee.span)) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
|
||||
} else if (self.instantiateTypeFunction(cd.name, callee_name, fd, call_data.args)) |result_ty| {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
|
||||
}
|
||||
}
|
||||
@@ -1065,7 +1078,12 @@ pub const Lowering = struct {
|
||||
// Type alias for generic struct (from type_bridge path)
|
||||
const pt = &cd.value.data.parameterized_type_expr;
|
||||
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
|
||||
if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| {
|
||||
const pt_qualified = std.mem.indexOfScalar(u8, pt.name, '.') != null;
|
||||
if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| reg: {
|
||||
if (!pt_qualified and self.headTypeLeak(base_name, cd.value.span)) {
|
||||
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
|
||||
break :reg;
|
||||
}
|
||||
const inst_id = self.instantiateGenericStruct(tmpl, pt.args);
|
||||
const alias_name_id = self.module.types.internString(cd.name);
|
||||
const inst_info = self.module.types.get(inst_id);
|
||||
@@ -6837,6 +6855,7 @@ pub const Lowering = struct {
|
||||
}
|
||||
// Try as generic struct
|
||||
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
|
||||
if (cl.callee.data != .field_access and self.headTypeLeak(callee_name, cl.callee.span)) return .unresolved;
|
||||
return self.instantiateGenericStruct(tmpl, cl.args);
|
||||
}
|
||||
return .unresolved;
|
||||
@@ -8575,8 +8594,11 @@ pub const Lowering = struct {
|
||||
const inner_name = inner_call.callee.data.identifier.name;
|
||||
const resolved = if (self.scope) |scope| (scope.lookupFn(inner_name) orelse inner_name) else inner_name;
|
||||
|
||||
// Generic struct static method: Animated(Size).make(...)
|
||||
// Generic struct static method: Animated(Size).make(...).
|
||||
// `inner_call.callee` is an identifier here (guarded above), so
|
||||
// the head is unqualified and subject to the bare-head gate (E4).
|
||||
if (self.program_index.struct_template_map.getPtr(resolved)) |tmpl| {
|
||||
if (self.headTypeLeak(inner_name, inner_call.callee.span)) return Ref.none;
|
||||
const inst_ty = self.instantiateGenericStruct(tmpl, inner_call.args);
|
||||
const inst_name = self.formatTypeName(inst_ty);
|
||||
// Look up template method, monomorphize, and call
|
||||
@@ -8601,6 +8623,7 @@ pub const Lowering = struct {
|
||||
|
||||
if (self.program_index.fn_ast_map.get(resolved)) |fd| {
|
||||
if (fd.type_params.len > 0) {
|
||||
if (self.headFnLeak(inner_name, inner_call.callee.span)) return Ref.none;
|
||||
// Try instantiate as type function
|
||||
if (self.instantiateTypeFunction(inner_name, inner_name, fd, inner_call.args)) |result_ty| {
|
||||
const type_info = self.module.types.get(result_ty);
|
||||
@@ -13821,8 +13844,65 @@ pub const Lowering = struct {
|
||||
d.addFmt(.err, arg_node.span, "value {} does not fit in {s} parameter {s}", .{ value, type_name, param_name });
|
||||
}
|
||||
|
||||
/// Single-hop non-transitive visibility gate for an UNQUALIFIED parameterized
|
||||
/// type HEAD that names a generic STRUCT or a parameterized PROTOCOL
|
||||
/// (`Box(s64)`, `VL(s64)`) — the constructor-head analog of the bare-leaf
|
||||
/// type gate (E4). A head is visible iff a TYPE author for `name` is reachable
|
||||
/// from the USE site over its OWN declaration or a DIRECT flat-import edge —
|
||||
/// the SAME single-hop set the bare leaf / value / fn leaves use (0706), NOT
|
||||
/// the transitive closure. Emits the leak diagnostic + returns TRUE when the
|
||||
/// head is a real type author somewhere but NOT reachable here (a 2-flat-hop
|
||||
/// leak), so the caller poisons with `.unresolved`. Falls open (FALSE, no
|
||||
/// diagnostic) when import facts are unwired (registration / comptime — no
|
||||
/// querying module), the source context is absent, or the compiler-synthesized
|
||||
/// default-Context emitter is running (built-in infrastructure resolves
|
||||
/// independent of the user program's import style, F1). A block-local generic
|
||||
/// of THIS source is visible in its own scope. Library-internal heads stay
|
||||
/// visible because every instantiation kind is source-pinned to the template's
|
||||
/// defining module (E3/E4 #1): the query originates THERE, where the head is a
|
||||
/// direct flat import — not at the cross-module call site. Only the bare
|
||||
/// (identifier-callee / dotless) form is gated; a namespaced `ns.Box(..)` head
|
||||
/// is an explicit qualified reach and is exempt (the caller skips this gate).
|
||||
fn headTypeLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool {
|
||||
if (self.emitting_default_context) return false;
|
||||
if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return false;
|
||||
const from = self.current_source_file orelse return false;
|
||||
// Reachable as a TYPE author over own / direct-flat edges → visible.
|
||||
if (self.moduleTypeAuthor(from, name) != null) return false;
|
||||
if (self.flatTypeAuthorCount(name, from) != .none) return false;
|
||||
// A block-local generic declared in THIS source is visible here.
|
||||
if (self.localTypeInSource(from, name)) return false;
|
||||
// Authored as a TYPE somewhere but unreachable from `from` → a leak.
|
||||
if (!self.nameAuthoredAsTypeAnywhere(name)) return false;
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Single-hop non-transitive visibility 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.
|
||||
/// Emits + returns TRUE when the fn is authored somewhere but not reachable
|
||||
/// from the use site (a 2-flat-hop leak). A scope-local (mangled) type-fn is
|
||||
/// visible in its own scope and 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;
|
||||
if (self.current_source_file == null) return false;
|
||||
if (self.scope) |s| if (s.lookupFn(name) != null) return false;
|
||||
if (self.isNameVisible(name)) return false;
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name});
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 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
|
||||
// exempt from the bare-head visibility gate; only a plain identifier head
|
||||
// is policed (E4).
|
||||
const is_qualified = cl.callee.data == .field_access;
|
||||
const callee_name: []const u8 = switch (cl.callee.data) {
|
||||
.identifier => |id| id.name,
|
||||
.field_access => |fa| fa.field,
|
||||
@@ -13836,6 +13916,7 @@ pub const Lowering = struct {
|
||||
}
|
||||
// User-defined generic struct
|
||||
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
|
||||
if (!is_qualified and self.headTypeLeak(callee_name, cl.callee.span)) return .unresolved;
|
||||
return self.instantiateGenericStruct(tmpl, cl.args);
|
||||
}
|
||||
// User-defined type-returning function: Complex(u32), Sx(f32)
|
||||
@@ -13843,6 +13924,7 @@ pub const Lowering = struct {
|
||||
const resolved_name = if (self.scope) |scope| (scope.lookupFn(callee_name) orelse callee_name) else callee_name;
|
||||
if (self.program_index.fn_ast_map.get(resolved_name)) |fd| {
|
||||
if (fd.type_params.len > 0) {
|
||||
if (!is_qualified and self.headFnLeak(callee_name, cl.callee.span)) return .unresolved;
|
||||
if (self.instantiateTypeFunction(callee_name, callee_name, fd, cl.args)) |ty| {
|
||||
return ty;
|
||||
}
|
||||
@@ -13859,6 +13941,10 @@ pub const Lowering = struct {
|
||||
fn resolveParameterizedWithBindings(self: *Lowering, pt: *const ast.ParameterizedTypeExpr, span: ?ast.Span) TypeId {
|
||||
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
|
||||
const table = &self.module.types;
|
||||
// A namespaced base (`ns.Box(..)`) is an explicit qualified reach and is
|
||||
// exempt from the bare-head visibility gate; only a dotless head is
|
||||
// policed (E4).
|
||||
const is_qualified = std.mem.indexOfScalar(u8, pt.name, '.') != null;
|
||||
|
||||
// Vector(N, T) — built-in parameterized type. A backtick raw base
|
||||
// (`` `Vector(…) ``) is the LITERAL user type named `Vector`, so it
|
||||
@@ -13873,6 +13959,7 @@ pub const Lowering = struct {
|
||||
|
||||
// User-defined generic struct: look up template and instantiate
|
||||
if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| {
|
||||
if (!is_qualified and self.headTypeLeak(base_name, span)) return .unresolved;
|
||||
return self.instantiateGenericStruct(tmpl, pt.args);
|
||||
}
|
||||
|
||||
@@ -13880,6 +13967,7 @@ pub const Lowering = struct {
|
||||
// 16-byte protocol value with the type-arg bound (not a 0-field stub).
|
||||
if (self.program_index.protocol_ast_map.get(base_name)) |pd| {
|
||||
if (pd.type_params.len > 0) {
|
||||
if (!is_qualified and self.headTypeLeak(base_name, span)) return .unresolved;
|
||||
return self.instantiateParamProtocol(pd, pt.args);
|
||||
}
|
||||
}
|
||||
@@ -13892,6 +13980,7 @@ pub const Lowering = struct {
|
||||
const resolved_name = if (self.scope) |scope| (scope.lookupFn(base_name) orelse base_name) else base_name;
|
||||
if (self.program_index.fn_ast_map.get(resolved_name)) |fd| {
|
||||
if (fd.type_params.len > 0) {
|
||||
if (!is_qualified and self.headFnLeak(base_name, span)) return .unresolved;
|
||||
if (self.instantiateTypeFunction(base_name, base_name, fd, pt.args)) |ty| {
|
||||
return ty;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user