refactor(ir): extract CoercionResolver (conversions.zig) for coercion planning (A4.3 step 2)
Coercion classification now lives in src/ir/conversions.zig behind a *Lowering facade (CoercionResolver), mirroring CallResolver / GenericResolver / ProtocolResolver. Two pure classifiers: - classify(src, dst) -> CoercionPlan (15 kinds: no_op / unbox_any / box_any / closure_to_fn_reject / tuple_elementwise / optional_unwrap / void_to_optional / optional_wrap / erase_protocol / int_to_float / float_to_int / ptr_int_bitcast / widen / narrow / none) — the built-in coercion ladder. - classifyXX(src, dst) -> XXPlan (unbox_any / no_op / erase_protocol / protocol_to_pointer / coerce) — the xx-operator head. coerceToType and lowerXX now `switch (classify…)` then emit; branch order mirrors the originals exactly and every arm reproduces the prior lowering — the f32/f64 Any match dispatch, buildProtocolErasure (lowerXX) vs buildProtocolValue (coerceToType), tuple/optional recursion, and the user-Into fallback + pointer materialization + recursion-guard/diagnostics (which stay in lowerXX / tryUserConversion). IR emission stays entirely in Lowering; the classifiers are pure. lowerXX keeps the operand's lowered Ref type as src_ty. `.none` means no built-in applies (pass through; the Into fallback runs) — no silent default. New pub: isFloat / isIntEx / typeBitsEx / resolveConcreteTypeName (the classifier reads them); coercionResolver() accessor. lower.zig net -54 lines. conversions.test.zig drives CoercionResolver directly: the full classify ladder (no-op, Any box/unbox, widen/narrow, int<->float, ptr<->int, optional wrap/unwrap, void->optional, tuple, closure-reject, .none for two unrelated structs), erase_protocol for a concrete source, and classifyXX (all 5 kinds incl. protocol-to-pointer vs coerce and pointer-materialization -> coerce). zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir churn.
This commit is contained in:
296
src/ir/lower.zig
296
src/ir/lower.zig
@@ -26,6 +26,7 @@ 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 semantic_diagnostics = @import("semantic_diagnostics.zig");
|
||||
|
||||
const TypeId = types.TypeId;
|
||||
@@ -13796,7 +13797,7 @@ pub const Lowering = struct {
|
||||
|
||||
/// Resolve the concrete type name for protocol erasure.
|
||||
/// Handles both direct types and pointer-to-types.
|
||||
fn resolveConcreteTypeName(self: *Lowering, ty: TypeId) ?[]const u8 {
|
||||
pub fn resolveConcreteTypeName(self: *Lowering, ty: TypeId) ?[]const u8 {
|
||||
if (ty.isBuiltin()) {
|
||||
// Primitive types like s64 — check if they have toName()
|
||||
return self.module.types.typeName(ty);
|
||||
@@ -13840,6 +13841,10 @@ pub const Lowering = struct {
|
||||
return .{ .l = self };
|
||||
}
|
||||
|
||||
pub fn coercionResolver(self: *Lowering) CoercionResolver {
|
||||
return .{ .l = self };
|
||||
}
|
||||
|
||||
/// Lower the `xx` operator (type coercion).
|
||||
/// Uses self.target_type for context when available. Handles:
|
||||
/// - Any → concrete type: unbox_any
|
||||
@@ -13855,59 +13860,54 @@ pub const Lowering = struct {
|
||||
const target_explicit = self.target_type != null;
|
||||
const dst_ty = self.target_type orelse .unresolved;
|
||||
|
||||
// Any → concrete type: unbox
|
||||
if (src_ty == .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);
|
||||
// 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
|
||||
if (src_ty == dst_ty) return operand;
|
||||
|
||||
// Concrete → Protocol: build protocol value
|
||||
if (self.getProtocolInfo(dst_ty)) |_| {
|
||||
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.
|
||||
if (self.getProtocolInfo(src_ty)) |_| {
|
||||
if (!dst_ty.isBuiltin()) {
|
||||
const dst_info = self.module.types.get(dst_ty);
|
||||
if (dst_info == .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);
|
||||
}
|
||||
}
|
||||
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.coerceToType(operand, src_ty, dst_ty);
|
||||
@@ -14468,42 +14468,35 @@ pub const Lowering = struct {
|
||||
/// Insert a conversion if src_ty and dst_ty differ.
|
||||
/// Handles int widening/narrowing, float widening/narrowing, and int↔float.
|
||||
fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
|
||||
if (src_ty == dst_ty) return val;
|
||||
// Unbox Any → concrete type
|
||||
if (src_ty == .any and dst_ty != .any) {
|
||||
return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty);
|
||||
}
|
||||
// Box concrete → Any
|
||||
if (dst_ty == .any and src_ty != .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.
|
||||
if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
|
||||
if (self.module.types.get(src_ty) == .closure and self.module.types.get(dst_ty) == .function) {
|
||||
// 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, at least one differing field (src_ty == dst_ty
|
||||
// already returned above): extract each slot, coerce it, rebuild.
|
||||
if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
|
||||
const si = self.module.types.get(src_ty);
|
||||
const di = self.module.types.get(dst_ty);
|
||||
if (si == .tuple and di == .tuple and si.tuple.fields.len == di.tuple.fields.len) {
|
||||
},
|
||||
// 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| {
|
||||
@@ -14511,100 +14504,53 @@ pub const Lowering = struct {
|
||||
elems.append(self.alloc, self.coerceToType(fv, sf, df)) 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)
|
||||
if (!src_ty.isBuiltin()) {
|
||||
const src_info = self.module.types.get(src_ty);
|
||||
if (src_info == .optional) {
|
||||
const child_ty = src_info.optional.child;
|
||||
if (child_ty == dst_ty or (dst_ty.isBuiltin() and child_ty.isBuiltin())) {
|
||||
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
||||
return self.coerceToType(unwrapped, child_ty, dst_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// void → Optional: produce null (void is the type of null_literal)
|
||||
if (src_ty == .void and !dst_ty.isBuiltin()) {
|
||||
const dst_info = self.module.types.get(dst_ty);
|
||||
if (dst_info == .optional) {
|
||||
return self.builder.constNull(dst_ty);
|
||||
}
|
||||
}
|
||||
|
||||
// Concrete → Optional wrapping
|
||||
if (!dst_ty.isBuiltin()) {
|
||||
const dst_info = self.module.types.get(dst_ty);
|
||||
if (dst_info == .optional) {
|
||||
const child_ty = dst_info.optional.child;
|
||||
// Coerce the value to the inner type first
|
||||
},
|
||||
// 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.coerceToType(unwrapped, child_ty, dst_ty);
|
||||
},
|
||||
// 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.coerceToType(val, src_ty, child_ty);
|
||||
return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty);
|
||||
}
|
||||
}
|
||||
|
||||
// Concrete → Protocol (auto type erasure)
|
||||
if (self.getProtocolInfo(dst_ty)) |_| {
|
||||
const dst_info = self.module.types.get(dst_ty);
|
||||
if (dst_info == .@"struct") {
|
||||
const proto_name = self.module.types.getString(dst_info.@"struct".name);
|
||||
if (self.resolveConcreteTypeName(src_ty)) |ctn| {
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
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 => 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),
|
||||
}
|
||||
|
||||
const src_float = isFloat(src_ty);
|
||||
const dst_float = isFloat(dst_ty);
|
||||
const src_int = self.isIntEx(src_ty);
|
||||
const dst_int = self.isIntEx(dst_ty);
|
||||
const src_ptr = !src_ty.isBuiltin() and self.module.types.get(src_ty) == .pointer;
|
||||
const dst_ptr = !dst_ty.isBuiltin() and self.module.types.get(dst_ty) == .pointer;
|
||||
|
||||
// Int → Float
|
||||
if (src_int and dst_float) {
|
||||
return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
|
||||
}
|
||||
// Float → Int
|
||||
if (src_float and dst_int) {
|
||||
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.
|
||||
if ((src_ptr and dst_int) or (src_int and dst_ptr)) {
|
||||
return self.builder.emit(.{ .bitcast = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
|
||||
}
|
||||
// Same kind — widen/narrow based on bit width
|
||||
const src_bits = self.typeBitsEx(src_ty);
|
||||
const dst_bits = self.typeBitsEx(dst_ty);
|
||||
if (src_bits > 0 and dst_bits > 0) {
|
||||
if (dst_bits < src_bits) {
|
||||
return self.builder.emit(.{ .narrow = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
|
||||
} else if (dst_bits > src_bits) {
|
||||
return self.builder.emit(.{ .widen = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/// Get the alloca Ref for an expression, if it's a simple variable reference.
|
||||
@@ -14639,7 +14585,7 @@ pub const Lowering = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn isFloat(ty: TypeId) bool {
|
||||
pub fn isFloat(ty: TypeId) bool {
|
||||
return ty == .f32 or ty == .f64;
|
||||
}
|
||||
|
||||
@@ -14650,7 +14596,7 @@ pub const Lowering = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn isIntEx(self: *Lowering, ty: TypeId) bool {
|
||||
pub fn isIntEx(self: *Lowering, ty: TypeId) bool {
|
||||
if (isInt(ty)) return true;
|
||||
if (!ty.isBuiltin()) {
|
||||
const info = self.module.types.get(ty);
|
||||
@@ -15984,7 +15930,7 @@ pub const Lowering = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn typeBitsEx(self: *Lowering, ty: TypeId) u32 {
|
||||
pub fn typeBitsEx(self: *Lowering, ty: TypeId) u32 {
|
||||
if (ty == .usize or ty == .isize) return @as(u32, self.module.types.pointer_size) * 8;
|
||||
const b = typeBits(ty);
|
||||
if (b > 0) return b;
|
||||
|
||||
Reference in New Issue
Block a user