kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit e4e0e56721a23e85259656d3677a4386f6963ee8
parent ee05a9bd27505aa0728952971e0a686ccecbfd75
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sun, 19 Jul 2026 12:40:24 -0700

refactor(native): centralize scratch and clobber ownership

Make physical register roles and shared policy lists the single source of truth across O0 and optimized native codegen. Centralize inline-asm allocation, callee-save merging, call marshalling names, and exhaustive machine effects while keeping emitter reservations policy-owned.

Fix AAPCS32 aggregate passing/returns, harden exact-width x64 scratch handling, include ARM/RISC-V backend coverage, and isolate cross-test target state.

Diffstat:
Mdoc/ARCH.md | 52+++++++++++++++++++++++++++++++++++-----------------
Mdoc/BACKEND_REFACTOR_CROSS_TEST.md | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdoc/INTERFACES.md | 15++++++++-------
Mdoc/OPT.md | 7++++---
Mdoc/plan/ARM32.md | 21+++++++++++++++++++--
Mscripts/cross_test.sh | 19++++++++++++++++---
Msrc/abi/abi.h | 7++++++-
Msrc/abi/abi_aapcs32.c | 29+++++++++++++++++------------
Msrc/abi/abi_aapcs64.c | 2+-
Msrc/abi/abi_aapcs64_windows.c | 4++--
Msrc/abi/abi_rv64.c | 4++--
Msrc/abi/abi_sysv_x64.c | 2+-
Msrc/arch/aa64/native.c | 208+++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/arch/arm32/native.c | 158++++++++++++++++++++++++++++++++++++++-----------------------------------------
Asrc/arch/native_target.c | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/native_target.h | 116++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/arch/riscv/native.c | 212++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/arch/x64/emit.h | 2+-
Msrc/arch/x64/native.c | 492+++++++++++++++++++++++++++++++++----------------------------------------------
Msrc/cg/native_argmove.c | 27++++++++++++++++++++++++++-
Msrc/cg/native_argmove.h | 6++++--
Msrc/cg/native_asm.c | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/cg/native_asm.h | 23+++++++++++++----------
Msrc/cg/native_direct_target.c | 32+++++++++++++++++---------------
Msrc/cg/native_direct_target.h | 20++++++++++----------
Msrc/opt/ir.h | 9++++-----
Msrc/opt/pass_lower.c | 6+++---
Msrc/opt/pass_machinize.c | 115++++++++++---------------------------------------------------------------------
Msrc/opt/pass_native_emit.c | 32++++++++++++++++----------------
Mtest/api/abi_classify_test.c | 157++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtest/arch/arm32_inline_test.c | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/cg/native_direct_target_test.c | 128++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mtest/opt/frame_value_backend_test.c | 12++++++------
Mtest/opt/location_mir_test.c | 14++++++++------
Mtest/opt/native_emit_frame_dst_test.c | 65++++++++++++++++++++++++++++++++++-------------------------------
Mtest/opt/reg_effects_test.c | 164++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtest/parse/run.sh | 17++++++++++++++++-
37 files changed, 1705 insertions(+), 955 deletions(-)

diff --git a/doc/ARCH.md b/doc/ARCH.md @@ -130,7 +130,7 @@ It is driven from two directions: is the `CgTarget`. It owns semantic local homes, a small register cache, and conservative flushes, and lowers each semantic op directly into `NativeTarget` hook calls. The arch supplies a `NativeTarget` plus a small - semantic adapter, `NativeOps` (bind_param, plan_call, emit_call/ret, va_*, + semantic adapter, `NativeOps` (bind_param, marshal_call, emit_call/ret, va_*, asm_block, barriers, legality predicates) — the parts that need a foot in the semantic world. Everything else (frame slots, `class_for_type`, `addr_legal`) the direct target calls straight through to `NativeTarget`. @@ -146,6 +146,16 @@ It is driven from two directions: `NativeLoc`s to the backend. `-O2` currently selects this same path; the maintained SSA schedule is not publicly reachable yet. + Both paths consume the same validated `NativeRegInfo`. Physical-register + role flags are the single authority for static allocability, reservation, + argument/return use, and caller/callee preservation; mask consumers derive + their view from those rows. A target may override only caller/callee masks + when the live OS ABI changes preservation (Win64 x64). Ordered policy lists + separately describe O0 allocation, backend scratch, optimized emit/asm temp + banks, and direct inline-asm allocation. Shared validation runs at both the + direct and optimized adapter boundaries, while the shared asm binder owns + selection from the target-declared inline-asm pool. + A physical address may retain a frame-resident base or index as `NATIVE_ADDR_BASE_FRAME_VALUE` / `NATIVE_ADDR_INDEX_FRAME_VALUE`. Its `base_type` / `index_type` is the exact scalar type to load, not merely the @@ -171,11 +181,12 @@ generators byte-compatible per arch. The hook families: `store`, `tls_addr_of`, `copy_bytes`/`set_bytes` (aggregate memcpy/memset), `bitfield_load`/`bitfield_store`, `spill`/`reload`. - **Arithmetic.** `binop`, `unop`, `cmp`, `convert`, `alloca_`. -- **Calls & returns.** A marshalling/emission split: `plan_call` is deliberately - mutating. It fills a `NativeCallPlan` and performs target-owned pre-call work +- **Calls & returns.** A marshalling/emission split: `marshal_call` is + deliberately mutating. It fills a `NativeCallPhase` and performs target-owned + pre-call work such as indirect-callee staging, outgoing stack stores, and register - parallel copies; the shared caller realizes any remaining plan argument - moves. `emit_call` emits the actual control transfer. `plan_ret` describes + parallel copies; the shared caller realizes any remaining phase argument + moves. `emit_call` emits the actual control transfer. `marshal_ret` describes and may perform target-owned return marshalling, then returns any remaining ordered moves for the caller to realize before `ret`. `call_stack_bytes` and `signature_stack_bytes` are the pure pre-pass queries used to size the @@ -309,8 +320,8 @@ Each native backend declares its register file as static `NativeRegInfo` data in its `native.c` (e.g. `aa_reg_info`, wired into the `NativeTarget` at construction; the DWARF-index ↔ assembler-name tables that the `ArchImpl` exposes for objdump/asm live separately in `regs.c`). A `NativeRegInfo` is a set -of `NativeAllocClassInfo` (one per `NativeAllocClass`: INT, FP, VEC), each -carrying: +of `NativeAllocClassInfo` (one per `NativeAllocClass`: INT, FP, VEC), keyed by +its `cls` field (table position is not semantic), with each class carrying: - an ordered O0 **ndt_allocable** list — the `NativeDirectTarget` value-cache bank and preference order; it is never an O1 allocator input; @@ -321,6 +332,8 @@ carrying: locations, not backend-private implementation registers; - an **asm_temps** list — a disjoint backend-internal bank the optimized emitter may borrow only while staging a compatible inline-asm block; +- a **direct_asm_allocable** list — the independent ordered register policy for + operands self-allocated by the O0 inline-asm binder; - an **emit_cache_mask** — the subset of `emit_temps` whose clean spill values an emitter may retain across ordinary hooks within one block without an ABI save; a dead O1 allocation register may also become a cache entry when its @@ -328,7 +341,9 @@ carrying: - a `NativePhysRegInfo` row per physical register (class, ABI arg/ret index, caller/callee-saved flags, the independent `NATIVE_REG_ALLOCABLE` O1 flag, and spill/copy costs); -- precomputed caller/callee/arg/ret/reserved bitmasks. +- caller/callee/arg/ret/reserved role masks derived from those rows (with a + live-ABI override where one architecture supports multiple preservation + conventions). This one declaration describes **both** code paths without making either path's register bank an input to the other: @@ -392,18 +407,21 @@ therefore enumerates the cross-product cells that both sides enable: | aa64 | `aapcs64` | `apple_arm64` | `aapcs64_windows` | | x64 | `sysv_x64` | `apple_x64` | `win64_x64` | | rv64 | `rv64` | — | — | +| rv32 | `rv32` | — | — | +| arm32 | `aapcs32` | — | — | | wasm | — (`wasm32`, via the wasm object format) | Each per-ABI TU (`abi_aapcs64.c`, `abi_sysv_x64.c`, `abi_apple_arm64.c`, -`abi_apple_x64.c`, `abi_win64_x64.c`, `abi_aapcs64_windows.c`, `abi_rv64.c`) -implements its `compute_func_info` and `va_list` facts; the Apple/Windows -variants encode their divergences (e.g. Apple ARM64 routes the variadic tail -exclusively to the stack, recorded as `vararg_on_stack` in `ABIFuncInfo`). The -classification is the *only* authority — the `NativeTarget` plan/bind hooks and -the optimizer both consume `ABIFuncInfo`; they never re-derive argument -placement. Frame-relevant ABI facts (the vararg register-save-area size) are -funneled through `src/cg/native_frame.c` so the per-arch magic numbers all trace -back to one `va_list`-layout query. +`abi_apple_x64.c`, `abi_win64_x64.c`, `abi_aapcs64_windows.c`, `abi_rv64.c`, +and `abi_aapcs32.c`) implements its `compute_func_info` and `va_list` facts; +the RISC-V TU supplies both XLEN variants, and the Apple/Windows variants encode +their divergences (e.g. Apple ARM64 routes the variadic tail exclusively to the +stack, recorded as `vararg_on_stack` in `ABIFuncInfo`). The classification is +the *only* authority — the `NativeTarget` plan/bind hooks and the optimizer both +consume `ABIFuncInfo`; they never re-derive argument placement. Frame-relevant +ABI facts (the vararg register-save-area size) are funneled through +`src/cg/native_frame.c` so the per-arch magic numbers all trace back to one +`va_list`-layout query. ## 8. Per-call cost model (aa64 -O1) diff --git a/doc/BACKEND_REFACTOR_CROSS_TEST.md b/doc/BACKEND_REFACTOR_CROSS_TEST.md @@ -68,3 +68,164 @@ Targeted validation after the fixes: green at O0/O1 on x86-64 and AArch64. This was a targeted RV64 follow-up, not a repeat of the full 19-target matrix. + +## RV32 follow-up + +RV32 uses the same RISC-V NativeTarget implementation as RV64, with `xlen=32`, +so the follow-up emphasized the refactor-sensitive shared paths: temporary and +clobber ownership, intrinsic operand locations, call marshalling, small +aggregate arguments, TLS, and both RV32 ABIs used by the test harness. + +The original full cross log was already clean for RV32: the Toy corpus passed +427 cases with two intentional AArch64-only skips, and the parser corpus passed +970 cases with 64 intentional skips. Current-tree validation added: + +- Targeted parser cases for signed-overflow intrinsics, initialized TLS, + instruction-temporary boundaries, machine-clobbered returns, and small + aggregate argument spills pass at O0 and O1. +- The Toy bit/overflow intrinsic case passes at O0 and O1. +- `test-smoke-rv32` passes all 20 checks. This covers hard-float `ilp32f` and + soft-float `ilp32` at O0/O1, automatic runtime linking, concurrent runtime + cache lanes, ELF flags, mixed-ABI rejection, and the negative control. +- The RV32 assembler corpus passes all 16 cases and the decoder unit passes all + 96 checks. The JIT image/lookup test builds and validates its bytes; native + execution is intentionally skipped on the non-RV32 host. +- The current full Toy run again passes 427 cases with the same two intentional + skips. The full parser run recorded 969 passes and one QEMU timeout, plus the + same 64 intentional skips. The timed-out O0 `6_5_45_ptr_eq_null` artifact + disassembles correctly, its saved ELF returns 3 when run directly, and an + isolated rerun passes at both O0 and O1. This is a transient QEMU/harness + timeout rather than a reproducible backend failure; the heavy corpus was not + rerun merely to replace that result. +- `test-cross TARGET=freestanding-rv32 DEPTH=smoke KIT_VM=1` passes all three + compile, link, and execution lane checks. + +No RV32-specific or shared production-code change was required. The shared +immediate-location correction made during the RV64 follow-up remains the +appropriate architectural fix and is covered by the RV32 overflow probes. + +## ARM32 follow-up + +The clean `8d7927db` revision reproduced both ARM32 failures from the original +matrix exactly: `130_record_sret_return` in the Toy corpus and +`6_8_6_4_05_struct_return_mixed_fp_int` in the C corpus passed at O0, while O1 +aborted after the first three ABI result registers made a fourth temporary +unavailable. The shared native emitter must load a frame/stack return part +directly into its final ABI register; routing it through a temporary creates a +false fifth-register requirement. The current emitter does that direct load. +The shared regression now fills an entire four-register result bank, matching +the ARM32 failure rather than testing only two parts. + +The audit also found two independent contract bugs that the original corpus +could not expose reliably because Kit was both caller and callee: + +- The AAPCS32 classifier treated arguments and results symmetrically at a + 16-byte cutoff. [AAPCS32](https://github.com/ARM-software/abi-aa/blob/main/aapcs32/aapcs32.rst) + instead returns composites larger than one word through caller-provided + storage, while every statically sized composite argument remains a by-value + memory image split across r0-r3 and the stack. Classification now represents + that asymmetry directly. `ABIArgInfo.nparts` is consequently 32-bit: a large + fixed-size argument cannot silently truncate at 65,535 word lanes. +- Known-frame prologues did not include callee-saved registers named by ARM32 + O1 inline-asm clobbers. Parsing the raw target clobbers, expanding the shared + ABI clobber sets, and merging the result with allocator-used callee saves now + lives in `cg/native_asm.c`; AA64, x64, RISC-V, and ARM32 provide only their + register-name parser and target preservation predicate. An ARM32 O1 test + verifies that a named r4 clobber produces a matching save/restore, and r7, + sp, and pc are rejected as structural clobbers. + +The shared direct-asm binder also used an I64 location as the address carrier +for every memory operand. It now selects I32 or I64 from the target pointer +width, keeping this target-neutral path valid on ARM32 and RV32. + +Validation on the corrected tree: + +- The two historical cases pass at O0 and O1. Focused AAPCS cases covering + 3-byte tails, 4/8/16/32-byte records, 8-byte alignment, and a 65,537-word + argument pass; the ABI classifier unit is 495/0. +- Two-way Clang interoperability passes at O0 and O1: a Clang caller consumes + Kit's 8-byte composite return and 32-byte by-value argument, and a Kit caller + consumes the same interfaces implemented by Clang. +- Shared native-boundary units pass: register effects 102/0, direct target 55, + location MIR 57/0, full-bank frame-result emission 335/0, and frame-value + backends 316/0. All four native inline-asm units pass. ARM32 ISA decoding is + 583/0 and target-feature coverage is 32/0. +- `test-cross TARGET=freestanding-arm32 DEPTH=full KIT_VM=1 RUN=1` passed all + eight compile/link/smoke/amalgam/Toy lane checks; Toy was 427 pass, 0 fail, + 2 intentional skips. The parser batch recorded 966 pass, 2 QEMU timeouts, + and 66 intentional skips. Both timed-out O0 artifacts + (`6_5_9_01_suppressed_null_ptr_cmp`, `6_6_01_enum_const`) and their O1 forms + passed immediately in isolated serial runs (4/0, 0.28-0.92 seconds), so the + only non-green full-lane verdict is a transient harness timeout rather than a + reproducible backend failure. The heavy corpus was not rerun merely to erase + that result. + +Artifacts: + +- `build/test-logs/test-cross-freestanding-arm32-full.log` +- `build/test-logs/arm32-timeout-isolated.log` + +## X64 follow-up + +The hosted x64 failures exposed a broader ownership problem than the original +R11 aggregate-copy corruption. Exact-width 3/5/6/7-byte loads and stores chose +address and data temporaries through backend-global `part_scratch` state while +the generic x64 memory emitter independently borrowed the same private bank. +Those operations now stabilize the complete effective address in an explicitly +selected private register for their local phase and use the other private +register for chunk data. The backend no longer carries `part_scratch`. + +Call setup had the same hidden-state shape. Stack bias, parallel-copy scratch, +tail-call state, and indirect-callee parking now live in a local +`X64CallMarshalCtx`; the old backend-global `call_callee_parked` flag is gone. +The mutating interface is named `marshal_call`/`marshal_ret` and fills a +`NativeCallPhase`, making its ownership and lifetime distinct from the pure +`call_stack_bytes` frame-sizing query. Dead call-plan clobber/return mask fields +were removed. + +The cross-architecture audit then consolidated the shared scratch/clobber +model: + +- `NativePhysRegInfo.flags` is the single static authority for allocability, + reservation, argument/return roles, and caller/callee preservation. Role + masks are derived centrally. The only live-ABI override is x64 + caller/callee preservation, where SysV/Apple and Win64 differ. +- A shared `native_reg_info_validate()` checks physical rows, ordered policy + lists, role contradictions, temp-bank separation, asm-temp reservation, + emit-temp non-allocability, and emitter cache containment at both the O0 + direct-target and O1 machinization boundaries. Machinization separately adds + the policy-owned emitter bank to each function's reserved set. +- O0 inline-asm register selection and ABI-clobber expansion live in + `cg/native_asm.c`; targets declare only their ordered operand pool and retain + target-specific constraint parsing/emission. Known-frame asm/callee-save + merging is shared there as well. +- Parallel argument-copy scratch is class-tagged explicitly, so physical + register zero cannot double as “unconfigured”; cycle breaking rejects a + missing, invalid, or live-aliased scratch register. +- Machine-operation effects are exhaustive through `NATIVE_MOP_COUNT`. Focused + x64 coverage includes division/remainder, variable shifts, bitfields, + atomics, variadics, intrinsics, syscalls, and TLS. Exact role-mask tests cover + SysV x64, dynamic Win64 preservation, AA64, RV64, and ARM32; they found and + corrected missing argument-role flags on x64 XMM6/XMM7. + +Two cross-harness ownership bugs were also corrected while validating x64: +FreeBSD/Windows full selectors now pass only the requested VM architecture, +and parse startup objects are cached per architecture rather than sharing a +mutable `parse_start.o` across concurrent workers. The x64 execution lane now +classifies the IEEE-binary128 semantic fixtures as inapplicable (while retaining +the layout/macro probe), matching the actual SysV/Apple x87 and Win64 +double-aliased `long double` ABIs. + +Final validation, in the requested platform order: + +- macOS x64: Mach-O build plus direct and queued Rosetta execution, 3/3. +- Linux x64, glibc + musl, full depth: 42/42 cross lanes. Each target passed + 427 Toy cases and 1,004 parser executions; glibc passed 9 libc cases and musl + passed 18. Only the documented AA64-only and inapplicable x64 binary128 cases + skipped. +- FreeBSD x64, full depth: 13/13 cross lanes and 668/668 applicable Toy cases. +- Windows x64, full depth: 13/13 cross lanes and 334/334 applicable Toy cases, + including FP callee saves, typed-asm ABI clobbers, indirect packed-stack + musttail, aggregate returns, variadics, and O0/O1 paths. +- Shared focused units: 865/865 checks plus all x64/AA64/RV64/ARM32 inline-asm + units. Tier-1 optimizer and architecture suites both pass. diff --git a/doc/INTERFACES.md b/doc/INTERFACES.md @@ -155,10 +155,10 @@ its raw word/halfword emit helpers for the assembler path: **Backend-tier notes:** - `NativeTarget` is the physical-emission contract: frame setup and prologue policy, control flow, data movement, arithmetic/compare/convert, calls - (`plan_call`/`emit_call`/`plan_ret`/`ret`), atomics, variadics, intrinsics, and - inline/file-scope asm — roughly three dozen hooks. The caller (NDT or the - optimizer) owns program-value placement and supplies legal physical - locations. A backend may materialize exact-typed `FRAME_VALUE` address + (`marshal_call`/`emit_call`/`marshal_ret`/`ret`), atomics, variadics, + intrinsics, and inline/file-scope asm — roughly three dozen hooks. The caller + (NDT or the optimizer) owns program-value placement and supplies legal + physical locations. A backend may materialize exact-typed `FRAME_VALUE` address components using the explicit destination or fixed backend-private implementation temps, but it must never allocate a persistent program value or borrow a caller-owned register outside that hook. All four native backends @@ -167,9 +167,10 @@ its raw word/halfword emit helpers for the assembler path: semantic shims the -O0 adapter calls). Keep the split clean: semantic decisions and semantic-to-physical adaptation live in `NativeOps`; physical marshalling, validation, and instruction emission live in `NativeTarget`. - `plan_call` is an intentionally mutating marshalling phase: it fills the plan - while performing target-owned callee/argument staging and stack stores. - `plan_ret` is the corresponding mutating return-marshalling phase; + `marshal_call` is an intentionally mutating marshalling phase: it fills the + `NativeCallPhase` while performing target-owned callee/argument staging and + stack stores. + `marshal_ret` is the corresponding mutating return-marshalling phase; `call_stack_bytes` is the pure frame-sizing query. - A handful of `NativeTarget` hooks are explicitly optional and exist for archs whose ISA needs them — `machine_op_clobbers` (including x64 division, shifts, diff --git a/doc/OPT.md b/doc/OPT.md @@ -600,9 +600,10 @@ operand materialization; after that bank is exhausted it may scavenge a caller-saved O1 allocation register only when physical liveness proves it dead for the instruction. Multiple references to the same location within the instruction share a scoped materialization. Calls have a mutating marshalling -phase: `plan_call` may stage the callee, store stack arguments, and perform -target-owned argument shuffles while it fills the plan. Each completed ABI -register destination remains phase-owned through the full multi-move phase. +phase: `marshal_call` may stage the callee, store stack arguments, and perform +target-owned argument shuffles while it fills the `NativeCallPhase`. Each +completed ABI register destination remains phase-owned through the full +multi-move phase. After `emit_call`, a phase barrier discards argument/callee staging before return writeback begins. Inline asm may additionally borrow `asm_temps` only under the target-declared constraints; its staging remains local to that asm diff --git a/doc/plan/ARM32.md b/doc/plan/ARM32.md @@ -11,6 +11,23 @@ emit `.ARM.attributes`, ARM32 `.thumb_func` symbols carry the ABI Thumb bit, and C inline asm templates now route real Thumb-2 mnemonics through `src/arch/arm32/asm.c`. +### Backend-refactor audit — 2026-07-19 + +The post-`8d7927db` audit is complete. The two O1 aggregate-return failures were +reproduced on that revision and are covered by a shared full-four-register +return regression; frame/stack parts load directly into their final ABI result +registers, without consuming an extra instruction-local temporary. + +The audit corrected the soft-float AAPCS32 aggregate contract: composites larger +than one word return indirectly through r0, while fixed-size composite +parameters of any size remain by-value across r0-r3 and the stack. Independent +Clang↔Kit tests cover both directions for an 8-byte return and a 32-byte +argument at O0/O1. Known-frame inline-asm clobber preservation is now shared +across native targets and covers ARM32 r4; structural r7/sp/pc clobbers are +rejected. See [BACKEND_REFACTOR_CROSS_TEST.md](../BACKEND_REFACTOR_CROSS_TEST.md#arm32-follow-up) +for the full evidence and the two isolated-green QEMU timeout artifacts from the +full ARM32 cross run. + ### Follow-on landed — 2026-06-17 — DSP, flag/macro parsing, code density, disasm Four follow-on items landed (parallel worktree agents, serial cherry-pick behind @@ -951,7 +968,7 @@ the internal seams — `NativeTarget` (`src/arch/native_target.h`), `ArchImpl` (`src/arch/arch.h`), the ABI vtable (`src/abi/abi_internal.h`), and `reloc_apply_insn`. Each later track lives behind exactly one of them: -- frame/ABI work behind `func_begin` / `plan_call` / `spill` / `frame_slot` +- frame/ABI work behind `func_begin` / `marshal_call` / `spill` / `frame_slot` - op-group work behind `binop` / `emit` + the isa table - reloc work behind `arm32_reloc_apply_insn` + the descriptor table - runtime entirely in `rt/` @@ -1013,7 +1030,7 @@ were built by parallel agents. ### Phase 2 — remaining tracks -Same seams as before (`func_begin`/`plan_call`/`spill`; `binop`/`emit` + the isa +Same seams as before (`func_begin`/`marshal_call`/`spill`; `binop`/`emit` + the isa table; `arm32_reloc_apply_insn`; `rt/`). Most of `native.c`/`abi`/`reloc` is in place; the unimplemented ops `compiler_panic` with a clear message. Gate stays **run-correctness under qemu**, reds left red. Op-groups contend on `native.c` / diff --git a/scripts/cross_test.sh b/scripts/cross_test.sh @@ -308,7 +308,7 @@ _full_lane() { } full_token() { - local t="$1" os arch libc + local t="$1" os arch libc vm_arch os="$(tok_os "$t")"; arch="$(tok_arch "$t")"; libc="$(tok_libc "$t")" case "$os" in linux) @@ -331,10 +331,23 @@ full_token() { esac ;; freebsd) - _full_lane "$t:toy-vm" env KIT="$KIT" bash "$ROOT/test/toy/vm.sh" freebsd + case "$arch" in + x64) vm_arch=amd64 ;; + aa64) vm_arch=aarch64 ;; + rv64) vm_arch=riscv64 ;; + esac + _full_lane "$t:toy-vm" \ + env KIT="$KIT" KIT_TOY_FREEBSD_ARCHES="$vm_arch" \ + bash "$ROOT/test/toy/vm.sh" freebsd ;; windows) - _full_lane "$t:toy-vm" env KIT="$KIT" bash "$ROOT/test/toy/vm.sh" windows + case "$arch" in + x64) vm_arch=x64 ;; + aa64) vm_arch=aarch64 ;; + esac + _full_lane "$t:toy-vm" \ + env KIT="$KIT" KIT_TOY_WINDOWS_ARCHES="$vm_arch" \ + bash "$ROOT/test/toy/vm.sh" windows ;; freestanding) case "$arch" in diff --git a/src/abi/abi.h b/src/abi/abi.h @@ -108,7 +108,12 @@ typedef struct ABIArgPart { typedef struct ABIArgInfo { u8 kind; /* ABIArgKind */ u8 flags; /* ABIArgFlag applying to the whole argument */ - u16 nparts; + u16 reserved; + /* A fixed-size by-value aggregate is not bounded by the argument-register + * bank: its remaining word parts continue on the stack. Keep this count + * wide enough to describe the full object rather than truncating at 64K + * carrier lanes. */ + u32 nparts; u32 indirect_align; /* required alignment for ABI_ARG_INDIRECT/byval copy */ const ABIArgPart* parts; } ABIArgInfo; diff --git a/src/abi/abi_aapcs32.c b/src/abi/abi_aapcs32.c @@ -7,8 +7,10 @@ * The 8-byte even-register / 8-byte-stack alignment that * AAPCS requires is surfaced to the native backend via * ABIArgPart.align; this layer just declares two lanes. - * aggregate <= 16B -> DIRECT, up to four word parts (r0..r3) - * aggregate > 16B -> INDIRECT (sret pointer in r0 / byval) + * aggregate return <= 4B -> DIRECT, one INT part in r0 + * aggregate return > 4B -> INDIRECT (sret pointer in r0) + * fixed-size aggregate argument -> DIRECT word parts, filled through + * r0..r3 and then the stack * * The sret pointer is passed in r0 (the first integer arg register), consuming * that slot, so sret_consumes_int_arg = 1 (like RISC-V, unlike AArch64's x8). @@ -21,8 +23,7 @@ #include "core/arena.h" #include "core/core.h" -#define ARM32_GPR_BYTES 4u /* r0..r3 are 32-bit */ -#define ARM32_AGG_GPR_BYTES 16u /* aggregates <= 4 words pass in registers */ +#define ARM32_GPR_BYTES 4u /* r0..r3 are 32-bit */ static void classify_scalar(TargetABI* a, KitCgTypeId t, ABIArgInfo* out) { ABITypeInfo ti = abi_cg_type_info(a, t); @@ -72,7 +73,17 @@ static void classify_aggregate(TargetABI* a, KitCgTypeId t, ABIArgInfo* out, abi_classify_void(out); return; } - if (ti.size <= ARM32_AGG_GPR_BYTES) { + /* AAPCS32 treats results and parameters asymmetrically. Composite results + * larger than one word use caller-provided result storage, while every + * statically sized composite parameter remains a by-value memory image that + * may occupy any number of consecutive core-register/stack words. */ + if (is_return && ti.size > ARM32_GPR_BYTES) { + out->kind = ABI_ARG_INDIRECT; + out->flags = ABI_AF_SRET; + out->indirect_align = ti.align ? ti.align : ARM32_GPR_BYTES; + out->parts = NULL; + out->nparts = 0; + } else { u32 nparts = (ti.size + ARM32_GPR_BYTES - 1u) / ARM32_GPR_BYTES; ABIArgPart* parts = arena_array(a->c->tu, ABIArgPart, nparts); u32 off = 0; @@ -99,14 +110,8 @@ static void classify_aggregate(TargetABI* a, KitCgTypeId t, ABIArgInfo* out, out->kind = ABI_ARG_DIRECT; out->flags = ABI_AF_NONE; out->parts = parts; - out->nparts = (u16)nparts; + out->nparts = nparts; out->indirect_align = 0; - } else { - out->kind = ABI_ARG_INDIRECT; - out->flags = is_return ? ABI_AF_SRET : ABI_AF_BYVAL; - out->indirect_align = ti.align ? ti.align : ARM32_GPR_BYTES; - out->parts = NULL; - out->nparts = 0; } } diff --git a/src/abi/abi_aapcs64.c b/src/abi/abi_aapcs64.c @@ -54,7 +54,7 @@ static void classify_aggregate(TargetABI* a, KitCgTypeId t, ABIArgInfo* out, out->kind = ABI_ARG_DIRECT; out->flags = ABI_AF_NONE; out->parts = parts; - out->nparts = (u16)nparts; + out->nparts = nparts; out->indirect_align = 0; } else { out->kind = ABI_ARG_INDIRECT; diff --git a/src/abi/abi_aapcs64_windows.c b/src/abi/abi_aapcs64_windows.c @@ -27,7 +27,7 @@ extern ABIFuncInfo* aapcs64_compute_func_info(TargetABI*, KitCgTypeId); static void remap_fp_parts_to_int(TargetABI* a, ABIArgInfo* ai) { if (!ai || ai->kind != ABI_ARG_DIRECT || ai->nparts == 0) return; int needs_copy = 0; - for (u16 i = 0; i < ai->nparts; ++i) { + for (u32 i = 0; i < ai->nparts; ++i) { if (ai->parts[i].cls == ABI_CLASS_FP) { needs_copy = 1; break; @@ -37,7 +37,7 @@ static void remap_fp_parts_to_int(TargetABI* a, ABIArgInfo* ai) { ABIArgPart* parts = arena_array(a->c->tu, ABIArgPart, ai->nparts); memcpy(parts, ai->parts, sizeof(ABIArgPart) * ai->nparts); - for (u16 i = 0; i < ai->nparts; ++i) { + for (u32 i = 0; i < ai->nparts; ++i) { if (parts[i].cls == ABI_CLASS_FP) parts[i].cls = ABI_CLASS_INT; } ai->parts = parts; diff --git a/src/abi/abi_rv64.c b/src/abi/abi_rv64.c @@ -213,7 +213,7 @@ static int riscv_classify_fp_aggregate(TargetABI* a, KitCgTypeId t, out->kind = ABI_ARG_DIRECT; out->flags = ABI_AF_NONE; out->parts = parts; - out->nparts = (u16)n; + out->nparts = n; out->indirect_align = 0; return 1; } @@ -245,7 +245,7 @@ static void classify_aggregate(TargetABI* a, KitCgTypeId t, ABIArgInfo* out, out->kind = ABI_ARG_DIRECT; out->flags = ABI_AF_NONE; out->parts = parts; - out->nparts = (u16)nparts; + out->nparts = nparts; out->indirect_align = 0; } else { out->kind = ABI_ARG_INDIRECT; diff --git a/src/abi/abi_sysv_x64.c b/src/abi/abi_sysv_x64.c @@ -154,7 +154,7 @@ static void classify_aggregate(TargetABI* a, KitCgTypeId t, ABIArgInfo* out, out->kind = ABI_ARG_DIRECT; out->flags = nparts > 1 ? ABI_AF_SPLIT : ABI_AF_NONE; out->parts = parts; - out->nparts = (u16)nparts; + out->nparts = nparts; out->indirect_align = 0; } else { out->kind = ABI_ARG_INDIRECT; diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -60,7 +60,7 @@ enum { /* Tail epilogues restore x30 and may use x16/x17 while rebuilding the frame, * so an indirect tail target is moved here only after argument marshalling. * x9 is an instruction-scoped emitter temp and no later operand phase runs - * between plan_call and the tail branch. */ + * between marshal_call and the tail branch. */ AA_TAIL_TARGET = 9u, AA_TMP0 = 16u, AA_TMP1 = 17u, @@ -1193,8 +1193,8 @@ static u32 aa_part_transfer_tmp(AANativeTarget* a, NativeLoc reg, /* A register carrier is 8 bytes, but the final part of a small aggregate may * carry any byte count from 1 through 8. Keep the one-instruction native-width * path; decompose 3/5/6/7-byte memory transfers into exact low-to-high chunks. - * This is deliberately below the call planner so incoming binds, outgoing - * args, and generic NativeCallPlanRet write phases all share the same rule. */ + * This is deliberately below call marshalling so incoming binds, outgoing + * args, and generic NativeCallPhaseRet write phases all share the same rule. */ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, NativeAddr addr, MemAccess mem) { u32 bytes = mem.size @@ -2378,31 +2378,21 @@ static void aa_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, * bits of v8..v15 (x29/x30 are the frame pointer and link register, handled by * the prologue head, not as ordinary callee-saves). This is the same register * selection the per-block spill used, hoisted into the prologue. */ +static int aa_asm_reg_is_callee_saved(NativeTarget* t, NativeAllocClass cls, + Reg r) { + (void)t; + return (cls == NATIVE_REG_INT && r >= 19u && r <= 28u) || + (cls == NATIVE_REG_FP && r >= 8u && r <= 15u); +} + static u32 aa_known_callee_saves(NativeTarget* t, const NativeKnownFrameDesc* frame, u32* out, u32 cap) { - u32 ncls = frame->ncallee_classes; - u32 clob_int = 0, clob_fp = 0, abi_int, abi_fp; - if (ncls > cap) ncls = cap; - for (u32 c = 0; c < ncls; ++c) - out[c] = frame->callee_saved_used ? frame->callee_saved_used[c] : 0u; - if (frame->asm_clobbers && frame->nasm_clobbers) { - AANativeTarget* a = aa_of(t); - SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; - aa_asm_clobber_masks(t->c, loc, frame->asm_clobbers, frame->nasm_clobbers, - &clob_int, &clob_fp); - } - native_asm_abi_clobber_masks(t, frame->asm_clobber_abi_sets, &abi_int, - &abi_fp); - clob_int |= abi_int; - clob_fp |= abi_fp; - for (Reg r = 0; r < 32u; ++r) { - if (NATIVE_REG_INT < ncls && (clob_int & (1u << r)) && r >= 19u && r <= 28u) - out[NATIVE_REG_INT] |= 1u << r; - if (NATIVE_REG_FP < ncls && (clob_fp & (1u << r)) && r >= 8u && r <= 15u) - out[NATIVE_REG_FP] |= 1u << r; - } - return ncls; + AANativeTarget* a = aa_of(t); + SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; + return native_asm_known_callee_saves( + t, loc, frame, out, cap, aa_asm_clobber_masks, + aa_asm_reg_is_callee_saved); } static u32 aa_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type, @@ -2425,8 +2415,8 @@ static void aa_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, aa_func_begin_common(t, fd); a->frame.known_frame = 1; if (frame) { - u32 cs[NATIVE_CALL_PLAN_CLASSES]; - u32 ncs = aa_known_callee_saves(t, frame, cs, NATIVE_CALL_PLAN_CLASSES); + u32 cs[NATIVE_REG_CLASS_COUNT]; + u32 ncs = aa_known_callee_saves(t, frame, cs, NATIVE_REG_CLASS_COUNT); a->frame.has_alloca = frame->has_alloca; if (ncs) aa_reserve_callee_saves(t, cs, ncs); for (u32 i = 0; i < frame->nslots; ++i) { @@ -3715,12 +3705,13 @@ static void aa_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves, s.reg_move = aa_move; s.scratch[NATIVE_REG_INT] = AA_TMP1; s.scratch[NATIVE_REG_FP] = 16u; + s.scratch_class_mask = (1u << NATIVE_REG_INT) | (1u << NATIVE_REG_FP); native_arg_shuffle(&s, moves, n); } -static void aa_plan_call(NativeTarget* t, const NativeCallDesc* desc, - NativeCallPlan* plan) { - NativeCallPlanRet* rets; +static void aa_marshal_call(NativeTarget* t, const NativeCallDesc* desc, + NativeCallPhase* plan) { + NativeCallPhaseRet* rets; const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); NativeLoc saved_indirect_callee; int preserve_indirect_callee = 0; @@ -3732,7 +3723,7 @@ static void aa_plan_call(NativeTarget* t, const NativeCallDesc* desc, : ((!abi && desc->nresults) ? 1u : 0u); memset(plan, 0, sizeof *plan); rets = - nrets_cap ? arena_zarray(t->c->tu, NativeCallPlanRet, nrets_cap) : NULL; + nrets_cap ? arena_zarray(t->c->tu, NativeCallPhaseRet, nrets_cap) : NULL; plan->callee = desc->callee; plan->rets = rets; plan->flags = desc->flags; @@ -3950,7 +3941,7 @@ static void aa_emit_tail_site(NativeTarget* t, NativeLoc callee) { } } -static void aa_emit_call(NativeTarget* t, const NativeCallPlan* plan) { +static void aa_emit_call(NativeTarget* t, const NativeCallPhase* plan) { int is_tail = (plan->flags & CG_CALL_TAIL) != 0; if (is_tail) { if (plan->callee.kind != NATIVE_LOC_GLOBAL && @@ -3973,11 +3964,11 @@ static void aa_emit_call(NativeTarget* t, const NativeCallPlan* plan) { aa_panic(aa_of(t), "unsupported call target"); } -static void aa_plan_ret(NativeTarget* t, const CGFuncDesc* fd, - const NativeLoc* value, NativeCallPlanRet** out_rets, - u32* out_nrets) { +static void aa_marshal_ret(NativeTarget* t, const CGFuncDesc* fd, + const NativeLoc* value, + NativeCallPhaseRet** out_rets, u32* out_nrets) { const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type); - NativeCallPlanRet* rets = NULL; + NativeCallPhaseRet* rets = NULL; u32 nr = 0; if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) { AANativeTarget* a = aa_of(t); @@ -4020,7 +4011,7 @@ static void aa_plan_ret(NativeTarget* t, const CGFuncDesc* fd, /* The generic return write phase materializes a memory source using the * scalar carrier type, so an I64 carrier for a 3/5/6/7-byte aggregate part - * becomes an eight-byte load before NativeCallPlanRet.mem can narrow the + * becomes an eight-byte load before NativeCallPhaseRet.mem can narrow the * final move. Once one exact-width part is present, marshal every return * part here in ABI order. aa_load_part retains the exact byte count and * uses only backend-private temporaries, so already-filled x0/x1 or v0..v3 @@ -4040,7 +4031,7 @@ static void aa_plan_ret(NativeTarget* t, const CGFuncDesc* fd, return; } - rets = arena_zarray(t->c->tu, NativeCallPlanRet, abi->ret.nparts); + rets = arena_zarray(t->c->tu, NativeCallPhaseRet, abi->ret.nparts); for (u32 p = 0; p < abi->ret.nparts; ++p) { const ABIArgPart* part = &abi->ret.parts[p]; NativeAllocClass cls = @@ -4063,7 +4054,7 @@ static void aa_plan_ret(NativeTarget* t, const CGFuncDesc* fd, nr++; } } else if (value) { - rets = arena_zarray(t->c->tu, NativeCallPlanRet, 1); + rets = arena_zarray(t->c->tu, NativeCallPhaseRet, 1); rets[0].src = *value; rets[0].dst = native_loc_reg(value->type, NATIVE_REG_INT, 0); rets[0].mem = aa_mem_for_type(t, value->type, 0); @@ -4672,25 +4663,34 @@ static void aa_trap(NativeTarget* t) { aa_emit32(t->mc, aa64_brk(0)); } /* file_scope_asm + finalize are shared (cg/native_asm.h). */ static int aa_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, - u32 mask[NATIVE_CALL_PLAN_CLASSES]) { + u32 mask[NATIVE_REG_CLASS_COUNT]) { mask[0] = mask[1] = mask[2] = 0; - if ((NativeMachineOpKind)op->kind == NATIVE_MOP_TLS_ADDR) { - /* ELF Local-Exec materializes the address using only the destination - * register (mrs tpidr_el0 + add/add into rd) — no extra clobbers. The - * Mach-O TLV sequence loads the descriptor into x0 and calls the resolver - * thunk through backend-private x16, also clobbering private x17/LR. Only - * x0 can hold an optimizer-owned value, so it is the sole effect at this - * boundary. */ - if (!obj_format_tls_via_descriptor(t->c)) return 0; - mask[NATIVE_REG_INT] = 1u << 0; - return 1; + switch ((NativeMachineOpKind)op->kind) { + case NATIVE_MOP_TLS_ADDR: + /* ELF Local-Exec materializes the address using only the destination + * register. Mach-O TLV calls its resolver through private x16/x17/LR; + * only x0 can hold an optimizer-owned value at this boundary. */ + if (!obj_format_tls_via_descriptor(t->c)) return 0; + mask[NATIVE_REG_INT] = 1u << 0; + return 1; + case NATIVE_MOP_INTRINSIC: + if ((IntrinKind)op->intrin != INTRIN_SYSCALL) return 0; + mask[NATIVE_REG_INT] = (1u << 0) | (1u << 1) | (1u << 2) | (1u << 3) | + (1u << 4) | (1u << 5) | (1u << AA_X8); + return 1; + case NATIVE_MOP_BINOP: + case NATIVE_MOP_BITFIELD_LOAD: + case NATIVE_MOP_BITFIELD_STORE: + case NATIVE_MOP_VA_START: + case NATIVE_MOP_VA_ARG: + case NATIVE_MOP_ATOMIC_CAS: + case NATIVE_MOP_ATOMIC_RMW: + return 0; + case NATIVE_MOP_COUNT: + break; } - if ((NativeMachineOpKind)op->kind != NATIVE_MOP_INTRINSIC || - (IntrinKind)op->intrin != INTRIN_SYSCALL) - return 0; - mask[NATIVE_REG_INT] = (1u << 0) | (1u << 1) | (1u << 2) | (1u << 3) | - (1u << 4) | (1u << 5) | (1u << AA_X8); - return 1; + aa_panic(aa_of(t), "invalid machine-effect operation"); + return 0; } static void aa_set_loc(NativeTarget* t, SrcLoc loc) { @@ -4725,6 +4725,8 @@ static const Reg aa_ndt_int_allocable[] = { * use the same bank through its independent `scratch` policy. */ static const Reg aa_int_scratch[] = {9u, 10u, 11u}; static const Reg aa_int_asm_temps[] = {AA_TMP0, AA_TMP1}; +static const Reg aa_direct_asm_int[] = {0u, 1u, 2u, 3u, 4u, 5u, 6u, + 7u, 8u, 11u, 12u, 13u, 14u, 15u}; /* ABI fp argument/return registers (v0..v7) come FIRST (same rationale as the * int pool: fp args land in place, the shuffle is a no-op). Then the caller- * saved fp temps v18/v19, v22..v31 — the rest of the -O0 value cache under @@ -4736,6 +4738,9 @@ static const Reg aa_ndt_fp_allocable[] = { 0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 18u, 19u, 22u, 23u, 24u, 25u, 26u, 27u, 28u, 29u, 30u, 31u, 8u, 9u, 10u, 11u, 12u, 13u, 14u, 15u}; static const Reg aa_fp_scratch[] = {20u, 21u}; +static const Reg aa_direct_asm_fp[] = { + 0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 16u, 17u, 18u, + 19u, 22u, 23u, 24u, 25u, 26u, 27u, 28u, 29u, 30u, 31u}; #define AA_PHYS_INT_ALLOC(r) \ {.reg = (r), \ @@ -4766,25 +4771,29 @@ static const Reg aa_fp_scratch[] = {20u, 21u}; .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \ .spill_cost = 4u, \ .copy_cost = 1u} -#define AA_PHYS_INT_RESERVED(r) \ - {.reg = (r), \ - .cls = NATIVE_REG_INT, \ - .abi_index = 0xffu, \ - .flags = NATIVE_REG_RESERVED, \ - .spill_cost = 0u, \ +#define AA_PHYS_INT_RESERVED_ROLE(r, role) \ + {.reg = (r), \ + .cls = NATIVE_REG_INT, \ + .abi_index = 0xffu, \ + .flags = NATIVE_REG_RESERVED | (role), \ + .spill_cost = 0u, \ .copy_cost = 0u} +#define AA_PHYS_INT_RESERVED(r) \ + AA_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_NONE) +#define AA_PHYS_INT_RESERVED_CALLER(r) \ + AA_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_CALLER_SAVED) static const NativePhysRegInfo aa_int_phys[] = { AA_PHYS_INT_ARG(0u), AA_PHYS_INT_ARG(1u), AA_PHYS_INT_ARG(2u), AA_PHYS_INT_ARG(3u), AA_PHYS_INT_ARG(4u), AA_PHYS_INT_ARG(5u), AA_PHYS_INT_ARG(6u), AA_PHYS_INT_ARG(7u), - AA_PHYS_INT_ALLOC(8u), AA_PHYS_INT_RESERVED(9u), - AA_PHYS_INT_RESERVED(10u), AA_PHYS_INT_RESERVED(11u), + AA_PHYS_INT_ALLOC(8u), AA_PHYS_INT_RESERVED_CALLER(9u), + AA_PHYS_INT_RESERVED_CALLER(10u), AA_PHYS_INT_RESERVED_CALLER(11u), AA_PHYS_INT_ALLOC(12u), AA_PHYS_INT_ALLOC(13u), AA_PHYS_INT_ALLOC(14u), AA_PHYS_INT_ALLOC(15u), - AA_PHYS_INT_RESERVED(16u), AA_PHYS_INT_RESERVED(17u), - AA_PHYS_INT_RESERVED(18u), AA_PHYS_INT_CALLEE(19u), + AA_PHYS_INT_RESERVED_CALLER(16u), AA_PHYS_INT_RESERVED_CALLER(17u), + AA_PHYS_INT_RESERVED_CALLER(18u), AA_PHYS_INT_CALLEE(19u), AA_PHYS_INT_CALLEE(20u), AA_PHYS_INT_CALLEE(21u), AA_PHYS_INT_CALLEE(22u), AA_PHYS_INT_CALLEE(23u), AA_PHYS_INT_CALLEE(24u), AA_PHYS_INT_CALLEE(25u), @@ -4822,12 +4831,13 @@ static const NativePhysRegInfo aa_int_phys[] = { .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \ .spill_cost = 4u, \ .copy_cost = 1u} -#define AA_PHYS_FP_RESERVED(r) \ - {.reg = (r), \ - .cls = NATIVE_REG_FP, \ - .abi_index = 0xffu, \ - .flags = NATIVE_REG_RESERVED, \ - .spill_cost = 0u, \ +#define AA_PHYS_FP_RESERVED(r) \ + {.reg = (r), \ + .cls = NATIVE_REG_FP, \ + .abi_index = 0xffu, \ + .flags = NATIVE_REG_RESERVED | \ + NATIVE_REG_CALLER_SAVED, \ + .spill_cost = 0u, \ .copy_cost = 0u} static const NativePhysRegInfo aa_fp_phys[] = { @@ -4855,15 +4865,12 @@ static const NativeAllocClassInfo aa_classes[] = { .nemit_temps = sizeof aa_int_scratch / sizeof aa_int_scratch[0], .asm_temps = aa_int_asm_temps, .nasm_temps = sizeof aa_int_asm_temps / sizeof aa_int_asm_temps[0], + .direct_asm_allocable = aa_direct_asm_int, + .ndirect_asm_allocable = + sizeof aa_direct_asm_int / sizeof aa_direct_asm_int[0], .emit_cache_mask = (1u << 9u) | (1u << 10u) | (1u << 11u), .phys = aa_int_phys, - .nphys = sizeof aa_int_phys / sizeof aa_int_phys[0], - .caller_saved_mask = 0x0007ffffu, - .callee_saved_mask = 0x1ff80000u, - .arg_mask = 0x000000ffu, - .ret_mask = 0x00000003u, - .reserved_mask = - (1u << AA_TMP0) | (1u << AA_TMP1) | (1u << AA_FP) | (1u << AA_LR)}, + .nphys = sizeof aa_int_phys / sizeof aa_int_phys[0]}, {.cls = NATIVE_REG_FP, .ndt_allocable = aa_ndt_fp_allocable, .ndt_allocable_count = @@ -4872,15 +4879,12 @@ static const NativeAllocClassInfo aa_classes[] = { .nscratch = sizeof aa_fp_scratch / sizeof aa_fp_scratch[0], .emit_temps = aa_fp_scratch, .nemit_temps = sizeof aa_fp_scratch / sizeof aa_fp_scratch[0], + .direct_asm_allocable = aa_direct_asm_fp, + .ndirect_asm_allocable = + sizeof aa_direct_asm_fp / sizeof aa_direct_asm_fp[0], .emit_cache_mask = (1u << 20u) | (1u << 21u), .phys = aa_fp_phys, - .nphys = sizeof aa_fp_phys / sizeof aa_fp_phys[0], - /* v8..v15 are callee-saved (low 64 bits per AAPCS64); the rest are - * caller-saved. */ - .caller_saved_mask = 0xffff00ffu, - .callee_saved_mask = 0x0000ff00u, - .arg_mask = 0x000000ffu, - .ret_mask = 0x0000000fu}, + .nphys = sizeof aa_fp_phys / sizeof aa_fp_phys[0]}, }; /* Resolve a register name ("x8", "v3", ...) to its (class, Reg). Powers the @@ -5042,9 +5046,9 @@ NativeTarget* aa64_native_target_new(Compiler* c, ObjBuilder* obj, t->alloca_ = aa_alloca; t->spill = aa_spill; t->reload = aa_reload; - t->plan_call = aa_plan_call; + t->marshal_call = aa_marshal_call; t->emit_call = aa_emit_call; - t->plan_ret = aa_plan_ret; + t->marshal_ret = aa_marshal_ret; t->ret = aa_ret; t->atomic_load = aa_atomic_load; t->atomic_store = aa_atomic_store; @@ -5588,29 +5592,6 @@ static void aa_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, } } -AA_UNUSED_FN static Reg aa_asm_alloc_reg(NativeDirectTarget* d, - NativeAllocClass cls, u32 allowed_mask, - u32* used_int, u32* used_fp) { - static const Reg int_pool[] = {0u, 1u, 2u, 3u, 4u, 5u, 6u, - 7u, 8u, 11u, 12u, 13u, 14u, 15u}; - static const Reg fp_pool[] = {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, - 16u, 17u, 18u, 19u, 22u, 23u, 24u, 25u, - 26u, 27u, 28u, 29u, 30u, 31u}; - const Reg* pool = cls == NATIVE_REG_FP ? fp_pool : int_pool; - u32 n = cls == NATIVE_REG_FP ? (u32)(sizeof fp_pool / sizeof fp_pool[0]) - : (u32)(sizeof int_pool / sizeof int_pool[0]); - u32* used = cls == NATIVE_REG_FP ? used_fp : used_int; - for (u32 i = 0; i < n; ++i) { - Reg r = pool[i]; - if (allowed_mask && (allowed_mask & (1u << r)) == 0) continue; - if ((*used & (1u << r)) != 0) continue; - *used |= 1u << r; - return r; - } - aa_asm_panic(d, "out of registers for asm operands"); - return REG_NONE; -} - /* Pin resolution + panic is the shared native_asm_bind_direct_operands path. */ AA_UNUSED_FN static void aa_direct_load_operand_to_reg(NativeDirectTarget* d, @@ -5764,16 +5745,11 @@ static void aa_direct_asm_block(NativeDirectTarget* d, const char* tmpl, const Sym* clobbers, u32 nclob, u32 clobber_abi_sets) { static const NativeAsmDirectHooks hooks = { - /* Reserve backend-private x16/x17, x18 (platform reg), fp/lr/sp. */ - .reserved_int_mask = (1u << AA_TMP0) | (1u << AA_TMP1) | (1u << 18u) | - (1u << AA_FP) | (1u << AA_LR) | (1u << AA_SP), - .reserved_fp_mask = (1u << 20u) | (1u << 21u), .opk_reg = AA64_INLINE_OPK_REG, .opcls_fp = AA64_INLINE_OPCLS_FP, .panic = aa_asm_panic, .bound_reg = aa_asm_bound_reg, .bound_mem = aa_asm_bound_mem, - .alloc_reg = aa_asm_alloc_reg, .clobber_masks = aa_asm_clobber_masks, .save_callee_clobbers = aa_asm_hook_save_callee_clobbers, .restore_one = aa_asm_hook_restore_one, diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c @@ -200,13 +200,15 @@ static void arm_emit_pop(MCEmitter* mc, u32 reglist) { .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \ .spill_cost = 4u, \ .copy_cost = 1u} -#define ARM_PHYS_RESERVED(r) \ - {.reg = (r), \ - .cls = NATIVE_REG_INT, \ - .abi_index = 0xffu, \ - .flags = NATIVE_REG_RESERVED, \ - .spill_cost = 0u, \ +#define ARM_PHYS_RESERVED_ROLE(r, role) \ + {.reg = (r), \ + .cls = NATIVE_REG_INT, \ + .abi_index = 0xffu, \ + .flags = NATIVE_REG_RESERVED | (role), \ + .spill_cost = 0u, \ .copy_cost = 0u} +#define ARM_PHYS_RESERVED(r) \ + ARM_PHYS_RESERVED_ROLE((r), NATIVE_REG_NONE) /* NDT (-O0) value-cache pool: the caller-saved arg registers r0..r3, fronted so * a producer materializes a call's args directly into their ABI registers @@ -221,15 +223,18 @@ static const Reg arm_int_scratch[] = {ARM_SCRATCH}; * exposing either as an operand location would couple independent lifetimes. */ static const Reg arm_int_emit_temps[] = {0u, 1u, 2u, 3u}; static const Reg arm_int_asm_temps[] = {ARM_TMP}; +static const Reg arm_direct_asm_int[] = {0u, 1u, 2u, 3u, 4u, 5u, + 6u, 8u, 9u, 10u, 11u}; static const NativePhysRegInfo arm_int_phys[] = { ARM_PHYS_ARG(0u, 0u), ARM_PHYS_ARG(1u, 1u), ARM_PHYS_ARG(2u, 2u), ARM_PHYS_ARG(3u, 3u), ARM_PHYS_CALLEE(4u), ARM_PHYS_CALLEE(5u), - ARM_PHYS_CALLEE(6u), ARM_PHYS_RESERVED(7u), /* fp */ + ARM_PHYS_CALLEE(6u), + ARM_PHYS_RESERVED_ROLE(7u, NATIVE_REG_CALLEE_SAVED), /* fp */ ARM_PHYS_CALLEE(8u), ARM_PHYS_CALLEE(9u), ARM_PHYS_CALLEE(10u), ARM_PHYS_CALLEE(11u), - ARM_PHYS_RESERVED(12u), /* ip = O0/backend temp */ + ARM_PHYS_RESERVED_ROLE(12u, NATIVE_REG_CALLER_SAVED), /* ip/temp */ ARM_PHYS_RESERVED(13u), /* sp */ ARM_PHYS_RESERVED(14u), /* lr */ ARM_PHYS_RESERVED(15u), /* pc */ @@ -288,17 +293,12 @@ static const NativeAllocClassInfo arm_classes[] = { .nemit_temps = sizeof arm_int_emit_temps / sizeof arm_int_emit_temps[0], .asm_temps = arm_int_asm_temps, .nasm_temps = sizeof arm_int_asm_temps / sizeof arm_int_asm_temps[0], + .direct_asm_allocable = arm_direct_asm_int, + .ndirect_asm_allocable = + sizeof arm_direct_asm_int / sizeof arm_direct_asm_int[0], .emit_cache_mask = 0x0000000fu, /* r0..r3; hooks use private ip/lr */ .phys = arm_int_phys, - .nphys = sizeof arm_int_phys / sizeof arm_int_phys[0], - /* r0..r3 + r12 (lr/ip caller-saved; lr reserved) */ - .caller_saved_mask = 0x0000100fu, - /* r4..r11 */ - .callee_saved_mask = 0x00000ff0u, - .arg_mask = 0x0000000fu, - .ret_mask = 0x00000003u, - /* r7 (fp), r12 (ip), r13 (sp), r14 (lr), r15 (pc) */ - .reserved_mask = 0x0000f080u}, + .nphys = sizeof arm_int_phys / sizeof arm_int_phys[0]}, }; static const NativeRegInfo arm_reg_info = { @@ -1298,18 +1298,30 @@ static int arm_reg_is_callee_int(Reg r) { return r >= 4u && r <= 11u && r != ARM_FP; } +static void arm_asm_clobber_masks(Compiler* c, SrcLoc loc, + const Sym* clobbers, u32 nclob, + u32* int_mask, u32* fp_mask); + /* Build the callee-saved set the prologue must preserve, from the allocator's - * per-class masks. Only the integer class exists in this soft-float backend, so - * the FP/vec classes are ignored; inline asm is unsupported (asm_block is not - * installed), so there are no opaque asm clobbers to fold in. Restricts to the - * register file's actual callee-saved set as a guard. */ -static u32 arm_known_callee_saves(const NativeKnownFrameDesc* frame) { - u32 mask = 0; - if (frame && frame->callee_saved_used && frame->ncallee_classes > NATIVE_REG_INT) - mask = frame->callee_saved_used[NATIVE_REG_INT]; - for (Reg r = 0; r < 16u; ++r) - if ((mask & (1u << r)) && !arm_reg_is_callee_int(r)) mask &= ~(1u << r); - return mask; + * per-class masks plus opaque inline-asm clobbers. Only the integer class + * exists in this soft-float backend. r7 is structural frame state and is + * excluded here (and rejected when named explicitly by the clobber parser). */ +static int arm_asm_reg_is_callee_saved(NativeTarget* t, NativeAllocClass cls, + Reg r) { + (void)t; + return cls == NATIVE_REG_INT && arm_reg_is_callee_int(r); +} + +static u32 arm_known_callee_saves(NativeTarget* t, + const NativeKnownFrameDesc* frame) { + Arm32NativeTarget* a = arm_of(t); + u32 masks[NATIVE_REG_CLASS_COUNT] = {0, 0, 0}; + SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; + native_asm_known_callee_saves(t, loc, frame, masks, + NATIVE_REG_CLASS_COUNT, + arm_asm_clobber_masks, + arm_asm_reg_is_callee_saved); + return masks[NATIVE_REG_INT]; } /* True (and sets *imm7) when `n` fits the 16-bit `SUB sp,sp,#imm7*4` form: @@ -1482,11 +1494,12 @@ static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, Arm32NativeTarget* a = arm_of(t); MCEmitter* mc = t->mc; const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type); - u32 cs_mask = arm_known_callee_saves(kf); + u32 cs_mask; u32 reglist, nsaved, frame, i; a->func = fd; a->loc = fd->loc; + cs_mask = arm_known_callee_saves(t, kf); native_frame_reset(&a->frame); a->next_param_int = 0; a->next_param_stack = 0; @@ -1511,9 +1524,9 @@ static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, /* Record the callee-save set (for parity / debug) and build the body's slots * and outgoing-area reservation, then settle the exact frame. */ if (cs_mask) { - u32 used[NATIVE_CALL_PLAN_CLASSES] = {0, 0, 0}; + u32 used[NATIVE_REG_CLASS_COUNT] = {0, 0, 0}; used[NATIVE_REG_INT] = cs_mask; - arm_reserve_callee_saves(t, used, NATIVE_CALL_PLAN_CLASSES); + arm_reserve_callee_saves(t, used, NATIVE_REG_CLASS_COUNT); } if (kf) { for (i = 0; i < kf->nslots; ++i) { @@ -1680,8 +1693,8 @@ static void arm_func_end(NativeTarget* t) { #define ARM32_GPR_BYTES 4u /* Lane scalar type for an ABI part: an 8-byte i64/double DIRECT argument is two - * 4-byte INT lanes, an aggregate up to four word/sub-word lanes. The NDT sizes - * every load/store/move it emits for a plan-return / plan-call / param-bind move + * 4-byte INT lanes, and an aggregate uses word/sub-word lanes. The NDT sizes + * every load/store/move it emits for return/call marshalling or a param bind * from the lane location's TYPE (nd_type_mem -> cg_type_size), so each lane must * carry a type whose natural size equals the lane's byte width — NOT the full * 8-byte scalar type (which would drive an 8-byte access the 32-bit backend has @@ -1724,9 +1737,9 @@ static void arm_bind_native_param(NativeTarget* t, const CGParamDesc* p, u32 i; if (!ai || ai->kind == ABI_ARG_IGNORE) return; if (ai->kind == ABI_ARG_INDIRECT) { - /* Large aggregate passed by reference: the incoming arg is a pointer to the - * caller's copy. Copy it into the parameter's frame home so the body owns a - * private copy (matching the by-value semantics). */ + /* An ABI-classified indirect argument is a pointer to caller-owned storage. + * Copy it into the parameter's frame home so the body owns a private copy + * (matching the by-value semantics). */ KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32); NativeLoc src = native_loc_reg(i32t, NATIVE_REG_INT, a->next_param_int < 4u @@ -1868,7 +1881,7 @@ static const ABIArgInfo* arm_param_abi(NativeTarget* t, const ABIFuncInfo* abi, * alignment so the AAPCS even/8-align walk treats a synthesized variadic * i64/double (or 8-aligned aggregate) the same as a named one. */ parts[0].align = align; - scratch->nparts = (u16)nparts; + scratch->nparts = nparts; scratch->parts = parts; return scratch; } @@ -1986,7 +1999,7 @@ static void arm_store_outgoing(NativeTarget* t, u32 stack_off, NativeLoc src, } /* Materialize the address of a frame/stack-resident NativeLoc into `dst` (used - * to pass a large INDIRECT aggregate argument by reference). */ + * for arguments that the ABI explicitly classifies as indirect). */ static void arm_addr_of_loc(NativeTarget* t, NativeLoc dst, NativeLoc src) { NativeAddr addr; if (!native_loc_storage_addr(src, 0, &addr)) @@ -2011,21 +2024,22 @@ static void arm_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves, s.emit_one = arm_emit_one_arg_move; s.reg_move = arm_move; s.scratch[NATIVE_REG_INT] = ARM_SCRATCH; + s.scratch_class_mask = 1u << NATIVE_REG_INT; native_arg_shuffle(&s, moves, n); } -static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, - NativeCallPlan* plan) { +static void arm_marshal_call(NativeTarget* t, const NativeCallDesc* desc, + NativeCallPhase* plan) { Arm32NativeTarget* a = arm_of(t); const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); - NativeCallPlanRet* rets; + NativeCallPhaseRet* rets; u32 nrets_cap = (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) ? abi->ret.nparts : ((!abi && desc->nresults) ? 1u : 0u); int tail = (desc->flags & CG_CALL_TAIL) != 0; int tail_callee_saved = 0; memset(plan, 0, sizeof *plan); - rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPlanRet, nrets_cap) : NULL; + rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPhaseRet, nrets_cap) : NULL; plan->callee = desc->callee; plan->rets = rets; plan->flags = desc->flags; @@ -2107,9 +2121,9 @@ static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, int even = arm_arg_needs_even(ai); if (ai->kind == ABI_ARG_IGNORE) continue; if (ai->kind == ABI_ARG_INDIRECT) { - /* Pass a large aggregate by reference: a pointer to the caller's copy - * (the callee makes its own copy in bind_param). The pointer rides a - * core reg when one is free, else the outgoing stack. */ + /* Pass an ABI-classified indirect argument as a pointer to the caller's + * copy (the callee makes its own copy in bind_param). The pointer rides + * a core reg when one is free, else the outgoing stack. */ if (next_int < 4u) { NativeArgMove* m = &moves[nmoves++]; m->dst = native_loc_reg(i32t, NATIVE_REG_INT, (Reg)(next_int++)); @@ -2202,7 +2216,7 @@ static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, } } -static void arm_emit_call(NativeTarget* t, const NativeCallPlan* plan) { +static void arm_emit_call(NativeTarget* t, const NativeCallPhase* plan) { Arm32NativeTarget* a = arm_of(t); MCEmitter* mc = t->mc; if (plan->flags & CG_CALL_TAIL) { @@ -2241,7 +2255,7 @@ static void arm_emit_call(NativeTarget* t, const NativeCallPlan* plan) { } if (plan->callee.kind == NATIVE_LOC_STACK) { /* -O0 indirect call with stack args: the callee was spilled to a frame home - * (arm_plan_call) to free the backend staging registers. Arg staging is + * (arm_marshal_call) to free the backend staging registers. Arg staging is * done now, so IP is free; reload the target and BLX it. */ NativeLoc tmp = native_loc_reg(plan->callee.type, NATIVE_REG_INT, ARM_SCRATCH); NativeAddr home; @@ -2256,14 +2270,14 @@ static void arm_emit_call(NativeTarget* t, const NativeCallPlan* plan) { arm_panic(a, "unsupported call target"); } -static void arm_plan_ret(NativeTarget* t, const CGFuncDesc* fd, - const NativeLoc* value, NativeCallPlanRet** out_rets, - u32* out_nrets) { +static void arm_marshal_ret(NativeTarget* t, const CGFuncDesc* fd, + const NativeLoc* value, + NativeCallPhaseRet** out_rets, u32* out_nrets) { Arm32NativeTarget* a = arm_of(t); const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type); - NativeCallPlanRet* rets = NULL; + NativeCallPhaseRet* rets = NULL; u32 nr = 0; - if (value) rets = arena_zarray(t->c->tu, NativeCallPlanRet, 4); + if (value) rets = arena_zarray(t->c->tu, NativeCallPhaseRet, 4); if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) { /* Large aggregate return: copy the value into the caller-provided * destination via the hidden sret pointer spilled at entry. The body's @@ -3402,8 +3416,9 @@ static void arm_asm_bound_mem(Operand* out, KitCgTypeId type, Reg base) { out->v.ind.index = CG_LOCAL_NONE; } -/* Parse one clobber name ("r0".."r12", "lr"/"sp", "cc", "memory") into a - * register mask bit; cc/memory contribute no register. */ +/* Parse one clobber name into a core-register mask bit. r7 is the live frame + * anchor and sp/pc are structural state, so an inline block may not declare + * them clobbered; cc/memory contribute no register. */ static void arm_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, u32 nclob, u32* int_mask, u32* fp_mask) { *int_mask = 0; @@ -3416,28 +3431,13 @@ static void arm_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, memcpy(buf, s.s, s.len); buf[s.len] = '\0'; if (!strcmp(buf, "cc") || !strcmp(buf, "memory")) continue; - if (arm32_register_index(buf, &dwarf) == 0 && dwarf <= 15u) + if (arm32_register_index(buf, &dwarf) != 0 || dwarf > 15u) + arm_asm_panic_at(c, loc, "unsupported clobber register"); + if (dwarf == ARM_FP || dwarf == 13u || dwarf == 15u) + arm_asm_panic_at(c, loc, "structural register cannot be clobbered"); + if (dwarf <= 15u) *int_mask |= 1u << dwarf; } - (void)loc; -} - -/* Allocate a free operand register from the core pool, honoring allowed_mask and - * the running used set. r7 (fp), r12 (ip), r13 (sp), r14 (lr), r15 (pc) are - * never handed out. */ -static Reg arm_asm_alloc_reg(NativeDirectTarget* d, NativeAllocClass cls, - u32 allowed_mask, u32* used_int, u32* used_fp) { - static const Reg int_pool[] = {0u, 1u, 2u, 3u, 4u, 5u, 6u, 8u, 9u, 10u, 11u}; - (void)used_fp; - if (cls != NATIVE_REG_INT) arm_asm_panic(d, "no FP registers (soft-float)"); - for (u32 i = 0; i < sizeof int_pool / sizeof int_pool[0]; ++i) { - Reg r = int_pool[i]; - if (allowed_mask && (allowed_mask & (1u << r)) == 0) continue; - if ((*used_int & (1u << r)) != 0) continue; - *used_int |= 1u << r; - return r; - } - arm_asm_panic(d, "out of registers for asm operands"); } /* Operand-address resolution for the direct (-O0) path (mirrors aa64). */ @@ -3612,17 +3612,11 @@ static void arm_direct_asm_block(NativeDirectTarget* d, const char* tmpl, const Sym* clobbers, u32 nclob, u32 clobber_abi_sets) { static const NativeAsmDirectHooks hooks = { - /* Reserve backend/O0 IP plus FP, SP, LR, and PC from this direct-path - * operand allocator. */ - .reserved_int_mask = (1u << ARM_SCRATCH) | (1u << ARM_FP) | - (1u << 13u) | (1u << 14u) | (1u << 15u), - .reserved_fp_mask = 0u, .opk_reg = ARM32_INLINE_OPK_REG, .opcls_fp = ARM32_INLINE_OPCLS_FP, .panic = arm_asm_panic, .bound_reg = arm_asm_bound_reg, .bound_mem = arm_asm_bound_mem, - .alloc_reg = arm_asm_alloc_reg, .clobber_masks = arm_asm_clobber_masks, .save_callee_clobbers = arm_asm_hook_save_callee_clobbers, .restore_one = arm_asm_hook_restore_one, @@ -3772,9 +3766,9 @@ NativeTarget* arm32_native_target_new(Compiler* c, ObjBuilder* obj, t->alloca_ = arm_alloca; t->spill = arm_spill; t->reload = arm_reload; - t->plan_call = arm_plan_call; + t->marshal_call = arm_marshal_call; t->emit_call = arm_emit_call; - t->plan_ret = arm_plan_ret; + t->marshal_ret = arm_marshal_ret; t->ret = arm_ret; t->atomic_load = arm_atomic_load; t->atomic_store = arm_atomic_store; diff --git a/src/arch/native_target.c b/src/arch/native_target.c @@ -0,0 +1,158 @@ +#include "arch/native_target.h" + +static const NativePhysRegInfo* native_class_phys_reg( + const NativeAllocClassInfo* ci, Reg reg) { + for (u32 i = 0; ci && i < ci->nphys; ++i) + if (ci->phys[i].reg == reg) return &ci->phys[i]; + return NULL; +} + +const NativeAllocClassInfo* native_reg_info_class_info( + const NativeRegInfo* ri, NativeAllocClass cls) { + if (!ri || cls >= NATIVE_REG_CLASS_COUNT) return NULL; + for (u32 i = 0; i < ri->nclasses; ++i) { + const NativeAllocClassInfo* ci = &ri->classes[i]; + if ((NativeAllocClass)ci->cls == cls) return ci; + } + return NULL; +} + +u32 native_reg_info_flag_mask(const NativeRegInfo* ri, NativeAllocClass cls, + u16 flag) { + const NativeAllocClassInfo* ci = native_reg_info_class_info(ri, cls); + u32 mask = 0; + for (u32 i = 0; ci && i < ci->nphys; ++i) + if ((ci->phys[i].flags & flag) != 0 && + ci->phys[i].reg < NATIVE_MAX_HARD_REGS) + mask |= 1u << ci->phys[i].reg; + return mask; +} + +static u32 native_validate_reg_list(Compiler* c, + const NativeAllocClassInfo* ci, + const Reg* regs, u32 nregs, + const char* list_name) { + u32 mask = 0; + if ((nregs != 0u) != (regs != NULL)) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u has inconsistent %s list", + (unsigned)ci->cls, list_name); + if (nregs > NATIVE_MAX_HARD_REGS) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u has too many %s registers " + "(%u)", + (unsigned)ci->cls, list_name, (unsigned)nregs); + for (u32 i = 0; i < nregs; ++i) { + Reg reg = regs[i]; + const NativePhysRegInfo* phys; + if (reg >= NATIVE_MAX_HARD_REGS || (mask & (1u << reg))) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u has invalid/duplicate " + "%s register %u", + (unsigned)ci->cls, list_name, (unsigned)reg); + phys = native_class_phys_reg(ci, reg); + if (!phys || phys->cls != ci->cls) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u %s register %u is not a " + "matching physical register", + (unsigned)ci->cls, list_name, (unsigned)reg); + mask |= 1u << reg; + } + return mask; +} + +void native_reg_info_validate(Compiler* c, const NativeRegInfo* ri) { + u32 seen = 0; + if (!ri) return; + if (ri->nclasses > NATIVE_REG_CLASS_COUNT || + (ri->nclasses != 0u) != (ri->classes != NULL)) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: invalid register class table"); + for (u32 n = 0; n < ri->nclasses; ++n) { + const NativeAllocClassInfo* ci = &ri->classes[n]; + u32 cls = ci->cls; + u32 known_mask = 0; + u32 emit_mask; + u32 asm_mask; + if (cls >= NATIVE_REG_CLASS_COUNT || (seen & (1u << cls))) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: duplicate/invalid register class " + "%u", + (unsigned)cls); + seen |= 1u << cls; + if ((ci->nphys != 0u) != (ci->phys != NULL) || + ci->nphys > NATIVE_MAX_HARD_REGS) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u has invalid physical " + "table size %u", + (unsigned)cls, (unsigned)ci->nphys); + for (u32 i = 0; i < ci->nphys; ++i) { + const NativePhysRegInfo* phys = &ci->phys[i]; + if (phys->reg >= NATIVE_MAX_HARD_REGS || phys->cls != cls || + (known_mask & (1u << phys->reg))) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u has invalid physical " + "register %u", + (unsigned)cls, (unsigned)phys->reg); + if ((phys->flags & NATIVE_REG_CALLER_SAVED) && + (phys->flags & NATIVE_REG_CALLEE_SAVED)) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u register %u is both " + "caller- and callee-saved", + (unsigned)cls, (unsigned)phys->reg); + if ((phys->flags & NATIVE_REG_ALLOCABLE) && + (phys->flags & NATIVE_REG_RESERVED)) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u register %u is both " + "allocable and reserved", + (unsigned)cls, (unsigned)phys->reg); + known_mask |= 1u << phys->reg; + } + (void)native_validate_reg_list(c, ci, ci->ndt_allocable, + ci->ndt_allocable_count, "O0 allocable"); + (void)native_validate_reg_list(c, ci, ci->scratch, ci->nscratch, + "O0 scratch"); + emit_mask = native_validate_reg_list(c, ci, ci->emit_temps, + ci->nemit_temps, "emit temp"); + asm_mask = native_validate_reg_list(c, ci, ci->asm_temps, ci->nasm_temps, + "asm-internal temp"); + (void)native_validate_reg_list(c, ci, ci->direct_asm_allocable, + ci->ndirect_asm_allocable, + "direct-asm allocable"); + if (emit_mask & asm_mask) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u emit and asm temp banks " + "overlap", + (unsigned)cls); + if (asm_mask & + ~native_reg_info_flag_mask(ri, (NativeAllocClass)cls, + NATIVE_REG_RESERVED)) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u asm temp bank is not " + "reserved", + (unsigned)cls); + if (ci->emit_cache_mask & ~emit_mask) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u cache mask escapes emit " + "temp bank", + (unsigned)cls); + for (u32 i = 0; i < ci->nemit_temps; ++i) { + const NativePhysRegInfo* phys = + native_class_phys_reg(ci, ci->emit_temps[i]); + if (phys && (phys->flags & NATIVE_REG_ALLOCABLE)) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u emit temp %u is " + "allocable", + (unsigned)cls, (unsigned)ci->emit_temps[i]); + } + for (u32 i = 0; i < ci->nasm_temps; ++i) { + const NativePhysRegInfo* phys = + native_class_phys_reg(ci, ci->asm_temps[i]); + if (phys && (phys->flags & NATIVE_REG_ALLOCABLE)) + compiler_panic(c, (SrcLoc){0, 0, 0}, + "native register model: class %u asm temp %u is " + "allocable", + (unsigned)cls, (unsigned)ci->asm_temps[i]); + } + } +} diff --git a/src/arch/native_target.h b/src/arch/native_target.h @@ -121,8 +121,13 @@ typedef enum NativeAllocClass { NATIVE_REG_INT, NATIVE_REG_FP, NATIVE_REG_VEC, + NATIVE_REG_CLASS_COUNT, } NativeAllocClass; +#define NATIVE_MAX_HARD_REGS 32u + +typedef u32 NativeRegMaskSet[NATIVE_REG_CLASS_COUNT]; + typedef enum NativeRegFlag { NATIVE_REG_NONE = 0, NATIVE_REG_ALLOCABLE = 1u << 0, @@ -174,6 +179,14 @@ typedef struct NativeAllocClassInfo { const Reg* asm_temps; u32 nasm_temps; + /* Ordered register policy for operands self-allocated by the O0 direct + * inline-asm binder. This is distinct from ndt_allocable and scratch: values + * live for the complete asm block, and the order is an architecture policy. + * Explicit hard-register pins are validated separately and need not appear + * here. Selection itself is shared in cg/native_asm.c. */ + const Reg* direct_asm_allocable; + u32 ndirect_asm_allocable; + /* Subset of reserved optimizer emitter temps whose contents survive ordinary * backend hooks when they are not passed as operands or named by a clobber * effect. The native emitter may retain clean FS_SPILL values in these and @@ -185,12 +198,6 @@ typedef struct NativeAllocClassInfo { const NativePhysRegInfo* phys; u32 nphys; - - u32 caller_saved_mask; - u32 callee_saved_mask; - u32 arg_mask; - u32 ret_mask; - u32 reserved_mask; } NativeAllocClassInfo; typedef struct NativeRegInfo NativeRegInfo; @@ -253,6 +260,22 @@ struct NativeRegInfo { u32 (*dwarf_reg)(const NativeRegInfo*, NativeAllocClass, Reg); }; +/* Validate the target-independent NativeRegInfo contract. Both the direct and + * optimized adapters call this at their boundary, so malformed physical rows, + * role flags, or temporary banks fail identically at O0 and O1. */ +void native_reg_info_validate(Compiler* c, const NativeRegInfo* ri); + +/* Classes are identified by NativeAllocClass, not by their position in the + * target's table. Keep the lookup policy shared by every register consumer. */ +const NativeAllocClassInfo* native_reg_info_class_info( + const NativeRegInfo* ri, NativeAllocClass cls); + +/* Static register roles have one authority: NativePhysRegInfo.flags. Targets + * with an OS-dependent preservation ABI override caller/callee masks through + * NativeTarget below; all other role consumers derive their masks here. */ +u32 native_reg_info_flag_mask(const NativeRegInfo* ri, NativeAllocClass cls, + u16 flag); + typedef enum NativeLocKind { NATIVE_LOC_NONE, NATIVE_LOC_REG, @@ -416,8 +439,6 @@ typedef struct NativeFramePatchState { u32 max_align; } NativeFramePatchState; -#define NATIVE_CALL_PLAN_CLASSES 3u - /* A semantic machine operation, enough for the target to report the physical * registers its encoding clobbers as a side effect (e.g. x86 idiv writes * rax/rdx, variable shifts use cl, and a bitfield read-modify-write borrows @@ -439,6 +460,7 @@ typedef enum NativeMachineOpKind { * whose TLS sequence touches only the destination register (ELF Local-Exec) * report no clobbers. */ NATIVE_MOP_TLS_ADDR, + NATIVE_MOP_COUNT, } NativeMachineOpKind; typedef struct NativeMachineOp { @@ -469,40 +491,38 @@ typedef struct NativeCallDesc { KitCgInlinePolicy inline_policy; } NativeCallDesc; -typedef enum NativeCallPlanMoveKind { +typedef enum NativeCallPhaseMoveKind { NATIVE_CALL_MOVE_NONE, NATIVE_CALL_MOVE_VALUE, NATIVE_CALL_MOVE_ADDR, -} NativeCallPlanMoveKind; +} NativeCallPhaseMoveKind; -typedef struct NativeCallPlanMove { +typedef struct NativeCallPhaseMove { NativeLoc src; NativeLoc dst; MemAccess mem; - u8 src_kind; /* NativeCallPlanMoveKind */ + u8 src_kind; /* NativeCallPhaseMoveKind */ u8 dst_kind; /* NativeLocKind */ u8 pad[2]; -} NativeCallPlanMove; +} NativeCallPhaseMove; -typedef struct NativeCallPlanRet { +typedef struct NativeCallPhaseRet { NativeLoc src; NativeLoc dst; MemAccess mem; -} NativeCallPlanRet; +} NativeCallPhaseRet; -typedef struct NativeCallPlan { +typedef struct NativeCallPhase { NativeLoc callee; - NativeCallPlanMove* args; - NativeCallPlanRet* rets; + NativeCallPhaseMove* args; + NativeCallPhaseRet* rets; u32 nargs; u32 nrets; u32 stack_arg_size; - u32 clobber_mask[NATIVE_CALL_PLAN_CLASSES]; - u32 return_mask[NATIVE_CALL_PLAN_CLASSES]; u16 flags; /* CGCallFlag */ u8 has_sret; u8 is_variadic; -} NativeCallPlan; +} NativeCallPhase; typedef struct NativeTarget NativeTarget; struct NativeTarget { @@ -545,7 +565,7 @@ struct NativeTarget { * clobbered, 0 otherwise. NULL promises no optimizer-visible fixed * clobbers. */ int (*machine_op_clobbers)(NativeTarget*, const NativeMachineOp* op, - u32 clobber_mask[NATIVE_CALL_PLAN_CLASSES]); + NativeRegMaskSet clobber_mask); void (*func_begin)(NativeTarget*, const CGFuncDesc*); void (*func_begin_known_frame)(NativeTarget*, const CGFuncDesc*, @@ -581,7 +601,7 @@ struct NativeTarget { * flags, nargs, and each args[i].type — never argument *locations* — so the * optimizer can call it in a frame-planning pre-pass, before any argument * marshalling is emitted, to size the outgoing area. Must equal the - * stack_arg_size plan_call computes for the same descriptor. May be NULL. */ + * stack_arg_size marshal_call computes for the same descriptor. May be NULL. */ u32 (*call_stack_bytes)(NativeTarget*, const NativeCallDesc*); /* Integer hardware zero register, if the ISA has one (aa64 wzr/xzr, rv64 * x0). When `has_store_zero_reg` is set, the emit path stores a constant 0 @@ -686,18 +706,18 @@ struct NativeTarget { void (*spill)(NativeTarget*, NativeLoc src_reg, NativeFrameSlot, MemAccess); void (*reload)(NativeTarget*, NativeLoc dst_reg, NativeFrameSlot, MemAccess); - /* Mutating ABI-marshalling phase, despite the historical `plan_` name. It - * may stage the callee, emit stack arguments and register shuffles, and fill - * any remaining generic moves/results in NativeCallPlan. On return, every - * populated ABI argument destination stays live until emit_call. */ - void (*plan_call)(NativeTarget*, const NativeCallDesc*, NativeCallPlan*); - void (*emit_call)(NativeTarget*, const NativeCallPlan*); + /* Mutating ABI-marshalling phase. It may stage the callee, emit stack + * arguments and register shuffles, and fill any remaining generic + * moves/results in NativeCallPhase. The phase owns every populated ABI + * argument destination and any staged callee until emit_call consumes it. */ + void (*marshal_call)(NativeTarget*, const NativeCallDesc*, NativeCallPhase*); + void (*emit_call)(NativeTarget*, const NativeCallPhase*); /* The corresponding mutating return-marshalling phase. `value` is the * single returned local's location, or NULL for void; the hook may emit an * indirect-result copy and returns the ordered register moves in * out_rets/out_nrets. Completed destinations remain live through the phase. */ - void (*plan_ret)(NativeTarget*, const CGFuncDesc*, const NativeLoc* value, - NativeCallPlanRet** out_rets, u32* out_nrets); + void (*marshal_ret)(NativeTarget*, const CGFuncDesc*, const NativeLoc* value, + NativeCallPhaseRet** out_rets, u32* out_nrets); void (*ret)(NativeTarget*); /* Like load(), a one-register atomic load must consume a plain BASE_REG @@ -752,28 +772,38 @@ struct NativeTarget { static inline const NativeAllocClassInfo* native_target_class_info( const NativeTarget* t, NativeAllocClass cls) { - if (!t || !t->regs) return NULL; - for (u32 i = 0; i < t->regs->nclasses; ++i) { - const NativeAllocClassInfo* ci = &t->regs->classes[i]; - if ((NativeAllocClass)ci->cls == cls) return ci; - } - return NULL; + return t ? native_reg_info_class_info(t->regs, cls) : NULL; } static inline u32 native_target_caller_saved_mask(NativeTarget* t, NativeAllocClass cls) { - const NativeAllocClassInfo* ci; if (t && t->caller_saved_mask) return t->caller_saved_mask(t, cls); - ci = native_target_class_info(t, cls); - return ci ? ci->caller_saved_mask : 0u; + return t ? native_reg_info_flag_mask(t->regs, cls, + NATIVE_REG_CALLER_SAVED) + : 0u; } static inline u32 native_target_callee_saved_mask(NativeTarget* t, NativeAllocClass cls) { - const NativeAllocClassInfo* ci; if (t && t->callee_saved_mask) return t->callee_saved_mask(t, cls); - ci = native_target_class_info(t, cls); - return ci ? ci->callee_saved_mask : 0u; + return t ? native_reg_info_flag_mask(t->regs, cls, + NATIVE_REG_CALLEE_SAVED) + : 0u; +} + +static inline u32 native_target_arg_mask(const NativeTarget* t, + NativeAllocClass cls) { + return t ? native_reg_info_flag_mask(t->regs, cls, NATIVE_REG_ARG) : 0u; +} + +static inline u32 native_target_ret_mask(const NativeTarget* t, + NativeAllocClass cls) { + return t ? native_reg_info_flag_mask(t->regs, cls, NATIVE_REG_RET) : 0u; +} + +static inline u32 native_target_reserved_mask(const NativeTarget* t, + NativeAllocClass cls) { + return t ? native_reg_info_flag_mask(t->regs, cls, NATIVE_REG_RESERVED) : 0u; } /* Intrinsic operands whose immediate form is shared across every NativeTarget. diff --git a/src/arch/riscv/native.c b/src/arch/riscv/native.c @@ -438,13 +438,17 @@ static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0x1fu; } .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \ .spill_cost = 4u, \ .copy_cost = 1u} -#define RV_PHYS_INT_RESERVED(r) \ - {.reg = (r), \ - .cls = NATIVE_REG_INT, \ - .abi_index = 0xffu, \ - .flags = NATIVE_REG_RESERVED, \ - .spill_cost = 0u, \ +#define RV_PHYS_INT_RESERVED_ROLE(r, role) \ + {.reg = (r), \ + .cls = NATIVE_REG_INT, \ + .abi_index = 0xffu, \ + .flags = NATIVE_REG_RESERVED | (role), \ + .spill_cost = 0u, \ .copy_cost = 0u} +#define RV_PHYS_INT_RESERVED(r) \ + RV_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_NONE) +#define RV_PHYS_INT_RESERVED_CALLER(r) \ + RV_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_CALLER_SAVED) /* t0..t3 (x5,x6,x7,x28) are backend-private hook/asm temporaries * (RV_TMP0..RV_TMP3), reserved and never exposed as optimizer operand @@ -471,6 +475,8 @@ static const Reg rv_ndt_int_allocable[] = {10u, 11u, 12u, 13u, 14u, 15u, 16u, 17u}; /* a0..a7 */ static const Reg rv_int_scratch[] = {29u, 30u, 31u}; /* t4, t5, t6 */ static const Reg rv_int_asm_temps[] = {RV_TMP2, RV_TMP3}; +static const Reg rv_direct_asm_int[] = {10u, 11u, 12u, 13u, 14u, 15u, + 16u, 17u, 29u, 30u, 31u}; static const NativePhysRegInfo rv_int_phys[] = { RV_PHYS_INT_RESERVED(0u), /* zero */ @@ -478,10 +484,10 @@ static const NativePhysRegInfo rv_int_phys[] = { RV_PHYS_INT_RESERVED(2u), /* sp */ RV_PHYS_INT_RESERVED(3u), /* gp */ RV_PHYS_INT_RESERVED(4u), /* tp */ - RV_PHYS_INT_RESERVED(5u), /* t0 = TMP0 */ - RV_PHYS_INT_RESERVED(6u), /* t1 = TMP1 */ - RV_PHYS_INT_RESERVED(7u), /* t2 = TMP2 (backend/asm) */ - RV_PHYS_INT_RESERVED(8u), /* s0/fp */ + RV_PHYS_INT_RESERVED_CALLER(5u), /* t0 = TMP0 */ + RV_PHYS_INT_RESERVED_CALLER(6u), /* t1 = TMP1 */ + RV_PHYS_INT_RESERVED_CALLER(7u), /* t2 = TMP2 (backend/asm) */ + RV_PHYS_INT_RESERVED_ROLE(8u, NATIVE_REG_CALLEE_SAVED), /* s0/fp */ RV_PHYS_INT_CALLEE(9u), /* s1 */ RV_PHYS_INT_ARG(10u, 0u), RV_PHYS_INT_ARG(11u, 1u), RV_PHYS_INT_ARG(12u, 2u), RV_PHYS_INT_ARG(13u, 3u), @@ -492,10 +498,10 @@ static const NativePhysRegInfo rv_int_phys[] = { RV_PHYS_INT_CALLEE(22u), RV_PHYS_INT_CALLEE(23u), RV_PHYS_INT_CALLEE(24u), RV_PHYS_INT_CALLEE(25u), RV_PHYS_INT_CALLEE(26u), RV_PHYS_INT_CALLEE(27u), - RV_PHYS_INT_RESERVED(28u), /* t3 = TMP3 (backend/asm) */ - RV_PHYS_INT_RESERVED(29u), /* t4 = O1 operand temp / O0 scratch */ - RV_PHYS_INT_RESERVED(30u), /* t5 = O1 operand temp / O0 scratch */ - RV_PHYS_INT_RESERVED(31u), /* t6 = O1 operand temp / O0 scratch */ + RV_PHYS_INT_RESERVED_CALLER(28u), /* t3 = TMP3 (backend/asm) */ + RV_PHYS_INT_RESERVED_CALLER(29u), /* t4 = O1 operand temp / O0 scratch */ + RV_PHYS_INT_RESERVED_CALLER(30u), /* t5 = O1 operand temp / O0 scratch */ + RV_PHYS_INT_RESERVED_CALLER(31u), /* t6 = O1 operand temp / O0 scratch */ }; #define RV_PHYS_FP_ARG(r, idx) \ @@ -520,12 +526,13 @@ static const NativePhysRegInfo rv_int_phys[] = { .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \ .spill_cost = 4u, \ .copy_cost = 1u} -#define RV_PHYS_FP_RESERVED(r) \ - {.reg = (r), \ - .cls = NATIVE_REG_FP, \ - .abi_index = 0xffu, \ - .flags = NATIVE_REG_RESERVED, \ - .spill_cost = 0u, \ +#define RV_PHYS_FP_RESERVED(r) \ + {.reg = (r), \ + .cls = NATIVE_REG_FP, \ + .abi_index = 0xffu, \ + .flags = NATIVE_REG_RESERVED | \ + NATIVE_REG_CALLER_SAVED, \ + .spill_cost = 0u, \ .copy_cost = 0u} /* NDT (-O0) fp value-cache / scratch pool (read only by NativeDirectTarget; the @@ -544,6 +551,9 @@ static const Reg rv_ndt_fp_allocable[] = { 28u, 29u, 30u, 31u}; /* fa0-7,ft4-7,ft8-11 */ static const Reg rv_fp_scratch[] = {2u, 3u}; /* ft2, ft3 */ static const Reg rv_fp_asm_temps[] = {RV_FTMP0, RV_FTMP1}; +static const Reg rv_direct_asm_fp[] = {10u, 11u, 12u, 13u, 14u, 15u, + 16u, 17u, 4u, 5u, 6u, 7u, + 28u, 29u, 30u, 31u}; static const NativePhysRegInfo rv_fp_phys[] = { RV_PHYS_FP_RESERVED(0u), /* ft0 = FTMP0 */ @@ -573,18 +583,12 @@ static const NativeAllocClassInfo rv_classes[] = { .nemit_temps = sizeof rv_int_scratch / sizeof rv_int_scratch[0], .asm_temps = rv_int_asm_temps, .nasm_temps = sizeof rv_int_asm_temps / sizeof rv_int_asm_temps[0], + .direct_asm_allocable = rv_direct_asm_int, + .ndirect_asm_allocable = + sizeof rv_direct_asm_int / sizeof rv_direct_asm_int[0], .emit_cache_mask = (1u << 29u) | (1u << 30u) | (1u << 31u), .phys = rv_int_phys, - .nphys = sizeof rv_int_phys / sizeof rv_int_phys[0], - /* t0-t6 (5-7,28-31) + a0-a7 (10-17) */ - .caller_saved_mask = 0xf00000e0u | 0x0003fc00u, - /* s0-s11 (8,9,18-27) */ - .callee_saved_mask = 0x0ffc0300u, - .arg_mask = 0x0003fc00u, - .ret_mask = 0x00000c00u, - /* zero,ra,sp,gp,tp,t0,t1,t2,s0 (bits 0-8) + t3 (bit 28). The validated - * emit_temps bank adds t4/t5/t6 to the optimizer's reserved mask. */ - .reserved_mask = 0x000001ffu | (1u << 28)}, + .nphys = sizeof rv_int_phys / sizeof rv_int_phys[0]}, {.cls = NATIVE_REG_FP, .ndt_allocable = rv_ndt_fp_allocable, .ndt_allocable_count = @@ -595,16 +599,12 @@ static const NativeAllocClassInfo rv_classes[] = { .nemit_temps = sizeof rv_fp_scratch / sizeof rv_fp_scratch[0], .asm_temps = rv_fp_asm_temps, .nasm_temps = sizeof rv_fp_asm_temps / sizeof rv_fp_asm_temps[0], + .direct_asm_allocable = rv_direct_asm_fp, + .ndirect_asm_allocable = + sizeof rv_direct_asm_fp / sizeof rv_direct_asm_fp[0], .emit_cache_mask = (1u << 2u) | (1u << 3u), .phys = rv_fp_phys, - .nphys = sizeof rv_fp_phys / sizeof rv_fp_phys[0], - /* ft0-ft7 (0-7), fa0-fa7 (10-17), ft8-ft11 (28-31) */ - .caller_saved_mask = 0xf00000ffu | 0x0003fc00u, - /* fs0-fs11 (8,9,18-27) */ - .callee_saved_mask = 0x0ffc0300u, - .arg_mask = 0x0003fc00u, - .ret_mask = 0x00000c00u, - .reserved_mask = 0x0000000fu /* ft0-ft3 */}, + .nphys = sizeof rv_fp_phys / sizeof rv_fp_phys[0]}, }; /* Resolve a register name ("a7", "fa0", ...) to its (class, Reg). Powers the @@ -1115,7 +1115,7 @@ static void rv_store(NativeTarget* t, NativeAddr addr, NativeLoc src, /* copy_bytes: resolve dst and src to dedicated pointer regs (RV_TMP3 / RV_TMP0) * once, then copy granule-by-granule advancing both pointers. dst is resolved * first because its base may itself live in RV_TMP1 (the transfer reg, e.g. the - * sret pointer from plan_ret); capturing it into RV_TMP3 before src resolution + * sret pointer from marshal_ret); capturing it into RV_TMP3 before src resolution * (which may clobber RV_TMP1 for far offsets) keeps it live. Advancing the * pointers keeps every load/store at offset 0, so no offset ever exceeds imm12 * and the transfer reg never aliases a base. */ @@ -2138,32 +2138,21 @@ static void rv_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, * callee-save). This is the same register selection the per-block spill used, * hoisted into the prologue. Writes up to `cap` per-class masks into `out` and * returns the class count to reserve. */ +static int rv_asm_reg_is_callee_saved(NativeTarget* t, NativeAllocClass cls, + Reg r) { + (void)t; + return (cls == NATIVE_REG_INT && rv_reg_is_callee_int(r)) || + (cls == NATIVE_REG_FP && rv_reg_is_callee_fp(r)); +} + static u32 rv_known_callee_saves(NativeTarget* t, const NativeKnownFrameDesc* frame, u32* out, u32 cap) { - u32 ncls = frame->ncallee_classes; - u32 clob_int = 0, clob_fp = 0, abi_int, abi_fp; - if (ncls > cap) ncls = cap; - for (u32 c = 0; c < ncls; ++c) - out[c] = frame->callee_saved_used ? frame->callee_saved_used[c] : 0u; - if (frame->asm_clobbers && frame->nasm_clobbers) { - RvNativeTarget* a = rv_of(t); - SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; - rv_asm_clobber_masks(t->c, loc, frame->asm_clobbers, frame->nasm_clobbers, - &clob_int, &clob_fp); - } - native_asm_abi_clobber_masks(t, frame->asm_clobber_abi_sets, &abi_int, - &abi_fp); - clob_int |= abi_int; - clob_fp |= abi_fp; - for (Reg r = 0; r < 32u; ++r) { - if (NATIVE_REG_INT < ncls && (clob_int & (1u << r)) && - rv_reg_is_callee_int(r)) - out[NATIVE_REG_INT] |= 1u << r; - if (NATIVE_REG_FP < ncls && (clob_fp & (1u << r)) && rv_reg_is_callee_fp(r)) - out[NATIVE_REG_FP] |= 1u << r; - } - return ncls; + RvNativeTarget* a = rv_of(t); + SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; + return native_asm_known_callee_saves( + t, loc, frame, out, cap, rv_asm_clobber_masks, + rv_asm_reg_is_callee_saved); } static u32 rv_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type, @@ -2187,8 +2176,8 @@ static void rv_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, rv_func_begin_common(t, fd); a->frame.known_frame = 1; if (frame) { - u32 cs[NATIVE_CALL_PLAN_CLASSES]; - u32 ncs = rv_known_callee_saves(t, frame, cs, NATIVE_CALL_PLAN_CLASSES); + u32 cs[NATIVE_REG_CLASS_COUNT]; + u32 ncs = rv_known_callee_saves(t, frame, cs, NATIVE_REG_CLASS_COUNT); a->frame.has_alloca = frame->has_alloca; if (ncs) rv_reserve_callee_saves(t, cs, ncs); for (i = 0; i < frame->nslots; ++i) { @@ -2271,7 +2260,7 @@ static const ABIArgInfo* rv_param_abi(NativeTarget* t, const ABIFuncInfo* abi, parts[p].align = gpr; parts[p].src_offset = off; } - scratch->nparts = (u16)nparts; + scratch->nparts = nparts; scratch->parts = parts; return scratch; } @@ -2624,14 +2613,15 @@ static void rv_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves, s.reg_move = rv_move; s.scratch[NATIVE_REG_INT] = RV_TMP1; s.scratch[NATIVE_REG_FP] = RV_FTMP1; + s.scratch_class_mask = (1u << NATIVE_REG_INT) | (1u << NATIVE_REG_FP); native_arg_shuffle(&s, moves, n); } -static void rv_plan_call(NativeTarget* t, const NativeCallDesc* desc, - NativeCallPlan* plan) { +static void rv_marshal_call(NativeTarget* t, const NativeCallDesc* desc, + NativeCallPhase* plan) { RvNativeTarget* a = rv_of(t); const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); - NativeCallPlanRet* rets; + NativeCallPhaseRet* rets; KitCgTypeId i64t = builtin_id(KIT_CG_BUILTIN_I64); /* Right-size the result scratch to the exact number of entries the ret loops * below write: nparts on a DIRECT register return, 1 on the !abi fallback, @@ -2640,7 +2630,7 @@ static void rv_plan_call(NativeTarget* t, const NativeCallDesc* desc, ? abi->ret.nparts : ((!abi && desc->nresults) ? 1u : 0u); memset(plan, 0, sizeof *plan); - rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPlanRet, nrets_cap) : NULL; + rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPhaseRet, nrets_cap) : NULL; plan->callee = desc->callee; plan->rets = rets; plan->flags = desc->flags; @@ -2800,7 +2790,7 @@ static void rv_emit_tail_site(NativeTarget* t, NativeLoc callee) { } } -static void rv_emit_call(NativeTarget* t, const NativeCallPlan* plan) { +static void rv_emit_call(NativeTarget* t, const NativeCallPhase* plan) { MCEmitter* mc = t->mc; ObjSecId sec = mc->section_id; if (plan->flags & CG_CALL_TAIL) { @@ -2822,14 +2812,14 @@ static void rv_emit_call(NativeTarget* t, const NativeCallPlan* plan) { rv_panic(rv_of(t), "unsupported call target"); } -static void rv_plan_ret(NativeTarget* t, const CGFuncDesc* fd, - const NativeLoc* value, NativeCallPlanRet** out_rets, - u32* out_nrets) { +static void rv_marshal_ret(NativeTarget* t, const CGFuncDesc* fd, + const NativeLoc* value, + NativeCallPhaseRet** out_rets, u32* out_nrets) { RvNativeTarget* a = rv_of(t); const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type); - NativeCallPlanRet* rets = NULL; + NativeCallPhaseRet* rets = NULL; u32 nr = 0; - if (value) rets = arena_zarray(t->c->tu, NativeCallPlanRet, 4); + if (value) rets = arena_zarray(t->c->tu, NativeCallPhaseRet, 4); if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) { KitCgTypeId i64t = builtin_id(KIT_CG_BUILTIN_I64); NativeLoc dstp = native_loc_reg(i64t, NATIVE_REG_INT, RV_TMP1); @@ -3746,17 +3736,30 @@ static void rv_intrinsic(NativeTarget* t, IntrinKind kind, * ABI registers even though the semantic IR operands are ordinary virtual * values. Keep live-across allocations out of those fixed destinations. */ static int rv_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, - u32 mask[NATIVE_CALL_PLAN_CLASSES]) { - (void)t; + u32 mask[NATIVE_REG_CLASS_COUNT]) { mask[0] = mask[1] = mask[2] = 0; - if ((NativeMachineOpKind)op->kind != NATIVE_MOP_INTRINSIC || - (IntrinKind)op->intrin != INTRIN_SYSCALL) - return 0; - /* a7 = syscall number; a0..a5 = up to six arguments / a0 result. */ - mask[NATIVE_REG_INT] = (1u << RV_A0) | (1u << RV_A1) | (1u << RV_A2) | - (1u << RV_A3) | (1u << RV_A4) | (1u << RV_A5) | - (1u << RV_A7); - return 1; + switch ((NativeMachineOpKind)op->kind) { + case NATIVE_MOP_INTRINSIC: + if ((IntrinKind)op->intrin != INTRIN_SYSCALL) return 0; + /* a7 = syscall number; a0..a5 = arguments / a0 result. */ + mask[NATIVE_REG_INT] = (1u << RV_A0) | (1u << RV_A1) | (1u << RV_A2) | + (1u << RV_A3) | (1u << RV_A4) | (1u << RV_A5) | + (1u << RV_A7); + return 1; + case NATIVE_MOP_BINOP: + case NATIVE_MOP_BITFIELD_LOAD: + case NATIVE_MOP_BITFIELD_STORE: + case NATIVE_MOP_VA_START: + case NATIVE_MOP_VA_ARG: + case NATIVE_MOP_ATOMIC_CAS: + case NATIVE_MOP_ATOMIC_RMW: + case NATIVE_MOP_TLS_ADDR: + return 0; + case NATIVE_MOP_COUNT: + break; + } + rv_panic(rv_of(t), "invalid machine-effect operation"); + return 0; } /* ============================ inline asm ============================ */ @@ -3839,32 +3842,6 @@ static void rv_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, /* Pin resolution + panic is the shared native_asm_bind_direct_operands path. */ -/* Pick a free register from the arch's caller-saved allocable pools for an - * asm operand the direct path must self-allocate. */ -static Reg rv_asm_alloc_reg(NativeDirectTarget* d, NativeAllocClass cls, - u32 allowed_mask, u32* used_int, u32* used_fp) { - /* int: a0..a7 (10..17) then O0-owned t4..t6. */ - static const Reg int_pool[] = {10u, 11u, 12u, 13u, 14u, 15u, - 16u, 17u, 29u, 30u, 31u}; - /* fp: fa0..fa7 (10..17) then ft caller-saved. */ - static const Reg fp_pool[] = {10u, 11u, 12u, 13u, 14u, 15u, 16u, 17u, - 4u, 5u, 6u, 7u, 28u, 29u, 30u, 31u}; - const Reg* pool = cls == NATIVE_REG_FP ? fp_pool : int_pool; - u32 n = cls == NATIVE_REG_FP ? (u32)(sizeof fp_pool / sizeof fp_pool[0]) - : (u32)(sizeof int_pool / sizeof int_pool[0]); - u32* used = cls == NATIVE_REG_FP ? used_fp : used_int; - u32 i; - for (i = 0; i < n; ++i) { - Reg r = pool[i]; - if (allowed_mask && (allowed_mask & (1u << r)) == 0) continue; - if ((*used & (1u << r)) != 0) continue; - *used |= 1u << r; - return r; - } - rv_asm_panic(d, "out of registers for asm operands"); - return REG_NONE; -} - /* Direct (-O0) path: resolve a semantic Operand to a NativeAddr. */ static NativeAddr rv_direct_addr(NativeDirectTarget* d, Operand op) { NativeAddr addr; @@ -4197,9 +4174,9 @@ NativeTarget* rv64_native_target_new(Compiler* c, ObjBuilder* obj, t->alloca_ = rv_alloca; t->spill = rv_spill; t->reload = rv_reload; - t->plan_call = rv_plan_call; + t->marshal_call = rv_marshal_call; t->emit_call = rv_emit_call; - t->plan_ret = rv_plan_ret; + t->marshal_ret = rv_marshal_ret; t->ret = rv_ret; t->atomic_load = rv_atomic_load; t->atomic_store = rv_atomic_store; @@ -4376,20 +4353,11 @@ static void rv_direct_asm_block(NativeDirectTarget* d, const char* tmpl, const Sym* clobbers, u32 nclob, u32 clobber_abi_sets) { static const NativeAsmDirectHooks hooks = { - /* Reserve backend temps t0..t3, SP/GP/TP/zero/RA, and the frame pointer so - * the direct asm operand allocator never hands them out. */ - .reserved_int_mask = - (1u << RV_ZERO) | (1u << RV_RA) | (1u << RV_SP) | (1u << RV_GP) | - (1u << RV_TP) | (1u << RV_TMP0) | (1u << RV_TMP1) | - (1u << RV_TMP2) | (1u << RV_TMP3) | (1u << RV_S0), - .reserved_fp_mask = - (1u << RV_FTMP0) | (1u << RV_FTMP1) | (1u << 2u) | (1u << 3u), .opk_reg = RV64_INLINE_OPK_REG, .opcls_fp = RV64_INLINE_OPCLS_FP, .panic = rv_asm_panic, .bound_reg = rv_asm_bound_reg, .bound_mem = rv_asm_bound_mem, - .alloc_reg = rv_asm_alloc_reg, .clobber_masks = rv_asm_clobber_masks, .save_callee_clobbers = rv_asm_hook_save_callee_clobbers, .restore_one = rv_asm_hook_restore_one, diff --git a/src/arch/x64/emit.h b/src/arch/x64/emit.h @@ -78,7 +78,7 @@ static inline void x64_abi_direct_reg_need(const ABIArgInfo* ai, u32* need_int, *need_int = 0; *need_fp = 0; if (!ai || ai->kind != ABI_ARG_DIRECT) return; - for (u16 i = 0; i < ai->nparts; ++i) { + for (u32 i = 0; i < ai->nparts; ++i) { const ABIArgPart* p = &ai->parts[i]; if (p->cls == ABI_CLASS_FP) ++*need_fp; diff --git a/src/arch/x64/native.c b/src/arch/x64/native.c @@ -94,7 +94,6 @@ typedef struct X64NativeTarget { u32 next_param_stack; u8 has_sret; u8 is_variadic; - u8 call_callee_parked; NativeFrameSlot sret_ptr_slot; NativeFrameSlot reg_save_slot; /* SysV variadic 176B __va_list_tag area */ @@ -131,11 +130,6 @@ typedef struct X64NativeTarget { NativeArgMove bind_moves[X64_MAX_BIND_MOVES]; u32 nbind_moves; - /* Integer data scratch used while packing/unpacking a non-power-of-two ABI - * part. Normally r10, keeping r11 exclusively available to resolve complex - * addresses. */ - Reg part_scratch; - const X64ABIRegs* abi; } X64NativeTarget; @@ -226,13 +220,19 @@ static void emit_jcc_rel32(MCEmitter* mc, u32 cc, MCLabel l); .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \ .spill_cost = 4u, \ .copy_cost = 1u} -#define X64_PHYS_INT_RESERVED(r) \ - {.reg = (r), \ - .cls = NATIVE_REG_INT, \ - .abi_index = 0xffu, \ - .flags = NATIVE_REG_RESERVED, \ - .spill_cost = 0u, \ +#define X64_PHYS_INT_RESERVED_ROLE(r, role) \ + {.reg = (r), \ + .cls = NATIVE_REG_INT, \ + .abi_index = 0xffu, \ + .flags = NATIVE_REG_RESERVED | (role), \ + .spill_cost = 0u, \ .copy_cost = 0u} +#define X64_PHYS_INT_RESERVED(r) \ + X64_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_NONE) +#define X64_PHYS_INT_RESERVED_CALLER(r) \ + X64_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_CALLER_SAVED) +#define X64_PHYS_INT_RESERVED_CALLEE(r) \ + X64_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_CALLEE_SAVED) /* The NDT (-O0) value-cache / scratch pool (read only by NativeDirectTarget; * the optimizer allocates over the phys[] ALLOCABLE flags instead, which also @@ -247,21 +247,23 @@ static const Reg x64_ndt_int_allocable[] = {X64_RSI, X64_RDI, X64_R13, X64_R14, X64_R15}; static const Reg x64_int_scratch[] = {X64_R8, X64_R9}; static const Reg x64_int_asm_temps[] = {X64_TMP_INT, X64_TMP_INT2}; +static const Reg x64_direct_asm_int[] = {X64_RDI, X64_RSI, X64_RDX, X64_RCX}; static const NativePhysRegInfo x64_int_phys[] = { - X64_PHYS_INT_RESERVED(X64_RAX), /* return / div-mul (asm-pinnable) */ + X64_PHYS_INT_RESERVED_ROLE( + X64_RAX, NATIVE_REG_CALLER_SAVED | NATIVE_REG_RET), X64_PHYS_INT_ARG(X64_RCX), X64_PHYS_INT_RET_ARG(X64_RDX), - X64_PHYS_INT_RESERVED(X64_RBX), + X64_PHYS_INT_RESERVED_CALLEE(X64_RBX), X64_PHYS_INT_RESERVED(X64_RSP), /* stack pointer */ X64_PHYS_INT_RESERVED(X64_RBP), /* frame pointer */ X64_PHYS_INT_ARG(X64_RSI), X64_PHYS_INT_ARG(X64_RDI), X64_PHYS_INT_ARG_RESERVED(X64_R8), /* O1 operand temp / O0 scratch */ X64_PHYS_INT_ARG_RESERVED(X64_R9), /* O1 operand temp / O0 scratch */ - X64_PHYS_INT_RESERVED(X64_R10), /* backend-private temp */ - X64_PHYS_INT_RESERVED(X64_R11), /* backend-private temp */ - X64_PHYS_INT_RESERVED(X64_R12), + X64_PHYS_INT_RESERVED_CALLER(X64_R10), /* backend-private temp */ + X64_PHYS_INT_RESERVED_CALLER(X64_R11), /* backend-private temp */ + X64_PHYS_INT_RESERVED_CALLEE(X64_R12), X64_PHYS_INT_CALLEE(X64_R13), X64_PHYS_INT_CALLEE(X64_R14), X64_PHYS_INT_CALLEE(X64_R15), @@ -296,12 +298,13 @@ static const NativePhysRegInfo x64_int_phys[] = { .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED, \ .spill_cost = 1u, \ .copy_cost = 1u} -#define X64_PHYS_FP_RESERVED(r) \ - {.reg = (r), \ - .cls = NATIVE_REG_FP, \ - .abi_index = 0xffu, \ - .flags = NATIVE_REG_RESERVED, \ - .spill_cost = 0u, \ +#define X64_PHYS_FP_RESERVED(r) \ + {.reg = (r), \ + .cls = NATIVE_REG_FP, \ + .abi_index = 0xffu, \ + .flags = NATIVE_REG_RESERVED | \ + NATIVE_REG_CALLER_SAVED, \ + .spill_cost = 0u, \ .copy_cost = 0u} /* Allocable FP pool: arg/ret xmm0..xmm3 lead so -O0 fp-arg producers @@ -316,12 +319,16 @@ static const Reg x64_ndt_fp_allocable[] = { X64_XMM0 + 10, X64_XMM0 + 11}; static const Reg x64_fp_scratch[] = {X64_XMM4, X64_XMM5}; static const Reg x64_fp_asm_temps[] = {X64_TMP_FP, X64_TMP_FP2}; +static const Reg x64_direct_asm_fp[] = { + X64_XMM0, X64_XMM1, X64_XMM2, X64_XMM3, + X64_XMM6, X64_XMM7, X64_XMM8, X64_XMM0 + 9, + X64_XMM0 + 10, X64_XMM0 + 11}; static const NativePhysRegInfo x64_fp_phys[] = { X64_PHYS_FP_ARG_RET(X64_XMM0), X64_PHYS_FP_ARG_RET(X64_XMM1), X64_PHYS_FP_ARG(X64_XMM2), X64_PHYS_FP_ARG(X64_XMM3), X64_PHYS_FP_ARG_RESERVED(X64_XMM4), X64_PHYS_FP_ARG_RESERVED(X64_XMM5), - X64_PHYS_FP_CALLER(X64_XMM6), X64_PHYS_FP_CALLER(X64_XMM7), + X64_PHYS_FP_ARG(X64_XMM6), X64_PHYS_FP_ARG(X64_XMM7), X64_PHYS_FP_CALLER(X64_XMM8), X64_PHYS_FP_CALLER(X64_XMM0 + 9), X64_PHYS_FP_CALLER(X64_XMM0 + 10), X64_PHYS_FP_CALLER(X64_XMM0 + 11), X64_PHYS_FP_RESERVED(X64_XMM0 + 12), X64_PHYS_FP_RESERVED(X64_XMM0 + 13), @@ -340,24 +347,12 @@ static const NativeAllocClassInfo x64_classes[] = { .nemit_temps = sizeof x64_int_scratch / sizeof x64_int_scratch[0], .asm_temps = x64_int_asm_temps, .nasm_temps = sizeof x64_int_asm_temps / sizeof x64_int_asm_temps[0], + .direct_asm_allocable = x64_direct_asm_int, + .ndirect_asm_allocable = + sizeof x64_direct_asm_int / sizeof x64_direct_asm_int[0], .emit_cache_mask = (1u << X64_R8) | (1u << X64_R9), .phys = x64_int_phys, - .nphys = sizeof x64_int_phys / sizeof x64_int_phys[0], - /* caller-saved: rax,rcx,rdx,rsi,rdi,r8,r9,r10,r11 (SysV) */ - .caller_saved_mask = (1u << X64_RAX) | (1u << X64_RCX) | (1u << X64_RDX) | - (1u << X64_RSI) | (1u << X64_RDI) | (1u << X64_R8) | - (1u << X64_R9) | (1u << X64_R10) | (1u << X64_R11), - /* callee-saved: rbx,r12,r13,r14,r15 (rbp handled by prologue head) */ - .callee_saved_mask = (1u << X64_RBX) | (1u << X64_R12) | (1u << X64_R13) | - (1u << X64_R14) | (1u << X64_R15), - /* SysV arg regs rdi,rsi,rdx,rcx,r8,r9 */ - .arg_mask = (1u << X64_RDI) | (1u << X64_RSI) | (1u << X64_RDX) | - (1u << X64_RCX) | (1u << X64_R8) | (1u << X64_R9), - .ret_mask = (1u << X64_RAX) | (1u << X64_RDX), - /* rax/rsp/rbp reserved; r8/r9 operand temps; r10/r11 backend-private */ - .reserved_mask = (1u << X64_RAX) | (1u << X64_RSP) | (1u << X64_RBP) | - (1u << X64_R8) | (1u << X64_R9) | (1u << X64_R10) | - (1u << X64_R11) | (1u << X64_RBX) | (1u << X64_R12)}, + .nphys = sizeof x64_int_phys / sizeof x64_int_phys[0]}, {.cls = NATIVE_REG_FP, .ndt_allocable = x64_ndt_fp_allocable, .ndt_allocable_count = @@ -368,18 +363,12 @@ static const NativeAllocClassInfo x64_classes[] = { .nemit_temps = sizeof x64_fp_scratch / sizeof x64_fp_scratch[0], .asm_temps = x64_fp_asm_temps, .nasm_temps = sizeof x64_fp_asm_temps / sizeof x64_fp_asm_temps[0], + .direct_asm_allocable = x64_direct_asm_fp, + .ndirect_asm_allocable = + sizeof x64_direct_asm_fp / sizeof x64_direct_asm_fp[0], .emit_cache_mask = (1u << X64_XMM4) | (1u << X64_XMM5), .phys = x64_fp_phys, - .nphys = sizeof x64_fp_phys / sizeof x64_fp_phys[0], - /* All xmm caller-saved on SysV. */ - .caller_saved_mask = 0xffffu, - .callee_saved_mask = 0u, - .arg_mask = 0xffu, /* xmm0..xmm7 */ - .ret_mask = (1u << X64_XMM0) | (1u << X64_XMM1), - /* xmm4/xmm5 operand temps; xmm14/xmm15 backend-private. */ - .reserved_mask = (1u << X64_XMM4) | (1u << X64_XMM5) | - (1u << (X64_XMM0 + 12)) | (1u << (X64_XMM0 + 13)) | - (1u << (X64_XMM0 + 14)) | (1u << X64_XMM15)}, + .nphys = sizeof x64_fp_phys / sizeof x64_fp_phys[0]}, }; /* Resolve a register name ("r10", "xmm3", ...) to its (class, Reg). Powers the @@ -769,47 +758,35 @@ static int x64_native_part_width(u32 size) { return size == 1u || size == 2u || size == 4u || size == 8u; } -static int x64_addr_explicitly_uses_reg(const NativeAddr* addr, Reg reg) { - return (addr->base_kind == NATIVE_ADDR_BASE_REG && - (addr->base.reg & 0xfu) == reg) || - (addr->index_kind == NATIVE_ADDR_INDEX_REG && - (addr->index.reg & 0xfu) == reg); +/* The two integer registers in the backend-private bank. Exact-width helpers + * assign them explicit address/data roles for their complete local phase. */ +static int x64_private_int_reg(Reg reg) { + return reg == X64_TMP_INT || reg == X64_TMP_INT2; } -/* A multi-chunk load cannot overwrite an explicit base/index with its first - * chunk and then reuse that destroyed register for the tail. Pin the complete - * effective address in r11 first. Generic NativeTarget destinations never use - * the backend-private r10/r11 bank; ABI-owned r10 loads use rbp/rsp storage and - * therefore do not enter this alias path. */ -static NativeAddr x64_stabilize_part_load_addr(X64NativeTarget* a, - NativeAddr addr, Reg dst) { +static Reg x64_other_private_int(Reg reg) { + return reg == X64_TMP_INT ? X64_TMP_INT2 : X64_TMP_INT; +} + +/* Resolve the complete effective address into one explicitly selected private + * register. Exact-width multi-chunk operations keep this address stable while + * their other private register carries chunk data; callers no longer depend on + * x64_emit_mem's internal address-scratch choice. */ +static NativeAddr x64_stabilize_part_addr(X64NativeTarget* a, NativeAddr addr, + Reg addr_reg) { u32 base; KitCgTypeId base_type; - if (!x64_addr_explicitly_uses_reg(&addr, dst)) return addr; - if (dst == X64_TMP_INT || dst == X64_TMP_INT2) - x64_panic(a, "exact-width load aliases private address scratch"); - base_type = addr.base_type; - base = x64_addr_to_base_reg(a, addr, X64_TMP_INT2); - if (base != X64_TMP_INT2) - emit_mov_rr(a->base.mc, 1, X64_TMP_INT2, base); + base = x64_addr_to_base_reg(a, addr, addr_reg); + if (base != addr_reg) emit_mov_rr(a->base.mc, 1, addr_reg, base); memset(&addr, 0, sizeof addr); addr.base_kind = NATIVE_ADDR_BASE_REG; addr.cls = NATIVE_REG_INT; - addr.base.reg = X64_TMP_INT2; + addr.base.reg = addr_reg; addr.base_type = base_type; return addr; } -static Reg x64_part_scratch(const X64NativeTarget* a, Reg avoid) { - Reg scratch = a->part_scratch; - if (scratch != X64_TMP_INT && scratch != X64_TMP_INT2) - scratch = X64_TMP_INT2; - if (scratch == avoid) - scratch = scratch == X64_TMP_INT ? X64_TMP_INT2 : X64_TMP_INT; - return scratch; -} - /* Load an exact-width little-endian integer ABI part. x86 has native memory * operations only for 1/2/4/8 bytes, so a 3/5/6/7-byte tail is assembled from * bounded chunks in the low bits of dst. No chunk crosses the semantic part. */ @@ -818,9 +795,11 @@ static void x64_load_int_part(X64NativeTarget* a, NativeLoc dst, NativePartChunkIter chunks; NativeTarget* t = &a->base; Reg rd = (Reg)loc_reg(dst); - Reg scratch; + Reg addr_reg; + Reg data_reg; NativeLoc tmp; u32 chunk_off, chunk_size; + int private_dst; int first = 1; if (x64_native_part_width(size)) { @@ -831,21 +810,30 @@ static void x64_load_int_part(X64NativeTarget* a, NativeLoc dst, if (size == 0u || size > 8u || native_loc_is_fp(dst)) x64_panic(a, "invalid exact-width integer part load"); - addr = x64_stabilize_part_load_addr(a, addr, rd); - scratch = x64_part_scratch(a, rd); + private_dst = x64_private_int_reg(rd); + addr_reg = private_dst ? x64_other_private_int(rd) : X64_TMP_INT2; + data_reg = private_dst ? addr_reg : X64_TMP_INT; + addr = x64_stabilize_part_addr(a, addr, addr_reg); + /* A private destination consumes both integer temps (one accumulator, one + * chunk destination). Keep the stable address in the private FP hold register + * and restore it immediately before each alias-safe memory load. */ + if (private_dst) + emit_sse_rr_w(t->mc, 0x66, 0x6E, 1, X64_TMP_FP, addr_reg); tmp = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, - scratch); + data_reg); chunks = native_part_chunks(size, 8u); while (native_part_chunk_next(&chunks, &chunk_off, &chunk_size)) { NativeAddr chunk_addr = addr; NativeLoc chunk_dst = first ? dst : tmp; chunk_addr.offset += (i32)chunk_off; mem.size = chunk_size; + if (private_dst) + emit_sse_rr_w(t->mc, 0x66, 0x7E, 1, X64_TMP_FP, addr_reg); x64_emit_mem(a, 1, chunk_dst, chunk_addr, mem); if (!first) { - emit_shift_imm(t->mc, 1, X64_SHIFT_SUB_SHL, scratch, + emit_shift_imm(t->mc, 1, X64_SHIFT_SUB_SHL, data_reg, (u8)(chunk_off * 8u)); - emit_alu_rr(t->mc, 1, X64_OPC_ALU_OR, rd, scratch); + emit_alu_rr(t->mc, 1, X64_OPC_ALU_OR, rd, data_reg); } first = 0; } @@ -859,9 +847,11 @@ static void x64_store_int_part(X64NativeTarget* a, NativeAddr addr, NativePartChunkIter chunks; NativeTarget* t = &a->base; Reg rs = (Reg)loc_reg(src); - Reg scratch; + Reg addr_reg; + Reg data_reg; NativeLoc tmp; u32 chunk_off, chunk_size, shifted = 0; + int private_src; int copied = 0; if (x64_native_part_width(size)) { @@ -872,20 +862,20 @@ static void x64_store_int_part(X64NativeTarget* a, NativeAddr addr, if (size == 0u || size > 8u || native_loc_is_fp(src)) x64_panic(a, "invalid exact-width integer part store"); - scratch = x64_part_scratch(a, rs); - /* x64_resolve_addr owns r11 for FRAME_VALUE/GOT materialization. The only - * ABI paths whose packed source itself occupies r10 store to plain rbp/rsp - * addresses; reject a future complex-address use instead of replacing the - * shifted data with an address component. Generic emitter operands never - * occupy either backend-private register and therefore use r10 above. */ - if (scratch == X64_TMP_INT2 && - (addr.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE || - addr.index_kind == NATIVE_ADDR_INDEX_FRAME_VALUE || - (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL && - x64_use_got_for_sym(t, addr.base.global.sym)))) - x64_panic(a, "exact-width store aliases address scratch"); + private_src = x64_private_int_reg(rs); + addr_reg = private_src ? x64_other_private_int(rs) : X64_TMP_INT2; + data_reg = private_src ? rs : X64_TMP_INT; + /* Preserve a backend-private source while complete address stabilization is + * free to use both integer temps. Restore it after the stores as well: these + * helpers promise not to consume their source even when another backend + * marshalling phase supplied it from the private bank. */ + if (private_src) + emit_sse_rr_w(t->mc, 0x66, 0x6E, 1, X64_TMP_FP, rs); + addr = x64_stabilize_part_addr(a, addr, addr_reg); + if (private_src) + emit_sse_rr_w(t->mc, 0x66, 0x7E, 1, X64_TMP_FP, data_reg); tmp = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, - scratch); + data_reg); chunks = native_part_chunks(size, 8u); while (native_part_chunk_next(&chunks, &chunk_off, &chunk_size)) { NativeAddr chunk_addr = addr; @@ -893,10 +883,10 @@ static void x64_store_int_part(X64NativeTarget* a, NativeAddr addr, chunk_addr.offset += (i32)chunk_off; if (chunk_off != 0u) { if (!copied) { - emit_mov_rr(t->mc, 1, scratch, rs); + emit_mov_rr(t->mc, 1, data_reg, rs); copied = 1; } - emit_shift_imm(t->mc, 1, X64_SHIFT_SUB_SHR, scratch, + emit_shift_imm(t->mc, 1, X64_SHIFT_SUB_SHR, data_reg, (u8)((chunk_off - shifted) * 8u)); shifted = chunk_off; chunk_src = tmp; @@ -904,6 +894,8 @@ static void x64_store_int_part(X64NativeTarget* a, NativeAddr addr, mem.size = chunk_size; x64_emit_mem(a, 0, chunk_src, chunk_addr, mem); } + if (private_src) + emit_sse_rr_w(t->mc, 0x66, 0x7E, 1, X64_TMP_FP, rs); } /* ============================ moves / data ============================ */ @@ -2042,13 +2034,11 @@ static void x64_func_begin_common(NativeTarget* t, const CGFuncDesc* fd) { a->next_param_stack = 0; a->has_sret = (abi && abi->has_sret) ? 1u : 0u; a->is_variadic = (abi && abi->variadic) ? 1u : 0u; - a->call_callee_parked = 0; a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE; a->reg_save_slot = NATIVE_FRAME_SLOT_NONE; a->npatches = 0; a->nalloca = 0; a->nbind_moves = 0; - a->part_scratch = X64_TMP_INT; a->slim_frame = 0; a->redzone_leaf = 0; /* Single-pass (-O0) reservation: the NDT caches only in its allocable pool's @@ -2148,9 +2138,9 @@ static u32 x64_live_callee_saved_mask(NativeTarget* t, NativeAllocClass cls) { } static u32 x64_live_caller_saved_mask(NativeTarget* t, NativeAllocClass cls) { - const NativeAllocClassInfo* ci = native_target_class_info(t, cls); - if (!ci) return 0; - return ci->caller_saved_mask & ~x64_live_callee_saved_mask(t, cls); + u32 caller_saved = + native_reg_info_flag_mask(t->regs, cls, NATIVE_REG_CALLER_SAVED); + return caller_saved & ~x64_live_callee_saved_mask(t, cls); } static void x64_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, @@ -2169,33 +2159,22 @@ static void x64_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, * reserved-but-callee- saved scratch rbx/r12 (which the caller still expects * preserved). This is the same register selection the per-block spill used, * hoisted into the prologue. */ -static u32 x64_known_callee_saves(NativeTarget* t, const X64ABIRegs* abi, +static int x64_asm_reg_is_callee_saved(NativeTarget* t, NativeAllocClass cls, + Reg r) { + X64NativeTarget* a = x64_of(t); + const X64ABIRegs* abi = a->abi ? a->abi : x64_abi_for_os(t->c->target.os); + return (cls == NATIVE_REG_INT && x64_reg_is_callee_int(abi, r)) || + (cls == NATIVE_REG_FP && x64_reg_is_callee_fp(abi, r)); +} + +static u32 x64_known_callee_saves(NativeTarget* t, const NativeKnownFrameDesc* frame, u32* out, u32 cap) { - u32 ncls = frame->ncallee_classes; - u32 clob_int = 0, clob_fp = 0, abi_int, abi_fp; - if (ncls > cap) ncls = cap; - for (u32 c = 0; c < ncls; ++c) - out[c] = frame->callee_saved_used ? frame->callee_saved_used[c] : 0u; - if (frame->asm_clobbers && frame->nasm_clobbers) { - X64NativeTarget* a = x64_of(t); - SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; - x64_asm_clobber_masks(t->c, loc, frame->asm_clobbers, frame->nasm_clobbers, - &clob_int, &clob_fp); - } - native_asm_abi_clobber_masks(t, frame->asm_clobber_abi_sets, &abi_int, - &abi_fp); - clob_int |= abi_int; - clob_fp |= abi_fp; - for (Reg r = 0; r < 16u; ++r) { - if (NATIVE_REG_INT < ncls && (clob_int & (1u << r)) && - x64_reg_is_callee_int(abi, r)) - out[NATIVE_REG_INT] |= 1u << r; - if (NATIVE_REG_FP < ncls && (clob_fp & (1u << r)) && - x64_reg_is_callee_fp(abi, r)) - out[NATIVE_REG_FP] |= 1u << r; - } - return ncls; + X64NativeTarget* a = x64_of(t); + SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; + return native_asm_known_callee_saves( + t, loc, frame, out, cap, x64_asm_clobber_masks, + x64_asm_reg_is_callee_saved); } /* Optimizer entry point: the full frame is supplied up front, so the prologue @@ -2214,9 +2193,9 @@ static void x64_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, x64_func_begin_common(t, fd); a->frame.known_frame = 1; if (frame) { - u32 cs[NATIVE_CALL_PLAN_CLASSES]; + u32 cs[NATIVE_REG_CLASS_COUNT]; u32 ncs = - x64_known_callee_saves(t, a->abi, frame, cs, NATIVE_CALL_PLAN_CLASSES); + x64_known_callee_saves(t, frame, cs, NATIVE_REG_CLASS_COUNT); a->frame.has_alloca = frame->has_alloca; if (ncs) x64_reserve_callee_saves(t, cs, ncs); for (i = 0; i < frame->nslots; ++i) { @@ -2421,7 +2400,7 @@ static u32 x64_call_stack_raw_size(NativeTarget* t, for (i = 0; i < desc->nargs; ++i) { ABIArgInfo tmp; const ABIArgInfo* ai = x64_param_abi(t, abi, desc, i, &tmp); - u16 p; + u32 p; if (ai->kind == ABI_ARG_IGNORE) continue; if (ai->kind == ABI_ARG_INDIRECT) { if (next_int < aregs->n_int_args) @@ -2598,14 +2577,22 @@ static void x64_addr_of_loc(NativeTarget* t, NativeLoc dst, NativeLoc src) { x64_load_addr(t, dst, addr); } -static void x64_store_outgoing_part(NativeTarget* t, int tail_call, +typedef struct X64CallMarshalCtx { + u32 rsp_bias; + Reg cycle_scratch; + u8 tail; + u8 callee_parked; +} X64CallMarshalCtx; + +static void x64_store_outgoing_part(NativeTarget* t, + const X64CallMarshalCtx* call, u32 stack_off, NativeLoc src, u32 size) { X64NativeTarget* a = x64_of(t); NativeAddr addr; memset(&addr, 0, sizeof addr); addr.base_kind = NATIVE_ADDR_BASE_REG; addr.base_type = src.type; - if (tail_call) { + if (call->tail) { /* A sibling call reuses the caller's frame: its outgoing stack args land in * the caller's incoming-arg window. `stack_off` already includes the * shadow-space prefix (the outgoing cursor starts at shadow_space), so the @@ -2615,10 +2602,9 @@ static void x64_store_outgoing_part(NativeTarget* t, int tail_call, addr.offset = (i32)(16u + stack_off); } else { addr.base.reg = X64_RSP; - /* An indirect callee is transiently pushed below rsp for the whole - * marshal phase. Keep semantic outgoing offsets anchored at the original - * rsp; the balanced pop restores that anchor before call/jmp emission. */ - addr.offset = (i32)(stack_off + (a->call_callee_parked ? 8u : 0u)); + /* Keep semantic outgoing offsets anchored at the pre-marshalling rsp. The + * per-call context owns any transient callee-save bias explicitly. */ + addr.offset = (i32)(stack_off + call->rsp_bias); } { MemAccess mem = native_mem_for_type(t, src.type, size); @@ -2633,8 +2619,9 @@ static void x64_store_outgoing_part(NativeTarget* t, int tail_call, * so copy a non-power-of-two memory part chunk-by-chunk instead of needlessly * packing it into a GPR and unpacking it again. Besides being smaller, this * needs only one private register while an indirect callee may occupy r11. */ -static void x64_copy_outgoing_part(NativeTarget* t, int tail_call, - u32 stack_off, NativeLoc src, +static void x64_copy_outgoing_part(NativeTarget* t, + const X64CallMarshalCtx* call, u32 stack_off, + NativeLoc src, const ABIArgPart* part) { NativeAllocClass cls = part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT; @@ -2648,14 +2635,14 @@ static void x64_copy_outgoing_part(NativeTarget* t, int tail_call, u32 chunk_off, chunk_size; while (native_part_chunk_next(&chunks, &chunk_off, &chunk_size)) { x64_load_part(t, tmpreg, src, part->src_offset + chunk_off, chunk_size); - x64_store_outgoing_part(t, tail_call, stack_off + chunk_off, tmpreg, + x64_store_outgoing_part(t, call, stack_off + chunk_off, tmpreg, chunk_size); } return; } x64_load_part(t, tmpreg, src, part->src_offset, part->size); - x64_store_outgoing_part(t, tail_call, stack_off, tmpreg, part->size); + x64_store_outgoing_part(t, call, stack_off, tmpreg, part->size); } /* NativeTarget bind_param: route incoming param (ABI loc) into dst. */ @@ -2702,7 +2689,7 @@ static void x64_bind_native_param(NativeTarget* t, const CGParamDesc* p, /* Incoming stack args sit above the saved rbp + return addr (+16); Win64 * additionally reserves 32B of home space. */ i32 incoming_bias = (i32)(16u + a->abi->shadow_space); - u16 i; + u32 i; if (!ai || ai->kind == ABI_ARG_IGNORE) return; if (ai->kind == ABI_ARG_INDIRECT) { @@ -2863,49 +2850,19 @@ static void x64_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves, u32 n, s.reg_move = x64_move; s.scratch[NATIVE_REG_INT] = int_scratch; s.scratch[NATIVE_REG_FP] = X64_TMP_FP; + s.scratch_class_mask = (1u << NATIVE_REG_INT) | (1u << NATIVE_REG_FP); native_arg_shuffle(&s, moves, n); } -/* Clobber masks: per-call all caller-saved regs are clobbered. */ -static u32 x64_clobber_mask(const X64ABIRegs* abi, NativeAllocClass cls) { - u32 mask = 0, r; - if (cls == NATIVE_REG_INT) { - for (r = 0; r < 16u; ++r) { - if (r == X64_RSP || r == X64_RBP) continue; - if ((abi->cs_int_mask & (1ull << r)) == 0) mask |= 1u << r; - } - } else if (cls == NATIVE_REG_FP) { - for (r = 0; r < 16u; ++r) - if ((abi->cs_fp_mask & (1ull << r)) == 0) mask |= 1u << r; - } - return mask; -} - -static u32 x64_return_mask(const ABIFuncInfo* abi, NativeAllocClass cls) { - u32 mask = 0, ni = 0, nf = 0; - static const u32 iregs[2] = {X64_RAX, X64_RDX}; - u16 i; - if (!abi || abi->ret.kind == ABI_ARG_IGNORE || - abi->ret.kind == ABI_ARG_INDIRECT) - return 0; - for (i = 0; i < abi->ret.nparts; ++i) { - const ABIArgPart* p = &abi->ret.parts[i]; - if (cls == NATIVE_REG_INT && p->cls == ABI_CLASS_INT && ni < 2) - mask |= 1u << iregs[ni++]; - else if (cls == NATIVE_REG_FP && p->cls == ABI_CLASS_FP && nf < 2) - mask |= 1u << (X64_XMM0 + nf++); - } - return mask; -} - -static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, - NativeCallPlan* plan) { +static void x64_marshal_call(NativeTarget* t, const NativeCallDesc* desc, + NativeCallPhase* plan) { X64NativeTarget* a = x64_of(t); const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); const X64ABIRegs* aregs = a->abi ? a->abi : x64_abi_for_os(t->c->target.os); - NativeCallPlanRet* rets; + NativeCallPhaseRet* rets; + X64CallMarshalCtx call; KitCgTypeId i64t = builtin_id(KIT_CG_BUILTIN_I64); - u32 c; + int tail = (desc->flags & CG_CALL_TAIL) != 0; /* Right-size the result scratch to the exact number of entries the ret loops * below write: nparts on a DIRECT register return, 1 on the !abi fallback, * 0 (NULL) otherwise (IGNORE / sret / no results). */ @@ -2913,7 +2870,10 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, ? abi->ret.nparts : ((!abi && desc->nresults) ? 1u : 0u); memset(plan, 0, sizeof *plan); - rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPlanRet, nrets_cap) : NULL; + memset(&call, 0, sizeof call); + call.tail = (u8)tail; + call.cycle_scratch = X64_TMP_INT2; + rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPhaseRet, nrets_cap) : NULL; plan->callee = desc->callee; plan->rets = rets; plan->flags = desc->flags; @@ -2922,10 +2882,6 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, plan->stack_arg_size = x64_call_stack_bytes(t, desc); if (plan->stack_arg_size > a->frame.max_outgoing) a->frame.max_outgoing = plan->stack_arg_size; - for (c = 0; c < NATIVE_CALL_PLAN_CLASSES; ++c) { - plan->clobber_mask[c] = x64_clobber_mask(aregs, (NativeAllocClass)c); - plan->return_mask[c] = x64_return_mask(abi, (NativeAllocClass)c); - } /* Argument transport owns r10 (part data), r11 (address stabilization), and * rax (parallel-copy cycle breaking). A register-indirect callee therefore * lives in a balanced stack home for the complete marshal phase, independent @@ -2933,18 +2889,17 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, if (plan->callee.kind == NATIVE_LOC_REG) { if ((NativeAllocClass)plan->callee.cls != NATIVE_REG_INT) x64_panic(a, "indirect callee is not in an integer register"); - if (a->call_callee_parked) - x64_panic(a, "nested indirect-callee marshal"); x64_push_reg(t->mc, (Reg)loc_reg(plan->callee)); - a->call_callee_parked = 1; + call.rsp_bias = 8u; + call.callee_parked = 1u; + call.cycle_scratch = X64_RAX; plan->callee = native_loc_reg(plan->callee.type, NATIVE_REG_INT, X64_R11); } { u32 next_int = (abi && abi->has_sret) ? 1u : 0u; u32 next_fp = 0, stack = aregs->shadow_space, nmoves = 0, i; - int tail = (desc->flags & CG_CALL_TAIL) != 0; u32 tail_payload = tail ? x64_call_stack_raw_size(t, desc) : 0u; - u16 p; + u32 p; X64ArgMove moves[X64_MAX_REG_ARG_MOVES]; x64_sync_slot(aregs, &next_int, &next_fp); for (i = 0; i < desc->nargs; ++i) { @@ -2983,7 +2938,7 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, } else { NativeLoc ptr = native_loc_reg(i64t, NATIVE_REG_INT, X64_RAX); x64_addr_of_loc(t, ptr, src); - x64_store_outgoing_part(t, tail, stack, ptr, 8); + x64_store_outgoing_part(t, &call, stack, ptr, 8); stack += 8u; } x64_sync_slot(aregs, &next_int, &next_fp); @@ -2993,7 +2948,7 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, x64_direct_to_stack(aregs, ai, next_int, next_fp)) { for (p = 0; p < ai->nparts; ++p) { const ABIArgPart* part = &ai->parts[p]; - x64_copy_outgoing_part(t, tail, stack, desc->args[i], part); + x64_copy_outgoing_part(t, &call, stack, desc->args[i], part); stack += 8u; } continue; @@ -3027,7 +2982,7 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, m->size = part->size; x64_sync_slot(aregs, &next_int, &next_fp); } else { - x64_copy_outgoing_part(t, tail, stack, desc->args[i], part); + x64_copy_outgoing_part(t, &call, stack, desc->args[i], part); stack += 8u; x64_sync_slot(aregs, &next_int, &next_fp); } @@ -3035,11 +2990,7 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, } /* If an indirect callee was staged in r11 above, the cycle scratch must * avoid it; rax is free here (not an int arg reg; AL count comes later). */ - x64_emit_reg_arg_moves( - t, moves, nmoves, - (plan->callee.kind == NATIVE_LOC_REG && plan->callee.v.reg == X64_R11) - ? X64_RAX - : X64_TMP_INT2); + x64_emit_reg_arg_moves(t, moves, nmoves, call.cycle_scratch); if (abi && abi->has_sret) { /* sret pointer in the first int-arg reg. A tail call forwards the * caller's own incoming sret pointer (spilled at entry); otherwise pass @@ -3054,16 +3005,17 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, /* Variadic call: AL = number of vector regs used. */ if (abi && abi->variadic) x64_emit_load_imm(t->mc, 0, X64_RAX, (i64)next_fp); - if (a->call_callee_parked) { + if (call.callee_parked) { x64_pop_reg(t->mc, X64_R11); - a->call_callee_parked = 0; + call.rsp_bias = 0u; + call.callee_parked = 0u; } } /* Return value receipt. */ if (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) { u32 nr = 0, ni = 0, nf = 0; static const u32 ret_int_regs[2] = {X64_RAX, X64_RDX}; - u16 p; + u32 p; for (p = 0; p < abi->ret.nparts; ++p) { const ABIArgPart* part = &abi->ret.parts[p]; NativeAllocClass cls = @@ -3105,7 +3057,7 @@ static void x64_emit_tail_site(NativeTarget* t, NativeLoc callee) { ObjSecId sec = mc->section_id; /* Restore callee-saves before the frame teardown (O1 path; none at -O0). * Their rbp-relative offsets are frame-size-independent, and the indirect - * callee was staged in r11 by plan_call — a caller-saved scratch — so these + * callee was staged in r11 by marshal_call — a caller-saved scratch — so these * restores never clobber it. Mirrors the x64_func_end epilogue. */ x64_emit_callee_restores(a); emit_leave(mc); @@ -3118,7 +3070,7 @@ static void x64_emit_tail_site(NativeTarget* t, NativeLoc callee) { mc_emit_reloc_at(mc, sec, disp_pos, R_X64_PLT32, callee.v.global.sym, callee.v.global.addend - 4, 1, 0); } else if (callee.kind == NATIVE_LOC_REG) { - /* indirect callee was staged in r11 by plan_call */ + /* indirect callee was staged in r11 by marshal_call */ u32 r = loc_reg(callee); x64_emit_indirect_rm(mc, r, 4u); /* jmp r/m, /4 */ } else { @@ -3126,7 +3078,7 @@ static void x64_emit_tail_site(NativeTarget* t, NativeLoc callee) { } } -static void x64_emit_call(NativeTarget* t, const NativeCallPlan* plan) { +static void x64_emit_call(NativeTarget* t, const NativeCallPhase* plan) { MCEmitter* mc = t->mc; ObjSecId sec = mc->section_id; if (plan->flags & CG_CALL_TAIL) { @@ -3151,14 +3103,14 @@ static void x64_emit_call(NativeTarget* t, const NativeCallPlan* plan) { x64_panic(x64_of(t), "unsupported call target"); } -static void x64_plan_ret(NativeTarget* t, const CGFuncDesc* fd, - const NativeLoc* value, NativeCallPlanRet** out_rets, - u32* out_nrets) { +static void x64_marshal_ret(NativeTarget* t, const CGFuncDesc* fd, + const NativeLoc* value, + NativeCallPhaseRet** out_rets, u32* out_nrets) { X64NativeTarget* a = x64_of(t); const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type); - NativeCallPlanRet* rets = NULL; + NativeCallPhaseRet* rets = NULL; u32 nr = 0; - if (value) rets = arena_zarray(t->c->tu, NativeCallPlanRet, 4); + if (value) rets = arena_zarray(t->c->tu, NativeCallPhaseRet, 4); if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) { /* sret: reload the destination pointer (spilled at entry) into rax and * copy the source aggregate into [rax]. Keep it out of R10/R11: those are @@ -3190,7 +3142,7 @@ static void x64_plan_ret(NativeTarget* t, const CGFuncDesc* fd, if (value && abi && abi->ret.kind == ABI_ARG_DIRECT) { u32 ni = 0, nf = 0; static const u32 ret_int_regs[2] = {X64_RAX, X64_RDX}; - u16 p; + u32 p; int exact_parts = 0; for (p = 0; p < abi->ret.nparts; ++p) if (abi->ret.parts[p].cls == ABI_CLASS_INT && @@ -4349,31 +4301,6 @@ static void x64_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, /* Pin resolution + panic is the shared native_asm_bind_direct_operands path. */ -/* Pick a free register from caller-saved allocable pools for an asm operand the - * direct path self-allocates. */ -static Reg x64_asm_alloc_reg(NativeDirectTarget* d, NativeAllocClass cls, - u32 allowed_mask, u32* used_int, u32* used_fp) { - static const Reg int_pool[] = {X64_RDI, X64_RSI, X64_RDX, - X64_RCX, X64_R8, X64_R9}; - static const Reg fp_pool[] = { - X64_XMM0, X64_XMM1, X64_XMM2, X64_XMM3, X64_XMM4, X64_XMM5, - X64_XMM6, X64_XMM7, X64_XMM8, X64_XMM0 + 9, X64_XMM0 + 10, X64_XMM0 + 11}; - const Reg* pool = cls == NATIVE_REG_FP ? fp_pool : int_pool; - u32 n = cls == NATIVE_REG_FP ? (u32)(sizeof fp_pool / sizeof fp_pool[0]) - : (u32)(sizeof int_pool / sizeof int_pool[0]); - u32* used = cls == NATIVE_REG_FP ? used_fp : used_int; - u32 i; - for (i = 0; i < n; ++i) { - Reg r = pool[i]; - if (allowed_mask && (allowed_mask & (1u << r)) == 0) continue; - if ((*used & (1u << r)) != 0) continue; - *used |= 1u << r; - return r; - } - x64_asm_panic(d, "out of registers for asm operands"); - return REG_NONE; -} - /* Direct (-O0) path: resolve a semantic Operand to a NativeAddr. */ static NativeAddr x64_direct_addr(NativeDirectTarget* d, Operand op) { NativeAddr addr; @@ -4640,6 +4567,33 @@ static void x64_set_loc(NativeTarget* t, SrcLoc loc) { mc_set_loc(t->mc, loc); } +static u32 x64_binop_visible_clobbers(const NativeMachineOp* op) { + switch ((BinOp)op->binop) { + case BO_SDIV: + case BO_UDIV: + case BO_SREM: + case BO_UREM: + return (1u << X64_RAX) | (1u << X64_RDX); + case BO_SHL: + case BO_SHR_S: + case BO_SHR_U: + return op->second_is_reg ? (1u << X64_RCX) : 0u; + default: + return 0u; + } +} + +static u32 x64_intrinsic_visible_clobbers(IntrinKind kind) { + if (kind == INTRIN_UMUL_OVERFLOW || kind == INTRIN_SMUL_HIGH || + kind == INTRIN_UMUL_HIGH || kind == INTRIN_READCYCLECOUNTER) + return (1u << X64_RAX) | (1u << X64_RDX); + if (kind == INTRIN_SYSCALL) + return (1u << X64_RAX) | (1u << X64_RCX) | (1u << X64_RDX) | + (1u << X64_RSI) | (1u << X64_RDI) | (1u << X64_R8) | + (1u << X64_R9); + return 0u; +} + /* Physical registers each x86-64 instruction's encoding clobbers as a side * effect, so the optimizer keeps values live across them out of those registers * (the backend is then free to use them). idiv/div write rax (quotient) and rdx @@ -4648,7 +4602,7 @@ static void x64_set_loc(NativeTarget* t, SrcLoc loc) { * All other ordinary helpers use declared results or backend-private R10/R11 * and XMM14/XMM15 temporaries. */ static int x64_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, - u32 mask[NATIVE_CALL_PLAN_CLASSES]) { + u32 mask[NATIVE_REG_CLASS_COUNT]) { mask[0] = mask[1] = mask[2] = 0; switch ((NativeMachineOpKind)op->kind) { case NATIVE_MOP_TLS_ADDR: @@ -4662,24 +4616,8 @@ static int x64_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, mask[NATIVE_REG_INT] = (1u << X64_RAX) | (1u << X64_RDI); return 1; case NATIVE_MOP_BINOP: - switch ((BinOp)op->binop) { - case BO_SDIV: - case BO_UDIV: - case BO_SREM: - case BO_UREM: - mask[NATIVE_REG_INT] = (1u << X64_RAX) | (1u << X64_RDX); - return 1; - case BO_SHL: - case BO_SHR_S: - case BO_SHR_U: - if (op->second_is_reg) { - mask[NATIVE_REG_INT] = (1u << X64_RCX); - return 1; - } - return 0; - default: - return 0; - } + mask[NATIVE_REG_INT] = x64_binop_visible_clobbers(op); + return mask[NATIVE_REG_INT] != 0u; case NATIVE_MOP_BITFIELD_LOAD: /* The only non-destination temporary is backend-private R11. */ return 0; @@ -4701,29 +4639,16 @@ static int x64_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, * two-operand IMUL (no fixed-register clobber). Linux syscall writes rax * and the CPU instruction itself clobbers rcx/r11; the kernel ABI treats * the integer caller-saved syscall registers as volatile. */ - if ((IntrinKind)op->intrin == INTRIN_UMUL_OVERFLOW || - (IntrinKind)op->intrin == INTRIN_SMUL_HIGH || - (IntrinKind)op->intrin == INTRIN_UMUL_HIGH || - (IntrinKind)op->intrin == INTRIN_READCYCLECOUNTER) { - /* MUL's rdx:rax product / RDTSC's edx:eax counter both write both - * registers; keep live values out of them across the op. */ - mask[NATIVE_REG_INT] = (1u << X64_RAX) | (1u << X64_RDX); - return 1; - } - if ((IntrinKind)op->intrin == INTRIN_SYSCALL) { - /* R8/R9 are cache-capable emitter temps, so the effect must invalidate - * retained spill values there even though they are not MIR homes. - * R10/R11 are pure backend-private temps and remain excluded. */ - mask[NATIVE_REG_INT] = - (1u << X64_RAX) | (1u << X64_RCX) | (1u << X64_RDX) | - (1u << X64_RSI) | (1u << X64_RDI) | (1u << X64_R8) | - (1u << X64_R9); - return 1; - } - return 0; - default: - return 0; + /* R8/R9 are cache-capable emitter temps, so the syscall effect includes + * them. R10/R11 are pure backend-private temps and remain excluded. */ + mask[NATIVE_REG_INT] = + x64_intrinsic_visible_clobbers((IntrinKind)op->intrin); + return mask[NATIVE_REG_INT] != 0u; + case NATIVE_MOP_COUNT: + break; } + x64_panic(x64_of(t), "invalid machine-effect operation"); + return 0; } /* ============================ construction ============================ */ @@ -4788,9 +4713,9 @@ NativeTarget* x64_native_target_new(Compiler* c, ObjBuilder* obj, t->alloca_ = x64_alloca; t->spill = x64_spill; t->reload = x64_reload; - t->plan_call = x64_plan_call; + t->marshal_call = x64_marshal_call; t->emit_call = x64_emit_call; - t->plan_ret = x64_plan_ret; + t->marshal_ret = x64_marshal_ret; t->ret = x64_ret; t->atomic_load = x64_atomic_load; t->atomic_store = x64_atomic_store; @@ -4943,20 +4868,11 @@ static void x64_direct_asm_block(NativeDirectTarget* d, const char* tmpl, const Sym* clobbers, u32 nclob, u32 clobber_abi_sets) { static const NativeAsmDirectHooks hooks = { - /* Reserve backend temps R10/R11, O0 scratch R8/R9, RAX (reserved; only - * self-allocated here when explicitly pinned), and SP/BP. */ - .reserved_int_mask = - (1u << X64_RAX) | (1u << X64_R11) | (1u << X64_RSP) | - (1u << X64_RBP) | (1u << X64_R8) | (1u << X64_R9) | - (1u << X64_R10), - .reserved_fp_mask = (1u << X64_XMM4) | (1u << X64_XMM5) | - (1u << (X64_XMM0 + 14)) | (1u << X64_XMM15), .opk_reg = X64_INLINE_OPK_REG, .opcls_fp = X64_INLINE_OPCLS_FP, .panic = x64_asm_panic, .bound_reg = x64_asm_bound_reg, .bound_mem = x64_asm_bound_mem, - .alloc_reg = x64_asm_alloc_reg, .clobber_masks = x64_asm_clobber_masks, .save_callee_clobbers = x64_asm_hook_save_callee_clobbers, .restore_one = x64_asm_hook_restore_one, diff --git a/src/cg/native_argmove.c b/src/cg/native_argmove.c @@ -58,11 +58,36 @@ void native_arg_shuffle(const NativeArgShuffle* s, NativeArgMove* moves, u32 k = 0; NativeAllocClass bc, sc; NativeLoc scratchloc; + Reg scratch; Reg broken_reg; KitCgTypeId val_type; while (k < n && (done[k] || nam_src_reg(&moves[k], &sc) == REG_NONE)) ++k; + if (k == n) + compiler_panic(s->t->c, (SrcLoc){0, 0, 0}, + "native arg shuffle: blocked move set has no register " + "cycle"); bc = (NativeAllocClass)moves[k].dst.cls; broken_reg = moves[k].dst.v.reg; + if (bc >= NATIVE_REG_CLASS_COUNT) + compiler_panic(s->t->c, (SrcLoc){0, 0, 0}, + "native arg shuffle: invalid class scratch"); + if ((s->scratch_class_mask & (1u << bc)) == 0u) + compiler_panic(s->t->c, (SrcLoc){0, 0, 0}, + "native arg shuffle: missing class scratch"); + scratch = s->scratch[bc]; + if (scratch >= NATIVE_MAX_HARD_REGS) + compiler_panic(s->t->c, (SrcLoc){0, 0, 0}, + "native arg shuffle: invalid class scratch"); + for (j = 0; j < n; ++j) { + Reg sr = nam_src_reg(&moves[j], &sc); + if (done[j]) continue; + if (((NativeAllocClass)moves[j].dst.cls == bc && + moves[j].dst.v.reg == scratch) || + (sr != REG_NONE && sc == bc && sr == scratch)) + compiler_panic(s->t->c, (SrcLoc){0, 0, 0}, + "native arg shuffle: cycle scratch aliases a live " + "move register"); + } /* The value parked in broken_reg is whatever the cycle's readers consume, * and its width is their *source* type — not moves[k].dst's own incoming * type, which can be narrower (e.g. a 4-byte int rotating into the slot a @@ -81,7 +106,7 @@ void native_arg_shuffle(const NativeArgShuffle* s, NativeArgMove* moves, scratchloc.kind = NATIVE_LOC_REG; scratchloc.cls = (u8)bc; scratchloc.type = val_type; - scratchloc.v.reg = s->scratch[bc]; + scratchloc.v.reg = scratch; s->reg_move(s->t, scratchloc, moves[k].dst); for (j = 0; j < n; ++j) { Reg sr = nam_src_reg(&moves[j], &sc); diff --git a/src/cg/native_argmove.h b/src/cg/native_argmove.h @@ -50,8 +50,10 @@ typedef struct NativeArgShuffle { void (*reg_move)(NativeTarget* t, NativeLoc dst, NativeLoc src); /* Scratch register per NativeAllocClass (index by class id) for breaking * cycles. A cycle's ring is register-to-register, so the scratch needs no - * address arithmetic — a bare register suffices. */ - Reg scratch[NATIVE_REG_VEC + 1]; + * address arithmetic — a bare register suffices. The mask makes configured + * classes explicit even when physical register zero is a valid scratch. */ + Reg scratch[NATIVE_REG_CLASS_COUNT]; + u32 scratch_class_mask; } NativeArgShuffle; /* Emit `moves[0..n)` as a parallel register copy via `s`. Mutates `moves` diff --git a/src/cg/native_asm.c b/src/cg/native_asm.c @@ -46,6 +46,35 @@ void native_asm_abi_clobber_masks(NativeTarget* t, u32 abi_sets, u32* int_mask, } } +u32 native_asm_known_callee_saves( + NativeTarget* t, SrcLoc loc, const NativeKnownFrameDesc* frame, u32* out, + u32 cap, NativeAsmClobberMasksFn clobber_masks, + NativeAsmCalleeSavedRegFn is_callee_saved) { + u32 ncls = frame ? frame->ncallee_classes : 0u; + u32 clob_int = 0, clob_fp = 0, abi_int = 0, abi_fp = 0; + if (ncls > cap) ncls = cap; + for (u32 c = 0; c < cap; ++c) out[c] = 0; + for (u32 c = 0; c < ncls; ++c) + out[c] = frame->callee_saved_used ? frame->callee_saved_used[c] : 0u; + if (frame && frame->asm_clobbers && frame->nasm_clobbers && clobber_masks) + clobber_masks(t->c, loc, frame->asm_clobbers, frame->nasm_clobbers, + &clob_int, &clob_fp); + if (frame) + native_asm_abi_clobber_masks(t, frame->asm_clobber_abi_sets, &abi_int, + &abi_fp); + clob_int |= abi_int; + clob_fp |= abi_fp; + for (Reg r = 0; r < NATIVE_MAX_HARD_REGS; ++r) { + if (NATIVE_REG_INT < ncls && (clob_int & (1u << r)) && + is_callee_saved(t, NATIVE_REG_INT, r)) + out[NATIVE_REG_INT] |= 1u << r; + if (NATIVE_REG_FP < ncls && (clob_fp & (1u << r)) && + is_callee_saved(t, NATIVE_REG_FP, r)) + out[NATIVE_REG_FP] |= 1u << r; + } + return ncls; +} + int native_asm_constraint_reg_class(const char* constraint, NativeAllocClass* cls_out) { const char* body = native_asm_constraint_body(constraint); @@ -63,8 +92,8 @@ int native_asm_constraint_reg_class(const char* constraint, static int native_asm_default_operand_reg_ok(const NativeRegInfo* ri, NativeAllocClass cls, Reg reg) { - if (!ri || cls >= ri->nclasses) return 0; - const NativeAllocClassInfo* ci = &ri->classes[cls]; + const NativeAllocClassInfo* ci = native_reg_info_class_info(ri, cls); + if (!ci) return 0; for (u32 i = 0; i < ci->nphys; ++i) { const NativePhysRegInfo* pi = &ci->phys[i]; if (pi->reg != reg) continue; @@ -194,6 +223,32 @@ static int native_asm_direct_resolve_pin(NativeDirectTarget* d, return 1; } +static Reg native_asm_direct_alloc_reg(NativeDirectTarget* d, + const NativeAsmDirectHooks* h, + NativeAllocClass cls, u32 allowed_mask, + u32* used_int, u32* used_fp) { + const NativeAllocClassInfo* ci = native_target_class_info(d->native, cls); + u32* used = cls == NATIVE_REG_FP ? used_fp : used_int; + for (u32 i = 0; ci && i < ci->ndirect_asm_allocable; ++i) { + Reg reg = ci->direct_asm_allocable[i]; + if (reg >= NATIVE_MAX_HARD_REGS) continue; + if (allowed_mask && (allowed_mask & (1u << reg)) == 0) continue; + if ((*used & (1u << reg)) != 0) continue; + *used |= 1u << reg; + return reg; + } + h->panic(d, "out of registers for asm operands"); + return REG_NONE; +} + +static KitCgTypeId native_asm_pointer_carrier_type(NativeTarget* t) { + if (t->c->target.ptr_size == 4u) return builtin_id(KIT_CG_BUILTIN_I32); + if (t->c->target.ptr_size == 8u) return builtin_id(KIT_CG_BUILTIN_I64); + compiler_panic(t->c, (SrcLoc){0, 0, 0}, + "native asm: unsupported pointer carrier width %u", + (unsigned)t->c->target.ptr_size); +} + void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, const AsmConstraint* outs, u32 nout, Operand* out_ops, const AsmConstraint* ins, @@ -212,8 +267,8 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, native_asm_abi_clobber_masks(d->native, clobber_abi_sets, &abi_int, &abi_fp); clob_int |= abi_int; clob_fp |= abi_fp; - used_int = clob_int | h->reserved_int_mask; - used_fp = clob_fp | h->reserved_fp_mask; + used_int = clob_int; + used_fp = clob_fp; for (i = 0; i < nout; ++i) { const char* body = native_asm_constraint_body(outs[i].str); @@ -234,8 +289,9 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, if (native_asm_constraint_reg_info(d->native, outs[i].str, &info)) { Reg reg = info.fixed_reg != REG_NONE ? info.fixed_reg - : h->alloc_reg(d, info.cls, info.allowed_mask, &used_int, - &used_fp); + : native_asm_direct_alloc_reg( + d, h, info.cls, info.allowed_mask, &used_int, + &used_fp); if (info.cls == NATIVE_REG_FP) { used_fp |= 1u << reg; /* Binding itself overwrites the staging register, independently of @@ -249,7 +305,8 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, } h->bound_reg(&bound_outs[i], type, info.cls, reg); } else if (body[0] == 'm') { - Reg reg = h->alloc_reg(d, NATIVE_REG_INT, 0, &used_int, &used_fp); + Reg reg = native_asm_direct_alloc_reg( + d, h, NATIVE_REG_INT, 0, &used_int, &used_fp); clob_int |= 1u << reg; h->bound_mem(&bound_outs[i], type, reg); } else { @@ -287,8 +344,9 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, if (native_asm_constraint_reg_info(d->native, ins[i].str, &info)) { Reg reg = info.fixed_reg != REG_NONE ? info.fixed_reg - : h->alloc_reg(d, info.cls, info.allowed_mask, &used_int, - &used_fp); + : native_asm_direct_alloc_reg( + d, h, info.cls, info.allowed_mask, &used_int, + &used_fp); if (info.cls == NATIVE_REG_FP) { used_fp |= 1u << reg; clob_fp |= 1u << reg; @@ -302,7 +360,8 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, h->panic(d, "immediate constraint requires immediate operand"); bound_ins[i] = in_ops[i]; } else if (body[0] == 'm') { - Reg reg = h->alloc_reg(d, NATIVE_REG_INT, 0, &used_int, &used_fp); + Reg reg = native_asm_direct_alloc_reg( + d, h, NATIVE_REG_INT, 0, &used_int, &used_fp); clob_int |= 1u << reg; h->bound_mem(&bound_ins[i], type, reg); } else { @@ -322,9 +381,9 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, (Reg)bound_outs[i].v.local)); } } else if (bound_outs[i].kind == OPK_INDIRECT) { - NativeLoc loc = - native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, - (Reg)bound_outs[i].v.ind.base); + NativeLoc loc = native_loc_reg(native_asm_pointer_carrier_type(d->native), + NATIVE_REG_INT, + (Reg)bound_outs[i].v.ind.base); h->load_address_to_reg(d, out_ops[i], loc); } } @@ -336,9 +395,9 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, d, in_ops[i], native_loc_reg(bound_ins[i].type, cls, (Reg)bound_ins[i].v.local)); } else if (bound_ins[i].kind == OPK_INDIRECT) { - NativeLoc loc = - native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, - (Reg)bound_ins[i].v.ind.base); + NativeLoc loc = native_loc_reg(native_asm_pointer_carrier_type(d->native), + NATIVE_REG_INT, + (Reg)bound_ins[i].v.ind.base); h->load_address_to_reg(d, in_ops[i], loc); } } diff --git a/src/cg/native_asm.h b/src/cg/native_asm.h @@ -43,6 +43,19 @@ int native_asm_match_index(const char* s); void native_asm_abi_clobber_masks(NativeTarget* t, u32 abi_sets, u32* int_mask, u32* fp_mask); +typedef void (*NativeAsmClobberMasksFn)(Compiler*, SrcLoc, const Sym*, u32, + u32*, u32*); +typedef int (*NativeAsmCalleeSavedRegFn)(NativeTarget*, NativeAllocClass, Reg); + +/* Merge allocator-used and inline-asm-clobbered callee-saved registers for a + * known frame. Target parsing remains behind clobber_masks; is_callee_saved + * defines the live ABI's preservable register set and excludes structural + * registers such as the frame pointer. */ +u32 native_asm_known_callee_saves( + NativeTarget* t, SrcLoc loc, const NativeKnownFrameDesc* frame, u32* out, + u32 cap, NativeAsmClobberMasksFn clobber_masks, + NativeAsmCalleeSavedRegFn is_callee_saved); + typedef enum NativeAsmRegPinStatus { NATIVE_ASM_REG_PIN_ABSENT = 0, NATIVE_ASM_REG_PIN_OK = 1, @@ -96,11 +109,6 @@ int native_asm_constraint_reg_class(const char* constraint, typedef struct NativeDirectTarget NativeDirectTarget; typedef struct NativeAsmDirectHooks { - /* Registers the O0 direct-asm operand allocator must never hand out - * (backend-private temporaries, ABI fixed regs, frame pointer, ...), seeded - * into the used masks alongside the clobber masks. Per-arch. */ - u32 reserved_int_mask; - u32 reserved_fp_mask; /* Arch inline-operand discriminators: Operand.kind for a bound register * pseudo-operand, and the Operand.pad[0] value marking the FP class. */ u8 opk_reg; @@ -113,11 +121,6 @@ typedef struct NativeAsmDirectHooks { void (*bound_reg)(Operand* out, KitCgTypeId type, NativeAllocClass cls, Reg reg); void (*bound_mem)(Operand* out, KitCgTypeId type, Reg base); - /* Pick a free register from the arch's allocable pool for cls, honoring - * allowed_mask and the running used masks (which it updates). Panics on - * exhaustion. */ - Reg (*alloc_reg)(NativeDirectTarget* d, NativeAllocClass cls, - u32 allowed_mask, u32* used_int, u32* used_fp); /* Parse the asm clobber list into per-class register masks (arch register * naming). */ void (*clobber_masks)(Compiler* c, SrcLoc loc, const Sym* clobbers, u32 nclob, diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c @@ -138,7 +138,8 @@ static NativeAllocClass nd_class_for_type(NativeDirectTarget* d, static const NativeAllocClassInfo* nd_class_info(NativeDirectTarget* d, NativeAllocClass cls) { - const NativeAllocClassInfo* ci = (u32)cls < 3u ? d->class_info[cls] : NULL; + const NativeAllocClassInfo* ci = + (u32)cls < NATIVE_REG_CLASS_COUNT ? d->class_info[cls] : NULL; if (!ci) nd_panic(d, "target has no requested register class"); return ci; } @@ -170,7 +171,7 @@ static u32 nd_caller_saved_mask(NativeDirectTarget* d, NativeAllocClass cls) { static void nd_note_reg_used(NativeDirectTarget* d, NativeAllocClass cls, Reg reg) { - if ((u32)cls >= 3u || reg >= 32u) return; + if ((u32)cls >= NATIVE_REG_CLASS_COUNT || reg >= NATIVE_MAX_HARD_REGS) return; if (nd_callee_saved_mask(d, cls) & (1u << reg)) d->callee_saved_used[cls] |= 1u << reg; } @@ -1271,7 +1272,7 @@ static void nd_func_end(CgTarget* t) { * saves/restores any callee-save, which is what lets the backend reserve * only a tiny tcc-style deferred-`sub` prologue region. Assert it so a * future regression can't silently overflow that region. */ - for (u32 cls = 0; cls < 3u; ++cls) { + for (u32 cls = 0; cls < NATIVE_REG_CLASS_COUNT; ++cls) { if (d->callee_saved_used[cls]) nd_panic(d, "ndt_caller_saved_only target used a callee-saved register"); } @@ -1280,7 +1281,7 @@ static void nd_func_end(CgTarget* t) { * pressure; have the backend reserve save slots for them. The * optimizer/known-frame path is separate and always callee-save-capable. */ u32 ncallee_classes = 0; - for (u32 cls = 0; cls < 3u; ++cls) { + for (u32 cls = 0; cls < NATIVE_REG_CLASS_COUNT; ++cls) { if (d->callee_saved_used[cls]) ncallee_classes = cls + 1u; } if (ncallee_classes) { @@ -2157,7 +2158,7 @@ static void nd_convert(CgTarget* t, ConvKind op, Operand dst, Operand src) { static void nd_call(CgTarget* t, const CGCallDesc* desc) { NativeDirectTarget* d = nd_of(t); - NativeCallPlan plan; + NativeCallPhase plan; NativeCallDesc nd; NativeLoc* args; NativeLoc* results; @@ -2241,11 +2242,11 @@ static void nd_call(CgTarget* t, const CGCallDesc* desc) { nd.tail_policy = desc->tail_policy; nd.inline_policy = desc->inline_policy; - if (d->ops && d->ops->plan_call) - d->ops->plan_call(d, &nd, &plan); + if (d->ops && d->ops->marshal_call) + d->ops->marshal_call(d, &nd, &plan); else { - ND_REQUIRE_NATIVE(d, plan_call, "target does not plan calls"); - d->native->plan_call(d->native, &nd, &plan); + ND_REQUIRE_NATIVE(d, marshal_call, "target does not marshal calls"); + d->native->marshal_call(d->native, &nd, &plan); } if (plan.stack_arg_size > d->max_outgoing) d->max_outgoing = plan.stack_arg_size; @@ -2311,7 +2312,7 @@ static void nd_ret(CgTarget* t, CGLocal value) { NativeDirectTarget* d = nd_of(t); NativeLoc loc; const NativeLoc* locp = NULL; - NativeCallPlanRet* rets = NULL; + NativeCallPhaseRet* rets = NULL; u32 nrets = 0; /* The emit_ret ops path reads `value` from its home, so it needs the cache * spilled first (no production arch installs it today). */ @@ -2335,8 +2336,8 @@ static void nd_ret(CgTarget* t, CGLocal value) { locp = &loc; } nd_drop_all(d); - ND_REQUIRE_NATIVE(d, plan_ret, "target does not plan returns"); - d->native->plan_ret(d->native, d->func, locp, &rets, &nrets); + ND_REQUIRE_NATIVE(d, marshal_ret, "target does not marshal returns"); + d->native->marshal_ret(d->native, d->func, locp, &rets, &nrets); for (u32 i = 0; i < nrets; ++i) nd_write_loc(d, rets[i].dst, rets[i].src, rets[i].mem); ND_REQUIRE_NATIVE(d, ret, "target does not emit returns"); @@ -2576,12 +2577,13 @@ CgTarget* native_direct_target_new(Compiler* c, ObjBuilder* obj, /* Resolve register/class info once; it is constant for the program. */ d->reg_info = cfg->native ? cfg->native->regs : NULL; - for (u32 i = 0; i < 3u; ++i) d->class_info[i] = NULL; + native_reg_info_validate(c, d->reg_info); + for (u32 i = 0; i < NATIVE_REG_CLASS_COUNT; ++i) d->class_info[i] = NULL; if (d->reg_info) { const NativeRegInfo* ri = d->reg_info; for (u32 i = 0; i < ri->nclasses; ++i) { u32 cls = ri->classes[i].cls; - if (cls < 3u) d->class_info[cls] = &ri->classes[i]; + if (cls < NATIVE_REG_CLASS_COUNT) d->class_info[cls] = &ri->classes[i]; } } @@ -2593,7 +2595,7 @@ CgTarget* native_direct_target_new(Compiler* c, ObjBuilder* obj, * (nd_note_reg_used) and reserved by reserve_callee_saves. */ { int caller_only = d->reg_info && d->reg_info->ndt_caller_saved_only; - for (u32 cls = 0; cls < 3u; ++cls) { + for (u32 cls = 0; cls < NATIVE_REG_CLASS_COUNT; ++cls) { const NativeAllocClassInfo* ci = d->class_info[cls]; u32 mask, n = 0; if (!ci) continue; diff --git a/src/cg/native_direct_target.h b/src/cg/native_direct_target.h @@ -86,11 +86,11 @@ struct NativeOps { NativeDirectAddrLegality (*semantic_addr_legal)(NativeDirectTarget*, Operand addr, MemAccess); - void (*plan_call)(NativeDirectTarget*, const NativeCallDesc*, - NativeCallPlan*); + void (*marshal_call)(NativeDirectTarget*, const NativeCallDesc*, + NativeCallPhase*); const char* (*tail_call_unrealizable_reason)(NativeDirectTarget*, const CGCallDesc*); - void (*emit_call)(NativeDirectTarget*, const NativeCallPlan*); + void (*emit_call)(NativeDirectTarget*, const NativeCallPhase*); /* `value` is the single returned local, or CG_LOCAL_NONE for void. */ void (*emit_ret)(NativeDirectTarget*, CGLocal value); @@ -135,7 +135,7 @@ struct NativeDirectTarget { * lookup instead of re-resolving reg_info and linearly scanning ri->classes. */ const NativeRegInfo* reg_info; - const NativeAllocClassInfo* class_info[3]; + const NativeAllocClassInfo* class_info[NATIVE_REG_CLASS_COUNT]; /* Per-class -O0 value-cache register pool, precomputed at construction in * `allocable[]` order. On ndt_caller_saved_only targets (aa64) it is the @@ -149,8 +149,8 @@ struct NativeDirectTarget { * Note: `allocable[]` is read only by the NDT, never the optimizer (which * allocates over the phys[] ALLOCABLE flags), so each arch tunes it for -O0 * register caching independently of -O1. */ - Reg cache_pool[3][32]; - u32 ncache_pool[3]; + Reg cache_pool[NATIVE_REG_CLASS_COUNT][NATIVE_MAX_HARD_REGS]; + u32 ncache_pool[NATIVE_REG_CLASS_COUNT]; const CGFuncDesc* func; SrcLoc loc; @@ -175,16 +175,16 @@ struct NativeDirectTarget { u32 nscopes; u32 scopes_cap; - u32 scratch_used[3]; + u32 scratch_used[NATIVE_REG_CLASS_COUNT]; /* Per-function callee-saved registers borrowed by direct scratch/cache * allocation. Reported to the native backend before prologue patching. */ - u32 callee_saved_used[3]; + u32 callee_saved_used[NATIVE_REG_CLASS_COUNT]; /* Local register cache (write-back, basic-block-scoped). reg_owner[cls][reg] * names the semantic local currently cached in that physical register, or * CG_LOCAL_NONE. scratch_used doubles as the per-class "pinned for the * current instruction" mask. Per-local cache state (reg/cls/dirty) lives on * NativeDirectLocal. See doc/CODEGEN.md "local register cache". */ - CGLocal reg_owner[3][32]; + CGLocal reg_owner[NATIVE_REG_CLASS_COUNT][NATIVE_MAX_HARD_REGS]; u32 use_tick; /* monotonic counter stamped onto NativeDirectLocal.last_use */ /* Flat mirror of the currently-owning local's last_use, keyed by physical * register, so the LRU victim scan reads one contiguous array instead of @@ -192,7 +192,7 @@ struct NativeDirectTarget { * whenever an owned register's last_use changes; only read for registers with * reg_owner[cls][reg] != CG_LOCAL_NONE, where it equals that local's * last_use. */ - u32 reg_last_use[3][32]; + u32 reg_last_use[NATIVE_REG_CLASS_COUNT][NATIVE_MAX_HARD_REGS]; /* Head/tail of the intrusive cached-locals list (in caching order), -1 when * empty; ncached is its length. Lets nd_flush_all run in O(cached) instead of * scanning all nlocals on every control-flow / barrier op; cache_tail makes diff --git a/src/opt/ir.h b/src/opt/ir.h @@ -219,8 +219,7 @@ typedef struct OptCGCallPlan { u32 nargs; u32 nrets; u32 stack_arg_size; - u32 clobber_mask[3]; - u32 return_mask[3]; + u32 clobber_mask[NATIVE_REG_CLASS_COUNT]; u16 flags; u8 has_sret; u8 is_variadic; @@ -421,11 +420,11 @@ typedef struct IRBitFieldAux { BitFieldAccess access; } IRBitFieldAux; -#define OPT_REG_CLASSES 3u -#define OPT_MAX_HARD_REGS 32u +#define OPT_REG_CLASSES NATIVE_REG_CLASS_COUNT +#define OPT_MAX_HARD_REGS NATIVE_MAX_HARD_REGS /* One per-class clobber bitmask set; see Func.inst_clobbers. */ -typedef u32 OptInstClobberMask[OPT_REG_CLASSES]; +typedef NativeRegMaskSet OptInstClobberMask; #define OPT_MAX_EMIT_TEMPS 4u typedef struct IRGepAux { diff --git a/src/opt/pass_lower.c b/src/opt/pass_lower.c @@ -396,7 +396,7 @@ static void set_preg_pref_for_params(Func* f) { } /* Advance the ABI cursors for every part of this param's home, regardless * of whether we hinted, so subsequent params see the right slot. */ - for (u16 j = 0; j < ai->nparts; ++j) { + for (u32 j = 0; j < ai->nparts; ++j) { u32* c = (ai->parts[j].cls == ABI_CLASS_FP) ? &next_fp : &next_int; *c += 1u; } @@ -407,7 +407,7 @@ static void set_preg_pref_for_params(Func* f) { * PReg to the matching ABI arg register. Sequential int/fp counters mirror * the per-class arg slot assignment used by set_preg_pref_for_params. Skips * variadic, has_sret, and indirect/aggregate args: they need per-target - * counter logic that hasn't been factored out of plan_call. */ + * counter logic that hasn't been factored out of marshal_call. */ static void set_preg_pref_for_call_args(Func* f, const CGCallDesc* desc) { if (!f || !desc) return; /* Tail calls handle arg routing through the tail-call shuffle in the @@ -439,7 +439,7 @@ static void set_preg_pref_for_call_args(Func* f, const CGCallDesc* desc) { if (phys_arg_reg_for_index(f, cls, *counter, &hint)) set_preg_pref_to_arg_reg(f, &a->storage, hint); } - for (u16 p = 0; p < ai->nparts; ++p) { + for (u32 p = 0; p < ai->nparts; ++p) { u32* c = (ai->parts[p].cls == ABI_CLASS_FP) ? &next_fp : &next_int; *c += 1u; } diff --git a/src/opt/pass_machinize.c b/src/opt/pass_machinize.c @@ -113,110 +113,23 @@ static void machinize_prepare_insts(Func* f, NativeTarget* target) { } } -static const NativePhysRegInfo* class_phys_reg( - const NativeAllocClassInfo* ci, Reg reg) { - for (u32 i = 0; ci && i < ci->nphys; ++i) - if (ci->phys[i].reg == reg) return &ci->phys[i]; - return NULL; -} - -static u32 validate_reg_list(Func* f, const NativeAllocClassInfo* ci, - const Reg* regs, u32 nregs, - const char* list_name) { - u32 mask = 0; - if ((nregs != 0u) != (regs != NULL)) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u has inconsistent %s list", - (unsigned)ci->cls, list_name); - if (nregs > OPT_MAX_HARD_REGS) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u has too many %s registers (%u)", - (unsigned)ci->cls, list_name, (unsigned)nregs); - for (u32 i = 0; i < nregs; ++i) { - Reg reg = regs[i]; - const NativePhysRegInfo* phys; - if (reg >= OPT_MAX_HARD_REGS || (mask & (1u << reg))) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u has invalid/duplicate %s reg %u", - (unsigned)ci->cls, list_name, (unsigned)reg); - phys = class_phys_reg(ci, reg); - if (!phys || phys->cls != ci->cls) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u %s reg %u is not a matching " - "physical register", - (unsigned)ci->cls, list_name, (unsigned)reg); - mask |= 1u << reg; - } - return mask; -} - static void collect_class(Func* f, NativeTarget* target, const NativeAllocClassInfo* ci) { u32 cls = ci->cls; - u32 known_mask = 0; - u32 emit_mask; - u32 asm_mask; + u32 emit_mask = 0; + u32 asm_mask = 0; if (cls >= OPT_REG_CLASSES) compiler_panic(f->c, (SrcLoc){0, 0, 0}, "opt_machinize: invalid register class %u", (unsigned)cls); - if ((ci->nphys != 0u) != (ci->phys != NULL) || - ci->nphys > OPT_MAX_HARD_REGS) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u has invalid physical table size %u", - (unsigned)cls, (unsigned)ci->nphys); - for (u32 i = 0; i < ci->nphys; ++i) { - const NativePhysRegInfo* phys = &ci->phys[i]; - if (phys->reg >= OPT_MAX_HARD_REGS || phys->cls != cls || - (known_mask & (1u << phys->reg))) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u has invalid physical reg %u", - (unsigned)cls, (unsigned)phys->reg); - known_mask |= 1u << phys->reg; - } - (void)validate_reg_list(f, ci, ci->ndt_allocable, ci->ndt_allocable_count, - "O0 allocable"); - (void)validate_reg_list(f, ci, ci->scratch, ci->nscratch, "O0 scratch"); if (ci->nemit_temps > OPT_MAX_EMIT_TEMPS) compiler_panic(f->c, (SrcLoc){0, 0, 0}, "opt_machinize: class %u needs %u emit temps (capacity %u)", (unsigned)cls, (unsigned)ci->nemit_temps, (unsigned)OPT_MAX_EMIT_TEMPS); - emit_mask = validate_reg_list(f, ci, ci->emit_temps, ci->nemit_temps, - "emit temp"); - asm_mask = validate_reg_list(f, ci, ci->asm_temps, ci->nasm_temps, - "asm-internal temp"); - if (emit_mask & asm_mask) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u emit and asm temp banks overlap", - (unsigned)cls); - if (asm_mask & ~ci->reserved_mask) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u asm temp bank is not reserved", - (unsigned)cls); - if (ci->emit_cache_mask & ~emit_mask) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u cache mask escapes emit temp bank", - (unsigned)cls); - if ((ci->caller_saved_mask | ci->callee_saved_mask | ci->arg_mask | - ci->ret_mask | ci->reserved_mask) & - ~known_mask) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u register mask names unknown phys", - (unsigned)cls); - for (u32 i = 0; i < ci->nemit_temps; ++i) { - const NativePhysRegInfo* phys = class_phys_reg(ci, ci->emit_temps[i]); - if (phys && (phys->flags & NATIVE_REG_ALLOCABLE)) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u emit temp %u is allocable", - (unsigned)cls, (unsigned)ci->emit_temps[i]); - } - for (u32 i = 0; i < ci->nasm_temps; ++i) { - const NativePhysRegInfo* phys = class_phys_reg(ci, ci->asm_temps[i]); - if (phys && (phys->flags & NATIVE_REG_ALLOCABLE)) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: class %u asm temp %u is allocable", - (unsigned)cls, (unsigned)ci->asm_temps[i]); - } + for (u32 i = 0; i < ci->nemit_temps; ++i) + emit_mask |= 1u << ci->emit_temps[i]; + for (u32 i = 0; i < ci->nasm_temps; ++i) + asm_mask |= 1u << ci->asm_temps[i]; f->opt_caller_saved[cls] = native_target_caller_saved_mask(target, (NativeAllocClass)cls); f->opt_callee_saved[cls] = @@ -226,10 +139,13 @@ static void collect_class(Func* f, NativeTarget* target, "opt_machinize: class %u emit temp bank is not entirely " "caller-saved", (unsigned)cls); - f->opt_reserved_regs[cls] = ci->reserved_mask | emit_mask; + /* Static reservations come from register roles. The emitter temp bank is + * separately policy-owned, so reserve it for this function here too. */ + f->opt_reserved_regs[cls] = + native_target_reserved_mask(target, (NativeAllocClass)cls) | emit_mask; f->asm_temp_mask[cls] = asm_mask; - f->opt_arg_regs[cls] = ci->arg_mask; - f->opt_ret_regs[cls] = ci->ret_mask; + f->opt_arg_regs[cls] = native_target_arg_mask(target, (NativeAllocClass)cls); + f->opt_ret_regs[cls] = native_target_ret_mask(target, (NativeAllocClass)cls); for (u32 i = 0; i < ci->nphys; ++i) { const NativePhysRegInfo* src = &ci->phys[i]; CGPhysRegInfo* dst = &f->opt_phys_regs[cls][f->opt_phys_reg_count[cls]++]; @@ -250,10 +166,7 @@ static void collect_class(Func* f, NativeTarget* target, static void machinize_collect_regs(Func* f, NativeTarget* target) { u32 seen = 0; if (!target || !target->regs) return; - if (target->regs->nclasses > OPT_REG_CLASSES || - (target->regs->nclasses != 0u) != (target->regs->classes != NULL)) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: invalid register class table"); + native_reg_info_validate(f->c, target->regs); for (u32 i = 0; i < target->regs->nclasses; ++i) { u32 cls = target->regs->classes[i].cls; if (cls >= OPT_REG_CLASSES || (seen & (1u << cls))) @@ -326,7 +239,7 @@ void opt_refresh_machine_clobbers(Func* f, NativeTarget* target) { for (u32 i = 0; i < bl->ninsts; ++i) { Inst* in = &bl->insts[i]; NativeMachineOp mop; - u32 mask[NATIVE_CALL_PLAN_CLASSES]; + u32 mask[NATIVE_REG_CLASS_COUNT]; int has_clobbers; memset(&mop, 0, sizeof mop); switch ((IROp)in->op) { diff --git a/src/opt/pass_native_emit.c b/src/opt/pass_native_emit.c @@ -1314,7 +1314,7 @@ done: } /* Execute one move in a multi-move ABI phase and retain its register - * destination. The backend call/return planners promise that moves are + * destination. The backend call/return marshallers promise that moves are * source-safe in the order returned; this helper supplies the complementary * destination-lifetime guarantee for emitter-side materialization. */ static void write_phase_loc(NativeEmitCtx* e, NativeLoc dst, NativeLoc src, @@ -1649,7 +1649,7 @@ static NativeLoc abi_storage_loc(NativeEmitCtx* e, const OptCGABIValue* v, static void emit_call(NativeEmitCtx* e, Inst* in) { IRCallAux* aux = (IRCallAux*)in->extra.aux; NativeCallDesc d; - NativeCallPlan plan; + NativeCallPhase plan; NativeLoc* args = NULL; NativeLoc* results = NULL; NativeLoc final_result = loc_none(); @@ -1666,9 +1666,9 @@ static void emit_call(NativeEmitCtx* e, Inst* in) { KitCgTypeId rty = aux->desc.ret.type; results = arena_zarray(e->f->arena, NativeLoc, 1); final_result = abi_storage_loc(e, &aux->desc.ret, in->loc); - /* Hand plan_call the value's real destination directly whenever it is a + /* Hand marshal_call the value's real destination directly whenever it is a * register or a frame slot: a scalar result is a single move out of the ABI - * result register, and an aggregate / oversized result — which plan_call or + * result register, and an aggregate / oversized result — which marshal_call or * the callee writes in parts and so must land in memory — lands straight in * its frame home. Routing either through a fresh temp slot (store then * reload / copy_bytes) was a pure round trip on every call. The temp slot @@ -1693,16 +1693,16 @@ static void emit_call(NativeEmitCtx* e, Inst* in) { d.flags = aux->desc.flags; d.tail_policy = aux->desc.tail_policy; d.inline_policy = aux->desc.inline_policy; - /* Materialize a frame-resident indirect callee before plan_call starts its + /* Materialize a frame-resident indirect callee before marshal_call starts its * argument phase. Real backends marshal register arguments inside - * plan_call; doing this afterward allowed the emitter to reuse an already + * marshal_call; doing this afterward allowed the emitter to reuse an already * populated ABI argument register for the callee. The acquired lease also - * keeps generic plan.args moves from selecting the callee register. */ + * keeps generic phase argument moves from selecting the callee register. */ if (d.callee.kind != NATIVE_LOC_REG && d.callee.kind != NATIVE_LOC_GLOBAL) d.callee = materialize(e, d.callee, NATIVE_REG_INT, d.callee.type, REG_NONE, REG_NONE, in->loc); - e->target->plan_call(e->target, &d, &plan); + e->target->marshal_call(e->target, &d, &plan); for (u32 i = 0; i < plan.nargs; ++i) write_phase_loc(e, plan.args[i].dst, plan.args[i].src, plan.args[i].mem, in->loc); @@ -1720,7 +1720,7 @@ static void emit_call(NativeEmitCtx* e, Inst* in) { type_size_or(e->c, rty, 8u) > e->c->target.ptr_size)) { /* Aggregate / oversized result: move bytes rather than a scalar copy * (which would exceed the single-register width). The result was either - * written in parts by plan_call's rets, or by the callee via the sret + * written in parts by marshal_call's rets, or by the callee via the sret * pointer; either way it now lives in the temp slot. */ AggregateAccess acc; NativeAddr da = addr_from_loc(e, final_result, in->loc); @@ -1741,19 +1741,19 @@ static void emit_ret(NativeEmitCtx* e, Inst* in, const CGFuncDesc* fd) { IRRetAux* aux = (IRRetAux*)in->extra.aux; NativeLoc value = loc_none(); const NativeLoc* values = NULL; - NativeCallPlanRet* rets = NULL; + NativeCallPhaseRet* rets = NULL; u32 nrets = 0; if (aux && aux->present) { - /* Hand plan_ret the value's location directly. For an aggregate / oversized - * result it is a memory location (plan_ret copies to the sret pointer or + /* Hand marshal_ret the value's location directly. For an aggregate / oversized + * result it is a memory location (marshal_ret copies to the sret pointer or * reads parts into the return registers); for a scalar it is the value's - * register or slot, which plan_ret moves into the return register. The old + * register or slot, which marshal_ret moves into the return register. The old * code spilled scalars to a fresh slot and reloaded them, a pure round * trip on every return. */ value = abi_storage_loc(e, &aux->val, in->loc); values = &value; } - e->target->plan_ret(e->target, fd, values, &rets, &nrets); + e->target->marshal_ret(e->target, fd, values, &rets, &nrets); for (u32 i = 0; i < nrets; ++i) write_phase_loc(e, rets[i].dst, rets[i].src, rets[i].mem, in->loc); /* Skip the trailing branch-to-epilogue when this IR_RET is the very last @@ -2567,7 +2567,7 @@ static void plan_one_asm_staging(NativeEmitCtx* e, Inst* in, IRAsmAux* aux) { temp_scope_end(e, &scope); } -static NativeLoc* plan_call_args_reserve(NativeEmitCtx* e, NativeLoc* args, +static NativeLoc* marshal_call_args_reserve(NativeEmitCtx* e, NativeLoc* args, u32* cap, u32 want) { u32 grown; if (want <= *cap) return args; @@ -2643,7 +2643,7 @@ static void plan_frame(NativeEmitCtx* e, const CGFuncDesc* fd) { if (aux && t->call_stack_bytes) { NativeCallDesc d; u32 sb; - call_args = plan_call_args_reserve( + call_args = marshal_call_args_reserve( e, call_args, &call_args_cap, aux->desc.nargs); memset(&d, 0, sizeof d); d.fn_type = aux->desc.fn_type; diff --git a/test/api/abi_classify_test.c b/test/api/abi_classify_test.c @@ -32,7 +32,8 @@ static void expect_direct_1x_int(const char* tag, const ABIArgInfo* ai, static KitTargetSpec test_target_spec(KitArchKind arch, KitOSKind os, KitObjFmt obj) { KitTargetSpec t = kit_unit_target(arch, os, obj); - if (arch == KIT_ARCH_RV32 || arch == KIT_ARCH_WASM) { + if (arch == KIT_ARCH_ARM_32 || arch == KIT_ARCH_RV32 || + arch == KIT_ARCH_WASM) { t.ptr_size = 4; t.ptr_align = 4; } @@ -80,6 +81,8 @@ static const char* arch_name(KitArchKind a) { return "x64"; case KIT_ARCH_ARM_64: return "aarch64"; + case KIT_ARCH_ARM_32: + return "arm32"; case KIT_ARCH_RV32: return "rv32"; case KIT_ARCH_RV64: @@ -191,6 +194,155 @@ static KitCgTypeId record2(KitCompiler* c, KitCgTypeId a, KitCgTypeId b) { return kit_cg_type_record(c, &desc); } +static KitCgTypeId record_i32s(KitCompiler* c, u32 n) { + KitCgFieldDesc fields[8]; + KitCgRecordDesc desc; + KitCgTypeId i32 = builtin(c, KIT_CG_BUILTIN_I32); + if (n == 0u || n > sizeof fields / sizeof fields[0]) return KIT_CG_TYPE_NONE; + memset(fields, 0, sizeof fields); + for (u32 i = 0; i < n; ++i) fields[i].type = i32; + memset(&desc, 0, sizeof desc); + desc.fields = fields; + desc.nfields = n; + return kit_cg_type_record(c, &desc); +} + +static void expect_arm32_word_parts(const char* tag, const ABIArgInfo* ai, + u32 nwords) { + EXPECT(ai->kind == ABI_ARG_DIRECT, "%s: kind=%d want DIRECT", tag, + (int)ai->kind); + EXPECT(ai->nparts == nwords, "%s: nparts=%u want %u", tag, + (unsigned)ai->nparts, (unsigned)nwords); + if (ai->nparts != nwords || !ai->parts) return; + for (u32 i = 0; i < nwords; ++i) { + EXPECT(ai->parts[i].cls == ABI_CLASS_INT, + "%s: parts[%u].cls=%d want INT", tag, (unsigned)i, + (int)ai->parts[i].cls); + EXPECT(ai->parts[i].size == 4u, + "%s: parts[%u].size=%u want 4", tag, (unsigned)i, + (unsigned)ai->parts[i].size); + EXPECT(ai->parts[i].src_offset == i * 4u, + "%s: parts[%u].src_offset=%u want %u", tag, (unsigned)i, + (unsigned)ai->parts[i].src_offset, (unsigned)(i * 4u)); + } +} + +static void test_aapcs32_specifics(void) { + KitCompiler* c = + new_compiler(KIT_ARCH_ARM_32, KIT_OS_FREESTANDING, KIT_OBJ_ELF); + static const u32 words[] = {1u, 2u, 4u, 8u}; + char tag[64]; + + for (u32 i = 0; i < sizeof words / sizeof words[0]; ++i) { + KitCgTypeId rec = record_i32s(c, words[i]); + const ABIFuncInfo* fi = classify_fn(c, rec, rec); + snprintf(tag, sizeof tag, "arm32 record%u arg", (unsigned)(words[i] * 4u)); + expect_arm32_word_parts(tag, &fi->params[0], words[i]); + snprintf(tag, sizeof tag, "arm32 record%u ret", (unsigned)(words[i] * 4u)); + if (words[i] == 1u) { + expect_arm32_word_parts(tag, &fi->ret, 1u); + EXPECT(fi->has_sret == 0, "%s: one-word result should not use sret", tag); + EXPECT(fi->sret_consumes_int_arg == 0, + "%s: one-word result should not consume an argument register", + tag); + } else { + EXPECT(fi->ret.kind == ABI_ARG_INDIRECT, + "%s: kind=%d want INDIRECT", tag, (int)fi->ret.kind); + EXPECT(fi->ret.nparts == 0, "%s: nparts=%u want 0", tag, + (unsigned)fi->ret.nparts); + EXPECT((fi->ret.flags & ABI_AF_SRET) != 0, + "%s: flags=0x%x missing SRET", tag, (unsigned)fi->ret.flags); + EXPECT(fi->ret.indirect_align == 4u, + "%s: indirect_align=%u want 4", tag, + (unsigned)fi->ret.indirect_align); + EXPECT(fi->has_sret == 1, "%s: composite result should use sret", tag); + EXPECT(fi->sret_consumes_int_arg == 1, + "%s: sret pointer should consume r0", tag); + } + } + + { + KitCgFieldDesc fields[3]; + KitCgRecordDesc desc; + KitCgTypeId i8 = builtin(c, KIT_CG_BUILTIN_I8); + const ABIFuncInfo* fi; + memset(fields, 0, sizeof fields); + for (u32 i = 0; i < 3u; ++i) fields[i].type = i8; + memset(&desc, 0, sizeof desc); + desc.fields = fields; + desc.nfields = 3u; + fi = classify_fn(c, kit_cg_type_record(c, &desc), + kit_cg_type_record(c, &desc)); + EXPECT(fi->params[0].kind == ABI_ARG_DIRECT, + "arm32 record3 arg: want DIRECT"); + EXPECT(fi->params[0].nparts == 1u, + "arm32 record3 arg: nparts=%u want 1", + (unsigned)fi->params[0].nparts); + EXPECT(fi->params[0].parts && fi->params[0].parts[0].size == 3u, + "arm32 record3 arg: tail size should be exact"); + EXPECT(fi->ret.kind == ABI_ARG_DIRECT, + "arm32 record3 ret: want DIRECT"); + EXPECT(fi->ret.nparts == 1u, + "arm32 record3 ret: nparts=%u want 1", + (unsigned)fi->ret.nparts); + EXPECT(fi->ret.parts && fi->ret.parts[0].size == 3u, + "arm32 record3 ret: tail size should be exact"); + EXPECT(fi->has_sret == 0, + "arm32 record3 ret: one-word result should not use sret"); + } + + { + KitCgTypeId i64 = builtin(c, KIT_CG_BUILTIN_I64); + KitCgTypeId rec = record2(c, i64, i64); + const ABIFuncInfo* fi = classify_fn(c, rec, rec); + expect_arm32_word_parts("arm32 aligned record16 arg", &fi->params[0], + 4u); + EXPECT(fi->params[0].parts && fi->params[0].parts[0].align == 8u, + "arm32 aligned record16 arg: first-part align=%u want 8", + fi->params[0].parts + ? (unsigned)fi->params[0].parts[0].align + : 0u); + EXPECT(fi->ret.kind == ABI_ARG_INDIRECT, + "arm32 aligned record16 ret: want INDIRECT"); + EXPECT(fi->ret.indirect_align == 8u, + "arm32 aligned record16 ret: indirect_align=%u want 8", + (unsigned)fi->ret.indirect_align); + EXPECT((fi->ret.flags & ABI_AF_SRET) != 0, + "arm32 aligned record16 ret: missing SRET"); + } + + { + static const u32 nwords = 65537u; + KitCgFieldDesc field; + KitCgRecordDesc desc; + KitCgTypeId i32 = builtin(c, KIT_CG_BUILTIN_I32); + KitCgTypeId array = kit_cg_type_array(c, i32, nwords); + const ABIFuncInfo* fi; + memset(&field, 0, sizeof field); + field.type = array; + memset(&desc, 0, sizeof desc); + desc.fields = &field; + desc.nfields = 1u; + fi = classify_fn(c, kit_cg_type_record(c, &desc), + kit_cg_type_record(c, &desc)); + EXPECT(fi->params[0].kind == ABI_ARG_DIRECT, + "arm32 huge record arg: want DIRECT"); + EXPECT(fi->params[0].nparts == nwords, + "arm32 huge record arg: nparts=%u want %u", + (unsigned)fi->params[0].nparts, (unsigned)nwords); + EXPECT(fi->params[0].parts && + fi->params[0].parts[nwords - 1u].src_offset == + (nwords - 1u) * 4u && + fi->params[0].parts[nwords - 1u].size == 4u, + "arm32 huge record arg: last carrier lane was truncated"); + EXPECT(fi->ret.kind == ABI_ARG_INDIRECT && + (fi->ret.flags & ABI_AF_SRET) != 0, + "arm32 huge record ret: want INDIRECT SRET"); + } + + kit_compiler_free(c); +} + static void check_target(KitArchKind arch, KitOSKind os, KitObjFmt obj) { KitCompiler* c = new_compiler(arch, os, obj); KitCgTypeId i128_ty = builtin(c, KIT_CG_BUILTIN_I128); @@ -622,6 +774,8 @@ static void test_scalar_split_lane_size(void) { * integer-register pairs regardless of the float ABI. */ check_scalar_split_lane_target(KIT_ARCH_RV32, KIT_OS_LINUX, KIT_OBJ_ELF, "rv32", 4, 4); + check_scalar_split_lane_target(KIT_ARCH_ARM_32, KIT_OS_FREESTANDING, + KIT_OBJ_ELF, "arm32", 4, 4); check_scalar_split_lane_target(KIT_ARCH_RV64, KIT_OS_LINUX, KIT_OBJ_ELF, "rv64", 0, 0); check_scalar_split_lane_target(KIT_ARCH_WASM, KIT_OS_WASI, KIT_OBJ_WASM, @@ -639,6 +793,7 @@ int main(void) { test_win64_specifics(); test_aarch64_windows_variadic(); test_apple_arm64_stack_traits(); + test_aapcs32_specifics(); test_scalar_split_lane_size(); kit_unit_summary(&g_u, "abi_classify_test"); return kit_unit_status(&g_u); diff --git a/test/arch/arm32_inline_test.c b/test/arch/arm32_inline_test.c @@ -24,6 +24,20 @@ static void arm32_bad_operand(KitCompiler* c, KitCg* cg, KitCgTypeId i64_ty) { it_inline_asm(c, cg, "mov %9, r0", NULL, 0, NULL, 0, NULL, 0); } +static void arm32_named_r4_clobber(KitCompiler* c, KitCg* cg, + KitCgTypeId i64_ty) { + static const char* const clobbers[] = {"r4"}; + (void)i64_ty; + it_inline_asm(c, cg, "movs r4, #99", NULL, 0, NULL, 0, clobbers, 1); +} + +static void arm32_bad_frame_clobber(KitCompiler* c, KitCg* cg, + KitCgTypeId i64_ty) { + static const char* const clobbers[] = {"r7"}; + (void)i64_ty; + it_inline_asm(c, cg, "", NULL, 0, NULL, 0, clobbers, 1); +} + /* r0-r3 are the direct binder's caller-saved operand pool. Five simultaneous * generic inputs therefore put the fifth value in callee-saved r4. */ static void arm32_generic_callee_saved_operand(KitCompiler* c, KitCg* cg, @@ -60,6 +74,49 @@ static int arm32_is_r4_mem(const KitInsn* insn, const char* mnemonic) { insn->operands.s[1] == '4'; } +static int arm32_is_r4_reglist(const KitInsn* insn, const char* mnemonic) { + size_t n = strlen(mnemonic); + if (!insn->mnemonic.s || insn->mnemonic.len < n || + memcmp(insn->mnemonic.s, mnemonic, n) != 0 || !insn->operands.s) + return 0; + for (size_t i = 0; i + 1u < insn->operands.len; ++i) + if (insn->operands.s[i] == 'r' && insn->operands.s[i + 1u] == '4') + return 1; + return 0; +} + +static int arm32_has_known_frame_r4_preservation(InlineTestEnv* env, + const InlineText* text) { + KitTargetOptions opts; + KitTarget* target = NULL; + KitDisasmContext dc; + KitDisasmIter* it = NULL; + KitInsn insn; + int saw_push = 0; + int saw_pop = 0; + + memset(&opts, 0, sizeof opts); + opts.spec = it_target(KIT_ARCH_ARM_32); + if (kit_target_new(&env->ctx, &opts, &target) != KIT_OK || !target) + goto done; + memset(&dc, 0, sizeof dc); + dc.target = target; + dc.context = env->ctx; + if (kit_disasm_iter_new(&dc, text->data, text->len, 0, text->file, &it) != + KIT_OK || + !it) + goto done; + while (kit_disasm_iter_next(it, &insn) == KIT_ITER_ITEM) { + if (arm32_is_r4_reglist(&insn, "push")) saw_push = 1; + if (arm32_is_r4_reglist(&insn, "pop")) saw_pop = 1; + } + +done: + if (it) kit_disasm_iter_free(it); + kit_target_free(target); + return saw_push && saw_pop; +} + static int arm32_has_generic_r4_preservation(InlineTestEnv* env, const InlineText* text) { KitTargetOptions opts; @@ -272,6 +329,12 @@ int main(void) { it_expect_panic(&env, KIT_ARCH_ARM_32, "arm32_bad_operand", arm32_bad_operand, "operand index"), "expected out-of-range arm32 asm operand to panic"); + IT_EXPECT(&env, + it_expect_panic(&env, KIT_ARCH_ARM_32, + "arm32_bad_frame_clobber", + arm32_bad_frame_clobber, + "structural register cannot be clobbered"), + "expected arm32 r7 clobber to panic"); IT_EXPECT(&env, arm32_check_thumb_func(&env), "arm32 .thumb_func symbol regression failed: %s", env.last_diag); @@ -286,6 +349,16 @@ int main(void) { "generic r4 asm operand was not saved and restored"); it_text_close(&text); + IT_EXPECT(&env, + it_emit_text_opt(&env, KIT_ARCH_ARM_32, + "arm32_named_r4_clobber_o1", + arm32_named_r4_clobber, 1, &text), + "failed to emit O1 named r4 clobber: %s", env.last_diag); + if (text.data) + IT_EXPECT(&env, arm32_has_known_frame_r4_preservation(&env, &text), + "O1 known frame did not preserve named r4 clobber"); + it_text_close(&text); + if (env.fails) { fprintf(stderr, "%d failure(s)\n", env.fails); return 1; diff --git a/test/cg/native_direct_target_test.c b/test/cg/native_direct_target_test.c @@ -7,6 +7,7 @@ #include <string.h> #include "core/arena.h" +#include "cg/native_asm.h" #include "lib/kit_unit.h" /* Shared test context replaces the per-file heap/diag/counter globals; @@ -53,9 +54,9 @@ typedef enum MockEventKind { EV_LOAD_IMM, EV_BINOP, EV_BARRIER, - EV_PLAN_CALL, + EV_MARSHAL_CALL, EV_EMIT_CALL, - EV_PLAN_RET, + EV_MARSHAL_RET, EV_MOVE, EV_RET, } MockEventKind; @@ -82,23 +83,51 @@ static const Reg mock_fp_scratch[] = {4u, 5u}; static const Reg mock_ndt_int_allocable[] = {1u, 2u, 3u, 6u}; static const Reg mock_ndt_fp_allocable[] = {4u, 5u, 6u}; +#define MOCK_PHYS(cls_, reg_, flags_) \ + {.reg = (reg_), \ + .cls = (cls_), \ + .abi_index = 0xffu, \ + .flags = (flags_), \ + .spill_cost = 1u, \ + .copy_cost = 1u} + +static const NativePhysRegInfo mock_int_phys[] = { + MOCK_PHYS(NATIVE_REG_INT, 0u, + NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | NATIVE_REG_RET), + MOCK_PHYS(NATIVE_REG_INT, 1u, + NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED), + MOCK_PHYS(NATIVE_REG_INT, 2u, + NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED), + MOCK_PHYS(NATIVE_REG_INT, 3u, + NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED), + MOCK_PHYS(NATIVE_REG_INT, 6u, NATIVE_REG_ALLOCABLE), +}; + +static const NativePhysRegInfo mock_fp_phys[] = { + MOCK_PHYS(NATIVE_REG_FP, 0u, + NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | NATIVE_REG_RET), + MOCK_PHYS(NATIVE_REG_FP, 4u, + NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED), + MOCK_PHYS(NATIVE_REG_FP, 5u, + NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED), + MOCK_PHYS(NATIVE_REG_FP, 6u, NATIVE_REG_ALLOCABLE), +}; + static const NativeAllocClassInfo mock_classes[] = { {.cls = NATIVE_REG_INT, .ndt_allocable = mock_ndt_int_allocable, .ndt_allocable_count = 4, .scratch = mock_int_scratch, .nscratch = 3, - .caller_saved_mask = (1u << 1) | (1u << 2) | (1u << 3), - .arg_mask = 1u << 0, - .ret_mask = 1u << 0}, + .phys = mock_int_phys, + .nphys = sizeof mock_int_phys / sizeof mock_int_phys[0]}, {.cls = NATIVE_REG_FP, .ndt_allocable = mock_ndt_fp_allocable, .ndt_allocable_count = 3, .scratch = mock_fp_scratch, .nscratch = 2, - .caller_saved_mask = (1u << 4) | (1u << 5), - .arg_mask = 1u << 0, - .ret_mask = 1u << 0}, + .phys = mock_fp_phys, + .nphys = sizeof mock_fp_phys / sizeof mock_fp_phys[0]}, }; static const NativeRegInfo mock_reg_info = { @@ -106,6 +135,48 @@ static const NativeRegInfo mock_reg_info = { .nclasses = sizeof mock_classes / sizeof mock_classes[0], }; +static int reordered_constraint_reg(const NativeRegInfo* ri, const char* body, + NativeAllocClass* cls_out, Reg* fixed_out, + u32* allowed_mask_out) { + (void)ri; + if (strcmp(body, "x") != 0) return 0; + *cls_out = NATIVE_REG_FP; + *fixed_out = 4u; + *allowed_mask_out = 1u << 4u; + return 1; +} + +static void test_register_class_lookup_is_not_positional(void) { + NativePhysRegInfo int_phys = + MOCK_PHYS(NATIVE_REG_INT, 1u, NATIVE_REG_ALLOCABLE); + NativePhysRegInfo fp_phys = + MOCK_PHYS(NATIVE_REG_FP, 4u, NATIVE_REG_ALLOCABLE); + NativeAllocClassInfo classes[2]; + NativeRegInfo regs; + NativeTarget target; + NativeAsmConstraintInfo info; + memset(classes, 0, sizeof classes); + classes[0].cls = NATIVE_REG_FP; + classes[0].phys = &fp_phys; + classes[0].nphys = 1u; + classes[1].cls = NATIVE_REG_INT; + classes[1].phys = &int_phys; + classes[1].nphys = 1u; + memset(&regs, 0, sizeof regs); + regs.classes = classes; + regs.nclasses = 2u; + regs.asm_constraint_reg = reordered_constraint_reg; + memset(&target, 0, sizeof target); + target.regs = &regs; + + EXPECT(native_reg_info_class_info(&regs, NATIVE_REG_FP) == &classes[0] && + native_reg_info_class_info(&regs, NATIVE_REG_INT) == &classes[1], + "register class lookup depends on table order"); + EXPECT(native_asm_constraint_reg_info(&target, "x", &info) && + info.cls == NATIVE_REG_FP && info.fixed_reg == 4u, + "inline-asm fallback used a positional register class"); +} + static MockNative* mock_of(NativeTarget* t) { return (MockNative*)t; } static void ev(MockNative* m, MockEventKind kind, u32 a, u32 b, u32 c) { @@ -193,18 +264,18 @@ static void mock_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) { ev(mock_of(t), EV_MOVE, dst.v.reg, src.v.reg, dst.type); } -static void mock_emit_call(NativeTarget* t, const NativeCallPlan* plan) { +static void mock_emit_call(NativeTarget* t, const NativeCallPhase* plan) { EXPECT(plan->callee.kind == NATIVE_LOC_REG, "frame callee should be materialized for call"); ev(mock_of(t), EV_EMIT_CALL, plan->nargs, plan->nrets, plan->callee.v.reg); } -static void mock_plan_ret(NativeTarget* t, const CGFuncDesc* fd, - const NativeLoc* value, NativeCallPlanRet** out_rets, - u32* out_nrets) { - NativeCallPlanRet* r; +static void mock_marshal_ret(NativeTarget* t, const CGFuncDesc* fd, + const NativeLoc* value, + NativeCallPhaseRet** out_rets, u32* out_nrets) { + NativeCallPhaseRet* r; (void)fd; - r = arena_zarray(t->c->tu, NativeCallPlanRet, 1); + r = arena_zarray(t->c->tu, NativeCallPhaseRet, 1); if (value) { r[0].src = *value; r[0].dst.kind = NATIVE_LOC_REG; @@ -217,7 +288,7 @@ static void mock_plan_ret(NativeTarget* t, const CGFuncDesc* fd, } *out_rets = r; *out_nrets = value ? 1u : 0u; - ev(mock_of(t), EV_PLAN_RET, 0, value ? 1u : 0u, 0); + ev(mock_of(t), EV_MARSHAL_RET, 0, value ? 1u : 0u, 0); } static void mock_ret(NativeTarget* t) { ev(mock_of(t), EV_RET, 0, 0, 0); } @@ -240,7 +311,7 @@ static void mock_native_init(MockNative* m, Compiler* c) { m->base.binop = mock_binop; m->base.move_rr = mock_move_rr; m->base.emit_call = mock_emit_call; - m->base.plan_ret = mock_plan_ret; + m->base.marshal_ret = mock_marshal_ret; m->base.ret = mock_ret; } @@ -250,13 +321,13 @@ static void mock_barrier(NativeDirectTarget* d, u32 flags) { ev(m, EV_BARRIER, 0, 0, flags); } -static void mock_plan_call(NativeDirectTarget* d, const NativeCallDesc* desc, - NativeCallPlan* plan) { +static void mock_marshal_call(NativeDirectTarget* d, const NativeCallDesc* desc, + NativeCallPhase* plan) { MockNative* m = (MockNative*)d->native; - NativeCallPlanMove* args = - arena_zarray(d->base.c->tu, NativeCallPlanMove, desc->nargs); - NativeCallPlanRet* rets = - arena_zarray(d->base.c->tu, NativeCallPlanRet, desc->nresults); + NativeCallPhaseMove* args = + arena_zarray(d->base.c->tu, NativeCallPhaseMove, desc->nargs); + NativeCallPhaseRet* rets = + arena_zarray(d->base.c->tu, NativeCallPhaseRet, desc->nresults); memset(plan, 0, sizeof *plan); plan->callee = desc->callee; plan->args = args; @@ -285,11 +356,11 @@ static void mock_plan_call(NativeDirectTarget* d, const NativeCallDesc* desc, rets[i].mem.align = 4; } m->last_stack_arg_size = plan->stack_arg_size; - ev(m, EV_PLAN_CALL, desc->nargs, desc->nresults, plan->stack_arg_size); + ev(m, EV_MARSHAL_CALL, desc->nargs, desc->nresults, plan->stack_arg_size); } static const NativeOps mock_ops = { - .plan_call = mock_plan_call, + .marshal_call = mock_marshal_call, .barrier = mock_barrier, }; @@ -432,7 +503,8 @@ static void test_frame_locals_scratch_storeback_and_branches(void) { EXPECT(count_event(&native, EV_CMP_BRANCH) == 1, "expected one compare branch"); EXPECT(count_event(&native, EV_JUMP) == 1, "expected one jump"); - EXPECT(count_event(&native, EV_PLAN_RET) == 1 && count_event(&native, EV_RET), + EXPECT(count_event(&native, EV_MARSHAL_RET) == 1 && + count_event(&native, EV_RET), "return should plan moves and emit ret"); tc_fini(&tc); } @@ -470,8 +542,9 @@ static void test_call_barrier_storeback_and_max_outgoing(void) { (NATIVE_DIRECT_BARRIER_CALL | NATIVE_DIRECT_BARRIER_MEMORY), "call should request call+memory barrier"); EXPECT(native.last_stack_arg_size == 24 && nd->max_outgoing == 24, - "call planning should track max outgoing stack size"); - EXPECT(count_event(&native, EV_PLAN_CALL) == 1, "expected one call plan"); + "call marshalling should track max outgoing stack size"); + EXPECT(count_event(&native, EV_MARSHAL_CALL) == 1, + "expected one call marshalling phase"); EXPECT(count_event(&native, EV_EMIT_CALL) == 1, "expected one emitted call"); EXPECT(count_event(&native, EV_STORE) == 2, "selective call setup should emit only its required stores (stores=%d)", @@ -618,6 +691,7 @@ static void test_b_call_still_flushes(void) { int main(void) { kit_unit_init(&g_u); g_u.ctx.now = -1; + test_register_class_lookup_is_not_positional(); test_frame_locals_scratch_storeback_and_branches(); test_call_barrier_storeback_and_max_outgoing(); test_b_cached_pointer_base_not_reloaded(); diff --git a/test/opt/frame_value_backend_test.c b/test/opt/frame_value_backend_test.c @@ -241,7 +241,7 @@ static void check_typed_frame_address(KitUnit *unit, const BackendCase *tc) { NativeLoc call_arg; NativeLoc exact_arg; NativeCallDesc call_desc; - NativeCallPlan call_plan; + NativeCallPhase call_plan; DecodedInsn body[12]; DecodedInsn call_body[8]; DecodedInsn global_body[12]; @@ -385,7 +385,7 @@ static void check_typed_frame_address(KitUnit *unit, const BackendCase *tc) { call_desc.nargs = 1u; memset(&call_plan, 0, sizeof call_plan); call_begin = mc_pos(mc); - native->plan_call(native, &call_desc, &call_plan); + native->marshal_call(native, &call_desc, &call_plan); call_end = mc_pos(mc); /* A GLOBAL NativeLoc is the scalar address value `&symbol + addend`, never @@ -404,7 +404,7 @@ static void check_typed_frame_address(KitUnit *unit, const BackendCase *tc) { call_desc.nargs = 1u; memset(&call_plan, 0, sizeof call_plan); global_begin = mc_pos(mc); - native->plan_call(native, &call_desc, &call_plan); + native->marshal_call(native, &call_desc, &call_plan); global_end = mc_pos(mc); /* ARM32 splits an i64 direct argument into low/high 32-bit ABI lanes. A @@ -422,7 +422,7 @@ static void check_typed_frame_address(KitUnit *unit, const BackendCase *tc) { call_desc.nargs = 1u; memset(&call_plan, 0, sizeof call_plan); imm_begin = mc_pos(mc); - native->plan_call(native, &call_desc, &call_plan); + native->marshal_call(native, &call_desc, &call_plan); imm_end = mc_pos(mc); } @@ -448,7 +448,7 @@ static void check_typed_frame_address(KitUnit *unit, const BackendCase *tc) { call_desc.nargs = 1u; memset(&call_plan, 0, sizeof call_plan); exact_call_begin = mc_pos(mc); - native->plan_call(native, &call_desc, &call_plan); + native->marshal_call(native, &call_desc, &call_plan); native->emit_call(native, &call_plan); exact_call_end = mc_pos(mc); } @@ -479,7 +479,7 @@ static void check_typed_frame_address(KitUnit *unit, const BackendCase *tc) { call_desc.nargs = 1u; memset(&call_plan, 0, sizeof call_plan); forced_vararg_begin = mc_pos(mc); - native->plan_call(native, &call_desc, &call_plan); + native->marshal_call(native, &call_desc, &call_plan); native->emit_call(native, &call_plan); forced_vararg_end = mc_pos(mc); synthetic->vararg_on_stack = saved_vararg_on_stack; diff --git a/test/opt/location_mir_test.c b/test/opt/location_mir_test.c @@ -157,7 +157,6 @@ static void machinize_rejects_cross_owner_registers(KitUnit* u) { cls.nemit_temps = 1u; cls.phys = phys; cls.nphys = 2u; - cls.caller_saved_mask = (1u << emit_temp) | (1u << asm_temp); memset(&regs, 0, sizeof regs); regs.classes = &cls; regs.nclasses = 1u; @@ -173,13 +172,16 @@ static void machinize_rejects_cross_owner_registers(KitUnit* u) { "machinize accepted a register shared by allocation and the " "emitter-temp owner"); - /* The asm bank has a separate lifetime and must be explicitly reserved. - * Exercise that ownership rule independently after making the emit bank - * valid, without trying to mirror every table-shape validation branch. */ - phys[0].flags = NATIVE_REG_CALLER_SAVED | NATIVE_REG_RESERVED; + phys[0].flags = NATIVE_REG_CALLER_SAVED; cls.asm_temps = &asm_temp; cls.nasm_temps = 1u; - cls.reserved_mask = 1u << emit_temp; + opt_machinize_native(f, &target); + CU_EXPECT(u, + (f->opt_reserved_regs[NATIVE_REG_INT] & (1u << emit_temp)) != 0, + "machinize did not reserve the policy-owned emitter temp"); + + /* The asm bank has a separate lifetime and must be statically reserved. */ + phys[1].flags = NATIVE_REG_CALLER_SAVED; CU_EXPECT(u, expect_compiler_panic(u, c, run_machinize, &call, "asm temp bank is not reserved"), diff --git a/test/opt/native_emit_frame_dst_test.c b/test/opt/native_emit_frame_dst_test.c @@ -14,12 +14,13 @@ typedef struct MockNative { NativeTarget base; KitUnit *unit; NativeAllocClassInfo classes[2]; + NativePhysRegInfo int_phys[1]; NativeRegInfo regs; Reg int_temps[4]; - NativeCallPlanMove call_arg; - NativeCallPlanRet call_ret; - NativeCallPlanMove phase_call_args[2]; - NativeCallPlanRet ret_parts[2]; + NativeCallPhaseMove call_arg; + NativeCallPhaseRet call_ret; + NativeCallPhaseMove phase_call_args[2]; + NativeCallPhaseRet ret_parts[4]; u32 callback_mask; u32 writeback_slots; u32 load_imms; @@ -42,7 +43,7 @@ typedef struct MockNative { Reg machine_clobber_reg; u8 report_bitfield_store_clobber; u8 plan_two_arg_indirect_call; - u8 plan_two_part_ret; + u8 plan_four_part_ret; u8 reject_indexed_addr; MCLabel next_label; } MockNative; @@ -231,7 +232,7 @@ static void mock_bitfield_store(NativeTarget *target, NativeAddr addr, static int mock_machine_op_clobbers( NativeTarget *target, const NativeMachineOp *op, - u32 mask[NATIVE_CALL_PLAN_CLASSES]) { + u32 mask[NATIVE_REG_CLASS_COUNT]) { MockNative *mock = mock_of(target); mask[0] = mask[1] = mask[2] = 0u; if (!mock->report_bitfield_store_clobber || @@ -380,8 +381,8 @@ static NativeLoc mock_reg_loc(KitCgTypeId type, NativeAllocClass cls, Reg reg) { /* Deliberately source the post-call copy from the same frame value used by * the argument. The call clobbers the sole staging register, so the emitter * must end the pre-call temp/cache phase and reload this source afterwards. */ -static void mock_plan_call(NativeTarget *target, const NativeCallDesc *desc, - NativeCallPlan *plan) { +static void mock_marshal_call(NativeTarget *target, const NativeCallDesc *desc, + NativeCallPhase *plan) { MockNative *mock = mock_of(target); if (mock->plan_two_arg_indirect_call) { NativeLoc staged_callee; @@ -444,10 +445,9 @@ static void mock_plan_call(NativeTarget *target, const NativeCallDesc *desc, mock->call_ret.mem.align = 8u; plan->rets = &mock->call_ret; plan->nrets = 1u; - plan->clobber_mask[NATIVE_REG_INT] = 1u << 8u; } -static void mock_emit_call(NativeTarget *target, const NativeCallPlan *plan) { +static void mock_emit_call(NativeTarget *target, const NativeCallPhase *plan) { MockNative *mock = mock_of(target); if (mock->plan_two_arg_indirect_call) { CU_EXPECT(mock->unit, @@ -474,24 +474,24 @@ static void mock_emit_call(NativeTarget *target, const NativeCallPlan *plan) { ++mock->calls; } -static void mock_plan_ret(NativeTarget *target, const CGFuncDesc *desc, +static void mock_marshal_ret(NativeTarget *target, const CGFuncDesc *desc, const NativeLoc *value, - NativeCallPlanRet **out_rets, u32 *out_nrets) { + NativeCallPhaseRet **out_rets, u32 *out_nrets) { MockNative *mock = mock_of(target); (void)desc; - if (!mock->plan_two_part_ret) { + if (!mock->plan_four_part_ret) { *out_rets = NULL; *out_nrets = 0u; return; } CU_EXPECT(mock->unit, value && value->kind == NATIVE_LOC_FRAME, - "two-part return test requires a frame-resident source"); + "four-part return test requires a frame-resident source"); if (!value || value->kind != NATIVE_LOC_FRAME) { *out_rets = NULL; *out_nrets = 0u; return; } - for (u32 i = 0; i < 2u; ++i) { + for (u32 i = 0; i < 4u; ++i) { memset(&mock->ret_parts[i], 0, sizeof mock->ret_parts[i]); mock->ret_parts[i].src = native_loc_stack(value->type, value->v.frame, (i32)(i * 8u)); @@ -502,7 +502,7 @@ static void mock_plan_ret(NativeTarget *target, const CGFuncDesc *desc, mock->ret_parts[i].mem.align = 8u; } *out_rets = mock->ret_parts; - *out_nrets = 2u; + *out_nrets = 4u; } static void mock_ret(NativeTarget *target) { (void)target; } @@ -548,10 +548,10 @@ static void mock_init(MockNative *mock, KitUnit *unit, Compiler *c) { mock->base.atomic_rmw = mock_atomic_rmw; mock->base.atomic_cas = mock_atomic_cas; mock->base.intrinsic = mock_intrinsic; - mock->base.plan_call = mock_plan_call; + mock->base.marshal_call = mock_marshal_call; mock->base.emit_call = mock_emit_call; mock->base.machine_op_clobbers = mock_machine_op_clobbers; - mock->base.plan_ret = mock_plan_ret; + mock->base.marshal_ret = mock_marshal_ret; mock->base.ret = mock_ret; } @@ -1395,7 +1395,7 @@ static void allocator_cache_waits_for_hard_liveness(KitUnit *unit) { FrameSlot clobbered_slot, preserved_slot, record_slot; Inst *in, *clobber; NativeMachineOp mop; - u32 clobber_mask[NATIVE_CALL_PLAN_CLASSES]; + u32 clobber_mask[NATIVE_REG_CLASS_COUNT]; u32 block; CU_EXPECT(unit, @@ -1482,7 +1482,11 @@ static void allocator_cache_waits_for_hard_liveness(KitUnit *unit) { /* The O0 direct-target value bank is deliberately empty. In particular, it * must not make the machinized allocation registers cache-eligible before * the lazy hard-liveness table has actually been built. */ - mock.classes[NATIVE_REG_INT].caller_saved_mask = 1u << 10u; + mock.int_phys[0].reg = 10u; + mock.int_phys[0].cls = NATIVE_REG_INT; + mock.int_phys[0].flags = NATIVE_REG_CALLER_SAVED; + mock.classes[NATIVE_REG_INT].phys = mock.int_phys; + mock.classes[NATIVE_REG_INT].nphys = 1u; memset(&mop, 0, sizeof mop); mop.kind = NATIVE_MOP_BITFIELD_STORE; @@ -1526,7 +1530,7 @@ static void reserved_cache_obeys_machine_clobbers(KitUnit *unit) { FrameSlot clobbered_slot, preserved_slot, record_slot; Inst *in, *clobber; NativeMachineOp mop; - u32 clobber_mask[NATIVE_CALL_PLAN_CLASSES]; + u32 clobber_mask[NATIVE_REG_CLASS_COUNT]; u32 block; CU_EXPECT(unit, @@ -1739,7 +1743,7 @@ static void return_parts_keep_prior_abi_destinations_live(KitUnit *unit) { f->opt_reserved_regs[RC_INT] |= 1u << (8u + i); } f->emit_temp_reg_count[RC_INT] = 4u; - value_slot = add_slot(f, i64_type, 16u, 8u); + value_slot = add_slot(f, i64_type, 32u, 8u); ret = emit_with_ops(f, block, IR_RET, 0u); aux = arena_znew(f->arena, IRRetAux); @@ -1749,7 +1753,7 @@ static void return_parts_keep_prior_abi_destinations_live(KitUnit *unit) { ret->extra.aux = aux; mock_init(&mock, unit, c); - mock.plan_two_part_ret = 1u; + mock.plan_four_part_ret = 1u; mock.int_temps[2] = 10u; mock.int_temps[3] = 11u; mock.classes[NATIVE_REG_INT].nscratch = 4u; @@ -1758,16 +1762,15 @@ static void return_parts_keep_prior_abi_destinations_live(KitUnit *unit) { (1u << 8u) | (1u << 9u) | (1u << 10u) | (1u << 11u); opt_emit_native(c, f, &mock.base); - CU_EXPECT(unit, mock.nframe_load_dsts == 2u, - "two return parts should each load their frame source " + CU_EXPECT(unit, mock.nframe_load_dsts == 4u, + "four return parts should each load their frame source " "(loads=%u)", mock.nframe_load_dsts); - if (mock.nframe_load_dsts == 2u) { - CU_EXPECT(unit, mock.frame_load_dsts[0] == 8u, - "first return part should load directly into r8"); - CU_EXPECT(unit, mock.frame_load_dsts[1] == 9u, - "second return part should load directly into r9"); - } + if (mock.nframe_load_dsts == 4u) + for (u32 i = 0; i < 4u; ++i) + CU_EXPECT(unit, mock.frame_load_dsts[i] == (Reg)(8u + i), + "return part %u should load directly into r%u", i, + (unsigned)(8u + i)); CU_EXPECT(unit, mock.moves == 0u, "frame return parts should not route through emitter temps"); diff --git a/test/opt/reg_effects_test.c b/test/opt/reg_effects_test.c @@ -5,6 +5,7 @@ #include <kit/frontend.h> #include "arch/aa64/aa64.h" +#include "arch/arm32/arm32.h" #include "arch/riscv/rv64.h" #include "arch/x64/x64.h" #include "core/pool.h" @@ -63,7 +64,7 @@ typedef struct MachinizeRunCtx { static int invalid_machine_effect( NativeTarget* target, const NativeMachineOp* op, - u32 mask[NATIVE_CALL_PLAN_CLASSES]) { + u32 mask[NATIVE_REG_CLASS_COUNT]) { (void)target; (void)op; mask[0] = mask[1] = mask[2] = 0; @@ -912,6 +913,165 @@ static void aa64_tls_model_fixed_effects(KitUnit* u) { } } +static void x64_machine_effect_families(KitUnit* u) { + static const struct { + NativeMachineOp op; + u32 expected; + const char* name; + } cases[] = { + {{.kind = NATIVE_MOP_BINOP, .binop = BO_SDIV}, + (1u << 0u) | (1u << 2u), "signed division"}, + {{.kind = NATIVE_MOP_BINOP, .binop = BO_SHL, .second_is_reg = 1u}, + 1u << 1u, "variable shift"}, + {{.kind = NATIVE_MOP_BINOP, .binop = BO_SHL}, 0u, "immediate shift"}, + {{.kind = NATIVE_MOP_BITFIELD_LOAD}, 0u, "bitfield load"}, + {{.kind = NATIVE_MOP_BITFIELD_STORE}, + (1u << 0u) | (1u << 1u) | (1u << 2u), "bitfield store"}, + {{.kind = NATIVE_MOP_ATOMIC_CAS}, + (1u << 0u) | (1u << 1u) | (1u << 2u), "atomic compare-exchange"}, + {{.kind = NATIVE_MOP_ATOMIC_RMW}, + (1u << 0u) | (1u << 1u) | (1u << 2u), "atomic read-modify-write"}, + {{.kind = NATIVE_MOP_VA_START}, 0u, "va_start"}, + {{.kind = NATIVE_MOP_VA_ARG}, 0u, "va_arg"}, + {{.kind = NATIVE_MOP_INTRINSIC, .intrin = INTRIN_UMUL_OVERFLOW}, + (1u << 0u) | (1u << 2u), "unsigned multiply overflow"}, + {{.kind = NATIVE_MOP_INTRINSIC, .intrin = INTRIN_READCYCLECOUNTER}, + (1u << 0u) | (1u << 2u), "cycle counter"}, + {{.kind = NATIVE_MOP_INTRINSIC, .intrin = INTRIN_SYSCALL}, + (1u << 0u) | (1u << 1u) | (1u << 2u) | (1u << 6u) | (1u << 7u) | + (1u << 8u) | (1u << 9u), + "syscall"}, + {{.kind = NATIVE_MOP_TLS_ADDR}, 0u, "ELF local-exec TLS"}, + }; + KitCompiler* kc = NULL; + NativeTarget* target; + + CU_EXPECT(u, + kit_unit_compiler_new( + u, kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed for x64 machine-effect family test"); + if (!kc) return; + target = x64_native_target_new((Compiler*)kc, NULL, NULL); + for (u32 i = 0; i < sizeof cases / sizeof cases[0]; ++i) { + NativeRegMaskSet mask = {0}; + int any = target->machine_op_clobbers(target, &cases[i].op, mask); + CU_EXPECT(u, + mask[NATIVE_REG_INT] == cases[i].expected && + any == (cases[i].expected != 0u), + "x64 %s fixed effects mismatch: got %#x, expected %#x", + cases[i].name, mask[NATIVE_REG_INT], cases[i].expected); + } + kit_compiler_free(kc); +} + +typedef NativeTarget* (*RoleTargetNew)(Compiler*, ObjBuilder*, MCEmitter*); + +typedef struct RoleMaskCase { + KitArchKind arch; + KitOSKind os; + KitObjFmt obj; + RoleTargetNew target_new; + const char* name; + NativeRegMaskSet caller; + NativeRegMaskSet callee; + NativeRegMaskSet arg; + NativeRegMaskSet ret; + NativeRegMaskSet reserved; +} RoleMaskCase; + +static void register_role_flags_are_authoritative(KitUnit* u) { + static const RoleMaskCase cases[] = { + {.arch = KIT_ARCH_X86_64, + .os = KIT_OS_LINUX, + .obj = KIT_OBJ_ELF, + .target_new = x64_native_target_new, + .name = "x64 SysV", + .caller = {0x00000fc7u, 0x0000ffffu, 0u}, + .callee = {0x0000f008u, 0u, 0u}, + .arg = {0x000003c6u, 0x000000ffu, 0u}, + .ret = {0x00000005u, 0x00000003u, 0u}, + .reserved = {0x00001f39u, 0x0000f030u, 0u}}, + {.arch = KIT_ARCH_X86_64, + .os = KIT_OS_WINDOWS, + .obj = KIT_OBJ_COFF, + .target_new = x64_native_target_new, + .name = "x64 Win64", + .caller = {0x00000f07u, 0x0000003fu, 0u}, + .callee = {0x0000f0c8u, 0x0000ffc0u, 0u}, + /* Argument/return flags describe the shared static x64 register table; + * caller/callee preservation is the only live-ABI override. */ + .arg = {0x000003c6u, 0x000000ffu, 0u}, + .ret = {0x00000005u, 0x00000003u, 0u}, + .reserved = {0x00001f39u, 0x0000f030u, 0u}}, + {.arch = KIT_ARCH_ARM_64, + .os = KIT_OS_LINUX, + .obj = KIT_OBJ_ELF, + .target_new = aa64_native_target_new, + .name = "aa64", + .caller = {0x0007ffffu, 0xffff00ffu, 0u}, + .callee = {0x1ff80000u, 0x0000ff00u, 0u}, + .arg = {0x000000ffu, 0x000000ffu, 0u}, + .ret = {0x00000003u, 0x0000000fu, 0u}, + .reserved = {0xe0070e00u, 0x00300000u, 0u}}, + {.arch = KIT_ARCH_RV64, + .os = KIT_OS_LINUX, + .obj = KIT_OBJ_ELF, + .target_new = rv64_native_target_new, + .name = "rv64", + .caller = {0xf003fce0u, 0xf003fcffu, 0u}, + .callee = {0x0ffc0300u, 0x0ffc0300u, 0u}, + .arg = {0x0003fc00u, 0x0003fc00u, 0u}, + .ret = {0x00000c00u, 0x00000c00u, 0u}, + .reserved = {0xf00001ffu, 0x0000000fu, 0u}}, + {.arch = KIT_ARCH_ARM_32, + .os = KIT_OS_FREESTANDING, + .obj = KIT_OBJ_ELF, + .target_new = arm32_native_target_new, + .name = "arm32", + .caller = {0x0000100fu, 0u, 0u}, + .callee = {0x00000ff0u, 0u, 0u}, + .arg = {0x0000000fu, 0u, 0u}, + .ret = {0x00000003u, 0u, 0u}, + .reserved = {0x0000f080u, 0u, 0u}}, + }; + + for (u32 i = 0; i < sizeof cases / sizeof cases[0]; ++i) { + const RoleMaskCase* tc = &cases[i]; + KitCompiler* kc = NULL; + NativeTarget* target; + CU_EXPECT(u, + kit_unit_compiler_new( + u, kit_unit_target(tc->arch, tc->os, tc->obj), &kc) == + KIT_OK && + kc != NULL, + "compiler allocation failed for %s register roles", tc->name); + if (!kc) continue; + target = tc->target_new((Compiler*)kc, NULL, NULL); + CU_EXPECT(u, target != NULL, "%s target allocation failed", tc->name); + if (target) { + native_reg_info_validate((Compiler*)kc, target->regs); + for (u32 cls = 0; cls < NATIVE_REG_CLASS_COUNT; ++cls) { + CU_EXPECT(u, + native_target_caller_saved_mask( + target, (NativeAllocClass)cls) == tc->caller[cls] && + native_target_callee_saved_mask( + target, (NativeAllocClass)cls) == tc->callee[cls] && + native_target_arg_mask(target, (NativeAllocClass)cls) == + tc->arg[cls] && + native_target_ret_mask(target, (NativeAllocClass)cls) == + tc->ret[cls] && + native_target_reserved_mask( + target, (NativeAllocClass)cls) == tc->reserved[cls], + "%s class %u derived register roles mismatch", tc->name, + cls); + } + } + kit_compiler_free(kc); + } +} + int main(void) { KitUnit u; kit_unit_init(&u); @@ -926,6 +1086,8 @@ int main(void) { aa64_syscall_fixed_effects(&u); x64_tls_model_fixed_effects(&u); aa64_tls_model_fixed_effects(&u); + x64_machine_effect_families(&u); + register_role_flags_are_authoritative(&u); x64_dying_fixed_sources_are_staged(&u); fprintf(stderr, "reg-effects: %d checks, %d failures\n", u.checks, u.fails); return kit_unit_status(&u); diff --git a/test/parse/run.sh b/test/parse/run.sh @@ -244,7 +244,11 @@ if [ $is_native_target -eq 1 ]; then fi # Cached start.o — build once for the harness run rather than per case. -START_OBJ="$BUILD_DIR/parse_start.o" +# This harness is routinely run for several architectures at once (for +# example, independent cross-test workers). Keep the cached startup object +# architecture-owned: sharing parse_start.o lets one worker replace another's +# object between the probe above and the per-case link below. +START_OBJ="$BUILD_DIR/parse_start.$TEST_ARCH.o" have_start_obj=0 if [ $have_clang_cross -eq 1 ]; then if clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ @@ -363,6 +367,17 @@ kit_lane_R() { } kit_lane_E() { + # The ldbl128_* execution fixtures assert IEEE binary128 semantics. x64 + # targets use either x87 extended precision (SysV/macOS) or double-aliased + # long double (Win64), so their deliberate return-0 guard cannot match the + # nonzero binary128 oracle. Keep the layout/macro probe, whose expected + # result is already zero, and classify the semantic fixtures as inapplicable. + if [ "$TEST_ARCH" = "x64" ] && \ + [[ "$KIT_BASE" == ldbl128_* ]] && \ + [[ "$KIT_BASE" != ldbl128_01_* ]]; then + kit_skip "$KIT_NAME/E" "x64 long double is not IEEE binary128" + return + fi # rv32: freestanding bare-metal. parse-runner --emit -> kit ld with a startup # that calls main() and reports its return via a SiFive finisher -> run under # qemu-system-riscv32 (test/lib/exec_bare.sh). The qemu exit equals