diff --git a/examples/0749-modules-namespaced-only-bare-param-alias-not-visible.sx b/examples/0749-modules-namespaced-only-bare-param-alias-not-visible.sx new file mode 100644 index 0000000..73d9017 --- /dev/null +++ b/examples/0749-modules-namespaced-only-bare-param-alias-not-visible.sx @@ -0,0 +1,17 @@ +// Bare PARAMETERIZED-struct alias visibility under a NAMESPACED-only import — +// the generic-struct sibling of 0747 (plain alias) and 0743 (named type). A +// generic-struct instantiation alias (`Secret :: Box(s32)`) registers ONLY a +// named struct type in the TypeTable; its raw import fact stays `.const_decl`, +// so before the fix it was NOT recognised as a type author and a BARE `Secret` +// leaked to the registered struct with NO diagnostic (the value silently came +// out 42). The unified declaration-fact writer routes the instantiation alias +// through `type_aliases_by_source`, so the bare-TYPE gate treats it like any +// other alias: `dep.sx` is imported only as `dep :: #import`, so bare `Secret` +// is reachable ONLY as `dep.Secret` and must NOT resolve. Regression +// (attempt-5 R4-parameterized-alias-leak). +dep :: #import "0749-modules-namespaced-only-bare-param-alias-not-visible/dep.sx"; + +main :: () -> s32 { + s : Secret = .{ value = 42 }; + s.value +} diff --git a/examples/0749-modules-namespaced-only-bare-param-alias-not-visible/dep.sx b/examples/0749-modules-namespaced-only-bare-param-alias-not-visible/dep.sx new file mode 100644 index 0000000..434c566 --- /dev/null +++ b/examples/0749-modules-namespaced-only-bare-param-alias-not-visible/dep.sx @@ -0,0 +1,5 @@ +Box :: struct($T: Type) { + value: T; +} + +Secret :: Box(s32); diff --git a/examples/expected/0749-modules-namespaced-only-bare-param-alias-not-visible.exit b/examples/expected/0749-modules-namespaced-only-bare-param-alias-not-visible.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0749-modules-namespaced-only-bare-param-alias-not-visible.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0749-modules-namespaced-only-bare-param-alias-not-visible.stderr b/examples/expected/0749-modules-namespaced-only-bare-param-alias-not-visible.stderr new file mode 100644 index 0000000..6c2f0a2 --- /dev/null +++ b/examples/expected/0749-modules-namespaced-only-bare-param-alias-not-visible.stderr @@ -0,0 +1,5 @@ +error: type 'Secret' is not visible; #import the module that declares it + --> examples/0749-modules-namespaced-only-bare-param-alias-not-visible.sx:15:9 + | +15 | s : Secret = .{ value = 42 }; + | ^^^^^^ diff --git a/examples/expected/0749-modules-namespaced-only-bare-param-alias-not-visible.stdout b/examples/expected/0749-modules-namespaced-only-bare-param-alias-not-visible.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0749-modules-namespaced-only-bare-param-alias-not-visible.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/lower.zig b/src/ir/lower.zig index a332dd7..114106e 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -782,29 +782,34 @@ pub const Lowering = struct { return ti.function.call_conv != .c; } - // ── Source-keyed cache writers (R5 §#4) ── - // Mirror each global `type_alias_map` / `module_const_map` / `global_names` - // write into its source-partitioned analogue, keyed by the registering - // decl's source. Behavior-preserving for now: the global maps stay the only - // readers; the per-source maps exist for the later read-side cutover. A null - // source (unreachable for a scanned top-level decl post-import-resolution) - // falls back to the main file; if even that is absent the write is skipped - // rather than recorded under a fabricated key. - fn recordTypeAliasBySource(self: *Lowering, source: ?[]const u8, name: []const u8, tid: TypeId) void { - const src = source orelse self.main_file orelse return; - self.program_index.putTypeAliasBySource(src, name, tid); + // ── Unified declaration-fact writers (R5 §#4) ── + // The SOLE writers of the three semantic maps — global + // `type_alias_map` / `module_const_map` / `global_names` AND their + // source-partitioned analogues (`*_by_source`). Invariant: the global and + // by-source write for a name are inseparable — a write-site that mirrors + // one without the other lets a ns-only author miss `*_by_source` and leak + // past the source-aware bare-TYPE gate. No raw `.put`/`.remove` to the + // three maps exists outside these helpers (grep-checkable — mirrors the + // no-raw-`TypeTable.update` discipline). The global map stays the only + // READER for now; the per-source cache feeds the gate. A null source + // (unreachable for a scanned top-level decl post-import-resolution) falls + // back to the main file; if even that is absent only the by-source write is + // skipped — the global map is always written. + fn putTypeAlias(self: *Lowering, source: ?[]const u8, name: []const u8, tid: TypeId) void { + self.program_index.type_alias_map.put(name, tid) catch {}; + if (source orelse self.main_file) |src| self.program_index.putTypeAliasBySource(src, name, tid); } - fn recordModuleConstBySource(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.ModuleConstInfo) void { - const src = source orelse self.main_file orelse return; - self.program_index.putModuleConstBySource(src, name, info); + fn putModuleConst(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.ModuleConstInfo) void { + self.program_index.module_const_map.put(name, info) catch {}; + if (source orelse self.main_file) |src| self.program_index.putModuleConstBySource(src, name, info); } - fn recordGlobalBySource(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.GlobalInfo) void { - const src = source orelse self.main_file orelse return; - self.program_index.putGlobalBySource(src, name, info); + fn putGlobal(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.GlobalInfo) void { + self.program_index.global_names.put(name, info) catch {}; + if (source orelse self.main_file) |src| self.program_index.putGlobalBySource(src, name, info); } - fn dropModuleConstBySource(self: *Lowering, source: ?[]const u8, name: []const u8) void { - const src = source orelse self.main_file orelse return; - self.program_index.removeModuleConstBySource(src, name); + fn dropModuleConst(self: *Lowering, source: ?[]const u8, name: []const u8) void { + _ = self.program_index.module_const_map.remove(name); + if (source orelse self.main_file) |src| self.program_index.removeModuleConstBySource(src, name); } /// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies. @@ -829,13 +834,11 @@ pub const Lowering = struct { switch (cd.value.data) { .int_literal => { const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .s64 }; - self.program_index.module_const_map.put(cd.name, info) catch {}; - self.recordModuleConstBySource(decl.source_file, cd.name, info); + self.putModuleConst(decl.source_file, cd.name, info); }, .float_literal => { const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .f64 }; - self.program_index.module_const_map.put(cd.name, info) catch {}; - self.recordModuleConstBySource(decl.source_file, cd.name, info); + self.putModuleConst(decl.source_file, cd.name, info); }, // A const whose RHS is an integer EXPRESSION over other consts // (`M :: 2; N :: M + 1`) is itself a usable count: register it so @@ -845,8 +848,7 @@ pub const Lowering = struct { // non-const), `moduleConstInt` yields null and the use diagnoses. .binary_op, .unary_op => { const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .s64 }; - self.program_index.module_const_map.put(cd.name, info) catch {}; - self.recordModuleConstBySource(decl.source_file, cd.name, info); + self.putModuleConst(decl.source_file, cd.name, info); }, else => {}, } @@ -930,8 +932,7 @@ pub const Lowering = struct { d.addFmt(.err, cd.value.span, "type alias '{s}' could not be resolved: an array dimension is not a compile-time integer constant", .{cd.name}); } } - self.program_index.type_alias_map.put(cd.name, target_ty) catch {}; - self.recordTypeAliasBySource(self.current_source_file, cd.name, target_ty); + self.putTypeAlias(self.current_source_file, cd.name, target_ty); } else if (cd.value.data == .identifier) { // Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide; // Chase through type_alias_map, then look up named types @@ -940,13 +941,11 @@ pub const Lowering = struct { // type_alias_map at use time. const rhs_name = cd.value.data.identifier.name; if (self.program_index.type_alias_map.get(rhs_name)) |chained| { - self.program_index.type_alias_map.put(cd.name, chained) catch {}; - self.recordTypeAliasBySource(self.current_source_file, cd.name, chained); + self.putTypeAlias(self.current_source_file, cd.name, chained); } else { const name_id = self.module.types.internString(rhs_name); if (self.module.types.findByName(name_id)) |tid| { - self.program_index.type_alias_map.put(cd.name, tid) catch {}; - self.recordTypeAliasBySource(self.current_source_file, cd.name, tid); + self.putTypeAlias(self.current_source_file, cd.name, tid); } } } @@ -972,6 +971,13 @@ pub const Lowering = struct { } }; const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info); self.module.types.updatePreservingKey(alias_id, alias_info); + // A generic-struct instantiation alias IS a type + // author: route it through the unified writer so it + // lands in `type_aliases_by_source` and the bare-TYPE + // gate treats it like any other alias (a ns-only + // `Secret :: Box(s32)` is rejected, a flat one + // resolves to the same TypeId `findByName` would). + self.putTypeAlias(self.current_source_file, cd.name, alias_id); } } else if (std.mem.eql(u8, callee_name, "Vector")) { // Builtin type constructor — checked BEFORE @@ -983,15 +989,13 @@ pub const Lowering = struct { // hard-codes the vector layout. const result_ty = self.resolveTypeCallWithBindings(call_data); if (result_ty != .void) { - self.program_index.type_alias_map.put(cd.name, result_ty) catch {}; - self.recordTypeAliasBySource(self.current_source_file, cd.name, result_ty); + self.putTypeAlias(self.current_source_file, cd.name, result_ty); } } 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| { - self.program_index.type_alias_map.put(cd.name, result_ty) catch {}; - self.recordTypeAliasBySource(self.current_source_file, cd.name, result_ty); + self.putTypeAlias(self.current_source_file, cd.name, result_ty); } } } @@ -1011,6 +1015,10 @@ pub const Lowering = struct { } }; const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info); self.module.types.updatePreservingKey(alias_id, alias_info); + // Same as the `.call` generic-struct branch: a + // parameterized-struct alias is a type author and + // must reach `type_aliases_by_source` so it gates. + self.putTypeAlias(self.current_source_file, cd.name, alias_id); } } else { // Builtin parameterised type (Vector(N, T) etc) — @@ -1019,8 +1027,7 @@ pub const Lowering = struct { // position can `const_type()`. const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); if (result_ty != .void and result_ty != .unresolved) { - self.program_index.type_alias_map.put(cd.name, result_ty) catch {}; - self.recordTypeAliasBySource(self.current_source_file, cd.name, result_ty); + self.putTypeAlias(self.current_source_file, cd.name, result_ty); } } } @@ -1045,8 +1052,7 @@ pub const Lowering = struct { }; if (lit_ty) |ty| { const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = ty }; - self.program_index.module_const_map.put(cd.name, info) catch {}; - self.recordModuleConstBySource(self.current_source_file, cd.name, info); + self.putModuleConst(self.current_source_file, cd.name, info); } } }, @@ -1126,8 +1132,7 @@ pub const Lowering = struct { // don't pile a bogus type-mismatch on top, and don't leave the pass-0 // placeholder behind as a usable const. if (ty == .unresolved) { - _ = self.program_index.module_const_map.remove(cd.name); - self.dropModuleConstBySource(self.current_source_file, cd.name); + self.dropModuleConst(self.current_source_file, cd.name); return; } // Validate the initializer against the explicit annotation BY TYPE, so a @@ -1146,8 +1151,7 @@ pub const Lowering = struct { if (self.isIntEx(ty) and isFloat(self.inferExprType(cd.value))) { if (program_index_mod.evalConstFloatExpr(cd.value, self)) |fv| { self.diagNonIntegralNarrow(cd.value.span, fv, ty); - _ = self.program_index.module_const_map.remove(cd.name); - self.dropModuleConstBySource(self.current_source_file, cd.name); + self.dropModuleConst(self.current_source_file, cd.name); return; } } @@ -1159,16 +1163,14 @@ pub const Lowering = struct { // Evict the pass-0 placeholder (`N : string : 4` and // `N : string : M + 2` are both pre-registered as `.s64` in scanDecls // pass 0); leaving it would let a count use still fold `N`. - _ = self.program_index.module_const_map.remove(cd.name); - self.dropModuleConstBySource(self.current_source_file, cd.name); + self.dropModuleConst(self.current_source_file, cd.name); return; } // Reconcile the registration with the resolved annotation (pass 0 stored // a literal/expression placeholder type), so the const folds and emits at // its declared type — the same `put` the literal path always did. const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = ty }; - self.program_index.module_const_map.put(cd.name, info) catch {}; - self.recordModuleConstBySource(self.current_source_file, cd.name, info); + self.putModuleConst(self.current_source_file, cd.name, info); } /// True iff a literal initializer of `value`'s kind is faithfully @@ -1284,8 +1286,7 @@ pub const Lowering = struct { .is_const = false, .is_extern = vd.is_foreign, }); - self.program_index.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {}; - self.recordGlobalBySource(self.current_source_file, vd.name, .{ .id = gid, .ty = var_ty }); + self.putGlobal(self.current_source_file, vd.name, .{ .id = gid, .ty = var_ty }); } /// Serialize a top-level global's initializer into a static `ConstantValue`. @@ -1380,14 +1381,12 @@ pub const Lowering = struct { if (self.program_index.type_alias_map.contains(cd.name)) continue; const rhs_name = cd.value.data.identifier.name; if (self.program_index.type_alias_map.get(rhs_name)) |chained| { - self.program_index.type_alias_map.put(cd.name, chained) catch {}; - self.recordTypeAliasBySource(decl.source_file, cd.name, chained); + self.putTypeAlias(decl.source_file, cd.name, chained); progressed = true; } else { const name_id = self.module.types.internString(rhs_name); if (self.module.types.findByName(name_id)) |tid| { - self.program_index.type_alias_map.put(cd.name, tid) catch {}; - self.recordTypeAliasBySource(decl.source_file, cd.name, tid); + self.putTypeAlias(decl.source_file, cd.name, tid); progressed = true; } } @@ -9940,8 +9939,7 @@ pub const Lowering = struct { }); // Register for runtime lookup: identifier resolution emits global_get - self.program_index.global_names.put(name, .{ .id = gid, .ty = global_ty }) catch {}; - self.recordGlobalBySource(self.current_source_file, name, .{ .id = gid, .ty = global_ty }); + self.putGlobal(self.current_source_file, name, .{ .id = gid, .ty = global_ty }); } /// Lower a standalone `#run expr;` at the top level (side-effect only). @@ -14730,8 +14728,7 @@ pub const Lowering = struct { .init_val = .{ .aggregate = ctx_fields }, .is_const = true, }); - self.program_index.global_names.put(global_name, .{ .id = gid, .ty = ctx_ty }) catch {}; - self.recordGlobalBySource(self.current_source_file, global_name, .{ .id = gid, .ty = ctx_ty }); + self.putGlobal(self.current_source_file, global_name, .{ .id = gid, .ty = ctx_ty }); } /// Create a thunk function: __thunk_ConcreteType_Protocol_method(ctx: *void, args...) -> ret