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:
116
src/ir/conversions.test.zig
Normal file
116
src/ir/conversions.test.zig
Normal file
@@ -0,0 +1,116 @@
|
||||
// Tests for conversions.zig — the coercion-planning classifier
|
||||
// (`CoercionResolver`). Reached via `ir.CoercionResolver{ .l = &lowering }`,
|
||||
// mirroring the other facade tests. These pin the `classify` / `classifyXX`
|
||||
// DECISIONS; `coerceToType` / `lowerXX` emit them (emission stays in Lowering).
|
||||
|
||||
const std = @import("std");
|
||||
const ast = @import("../ast.zig");
|
||||
const Node = ast.Node;
|
||||
|
||||
const ir_mod = @import("ir.zig");
|
||||
const TypeId = ir_mod.TypeId;
|
||||
const Lowering = ir_mod.Lowering;
|
||||
const CoercionResolver = ir_mod.CoercionResolver;
|
||||
const Plan = CoercionResolver.CoercionPlan;
|
||||
const XXPlan = CoercionResolver.XXPlan;
|
||||
|
||||
fn protoMethodReq(name: []const u8) ast.ProtocolMethodDecl {
|
||||
return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null, .default_body = null };
|
||||
}
|
||||
|
||||
test "conversions: classify covers the built-in coercion ladder" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var l = Lowering.init(&module);
|
||||
const cr = CoercionResolver{ .l = &l };
|
||||
const tt = &module.types;
|
||||
|
||||
// no-op + Any box/unbox.
|
||||
try std.testing.expectEqual(Plan.no_op, cr.classify(.s64, .s64));
|
||||
try std.testing.expectEqual(Plan.unbox_any, cr.classify(.any, .s64));
|
||||
try std.testing.expectEqual(Plan.box_any, cr.classify(.s64, .any));
|
||||
|
||||
// Numeric / pointer ladder.
|
||||
try std.testing.expectEqual(Plan.widen, cr.classify(.s32, .s64));
|
||||
try std.testing.expectEqual(Plan.narrow, cr.classify(.s64, .s32));
|
||||
try std.testing.expectEqual(Plan.int_to_float, cr.classify(.s32, .f64));
|
||||
try std.testing.expectEqual(Plan.float_to_int, cr.classify(.f64, .s32));
|
||||
const ptr_s64 = tt.ptrTo(.s64);
|
||||
try std.testing.expectEqual(Plan.ptr_int_bitcast, cr.classify(ptr_s64, .s64));
|
||||
try std.testing.expectEqual(Plan.ptr_int_bitcast, cr.classify(.s64, ptr_s64));
|
||||
|
||||
// Optional wrap / unwrap, and void → optional.
|
||||
const opt_s64 = tt.optionalOf(.s64);
|
||||
try std.testing.expectEqual(Plan.optional_wrap, cr.classify(.s64, opt_s64));
|
||||
try std.testing.expectEqual(Plan.optional_unwrap, cr.classify(opt_s64, .s64));
|
||||
try std.testing.expectEqual(Plan.void_to_optional, cr.classify(.void, opt_s64));
|
||||
|
||||
// Tuple → tuple, same arity.
|
||||
const t_ss = tt.intern(.{ .tuple = .{ .fields = &[_]TypeId{ .s64, .s64 }, .names = null } });
|
||||
const t_ii = tt.intern(.{ .tuple = .{ .fields = &[_]TypeId{ .s32, .s32 }, .names = null } });
|
||||
try std.testing.expectEqual(Plan.tuple_elementwise, cr.classify(t_ss, t_ii));
|
||||
|
||||
// Closure value → bare fn-ptr: rejected.
|
||||
const clo = tt.closureType(&.{}, .void);
|
||||
const fnp = tt.functionType(&.{}, .void);
|
||||
try std.testing.expectEqual(Plan.closure_to_fn_reject, cr.classify(clo, fnp));
|
||||
|
||||
// Two unrelated structs: no built-in applies → `.none` (lowerXX then tries
|
||||
// a user `Into`). No silent numeric default.
|
||||
const a = tt.intern(.{ .@"struct" = .{ .name = tt.internString("A"), .fields = &.{} } });
|
||||
const b = tt.intern(.{ .@"struct" = .{ .name = tt.internString("B"), .fields = &.{} } });
|
||||
try std.testing.expectEqual(Plan.none, cr.classify(a, b));
|
||||
}
|
||||
|
||||
test "conversions: classify selects protocol erasure for a concrete source" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var l = Lowering.init(&module);
|
||||
const cr = CoercionResolver{ .l = &l };
|
||||
|
||||
const methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
|
||||
const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &methods };
|
||||
l.registerProtocolDecl(&pd);
|
||||
const drawable = module.types.findByName(module.types.internString("Drawable")).?;
|
||||
const circle = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Circle"), .fields = &.{} } });
|
||||
|
||||
// Concrete struct → protocol value: erasure.
|
||||
try std.testing.expectEqual(Plan.erase_protocol, cr.classify(circle, drawable));
|
||||
}
|
||||
|
||||
test "conversions: classifyXX picks the xx-operator head decision" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var l = Lowering.init(&module);
|
||||
const cr = CoercionResolver{ .l = &l };
|
||||
const tt = &module.types;
|
||||
|
||||
const methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
|
||||
const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &methods };
|
||||
l.registerProtocolDecl(&pd);
|
||||
const drawable = tt.findByName(tt.internString("Drawable")).?;
|
||||
|
||||
// Any source unboxes regardless of dst.
|
||||
try std.testing.expectEqual(XXPlan.unbox_any, cr.classifyXX(.any, .s64));
|
||||
// Same type → no-op.
|
||||
try std.testing.expectEqual(XXPlan.no_op, cr.classifyXX(.s64, .s64));
|
||||
// dst is a protocol → erasure (checked before the src-protocol case).
|
||||
try std.testing.expectEqual(XXPlan.erase_protocol, cr.classifyXX(.s64, drawable));
|
||||
// src is a protocol, dst is a pointer → recover the ctx pointer.
|
||||
try std.testing.expectEqual(XXPlan.protocol_to_pointer, cr.classifyXX(drawable, tt.ptrTo(.s64)));
|
||||
// src is a protocol but dst is NOT a pointer → fall to the ladder.
|
||||
try std.testing.expectEqual(XXPlan.coerce, cr.classifyXX(drawable, .s64));
|
||||
// Pointer materialization (`xx value` into a `*T` slot, no built-in) defers
|
||||
// to the ladder + the user-`Into` pointer fallback in lowerXX.
|
||||
const a = tt.intern(.{ .@"struct" = .{ .name = tt.internString("A"), .fields = &.{} } });
|
||||
try std.testing.expectEqual(XXPlan.coerce, cr.classifyXX(a, tt.ptrTo(.s32)));
|
||||
}
|
||||
136
src/ir/conversions.zig
Normal file
136
src/ir/conversions.zig
Normal file
@@ -0,0 +1,136 @@
|
||||
const std = @import("std");
|
||||
const ast = @import("../ast.zig");
|
||||
const types = @import("types.zig");
|
||||
const lower = @import("lower.zig");
|
||||
|
||||
const Node = ast.Node;
|
||||
const TypeId = types.TypeId;
|
||||
const Lowering = lower.Lowering;
|
||||
|
||||
/// Coercion planning (architecture phase A4.3): classify HOW a value of one
|
||||
/// type converts to another, before `Lowering` emits the IR for it. The
|
||||
/// classifier is pure (reads the type table + protocol/impl registries); all
|
||||
/// actual IR emission — `unbox_any`/`optional_wrap`/`int_to_float`/protocol
|
||||
/// erasure/the `Into` call — stays in `Lowering`.
|
||||
///
|
||||
/// A `*Lowering` facade (Principle 5, like `CallResolver`/`GenericResolver`/
|
||||
/// `ProtocolResolver`). Two entry points:
|
||||
/// - `classify(src, dst)` — the built-in coercion ladder consumed by
|
||||
/// `coerceToType` (the shared, recursive value-conversion path).
|
||||
/// - `classifyXX(src, dst)` — the `xx`-operator head consumed by `lowerXX`
|
||||
/// (Any unbox, no-op, protocol erasure, protocol→pointer, else the ladder
|
||||
/// + the user-`Into` fallback).
|
||||
pub const CoercionResolver = struct {
|
||||
l: *Lowering,
|
||||
|
||||
/// The built-in coercion the `coerceToType` ladder will emit for `src → dst`.
|
||||
/// `.none` means no built-in applies (the value passes through unchanged;
|
||||
/// `lowerXX` then tries a user `Into`). Branch order mirrors `coerceToType`
|
||||
/// exactly — the emitter switches on this and reproduces each arm.
|
||||
pub const CoercionPlan = enum {
|
||||
no_op, // src == dst
|
||||
unbox_any, // any → concrete
|
||||
box_any, // concrete → any
|
||||
closure_to_fn_reject, // closure value → bare fn-ptr (diagnostic, returns operand)
|
||||
tuple_elementwise, // (A,B) → (C,D), same arity
|
||||
optional_unwrap, // ?T → concrete (narrowing)
|
||||
void_to_optional, // void (null literal) → ?T
|
||||
optional_wrap, // concrete → ?T
|
||||
erase_protocol, // concrete → protocol value
|
||||
int_to_float,
|
||||
float_to_int,
|
||||
ptr_int_bitcast, // ptr ↔ int
|
||||
widen, // same kind, dst wider
|
||||
narrow, // same kind, dst narrower
|
||||
none, // nothing applies — pass the value through
|
||||
};
|
||||
|
||||
pub fn classify(self: CoercionResolver, src_ty: TypeId, dst_ty: TypeId) CoercionPlan {
|
||||
if (src_ty == dst_ty) return .no_op;
|
||||
if (src_ty == .any and dst_ty != .any) return .unbox_any;
|
||||
if (dst_ty == .any and src_ty != .any) return .box_any;
|
||||
|
||||
if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
|
||||
if (self.l.module.types.get(src_ty) == .closure and self.l.module.types.get(dst_ty) == .function) {
|
||||
return .closure_to_fn_reject;
|
||||
}
|
||||
}
|
||||
|
||||
// Tuple → Tuple, same arity.
|
||||
if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
|
||||
const si = self.l.module.types.get(src_ty);
|
||||
const di = self.l.module.types.get(dst_ty);
|
||||
if (si == .tuple and di == .tuple and si.tuple.fields.len == di.tuple.fields.len) {
|
||||
return .tuple_elementwise;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional → Concrete unwrap (narrowing).
|
||||
if (!src_ty.isBuiltin()) {
|
||||
const src_info = self.l.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())) {
|
||||
return .optional_unwrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// void (null literal) → Optional.
|
||||
if (src_ty == .void and !dst_ty.isBuiltin()) {
|
||||
if (self.l.module.types.get(dst_ty) == .optional) return .void_to_optional;
|
||||
}
|
||||
|
||||
// Concrete → Optional wrap.
|
||||
if (!dst_ty.isBuiltin()) {
|
||||
if (self.l.module.types.get(dst_ty) == .optional) return .optional_wrap;
|
||||
}
|
||||
|
||||
// Concrete → Protocol (auto type erasure) — only when the source has a
|
||||
// resolvable concrete type name; otherwise fall through to the numeric
|
||||
// ladder (matching `coerceToType`, which leaves the erase block).
|
||||
if (self.l.getProtocolInfo(dst_ty) != null) {
|
||||
if (self.l.resolveConcreteTypeName(src_ty) != null) return .erase_protocol;
|
||||
}
|
||||
|
||||
// Numeric / pointer ladder.
|
||||
const src_float = Lowering.isFloat(src_ty);
|
||||
const dst_float = Lowering.isFloat(dst_ty);
|
||||
const src_int = self.l.isIntEx(src_ty);
|
||||
const dst_int = self.l.isIntEx(dst_ty);
|
||||
const src_ptr = !src_ty.isBuiltin() and self.l.module.types.get(src_ty) == .pointer;
|
||||
const dst_ptr = !dst_ty.isBuiltin() and self.l.module.types.get(dst_ty) == .pointer;
|
||||
|
||||
if (src_int and dst_float) return .int_to_float;
|
||||
if (src_float and dst_int) return .float_to_int;
|
||||
if ((src_ptr and dst_int) or (src_int and dst_ptr)) return .ptr_int_bitcast;
|
||||
|
||||
const src_bits = self.l.typeBitsEx(src_ty);
|
||||
const dst_bits = self.l.typeBitsEx(dst_ty);
|
||||
if (src_bits > 0 and dst_bits > 0) {
|
||||
if (dst_bits < src_bits) return .narrow;
|
||||
if (dst_bits > src_bits) return .widen;
|
||||
}
|
||||
return .none;
|
||||
}
|
||||
|
||||
/// The `xx`-operator head decision for `lowerXX`. `.coerce` defers to the
|
||||
/// built-in ladder (`coerceToType` / `classify`) + the user-`Into` fallback.
|
||||
/// Branch order mirrors `lowerXX` exactly.
|
||||
pub const XXPlan = enum {
|
||||
unbox_any, // src is Any → unbox (lowerXX adds the f32/f64 match dispatch)
|
||||
no_op, // src == dst
|
||||
erase_protocol, // dst is a protocol → buildProtocolErasure
|
||||
protocol_to_pointer, // src is a protocol, dst is a pointer → recover ctx
|
||||
coerce, // built-in ladder + user `Into` fallback
|
||||
};
|
||||
|
||||
pub fn classifyXX(self: CoercionResolver, src_ty: TypeId, dst_ty: TypeId) XXPlan {
|
||||
if (src_ty == .any) return .unbox_any;
|
||||
if (src_ty == dst_ty) return .no_op;
|
||||
if (self.l.getProtocolInfo(dst_ty) != null) return .erase_protocol;
|
||||
if (self.l.getProtocolInfo(src_ty) != null and !dst_ty.isBuiltin() and
|
||||
self.l.module.types.get(dst_ty) == .pointer) return .protocol_to_pointer;
|
||||
return .coerce;
|
||||
}
|
||||
};
|
||||
@@ -11,6 +11,7 @@ pub const expr_typer = @import("expr_typer.zig");
|
||||
pub const calls = @import("calls.zig");
|
||||
pub const generics = @import("generics.zig");
|
||||
pub const protocols = @import("protocols.zig");
|
||||
pub const conversions = @import("conversions.zig");
|
||||
pub const semantic_diagnostics = @import("semantic_diagnostics.zig");
|
||||
|
||||
pub const TypeId = types.TypeId;
|
||||
@@ -47,6 +48,8 @@ pub const CallResolver = calls.CallResolver;
|
||||
pub const CallPlan = calls.CallPlan;
|
||||
pub const GenericResolver = generics.GenericResolver;
|
||||
pub const ProtocolResolver = protocols.ProtocolResolver;
|
||||
pub const CoercionResolver = conversions.CoercionResolver;
|
||||
pub const CoercionPlan = conversions.CoercionResolver.CoercionPlan;
|
||||
|
||||
pub const compiler_hooks = @import("compiler_hooks.zig");
|
||||
pub const emit_llvm = @import("emit_llvm.zig");
|
||||
@@ -72,6 +75,7 @@ pub const expr_typer_tests = @import("expr_typer.test.zig");
|
||||
pub const calls_tests = @import("calls.test.zig");
|
||||
pub const generics_tests = @import("generics.test.zig");
|
||||
pub const protocols_tests = @import("protocols.test.zig");
|
||||
pub const conversions_tests = @import("conversions.test.zig");
|
||||
pub const type_bridge_tests = @import("type_bridge.test.zig");
|
||||
pub const emit_llvm_tests = @import("emit_llvm.test.zig");
|
||||
pub const jni_descriptor_tests = @import("jni_descriptor.test.zig");
|
||||
|
||||
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