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 array_to_slice, // [N]T → []T (materialize backing storage + header) 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; } } // Fixed array → slice of the same element: an aggregate array value // (e.g. a `.[...]` literal passed directly as a call arg) needs to be // materialized into addressable storage and wrapped in a {ptr,len} // header. Without this the array value is passed where a slice is // expected — the callee reads the header off the wrong bytes (issue // 0084). The local-bound path already does this conversion on its own. 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 == .array and di == .slice and si.array.element == di.slice.element) { return .array_to_slice; } } // 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; } };