// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module abi import v2.mir import v2.pref pub enum X64Abi { sysv windows } const sysv_int_arg_reg_count = 6 const sysv_sse_arg_reg_count = 8 const sysv_max_direct_aggregate_size = 16 struct SysVLocationState { mut: int_regs int sse_regs int stack_slots int } // lower annotates MIR with ABI classification metadata. // Current scope is intentionally conservative: it classifies which arguments // and return values must be passed indirectly for each function. pub fn lower(mut m mir.Module, arch pref.Arch) { lower_with_x64_abi(mut m, arch, .sysv) } pub fn lower_with_x64_abi(mut m mir.Module, arch pref.Arch, x64_abi X64Abi) { is_x64 := arch == .x64 mut fn_by_name := map[string]int{} for i := 0; i < m.funcs.len; i++ { mut f := &m.funcs[i] fn_by_name[f.name] = i if is_x64 { f.abi_ret_class = abi_value_class(m, f.typ, arch, x64_abi) f.abi_ret_indirect = abi_class_is_indirect(f.abi_ret_class, m, f.typ, arch, x64_abi) } else { f.abi_ret_indirect = needs_indirect(m, f.typ, arch, x64_abi) } f.abi_param_class = []mir.AbiArgClass{len: f.params.len, init: .in_reg} if is_x64 { f.abi_param_classes = []mir.AbiValueClass{len: f.params.len} f.abi_param_layouts = []mir.AbiValueLayout{len: f.params.len} } mut param_loc_state := SysVLocationState{} if arch == .x64 && x64_abi == .sysv && f.abi_ret_indirect { param_loc_state.int_regs = 1 } for pi, param_id in f.params { if param_id < 0 || param_id >= m.values.len { continue } param_typ := m.values[param_id].typ if is_x64 { param_class := abi_value_class(m, param_typ, arch, x64_abi) f.abi_param_classes[pi] = param_class if x64_abi == .sysv { f.abi_param_layouts[pi] = sysv_assign_value_layout(param_class, mut param_loc_state) } if abi_class_is_indirect(param_class, m, param_typ, arch, x64_abi) { f.abi_param_class[pi] = .indirect } } else if needs_indirect(m, param_typ, arch, x64_abi) { f.abi_param_class[pi] = .indirect } } } lower_calls(mut m, arch, x64_abi, fn_by_name) } fn needs_indirect(m mir.Module, typ_id int, arch pref.Arch, x64_abi X64Abi) bool { ssa_mod := m.ssa() if ssa_mod == unsafe { nil } || typ_id <= 0 || typ_id >= ssa_mod.type_store.types.len { return false } typ := ssa_mod.type_store.types[typ_id] if typ.kind !in [.struct_t, .array_t] { return false } size := m.type_size(typ_id) return match arch { .arm64 { size > 16 } .x64 { match x64_abi { .sysv { size > sysv_max_direct_aggregate_size } .windows { size !in [1, 2, 4, 8] } } } else { size > 16 } } } fn abi_class_is_indirect(value_class mir.AbiValueClass, m mir.Module, typ_id int, arch pref.Arch, x64_abi X64Abi) bool { if value_class.mode == .indirect { return true } return needs_indirect(m, typ_id, arch, x64_abi) } fn abi_value_class(m mir.Module, typ_id int, arch pref.Arch, x64_abi X64Abi) mir.AbiValueClass { ssa_mod := m.ssa() size := m.type_size(typ_id) if ssa_mod == unsafe { nil } || typ_id <= 0 || typ_id >= ssa_mod.type_store.types.len { return mir.AbiValueClass{ mode: .direct size: size } } typ := ssa_mod.type_store.types[typ_id] if arch == .x64 && x64_abi == .sysv { if typ.kind !in [.struct_t, .array_t] { return sysv_scalar_abi_value_class(m, typ_id) } return sysv_abi_value_class(m, typ_id) } indirect := needs_indirect(m, typ_id, arch, x64_abi) return mir.AbiValueClass{ mode: if indirect { .indirect } else { .direct } size: size } } fn sysv_scalar_abi_value_class(m mir.Module, typ_id int) mir.AbiValueClass { ssa_mod := m.ssa() size := m.type_size(typ_id) if ssa_mod == unsafe { nil } || typ_id <= 0 || typ_id >= ssa_mod.type_store.types.len { return mir.AbiValueClass{ mode: .direct size: size } } typ := ssa_mod.type_store.types[typ_id] classes := match typ.kind { .int_t, .ptr_t, .func_t { [mir.AbiEightbyteClass.integer] } .float_t { match typ.width { 32, 64 { [mir.AbiEightbyteClass.sse] } 128 { [mir.AbiEightbyteClass.sse, .sseup] } else { []mir.AbiEightbyteClass{} } } } else { []mir.AbiEightbyteClass{} } } return mir.AbiValueClass{ mode: .direct size: size classes: classes } } fn sysv_abi_value_class(m mir.Module, typ_id int) mir.AbiValueClass { size := m.type_size(typ_id) if size <= 0 { return mir.AbiValueClass{ mode: .direct size: size } } if sysv_aggregate_must_be_memory_before_classification(size) { return sysv_memory_value_class(size) } mut classes := []mir.AbiEightbyteClass{len: (size + 7) / 8, init: .no_class} mut visiting := map[int]bool{} if !sysv_classify_type_into(m, typ_id, 0, mut classes, mut visiting) { return sysv_memory_value_class(size) } classes = sysv_post_merge_classes(size, classes) if classes.len == 1 && classes[0] == .memory { return sysv_memory_value_class(size) } return mir.AbiValueClass{ mode: .direct size: size classes: classes } } fn sysv_aggregate_must_be_memory_before_classification(size int) bool { // Current SSA aggregate types do not model a SysV vector aggregate larger // than two eightbytes, so larger struct/array aggregates cannot be passed // in registers and should not be classified element by element. return size > sysv_max_direct_aggregate_size } fn sysv_memory_value_class(size int) mir.AbiValueClass { return mir.AbiValueClass{ mode: .indirect size: size classes: [.memory] } } fn sysv_classify_type_into(m mir.Module, typ_id int, byte_offset int, mut classes []mir.AbiEightbyteClass, mut visiting map[int]bool) bool { ssa_mod := m.ssa() if ssa_mod == unsafe { nil } || typ_id <= 0 || typ_id >= ssa_mod.type_store.types.len { return true } if visiting[typ_id] { return sysv_merge_class_range(mut classes, byte_offset, 8, .integer) } visiting[typ_id] = true typ := ssa_mod.type_store.types[typ_id] ok := match typ.kind { .int_t, .ptr_t, .func_t { sysv_merge_class_range(mut classes, byte_offset, m.type_size(typ_id), .integer) } .float_t { match typ.width { 32, 64 { sysv_merge_class_range(mut classes, byte_offset, m.type_size(typ_id), .sse) } 80 { false } 128 { sysv_merge_class_range(mut classes, byte_offset, 8, .sse) && sysv_merge_class_range(mut classes, byte_offset + 8, 8, .sseup) } else { false } } } .array_t { sysv_classify_array_into(m, typ.elem_type, typ.len, byte_offset, mut classes, mut visiting) } .struct_t { sysv_classify_struct_into(m, typ.fields, byte_offset, mut classes, mut visiting) } .void_t, .label_t, .metadata_t { true } } visiting.delete(typ_id) return ok } fn sysv_classify_array_into(m mir.Module, elem_type int, len int, byte_offset int, mut classes []mir.AbiEightbyteClass, mut visiting map[int]bool) bool { elem_size := m.type_size(elem_type) for i := 0; i < len; i++ { if !sysv_classify_type_into(m, elem_type, byte_offset + i * elem_size, mut classes, mut visiting) { return false } } return true } fn sysv_classify_struct_into(m mir.Module, fields []int, byte_offset int, mut classes []mir.AbiEightbyteClass, mut visiting map[int]bool) bool { mut field_offset := 0 for field_typ in fields { align := m.type_align(field_typ) if align > 1 && field_offset % align != 0 { field_offset = (field_offset + align - 1) & ~(align - 1) } if !sysv_classify_type_into(m, field_typ, byte_offset + field_offset, mut classes, mut visiting) { return false } field_offset += m.type_size(field_typ) } return true } fn sysv_merge_class_range(mut classes []mir.AbiEightbyteClass, byte_offset int, size int, class mir.AbiEightbyteClass) bool { if size <= 0 { return true } start := byte_offset / 8 end := (byte_offset + size + 7) / 8 if start < 0 || end > classes.len { return false } for i := start; i < end; i++ { classes[i] = sysv_merge_eightbyte_class(classes[i], class) if classes[i] == .memory { return false } } return true } fn sysv_merge_eightbyte_class(a mir.AbiEightbyteClass, b mir.AbiEightbyteClass) mir.AbiEightbyteClass { if a == b { return a } if a == .no_class { return b } if b == .no_class { return a } if a == .memory || b == .memory { return .memory } if a == .integer || b == .integer { return .integer } return .sse } fn sysv_post_merge_classes(size int, input []mir.AbiEightbyteClass) []mir.AbiEightbyteClass { mut classes := input.clone() for class in classes { if class == .memory { return [.memory] } } if size > sysv_max_direct_aggregate_size { mut vector_like := classes.len > 0 && classes[0] == .sse for i := 1; i < classes.len; i++ { if classes[i] != .sseup { vector_like = false break } } if !vector_like { return [.memory] } } for i, class in classes { if class == .sseup && (i == 0 || classes[i - 1] !in [.sse, .sseup]) { classes[i] = .sse } } return classes } fn sysv_assign_value_layout(value_class mir.AbiValueClass, mut state SysVLocationState) mir.AbiValueLayout { if value_class.classes.len == 0 { return mir.AbiValueLayout{ value_class: value_class } } if value_class.mode == .indirect || value_class.classes == [mir.AbiEightbyteClass.memory] { return sysv_indirect_pointer_layout(value_class, mut state) } mut needed_int := 0 mut needed_sse := 0 for class in value_class.classes { match class { .integer { needed_int++ } .sse { needed_sse++ } else {} } } if state.int_regs + needed_int > sysv_int_arg_reg_count || state.sse_regs + needed_sse > sysv_sse_arg_reg_count { return sysv_stack_value_layout(value_class, mut state) } mut locs := []mir.AbiLocation{cap: value_class.classes.len} mut last_sse := -1 for i, class in value_class.classes { offset := i * 8 match class { .integer { locs << mir.AbiLocation{ kind: .int_reg index: state.int_regs offset: offset class: class } state.int_regs++ } .sse { last_sse = state.sse_regs locs << mir.AbiLocation{ kind: .sse_reg index: state.sse_regs offset: offset class: class } state.sse_regs++ } .sseup { locs << mir.AbiLocation{ kind: .sse_reg index: if last_sse >= 0 { last_sse } else { state.sse_regs } offset: offset class: class } } else { locs << mir.AbiLocation{ kind: .none index: -1 offset: offset class: class } } } } return mir.AbiValueLayout{ value_class: value_class locs: locs } } fn sysv_indirect_pointer_layout(value_class mir.AbiValueClass, mut state SysVLocationState) mir.AbiValueLayout { mut locs := []mir.AbiLocation{cap: 1} if state.int_regs < sysv_int_arg_reg_count { locs << mir.AbiLocation{ kind: .int_reg index: state.int_regs offset: 0 class: .integer } state.int_regs++ } else { locs << mir.AbiLocation{ kind: .stack index: state.stack_slots offset: 0 class: .integer } state.stack_slots++ } return mir.AbiValueLayout{ value_class: value_class locs: locs } } fn sysv_stack_value_layout(value_class mir.AbiValueClass, mut state SysVLocationState) mir.AbiValueLayout { slots := if value_class.size > 0 { (value_class.size + 7) / 8 } else { value_class.classes.len } mut locs := []mir.AbiLocation{cap: slots} for i := 0; i < slots; i++ { class := if i < value_class.classes.len { value_class.classes[i] } else { mir.AbiEightbyteClass.memory } locs << mir.AbiLocation{ kind: .stack index: state.stack_slots offset: i * 8 class: class } state.stack_slots++ } return mir.AbiValueLayout{ value_class: value_class locs: locs } } fn fallback_arg_type(m mir.Module, arg_id int) int { if arg_id < 0 || arg_id >= m.values.len { return 0 } if logical_typ := logical_arg_type_from_value(m, arg_id, 0) { return logical_typ } return m.values[arg_id].typ } fn logical_arg_type_from_value(m mir.Module, val_id int, depth int) ?int { if depth > 8 || val_id < 0 || val_id >= m.values.len { return none } val := m.values[val_id] if val.kind != .instruction { return none } instr := m.instrs[val.index] match instr.op { .alloca { if val.typ <= 0 || val.typ >= m.type_store.types.len { return none } typ := m.type_store.types[val.typ] if typ.kind != .ptr_t || typ.elem_type <= 0 || typ.elem_type >= m.type_store.types.len { return none } elem_typ := m.type_store.types[typ.elem_type] if elem_typ.kind in [.struct_t, .array_t] { return typ.elem_type } } .bitcast { if instr.operands.len > 0 { return logical_arg_type_from_value(m, instr.operands[0], depth + 1) } } .assign { if instr.operands.len > 1 { return logical_arg_type_from_value(m, instr.operands[1], depth + 1) } } else {} } return none } fn lower_calls(mut m mir.Module, arch pref.Arch, x64_abi X64Abi, fn_by_name map[string]int) { if arch !in [.arm64, .x64] { return } is_x64 := arch == .x64 for i := 0; i < m.instrs.len; i++ { mut instr := &m.instrs[i] if instr.op !in [.call, .call_indirect, .call_sret] || instr.operands.len == 0 { continue } ret_typ, sig_param_types := call_signature(m, instr, fn_by_name) ret_class := if is_x64 { abi_value_class(m, ret_typ, arch, x64_abi) } else { mir.AbiValueClass{} } ret_indirect := if is_x64 { abi_class_is_indirect(ret_class, m, ret_typ, arch, x64_abi) } else { needs_indirect(m, ret_typ, arch, x64_abi) } num_args := instr.operands.len - 1 instr.abi_arg_class = []mir.AbiArgClass{len: num_args, init: .in_reg} if is_x64 { instr.abi_arg_classes = []mir.AbiValueClass{len: num_args} instr.abi_arg_layouts = []mir.AbiValueLayout{len: num_args} } mut arg_loc_state := SysVLocationState{} if arch == .x64 && x64_abi == .sysv && ret_indirect { arg_loc_state.int_regs = 1 } for arg_idx := 0; arg_idx < num_args; arg_idx++ { mut arg_typ := 0 if arg_idx < sig_param_types.len && sig_param_types[arg_idx] > 0 { arg_typ = sig_param_types[arg_idx] } else { arg_id := instr.operands[arg_idx + 1] arg_typ = fallback_arg_type(m, arg_id) } if is_x64 { arg_class := abi_value_class(m, arg_typ, arch, x64_abi) instr.abi_arg_classes[arg_idx] = arg_class if x64_abi == .sysv { instr.abi_arg_layouts[arg_idx] = sysv_assign_value_layout(arg_class, mut arg_loc_state) } if abi_class_is_indirect(arg_class, m, arg_typ, arch, x64_abi) { instr.abi_arg_class[arg_idx] = .indirect } } else if needs_indirect(m, arg_typ, arch, x64_abi) { instr.abi_arg_class[arg_idx] = .indirect } } instr.abi_ret_class = ret_class instr.abi_ret_indirect = ret_indirect // Lower ABI-indirect returns to call_sret for backend consumption. if instr.abi_ret_indirect { instr.op = .call_sret } } } fn call_signature(m mir.Module, instr &mir.Instruction, fn_by_name map[string]int) (int, []int) { mut ret_typ := instr.typ mut param_types := []int{} if instr.operands.len == 0 { return ret_typ, param_types } if instr.op in [.call, .call_sret] { callee_id := instr.operands[0] if callee_id >= 0 && callee_id < m.values.len { callee_name := m.values[callee_id].name if callee_name != '' && callee_name in fn_by_name { fn_idx := fn_by_name[callee_name] if fn_idx >= 0 && fn_idx < m.funcs.len { callee := m.funcs[fn_idx] ret_typ = callee.typ param_types = []int{len: callee.params.len} for i, pid in callee.params { if pid >= 0 && pid < m.values.len { param_types[i] = m.values[pid].typ } } } } } } else if instr.op == .call_indirect { callee_id := instr.operands[0] if callee_id >= 0 && callee_id < m.values.len { fn_ptr_typ_id := m.values[callee_id].typ if fn_ptr_typ_id > 0 && fn_ptr_typ_id < m.type_store.types.len { fn_ptr_typ := m.type_store.types[fn_ptr_typ_id] if fn_ptr_typ.kind == .ptr_t && fn_ptr_typ.elem_type > 0 && fn_ptr_typ.elem_type < m.type_store.types.len { fn_typ := m.type_store.types[fn_ptr_typ.elem_type] if fn_typ.kind == .func_t { ret_typ = fn_typ.ret_type param_types = fn_typ.params.clone() } } else if fn_ptr_typ.kind == .func_t { // Function pointer extracted from struct field via extractvalue // has func_t type directly (not wrapped in ptr_t). ret_typ = fn_ptr_typ.ret_type param_types = fn_ptr_typ.params.clone() } } } } // Fallback to call operand types when we could not resolve a signature. if param_types.len == 0 { num_args := if instr.operands.len > 0 { instr.operands.len - 1 } else { 0 } param_types = []int{len: num_args} for i := 0; i < num_args; i++ { arg_id := instr.operands[i + 1] param_types[i] = fallback_arg_type(m, arg_id) } } return ret_typ, param_types }