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:
agra
2026-06-02 22:45:56 +03:00
parent 50dd2cc3d8
commit f3bda369f6
4 changed files with 377 additions and 175 deletions

116
src/ir/conversions.test.zig Normal file
View 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
View 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;
}
};

View File

@@ -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");

View File

@@ -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;