diff --git a/examples/0765-modules-import-reflection-type-non-transitive.sx b/examples/0765-modules-import-reflection-type-non-transitive.sx new file mode 100644 index 0000000..84db8a2 --- /dev/null +++ b/examples/0765-modules-import-reflection-type-non-transitive.sx @@ -0,0 +1,27 @@ +// `#import` is non-transitive for a type named in a REFLECTION / type-arg slot +// (`size_of(T)`, `size_of(*T)`) and in a TYPED ARRAY-LITERAL annotation +// (`T.[...]`), exactly like a bare leaf annotation (0763), a parameterized head +// (0764), and values/functions (0706): when A imports B and B imports C, A must +// NOT see C's top-level types here either. This file imports `b.sx` (which +// imports `c.sx`) and references C's `Nums` / `COnly` in those positions — each +// is rejected with a "type ... is not visible; #import the module that declares +// it" diagnostic, BEFORE the global type table can resolve it. +// +// `b.sx` ↔ `c.sx` together still compile: `b_sizes` / `b_arr` resolve `Nums` / +// `COnly` because b.sx directly imports c.sx (one flat hop there, two from a +// file that imports b.sx). +// +// Regression (Phase E4): before the bare-TYPE gate reached the reflection +// type-arg and array-literal leaf sites, these 2-flat-hop references leaked +// through the global `type_alias_map` / `findByName` / `type_bridge` lookup. + +#import "modules/std.sx"; +#import "0765-modules-import-reflection-type-non-transitive/b.sx"; + +main :: () -> s32 { + print("{}\n", size_of(Nums)); + print("{}\n", size_of(*COnly)); + xs := Nums.[1, 2]; + print("{} {}\n", xs[0], xs[1]); + 0 +} diff --git a/examples/0765-modules-import-reflection-type-non-transitive/b.sx b/examples/0765-modules-import-reflection-type-non-transitive/b.sx new file mode 100644 index 0000000..eb3cfb4 --- /dev/null +++ b/examples/0765-modules-import-reflection-type-non-transitive/b.sx @@ -0,0 +1,13 @@ +#import "c.sx"; + +// b.sx directly imports c.sx, so it CAN name these types in reflection / +// type-arg / array-literal positions — the type is one flat hop away here, +// two from a file that imports b.sx. +b_sizes :: () -> s64 { + size_of(Nums) + size_of(*COnly) +} + +b_arr :: () -> s64 { + xs := Nums.[1, 2]; + xs[0] + xs[1] +} diff --git a/examples/0765-modules-import-reflection-type-non-transitive/c.sx b/examples/0765-modules-import-reflection-type-non-transitive/c.sx new file mode 100644 index 0000000..dcacacc --- /dev/null +++ b/examples/0765-modules-import-reflection-type-non-transitive/c.sx @@ -0,0 +1,3 @@ +Nums :: [2]s64; + +COnly :: struct { v: s64 = 0; } diff --git a/examples/0766-modules-reflection-type-direct-ok.sx b/examples/0766-modules-reflection-type-direct-ok.sx new file mode 100644 index 0000000..991fd5d --- /dev/null +++ b/examples/0766-modules-reflection-type-direct-ok.sx @@ -0,0 +1,16 @@ +// The pass side of 0765: a DIRECTLY imported module's types are bare-visible in +// reflection / type-arg slots (`size_of(Nums)`, `size_of(*COnly)`) and in a +// typed array-literal annotation (`Nums.[...]`) — single-hop visibility, so a +// 1-flat-hop type resolves exactly as before the E4 gate. Confirms the gate +// only rejects the 2-hop case (0765), never a directly-imported reference. + +#import "modules/std.sx"; +#import "0766-modules-reflection-type-direct-ok/c.sx"; + +main :: () -> s32 { + print("{}\n", size_of(Nums)); + print("{}\n", size_of(*COnly)); + xs := Nums.[1, 2]; + print("{} {}\n", xs[0], xs[1]); + 0 +} diff --git a/examples/0766-modules-reflection-type-direct-ok/c.sx b/examples/0766-modules-reflection-type-direct-ok/c.sx new file mode 100644 index 0000000..dcacacc --- /dev/null +++ b/examples/0766-modules-reflection-type-direct-ok/c.sx @@ -0,0 +1,3 @@ +Nums :: [2]s64; + +COnly :: struct { v: s64 = 0; } diff --git a/examples/expected/0765-modules-import-reflection-type-non-transitive.exit b/examples/expected/0765-modules-import-reflection-type-non-transitive.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0765-modules-import-reflection-type-non-transitive.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0765-modules-import-reflection-type-non-transitive.stderr b/examples/expected/0765-modules-import-reflection-type-non-transitive.stderr new file mode 100644 index 0000000..49c25c6 --- /dev/null +++ b/examples/expected/0765-modules-import-reflection-type-non-transitive.stderr @@ -0,0 +1,17 @@ +error: type 'Nums' is not visible; #import the module that declares it + --> /Users/agra/projects/sx/examples/0765-modules-import-reflection-type-non-transitive.sx:22:27 + | +22 | print("{}\n", size_of(Nums)); + | ^^^^ + +error: type 'COnly' is not visible; #import the module that declares it + --> /Users/agra/projects/sx/examples/0765-modules-import-reflection-type-non-transitive.sx:23:28 + | +23 | print("{}\n", size_of(*COnly)); + | ^^^^^ + +error: type 'Nums' is not visible; #import the module that declares it + --> /Users/agra/projects/sx/examples/0765-modules-import-reflection-type-non-transitive.sx:24:11 + | +24 | xs := Nums.[1, 2]; + | ^^^^ diff --git a/examples/expected/0765-modules-import-reflection-type-non-transitive.stdout b/examples/expected/0765-modules-import-reflection-type-non-transitive.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0765-modules-import-reflection-type-non-transitive.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/0766-modules-reflection-type-direct-ok.exit b/examples/expected/0766-modules-reflection-type-direct-ok.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0766-modules-reflection-type-direct-ok.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0766-modules-reflection-type-direct-ok.stderr b/examples/expected/0766-modules-reflection-type-direct-ok.stderr new file mode 100644 index 0000000..e69de29 diff --git a/examples/expected/0766-modules-reflection-type-direct-ok.stdout b/examples/expected/0766-modules-reflection-type-direct-ok.stdout new file mode 100644 index 0000000..d7ddf32 --- /dev/null +++ b/examples/expected/0766-modules-reflection-type-direct-ok.stdout @@ -0,0 +1,3 @@ +16 +8 +1 2 diff --git a/readme.md b/readme.md index e7edb7e..2b399ad 100644 --- a/readme.md +++ b/readme.md @@ -411,7 +411,10 @@ gated exactly like a bare leaf type — the constructor head must be reachable o your own or a direct flat import, not two hops away. A bare reference to a namespaced-only import's member — function, module constant, or **type** (leaf or generic head) — 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 +#import the module that declares it`); qualify it as `m.name`. The type gate holds +wherever a bare type name is named — a value/field annotation, a reflection / +type-arg slot (`size_of(T)`, `size_of(*T)`), a typed array-literal head (`T.[…]`), +or a type-as-value / type-match arm — not just plain annotations. (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.) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 889e230..7a7798a 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -4097,6 +4097,12 @@ pub const Lowering = struct { // (`x: Type = Vec4`), comparison (`x == Vec4`), and // pack-arg / Any context (boxing happens at the // consumer). + // E4 single-hop visibility gate: a bare type name used as a VALUE + // (`x: Type = COnly`, `x == COnly`) reachable only over 2+ flat hops + // is not bare-visible either (consistent with annotations / 0763). + // `headTypeLeak` fires only for a real type author unreachable from + // here; a value name / generic param / undeclared name falls through. + if (self.headTypeLeak(id.name, node.span)) break :blk self.emitPlaceholder(id.name); const ty = blk_ty: { if (self.type_bindings) |tb| { if (tb.get(id.name)) |t| break :blk_ty t; @@ -5425,6 +5431,14 @@ pub const Lowering = struct { .type_expr => |te| te.name, else => "", }; + // E4 single-hop visibility gate: a SPECIFIC 2-flat-hop type name in + // a type-match arm (`case COnly:`) is not bare-visible (consistent + // with annotations / 0763). A category keyword (`int`, `struct`, …) + // is not a type author anywhere, so the gate is a no-op for those. + if (self.headTypeLeak(name, pat.span)) { + arm_tag_values.append(self.alloc, &.{}) catch unreachable; + continue; + } const tag_values = self.resolveTypeCategoryTags(name); arm_tag_values.append(self.alloc, tag_values) catch unreachable; for (tag_values) |tag| { @@ -6862,10 +6876,17 @@ pub const Lowering = struct { }, .parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span), .identifier => |id| { + // E4 single-hop visibility gate: a 2-flat-hop bare type name in a + // typed array/vector-literal annotation (`Nums.[1, 2]`) is not + // bare-visible (consistent with annotations / 0763). + if (self.headTypeLeak(id.name, te.span)) return .unresolved; const name_id = self.module.types.internString(id.name); return self.module.types.findByName(name_id) orelse .unresolved; }, - .type_expr => return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map), + .type_expr => |inner| { + if (self.headTypeLeak(inner.name, te.span)) return .unresolved; + return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); + }, .field_access => |fa| { // Module.Type — try to resolve the field as a type name const name_id = self.module.types.internString(fa.field); @@ -12330,6 +12351,13 @@ pub const Lowering = struct { } return .unresolved; } + // E4 single-hop visibility gate: each element leaf is resolved through + // the source-aware resolver, so a 2-flat-hop inner leaf (`(COnly, s64)`) + // emits "not visible" + poisons rather than leaking through + // `type_bridge`'s ungated global lookup. A valid element resolves to the + // same TypeId the delegated build produces below (no diagnostic, no + // drift); only the poison short-circuits. + if (self.resolveTypeWithBindings(el.value) == .unresolved) return .unresolved; } return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); } @@ -12382,6 +12410,12 @@ pub const Lowering = struct { if (self.type_bindings) |tb| { if (tb.get(id.name)) |ty| return ty; } + // E4 single-hop visibility gate: a bare type name reachable only + // over 2+ flat hops is not bare-visible in a reflection / type-arg + // slot either (consistent with normal annotations / 0763). A + // genuinely-undeclared name is NOT authored as a type anywhere, so + // the gate falls through to the "unresolved type" diagnostic below. + if (self.headTypeLeak(id.name, node.span)) return .unresolved; if (self.program_index.type_alias_map.get(id.name)) |alias_ty| return alias_ty; const name_id = self.module.types.internString(id.name); if (self.module.types.findByName(name_id)) |t| return t; @@ -12391,6 +12425,7 @@ pub const Lowering = struct { return .unresolved; }, .type_expr => |te| { + if (self.headTypeLeak(te.name, node.span)) return .unresolved; if (self.program_index.type_alias_map.get(te.name)) |alias_ty| return alias_ty; return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, @@ -12410,14 +12445,20 @@ pub const Lowering = struct { // Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32)) return self.resolveTypeCallWithBindings(&cl); }, - .tuple_literal => return self.resolveTupleLiteralTypeArg(node), + // Wrapped / structural forms (`*T`, `[N]T`, `[]T`, `?T`, fn-ptr, tuple) + // route through the gated `resolveTypeWithBindings`, whose + // `resolveCompound` recurses each element through the source-aware leaf + // (`resolveNominalLeaf`) — so a 2-hop inner leaf (`*COnly`, `[2]COnly`, + // `(COnly, s64)`) is rejected exactly as in a normal annotation, instead + // of `type_bridge.resolveAstType`'s ungated global lookup (E4). + .tuple_literal, .pointer_type_expr, .many_pointer_type_expr, .array_type_expr, .slice_type_expr, .optional_type_expr, .function_type_expr, - => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map), + => return self.resolveTypeWithBindings(node), else => return .unresolved, } }