refactor(B5.2): move coercions to lower/coerce.zig
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.
This commit is contained in:
746
src/ir/lower.zig
746
src/ir/lower.zig
@@ -40,6 +40,7 @@ const lower_control_flow = @import("lower/control_flow.zig");
|
||||
const lower_decl = @import("lower/decl.zig");
|
||||
const lower_nominal = @import("lower/nominal.zig");
|
||||
const lower_protocol = @import("lower/protocol.zig");
|
||||
const lower_coerce = @import("lower/coerce.zig");
|
||||
|
||||
const TypeId = types.TypeId;
|
||||
const StringId = types.StringId;
|
||||
@@ -4850,7 +4851,7 @@ pub const Lowering = struct {
|
||||
/// themselves with __sx_ctx already prepended (protocol thunks, FFI
|
||||
/// wrappers in Step 4) should NOT call this — they already manage
|
||||
/// slot 0.
|
||||
fn prependCtxIfNeeded(self: *Lowering, callee: *const Function, args: []Ref) []Ref {
|
||||
pub fn prependCtxIfNeeded(self: *Lowering, callee: *const Function, args: []Ref) []Ref {
|
||||
if (!callee.has_implicit_ctx) return args;
|
||||
const new_args = self.alloc.alloc(Ref, args.len + 1) catch return args;
|
||||
new_args[0] = self.current_ctx_ref;
|
||||
@@ -9948,534 +9949,6 @@ pub const Lowering = struct {
|
||||
return .{ .l = self };
|
||||
}
|
||||
|
||||
/// 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
|
||||
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.
|
||||
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.
|
||||
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).
|
||||
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).
|
||||
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.
|
||||
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.)
|
||||
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.
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if a name refers to a known type (primitive or registered struct/enum/union).
|
||||
/// Used to distinguish type-as-value (silent placeholder) from genuinely unresolved names.
|
||||
pub fn isKnownTypeName(self: *Lowering, name: []const u8) bool {
|
||||
@@ -10551,155 +10024,6 @@ pub const Lowering = struct {
|
||||
return self.emitPlaceholder(field);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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.
|
||||
fn coerceExplicit(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
|
||||
return self.coerceMode(val, src_ty, dst_ty, .explicit);
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the alloca Ref for an expression, if it's a simple variable reference.
|
||||
/// Returns null for complex expressions (field access, function calls, etc.)
|
||||
pub fn getExprAlloca(self: *Lowering, node: *const Node) ?Ref {
|
||||
@@ -10891,51 +10215,6 @@ pub const Lowering = struct {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 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 `...`.
|
||||
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.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a C-ABI exported function for every bodied method on a
|
||||
/// `#jni_main #jni_class("...")` declaration. The symbol name follows
|
||||
/// JNI's name-mangling convention so Android's JNI runtime can resolve
|
||||
@@ -12579,6 +11858,27 @@ pub const Lowering = struct {
|
||||
pub const emitProtocolDispatch = lower_protocol.emitProtocolDispatch;
|
||||
pub const resolveConcreteTypeName = lower_protocol.resolveConcreteTypeName;
|
||||
pub const computeHasImpl = lower_protocol.computeHasImpl;
|
||||
|
||||
// --- moved to lower/coerce.zig (lower_coerce) ---
|
||||
pub const lowerXX = lower_coerce.lowerXX;
|
||||
pub const isClosureToBlockCast = lower_coerce.isClosureToBlockCast;
|
||||
pub const tryPackImplMatch = lower_coerce.tryPackImplMatch;
|
||||
pub const tryUserConversion = lower_coerce.tryUserConversion;
|
||||
pub const isLvalueExpr = lower_coerce.isLvalueExpr;
|
||||
pub const coerceOrErase = lower_coerce.coerceOrErase;
|
||||
pub const buildProtocolErasure = lower_coerce.buildProtocolErasure;
|
||||
pub const inferConcreteTypeName = lower_coerce.inferConcreteTypeName;
|
||||
pub const lowerAnyToF64Dispatch = lower_coerce.lowerAnyToF64Dispatch;
|
||||
pub const buildDefaultValue = lower_coerce.buildDefaultValue;
|
||||
pub const optionalOfFlattened = lower_coerce.optionalOfFlattened;
|
||||
pub const zeroValue = lower_coerce.zeroValue;
|
||||
pub const lowerCoercedDefault = lower_coerce.lowerCoercedDefault;
|
||||
pub const coerceToType = lower_coerce.coerceToType;
|
||||
pub const coerceExplicit = lower_coerce.coerceExplicit;
|
||||
pub const coerceMode = lower_coerce.coerceMode;
|
||||
pub const diagNonIntegralNarrow = lower_coerce.diagNonIntegralNarrow;
|
||||
pub const promoteCVariadicArgs = lower_coerce.promoteCVariadicArgs;
|
||||
pub const coerceCallArgs = lower_coerce.coerceCallArgs;
|
||||
};
|
||||
|
||||
/// JNI param/return type resolution: user-declared types pass through
|
||||
|
||||
772
src/ir/lower/coerce.zig
Normal file
772
src/ir/lower/coerce.zig
Normal file
@@ -0,0 +1,772 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user