Make same-name top-level types in different sources DISTINCT nominal types instead of collapsing last-wins in the type table (issue 0105). Registration: - internNamedTypeDecl assigns a per-decl nominal_id and populates type_decl_tids. The first author of a name keeps nominal_id 0 (byte-identical to pre-E2); a genuine cross-module shadow (>=2 distinct normalized-path authors per the import facts) gets a fresh id -> a distinct TypeId. - mergeFlat/addOwnDecl stop first-wins-dropping per-source decls (named types + non-fn const_decls) so every same-name author reaches registration; functions and var_decls (incl. #foreign extern globals) keep first-wins. Resolution (selectNominalLeaf): - own-author wins; else flatTypeAuthorCount over the transitive flat closure: >=2 distinct -> .ambiguous (loud diagnostic + poison); exactly one -> resolved; a flat author not yet findByName-registered -> .undeclared stub (not a leak). - struct-literal type names route through the same source-aware leaf. - lazyLowerFunction pins the function's own source before resolving its return type, so a shadowed signature type resolves in its module, not the caller's. Codegen: - mangleTypeName appends __n<id> for nonzero nominal_id so same-name shadows get distinct monomorph symbols (struct_to_string__Box vs __Box__n1). Library hygiene: - rename trace.sx's compiler-contracted Frame -> TraceFrame (+ the two compiler findByName sites) so it never collides with a UI/geometry Frame; the layout is structural (getFrameStructType / SxFrame), name-independent. Examples: 0752-0756 pin the five 0105 cases (distinct fields / same fields / own-wins / ambiguous / alias per-source); 0170 pins the folded anon-struct-field regression.
346 lines
16 KiB
Zig
346 lines
16 KiB
Zig
const std = @import("std");
|
|
const ast = @import("../ast.zig");
|
|
const types = @import("types.zig");
|
|
const type_bridge = @import("type_bridge.zig");
|
|
const lower = @import("lower.zig");
|
|
|
|
const Node = ast.Node;
|
|
const TypeId = types.TypeId;
|
|
const Lowering = lower.Lowering;
|
|
|
|
/// Generic substitution + monomorphization-key construction (architecture
|
|
/// phase A4.1), extracted from `Lowering`. Owns:
|
|
/// - the type-name mangler (`mangleTypeName` / `mangleParamList`) — the leaf
|
|
/// fragment every mono key is built from,
|
|
/// - the generic mono key (`mangleGenericName`) and the comptime-value mono
|
|
/// fragment (`appendComptimeValueMangle`),
|
|
/// - type-parameter substitution: `buildTypeBindings` (call-site inference)
|
|
/// and `inferGenericReturnType` (generic return resolution).
|
|
///
|
|
/// A `*Lowering` facade (Principle 5, like `CallResolver` / `ExprTyper`):
|
|
/// substitution reads live type-binding / scope state and the type resolver
|
|
/// helpers, so it borrows `*Lowering` rather than re-threading every field.
|
|
/// `Lowering` keeps a thin `mangleTypeName` wrapper (it has ~30 cross-cutting
|
|
/// callers — impl-map keys, conversion keys, shape keys — well beyond
|
|
/// generics); the rest call through `Lowering.genericResolver()`.
|
|
pub const GenericResolver = struct {
|
|
l: *Lowering,
|
|
|
|
// ── Mono-key construction ───────────────────────────────────────────
|
|
|
|
/// Mangle a TypeId into its mono-key fragment ("s64", "ptr_T", "SL_T",
|
|
/// "AR_n_T", struct name, "tu_X_Y", …). Recursive for compound shapes.
|
|
pub fn mangleTypeName(self: GenericResolver, ty: TypeId) []const u8 {
|
|
// Builtin types
|
|
if (ty == .s8) return "s8";
|
|
if (ty == .s16) return "s16";
|
|
if (ty == .s32) return "s32";
|
|
if (ty == .s64) return "s64";
|
|
if (ty == .u8) return "u8";
|
|
if (ty == .u16) return "u16";
|
|
if (ty == .u32) return "u32";
|
|
if (ty == .u64) return "u64";
|
|
if (ty == .f32) return "f32";
|
|
if (ty == .f64) return "f64";
|
|
if (ty == .bool) return "bool";
|
|
if (ty == .void) return "void";
|
|
if (ty == .string) return "string";
|
|
if (ty == .any) return "Any";
|
|
if (ty == .usize) return "usize";
|
|
if (ty == .isize) return "isize";
|
|
|
|
const info = self.l.module.types.get(ty);
|
|
return switch (info) {
|
|
// A nominal type's mangle includes its `nominal_id` when nonzero so two
|
|
// same-DISPLAY-name authors in different sources (issue 0105) produce
|
|
// DISTINCT monomorph symbols (`struct_to_string__Box` vs
|
|
// `struct_to_string__Box__n1`) instead of one symbol with conflicting
|
|
// signatures. `nominal_id == 0` (the single-author / structural case)
|
|
// appends nothing — byte-identical to the pre-E2 mangle.
|
|
.@"struct" => |s| self.mangleNominalName(self.l.module.types.getString(s.name), s.nominal_id),
|
|
.@"union" => |u| self.mangleNominalName(self.l.module.types.getString(u.name), u.nominal_id),
|
|
.tagged_union => |u| self.mangleNominalName(self.l.module.types.getString(u.name), u.nominal_id),
|
|
.@"enum" => |e| self.mangleNominalName(self.l.module.types.getString(e.name), e.nominal_id),
|
|
.pointer => |p| blk: {
|
|
const inner = self.mangleTypeName(p.pointee);
|
|
break :blk std.fmt.allocPrint(self.l.alloc, "ptr_{s}", .{inner}) catch "pointer";
|
|
},
|
|
.many_pointer => |p| blk: {
|
|
const inner = self.mangleTypeName(p.element);
|
|
break :blk std.fmt.allocPrint(self.l.alloc, "mptr_{s}", .{inner}) catch "many_pointer";
|
|
},
|
|
.slice => |s| blk: {
|
|
const inner = self.mangleTypeName(s.element);
|
|
break :blk std.fmt.allocPrint(self.l.alloc, "SL_{s}", .{inner}) catch "slice";
|
|
},
|
|
.array => |a| blk: {
|
|
const inner = self.mangleTypeName(a.element);
|
|
break :blk std.fmt.allocPrint(self.l.alloc, "AR_{d}_{s}", .{ a.length, inner }) catch "array";
|
|
},
|
|
.signed => |w| std.fmt.allocPrint(self.l.alloc, "s{d}", .{w}) catch "signed",
|
|
.unsigned => |w| std.fmt.allocPrint(self.l.alloc, "u{d}", .{w}) catch "unsigned",
|
|
.optional => |o| blk: {
|
|
const inner = self.mangleTypeName(o.child);
|
|
break :blk std.fmt.allocPrint(self.l.alloc, "opt_{s}", .{inner}) catch "optional";
|
|
},
|
|
.vector => |v| blk: {
|
|
const inner = self.mangleTypeName(v.element);
|
|
break :blk std.fmt.allocPrint(self.l.alloc, "vec_{d}_{s}", .{ v.length, inner }) catch "vector";
|
|
},
|
|
.closure => |c| self.mangleParamList("cl", c.params, c.ret),
|
|
.function => |f| self.mangleParamList("fn", f.params, f.ret),
|
|
.tuple => |t| blk: {
|
|
var buf = std.ArrayList(u8).empty;
|
|
buf.appendSlice(self.l.alloc, "tu") catch break :blk "tuple";
|
|
for (t.fields) |fid| {
|
|
buf.append(self.l.alloc, '_') catch break :blk "tuple";
|
|
buf.appendSlice(self.l.alloc, self.mangleTypeName(fid)) catch break :blk "tuple";
|
|
}
|
|
break :blk buf.items;
|
|
},
|
|
else => @tagName(info),
|
|
};
|
|
}
|
|
|
|
/// Append a `__n<id>` disambiguator to a nominal type's display name when its
|
|
/// `nominal_id` is nonzero (a same-name shadow, issue 0105); id 0 returns the
|
|
/// name unchanged so single-author mangling is byte-identical.
|
|
fn mangleNominalName(self: GenericResolver, name: []const u8, nominal_id: u32) []const u8 {
|
|
if (nominal_id == 0) return name;
|
|
return std.fmt.allocPrint(self.l.alloc, "{s}__n{d}", .{ name, nominal_id }) catch name;
|
|
}
|
|
|
|
fn mangleParamList(self: GenericResolver, prefix: []const u8, params: []const TypeId, ret: TypeId) []const u8 {
|
|
var buf = std.ArrayList(u8).empty;
|
|
buf.appendSlice(self.l.alloc, prefix) catch return prefix;
|
|
for (params) |p| {
|
|
buf.append(self.l.alloc, '_') catch return prefix;
|
|
buf.appendSlice(self.l.alloc, self.mangleTypeName(p)) catch return prefix;
|
|
}
|
|
buf.appendSlice(self.l.alloc, "__") catch return prefix;
|
|
buf.appendSlice(self.l.alloc, self.mangleTypeName(ret)) catch return prefix;
|
|
return buf.items;
|
|
}
|
|
|
|
/// Mangle a generic call site into "base__Type1_Type2".
|
|
/// Returns a heap-allocated string owned by the lowering allocator.
|
|
pub fn mangleGenericName(
|
|
self: GenericResolver,
|
|
base_name: []const u8,
|
|
fd: *const ast.FnDecl,
|
|
bindings: *const std.StringHashMap(TypeId),
|
|
) []const u8 {
|
|
var mangled_buf: [256]u8 = undefined;
|
|
var mangled_len: usize = 0;
|
|
for (base_name) |ch| {
|
|
if (mangled_len < mangled_buf.len) {
|
|
mangled_buf[mangled_len] = ch;
|
|
mangled_len += 1;
|
|
}
|
|
}
|
|
for (fd.type_params) |tp| {
|
|
for ("__") |ch| {
|
|
if (mangled_len < mangled_buf.len) {
|
|
mangled_buf[mangled_len] = ch;
|
|
mangled_len += 1;
|
|
}
|
|
}
|
|
const ty = bindings.get(tp.name) orelse .unresolved;
|
|
const type_name_str = self.mangleTypeName(ty);
|
|
for (type_name_str) |ch| {
|
|
if (mangled_len < mangled_buf.len) {
|
|
mangled_buf[mangled_len] = ch;
|
|
mangled_len += 1;
|
|
}
|
|
}
|
|
}
|
|
return self.l.alloc.dupe(u8, mangled_buf[0..mangled_len]) catch base_name;
|
|
}
|
|
|
|
/// Append a comptime parameter VALUE's mono fragment to `buf` (int/bool
|
|
/// verbatim, float with `.`/`-` escaped, string hashed) so distinct
|
|
/// comptime-value call sites get distinct monos.
|
|
pub fn appendComptimeValueMangle(self: GenericResolver, buf: *std.ArrayList(u8), node: *const Node) void {
|
|
switch (node.data) {
|
|
.int_literal => |lit| {
|
|
var tmp: [32]u8 = undefined;
|
|
const written = std.fmt.bufPrint(&tmp, "{d}", .{lit.value}) catch return;
|
|
buf.appendSlice(self.l.alloc, written) catch return;
|
|
},
|
|
.bool_literal => |lit| {
|
|
buf.appendSlice(self.l.alloc, if (lit.value) "true" else "false") catch return;
|
|
},
|
|
.float_literal => |lit| {
|
|
var tmp: [64]u8 = undefined;
|
|
const written = std.fmt.bufPrint(&tmp, "{d}", .{lit.value}) catch return;
|
|
for (written) |c| {
|
|
buf.append(self.l.alloc, if (c == '.') '_' else if (c == '-') 'n' else c) catch return;
|
|
}
|
|
},
|
|
.string_literal => |lit| {
|
|
// Hash the string to a fixed-length tag — keeps the
|
|
// mangle short and stable for arbitrary content.
|
|
var h = std.hash.Wyhash.init(0);
|
|
h.update(lit.raw);
|
|
var tmp: [32]u8 = undefined;
|
|
const written = std.fmt.bufPrint(&tmp, "s{x}", .{h.final()}) catch return;
|
|
buf.appendSlice(self.l.alloc, written) catch return;
|
|
},
|
|
else => buf.append(self.l.alloc, '?') catch return,
|
|
}
|
|
}
|
|
|
|
// ── Type-parameter substitution ─────────────────────────────────────
|
|
|
|
/// Build the `$T → concrete TypeId` bindings for a generic call site.
|
|
/// Strategy 1: explicit type args (the param named `$T` IS a type
|
|
/// expression). Strategy 2: infer from value params that use `T`
|
|
/// (`a: $T`, `items: []$T`), picking the widest match.
|
|
pub fn buildTypeBindings(
|
|
self: GenericResolver,
|
|
fd: *const ast.FnDecl,
|
|
args_ast: []const *const Node,
|
|
) std.StringHashMap(TypeId) {
|
|
var bindings = std.StringHashMap(TypeId).init(self.l.alloc);
|
|
const types_passed_explicitly = args_ast.len == fd.params.len;
|
|
for (fd.type_params) |tp| {
|
|
var found = false;
|
|
// Strategy 1: explicit — the param whose name matches `tp.name` IS
|
|
// the `$T: Type` declaration; the arg at that position is a type expression.
|
|
if (types_passed_explicitly) {
|
|
for (fd.params, 0..) |param, pi| {
|
|
if (std.mem.eql(u8, param.name, tp.name)) {
|
|
if (pi < args_ast.len and type_bridge.isTypeShapedAstNode(args_ast[pi], &self.l.module.types)) {
|
|
const ty = self.l.resolveTypeArg(args_ast[pi]);
|
|
bindings.put(tp.name, ty) catch {};
|
|
found = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (found) continue;
|
|
// Strategy 2: infer from value params that USE the type param
|
|
// (e.g. a: $T, b: T, items: []$T). Pick widest type across matches.
|
|
var inferred_ty: ?TypeId = null;
|
|
var s2_arg_idx: usize = 0;
|
|
for (fd.params) |param| {
|
|
const is_type_decl = Lowering.isTypeParamDecl(¶m, fd.type_params);
|
|
defer if (!is_type_decl) {
|
|
s2_arg_idx += 1;
|
|
};
|
|
if (is_type_decl) {
|
|
if (types_passed_explicitly) s2_arg_idx += 1;
|
|
continue;
|
|
}
|
|
const matched = self.l.matchTypeParam(param.type_expr, tp.name);
|
|
if (matched) {
|
|
if (s2_arg_idx < args_ast.len) {
|
|
const arg_ty = self.l.inferExprType(args_ast[s2_arg_idx]);
|
|
const extracted = self.l.extractTypeParam(param.type_expr, arg_ty, tp.name);
|
|
if (extracted) |ety| {
|
|
if (inferred_ty) |prev| {
|
|
if (ety == .f64 and prev != .f64) {
|
|
inferred_ty = ety;
|
|
} else if (ety == .f32 and prev != .f64 and prev != .f32) {
|
|
inferred_ty = ety;
|
|
}
|
|
} else {
|
|
inferred_ty = ety;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (inferred_ty) |ty| {
|
|
bindings.put(tp.name, ty) catch {};
|
|
}
|
|
}
|
|
return bindings;
|
|
}
|
|
|
|
/// Infer the return type of a generic function call by resolving type bindings.
|
|
pub fn inferGenericReturnType(self: GenericResolver, fd: *const ast.FnDecl, c: *const ast.Call) TypeId {
|
|
if (fd.return_type == null) return .void;
|
|
|
|
// Build ALL type bindings from call args before resolving return type
|
|
var tmp_bindings = std.StringHashMap(TypeId).init(self.l.alloc);
|
|
defer tmp_bindings.deinit();
|
|
|
|
for (fd.type_params) |tp| {
|
|
// Strategy 1: direct type param decl ($T: Type) — param.name == tp.name.
|
|
// Only fires when the caller actually supplied a type expression at
|
|
// that position; otherwise fall through to value-based inference.
|
|
var found = false;
|
|
for (fd.params, 0..) |param, pi| {
|
|
if (std.mem.eql(u8, param.name, tp.name)) {
|
|
if (pi < c.args.len and type_bridge.isTypeShapedAstNode(c.args[pi], &self.l.module.types)) {
|
|
const ty = self.l.resolveTypeArg(c.args[pi]);
|
|
tmp_bindings.put(tp.name, ty) catch {};
|
|
found = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (found) continue;
|
|
|
|
// Strategy 2: inferred from usage (a: $T, b: T) — check ALL matching params, pick widest
|
|
var inferred_ty: ?TypeId = null;
|
|
for (fd.params, 0..) |param, pi| {
|
|
if (param.type_expr.data == .type_expr) {
|
|
const te = param.type_expr.data.type_expr;
|
|
if (std.mem.eql(u8, te.name, tp.name)) {
|
|
if (pi < c.args.len) {
|
|
const arg_ty = self.l.inferExprType(c.args[pi]);
|
|
if (inferred_ty) |prev| {
|
|
if (arg_ty == .f64 and prev != .f64) {
|
|
inferred_ty = arg_ty;
|
|
} else if (arg_ty == .f32 and prev != .f64 and prev != .f32) {
|
|
inferred_ty = arg_ty;
|
|
}
|
|
} else {
|
|
inferred_ty = arg_ty;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (inferred_ty) |ty| {
|
|
tmp_bindings.put(tp.name, ty) catch {};
|
|
}
|
|
}
|
|
|
|
// Resolve return type with whatever bindings we built. Even an
|
|
// empty `tmp_bindings` is a valid input — non-generic literal
|
|
// return types (e.g. `walk(..$args) -> string`) still need to
|
|
// resolve through `resolveTypeWithBindings`, not fall through
|
|
// to the historical `.s64` default. The default silently
|
|
// misclassified pack-fn calls whose return type was a fixed
|
|
// literal — every consumer (e.g. print's pack-shape mangling)
|
|
// inferred `s64` and routed the value through the wrong Any
|
|
// tag.
|
|
var scope = TypeBindingScope.enter(self.l, tmp_bindings);
|
|
defer scope.exit();
|
|
return self.l.resolveTypeWithBindings(fd.return_type.?);
|
|
}
|
|
};
|
|
|
|
/// Scoped override of `Lowering.type_bindings`: install a binding set for the
|
|
/// duration of a substitution, restoring the prior set on `exit`. Replaces the
|
|
/// manual save/restore the generic-return resolution used (PLAN-ARCH A4.1
|
|
/// "scoped substitution envs").
|
|
const TypeBindingScope = struct {
|
|
l: *Lowering,
|
|
saved: ?std.StringHashMap(TypeId),
|
|
|
|
fn enter(l: *Lowering, bindings: std.StringHashMap(TypeId)) TypeBindingScope {
|
|
const saved = l.type_bindings;
|
|
l.type_bindings = bindings;
|
|
return .{ .l = l, .saved = saved };
|
|
}
|
|
|
|
fn exit(self: *TypeBindingScope) void {
|
|
self.l.type_bindings = self.saved;
|
|
}
|
|
};
|