From f3bda369f6ef139e1023aa22f6670d7e525a6c93 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 22:45:56 +0300 Subject: [PATCH] refactor(ir): extract CoercionResolver (conversions.zig) for coercion planning (A4.3 step 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/ir/conversions.test.zig | 116 ++++++++++++++ src/ir/conversions.zig | 136 +++++++++++++++++ src/ir/ir.zig | 4 + src/ir/lower.zig | 296 +++++++++++++++--------------------- 4 files changed, 377 insertions(+), 175 deletions(-) create mode 100644 src/ir/conversions.test.zig create mode 100644 src/ir/conversions.zig diff --git a/src/ir/conversions.test.zig b/src/ir/conversions.test.zig new file mode 100644 index 0000000..8873bd1 --- /dev/null +++ b/src/ir/conversions.test.zig @@ -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))); +} diff --git a/src/ir/conversions.zig b/src/ir/conversions.zig new file mode 100644 index 0000000..d2ce1c7 --- /dev/null +++ b/src/ir/conversions.zig @@ -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; + } +}; diff --git a/src/ir/ir.zig b/src/ir/ir.zig index 42b8922..b43c508 100644 --- a/src/ir/ir.zig +++ b/src/ir/ir.zig @@ -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"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index fe0008a..9e0da23 100644 --- a/src/ir/lower.zig +++ b/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;