Verbatim relocation of the 19-method coercion cluster (lowerXX, user conversions, protocol erasure, default-value construction, zero values, coerceToType implicit/explicit ladder, C-variadic promotion, call-arg coercion) plus the nested single-home CoerceMode enum into src/ir/lower/coerce.zig. 19 aliases on Lowering keep all call sites unchanged. Method pub-flip: prependCtxIfNeeded. ParamImplEntry stays a Lowering nested type (field type of param_impl_map) and is reached via an alias const. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn.
773 lines
37 KiB
Zig
773 lines
37 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const ast = @import("../../ast.zig");
|
|
const Node = ast.Node;
|
|
const types = @import("../types.zig");
|
|
const inst_mod = @import("../inst.zig");
|
|
const mod_mod = @import("../module.zig");
|
|
const type_bridge = @import("../type_bridge.zig");
|
|
const unescape = @import("../../unescape.zig");
|
|
const parser_mod = @import("../../parser.zig");
|
|
const interp_mod = @import("../interp.zig");
|
|
const errors = @import("../../errors.zig");
|
|
const jni_descriptor = @import("../jni_descriptor.zig");
|
|
const program_index_mod = @import("../program_index.zig");
|
|
const resolver_mod = @import("../resolver.zig");
|
|
const imports_mod = @import("../../imports.zig");
|
|
const ProgramIndex = program_index_mod.ProgramIndex;
|
|
const GlobalInfo = program_index_mod.GlobalInfo;
|
|
const StructTemplate = program_index_mod.StructTemplate;
|
|
const TemplateParam = program_index_mod.TemplateParam;
|
|
const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo;
|
|
const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo;
|
|
const ModuleConstInfo = program_index_mod.ModuleConstInfo;
|
|
const TypeResolver = @import("../type_resolver.zig").TypeResolver;
|
|
const ResolveEnv = @import("../type_resolver.zig").ResolveEnv;
|
|
const PackResolver = @import("../packs.zig").PackResolver;
|
|
const ExprTyper = @import("../expr_typer.zig").ExprTyper;
|
|
const CallResolver = @import("../calls.zig").CallResolver;
|
|
const GenericResolver = @import("../generics.zig").GenericResolver;
|
|
const ProtocolResolver = @import("../protocols.zig").ProtocolResolver;
|
|
const CoercionResolver = @import("../conversions.zig").CoercionResolver;
|
|
const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis;
|
|
const ErrorFlow = @import("../error_flow.zig").ErrorFlow;
|
|
const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering;
|
|
const semantic_diagnostics = @import("../semantic_diagnostics.zig");
|
|
|
|
const TypeId = types.TypeId;
|
|
const StringId = types.StringId;
|
|
const Ref = inst_mod.Ref;
|
|
const BlockId = inst_mod.BlockId;
|
|
const FuncId = inst_mod.FuncId;
|
|
const Function = inst_mod.Function;
|
|
const Module = mod_mod.Module;
|
|
const Builder = mod_mod.Builder;
|
|
|
|
|
|
const lower = @import("../lower.zig");
|
|
const Lowering = lower.Lowering;
|
|
const Scope = lower.Scope;
|
|
const ParamImplEntry = Lowering.ParamImplEntry;
|
|
|
|
/// Lower the `xx` operator (type coercion).
|
|
/// Uses self.target_type for context when available. Handles:
|
|
/// - Any → concrete type: unbox_any
|
|
/// - int → int: widen/narrow
|
|
/// - int ↔ float: int_to_float/float_to_int
|
|
pub fn lowerXX(self: *Lowering, operand: Ref, operand_node: *const Node) Ref {
|
|
// Use the operand's *actual* lowered Ref type rather than reaching
|
|
// back through inferExprType — the latter doesn't cover every
|
|
// expression shape (notably lambdas), and a wrong src_ty here can
|
|
// route the cast through coerceToType (e.g. a bogus s64→ptr bitcast)
|
|
// and silently skip the user-space Into fallback.
|
|
const src_ty = self.builder.getRefType(operand);
|
|
const target_explicit = self.target_type != null;
|
|
const dst_ty = self.target_type orelse .unresolved;
|
|
|
|
// PLANNING: the `xx`-head decision (conversions.zig). `.coerce` falls
|
|
// through to the built-in ladder + the user-`Into` fallback below.
|
|
switch (self.coercionResolver().classifyXX(src_ty, dst_ty)) {
|
|
// Any → concrete type: unbox.
|
|
.unbox_any => {
|
|
// When inside a float match arm covering both f32 and f64,
|
|
// and target is f64, we need a mini-dispatch to unbox correctly.
|
|
// f32 values are stored as zext(bitcast(f32→i32), i64) in Any,
|
|
// so bitcasting i64→f64 directly gives wrong results for f32.
|
|
if (dst_ty == .f64) {
|
|
if (self.current_match_tags) |tags| {
|
|
var has_f32 = false;
|
|
var has_f64 = false;
|
|
for (tags) |t| {
|
|
const tid = TypeId.fromIndex(@intCast(t));
|
|
if (tid == .f32) has_f32 = true;
|
|
if (tid == .f64) has_f64 = true;
|
|
}
|
|
if (has_f32 and has_f64) {
|
|
return self.lowerAnyToF64Dispatch(operand);
|
|
}
|
|
if (has_f32 and !has_f64) {
|
|
// Only f32 values: unbox as f32, then widen
|
|
const f32_val = self.builder.emit(.{ .unbox_any = .{
|
|
.operand = operand,
|
|
} }, .f32);
|
|
return self.builder.emit(.{ .widen = .{ .operand = f32_val, .from = .f32, .to = .f64 } }, .f64);
|
|
}
|
|
}
|
|
}
|
|
return self.builder.emit(.{ .unbox_any = .{
|
|
.operand = operand,
|
|
} }, dst_ty);
|
|
},
|
|
// Same type: no-op.
|
|
.no_op => return operand,
|
|
// Concrete → Protocol: build protocol value.
|
|
.erase_protocol => return self.buildProtocolErasure(operand, operand_node, src_ty, dst_ty),
|
|
// Protocol → pointer: recover the typed ctx pointer (field 0).
|
|
// The protocol value is `{ ctx, fn1, fn2, ... }` (inline) or
|
|
// `{ ctx, vtable_ptr }` — either way, ctx lives at field 0.
|
|
.protocol_to_pointer => {
|
|
const void_ptr_ty = self.module.types.ptrTo(.void);
|
|
const ctx_ref = self.builder.emit(.{ .struct_get = .{ .base = operand, .field_index = 0 } }, void_ptr_ty);
|
|
if (dst_ty == void_ptr_ty) return ctx_ref;
|
|
return self.builder.emit(.{ .bitcast = .{ .operand = ctx_ref, .from = void_ptr_ty, .to = dst_ty } }, dst_ty);
|
|
},
|
|
.coerce => {},
|
|
}
|
|
|
|
const result = self.coerceExplicit(operand, src_ty, dst_ty);
|
|
|
|
// User-space fallback via `impl Into(Target) for Source`. Only fires
|
|
// when the target was explicitly named (not the .s64 default), src and
|
|
// dst differ, and the built-in ladder made no progress. Built-ins
|
|
// always win.
|
|
if (target_explicit and src_ty != dst_ty and result == operand) {
|
|
if (self.tryUserConversion(operand, operand_node, src_ty, dst_ty)) |converted| {
|
|
return converted;
|
|
}
|
|
// Pointer-target fallback: `xx <expr>` whose surrounding context
|
|
// expects `*T` (a fn arg slot, a var typed as a pointer-to-aggregate)
|
|
// can be satisfied by `impl Into(T) for src` plus an implicit
|
|
// alloca+store on the result. Lets users write
|
|
// `fn(xx () => { ... })` instead of materialising a named Block local
|
|
// just to take its address.
|
|
if (!dst_ty.isBuiltin()) {
|
|
const dst_info = self.module.types.get(dst_ty);
|
|
if (dst_info == .pointer) {
|
|
const pointee = dst_info.pointer.pointee;
|
|
if (pointee != src_ty) {
|
|
if (self.tryUserConversion(operand, operand_node, src_ty, pointee)) |converted| {
|
|
const slot = self.builder.alloca(pointee);
|
|
self.builder.store(slot, converted);
|
|
return slot;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Detect the `xx closure : Block` cast pattern so `tryUserConversion`
|
|
/// can emit a focused diagnostic when no `Into(Block) for Closure(...)`
|
|
/// impl is reachable. Replaces what was briefly a compiler-synthesised
|
|
/// trampoline path with a "declare an impl" requirement — the stdlib
|
|
/// covers common signatures (see modules/std/objc_block.sx), users
|
|
/// add their own for unusual ones.
|
|
pub fn isClosureToBlockCast(self: *Lowering, src_ty: TypeId, dst_ty: TypeId) bool {
|
|
if (src_ty.isBuiltin()) return false;
|
|
const src_info = self.module.types.get(src_ty);
|
|
if (src_info != .closure) return false;
|
|
if (dst_ty.isBuiltin()) return false;
|
|
const dst_info = self.module.types.get(dst_ty);
|
|
if (dst_info != .@"struct") return false;
|
|
const block_name = self.module.types.internString("Block");
|
|
return dst_info.@"struct".name == block_name;
|
|
}
|
|
|
|
/// Pack-variadic impl matching. Walks `param_impl_pack_map[pack_key]`
|
|
/// and returns a call ref when a single pack impl matches `src_ty`'s
|
|
/// shape (concrete src closure / fn with the same fixed prefix as
|
|
/// the impl's source pack closure). Binds the pack-var to the source's
|
|
/// tail param types and the return-var (when generic) to the source's
|
|
/// return type, then monomorphises the convert method.
|
|
/// Returns null if no pack impls registered for this (proto, dst) or
|
|
/// none of them match `src_ty`'s shape.
|
|
pub fn tryPackImplMatch(
|
|
self: *Lowering,
|
|
operand: Ref,
|
|
operand_node: *const Node,
|
|
src_ty: TypeId,
|
|
dst_ty: TypeId,
|
|
proto_name: []const u8,
|
|
pack_key: []const u8,
|
|
guard_key: u64,
|
|
) ?Ref {
|
|
_ = operand_node;
|
|
// PLANNING: select the matching pack impl + its `convert` (registry).
|
|
const match = self.protocolResolver().matchPackImpl(src_ty, pack_key) orelse return null;
|
|
const entry = match.entry;
|
|
const fd = match.convert_fd;
|
|
const src_params = match.src_params;
|
|
const src_ret = match.src_ret;
|
|
const table = &self.module.types;
|
|
// EMISSION: bind the pack tail + ret-var, monomorphise, call (Lowering).
|
|
|
|
// Build bindings. Target → dst_ty (already in the protocol's type
|
|
// params), pack-var → src tail TypeIds, ret-var (when generic) →
|
|
// src ret.
|
|
const ent_pack_start = table.get(entry.source_pack_ty).closure.pack_start.?;
|
|
const tail = src_params[ent_pack_start..];
|
|
const tail_owned = self.alloc.dupe(TypeId, tail) catch return null;
|
|
|
|
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
|
defer bindings.deinit();
|
|
const pd = self.program_index.protocol_ast_map.get(proto_name) orelse return null;
|
|
bindings.put(pd.type_params[0].name, dst_ty) catch return null;
|
|
if (entry.ret_var_name) |rv| bindings.put(rv, src_ret) catch return null;
|
|
|
|
var pack_bindings = std.StringHashMap([]const TypeId).init(self.alloc);
|
|
defer pack_bindings.deinit();
|
|
pack_bindings.put(entry.pack_var_name, tail_owned) catch return null;
|
|
|
|
// Mangled name keyed on the CONCRETE source so distinct shapes
|
|
// monomorphise separately. Same scheme as the concrete path:
|
|
// "<src>.convert__<dst>".
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
}) catch return null;
|
|
|
|
self.xx_reentrancy.put(guard_key, {}) catch {};
|
|
defer _ = self.xx_reentrancy.remove(guard_key);
|
|
|
|
if (!self.lowered_functions.contains(mangled)) {
|
|
const saved_pack = self.pack_bindings;
|
|
self.pack_bindings = pack_bindings;
|
|
defer self.pack_bindings = saved_pack;
|
|
self.monomorphizeFunction(fd, mangled, &bindings);
|
|
}
|
|
|
|
const fid = self.resolveFuncByName(mangled) orelse return null;
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
var single = [_]Ref{operand};
|
|
const final_args = self.prependCtxIfNeeded(func, single[0..]);
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
|
|
/// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise
|
|
/// the impl's `convert` method and emit a direct call. Returns null when
|
|
/// no impl matches (caller falls back to the built-in result, which is
|
|
/// the unchanged operand — Phase 3 emits no diagnostic for v0).
|
|
pub fn tryUserConversion(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) ?Ref {
|
|
// Reentrancy guard — pack (src, dst) into a u64.
|
|
const guard_key: u64 = (@as(u64, src_ty.index()) << 32) | @as(u64, dst_ty.index());
|
|
if (self.xx_reentrancy.contains(guard_key)) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, operand_node.span, "recursive xx conversion from '{s}' to '{s}'", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
});
|
|
}
|
|
return operand;
|
|
}
|
|
|
|
// Build lookup key: "Into\x00<dst_mangled>\x00<src_mangled>".
|
|
// Hardcoded to the "Into" protocol for v1. Generalising to other
|
|
// parameterised protocols would walk protocol_decl_map looking for
|
|
// protocols that take a single type-param and have a `convert` method.
|
|
const proto_name = "Into";
|
|
const pd = self.program_index.protocol_ast_map.get(proto_name) orelse return null;
|
|
if (pd.type_params.len != 1) return null;
|
|
|
|
var key_buf = std.ArrayList(u8).empty;
|
|
key_buf.appendSlice(self.alloc, proto_name) catch return null;
|
|
key_buf.append(self.alloc, 0) catch return null;
|
|
key_buf.appendSlice(self.alloc, self.mangleTypeName(dst_ty)) catch return null;
|
|
key_buf.append(self.alloc, 0) catch return null;
|
|
key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return null;
|
|
const key = key_buf.items;
|
|
|
|
// Pack-only key (proto + dst) — used if the concrete lookup misses.
|
|
// Same prefix as the concrete key, minus the `\x00<src_mangled>` tail.
|
|
const dst_mangled_len = self.mangleTypeName(dst_ty).len;
|
|
const pack_key = key_buf.items[0 .. proto_name.len + 1 + dst_mangled_len];
|
|
|
|
const entries_opt = self.param_impl_map.get(key);
|
|
const has_concrete = entries_opt != null and entries_opt.?.items.len > 0;
|
|
if (!has_concrete) {
|
|
// Concrete miss — try the pack map before emitting a diagnostic.
|
|
if (self.tryPackImplMatch(operand, operand_node, src_ty, dst_ty, proto_name, pack_key, guard_key)) |result| {
|
|
return result;
|
|
}
|
|
if (self.isClosureToBlockCast(src_ty, dst_ty)) {
|
|
if (self.diagnostics) |diags| {
|
|
const saved = diags.current_source_file;
|
|
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
|
|
defer diags.current_source_file = saved;
|
|
diags.addFmt(.err, operand_node.span, "no `Into(Block) for {s}` impl — add a per-signature `__block_invoke_<sig>` trampoline + Into impl alongside the existing ones in modules/std/objc_block.sx, or declare it in your own code", .{self.mangleTypeName(src_ty)});
|
|
}
|
|
return operand;
|
|
}
|
|
return null;
|
|
}
|
|
const entries = entries_opt.?;
|
|
|
|
// Filter by import visibility: only impls in modules that the current
|
|
// file transitively imports (or the current file itself) are reachable.
|
|
// Falls open when import_graph isn't wired (e.g. comptime callers).
|
|
var visible_impls = std.ArrayList(ParamImplEntry).empty;
|
|
defer visible_impls.deinit(self.alloc);
|
|
self.protocolResolver().findVisibleImpls(entries.items, &visible_impls);
|
|
|
|
if (visible_impls.items.len == 0) {
|
|
if (self.diagnostics) |diags| {
|
|
const saved = diags.current_source_file;
|
|
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
|
|
defer diags.current_source_file = saved;
|
|
diags.addFmt(.err, operand_node.span, "no visible xx conversion from '{s}' to '{s}' — impl exists in another module but is not imported", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
});
|
|
}
|
|
return operand;
|
|
}
|
|
if (visible_impls.items.len > 1) {
|
|
if (self.diagnostics) |diags| {
|
|
const saved = diags.current_source_file;
|
|
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
|
|
defer diags.current_source_file = saved;
|
|
diags.addFmt(.err, operand_node.span, "duplicate xx conversion from '{s}' to '{s}': impls in {s} and {s}", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
visible_impls.items[0].defining_module, visible_impls.items[1].defining_module,
|
|
});
|
|
}
|
|
return operand;
|
|
}
|
|
const entry = visible_impls.items[0];
|
|
|
|
// Find the `convert` method on this impl.
|
|
var convert_fd: ?*const ast.FnDecl = null;
|
|
for (entry.methods) |m| {
|
|
if (std.mem.eql(u8, m.name, "convert")) {
|
|
convert_fd = m;
|
|
break;
|
|
}
|
|
}
|
|
const fd = convert_fd orelse return null;
|
|
|
|
// Bind Target → dst_ty.
|
|
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
|
defer bindings.deinit();
|
|
bindings.put(pd.type_params[0].name, dst_ty) catch return null;
|
|
|
|
// Mangled name: "<src>.convert__<dst>".
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
}) catch return null;
|
|
|
|
self.xx_reentrancy.put(guard_key, {}) catch {};
|
|
defer _ = self.xx_reentrancy.remove(guard_key);
|
|
|
|
if (!self.lowered_functions.contains(mangled)) {
|
|
self.monomorphizeFunction(fd, mangled, &bindings);
|
|
}
|
|
|
|
const fid = self.resolveFuncByName(mangled) orelse return null;
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
var single = [_]Ref{operand};
|
|
const final_args = self.prependCtxIfNeeded(func, single[0..]);
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
|
|
/// True for expression shapes that name an addressable storage location
|
|
/// (variables, fields, array elements, dereferenced pointers). Used by
|
|
/// `xx <struct-typed expr>` to decide between borrow (lvalue → take the
|
|
/// address) and heap-copy (rvalue → allocate a fresh copy).
|
|
pub fn isLvalueExpr(self: *Lowering, node: *const Node) bool {
|
|
_ = self;
|
|
return switch (node.data) {
|
|
.identifier, .field_access, .index_expr, .deref_expr => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Build a protocol value from a concrete value via xx conversion.
|
|
/// Coerce `val` (type `src`) to `dst`: if `dst` is a protocol, `xx`-erase
|
|
/// the concrete value into it; otherwise fall back to numeric/struct
|
|
/// coercion. Used to materialize a pack into a protocol-typed tuple field.
|
|
pub fn coerceOrErase(self: *Lowering, val: Ref, src: TypeId, dst: TypeId, node: *const Node) Ref {
|
|
if (src == dst) return val;
|
|
if (!dst.isBuiltin()) {
|
|
const di = self.module.types.get(dst);
|
|
if (di == .@"struct" and di.@"struct".is_protocol) {
|
|
return self.buildProtocolErasure(val, node, src, dst);
|
|
}
|
|
}
|
|
return self.coerceToType(val, src, dst);
|
|
}
|
|
|
|
pub fn buildProtocolErasure(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) Ref {
|
|
const dst_info = self.module.types.get(dst_ty);
|
|
if (dst_info != .@"struct") return operand;
|
|
const proto_name = self.module.types.getString(dst_info.@"struct".name);
|
|
|
|
// Determine concrete type name and type — resolve through pointer if needed
|
|
var concrete_ptr = operand;
|
|
var concrete_type_name: ?[]const u8 = null;
|
|
var concrete_ty: TypeId = src_ty;
|
|
var heap_copy = false;
|
|
|
|
if (!src_ty.isBuiltin()) {
|
|
const src_info = self.module.types.get(src_ty);
|
|
if (src_info == .pointer) {
|
|
// xx @acc — operand is already a pointer (user manages lifetime)
|
|
const pointee = src_info.pointer.pointee;
|
|
concrete_type_name = self.resolveConcreteTypeName(pointee);
|
|
concrete_ty = pointee;
|
|
heap_copy = false;
|
|
} else if (src_info == .@"struct") {
|
|
// Struct-typed operand. Split on lvalue-ness:
|
|
// - lvalue (identifier, field, index, deref): borrow the
|
|
// storage the operand already names. No heap copy; the
|
|
// protocol value's ctx points at the caller's slot, and
|
|
// mutations through the protocol are visible to the
|
|
// original. Lifetime is the caller's responsibility.
|
|
// - rvalue (struct literal, call result, etc.): heap-copy
|
|
// into a fresh allocation so the protocol value is
|
|
// self-contained and outlives this expression.
|
|
concrete_type_name = self.module.types.getString(src_info.@"struct".name);
|
|
concrete_ty = src_ty;
|
|
if (self.isLvalueExpr(operand_node)) {
|
|
concrete_ptr = self.lowerExprAsPtr(operand_node);
|
|
heap_copy = false;
|
|
} else {
|
|
heap_copy = true;
|
|
const slot = self.builder.alloca(src_ty);
|
|
self.builder.store(slot, operand);
|
|
concrete_ptr = slot;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also try from the operand node for struct literals: xx Accumulator.{ total = 0 }
|
|
if (concrete_type_name == null) {
|
|
concrete_type_name = self.inferConcreteTypeName(operand_node);
|
|
if (concrete_type_name != null) heap_copy = true;
|
|
}
|
|
|
|
if (concrete_type_name) |ctn| {
|
|
return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy);
|
|
}
|
|
return operand;
|
|
}
|
|
|
|
/// Try to infer the concrete type name from an AST node (for struct literals etc.)
|
|
pub fn inferConcreteTypeName(self: *Lowering, node: *const Node) ?[]const u8 {
|
|
return switch (node.data) {
|
|
.struct_literal => |sl| if (sl.struct_name) |n| n else null,
|
|
.unary_op => |uop| if (uop.op == .address_of) self.inferConcreteTypeName(uop.operand) else null,
|
|
.identifier => |id| blk: {
|
|
// Check if identifier's type resolves to a struct
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
if (!binding.ty.isBuiltin()) {
|
|
const bi = self.module.types.get(binding.ty);
|
|
if (bi == .@"struct") break :blk self.module.types.getString(bi.@"struct".name);
|
|
if (bi == .pointer) {
|
|
const pointee = bi.pointer.pointee;
|
|
if (!pointee.isBuiltin()) {
|
|
const pi = self.module.types.get(pointee);
|
|
if (pi == .@"struct") break :blk self.module.types.getString(pi.@"struct".name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break :blk null;
|
|
},
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// Generate a mini-dispatch for unboxing Any to f64 when the value might be f32 or f64.
|
|
/// Uses alloca-based merge: create result slot, branch, store in each arm, load after merge.
|
|
pub fn lowerAnyToF64Dispatch(self: *Lowering, any_val: Ref) Ref {
|
|
// Create result alloca BEFORE the branch
|
|
const result_slot = self.builder.alloca(.f64);
|
|
|
|
// Extract type tag from Any
|
|
const tag = self.builder.structGet(any_val, 0, .s64);
|
|
|
|
const f32_bb = self.freshBlock("f32.unbox");
|
|
const f64_bb = self.freshBlock("f64.unbox");
|
|
const merge_bb = self.freshBlock("float.merge");
|
|
|
|
// Branch: tag == f32_tag ? f32_bb : f64_bb
|
|
const f32_tag = self.builder.constInt(TypeId.f32.index(), .s64);
|
|
const cond = self.builder.emit(.{ .cmp_eq = .{ .lhs = tag, .rhs = f32_tag } }, .bool);
|
|
self.builder.condBr(cond, f32_bb, &.{}, f64_bb, &.{});
|
|
|
|
// f32 block: unbox as f32, fpext to f64, store
|
|
self.builder.switchToBlock(f32_bb);
|
|
const f32_val = self.builder.emit(.{ .unbox_any = .{
|
|
.operand = any_val,
|
|
} }, .f32);
|
|
const f64_from_f32 = self.builder.emit(.{ .widen = .{ .operand = f32_val, .from = .f32, .to = .f64 } }, .f64);
|
|
self.builder.store(result_slot, f64_from_f32);
|
|
self.builder.br(merge_bb, &.{});
|
|
|
|
// f64 block: unbox as f64 directly, store
|
|
self.builder.switchToBlock(f64_bb);
|
|
const f64_val = self.builder.emit(.{ .unbox_any = .{
|
|
.operand = any_val,
|
|
} }, .f64);
|
|
self.builder.store(result_slot, f64_val);
|
|
self.builder.br(merge_bb, &.{});
|
|
|
|
// Merge block: load result
|
|
self.builder.switchToBlock(merge_bb);
|
|
return self.builder.load(result_slot, .f64);
|
|
}
|
|
|
|
/// Produce a default value for a type, applying struct field defaults.
|
|
/// For structs with defaults (e.g., `b: s32 = 99`), creates a struct_literal with defaults applied.
|
|
/// For other types, returns a zero value.
|
|
pub fn buildDefaultValue(self: *Lowering, ty: TypeId) Ref {
|
|
if (ty.isBuiltin()) return self.builder.constInt(0, ty);
|
|
const info = self.module.types.get(ty);
|
|
if (info != .@"struct" and info != .tuple) return self.zeroValue(ty);
|
|
// For tuples, build a zero-initialized tuple
|
|
if (info == .tuple) {
|
|
var field_vals = std.ArrayList(Ref).empty;
|
|
defer field_vals.deinit(self.alloc);
|
|
for (info.tuple.fields) |f| {
|
|
field_vals.append(self.alloc, self.zeroValue(f)) catch unreachable;
|
|
}
|
|
return self.builder.emit(.{
|
|
.tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable },
|
|
}, ty);
|
|
}
|
|
// Check for struct defaults
|
|
const struct_name_str = self.module.types.getString(info.@"struct".name);
|
|
const field_defaults = self.struct_defaults_map.get(struct_name_str) orelse
|
|
return self.builder.constUndef(ty);
|
|
const fields = info.@"struct".fields;
|
|
var field_vals = std.ArrayList(Ref).empty;
|
|
defer field_vals.deinit(self.alloc);
|
|
for (fields, 0..) |f, i| {
|
|
if (i < field_defaults.len) {
|
|
if (field_defaults[i]) |default_expr| {
|
|
field_vals.append(self.alloc, self.lowerCoercedDefault(default_expr, f.ty)) catch unreachable;
|
|
} else {
|
|
field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable;
|
|
}
|
|
} else {
|
|
field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable;
|
|
}
|
|
}
|
|
return self.builder.emit(.{
|
|
.struct_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable },
|
|
}, ty);
|
|
}
|
|
|
|
/// Wrap ty in ?ty, but flatten: if ty is already ?U, return ?U (not ??U)
|
|
pub fn optionalOfFlattened(self: *Lowering, ty: TypeId) TypeId {
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
if (info == .optional) return ty;
|
|
}
|
|
return self.module.types.optionalOf(ty);
|
|
}
|
|
|
|
/// Produce a zero/default value for any type — constInt(0) for integers,
|
|
/// constNull for pointers, constUndef for structs/complex types.
|
|
pub fn zeroValue(self: *Lowering, ty: TypeId) Ref {
|
|
if (ty.isBuiltin()) return self.builder.constInt(0, ty);
|
|
const info = self.module.types.get(ty);
|
|
return switch (info) {
|
|
// Arbitrary-width integer types (u1, u2, s4, ...) interned as
|
|
// `.signed`/`.unsigned` variants — fall through `isBuiltin()`.
|
|
.signed, .unsigned => self.builder.constInt(0, ty),
|
|
.pointer, .tuple, .optional => self.builder.constNull(ty),
|
|
.@"struct", .array, .slice, .many_pointer => self.builder.constNull(ty),
|
|
else => self.builder.constUndef(ty),
|
|
};
|
|
}
|
|
|
|
/// Emit the unified non-integral float→int narrowing diagnostic (F0.11 /
|
|
/// issue 0095). ONE wording, ONE place: every site that rejects an implicit
|
|
/// narrowing of a non-integral compile-time float to an integer type calls
|
|
/// this, so the message + fix-it stay identical across the typed-binding
|
|
/// coerce arm, the field/param-default sites, the typed-const path, and the
|
|
/// global-initializer path.
|
|
pub fn diagNonIntegralNarrow(self: *Lowering, span: ast.Span, value: f64, dst_ty: TypeId) void {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ value, self.formatTypeName(dst_ty) });
|
|
}
|
|
|
|
/// Lower a struct field default `default_expr`, coerced to the field type
|
|
/// `field_ty`. A compile-time float default narrowing into an integer field
|
|
/// follows the unified rule via `foldComptimeFloatInit`; everything else
|
|
/// lowers under the field type as target and coerces at the IR level.
|
|
pub fn lowerCoercedDefault(self: *Lowering, default_expr: *const Node, field_ty: TypeId) Ref {
|
|
if (self.foldComptimeFloatInit(default_expr, field_ty)) |folded| return folded;
|
|
const saved_tt = self.target_type;
|
|
self.target_type = field_ty;
|
|
const raw = self.lowerExpr(default_expr);
|
|
self.target_type = saved_tt;
|
|
return self.coerceToType(raw, self.builder.getRefType(raw), field_ty);
|
|
}
|
|
|
|
/// How a float→int conversion is treated. An IMPLICIT coercion (a typed
|
|
/// binding initializer) folds an integral compile-time float to its int and
|
|
/// REJECTS a non-integral one; an EXPLICIT `xx` / `cast` always truncates.
|
|
const CoerceMode = enum { implicit, explicit };
|
|
|
|
/// Insert a conversion if src_ty and dst_ty differ.
|
|
/// Handles int widening/narrowing, float widening/narrowing, and int↔float.
|
|
/// IMPLICIT coercion — the typed-binding initializer path. A compile-time
|
|
/// float narrowing to an integer folds when integral, errors when not.
|
|
pub fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
|
|
return self.coerceMode(val, src_ty, dst_ty, .implicit);
|
|
}
|
|
|
|
/// EXPLICIT coercion — the `xx` / `cast(T)` escape hatch. A float→int here
|
|
/// always truncates, bypassing the integral-fold / non-integral-error rule.
|
|
pub fn coerceExplicit(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
|
|
return self.coerceMode(val, src_ty, dst_ty, .explicit);
|
|
}
|
|
|
|
pub fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mode: CoerceMode) Ref {
|
|
// PLANNING: classify the built-in coercion (conversions.zig).
|
|
// EMISSION: each arm below reproduces the original lowering.
|
|
switch (self.coercionResolver().classify(src_ty, dst_ty)) {
|
|
.no_op, .none => return val,
|
|
// Unbox Any → concrete type
|
|
.unbox_any => return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty),
|
|
// Box concrete → Any
|
|
.box_any => return self.builder.boxAny(val, src_ty),
|
|
// Closure VALUE → bare function-pointer slot: not soundly representable.
|
|
// A bare `(T) -> U` slot is called as `fn_ptr(ctx, args)` with NO env
|
|
// arg, but a closure's underlying fn takes an env slot — so passing a
|
|
// closure value's fn_ptr drops the env and shifts the args (UB for a
|
|
// matching ABI, a wrong-tuple read for ∅-widening, a segfault when the
|
|
// closure captures). Only a closure LITERAL can cross this boundary,
|
|
// via the static adapter `lowerLambda` emits (so a literal arrives here
|
|
// already typed `.function`). Reject the variable case loudly.
|
|
.closure_to_fn_reject => {
|
|
if (self.diagnostics) |d| {
|
|
const cs = self.builder.current_span;
|
|
d.addFmt(.err, ast.Span{ .start = cs.start, .end = cs.end }, "a closure value cannot be passed as a bare function-pointer `(...) -> ...` — its environment can't be carried across the bare ABI; pass the closure literal directly at the call site, or declare the parameter type as `Closure(...)`", .{});
|
|
}
|
|
return val;
|
|
},
|
|
// Tuple → Tuple element-wise coercion (e.g. a `(s64, s64)` literal
|
|
// flowing into a `(s32, s32)` slot — the multi-value failable success
|
|
// tuple). Same arity: extract each slot, coerce it, rebuild.
|
|
.tuple_elementwise => {
|
|
const si = self.module.types.get(src_ty);
|
|
const di = self.module.types.get(dst_ty);
|
|
var elems = std.ArrayList(Ref).empty;
|
|
defer elems.deinit(self.alloc);
|
|
for (si.tuple.fields, di.tuple.fields, 0..) |sf, df, i| {
|
|
const fv = self.builder.emit(.{ .tuple_get = .{ .base = val, .field_index = @intCast(i), .base_type = src_ty } }, sf);
|
|
elems.append(self.alloc, self.coerceMode(fv, sf, df, mode)) catch unreachable;
|
|
}
|
|
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty);
|
|
},
|
|
// Optional → Concrete unwrapping (flow-sensitive narrowing coercion)
|
|
.optional_unwrap => {
|
|
const child_ty = self.module.types.get(src_ty).optional.child;
|
|
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
|
return self.coerceMode(unwrapped, child_ty, dst_ty, mode);
|
|
},
|
|
// void → Optional: produce null (void is the type of null_literal)
|
|
.void_to_optional => return self.builder.constNull(dst_ty),
|
|
// Concrete → Optional wrapping (coerce to the inner type first)
|
|
.optional_wrap => {
|
|
const child_ty = self.module.types.get(dst_ty).optional.child;
|
|
const coerced = self.coerceMode(val, src_ty, child_ty, mode);
|
|
return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty);
|
|
},
|
|
// Concrete → Protocol (auto type erasure)
|
|
.erase_protocol => {
|
|
const proto_name = self.module.types.getString(self.module.types.get(dst_ty).@"struct".name);
|
|
const ctn = self.resolveConcreteTypeName(src_ty).?;
|
|
// If src is a pointer, use directly; otherwise alloca+store + heap-copy
|
|
var concrete_ptr = val;
|
|
var concrete_ty = src_ty;
|
|
var heap_copy = false;
|
|
if (!src_ty.isBuiltin()) {
|
|
const si = self.module.types.get(src_ty);
|
|
if (si == .pointer) {
|
|
concrete_ty = si.pointer.pointee;
|
|
heap_copy = false;
|
|
} else {
|
|
const slot = self.builder.alloca(src_ty);
|
|
self.builder.store(slot, val);
|
|
concrete_ptr = slot;
|
|
heap_copy = true;
|
|
}
|
|
}
|
|
return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy);
|
|
},
|
|
.int_to_float => return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
|
.float_to_int => {
|
|
// Implicit float→int narrowing follows the unified rule (the
|
|
// same `floatToIntExact` the array-dim / `$K: Count` paths use):
|
|
// a compile-time INTEGRAL float folds to its int, a NON-integral
|
|
// one is a compile error. Explicit `xx` / `cast` (mode
|
|
// `.explicit`) skips this and truncates. A runtime float has no
|
|
// compile-time value to fold — it truncates as before.
|
|
if (mode == .implicit) {
|
|
if (self.builder.constFloatInfo(val)) |info| {
|
|
if (program_index_mod.floatToIntExact(info.value)) |iv| {
|
|
return self.builder.constInt(iv, dst_ty);
|
|
}
|
|
// Non-integral: diagnose, then fall through to the
|
|
// truncating op below so lowering finishes and
|
|
// `hasErrors()` aborts the build.
|
|
self.diagNonIntegralNarrow(.{ .start = info.span.start, .end = info.span.end }, info.value, dst_ty);
|
|
}
|
|
}
|
|
return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
|
|
},
|
|
// Ptr ↔ Int — explicit `xx ptr` to/from an integer-typed slot.
|
|
// Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches
|
|
// to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level
|
|
// since LLVMBuildBitCast itself doesn't accept ptr↔int.
|
|
.ptr_int_bitcast => return self.builder.emit(.{ .bitcast = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
|
.narrow => return self.builder.emit(.{ .narrow = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
|
.widen => return self.builder.emit(.{ .widen = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
|
.array_to_slice => return self.builder.emit(.{ .array_to_slice = .{ .operand = val } }, dst_ty),
|
|
}
|
|
}
|
|
|
|
/// Apply C default argument promotion to variadic-tail args. These rules
|
|
/// (bool/s8/s16/u8/u16 → s32, f32 → f64) match the C calling convention's
|
|
/// implicit promotions when an argument is passed through `...`.
|
|
pub fn promoteCVariadicArgs(self: *Lowering, args: []Ref, fixed_count: usize) void {
|
|
if (args.len <= fixed_count) return;
|
|
for (args[fixed_count..]) |*arg| {
|
|
const src_ty = self.builder.getRefType(arg.*);
|
|
const promoted: TypeId = switch (src_ty) {
|
|
.bool, .s8, .s16, .u8, .u16 => .s32,
|
|
.f32 => .f64,
|
|
else => continue,
|
|
};
|
|
arg.* = self.coerceToType(arg.*, src_ty, promoted);
|
|
}
|
|
}
|
|
|
|
/// Coerce call arguments in-place to match function parameter types.
|
|
pub fn coerceCallArgs(self: *Lowering, args: []Ref, params: []const Function.Param) void {
|
|
for (0..@min(args.len, params.len)) |i| {
|
|
const src_ty = self.builder.getRefType(args[i]);
|
|
const dst_ty = params[i].ty;
|
|
if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
|
|
const src_info = self.module.types.get(src_ty);
|
|
const dst_info = self.module.types.get(dst_ty);
|
|
// Array → many_pointer decay: alloca the array, GEP to first element
|
|
if (src_info == .array and dst_info == .many_pointer) {
|
|
const slot = self.builder.alloca(src_ty);
|
|
self.builder.store(slot, args[i]);
|
|
const zero = self.builder.constInt(0, .s64);
|
|
args[i] = self.builder.emit(.{ .index_gep = .{ .lhs = slot, .rhs = zero } }, dst_ty);
|
|
continue;
|
|
}
|
|
// Implicit address-of: passing T value where *T is expected → alloca + store
|
|
// Only when the pointee type matches the source type.
|
|
if (dst_info == .pointer and src_info != .pointer and dst_info.pointer.pointee == src_ty) {
|
|
const slot = self.builder.alloca(src_ty);
|
|
self.builder.store(slot, args[i]);
|
|
args[i] = slot;
|
|
continue;
|
|
}
|
|
}
|
|
args[i] = self.coerceToType(args[i], src_ty, dst_ty);
|
|
}
|
|
}
|