const std = @import("std"); const Allocator = std.mem.Allocator; const ast = @import("../../ast.zig"); const Node = ast.Node; const types = @import("../types.zig"); const inst_mod = @import("../inst.zig"); const mod_mod = @import("../module.zig"); const type_bridge = @import("../type_bridge.zig"); const unescape = @import("../../unescape.zig"); const parser_mod = @import("../../parser.zig"); const interp_mod = @import("../interp.zig"); const errors = @import("../../errors.zig"); const jni_descriptor = @import("../jni_descriptor.zig"); const program_index_mod = @import("../program_index.zig"); const resolver_mod = @import("../resolver.zig"); const imports_mod = @import("../../imports.zig"); const ProgramIndex = program_index_mod.ProgramIndex; const GlobalInfo = program_index_mod.GlobalInfo; const StructTemplate = program_index_mod.StructTemplate; const TemplateParam = program_index_mod.TemplateParam; const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; const ModuleConstInfo = program_index_mod.ModuleConstInfo; const TypeResolver = @import("../type_resolver.zig").TypeResolver; const ResolveEnv = @import("../type_resolver.zig").ResolveEnv; const PackResolver = @import("../packs.zig").PackResolver; 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 ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; const ErrorFlow = @import("../error_flow.zig").ErrorFlow; const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; const semantic_diagnostics = @import("../semantic_diagnostics.zig"); const TypeId = types.TypeId; const StringId = types.StringId; const Ref = inst_mod.Ref; const BlockId = inst_mod.BlockId; const FuncId = inst_mod.FuncId; const Function = inst_mod.Function; const Module = mod_mod.Module; const Builder = mod_mod.Builder; const lower = @import("../lower.zig"); const Lowering = lower.Lowering; const Scope = lower.Scope; const ParamImplEntry = Lowering.ParamImplEntry; /// Lower the `xx` operator (type coercion). /// Uses self.target_type for context when available. Handles: /// - Any → concrete type: unbox_any /// - int → int: widen/narrow /// - int ↔ float: int_to_float/float_to_int pub fn lowerXX(self: *Lowering, operand: Ref, operand_node: *const Node) Ref { // Use the operand's *actual* lowered Ref type rather than reaching // back through inferExprType — the latter doesn't cover every // expression shape (notably lambdas), and a wrong src_ty here can // route the cast through coerceToType (e.g. a bogus s64→ptr bitcast) // and silently skip the user-space Into fallback. const src_ty = self.builder.getRefType(operand); const target_explicit = self.target_type != null; const dst_ty = self.target_type orelse .unresolved; // 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. .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.coerceExplicit(operand, src_ty, dst_ty); // User-space fallback via `impl Into(Target) for Source`. Only fires // when the target was explicitly named (not the .s64 default), src and // dst differ, and the built-in ladder made no progress. Built-ins // always win. if (target_explicit and src_ty != dst_ty and result == operand) { if (self.tryUserConversion(operand, operand_node, src_ty, dst_ty)) |converted| { return converted; } // Pointer-target fallback: `xx ` whose surrounding context // expects `*T` (a fn arg slot, a var typed as a pointer-to-aggregate) // can be satisfied by `impl Into(T) for src` plus an implicit // alloca+store on the result. Lets users write // `fn(xx () => { ... })` instead of materialising a named Block local // just to take its address. if (!dst_ty.isBuiltin()) { const dst_info = self.module.types.get(dst_ty); if (dst_info == .pointer) { const pointee = dst_info.pointer.pointee; if (pointee != src_ty) { if (self.tryUserConversion(operand, operand_node, src_ty, pointee)) |converted| { const slot = self.builder.alloca(pointee); self.builder.store(slot, converted); return slot; } } } } } return result; } /// Detect the `xx closure : Block` cast pattern so `tryUserConversion` /// can emit a focused diagnostic when no `Into(Block) for Closure(...)` /// impl is reachable. Replaces what was briefly a compiler-synthesised /// trampoline path with a "declare an impl" requirement — the stdlib /// covers common signatures (see modules/std/objc_block.sx), users /// add their own for unusual ones. pub fn isClosureToBlockCast(self: *Lowering, src_ty: TypeId, dst_ty: TypeId) bool { if (src_ty.isBuiltin()) return false; const src_info = self.module.types.get(src_ty); if (src_info != .closure) return false; if (dst_ty.isBuiltin()) return false; const dst_info = self.module.types.get(dst_ty); if (dst_info != .@"struct") return false; const block_name = self.module.types.internString("Block"); return dst_info.@"struct".name == block_name; } /// Pack-variadic impl matching. Walks `param_impl_pack_map[pack_key]` /// and returns a call ref when a single pack impl matches `src_ty`'s /// shape (concrete src closure / fn with the same fixed prefix as /// the impl's source pack closure). Binds the pack-var to the source's /// tail param types and the return-var (when generic) to the source's /// return type, then monomorphises the convert method. /// Returns null if no pack impls registered for this (proto, dst) or /// none of them match `src_ty`'s shape. pub fn tryPackImplMatch( self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId, proto_name: []const u8, pack_key: []const u8, guard_key: u64, ) ?Ref { _ = operand_node; // PLANNING: select the matching pack impl + its `convert` (registry). const match = self.protocolResolver().matchPackImpl(src_ty, pack_key) orelse return null; const entry = match.entry; const fd = match.convert_fd; const src_params = match.src_params; const src_ret = match.src_ret; const table = &self.module.types; // EMISSION: bind the pack tail + ret-var, monomorphise, call (Lowering). // Build bindings. Target → dst_ty (already in the protocol's type // params), pack-var → src tail TypeIds, ret-var (when generic) → // src ret. const ent_pack_start = table.get(entry.source_pack_ty).closure.pack_start.?; const tail = src_params[ent_pack_start..]; const tail_owned = self.alloc.dupe(TypeId, tail) catch return null; var bindings = std.StringHashMap(TypeId).init(self.alloc); defer bindings.deinit(); const pd = self.program_index.protocol_ast_map.get(proto_name) orelse return null; bindings.put(pd.type_params[0].name, dst_ty) catch return null; if (entry.ret_var_name) |rv| bindings.put(rv, src_ret) catch return null; var pack_bindings = std.StringHashMap([]const TypeId).init(self.alloc); defer pack_bindings.deinit(); pack_bindings.put(entry.pack_var_name, tail_owned) catch return null; // Mangled name keyed on the CONCRETE source so distinct shapes // monomorphise separately. Same scheme as the concrete path: // ".convert__". const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }) catch return null; self.xx_reentrancy.put(guard_key, {}) catch {}; defer _ = self.xx_reentrancy.remove(guard_key); if (!self.lowered_functions.contains(mangled)) { const saved_pack = self.pack_bindings; self.pack_bindings = pack_bindings; defer self.pack_bindings = saved_pack; self.monomorphizeFunction(fd, mangled, &bindings); } const fid = self.resolveFuncByName(mangled) orelse return null; const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; var single = [_]Ref{operand}; const final_args = self.prependCtxIfNeeded(func, single[0..]); self.coerceCallArgs(final_args, params); return self.builder.call(fid, final_args, ret_ty); } /// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise /// the impl's `convert` method and emit a direct call. Returns null when /// no impl matches (caller falls back to the built-in result, which is /// the unchanged operand — Phase 3 emits no diagnostic for v0). pub fn tryUserConversion(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) ?Ref { // Reentrancy guard — pack (src, dst) into a u64. const guard_key: u64 = (@as(u64, src_ty.index()) << 32) | @as(u64, dst_ty.index()); if (self.xx_reentrancy.contains(guard_key)) { if (self.diagnostics) |diags| { diags.addFmt(.err, operand_node.span, "recursive xx conversion from '{s}' to '{s}'", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }); } return operand; } // Build lookup key: "Into\x00\x00". // Hardcoded to the "Into" protocol for v1. Generalising to other // parameterised protocols would walk protocol_decl_map looking for // protocols that take a single type-param and have a `convert` method. const proto_name = "Into"; const pd = self.program_index.protocol_ast_map.get(proto_name) orelse return null; if (pd.type_params.len != 1) return null; var key_buf = std.ArrayList(u8).empty; key_buf.appendSlice(self.alloc, proto_name) catch return null; key_buf.append(self.alloc, 0) catch return null; key_buf.appendSlice(self.alloc, self.mangleTypeName(dst_ty)) catch return null; key_buf.append(self.alloc, 0) catch return null; key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return null; const key = key_buf.items; // Pack-only key (proto + dst) — used if the concrete lookup misses. // Same prefix as the concrete key, minus the `\x00` tail. const dst_mangled_len = self.mangleTypeName(dst_ty).len; const pack_key = key_buf.items[0 .. proto_name.len + 1 + dst_mangled_len]; const entries_opt = self.param_impl_map.get(key); const has_concrete = entries_opt != null and entries_opt.?.items.len > 0; if (!has_concrete) { // Concrete miss — try the pack map before emitting a diagnostic. if (self.tryPackImplMatch(operand, operand_node, src_ty, dst_ty, proto_name, pack_key, guard_key)) |result| { return result; } if (self.isClosureToBlockCast(src_ty, dst_ty)) { if (self.diagnostics) |diags| { const saved = diags.current_source_file; diags.current_source_file = operand_node.source_file orelse self.current_source_file; defer diags.current_source_file = saved; diags.addFmt(.err, operand_node.span, "no `Into(Block) for {s}` impl — add a per-signature `__block_invoke_` trampoline + Into impl alongside the existing ones in modules/std/objc_block.sx, or declare it in your own code", .{self.mangleTypeName(src_ty)}); } return operand; } return null; } const entries = entries_opt.?; // Filter by import visibility: only impls in modules that the current // file transitively imports (or the current file itself) are reachable. // Falls open when import_graph isn't wired (e.g. comptime callers). var visible_impls = std.ArrayList(ParamImplEntry).empty; defer visible_impls.deinit(self.alloc); self.protocolResolver().findVisibleImpls(entries.items, &visible_impls); if (visible_impls.items.len == 0) { if (self.diagnostics) |diags| { const saved = diags.current_source_file; diags.current_source_file = operand_node.source_file orelse self.current_source_file; defer diags.current_source_file = saved; diags.addFmt(.err, operand_node.span, "no visible xx conversion from '{s}' to '{s}' — impl exists in another module but is not imported", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }); } return operand; } if (visible_impls.items.len > 1) { if (self.diagnostics) |diags| { const saved = diags.current_source_file; diags.current_source_file = operand_node.source_file orelse self.current_source_file; defer diags.current_source_file = saved; diags.addFmt(.err, operand_node.span, "duplicate xx conversion from '{s}' to '{s}': impls in {s} and {s}", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), visible_impls.items[0].defining_module, visible_impls.items[1].defining_module, }); } return operand; } const entry = visible_impls.items[0]; // Find the `convert` method on this impl. var convert_fd: ?*const ast.FnDecl = null; for (entry.methods) |m| { if (std.mem.eql(u8, m.name, "convert")) { convert_fd = m; break; } } const fd = convert_fd orelse return null; // Bind Target → dst_ty. var bindings = std.StringHashMap(TypeId).init(self.alloc); defer bindings.deinit(); bindings.put(pd.type_params[0].name, dst_ty) catch return null; // Mangled name: ".convert__". const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{ self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), }) catch return null; self.xx_reentrancy.put(guard_key, {}) catch {}; defer _ = self.xx_reentrancy.remove(guard_key); if (!self.lowered_functions.contains(mangled)) { self.monomorphizeFunction(fd, mangled, &bindings); } const fid = self.resolveFuncByName(mangled) orelse return null; const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; var single = [_]Ref{operand}; const final_args = self.prependCtxIfNeeded(func, single[0..]); self.coerceCallArgs(final_args, params); return self.builder.call(fid, final_args, ret_ty); } /// True for expression shapes that name an addressable storage location /// (variables, fields, array elements, dereferenced pointers). Used by /// `xx ` to decide between borrow (lvalue → take the /// address) and heap-copy (rvalue → allocate a fresh copy). pub fn isLvalueExpr(self: *Lowering, node: *const Node) bool { _ = self; return switch (node.data) { .identifier, .field_access, .index_expr, .deref_expr => true, else => false, }; } /// Build a protocol value from a concrete value via xx conversion. /// Coerce `val` (type `src`) to `dst`: if `dst` is a protocol, `xx`-erase /// the concrete value into it; otherwise fall back to numeric/struct /// coercion. Used to materialize a pack into a protocol-typed tuple field. pub fn coerceOrErase(self: *Lowering, val: Ref, src: TypeId, dst: TypeId, node: *const Node) Ref { if (src == dst) return val; if (!dst.isBuiltin()) { const di = self.module.types.get(dst); if (di == .@"struct" and di.@"struct".is_protocol) { return self.buildProtocolErasure(val, node, src, dst); } } return self.coerceToType(val, src, dst); } pub fn buildProtocolErasure(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) Ref { const dst_info = self.module.types.get(dst_ty); if (dst_info != .@"struct") return operand; const proto_name = self.module.types.getString(dst_info.@"struct".name); // Determine concrete type name and type — resolve through pointer if needed var concrete_ptr = operand; var concrete_type_name: ?[]const u8 = null; var concrete_ty: TypeId = src_ty; var heap_copy = false; if (!src_ty.isBuiltin()) { const src_info = self.module.types.get(src_ty); if (src_info == .pointer) { // xx @acc — operand is already a pointer (user manages lifetime) const pointee = src_info.pointer.pointee; concrete_type_name = self.resolveConcreteTypeName(pointee); concrete_ty = pointee; heap_copy = false; } else if (src_info == .@"struct") { // Struct-typed operand. Split on lvalue-ness: // - lvalue (identifier, field, index, deref): borrow the // storage the operand already names. No heap copy; the // protocol value's ctx points at the caller's slot, and // mutations through the protocol are visible to the // original. Lifetime is the caller's responsibility. // - rvalue (struct literal, call result, etc.): heap-copy // into a fresh allocation so the protocol value is // self-contained and outlives this expression. concrete_type_name = self.module.types.getString(src_info.@"struct".name); concrete_ty = src_ty; if (self.isLvalueExpr(operand_node)) { concrete_ptr = self.lowerExprAsPtr(operand_node); heap_copy = false; } else { heap_copy = true; const slot = self.builder.alloca(src_ty); self.builder.store(slot, operand); concrete_ptr = slot; } } } // Also try from the operand node for struct literals: xx Accumulator.{ total = 0 } if (concrete_type_name == null) { concrete_type_name = self.inferConcreteTypeName(operand_node); if (concrete_type_name != null) heap_copy = true; } if (concrete_type_name) |ctn| { return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy); } return operand; } /// Try to infer the concrete type name from an AST node (for struct literals etc.) pub fn inferConcreteTypeName(self: *Lowering, node: *const Node) ?[]const u8 { return switch (node.data) { .struct_literal => |sl| if (sl.struct_name) |n| n else null, .unary_op => |uop| if (uop.op == .address_of) self.inferConcreteTypeName(uop.operand) else null, .identifier => |id| blk: { // Check if identifier's type resolves to a struct if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { if (!binding.ty.isBuiltin()) { const bi = self.module.types.get(binding.ty); if (bi == .@"struct") break :blk self.module.types.getString(bi.@"struct".name); if (bi == .pointer) { const pointee = bi.pointer.pointee; if (!pointee.isBuiltin()) { const pi = self.module.types.get(pointee); if (pi == .@"struct") break :blk self.module.types.getString(pi.@"struct".name); } } } } } break :blk null; }, else => null, }; } /// Generate a mini-dispatch for unboxing Any to f64 when the value might be f32 or f64. /// Uses alloca-based merge: create result slot, branch, store in each arm, load after merge. pub fn lowerAnyToF64Dispatch(self: *Lowering, any_val: Ref) Ref { // Create result alloca BEFORE the branch const result_slot = self.builder.alloca(.f64); // Extract type tag from Any const tag = self.builder.structGet(any_val, 0, .s64); const f32_bb = self.freshBlock("f32.unbox"); const f64_bb = self.freshBlock("f64.unbox"); const merge_bb = self.freshBlock("float.merge"); // Branch: tag == f32_tag ? f32_bb : f64_bb const f32_tag = self.builder.constInt(TypeId.f32.index(), .s64); const cond = self.builder.emit(.{ .cmp_eq = .{ .lhs = tag, .rhs = f32_tag } }, .bool); self.builder.condBr(cond, f32_bb, &.{}, f64_bb, &.{}); // f32 block: unbox as f32, fpext to f64, store self.builder.switchToBlock(f32_bb); const f32_val = self.builder.emit(.{ .unbox_any = .{ .operand = any_val, } }, .f32); const f64_from_f32 = self.builder.emit(.{ .widen = .{ .operand = f32_val, .from = .f32, .to = .f64 } }, .f64); self.builder.store(result_slot, f64_from_f32); self.builder.br(merge_bb, &.{}); // f64 block: unbox as f64 directly, store self.builder.switchToBlock(f64_bb); const f64_val = self.builder.emit(.{ .unbox_any = .{ .operand = any_val, } }, .f64); self.builder.store(result_slot, f64_val); self.builder.br(merge_bb, &.{}); // Merge block: load result self.builder.switchToBlock(merge_bb); return self.builder.load(result_slot, .f64); } /// Produce a default value for a type, applying struct field defaults. /// For structs with defaults (e.g., `b: s32 = 99`), creates a struct_literal with defaults applied. /// For other types, returns a zero value. pub fn buildDefaultValue(self: *Lowering, ty: TypeId) Ref { if (ty.isBuiltin()) return self.builder.constInt(0, ty); const info = self.module.types.get(ty); if (info != .@"struct" and info != .tuple) return self.zeroValue(ty); // For tuples, build a zero-initialized tuple if (info == .tuple) { var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); for (info.tuple.fields) |f| { field_vals.append(self.alloc, self.zeroValue(f)) catch unreachable; } return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable }, }, ty); } // Check for struct defaults const struct_name_str = self.module.types.getString(info.@"struct".name); const field_defaults = self.struct_defaults_map.get(struct_name_str) orelse return self.builder.constUndef(ty); const fields = info.@"struct".fields; var field_vals = std.ArrayList(Ref).empty; defer field_vals.deinit(self.alloc); for (fields, 0..) |f, i| { if (i < field_defaults.len) { if (field_defaults[i]) |default_expr| { field_vals.append(self.alloc, self.lowerCoercedDefault(default_expr, f.ty)) catch unreachable; } else { field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable; } } else { field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable; } } return self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable }, }, ty); } /// Wrap ty in ?ty, but flatten: if ty is already ?U, return ?U (not ??U) pub fn optionalOfFlattened(self: *Lowering, ty: TypeId) TypeId { if (!ty.isBuiltin()) { const info = self.module.types.get(ty); if (info == .optional) return ty; } return self.module.types.optionalOf(ty); } /// Produce a zero/default value for any type — constInt(0) for integers, /// constNull for pointers, constUndef for structs/complex types. pub fn zeroValue(self: *Lowering, ty: TypeId) Ref { if (ty.isBuiltin()) return self.builder.constInt(0, ty); const info = self.module.types.get(ty); return switch (info) { // Arbitrary-width integer types (u1, u2, s4, ...) interned as // `.signed`/`.unsigned` variants — fall through `isBuiltin()`. .signed, .unsigned => self.builder.constInt(0, ty), .pointer, .tuple, .optional => self.builder.constNull(ty), .@"struct", .array, .slice, .many_pointer => self.builder.constNull(ty), else => self.builder.constUndef(ty), }; } /// Emit the unified non-integral float→int narrowing diagnostic (F0.11 / /// issue 0095). ONE wording, ONE place: every site that rejects an implicit /// narrowing of a non-integral compile-time float to an integer type calls /// this, so the message + fix-it stay identical across the typed-binding /// coerce arm, the field/param-default sites, the typed-const path, and the /// global-initializer path. pub fn diagNonIntegralNarrow(self: *Lowering, span: ast.Span, value: f64, dst_ty: TypeId) void { if (self.diagnostics) |d| d.addFmt(.err, span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ value, self.formatTypeName(dst_ty) }); } /// Lower a struct field default `default_expr`, coerced to the field type /// `field_ty`. A compile-time float default narrowing into an integer field /// follows the unified rule via `foldComptimeFloatInit`; everything else /// lowers under the field type as target and coerces at the IR level. pub fn lowerCoercedDefault(self: *Lowering, default_expr: *const Node, field_ty: TypeId) Ref { if (self.foldComptimeFloatInit(default_expr, field_ty)) |folded| return folded; const saved_tt = self.target_type; self.target_type = field_ty; const raw = self.lowerExpr(default_expr); self.target_type = saved_tt; return self.coerceToType(raw, self.builder.getRefType(raw), field_ty); } /// How a float→int conversion is treated. An IMPLICIT coercion (a typed /// binding initializer) folds an integral compile-time float to its int and /// REJECTS a non-integral one; an EXPLICIT `xx` / `cast` always truncates. const CoerceMode = enum { implicit, explicit }; /// Insert a conversion if src_ty and dst_ty differ. /// Handles int widening/narrowing, float widening/narrowing, and int↔float. /// IMPLICIT coercion — the typed-binding initializer path. A compile-time /// float narrowing to an integer folds when integral, errors when not. pub fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { return self.coerceMode(val, src_ty, dst_ty, .implicit); } /// EXPLICIT coercion — the `xx` / `cast(T)` escape hatch. A float→int here /// always truncates, bypassing the integral-fold / non-integral-error rule. pub fn coerceExplicit(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { return self.coerceMode(val, src_ty, dst_ty, .explicit); } pub fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mode: CoerceMode) Ref { // 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: 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| { const fv = self.builder.emit(.{ .tuple_get = .{ .base = val, .field_index = @intCast(i), .base_type = src_ty } }, sf); elems.append(self.alloc, self.coerceMode(fv, sf, df, mode)) 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) .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.coerceMode(unwrapped, child_ty, dst_ty, mode); }, // 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.coerceMode(val, src_ty, child_ty, mode); return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty); }, // 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); }, .int_to_float => return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty), .float_to_int => { // Implicit float→int narrowing follows the unified rule (the // same `floatToIntExact` the array-dim / `$K: Count` paths use): // a compile-time INTEGRAL float folds to its int, a NON-integral // one is a compile error. Explicit `xx` / `cast` (mode // `.explicit`) skips this and truncates. A runtime float has no // compile-time value to fold — it truncates as before. if (mode == .implicit) { if (self.builder.constFloatInfo(val)) |info| { if (program_index_mod.floatToIntExact(info.value)) |iv| { return self.builder.constInt(iv, dst_ty); } // Non-integral: diagnose, then fall through to the // truncating op below so lowering finishes and // `hasErrors()` aborts the build. self.diagNonIntegralNarrow(.{ .start = info.span.start, .end = info.span.end }, info.value, dst_ty); } } 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), .array_to_slice => return self.builder.emit(.{ .array_to_slice = .{ .operand = val } }, dst_ty), } } /// Apply C default argument promotion to variadic-tail args. These rules /// (bool/s8/s16/u8/u16 → s32, f32 → f64) match the C calling convention's /// implicit promotions when an argument is passed through `...`. pub fn promoteCVariadicArgs(self: *Lowering, args: []Ref, fixed_count: usize) void { if (args.len <= fixed_count) return; for (args[fixed_count..]) |*arg| { const src_ty = self.builder.getRefType(arg.*); const promoted: TypeId = switch (src_ty) { .bool, .s8, .s16, .u8, .u16 => .s32, .f32 => .f64, else => continue, }; arg.* = self.coerceToType(arg.*, src_ty, promoted); } } /// Coerce call arguments in-place to match function parameter types. pub fn coerceCallArgs(self: *Lowering, args: []Ref, params: []const Function.Param) void { for (0..@min(args.len, params.len)) |i| { const src_ty = self.builder.getRefType(args[i]); const dst_ty = params[i].ty; if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) { const src_info = self.module.types.get(src_ty); const dst_info = self.module.types.get(dst_ty); // Array → many_pointer decay: alloca the array, GEP to first element if (src_info == .array and dst_info == .many_pointer) { const slot = self.builder.alloca(src_ty); self.builder.store(slot, args[i]); const zero = self.builder.constInt(0, .s64); args[i] = self.builder.emit(.{ .index_gep = .{ .lhs = slot, .rhs = zero } }, dst_ty); continue; } // Implicit address-of: passing T value where *T is expected → alloca + store // Only when the pointee type matches the source type. if (dst_info == .pointer and src_info != .pointer and dst_info.pointer.pointee == src_ty) { const slot = self.builder.alloca(src_ty); self.builder.store(slot, args[i]); args[i] = slot; continue; } } args[i] = self.coerceToType(args[i], src_ty, dst_ty); } }