kit

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

commit 8d7927db63f2281b5292479137dfb30a9345e263
parent d5e7e1a3f6222191af65cfae4c7b015631d8ce73
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 18 Jul 2026 23:54:21 -0700

refactor(opt): make scratch and clobber ownership explicit

Separate semantic HIR from location MIR, centralize register effects, and give native emission and inline asm distinct scoped temp banks across backends.

Add red-green unit, optimizer, corpus, cross-architecture, bootstrap, and backend-pressure coverage for the new boundaries.

Diffstat:
Mdoc/ARCH.md | 132+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mdoc/BUILD.md | 35++++++++++++++++++++---------------
Mdoc/CODEGEN.md | 23++++++++++++++---------
Mdoc/INTERFACES.md | 60++++++++++++++++++++++++++++++++++++++++--------------------
Mdoc/JIT.md | 9+++++----
Mdoc/OPT.md | 230+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mdoc/plan/ARM32.md | 52++++++++++++++++++++++++++++++----------------------
Mdoc/plan/OPTIMIZER.md | 91++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mdoc/plan/TODO.md | 8--------
Mdriver/env/build_host_posix.c | 5++++-
Mlang/c/parse/cg.c | 25+++++++++++++++++++------
Mmk/bootstrap.mk | 6+++++-
Mmk/test.mk | 31+++++++++++++++++++++++++++++--
Mmk/test_unit.mk | 10+++++++++-
Mscripts/cross_test.sh | 10+++++-----
Msrc/arch/aa64/native.c | 545++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/arch/arm32/native.c | 409+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/arch/mc.c | 5+++++
Msrc/arch/native_target.h | 263++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/arch/riscv/native.c | 538+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/arch/x64/native.c | 819++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/build/trace.c | 12+++++++-----
Msrc/cg/call.c | 7++++---
Msrc/cg/cgir.h | 2+-
Msrc/cg/native_asm.c | 138+++++++++++++++++++++++++------------------------------------------------------
Msrc/cg/native_asm.h | 50++++++++++++++------------------------------------
Msrc/cg/native_direct_target.c | 24++++++++++++------------
Msrc/cg/native_direct_target.h | 2+-
Msrc/link/link_relocatable.c | 1+
Msrc/obj/macho/emit.c | 2++
Msrc/obj/macho/macho.h | 1+
Msrc/obj/macho/read.c | 5++++-
Msrc/obj/obj.c | 10++++++++++
Msrc/obj/obj.h | 5+++++
Msrc/opt/cg_ir_lower.c | 17++++++++++++-----
Msrc/opt/ir.h | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/opt/ir_print.c | 32+++++++++++++++++++++++++++++++-
Asrc/opt/mir_clone.c | 338+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/opt/opt.c | 17++++++++++++++++-
Msrc/opt/opt.h | 4++++
Msrc/opt/opt_internal.h | 40++++++++++++++++++++++++++++++++++++++--
Msrc/opt/opt_util.c | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/opt/pass_addr_fold.c | 6++++--
Msrc/opt/pass_coalesce.c | 86++++---------------------------------------------------------------------------
Msrc/opt/pass_combine.c | 905+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/opt/pass_dce.c | 21+++++++++++++--------
Msrc/opt/pass_hard_live.c | 184++++---------------------------------------------------------------------------
Msrc/opt/pass_lower.c | 629+++++++++++++++++++++++--------------------------------------------------------
Msrc/opt/pass_machinize.c | 325+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/opt/pass_mir.c | 549+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/opt/pass_native_emit.c | 1599++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/opt/pass_o2.c | 21+++++++++++++++++++--
Msrc/opt/pass_ssa.c | 25+++++++------------------
Asrc/opt/reg_effects.c | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/arch/arm32_inline_test.c | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/arch/x64_inline_test.c | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtest/cg/native_direct_target_test.c | 48+++++++++++++++++++++++++++---------------------
Mtest/lib/exec_bare.sh | 2+-
Mtest/macho/kit-roundtrip-macho.c | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Atest/opt/aa64_apple_varargs.sh | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/opt/arm32_emit_temp_pressure.c | 15+++++++++++++++
Atest/opt/arm32_emit_temp_pressure.sh | 21+++++++++++++++++++++
Atest/opt/combine_cse_test.c | 517+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/opt/frame_value_backend_test.c | 766+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/opt/location_mir_test.c | 1162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/opt/macho_block_alt_entry.sh | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/opt/native_emit_frame_dst_test.c | 1751+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/opt/native_part_chunk_test.c | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/opt/o1_stack_dse.sh | 10++++++----
Mtest/opt/o1p_combine.sh | 18+++++++++---------
Atest/opt/reg_effects_test.c | 906+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/parse/CORPUS.md | 51++++++++++++++++++++++++++++++++++++---------------
Mtest/parse/cases/asm_03_register_operand.c | 11++++++-----
Atest/parse/cases/asm_06_temp_boundary.c | 19+++++++++++++++++++
Atest/parse/cases/asm_06_temp_boundary.expected | 1+
Atest/parse/cases/asm_06_temp_boundary.wasm.skip | 1+
Atest/parse/cases/cg_inline_asm_clobber_return.c | 20++++++++++++++++++++
Atest/parse/cases/cg_inline_asm_clobber_return.expected | 1+
Atest/parse/cases/cg_inline_asm_clobber_return.wasm.skip | 1+
Atest/parse/cases/cg_machine_clobber_return.c | 17+++++++++++++++++
Atest/parse/cases/cg_machine_clobber_return.expected | 1+
Atest/parse/cases/opt_04_global_incomplete_array_decay.c | 13+++++++++++++
Atest/parse/cases/opt_04_global_incomplete_array_decay.expected | 1+
Atest/parse/cases/opt_05_frame_addr_call_arg.c | 12++++++++++++
Atest/parse/cases/opt_05_frame_addr_call_arg.expected | 1+
Atest/parse/cases/opt_06_flexible_array_element_address.c | 37+++++++++++++++++++++++++++++++++++++
Atest/parse/cases/opt_06_flexible_array_element_address.expected | 1+
Atest/parse/cases/opt_07_pointer_difference_index.c | 14++++++++++++++
Atest/parse/cases/opt_07_pointer_difference_index.expected | 1+
Atest/parse/cases/opt_08_copy_preserves_compare_type.c | 14++++++++++++++
Atest/parse/cases/opt_08_copy_preserves_compare_type.expected | 1+
Atest/parse/cases/opt_09_copy_preserves_call_arg_type.c | 17+++++++++++++++++
Atest/parse/cases/opt_09_copy_preserves_call_arg_type.expected | 1+
Atest/parse/cases/opt_10_packed_local_member.c | 16++++++++++++++++
Atest/parse/cases/opt_11_large_local_member_loop.c | 17+++++++++++++++++
Atest/parse/cases/opt_12_small_aggregate_arg_spill.c | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/parse/cases/opt_13_pointer_first_record_copy.c | 23+++++++++++++++++++++++
Atest/toy/cases/155_musttail_indirect_packed_stack.toy | 25+++++++++++++++++++++++++
Atest/toy/cases/155_musttail_indirect_packed_stack.wasm.skip | 1+
Atest/toy/cases/156_indirect_stack_args.toy | 25+++++++++++++++++++++++++
100 files changed, 11982 insertions(+), 3002 deletions(-)

diff --git a/doc/ARCH.md b/doc/ARCH.md @@ -2,14 +2,14 @@ This document describes kit's arch backend abstraction: how a target architecture plugs into the compiler, what its responsibilities are, and how -the three native backends (aa64, x64, rv64) are structured to maximize sharing -while keeping the ISA-specific seams thin. It also covers the ABI / calling -convention layer in `src/abi`, which is the single authority for storage layout -and call classification. The semantic codegen surface a backend sits behind is -in [CODEGEN.md](CODEGEN.md); the IR the optimizer feeds it is in [IR.md](IR.md); -the no-SSA O1 and future SSA O2 optimizer paths are in [OPT.md](OPT.md); -the standalone assembler that shares the ISA tables is in [ASM.md](ASM.md). ABI -content is canonical here. +the four native backends (aa64, x64, rv64, arm32) are structured to maximize +sharing while keeping the ISA-specific seams thin. It also covers the ABI / +calling convention layer in `src/abi`, which is the single authority for +storage layout and call classification. The semantic codegen surface a backend +sits behind is in [CODEGEN.md](CODEGEN.md); the IR the optimizer feeds it is in +[IR.md](IR.md); the no-SSA O1 and future SSA O2 optimizer paths are in +[OPT.md](OPT.md); the standalone assembler that shares the ISA tables is in +[ASM.md](ASM.md). ABI content is canonical here. ## 1. Two layers of "backend": CGBackend and ArchImpl @@ -53,9 +53,9 @@ struct-prefix subtyping (`src/cg/cgtarget.h`, `src/arch/arch.h`). debugger op tables, the register file, the predefined macros (`__aarch64__`, `__x86_64__`, ...), and the DWARF CFI defaults that seed the `.eh_frame` CIE. -`aa64`, `x64`, and `rv64` expose full `ArchImpl`s (`arch_impl_aa64`, -`arch_impl_x64`, `arch_impl_rv64`). `wasm` also exposes an `ArchImpl` -(`arch_impl_wasm`), but it is a *thin* one: its machine-code seams +`aa64`, `x64`, `rv64`, and `arm32` expose full `ArchImpl`s (`arch_impl_aa64`, +`arch_impl_x64`, `arch_impl_rv64`, `arch_impl_arm32`). `wasm` also exposes an +`ArchImpl` (`arch_impl_wasm`), but it is a *thin* one: its machine-code seams (`asm_new`, `apply_label_fixup`, `link`, register accessors, CFI) are all NULL, since wasm32 has no native machine encoding, no stack-frame ABI, and no assembly form in this toolchain — it produces a `WasmModule` attached to the @@ -105,12 +105,14 @@ answer rather than a crash. ## 3. The NativeTarget contract `src/arch/native_target.h` defines `NativeTarget`, the physical machine-emission -contract that all three native backends implement. It is the layer where the +contract that all four native backends implement. It is the layer where the generic codegen drivers stop speaking in semantic terms (`CGLocal` ids, high-level types) and start speaking in physical terms: hard registers, frame slots, legal immediates, and concrete addressing modes. A `NativeTarget` never -allocates registers and never decides storage layout — callers hand it -caller-selected, target-legal physical operands; the target only encodes. +assigns persistent homes to program values and never decides source storage +layout. Callers supply target-legal physical locations. A backend may use its +fixed, backend-private implementation registers while realizing a hook, but +those temporaries are not MIR values and cannot escape the hook. It is driven from two directions: @@ -136,10 +138,21 @@ It is driven from two directions: - At **-O1**, the optimizer records IR, builds a CFG, runs the no-SSA local schedule, machinizes, and allocates registers with the linear-scan path (see [OPT.md](OPT.md)); `src/opt/pass_native_emit.c` then replays the allocated - program against the *same* `NativeTarget` hooks. By this point every value - already has a physical home, so the emit pass hands the target hard registers - and frame slots and the target just encodes. `-O2` currently selects this same - path; the maintained SSA schedule is not publicly reachable yet. + program against the *same* `NativeTarget` hooks. By this point every program + value has one canonical MIR home (hard register, frame slot, or a + rematerialization recipe). The emit pass leases target-declared + instruction-local operand temps as needed, with a liveness-proven dead + caller-saved allocation register as a pressure fallback, and hands concrete + `NativeLoc`s to the backend. `-O2` currently selects this same path; the + maintained SSA schedule is not publicly reachable yet. + + 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 + pointer width. Materializing that component is a `NativeTarget` + responsibility and uses the explicit destination or backend-private fixed + temps as needed; it does not create a second allocator or change the + component's MIR home. That a single ~35-hook contract serves both paths is what keeps the two code generators byte-compatible per arch. The hook families: @@ -158,11 +171,14 @@ 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 two-phase split: `plan_call` turns a `NativeCallDesc` - into a `NativeCallPlan` (arg moves, return slots, clobber/return masks, - outgoing stack size) that the optimizer can inspect during frame planning, and - `emit_call` realizes it. Symmetrically `plan_ret`/`ret`. `call_stack_bytes` - and `signature_stack_bytes` are pure pre-pass queries used to size the +- **Calls & returns.** A marshalling/emission split: `plan_call` is deliberately + mutating. It fills a `NativeCallPlan` 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 + 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 outgoing area and to decide tail-call (sibling) realizability. - **Atomics & fences.** `atomic_load`/`store`/`rmw`/`cas`, `fence`. - **Variadics.** `va_start_`/`va_arg_`/`va_end_`/`va_copy_`. All `va_list` @@ -177,14 +193,20 @@ A handful of small capability flags/queries let the generic drivers specialize without arch branches: `imm_legal`/`addr_legal` (immediate and addressing-mode legality), `has_store_zero_reg`/`store_zero_reg` (aa64 `xzr`, rv64 `x0` — store a constant 0 without materializing it), and the optional `machine_op_clobbers`, -which reports the fixed registers an encoding clobbers as a side effect (x86 -`idiv` writes rax/rdx, a variable shift uses cl, atomics use rax/rcx/rdx) so the -allocator keeps values out of them; aa64/rv64 leave it NULL because their -encodings have no such fixed clobbers. +which reports the fixed registers an encoding or target sequence destroys so +the allocator keeps live-across values out of them. x64 reports division, +variable-shift, atomic, bitfield-store, intrinsic, syscall, and descriptor-TLS +effects; aa64 reports syscall and descriptor-TLS effects; rv64 reports syscall +effects. A backend leaves the hook NULL only when none of its operations have +fixed clobbers. This hook is an exhaustive preservation contract: except for +explicit destination registers and reported effects, an ordinary +`NativeTarget` operation preserves every caller-owned register, including one +holding a clean frame-cache entry. Machinization rejects any reported bit that +is not a known physical register in that class. **aa64 is the reference backend.** `src/arch/aa64/native.c` is the most complete -and most heavily commented implementation; the x64 and rv64 ports are written -against it. Shared scaffolding extracted across all three lives in +and most heavily commented implementation; the x64, rv64, and arm32 ports are +written against it. Shared scaffolding extracted across all four lives in `src/cg/native_frame.c` (slot-offset arithmetic, the frame-final gate, the used-callee-save derivation, ABI-driven va-save sizing) and `src/cg/native_argmove.c` (the parallel-copy register shuffle for call-arg and @@ -290,31 +312,49 @@ exposes for objdump/asm live separately in `regs.c`). A `NativeRegInfo` is a set of `NativeAllocClassInfo` (one per `NativeAllocClass`: INT, FP, VEC), each carrying: -- an ordered **allocable** list — registers the allocator may assign, ordered by - preference (aa64 lists caller-saved first so the allocator prefers them and - avoids prologue saves); -- a **scratch** list — registers reserved for the backend's own temporaries - (address materialization, atomic retry loops) and never handed to the - allocator; +- an ordered O0 **ndt_allocable** list — the `NativeDirectTarget` value-cache + bank and preference order; it is never an O1 allocator input; +- an O0 **scratch** list — the `NativeDirectTarget` value-cache/acquisition + policy only; +- an O1 **emit_temps** list — non-allocable, operand-facing registers leased for + exactly one native-emission instruction; these are MIR materialization + 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; +- 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 + preservation is already established; - a `NativePhysRegInfo` row per physical register (class, ABI arg/ret index, - caller/callee-saved flags, spill/copy costs); + caller/callee-saved flags, the independent `NATIVE_REG_ALLOCABLE` O1 flag, + and spill/copy costs); - precomputed caller/callee/arg/ret/reserved bitmasks. -This one declaration feeds **both** code paths: +This one declaration describes **both** code paths without making either path's +register bank an input to the other: - the **-O0 direct path** resolves `reg_info` and the three `class_info[]` pointers once at `NativeDirectTarget` construction, so its register cache - (allocate / evict / scratch-acquire) is an O(1) lookup. It uses the allocable - order as a simple "next free register" pool with conservative flushes. -- the **-O1 allocator** (`src/opt`) consumes the same allocable lists, costs, and - masks as its interference-graph inputs, and reports the callee-saves it + (allocate / evict / scratch-acquire) is an O(1) lookup. It uses only the + `ndt_allocable` order and O0 scratch policy with conservative flushes. +- the **-O1 allocator** (`src/opt`) derives its bank from + `NativePhysRegInfo.flags & NATIVE_REG_ALLOCABLE`, consumes the physical costs + and masks as its interference-graph inputs, and reports the callee-saves it actually used back through `func_begin_known_frame` / `reserve_callee_saves` so the backend can reserve save slots and emit the matching prologue/epilogue. - -Because incoming arg registers are marked non-allocable, register-destination -param binds can never alias a live incoming arg, which is what lets `bind_param` -ordering be unconstrained and lets `bind_params_end` resolve a param permutation -as a single parallel copy. + The optimized emitter independently consumes the validated `emit_temps`, + `asm_temps`, and `emit_cache_mask` ownership declarations. It never falls back + to the O0 scratch or `ndt_allocable` policies, though it may lease a + caller-saved O1 allocation register proven dead at the instruction. + Cross-instruction cache retention is limited to `emit_cache_mask` or a dead + O1 allocation register whose caller/callee-save preservation is already + established, with calls and exhaustive machine effects providing the + invalidation boundaries. + +The ABI-argument role and persistent allocability are independent target +choices. Each backend captures incoming parts before body emission can overwrite +them; deferred register destinations are completed by `bind_params_end` as one +parallel copy, so a parameter permutation has an explicit ownership boundary. ## 7. The ABI / calling-convention layer diff --git a/doc/BUILD.md b/doc/BUILD.md @@ -324,28 +324,33 @@ following approach is the default starting point. target object — isolating a linker bug from codegen for a specific source file. - **Inspect MIR around the suspect symbol.** A filtered MIR dump taken after lowering and the combine pass is usually enough to see the divergence (e.g. a - call argument referencing a backend scratch register where it should reference - an allocable one). + call argument location aliasing an already-owned ABI destination, or a spill + whose frame slot/type changed across a transformation). - **Avoid `-g` while triaging `-O1` codegen.** Debug info changes object layout and can create or mask layout-sensitive bugs; triage on the non-`-g` object first. The bug classes that have historically broken the `-O1` fixed point all live in -the interaction between the optimizer's register-level reasoning and the -backend's scratch-register discipline, with the object/link layer as where the -symptom surfaces. Keep them in mind when bringing up a new arch or platform: - -- **Operand clobber in native emit** — materializing the left operand of a - binop/compare into a scratch register that already holds the right operand - (compute the RHS location first and exclude its register when materializing the - LHS). -- **Copy propagation across backend scratch registers** — scratch registers may - appear in lowered MIR but must not be extended across later instructions, since - native lowering reuses them as transient temporaries. +the interaction between physical-location MIR, canonical register effects, and +the emitter's instruction-local temporary leases, with the object/link layer as +where the symptom surfaces. Keep them in mind when bringing up a new arch or +platform: + +- **Operand clobber in native emit** — leasing a materialization register that + already holds another operand or phase-owned ABI destination. The instruction + scope derives exclusions from canonical uses/defs/clobbers and holds every + destination until its multi-move phase completes. +- **Emitter temporaries escaping into MIR** — location MIR contains only + physical allocation registers, stack values, frame-address recipes, and + semantic operands. `emit_temps`/`asm_temps` are scoped emitter-owned leases; + the MIR verifier rejects them rather than relying on later passes not to + extend their lifetime. - **Coalesce overlap checks** must use raw range points, not compressed points. - **Lower-pass hint fallback** must not place values live across a call into caller-saved hint registers. -- **Native scratch budget** — a backend needs enough integer scratch registers - for all-spilled three-operand operations (aa64 needs three). +- **Native emission-temp budget** — a backend needs enough operand-facing + `emit_temps` for all-spilled three-operand operations (aa64 needs three), + independently of its O0 `scratch`/`ndt_allocable` policy and backend-private + implementation registers. - **Aggregate copy/set with pointer operands** must not force-home the pointer local; genuinely frame-backed pointer locals need prematerialized indirect bases. diff --git a/doc/CODEGEN.md b/doc/CODEGEN.md @@ -270,9 +270,11 @@ load-bearing invariants are: - **What is cached:** only scalar locals that fit a register and are neither address-taken nor memory-required. Aggregates and escaped locals stay frame-only. -- **Where:** only caller-saved allocable registers. This sidesteps prologue - bookkeeping (the direct path reports no clobbered callee-saves) and means the - blanket flush before any call already covers ABI clobbering. +- **Where:** only the backend-declared `ndt_allocable` bank, which is independent + of the O1 allocator bank. Targets may restrict NDT to caller-saved registers; + x64 instead admits callee-saved entries (r13-r15 under SysV, plus rsi/rdi on + Win64) and reports every one actually used so the backend reserves and emits + the matching save/restore slots. - **Block scope:** with no CFG/liveness, a cached value cannot cross a control edge or join. The cache is spilled and emptied at every branch, label, and return; `func_begin` starts empty. @@ -283,9 +285,11 @@ load-bearing invariants are: at that register instead of reloading the frame home. Direct frame-home accesses to a cached local (e.g. by-value field extraction) flush just that one local. -- **Calls and barriers** still flush the whole cache (caller-saved regs die, - and a memory clobber may observe everything). Address-taking a cached local - flushes just that local and marks it uncacheable thereafter. +- **Calls and barriers** still flush the whole cache: caller-saved registers may + die and a memory clobber may observe every local. This is deliberately + conservative even when an entry occupies a preserved callee-saved register. + Address-taking a cached local flushes just that local and marks it + uncacheable thereafter. A monotonic use-tick drives approximate-LRU eviction; cached locals are tracked in an intrusive insertion-order list so flush-all is O(cached), not O(locals). @@ -337,9 +341,10 @@ tail-call / entry permutations it is free to rotate). When a true cycle remains member into a scratch register and redirecting that value's readers. The scheduling — topological emission, cycle detection, the scratch break — is -identical across the three arches; only the leaf operations differ (how one move -is emitted, which register is scratch), supplied through a small ops struct. All -three native backends plus the entry param-bind path share this one scheduler. +identical across native architectures; only the leaf operations differ (how one +move is emitted, which register is scratch), supplied through a small ops +struct. Every native backend plus the entry param-bind path shares this one +scheduler. ## Why this shape diff --git a/doc/INTERFACES.md b/doc/INTERFACES.md @@ -123,20 +123,20 @@ fixed header count, which becomes stale whenever a public component is added. The codegen path is **tiered**: each tier is a distinct contract struct (a vtable) that a backend or layer fills in. A frontend records into the highest -tier; the bytes come out of the lowest. The native backends — aarch64, x64, and -rv64 — are all live and all satisfy the same contracts. aarch64 is the reference -implementation; x64 and rv64 are full peers built on the shared -`NativeTarget` / `NativeOps` / `NativeFrame` substrate rather than bespoke -per-arch frame and lowering code. +tier; the bytes come out of the lowest. The native backends — aarch64, x64, +rv64, and arm32 — are all live and satisfy the same contracts. aarch64 is the +reference implementation; the other backends are full peers built on the +shared `NativeTarget` / `NativeOps` / `NativeFrame` substrate rather than +bespoke per-arch frame and lowering code. | Tier | Header | Contract type | What implements it | Role | |------|--------|---------------|--------------------|------| -| ABI | `src/abi/abi.h` (+ `abi_internal.h`) | `TargetABI` | per-ABI TUs (`aapcs64`, `aapcs64_windows`, `sysv_x64`, `rv64`, `wasm32`, `apple_arm64`, `apple_x64`, `win64_x64`) | calling-convention + storage-layout queries; `abi_new(Compiler)` selects the implementation by `(arch, obj-format)` | +| ABI | `src/abi/abi.h` (+ `abi_internal.h`) | `TargetABI` | per-ABI TUs (`aapcs32`, `aapcs64`, `aapcs64_windows`, `sysv_x64`, `rv64`, `wasm32`, `apple_arm64`, `apple_x64`, `win64_x64`) | calling-convention + storage-layout queries; `abi_new(Compiler)` selects the implementation by `(arch, obj-format)` | | Arch registry | `src/arch/arch.h` | `ArchImpl` | one singleton per arch, via `arch_lookup(kind)` | discovery + dispatch to backend/decode/emu/link/dbg/dwarf surfaces; CFI defaults | | Semantic CG | `src/cg/cgtarget.h` | `CgTarget` | `native_direct_target` (-O0) or `opt_cgtarget` (-O>=1), wrapping a per-arch `CgTarget` | frontend-facing typed lowering, pre-regalloc | | -O0 adapter | `src/cg/native_direct_target.h` | `NativeDirectTarget` + `NativeOps` | shared adapter, parameterized by each arch's `NativeOps` | adapts a `NativeTarget` to `CgTarget` for the direct -O0 path | -| Physical emit | `src/arch/native_target.h` | `NativeTarget` | aa64/x64/rv64 `*_native_target_new()` | hard-register, machine-code emission + frame/CFI | -| Frame model (shared) | `src/cg/native_frame.h` | `NativeFrame` | shared `native_frame.c`, embedded by all three native backends | arch-neutral frame-slot bookkeeping the `NativeTarget` impls delegate to | +| Physical emit | `src/arch/native_target.h` | `NativeTarget` | aa64/x64/rv64/arm32 `*_native_target_new()` | hard-register, machine-code emission + frame/CFI | +| Frame model (shared) | `src/cg/native_frame.h` | `NativeFrame` | shared `native_frame.c`, embedded by all four native backends | arch-neutral frame-slot bookkeeping the `NativeTarget` impls delegate to | | Machine code | `src/arch/mc.h` | `MCEmitter` | one generic impl, `mc_new(Compiler, ObjBuilder)` | section/label/reloc/CFI byte emission for all machine-code archs | **Per-arch entry points** — the surface each backend exposes to the rest of the @@ -147,7 +147,8 @@ its raw word/halfword emit helpers for the assembler path: |------|--------|--------------| | aa64 (reference) | `src/arch/aa64/aa64.h` | `aa64_native_target_new`, `aa64_native_direct_ops` | | x64 | `src/arch/x64/x64.h` | `x64_native_target_new`, `x64_native_direct_ops` | -| rv64 | `src/arch/rv64/rv64.h` | `rv64_native_target_new`, `rv64_native_direct_ops`, `rv64_emit32/16` | +| rv64 | `src/arch/riscv/rv64.h` | `rv64_native_target_new`, `rv64_native_direct_ops`, `rv64_emit32/16` | +| arm32 | `src/arch/arm32/arm32.h` | `arm32_native_target_new`, `arm32_native_direct_ops` | | c_target | `src/arch/c_target/{c_emit,ir_emit}.h` | C-source emission backend (standalone `CGBackend`, no `ArchImpl`) | | wasm | `src/arch/wasm/*` | wasm emission backend | @@ -156,19 +157,37 @@ its raw word/halfword emit helpers for the assembler path: 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) has already selected legal physical operands and run register - allocation; `NativeTarget` validates and emits but must never allocate - registers itself. All three native backends implement the full contract. + 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 + implement the full contract. - The same arch fills both `NativeTarget` (physical) and `NativeOps` (the semantic shims the -O0 adapter calls). Keep the split clean: semantic decisions - (operand legality, call planning policy, `va_*`, asm binding, barriers) live in - `NativeOps`; pure emission lives in `NativeTarget`. + 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; + `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` (x86 idiv/shift fixed-register - clobbers; NULL on aa64/rv64), `emit_prologue` / `emit_minimal_prologue`, - `bind_params_end` (for backends that resolve param binds as a parallel copy), + whose ISA needs them — `machine_op_clobbers` (including x64 division, shifts, + bitfield stores, atomics, and descriptor TLS, plus aa64 syscall and descriptor + TLS effects and rv64 syscall effects), `emit_prologue` / + `emit_minimal_prologue`, `bind_params_end` (for backends that resolve param + binds as a parallel copy), and the zero-register store fast path (`has_store_zero_reg`). NULL is the documented "this arch doesn't need it" answer, not an unimplemented gap. + The `machine_op_clobbers` contract is exhaustive: NULL asserts zero fixed + optimizer-visible clobbers, while an implementation must report every + exception, including an `emit_cache_mask` temporary when an operation uses it + internally. Backend-private registers are outside that contract. An ordinary + hook preserves every caller-owned or cache-eligible register that is neither + an explicit destination nor reported by this effect. That contract + permits clean frame values to remain in `emit_cache_mask` registers and + already-preserved dead O1 allocation registers between invalidation + boundaries. - `CgTarget` likewise carries capability queries so the semantic layer can stay arch-neutral: `supports_label_table` (false on Wasm, which has no code addresses in linear memory), `switch_` (overridden only by backends with a @@ -184,7 +203,7 @@ its raw word/halfword emit helpers for the assembler path: Every native backend lays out a stack frame the same way at the *bookkeeping* level, so that bookkeeping was lifted into one shared module. `NativeFrame` owns the arch-neutral parts; each `NativeTarget` embeds one and keeps the ISA/ABI -specifics. All three native backends embed a `NativeFrame`. +specifics. All four native backends embed a `NativeFrame`. **The split — what `NativeFrame` owns vs. what stays in the backend:** @@ -198,8 +217,9 @@ specifics. All three native backends embed a `NativeFrame`. **Why it is shared:** the slot arithmetic, the no-grow-after-prologue gate, and the derivation of the used-callee-save set from the optimizer's per-class masks -are identical across the three archs. Consolidating them also folds the three -per-arch vararg-save magic numbers (rv64 64, x64 176, aa64 64+128) into a single +are identical across the native archs. Consolidating them also folds the +per-arch vararg-save sizes (rv64 64, x64 176, aa64 64+128; zero where an ABI +does not require a register-save area) into a single ABI-driven `native_frame_va_save_bytes` query, in line with the no-magic-numbers rule. Frame-slot handles are **1-indexed** with `NATIVE_FRAME_SLOT_NONE` as the sentinel; callers and the shared allocator must agree on that. diff --git a/doc/JIT.md b/doc/JIT.md @@ -240,10 +240,11 @@ A subtlety the access lowering imposes on codegen: the Mach-O TLV sequence materializes the descriptor in `x0` and (in the AOT form) calls the resolver thunk via `x16`, clobbering `x0`/`x16`/`x17`/`lr`. Codegen is shared between AOT and JIT, so the optimizer must model that clobber set -or a value left live in `x0` across a TLS access is corrupted at `-O1`+. -The backend reports it via `NATIVE_MOP_TLS_ADDR` from -`machine_op_clobbers` (ELF Local-Exec, which uses only the destination -register, reports none). +at its ownership boundary or a value left live in `x0` across a TLS access is +corrupted at `-O1`+. The backend reports optimizer-visible `x0` via +`NATIVE_MOP_TLS_ADDR` from `machine_op_clobbers`; `x16`/`x17`/`lr` are reserved +backend-private registers and therefore remain outside that effect interface. +ELF Local-Exec, which uses only the destination register, reports none. ## Symbol and inspector surface diff --git a/doc/OPT.md b/doc/OPT.md @@ -29,7 +29,7 @@ frontend --CgTarget calls--> CgIrRecorder --records--> CgIrFunc tape | passes (analysis + transform) | - regalloc -> MFunc (physical MIR) + regalloc -> location MFunc (physical MIR) v opt_emit_native --NativeTarget calls--> machine code ``` @@ -103,7 +103,7 @@ The optimizer IR lives in `src/opt/ir.h` / `src/opt/ir.c`. Its shape: - `Func` owns one function: its CFG (`blocks`, `entry`, `emit_order`), frame slots, params, locals, the pseudo-register table, the SSA value table, scope - bookkeeping, allocation results, and per-pass scratch. + bookkeeping, allocation results, and pass-owned analysis state. - `Block` is a basic block: a growable `Inst[]`, explicit `preds`/`succ` edges, and a pre-allocated `MCLabel` for blocks born from `cg_label_new`. - `Inst` is one recorded operation. The `IROp` enum mirrors the `CgTarget` @@ -140,6 +140,15 @@ the nearest checkpoint rather than in the backend encoder. `FrameSlot` is the frame-storage currency: locals forced to memory, spill slots, ABI parameter slots, alloca regions, and outgoing-argument areas. +MIR extends the operand vocabulary with locations rather than hiding locations +behind temporary register ids. `OPK_STACK` is the scalar value in an `FS_SPILL` +slot, while `OPK_FRAME_ADDR` is a rematerializable address of a semantic frame +slot. An `OPK_INDIRECT` address records the location kind and exact type of its +base and index independently: either a physical register, a spilled scalar, or +(for the base) a frame-address recipe. These kinds never occur in semantic HIR. +They let post-allocation passes preserve the actual allocation result all the +way to emission without manufacturing register lifetimes between instructions. + ### Token aliasing: optimizer-local names onto NativeTarget types `src/opt/ir.h` deliberately reuses the physical backend's data types as the @@ -216,11 +225,12 @@ promote_scalar_locals non-escaped scalar frame slot -> PReg addr_of_global_cse hoist duplicate ADDR_OF(global) to entry build_loop_tree lower_loop_imm_operands / hoist_loop_consts loop-invariant imm materialization +opt_refresh_machine_clobbers rederive effects from final HIR instruction shapes live_blocks per-block PReg liveness (backward dataflow) dead_def_elim_with_live pre-RA dead-definition elimination regalloc_locations PReg -> hard reg / spill slot (no live-range splitting) verify "post-regalloc" -lower_to_mir build physical MFunc; insert spill/reload +lower_to_mir deep-clone HIR; rewrite values to physical locations mir_verify "lower-mir" mir_combine post-RA peephole / addressing-mode synthesis mir_dce post-RA dead-code elimination @@ -422,17 +432,19 @@ transform or analysis; the file paths orient the reader. - **Order + dominators + verify** (`src/opt/pass_analysis.c`): postorder / reverse-postorder, reachability, immediate dominators, dominator children, dominance frontiers (`OptAnalysis`), the coarse analysis-validity bits - (`OPT_ANALYSIS_CFG/DEF_USE/DOM/LOOP`), and the debug verifier `opt_verify`. + (`OPT_ANALYSIS_DEF_USE/DOM/LOOP`), and the debug verifier `opt_verify`. - **Liveness** (`src/opt/pass_live.c`): `opt_live_blocks` solves per-block PReg liveness by backward dataflow into elastic 64-bit-word bitsets (`OptBitset`, grown on demand, trailing-zero-trimmed); `opt_live_ranges_build` produces the compressed point-indexed live ranges and per-PReg frequency/spill-cost metrics the allocator consumes. - **Hard-register liveness** (`src/opt/pass_hard_live.c`): physical-register - live-in/out over the post-RA MIR, plus the per-call clobber mask - (`opt_call_clobber_mask_for`). This is what makes post-RA combine/DCE safe: a - value in a callee-saved register survives a call, while caller-saved registers - are killed by it. + live-in/out over the post-RA MIR. It consumes the same canonical instruction + effects (`opt_inst_reg_effects`) as post-RA combine, DCE, and native emission, + so explicit operands, call effects, inline-asm clobbers, and target machine + clobbers cannot drift between analyses. This is what makes post-RA + combine/DCE safe: a value in a callee-saved register survives a call, while a + register in that call's clobber mask is killed by it. - **Loop detection** (`src/opt/pass_loop.c`): `opt_build_loop_tree` computes loop nesting depth from dominators; depth feeds the allocator's spill-cost weighting and LICM. @@ -445,16 +457,23 @@ transform or analysis; the file paths orient the reader. - **Machinize** (`src/opt/pass_machinize.c`): `opt_machinize_native` is ABI lowering against the `NativeTarget`. It annotates calls/returns/params with calling-convention constraints (argument/result registers, the call clobber - and return masks, callee-save markers), collects the target's register classes - (allocable set, reserved scratch set, caller/callee-saved masks) and checks - allocable and scratch sets do not overlap, resolves inline-asm - named-register constraint strings into masks, and records per-instruction - fixed-register clobbers (Section 5). + and return masks, callee-save markers), collects and validates the target's + register classes (the physical-register table and its O1 allocation flags, + the independent O0 `ndt_allocable` cache policy, the O1 emission temporary + bank, asm-only temporary bank, caller/callee-saved masks, and cache-preserved + subset), resolves inline-asm named-register constraint strings into masks, and + records per-instruction fixed-register clobbers (Section 5). The O1 emission + bank must be physical, unique, non-allocable, disjoint from the asm-only bank, + and large enough for the target's operand-facing hooks. Every target-reported + clobber bit is validated against the class's physical-register table before + it can enter the allocator/effect side table. - **MIR view** (`src/opt/pass_mir.c`): the post-allocation physical IR. Rather than duplicate the CFG passes, `pass_mir.c` builds a transient `Func` *view* whose block arrays point at `Func.mir`, runs the shared `opt_combine`, `opt_dce`, `opt_build_cfg`, and `opt_jump_cleanup` over that view, and commits - it back. The `opt_mir_*` wrappers are thin shims over this view; the shared + it back. The MIR itself starts as a deep mutable clone, including operand and + aux arrays; HIR remains virtual and cannot be changed through an aliased MIR + pointer. The `opt_mir_*` wrappers are thin shims over this view; the shared passes are written once and reused for both HIR and MIR. - **Coalescing / allocation** (`src/opt/pass_coalesce.c`, `src/opt/pass_lower.c`): `opt_regalloc_locations` is a point-bitmap linear-scan allocator producing the @@ -477,55 +496,71 @@ transform or analysis; the file paths orient the reader. registers the allocator used, pre-maps frame slots, drives the backend's minimal-prologue hook when available, routes scalar call results straight to their destination, uses a hardware zero register for stored zeros where the - backend advertises one, and legalizes addresses the backend rejects into a - reserved scratch register. See [ARCH.md](ARCH.md) for the backend contract. - -## 5. Machine register-constraint model - -Some target instructions pin operands or results to specific physical registers -and clobber others as a side effect of their encoding — hardware constraints, -not allocator choices (x86-64 `idiv`/`div` pinning the dividend to `rax` and -clobbering `rdx`, variable shifts requiring the count in `cl`, one-operand -`mul`, `cmpxchg`, and the `va_arg` offset scratch). aarch64 and riscv64 have -no such instructions — their div/shift/mul are ordinary three-operand forms — -so on those targets the constraint hooks are inert. - -The optimizer models all fixed-register requirements through two allocator -primitives, and the allocator (`pass_lower.c`) speaks only in physical register -numbers here: - -- **Tied hard register** (`OptPRegInfo.tied_hard_reg`): pin a value to a - specific physical register. Set for inline-asm operands with a `{reg}` - constraint and for fixed-input/fixed-output machine operands. -- **Forbidden / clobbered hard registers** - (`OptPRegInfo.forbidden_hard_regs` / `clobbered_hard_regs`): for each register - an instruction clobbers, every value live *across* the instruction (live-after, - not a use or def of it) is forbidden from that register. The clobbered subset - is recorded separately so the soft return-register placement hint cannot later - clear a forbid that came from a real hardware clobber. - -Three sources feed these, all unified at allocation time: - -1. **Calls** — the call plan's `clobber_mask` (caller-saved by default, or the - call-specific mask) drives the live-across-forbid loop; argument/result - registers come from the plan. -2. **Inline asm** — `pass_machinize.c` resolves named-register constraint - strings and clobber lists into masks and fixed-register indices on the - `IRAsmAux`; `pass_lower.c`'s `apply_asm_register_constraints` ties the fixed - operands and runs the live-across-forbid loop. -3. **Generic machine instructions** — a binop or convert has no `aux` to hang - constraints on, so machinization queries the target's machine-clobber hook - per instruction and stores the result in a per-function side table keyed by - `InstId` (`Func.inst_clobbers`, built in `machinize_inst_clobbers`). At - allocation, `apply_machine_reg_clobbers` looks up the instruction's clobber - mask and applies the same live-across-forbid loop. A NULL hook (aa64/rv64) - means no entries and zero behavior change. - -This is the single place where target ISA register rules enter the allocator, -and all three sources reuse one mechanism — tie + forbid — rather than patching -assignments after the fact. A value that merely dies at the instruction needs no -constraint (the backend stages it into/out of the fixed register itself); only -values that survive past the instruction are forbidden. + backend advertises one, and materializes frame/recipe operands in an explicit + instruction-scoped temporary lease. The target declares the operand-facing + `emit_temps` bank separately from its O0 `scratch` policy and its asm-only + `asm_temps`; emitter temporaries never appear as MIR values. See + [ARCH.md](ARCH.md) for the backend contract. + +## 5. Register constraints and canonical effects + +Some instructions need registers that are fixed by their encoding, while other +instructions destroy registers as a side effect. Those are instruction-local +machine requirements, not persistent homes for the values involved. For +example, x86-64 division stages its dividend through `rax` and destroys +`rax`/`rdx`, a variable shift stages its count through `cl`, and atomics and +bitfield stores use fixed implementation registers. TLS descriptor sequences +and syscall intrinsics can also have format- or target-specific effects. + +The allocator has one authoritative hard constraint: +`OptPRegInfo.forbidden_hard_regs`. A hard register present in that mask cannot +be selected for the PReg. `preferred_hard_reg` is only a placement hint and is +never allowed to override a forbid. There is no persistent fixed-register tie +or pin in the allocation model. + +The two instruction-boundary sources populate that constraint differently: + +1. **Inline asm.** `pass_machinize.c` resolves each register constraint once + into an instruction-local `IRAsmRegRequirement` (register class, optional + fixed register, and optional allowed-register mask), and resolves the + clobber list into per-class masks. With the current unsplit allocator, + `pass_lower.c` conservatively forbids the candidate or clobbered registers + for PRegs that touch or cross that asm instruction. At emission the + optimizer stages each operand into a scoped register lease satisfying the + requirement, then writes outputs back to their real MIR homes. A generic + whole-class constraint needs no reservation because either the allocated + register already satisfies it or a spilled value uses an emitter temp. +2. **Generic machine instructions.** Machinization initially queries the + target's `machine_op_clobbers` hook and stores each nonzero result in the + dense, per-function `Func.inst_clobbers` side table keyed by `InstId`. + `lower_loop_imm_operands` can still change an instruction's fixed effects + (for example, by turning an x64 immediate shift into a register shift), and + `hoist_loop_consts` is the final HIR-shape mutator. The explicit + `opt_refresh_machine_clobbers` boundary after both passes replaces the table + with effects derived from those final HIR shapes. Allocation forbids those + registers for non-def values live after the instruction. The backend still + performs the instruction-local staging required by the encoding; a dying + operand is free to have any otherwise legal home. + +Calls use a separate preservation policy rather than feeding this machine +forbid mechanism. Allocation cost prefers callee-saved registers for values +that cross calls. If pressure nevertheless puts such a value in a register the +call clobbers, MIR lowering inserts the required save and restore. ABI argument +and result placement remains a call-marshalling concern. + +The corresponding post-allocation authority is `OptRegEffects`, built only by +`opt_inst_reg_effects`. It separates explicit `uses`, explicit `defs`, and +implicit `clobbers`; the distinction is intentional because a clobber kills a +live value but does not produce one for DCE. Calls contribute their lowered call +plan and ABI clobber mask, inline asm contributes its bound operands and resolved +clobber mask, and target instructions contribute the machinization side-table +mask. Hard-register liveness, MIR combine/DCE, and the native emitter all query +this one description. Emitter temporary leases are outside this model: they are +owned by one emission scope and are neither MIR defs nor implicit machine +clobbers. `machine_op_clobbers` is an exhaustive preservation contract for +ordinary `NativeTarget` operations: apart from explicit defs and reported +clobbers, a hook preserves every register that may hold an allocated or cached +value. ## 6. Allocation, MIR, and the physical boundary @@ -535,14 +570,56 @@ PReg into `Func.preg_locs` (`OptLoc`: hard register or spill slot). HIR operands stay virtual after allocation — the verifier checks this. `opt_lower_to_mir` then builds the physical IR `Func.mir` (an `MFunc`): each -virtual `OPK_REG` is translated through its `OptLoc` into a physical register or -a frame access, spilled values get a reload before each use and a store after -each def, and call plans are lowered into physical argument/return moves. From -this point the IR is physical and non-SSA (registers may be multiply defined). -All PReg-to-physical knowledge lives in this one step; after it, the HIR is -untouched and the MIR is fully physical. The downstream MIR passes (combine, -DCE, jump/layout cleanup) run over the MIR view and rely on physical-register -liveness for their safety checks, then `opt_emit_native` replays the MIR. +virtual `OPK_REG` is translated through its `OptLoc` into either a physical +`OPK_REG` or an `OPK_STACK` spill value. Spilled indirect bases and indices stay +as typed frame components, and cheap rematerializable values stay as `OPK_IMM` +or `OPK_FRAME_ADDR` recipes. Lowering does not surround each spilled use/def +with synthetic reload/store instructions. It inserts explicit location copies +only where the program really crosses a physical boundary, such as preserving +an intentionally caller-clobbered allocation across a call. Call plans are +lowered to the same location vocabulary. + +The MIR is a deep mutable clone, not a shallow rewrite of HIR storage. From this +point it is physical and non-SSA (registers may be multiply defined), and stale +HIR definition metadata is cleared. All PReg-to-physical knowledge lives in +this one step; after it, HIR is untouched and MIR is fully physical. The +downstream MIR passes (combine, DCE, jump/layout cleanup) run over the MIR view +and rely on canonical physical-register effects for safety. A pass that grows +the physical graph commits its extended instruction-id namespace with that +graph so later side tables remain correctly sized. Adjacent spill copies and +same-block spill dead stores are normalized over the `OPK_STACK` location +form. The same canonical operand walker used by liveness and emission accounts +for every stack use, including aux operands and spilled indirect-address +components, before DSE can retire an overwritten store. Then +`opt_emit_native` replays the MIR. + +At emission, each instruction opens a `NativeEmitTempScope`. The scope derives +its unavailable registers from physical live-after plus the instruction's +canonical uses/defs/clobbers. It prefers target-declared `emit_temps` for +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. +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 +instruction. + +Frame memory is authoritative. A spill definition is stored to its `FS_SPILL` +slot even when the emitter retains the clean register value for an adjacent +use; there is no dirty deferred writeback. The optional block-local frame cache +is keyed by exact slot and value type. Persistent entries may use a +target-declared cache-enabled emission temporary in `emit_cache_mask`, or a +dead O1 allocation register whose preservation is already established: +caller-saved within the current call-free region, or a callee-saved register +already present in the known frame. The O0 `ndt_allocable` bank is not consulted +by optimized emission. The exhaustive machine-effect contract invalidates any +exceptional fixed clobber, and all entries are cleared at block, call, and +inline-asm boundaries. Thus cache state is a replay optimization, never part of +MIR semantics or a second owner of a spilled value. The reason allocation results are a separate table rather than rewritten operands is the same mode-clarity principle from Section 2: a post-allocation @@ -557,11 +634,12 @@ checks CFG reciprocity, reachable-block shape, emit-order validity, instruction ids, operand namespaces (no physical registers in HIR; correct PReg-vs-Val namespace for the current mode), phi consistency, and def-use freshness; `opt_mir_verify` checks the physical boundary (no virtual operands, valid frame -slots, fully physical call plans). Each pass tags its checkpoint with the name -of the transformation just completed, so a failure localizes to the nearest -boundary. `Func.opt_valid_analyses` tracks coarse invalidation; passes that -mutate control flow, operands, or instructions rebuild or invalidate the -relevant analysis. +slots and location kinds/types, fully physical call plans, no emitter-owned +temporary in MIR, and no stale HIR def metadata). Each pass tags its checkpoint +with the name of the transformation just completed, so a failure localizes to +the nearest boundary. `Func.opt_valid_analyses` tracks coarse invalidation; +passes that mutate control flow, operands, or instructions rebuild or invalidate +the relevant analysis. Observability hooks: `KIT_DUMP=<tag>` dumps the optimizer IR at a named stage, `KIT_DUMPCG=1` dumps the recorded semantic tape before lowering, diff --git a/doc/plan/ARM32.md b/doc/plan/ARM32.md @@ -46,9 +46,10 @@ goto scratch), `asm_01_grammar` + `builtin_31_readcyclecounter` skipped (see "Broad parse-corpus triage" below); the 12 wrong-runtime-result reds were already CLOSED (2026-06-17). The earlier fix that took the lane from 33 to 17 fails: the O1 frameless-leaf tier returned through a bare -`BX lr` while the backend uses `lr` as an emit scratch (global-address staging, -overflow/popcount/bitfield/byte-copy), so any such leaf clobbered its return -address (the 16-case O1 globals/statics/attributes `rc 134` cluster). The slim +`BX lr` while the backend uses LR as a backend-private temporary +(global-address staging, overflow/popcount/bitfield/byte-copy), so any such +leaf clobbered its return address (the 16-case O1 globals/statics/attributes +`rc 134` cluster). The slim tier now saves `lr` (`PUSH {lr}`/`POP {pc}`). ### Broad parse-corpus triage — ALL reds CLOSED 2026-06-17 @@ -59,21 +60,23 @@ data-model-agnostic test rewrites, 2 data-model skips). The 5 remaining assembler bug it surfaced, 2 aa64-template / no-cycle-counter skips): - **Emit/codegen gaps (5) — DONE:** - [x] `gnu_labels_as_values_threaded` O1 — `opt native emit: no scratch - register` was a computed-goto store whose address consumed both arm32 - opt-scratch (`ip`+`lr`) as base+index, leaving none to materialize the - stored value. `collapse_addr_to_reg` (shared `pass_native_emit.c`) now - reuses the base when it is itself a reserved scratch (a transient dead - after the access): it folds the index into the base in place, freeing the - other scratch. Arch-neutral; only triggers when no third scratch exists, - so it is a no-op on register-rich backends. Runs `35` at O0+O1. + register` exposed the old emitter's implicit two-register (`ip`+`lr`) + staging policy: a computed-goto store consumed both for base+index and + still needed to materialize the value. The immediate fix reused a dead + address base in place. The later location-MIR redesign removed that + policy entirely: ARM32 now declares r0..r3 as instruction-scoped O1 + operand temporaries, while IP/LR remain backend-private. Runs `35` at + O0+O1. - [x] `asm_01_grammar` O0+O1 — `.arm32.skip`. The templates are aarch64-specific (`%w0` modifiers, `mov w0`, `dmb sy`, `x0` clobbers) and never assemble on arm32, exactly as on rv32/rv64/x64. The underlying error (`optimizer asm input not in a register`) was a *real* arm32 bug too — a register- constrained input whose value is a frame-resident local (e.g. a value that is `+r` inout AND returned) arrives as a frame loc, not a register. - Fixed in `arm32 native.c` `arm_asm_block_native`: stage such inputs into a - reserved scratch (IP, then LR) before binding, mirroring the aa64 path. + The original ARM backend fix staged such inputs before binding. The + current shared asm staging path leases an operand register within the + instruction scope, with LR exposed separately as an asm-only temporary; + IP remains backend-private. Surfaced and fixed a second bug in `arm32 asm.c`: `add`/`sub rd, rn, {#imm | rm}` only had the 2-operand hi-register `add` and the sp-adjust rows, so `add r4, r4, #1` assembled as `add r4, r4` (×2) and `sub r4, r4, @@ -96,10 +99,12 @@ assembler bug it surfaced, 2 aa64-template / no-cycle-counter skips): legality accepted the negate-trick but emit never implemented it. Fixed: shared `arm_resolve_addsub_imm` for legality + emit. (commit e98cc403) - [x] `variadic_02_many_ints` O1 — under pressure the spilled constant call args - rematerialized into the round-robin opt-scratch (ip/lr), aliasing the - simultaneously-live register args. Fixed: exclude call-arg values from - remat (keep their spill store) + store register-resident stack args - directly. (commit 51681b45) + exposed the old round-robin temporary policy: rematerialized call args + could alias simultaneously-live register args. The immediate fix kept + those spill stores and wrote register-resident stack args directly; the + location-MIR redesign now prevents the class of bug with scoped leases + plus phase-owned completed ABI destinations through the full argument + shuffle, followed by a post-call phase barrier. (commit 51681b45) - [x] `far_slot_large_frame` — was a data-model artifact (`long` is 32-bit on arm32, so `x`'s 8-byte check failed and the 8-byte far slot wasn't even exercised). Made the shared case `long long` → tests the 8-byte far slot @@ -390,9 +395,12 @@ board; the toy corpus went from 355/425 to ~418/425.** reads at a non-zero element offset, and the switch jump-table crashes. Globals apply a non-zero byte offset as a separate add (ARM REL MOVW/MOVT addends are carry-lossy), matching `elf_arm_reloc_field_addend`'s contract. -- **-O1 native-emit scratch** — `NativeAllocClassInfo.opt_scratch` gives the - optimizer two scratch (IP+LR) without the NDT taking LR; SREM/UREM no longer - need a hard LR temp. Fixes the "no scratch register" -O1 panics. +- **-O1 operand-temp ownership** — `NativeAllocClassInfo.emit_temps` declares + r0..r3 as the optimizer emitter's instruction-scoped bank, distinct from the + O0 `scratch` policy (IP) and the asm-only bank (LR). IP/LR stay private to + backend hook internals, so operand materialization cannot overlap an internal + address or arithmetic temporary. This supersedes the earlier shared IP/LR + emission policy and fixes the "no register available" pressure failures. - **Tail/sibling calls** — full AAPCS lowering (direct/indirect/void/sret/ variadic/stack-args), frame-size-independent epilogue, in-window stack args. - **Frame correctness** — sp kept 8-byte aligned at calls (odd-word saved block @@ -685,9 +693,9 @@ gated behind a target feature and used only to lower saturating builtins ### Registers (AAPCS) | reg | role | |---|---| -| r0–r3 | argument / result / scratch — **caller-saved**; r0(:r1) returns scalars | +| r0–r3 | argument / result — **caller-saved**; r0(:r1) returns scalars; O1 instruction-scoped operand-temp bank | | r4–r11 | **callee-saved** variable registers (r9 is general here — bare-metal, no platform/static-base reservation) | -| r12 (IP) | intra-procedure scratch — **caller-saved**; kit's codegen scratch for address/immediate materialization (the analog of aa64 x16/x17, rv `t`-regs) | +| r12 (IP) | intra-procedure temporary — **caller-saved**; O0 scratch policy and backend-private address/immediate staging | | r13 (SP) | stack pointer — 8-byte aligned at public interfaces (AAPCS) | | r14 (LR) | link register | | r15 (PC) | program counter (reads as +4, Thumb) | @@ -790,7 +798,7 @@ target): | `disasm.c` | descriptor-driven decode (read first half-word; if `[15:11] ∈ {0b11101,0b11110,0b11111}` it is a 32-bit instruction → read second half-word); IT-state tracking; `.inst`/`.hword` fallback | `riscv/disasm.c` | | `asm.c` | operand parsing, `ThumbExpandImm`/MOVW-MOVT/branch immediate encoding, IT-block syntax, `.syntax unified` | `aa64/asm.c` | | `regs.h` / `regs.c` | DWARF index ↔ name table (r0–r15, then s0–s31/d0–d15 for VFP) | `aa64/regs.c` | -| `native.c` | the `NativeTarget` (`src/arch/native_target.h:459`) + `NativeOps`: register pools (`arm_int_allocable[]` = r0–r3 then r4–r11; scratch = IP/r12, with r0–r3 fronted as arg regs per the rv32 Lever-1 forward-order rule), frame layout (PUSH/POP, prologue/epilogue), all ops, IT-based `cmp`→select, MOVW/MOVT materialization, SDIV/UDIV+MLS, BFI/UBFX bitfields, LDRD/STRD + lane ops for `i64` | `riscv/native.c`, `aa64/native.c` | +| `native.c` | the `NativeTarget` (defined in `src/arch/native_target.h`) + `NativeOps`: O0 cache pool r0–r3 and `scratch` IP; O1 persistent allocation r4–r11, scoped `emit_temps` r0–r3, and asm-only LR; frame layout (PUSH/POP, prologue/epilogue); all ops; IT-based `cmp`→select; MOVW/MOVT materialization; SDIV/UDIV+MLS; BFI/UBFX bitfields; LDRD/STRD + lane ops for `i64` | `riscv/native.c`, `aa64/native.c` | | `reloc.c` | `arm32_reloc_desc` table + `arm32_reloc_apply_insn` (the Thumb-2 split-immediate byte patcher) + `link_arch_arm32` | `riscv/reloc.c`, `aa64/reloc.c` | | `link.c` | `LinkArchDesc` (`src/link/link_arch.h:67`) — static-only, no PLT for v1 | `riscv/link.c` | | `dbg.c` | breakpoint (BKPT) + displaced-step shim; `min_insn_len=2, max_insn_len=4` | `riscv/dbg.c` | diff --git a/doc/plan/OPTIMIZER.md b/doc/plan/OPTIMIZER.md @@ -22,8 +22,7 @@ A few facts about the current code path frame everything below: - **O1 is the only live optimized path.** `opt_cgtarget_new` normalizes every `opt_level >= 1` request to `1` (`src/opt/opt.c`), so no compilation selects O2 today. The O1 pipeline runs entirely in the PReg namespace with the - non-splitting allocator (`opt_regalloc_locations(..., allow_live_range_split - = 0)`). + non-splitting allocator (`opt_regalloc_locations`). - **The O2 SSA mid-end (`opt_cleanup`) is fully implemented but unreached.** The schedule — register SSA, mem2reg, GVN, copy-prop, DSE, LICM, pressure relief, conventional SSA, undo-SSA — is defined and maintained against targeted opt @@ -34,12 +33,21 @@ A few facts about the current code path frame everything below: callees under an 8-op cost cap, always inlines `always_inline`, and refuses `noinline`/recursive/control-rich callees. The whole-program inliner (`opt_inline`) also exists but is not wired into a live path. -- **The generic machine register-constraint mechanism is done (baseline).** - The tie/forbid/clobber primitives, the per-instruction clobber side table - (`Func.inst_clobbers`), `machinize_inst_clobbers`, and - `apply_machine_reg_clobbers` are in tree, fed by the x64 machine-clobber hook - for div/mod/shift/mul/CAS/RMW/`va_arg`; aarch64/riscv64 leave the hook inert. - Remaining work is follow-on cleanup, not the core model. +- **The generic register-constraint/effect mechanism is done (baseline).** + `OptPRegInfo.forbidden_hard_regs` is allocation's one hard constraint; a + preferred register is only a hint and can never clear it. Machinization + resolves inline-asm register requirements at the instruction and builds the + initial target-specific implicit-kill table in `Func.inst_clobbers`; after + loop-immediate lowering and constant hoisting, the final HIR-shape boundary + refreshes that table before liveness and allocation. After allocation, + `OptRegEffects` is the one authority for explicit uses/defs and + call/asm/machine clobbers, consumed by hard-register liveness, combine, DCE, + and native emission. The x64 hook covers fixed effects including division, + shifts, atomics, bitfield stores, intrinsics, syscall, and descriptor TLS; + aarch64 covers syscall and descriptor TLS; rv64 covers syscall. A hook is + inert only for targets and operations with no fixed clobbers. The hook is + exhaustive: ordinary backend operations preserve every allocated or cached + register not named as an explicit def or machine clobber. ## 1. Complete and turn on the O2 SSA mid-end @@ -135,7 +143,7 @@ beyond coalescing. singleton-only, non-call-crossing shape sketched earlier. - Keep coalescing gated to O2 (matching MIR), preserving the unit-overlap conflict counting that prevents merging a multiply-defined local with a - tied-register param. + conflicting parameter live range. ## 3. O1 generated-code quality @@ -242,28 +250,49 @@ they do not change which source variables get stable stack homes. that clang does not, inflating object text and the linked image and obscuring measurements. Emit static-inline/header helpers only when referenced. -## 5. Machine register-constraint model: remaining work - -**Goal.** Finish the follow-on cleanup now that the generic fixed-register/ -clobber mechanism is in place, and keep the model the single entry point for -target ISA register rules into the allocator. - -**Rationale.** The core mechanism (tie + forbid + per-instruction clobber side -table, with the x64 hook covering div/mod/shift/mul/CAS/RMW/`va_arg`) is shipped -and is what makes the x64 `-O1` div/mod/atomic/overflow/varargs cases correct. -What remains is removing the now-redundant defensive code the mechanism made -unnecessary, and keeping the contract clean as new constrained ops appear. - -- **Remove defensive backend moves the allocator now guarantees.** With the - dividend tied to `rax` and nothing live in `rdx`/`rcx`, the `mov rax, - dividend` in `x64_binop`, the address-staging in `x64_atomic_cas`/`_rmw`, and - the shift `mov rcx, count` are register-to-itself no-ops. Simplify them away. -- **Confirm `InstId` density and stability** through `pass_lower` so the - side-table lookup stays O(1); fall back to a small open-addressed map keyed by - instruction only if ids ever become sparse. -- **Add focused regressions** for the live-across-the-constraint shape (e.g. a - function that divides while keeping an unrelated value live across the divide) - so the live-across-forbid loop stays covered as new ops are added. +## 5. Register constraints and effects: maintained contract + +**Goal.** Keep target ISA register rules at one allocation boundary and one +post-allocation effects boundary, with instruction-local staging owned by the +emitter/backend that performs the operation. + +**Rationale.** The allocation mechanism has one hard forbid mask, while +machinization carries instruction-local asm requirements and the dense machine +clobber side table. `OptRegEffects` remains the physical authority for distinct +uses, defs, and clobbers. The target effect hook is also an exhaustive +preservation contract for clean frame-cache values. This division avoids both +persistent operand pinning and a second, backend-local liveness model. + +- **Keep fixed-encoding staging local.** Do not remove moves into registers + such as x64 `rax` or `rcx` merely because live-across values are forbidden + there. A dying operand may still begin in any legal home; the backend must + stage the current operation's operands and results as its encoding requires. +- **Keep `Func.inst_clobbers` dense and derived from final HIR.** Machinization + builds the initial table. After `lower_loop_imm_operands` and + `hoist_loop_consts` (the last HIR-shape mutator), + `opt_refresh_machine_clobbers` replaces it before liveness and allocation. + Preserve `InstId` density and stability through lowering so lookup stays + O(1), and refresh or verify the table whenever instruction identity or effect + shape changes. +- **Keep focused regressions** for live-across fixed effects (division, + variable shifts, bitfield stores, atomics, TLS descriptor calls, syscalls) + and for inline-asm fixed/subset constraints under spills and pressure. +- **Resolve inline-asm constraints once.** New constraint forms become + `IRAsmRegRequirement`s during machinization and are satisfied by scoped + emitter staging at the asm instruction. They must not introduce a + whole-live-range hard-register pin or a second allocator in the backend. +- **Keep frame-cache eligibility derived.** A persistent clean entry may use a + target-declared `emit_cache_mask` register or a liveness-proven dead O1 + allocation register whose ABI preservation is already established. Effects + invalidate every exceptional fixed clobber, and block/call/asm boundaries + clear the cache; + do not add another cache ownership mask. +- **Keep target hooks declarative.** A new implicit-register operation enters + through `machine_op_clobbers` so allocation sees the forbid and + `opt_inst_reg_effects` sees the same mask after MIR lowering. The report must + include every fixed clobber of a register that can hold an allocated or + cached value; backend-local liveness or unreported clobber exceptions are not + a supported extension point. ## 6. Inlining beyond tiny streaming callees diff --git a/doc/plan/TODO.md b/doc/plan/TODO.md @@ -22,14 +22,6 @@ Add new deferred fixes below as they are discovered. ## Deferred dedups & abstraction cleanups ("use the shared seam") -- **A.2 — optimizer-path inline-asm dedup.** The three `aa_/x64_/rv_asm_block_native` - share an out/in/bind/run/store skeleton but diverge by *policy* (staging coverage + - structurally different scratch-register models: aa64/x64 reuse 2 mem-scratch regs via - a shared `ntmp`; rv64 uses 4 dedicated stage regs). A hooks dedup would relocate, not - unify, behavior and add miscompile risk in a hot path. Now that the aa64/x64 - staging gaps are closed, a shared `native_asm_bind_optimizer_operands` can be - considered. (The -O0 direct path is already shared via - `native_asm_bind_direct_operands`.) - **E.3 — OS-neutral env code duplicated** between `driver/env/posix.c` and `windows.c` (stdio writer + thunks, `env→{context,jit_host,dbg_host}` trio, dir-handle structs + read/close, `read_stdin` grow/shrink). Move to `driver/env/common.c`. **Blocked on a diff --git a/driver/env/build_host_posix.c b/driver/env/build_host_posix.c @@ -239,7 +239,10 @@ static int db_transport_try_accept(void* user, KitBuildListener* lst, if (!bh || !lst || !out || !ready) return 1; *out = NULL; *ready = 0; - FD_ZERO(&fds); + /* Darwin's FD_ZERO expands to the non-C11 __builtin_bzero. Initialize the + * public fd_set object directly so the hosted adapter remains bootstrapable + * by a strict C11 compiler. */ + memset(&fds, 0, sizeof fds); FD_SET(lst->fd, &fds); tv.tv_sec = 0; tv.tv_usec = 10000; diff --git a/lang/c/parse/cg.c b/lang/c/parse/cg.c @@ -21,14 +21,27 @@ static uint16_t c_cg_lvalue_flags_for_type(const Type* ty) { return flags; } -/* Resolve the CG type id for a value-stack slot: the slot's recorded cg type - * when present, else map the frontend lang_type. kit_cg_slot_cg_type reads the - * slot's type directly (no struct copy); the lang_type fallback is taken only - * when the slot carries no cg type. */ +/* Resolve the operational CG type for a value-stack slot. A complete recorded + * type is authoritative. A missing type, or an incomplete nominal record left + * by dereferencing a pointer, crosses back through the frontend lang_type so a + * by-value operation sees the completed layout. kit_cg_slot_cg_type reads the + * slot's id directly (no struct copy). */ static KitCgTypeId c_cg_slot_cg_id(Parser* p, u32 depth) { + const Type* lang_ty = + (const Type*)kit_cg_slot_lang_type(p->cg, depth); KitCgTypeId cg = kit_cg_slot_cg_type(p->cg, depth); - if (cg) return cg; - return c_cg_tid(p, (const Type*)kit_cg_slot_lang_type(p->cg, depth)); + KitCgTypeId resolved; + if (cg && kit_cg_type_is_complete(p->c, cg)) return cg; + + /* A pointer to a record needs only its nominal CG declaration. Dereferencing + * that pointer therefore leaves an incomplete record id on the value stack, + * even when the C record itself is complete. A later by-value operation + * (load/store/aggregate copy) must cross back through the language type so + * its field layout is installed before the memory operation captures a byte + * count. Returning the recorded id unconditionally made the first + * `*dst = *src` for such a type a zero-byte copy. */ + resolved = lang_ty ? c_cg_tid(p, lang_ty) : KIT_CG_TYPE_NONE; + return resolved ? resolved : cg; } static KitCgTypeId c_cg_top_cg_id(Parser* p) { return c_cg_slot_cg_id(p, 0); } diff --git a/mk/bootstrap.mk b/mk/bootstrap.mk @@ -41,7 +41,8 @@ endif BOOTSTRAP_STAMP = $(BOOTSTRAP_DIR)/.stamp BOOTSTRAP_RT_LIBS = $(addprefix $(RT_BUILD_DIR)/,$(addsuffix /libkit_rt.a,$(RT_DEFAULT_VARIANTS))) BOOTSTRAP_MAKEFILES = Makefile mk/env.mk mk/config.mk mk/flags.mk \ - mk/lib_srcs.mk mk/driver_srcs.mk mk/rt.mk + mk/lib_srcs.mk mk/driver_srcs.mk mk/rt.mk \ + mk/bootstrap.mk # BOOTSTRAP_SEED: a pre-built stage1 kit. Empty (default) = the normal native # flow where stage1 is the host $(BIN). When set (a path), stage1 is copied from @@ -85,6 +86,9 @@ $(BOOTSTRAP_STAMP): $(BOOTSTRAP_DEPS) rm -rf $(BOOTSTRAP_DIR) @mkdir -p $(BOOTSTRAP_STAGE1_DIR) cp $(BOOTSTRAP_STAGE1_SRC) $(BOOTSTRAP_STAGE1_BIN) + # The copied stage1 binary no longer sits beside $(BUILD_DIR)/support. Give + # it the same self-contained support/rt layout as every normal bin target. + $(MAKE) $(BOOTSTRAP_STAGE1_DIR)/support/rt @for tool in $(BOOTSTRAP_TOOLS); do ln -sf kit "$(BOOTSTRAP_STAGE1_DIR)/$$tool"; done $(MAKE) lib bin \ BUILD_DIR='$(abspath $(BOOTSTRAP_STAGE2_DIR))' \ diff --git a/mk/test.mk b/mk/test.mk @@ -1123,9 +1123,21 @@ test-link-macho-symbols: bin $(LINK_EXE_RUNNER) OPT_TEST_BIN = build/test/cg_ir_lower_test TINY_INLINE_TEST_BIN = build/test/tiny_inline_test - -test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-x64-win-tail-sret test-opt-prologue-tier test-opt-whole-program-inline test-opt-lto-phase1 test-opt-redundant-copy-ext test-opt-redundant-frame-sub test-opt-o1-branch-cleanup test-opt-hot-slot-order test-opt-o1-remat test-opt-aa64-x29-bottom test-opt-o1-coalesce test-opt-o1-switch-imm test-opt-o1-inline-cap test-opt-rv64-far-slot test-opt-o1-cmp-imm test-opt-o1-stack-dse test-opt-o1-local-cse test-opt-o1p-aa64 test-opt-o1p-const-divmul test-opt-o1p-combine test-opt-o1p-rider +REG_EFFECTS_TEST_BIN = build/test/reg_effects_test +LOCATION_MIR_TEST_BIN = build/test/location_mir_test +COMBINE_CSE_TEST_BIN = build/test/combine_cse_test +NATIVE_EMIT_FRAME_DST_TEST_BIN = build/test/native_emit_frame_dst_test +FRAME_VALUE_BACKEND_TEST_BIN = build/test/frame_value_backend_test +NATIVE_PART_CHUNK_TEST_BIN = build/test/native_part_chunk_test + +test-opt: bin $(OPT_TEST_BIN) $(REG_EFFECTS_TEST_BIN) $(LOCATION_MIR_TEST_BIN) $(COMBINE_CSE_TEST_BIN) $(NATIVE_EMIT_FRAME_DST_TEST_BIN) $(FRAME_VALUE_BACKEND_TEST_BIN) $(NATIVE_PART_CHUNK_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-aa64-apple-varargs test-opt-x64-win-tail-sret test-opt-prologue-tier test-opt-whole-program-inline test-opt-lto-phase1 test-opt-redundant-copy-ext test-opt-redundant-frame-sub test-opt-o1-branch-cleanup test-opt-macho-block-alt-entry test-opt-hot-slot-order test-opt-o1-remat test-opt-aa64-x29-bottom test-opt-o1-coalesce test-opt-o1-switch-imm test-opt-o1-inline-cap test-opt-rv64-far-slot test-opt-o1-cmp-imm test-opt-o1-stack-dse test-opt-arm32-emit-temp-pressure test-opt-o1-local-cse test-opt-o1p-aa64 test-opt-o1p-const-divmul test-opt-o1p-combine test-opt-o1p-rider $(OPT_TEST_BIN) + $(REG_EFFECTS_TEST_BIN) + $(LOCATION_MIR_TEST_BIN) + $(COMBINE_CSE_TEST_BIN) + $(NATIVE_EMIT_FRAME_DST_TEST_BIN) + $(FRAME_VALUE_BACKEND_TEST_BIN) + $(NATIVE_PART_CHUNK_TEST_BIN) # Structural disasm check: the -O1 redundant copy / extension folds @@ -1146,6 +1158,10 @@ test-opt-redundant-frame-sub: bin test-opt-o1-branch-cleanup: bin @KIT=$(abspath $(BIN)) bash test/opt/o1_branch_cleanup.sh +.PHONY: test-opt-macho-block-alt-entry +test-opt-macho-block-alt-entry: bin $(ROUNDTRIP_BIN_MACHO) + @KIT=$(abspath $(BIN)) bash test/opt/macho_block_alt_entry.sh + # Structural disasm check: hot-slot-low frame ordering keeps the hottest spill # slots in cheap displacement range (x64 disp8, rv64 imm12 single-instruction). .PHONY: test-opt-hot-slot-order @@ -1202,6 +1218,13 @@ test-opt-o1-cmp-imm: bin test-opt-o1-stack-dse: bin @KIT=$(abspath $(BIN)) bash test/opt/o1_stack_dse.sh +# Compile-only ARMv7-M pressure guard: location MIR leaves spill values in +# frame homes, and the native emitter must stage operand-facing values from its +# explicit r0-r3 bank without borrowing backend-private IP/LR. +.PHONY: test-opt-arm32-emit-temp-pressure +test-opt-arm32-emit-temp-pressure: bin + @KIT=$(abspath $(BIN)) bash test/opt/arm32_emit_temp_pressure.sh + # Behavioral+structural check: same-block redundant-load/expression CSE reuses # the last load/compute, but never across a may-alias store, call, or volatile. .PHONY: test-opt-o1-local-cse @@ -1256,6 +1279,10 @@ test-opt-static-prune-aa64: bin test-opt-aa64-tail: bin @KIT=$(abspath $(BIN)) bash test/opt/aa64_tail_call.sh +.PHONY: test-opt-aa64-apple-varargs +test-opt-aa64-apple-varargs: bin + @KIT=$(abspath $(BIN)) bash test/opt/aa64_apple_varargs.sh + .PHONY: test-opt-x64-win-tail-sret test-opt-x64-win-tail-sret: bin @KIT=$(abspath $(BIN)) bash test/opt/x64_win_tail_sret.sh diff --git a/mk/test_unit.mk b/mk/test_unit.mk @@ -72,7 +72,9 @@ UNIT_TESTS_INTERNAL := \ interp_smoke_test jit_tls_relax_test coff_weak_alias_test \ coff_archive_fixpoint_test elf_version_import_test \ rv64_interp_smoke_test abi_classify_test ir_recorder_test \ - native_direct_target_test x64_dbg_test cg_ir_lower_test tiny_inline_test + native_direct_target_test x64_dbg_test cg_ir_lower_test tiny_inline_test \ + reg_effects_test location_mir_test combine_cse_test \ + native_emit_frame_dst_test frame_value_backend_test native_part_chunk_test dwarf_test_SRC := test/dwarf/dwarf_test.c debug_roundtrip_unit_SRC := test/debug/roundtrip_unit.c debug_cfi_unit_SRC := test/debug/cfi_unit.c @@ -97,6 +99,12 @@ native_direct_target_test_SRC := test/cg/native_direct_target_test.c x64_dbg_test_SRC := test/arch/x64_dbg_test.c cg_ir_lower_test_SRC := test/opt/cg_ir_lower_test.c tiny_inline_test_SRC := test/opt/tiny_inline_test.c +reg_effects_test_SRC := test/opt/reg_effects_test.c +location_mir_test_SRC := test/opt/location_mir_test.c +combine_cse_test_SRC := test/opt/combine_cse_test.c +native_emit_frame_dst_test_SRC := test/opt/native_emit_frame_dst_test.c +frame_value_backend_test_SRC := test/opt/frame_value_backend_test.c +native_part_chunk_test_SRC := test/opt/native_part_chunk_test.c build_pure_test_SRC := test/build/build_pure_test.c # ---- build rules ------------------------------------------------------------ diff --git a/scripts/cross_test.sh b/scripts/cross_test.sh @@ -70,14 +70,14 @@ tok_libc() { case "$1" in linux-*) local r="${1#linux-}"; printf '%s' "${r%%-*}" # Per-arch -march/-mabi a freestanding corpus object must be compiled with so # its ELF e_flags match the bare-metal stub + runtime it is linked against -# (test/lib/exec_bare.sh). rv32 is the one with a float-ABI axis: the bare -# harness (and the toy cross lane) are hard-single (ilp32f), but kit's rv32 -# *default* is now soft-float (ilp32, commit b436d6ef) — without pinning, the -# corpus object's e_flags (0x1) mismatch the stub's (0x3) and the freestanding -# link rejects it. Keep in sync with _bare_emit_rv32 / test/toy/run.sh. +# (test/lib/exec_bare.sh). Both RISC-V widths have a float-ABI axis, so make the +# contract explicit rather than inheriting a compiler default. rv32's harness +# is hard-single (ilp32f); rv64 enables D and uses hard-double (lp64d). Keep in +# sync with _bare_emit_rv32/_bare_emit_rv64 and test/toy/run.sh. freestanding_abi_flags() { # token -> echoes extra cc flags ("" for most arches) case "$(tok_arch "$1")" in rv32) printf '%s' "-march=rv32imafc_zicsr_zifencei -mabi=ilp32f" ;; + rv64) printf '%s' "-march=rv64imafd_zicsr_zifencei -mabi=lp64d" ;; *) printf '' ;; esac } diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -57,6 +57,11 @@ enum { AA_X8 = 8u, /* indirect-result (sret) register; usable as a copy base that aa_copy_bytes (which scratches only x16/x17) never clobbers */ + /* 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. */ + AA_TAIL_TARGET = 9u, AA_TMP0 = 16u, AA_TMP1 = 17u, /* Callee-saved frame-base anchor. The single-pass far-slot fast path @@ -873,8 +878,50 @@ static AANativeSlot* aa_slot(AANativeTarget* a, NativeFrameSlot slot) { return native_frame_slot_at(&a->frame, slot); } -static void aa_addr_base(AANativeTarget* a, NativeAddr addr, u32* base_out, - i32* off_out) { +/* x16/x17 are the backend-private integer temporaries available to memory + * primitives. Address formation must not silently overwrite a store payload + * or a register index that the final memory instruction still consumes. Keep + * those conflicts explicit at the address-materialization boundary. */ +typedef u8 AATmpAvoid; +enum { + AA_TMP_AVOID_0 = 1u << 0, + AA_TMP_AVOID_1 = 1u << 1, +}; + +static AATmpAvoid aa_tmp_avoid_reg(Reg reg) { + if (reg == AA_TMP0) return AA_TMP_AVOID_0; + if (reg == AA_TMP1) return AA_TMP_AVOID_1; + return 0u; +} + +static Reg aa_addr_tmp(AANativeTarget* a, AATmpAvoid avoid) { + if (!(avoid & AA_TMP_AVOID_0)) return AA_TMP0; + if (!(avoid & AA_TMP_AVOID_1)) return AA_TMP1; + aa_panic(a, "address formation has no scratch register"); + return AA_TMP0; +} + +static Reg aa_addr_adjust_tmp(AANativeTarget* a, Reg base, + AATmpAvoid avoid) { + AATmpAvoid base_bit = aa_tmp_avoid_reg(base); + if (base_bit && !(avoid & base_bit)) return base; + return aa_addr_tmp(a, avoid); +} + +static AATmpAvoid aa_mem_addr_avoid(int load, NativeLoc reg, + NativeAddr addr) { + AATmpAvoid avoid = 0u; + /* v16/v17 and x16/x17 are separate register files. Only an integer store + * keeps its Rt live across integer address materialization. */ + if (!load && !native_loc_is_fp(reg)) + avoid |= aa_tmp_avoid_reg(loc_reg(reg)); + if (addr.index_kind == NATIVE_ADDR_INDEX_REG) + avoid |= aa_tmp_avoid_reg(addr.index.reg); + return avoid; +} + +static void aa_addr_base(AANativeTarget* a, NativeAddr addr, + AATmpAvoid avoid, u32* base_out, i32* off_out) { *base_out = AA_TMP0; *off_out = addr.offset; switch ((NativeAddrBaseKind)addr.base_kind) { @@ -887,15 +934,39 @@ static void aa_addr_base(AANativeTarget* a, NativeAddr addr, u32* base_out, *off_out = aa_fp_off_slot(a, s->off) + addr.offset; return; } + case NATIVE_ADDR_BASE_FRAME_VALUE: { + NativeLoc tmp; + NativeAddr base_addr = addr; + Reg addr_tmp = aa_addr_tmp(a, avoid); + if (!addr.base_type) + aa_panic(a, "frame-value base has no exact type"); + base_addr.index_kind = NATIVE_ADDR_INDEX_NONE; + base_addr.index_ext = NATIVE_ADDR_IDX_EXT_NONE; + base_addr.log2_scale = 0; + memset(&tmp, 0, sizeof tmp); + tmp.kind = NATIVE_LOC_REG; + tmp.cls = NATIVE_REG_INT; + tmp.type = addr.base_type; + tmp.v.reg = addr_tmp; + a->base.load_addr(&a->base, tmp, base_addr); + *base_out = addr_tmp; + *off_out = 0; + return; + } case NATIVE_ADDR_BASE_GLOBAL: { NativeLoc tmp; + NativeAddr base_addr = addr; + Reg addr_tmp = aa_addr_tmp(a, avoid); + base_addr.index_kind = NATIVE_ADDR_INDEX_NONE; + base_addr.index_ext = NATIVE_ADDR_IDX_EXT_NONE; + base_addr.log2_scale = 0; memset(&tmp, 0, sizeof tmp); tmp.kind = NATIVE_LOC_REG; tmp.cls = NATIVE_REG_INT; tmp.type = builtin_id(KIT_CG_BUILTIN_I64); - tmp.v.reg = AA_TMP0; - a->base.load_addr(&a->base, tmp, addr); - *base_out = AA_TMP0; + tmp.v.reg = addr_tmp; + a->base.load_addr(&a->base, tmp, base_addr); + *base_out = addr_tmp; *off_out = 0; return; } @@ -915,7 +986,7 @@ static void aa_emit_mem_q(AANativeTarget* a, int load, NativeLoc reg, MCEmitter* mc = a->base.mc; if (addr.index_kind != NATIVE_ADDR_INDEX_NONE) aa_panic(a, "unsupported q-register indexed memory access"); - aa_addr_base(a, addr, &base, &off); + aa_addr_base(a, addr, 0u, &base, &off); rt = loc_reg(reg); if (off >= 0 && (((u32)off & 15u) == 0) && ((u32)off >> 4) <= 0xfffu) { aa_emit32(mc, aa_ldst_q_uimm(load, rt, base, (u32)off)); @@ -929,19 +1000,25 @@ static void aa_emit_mem_q(AANativeTarget* a, int load, NativeLoc reg, aa_emit32(mc, aa_ldst_q_uimm(load, rt, AA_TMP1, 0)); } -static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, - NativeAddr addr, MemAccess mem) { - u32 base, rt, sz, ld_opc; +static void aa_emit_mem_native(AANativeTarget* a, int load, NativeLoc reg, + NativeAddr addr, MemAccess mem) { + u32 base, rt, sz, bytes, ld_opc; + AATmpAvoid addr_avoid; i32 off; MCEmitter* mc = a->base.mc; rt = loc_reg(reg); - sz = size_idx(mem.size - ? mem.size - : type_size32(&a->base, reg.type ? reg.type : mem.type)); - if (native_loc_is_fp(reg) && - (mem.size - ? mem.size - : type_size32(&a->base, reg.type ? reg.type : mem.type)) == 16u) { + addr_avoid = aa_mem_addr_avoid(load, reg, addr); + bytes = mem.size + ? mem.size + : type_size32(&a->base, reg.type ? reg.type : mem.type); + if (native_loc_is_fp(reg)) { + if (bytes != 4u && bytes != 8u && bytes != 16u) + aa_panic(a, "unsupported native fp memory width"); + } else if (bytes != 1u && bytes != 2u && bytes != 4u && bytes != 8u) { + aa_panic(a, "unsupported native integer memory width"); + } + sz = size_idx(bytes); + if (native_loc_is_fp(reg) && bytes == 16u) { aa_emit_mem_q(a, load, reg, addr); return; } @@ -1020,7 +1097,7 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, if (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL && addr.index_kind == NATIVE_ADDR_INDEX_NONE) { i64 addend = addr.base.global.addend + (i64)addr.offset; - u32 scratch = (!load && rt == AA_TMP0) ? AA_TMP1 : AA_TMP0; + u32 scratch = aa_addr_tmp(a, addr_avoid); u32 pos = mc_pos(mc); if (aa_use_got_for_sym(&a->base, addr.base.global.sym)) { aa_emit32(mc, aa64_adrp(scratch, 0, 0)); @@ -1049,7 +1126,7 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, addr.base.global.sym, addend, 0, 0); return; } - aa_addr_base(a, addr, &base, &off); + aa_addr_base(a, addr, addr_avoid, &base, &off); if (addr.index_kind != NATIVE_ADDR_INDEX_NONE) { u32 use_base = base; u32 scaled = 0; @@ -1064,7 +1141,7 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, else if (addr.index_ext == NATIVE_ADDR_IDX_EXT_UXTW) option = AA64_LDST_OPTION_UXTW; if (off) { - use_base = AA_TMP1; + use_base = aa_addr_adjust_tmp(a, base, addr_avoid); aa_emit_add_imm(a, use_base, base, off); } if (addr.log2_scale == 0) { @@ -1093,10 +1170,93 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, : aa_stur_v(sz, native_loc_is_fp(reg), rt, base, off)); return; } - aa_emit_add_imm(a, AA_TMP1, base, off); - aa_emit32( - mc, load ? aa_ldur_op_v(sz, native_loc_is_fp(reg), ld_opc, rt, AA_TMP1, 0) - : aa_stur_v(sz, native_loc_is_fp(reg), rt, AA_TMP1, 0)); + { + u32 scratch = aa_addr_adjust_tmp(a, base, addr_avoid); + aa_emit_add_imm(a, scratch, base, off); + aa_emit32(mc, load ? aa_ldur_op_v(sz, native_loc_is_fp(reg), ld_opc, rt, + scratch, 0) + : aa_stur_v(sz, native_loc_is_fp(reg), rt, scratch, 0)); + } +} + +static u32 aa_part_transfer_tmp(AANativeTarget* a, NativeLoc reg, + NativeAddr addr) { + u32 used = 1u << loc_reg(reg); + if (addr.base_kind == NATIVE_ADDR_BASE_REG) used |= 1u << addr.base.reg; + if (addr.index_kind == NATIVE_ADDR_INDEX_REG) used |= 1u << addr.index.reg; + if (!(used & (1u << AA_TMP0))) return AA_TMP0; + if (!(used & (1u << AA_TMP1))) return AA_TMP1; + aa_panic(a, "exact ABI part transfer has no scratch register"); + return AA_TMP0; +} + +/* 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. */ +static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, + NativeAddr addr, MemAccess mem) { + u32 bytes = mem.size + ? mem.size + : type_size32(&a->base, reg.type ? reg.type : mem.type); + NativePartChunkIter it; + NativeLoc lane; + u32 chunk_off, chunk_size, regno, tmp; + if (bytes == 1u || bytes == 2u || bytes == 4u || bytes == 8u || + (native_loc_is_fp(reg) && bytes == 16u)) { + aa_emit_mem_native(a, load, reg, addr, mem); + return; + } + if (native_loc_is_fp(reg) || bytes == 0u || bytes > 8u) + aa_panic(a, "unsupported exact ABI part memory width"); + + regno = loc_reg(reg); + /* A native-width load may overwrite its address register because it is one + * instruction (`ldr x0, [x0]`). An exact-width load is a sequence, so retain + * the effective address in a backend-private register when dst aliases its + * base or index. The other private register remains available for packing + * later chunks into dst. */ + if (load && + ((addr.base_kind == NATIVE_ADDR_BASE_REG && addr.base.reg == regno) || + (addr.index_kind == NATIVE_ADDR_INDEX_REG && + addr.index.reg == regno))) { + NativeLoc addr_loc; + NativeAddr retained; + u32 addr_reg = regno == AA_TMP0 ? AA_TMP1 : AA_TMP0; + if (regno == AA_TMP0 || regno == AA_TMP1) + aa_panic(a, "exact load destination aliases private address register"); + addr_loc = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), + NATIVE_REG_INT, addr_reg); + a->base.load_addr(&a->base, addr_loc, addr); + memset(&retained, 0, sizeof retained); + retained.base_kind = NATIVE_ADDR_BASE_REG; + retained.base.reg = addr_reg; + retained.base_type = addr_loc.type; + addr = retained; + } + tmp = aa_part_transfer_tmp(a, reg, addr); + it = native_part_chunks(bytes, 8u); + while (native_part_chunk_next(&it, &chunk_off, &chunk_size)) { + NativeAddr chunk_addr = addr; + MemAccess chunk_mem = mem; + chunk_addr.offset += (i32)chunk_off; + chunk_mem.size = chunk_size; + if (chunk_mem.align > chunk_size) chunk_mem.align = chunk_size; + lane = reg; + if (chunk_off == 0u) { + lane.v.reg = regno; + } else { + lane.v.reg = tmp; + if (!load) + aa_emit32(a->base.mc, + aa_ubfm(1u, tmp, regno, chunk_off * 8u, 63u)); + } + aa_emit_mem_native(a, load, lane, chunk_addr, chunk_mem); + if (load && chunk_off != 0u) + aa_emit32(a->base.mc, + aa_logsr_lsl(1u, 1u, regno, regno, tmp, chunk_off * 8u)); + } } static NativeAllocClass aa_class_for_type(NativeTarget* t, KitCgTypeId type) { @@ -1264,12 +1424,13 @@ static void aa_materialize_frame_index(AANativeTarget* a, NativeAddr* addr, MemAccess mem; u32 reg; if (addr->index_kind != NATIVE_ADDR_INDEX_FRAME_VALUE) return; + if (!addr->index_type) + aa_panic(a, "frame-value index has no exact type"); reg = avoid_reg == AA_TMP1 ? AA_TMP0 : AA_TMP1; memset(&load, 0, sizeof load); load.base_kind = NATIVE_ADDR_BASE_FRAME; load.base.frame = addr->index.frame; - load.base_type = - addr->index_type ? addr->index_type : builtin_id(KIT_CG_BUILTIN_I64); + load.base_type = addr->index_type; memset(&idx, 0, sizeof idx); idx.kind = NATIVE_LOC_REG; idx.cls = NATIVE_REG_INT; @@ -1277,8 +1438,8 @@ static void aa_materialize_frame_index(AANativeTarget* a, NativeAddr* addr, idx.v.reg = reg; memset(&mem, 0, sizeof mem); mem.type = load.base_type; - mem.size = 8; - mem.align = 8; + mem.size = type_size32(&a->base, load.base_type); + mem.align = type_align32(&a->base, load.base_type); aa_emit_mem(a, 1, idx, load, mem); addr->index_kind = NATIVE_ADDR_INDEX_REG; addr->index.reg = reg; @@ -2504,8 +2665,8 @@ static void aa_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) { addr.base_type ? addr.base_type : builtin_id(KIT_CG_BUILTIN_I64); memset(&mem, 0, sizeof mem); mem.type = load.base_type; - mem.size = 8; - mem.align = 8; + mem.size = type_size32(t, load.base_type); + mem.align = type_align32(t, load.base_type); aa_emit_mem(a, 1, native_loc_reg(dst.type, NATIVE_REG_INT, base_reg), load, mem); aa_load_addr_from_base(a, rd, base_reg, addr.offset, &addr); @@ -3249,35 +3410,17 @@ static MemAccess aa_mem_for_type(NativeTarget* t, KitCgTypeId type, u32 size) { /* native_loc_reg / native_loc_stack are shared in native_target.h. */ -static NativeAddr aa_loc_addr(AANativeTarget* a, NativeLoc loc, u32 offset) { +static NativeAddr aa_storage_addr(AANativeTarget* a, NativeLoc loc, + u32 offset) { NativeAddr addr; - memset(&addr, 0, sizeof addr); - switch ((NativeLocKind)loc.kind) { - case NATIVE_LOC_FRAME: - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = loc.v.frame; - addr.base_type = loc.type; - addr.offset = (i32)offset; - return addr; - case NATIVE_LOC_STACK: - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = loc.v.stack.slot; - addr.base_type = loc.type; - addr.offset = loc.v.stack.offset + (i32)offset; - return addr; - case NATIVE_LOC_ADDR: - addr = loc.v.addr; - addr.offset += (i32)offset; - return addr; - default: - aa_panic(a, "location is not addressable"); - } + if (!native_loc_storage_addr(loc, (i32)offset, &addr)) + aa_panic(a, "location is not storage"); return addr; } static void aa_addr_of_loc(NativeTarget* t, NativeLoc dst, NativeLoc src) { AANativeTarget* a = aa_of(t); - NativeAddr addr = aa_loc_addr(a, src, 0); + NativeAddr addr = aa_storage_addr(a, src, 0); aa_load_addr(t, dst, addr); } @@ -3289,15 +3432,25 @@ static void aa_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src, aa_move(t, dst, src); return; } - if (src.kind == NATIVE_LOC_FRAME || src.kind == NATIVE_LOC_STACK || - src.kind == NATIVE_LOC_ADDR) { - NativeAddr addr = aa_loc_addr(a, src, offset); + if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_VALUE) { + NativeAddr addr; + if (offset != 0u || size != t->c->target.ptr_size || + !native_loc_address_value(src, &addr)) + aa_panic(a, "split or invalid address-value argument"); + aa_load_addr(t, dst, addr); + return; + } + if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_STORAGE) { + NativeAddr addr = aa_storage_addr(a, src, offset); addr.base_type = dst.type; aa_emit_mem(a, 1, dst, addr, mem); return; } if (src.kind == NATIVE_LOC_IMM) { - aa_emit_load_imm(t->mc, loc_is_64(t, dst), loc_reg(dst), src.v.imm); + i64 part; + if (!native_loc_imm_part(src, offset, size, &part)) + aa_panic(a, "invalid immediate argument part"); + aa_emit_load_imm(t->mc, loc_is_64(t, dst), loc_reg(dst), part); return; } aa_panic(a, "unsupported call argument source"); @@ -3307,9 +3460,8 @@ static void aa_store_part(NativeTarget* t, NativeLoc dst, NativeLoc src, u32 offset, u32 size) { AANativeTarget* a = aa_of(t); MemAccess mem = aa_mem_for_type(t, src.type, size); - if (dst.kind == NATIVE_LOC_FRAME || dst.kind == NATIVE_LOC_STACK || - dst.kind == NATIVE_LOC_ADDR) { - NativeAddr addr = aa_loc_addr(a, dst, offset); + if (native_loc_addr_role(dst) == NATIVE_LOC_ADDR_ROLE_STORAGE) { + NativeAddr addr = aa_storage_addr(a, dst, offset); addr.base_type = src.type; aa_emit_mem(a, 0, src, addr, mem); return; @@ -3338,6 +3490,24 @@ static void aa_store_outgoing_part(NativeTarget* t, int tail_call, aa_emit_mem(aa_of(t), 0, src, addr, mem); } +/* Copy only the bytes that belong to a stack-routed value. The caller owns the + * separately rounded ABI slot and advances its physical stack cursor itself; + * this helper never treats that carrier/padding size as readable source data. */ +static void aa_store_outgoing_value_bytes(NativeTarget* t, int tail_call, + u32 stack_off, NativeLoc src, + u32 value_size) { + NativeLoc tmp = + native_loc_reg(src.type, NATIVE_REG_INT, AA_TMP0); + u32 off = 0; + while (off < value_size) { + u32 chunk = value_size - off; + if (chunk > 8u) chunk = 8u; + aa_load_part(t, tmp, src, off, chunk); + aa_store_outgoing_part(t, tail_call, stack_off + off, tmp, chunk); + off += chunk; + } +} + static const ABIArgInfo* aa_param_abi(NativeTarget* t, const ABIFuncInfo* abi, const NativeCallDesc* desc, u32 i, ABIArgInfo* scratch) { @@ -3552,6 +3722,8 @@ static void aa_plan_call(NativeTarget* t, const NativeCallDesc* desc, NativeCallPlan* plan) { NativeCallPlanRet* rets; const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); + NativeLoc saved_indirect_callee; + int preserve_indirect_callee = 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). */ @@ -3568,17 +3740,19 @@ static void aa_plan_call(NativeTarget* t, const NativeCallDesc* desc, plan->is_variadic = abi && abi->variadic; plan->stack_arg_size = aa_call_stack_size(t, desc); native_frame_note_outgoing(&aa_of(t)->frame, plan->stack_arg_size); - /* Indirect call whose callee lives in x0..x7: the upcoming arg-load loop - * writes those same registers and would clobber the function pointer - * before blr reads it. Stash callee into AA_TMP0 (x16) up front and - * retarget the call. (AA_TMP0 is a backend scratch, never an arg reg.) */ + /* An indirect target in an argument/result or backend-private register can + * be overwritten while this hook fills x0..x7, x8 (sret), or uses x16/x17 + * for exact-width stack arguments. Preserve it in LR, whose incoming value + * is already frame-saved for a non-tail call and is dead on a tail path. + * After all argument work, move it to the final call/branch register. */ if (plan->callee.kind == NATIVE_LOC_REG && (NativeAllocClass)plan->callee.cls == NATIVE_REG_INT && - plan->callee.v.reg < 8u) { - NativeLoc scratch = - native_loc_reg(plan->callee.type, NATIVE_REG_INT, AA_TMP0); - aa_move(t, scratch, plan->callee); - plan->callee = scratch; + (plan->callee.v.reg <= AA_X8 || plan->callee.v.reg == AA_TMP0 || + plan->callee.v.reg == AA_TMP1)) { + saved_indirect_callee = + native_loc_reg(plan->callee.type, NATIVE_REG_INT, AA_LR); + aa_move(t, saved_indirect_callee, plan->callee); + preserve_indirect_callee = 1; } { u32 next_int = 0, next_fp = 0, stack = 0, nmoves = 0; @@ -3623,17 +3797,13 @@ static void aa_plan_call(NativeTarget* t, const NativeCallDesc* desc, continue; } { - /* Aggregate / multi-word variadic argument: copy its bytes in - * 8-byte chunks (each chunk's home bytes are all live). */ - NativeLoc tmpreg = - native_loc_reg(desc->args[i].type, NATIVE_REG_INT, AA_TMP0); - u32 off = 0; - while (off < n) { - u32 chunk = (n - off > 8u) ? 8u : (n - off); - aa_load_part(t, tmpreg, desc->args[i], off, chunk); - aa_store_outgoing_part(t, tail_call, stack + off, tmpreg, chunk); - off += chunk; - } + /* Aggregate / multi-word variadic argument: `n` is the physical ABI + * carrier (Apple rounds a 12-byte unnamed aggregate to 16), not a + * source access width. Copy the value's exact object bytes, leaving + * carrier padding untouched, then advance by the full slot. */ + u32 value_size = type_size32(t, desc->args[i].type); + aa_store_outgoing_value_bytes(t, tail_call, stack, desc->args[i], + value_size); stack += n; } continue; @@ -3696,6 +3866,13 @@ static void aa_plan_call(NativeTarget* t, const NativeCallDesc* desc, aa_addr_of_loc(t, x8, desc->results[0]); } } + if (preserve_indirect_callee) { + NativeLoc final_callee = native_loc_reg( + saved_indirect_callee.type, NATIVE_REG_INT, + tail_call ? AA_TAIL_TARGET : AA_TMP0); + aa_move(t, final_callee, saved_indirect_callee); + plan->callee = final_callee; + } } if (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) { u32 nr = 0, ni = 0, nf = 0; @@ -3818,7 +3995,7 @@ static void aa_plan_ret(NativeTarget* t, const CGFuncDesc* fd, dst_addr.base_kind = NATIVE_ADDR_BASE_REG; dst_addr.base.reg = AA_X8; dst_addr.base_type = value->type; - src_addr = aa_loc_addr(a, *value, 0); + src_addr = aa_storage_addr(a, *value, 0); src_addr.base_type = value->type; memset(&access, 0, sizeof access); access.type = value->type; @@ -3831,6 +4008,38 @@ static void aa_plan_ret(NativeTarget* t, const CGFuncDesc* fd, } if (value && abi && abi->ret.kind == ABI_ARG_DIRECT) { u32 ni = 0, nf = 0; + int exact_parts = 0; + for (u32 p = 0; p < abi->ret.nparts; ++p) { + const ABIArgPart* part = &abi->ret.parts[p]; + if (part->cls == ABI_CLASS_INT && part->size != 1u && + part->size != 2u && part->size != 4u && part->size != 8u) { + exact_parts = 1; + break; + } + } + + /* 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 + * 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 + * return registers remain live while later parts are loaded. */ + if (exact_parts) { + for (u32 p = 0; p < abi->ret.nparts; ++p) { + const ABIArgPart* part = &abi->ret.parts[p]; + NativeAllocClass cls = + part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT; + KitCgTypeId pty = aa_part_scalar_type(part); + Reg rreg = cls == NATIVE_REG_FP ? nf++ : ni++; + NativeLoc dst = native_loc_reg(pty, cls, rreg); + aa_load_part(t, dst, *value, part->src_offset, part->size); + } + *out_rets = NULL; + *out_nrets = 0; + return; + } + rets = arena_zarray(t->c->tu, NativeCallPlanRet, abi->ret.nparts); for (u32 p = 0; p < abi->ret.nparts; ++p) { const ABIArgPart* part = &abi->ret.parts[p]; @@ -4469,12 +4678,11 @@ static int aa_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, /* 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 x16, clobbering x0/x16/x17 and the link register; the JIT - * relaxation of that same sequence keeps the x0/x16/x17 footprint. Model - * the descriptor-model clobbers so a value live across a TLS access is not - * left in one of these registers. */ + * 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) | (1u << 16) | (1u << 17) | (1u << AA_LR); + mask[NATIVE_REG_INT] = 1u << 0; return 1; } if ((NativeMachineOpKind)op->kind != NATIVE_MOP_INTRINSIC || @@ -4497,33 +4705,34 @@ static void aa_bind_native_param(NativeTarget* t, const CGParamDesc* p, /* ABI argument/return registers (x0..x7) come FIRST so the -O0 value cache * prefers them: an argument expression then computes straight into its ABI arg * register and the per-call parallel-copy (native_arg_shuffle) collapses to - * no-ops — the tcc get_reg(0..N) trick (mirrors rv_int_allocable a0..a7 and + * no-ops — the tcc get_reg(0..N) trick (mirrors RV's NDT a0..a7 bank and * x64's leading rsi/rdi). They are caller-saved, so the deferred prologue stays * callee-save-free. Incoming params are spilled to frame homes at entry before * any body op (aa_bind_native_param), so caching in x0..x7 cannot clobber a * live parameter. Next come the remaining caller-saved temps x8/x12..x15; * callee-saved x19..x28 are appended and only chosen under register pressure, - * after which the prologue saves/restores them. x9/x10/x11 are the int emit - * scratch (aa_int_scratch) and stay out of this pool. */ -static const Reg aa_int_allocable[] = {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, - 8u, 12u, 13u, 14u, 15u, 19u, 20u, 21u, - 22u, 23u, 24u, 25u, 26u, 27u, 28u}; -/* Three int scratch registers, not two: a 3-operand op (e.g. `binop dst, a, b` - * or `store [base+index], value`) whose dst/sources all spill needs three - * distinct scratch regs at emit time — the IR spill-rewrite round-robins - * operands across this pool and the native emitter materializes each into one. - * With only two, an immediate operand of an all-spilled binop had nowhere to - * land. x9/x10/x11 are all caller-saved temporaries reserved out of the - * allocable set above. */ + * after which the prologue saves/restores them. x9/x10/x11 are the explicit O1 + * operand-temp bank (also the O0 scratch policy) and stay out of this pool. */ +static const Reg aa_ndt_int_allocable[] = { + 0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 12u, 13u, 14u, + 15u, 19u, 20u, 21u, 22u, 23u, 24u, 25u, 26u, 27u, 28u}; +/* Three O1 operand temporaries, not two: a 3-operand op (for example + * `binop dst, a, b` or `store [base+index], value`) can require three distinct + * materializations in one instruction scope. Location MIR leaves spilled + * operands in their frame homes until this point; the scope leases x9/x10/x11 + * without inventing persistent MIR register values. All three are caller-saved + * and reserved out of the allocable set above. The O0 direct target happens to + * 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}; /* 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 - * ndt_caller_saved_only. v16 is the fp emit scratch (the fp AA_TMP0, also the - * arg-shuffle fp cycle scratch) and v20/v21 are the NDT per-op fp scratch, so - * all three stay out of the pool. Callee-saved v8..v15 (AAPCS64) are appended + * ndt_caller_saved_only. v16 is a backend-private/arg-shuffle cycle temporary; + * v20/v21 are the O1 operand-temp bank and the O0 per-op scratch policy, so all + * three stay out of the pool. Callee-saved v8..v15 (AAPCS64) are appended * like the int set; the caller-saved-only NDT never selects them. */ -static const Reg aa_fp_allocable[] = { +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}; @@ -4637,10 +4846,16 @@ static const NativePhysRegInfo aa_fp_phys[] = { static const NativeAllocClassInfo aa_classes[] = { {.cls = NATIVE_REG_INT, - .allocable = aa_int_allocable, - .nallocable = sizeof aa_int_allocable / sizeof aa_int_allocable[0], + .ndt_allocable = aa_ndt_int_allocable, + .ndt_allocable_count = + sizeof aa_ndt_int_allocable / sizeof aa_ndt_int_allocable[0], .scratch = aa_int_scratch, .nscratch = sizeof aa_int_scratch / sizeof aa_int_scratch[0], + .emit_temps = aa_int_scratch, + .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], + .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, @@ -4650,10 +4865,14 @@ static const NativeAllocClassInfo aa_classes[] = { .reserved_mask = (1u << AA_TMP0) | (1u << AA_TMP1) | (1u << AA_FP) | (1u << AA_LR)}, {.cls = NATIVE_REG_FP, - .allocable = aa_fp_allocable, - .nallocable = sizeof aa_fp_allocable / sizeof aa_fp_allocable[0], + .ndt_allocable = aa_ndt_fp_allocable, + .ndt_allocable_count = + sizeof aa_ndt_fp_allocable / sizeof aa_ndt_fp_allocable[0], .scratch = aa_fp_scratch, .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], + .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 @@ -4747,7 +4966,7 @@ static const NativeRegInfo aa_reg_info = { /* aa_emit_mem emits ldrsb/ldrsh (opc=10, X form) for an MF_SEXT_LOAD narrow * integer load, filling the whole register — so the -O0 NDT may drop the * following CV_SEXT (Lever 4). */ - .ndt_load_sext = 1u, + .load_sext = 1u, .resolve_name = aa_resolve_name, .asm_operand_reg_ok = aa_asm_operand_reg_ok, .asm_constraint_reg = aa_asm_constraint_reg, @@ -4902,7 +5121,7 @@ static void aa_bind_native_param(NativeTarget* t, const CGParamDesc* p, src = native_loc_reg(p->type, cls, a->next_param_int++); } else { /* Stack-passed part: load straight into the dst register when possible, - * otherwise a scratch for the store-to-frame path. */ + * otherwise use a backend-private transfer register for the frame store. */ Reg tmp = reg_dst ? (Reg)dst.v.reg : (cls == NATIVE_REG_FP ? 16u : AA_TMP0); NativeAddr saddr; @@ -5033,11 +5252,10 @@ static void aa_load_ap_addr(NativeDirectTarget* d, Operand ap_addr, d->native->load_addr(d->native, dst, ap); } -/* The va cores use only non-allocable registers for their temporaries - * (scratch x9/x10, reserved x16=TMP0 / x17=TMP1, vector v16) so they never - * clobber a value the optimizer's register allocator may hold live across the - * op. The va_list base register is supplied by the caller (ap.base.reg), which - * the optimizer materializes into a safe register before the call. */ +/* The va cores use caller-provided operand registers plus backend-private + * x16/x17 and v16. Direct wrappers choose x9/x10 from the O0 scratch policy; + * the optimized emitter supplies an instruction-scoped operand location. None + * can alias a persistently allocated live value. */ static u32 aa_va_base_reg(AANativeTarget* a, NativeAddr ap) { if (ap.base_kind != NATIVE_ADDR_BASE_REG) compiler_panic(a->base.c, a->func ? a->func->loc : (SrcLoc){0, 0, 0}, @@ -5126,9 +5344,8 @@ static void aa_va_arg_core(AANativeTarget* a, NativeLoc dst, NativeAddr ap, ABIVaListInfo vai = abi_va_list_layout(t->c->abi); NativeLoc cur = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, AA_TMP0); - /* The fetched value is written directly into the caller-provided register - * `dst`, which the caller guarantees is distinct from the va_list base - * register. Only TMP0/TMP1 are used as private scratch. */ + /* The fetched value is written directly into caller-provided `dst`, which is + * distinct from the va_list base. Only TMP0/TMP1 are backend-private. */ NativeLoc val = dst; NativeAddr src; MemAccess ptr_mem = aa_mem_for_type(t, cur.type, 8); @@ -5223,8 +5440,8 @@ static void aa_va_copy_core(AANativeTarget* a, NativeAddr dst_ap, /* ---- Direct-path (NativeDirectTarget) wrappers: convert semantic operands to * NativeAddr/NativeLoc, then call the shared cores above. ---- */ -/* The cores reserve x16/x17 (TMP0/TMP1) as private scratch and require the - * va_list base register(s) to be distinct from those. aa_direct_pointer_addr +/* The cores reserve x16/x17 (TMP0/TMP1) as backend-private temporaries and + * require the va_list base register(s) to be distinct. aa_direct_pointer_addr * returns the pointer in TMP1, so the direct wrappers first relocate it into * x9/x10 before calling the cores. */ static NativeAddr aa_direct_va_base(NativeDirectTarget* d, Operand ap_addr, @@ -5547,10 +5764,10 @@ 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 the two emit scratch regs, x18 (platform reg), fp/lr/sp. */ - .scratch_int = (1u << AA_TMP0) | (1u << AA_TMP1) | (1u << 18u) | - (1u << AA_FP) | (1u << AA_LR) | (1u << AA_SP), - .scratch_fp = (1u << 20u) | (1u << 21u), + /* 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, @@ -5572,15 +5789,9 @@ static void aa_direct_asm_block(NativeDirectTarget* d, const char* tmpl, /* ---- NativeTarget (optimizer) asm hook ---- * - * The optimizer has already allocated every operand register and arranged the - * surrounding data flow (inputs are live in their registers on entry, outputs - * are consumed from their registers on exit; the asm's clobber_mask kept the - * allocator from holding live values in clobbered registers). So unlike the - * direct path this hook does NOT self-allocate registers and does NOT load - * inputs / store outputs -- it only binds the pre-allocated registers to the - * template, materializing memory-operand base addresses into the reserved - * scratch registers and saving/restoring callee-saved registers the asm - * clobbers (the only ABI obligation the allocator cannot discharge itself). */ + * The optimized emitter owns register placement, staging, and output + * writeback. This hook binds its concrete locations and only materializes + * memory-constraint bases in backend-private registers. */ static NativeAddr aa_asm_loc_to_addr(AANativeTarget* a, SrcLoc loc, NativeLoc src) { @@ -5610,8 +5821,8 @@ static NativeAddr aa_asm_loc_to_addr(AANativeTarget* a, SrcLoc loc, } /* Resolve a memory-constraint operand to a single base register with zero - * offset, folding any frame/global/offset into a scratch register. At most the - * two reserved scratch registers are used across one asm block. */ + * offset, folding any frame/global/offset into a backend-private register. At + * most x16/x17 are used across one asm block. */ static Reg aa_asm_native_mem_base(AANativeTarget* a, SrcLoc loc, NativeLoc src, u32* ntmp) { NativeAddr addr = aa_asm_loc_to_addr(a, loc, src); @@ -5620,7 +5831,7 @@ static Reg aa_asm_native_mem_base(AANativeTarget* a, SrcLoc loc, NativeLoc src, Reg dst; if (addr.index_kind != NATIVE_ADDR_INDEX_NONE) aa_asm_panic_at(a->base.c, loc, "indexed memory asm operand unsupported"); - aa_addr_base(a, addr, &base, &off); + aa_addr_base(a, addr, 0u, &base, &off); if (off == 0) return (Reg)base; if (*ntmp >= 2u) aa_asm_panic_at(a->base.c, loc, "too many memory asm operands"); @@ -5630,50 +5841,6 @@ static Reg aa_asm_native_mem_base(AANativeTarget* a, SrcLoc loc, NativeLoc src, return dst; } -static Reg aa_asm_stage_reg(AANativeTarget* a, SrcLoc loc, - NativeAllocClass cls, u32* nint, u32* nfp) { - static const Reg int_regs[] = {AA_TMP0, AA_TMP1}; - static const Reg fp_regs[] = {20u, 21u}; - if (cls == NATIVE_REG_FP) { - if (*nfp >= (u32)(sizeof fp_regs / sizeof fp_regs[0])) - aa_asm_panic_at(a->base.c, loc, "too many staged fp asm operands"); - return fp_regs[(*nfp)++]; - } - if (*nint >= (u32)(sizeof int_regs / sizeof int_regs[0])) - aa_asm_panic_at(a->base.c, loc, "too many staged integer asm operands"); - return int_regs[(*nint)++]; -} - -static void aa_asm_load_loc_to_reg(AANativeTarget* a, SrcLoc loc, NativeLoc src, - NativeLoc dst) { - NativeTarget* t = &a->base; - NativeAllocClass cls = (NativeAllocClass)dst.cls; - if (src.kind == NATIVE_LOC_REG) { - if (src.v.reg != dst.v.reg || src.cls != dst.cls) t->move(t, dst, src); - return; - } - if (src.kind == NATIVE_LOC_IMM) { - if (cls != NATIVE_REG_INT) - aa_asm_panic_at(t->c, loc, - "floating-point immediate asm input is unsupported"); - t->load_imm(t, dst, src.v.imm); - return; - } - aa_emit_mem(a, 1, dst, aa_asm_loc_to_addr(a, loc, src), - aa_mem_for_type(t, dst.type, loc_size32(t, dst))); -} - -static void aa_asm_store_reg_to_loc(AANativeTarget* a, SrcLoc loc, - NativeLoc dst, NativeLoc src) { - NativeTarget* t = &a->base; - if (dst.kind == NATIVE_LOC_REG) { - if (dst.v.reg != src.v.reg || dst.cls != src.cls) t->move(t, dst, src); - return; - } - aa_emit_mem(a, 0, src, aa_asm_loc_to_addr(a, loc, dst), - aa_mem_for_type(t, src.type, loc_size32(t, src))); -} - static void aa_asm_native_panic(NativeTarget* t, SrcLoc loc, const char* msg) { aa_asm_panic_at(t->c, loc, msg); } @@ -5683,23 +5850,6 @@ static Reg aa_asm_native_mem_base_hook(NativeTarget* t, SrcLoc loc, return aa_asm_native_mem_base(aa_of(t), loc, src, ntmp); } -static Reg aa_asm_native_stage_reg_hook(NativeTarget* t, SrcLoc loc, - NativeAllocClass cls, u32* ntmp, - u32* nstage_int, u32* nstage_fp) { - (void)nstage_int; - return aa_asm_stage_reg(aa_of(t), loc, cls, ntmp, nstage_fp); -} - -static void aa_asm_native_load_loc_hook(NativeTarget* t, SrcLoc loc, - NativeLoc src, NativeLoc dst) { - aa_asm_load_loc_to_reg(aa_of(t), loc, src, dst); -} - -static void aa_asm_native_store_loc_hook(NativeTarget* t, SrcLoc loc, - NativeLoc dst, NativeLoc src) { - aa_asm_store_reg_to_loc(aa_of(t), loc, dst, src); -} - static void aa_asm_native_run_template_hook( NativeTarget* t, const char* tmpl, const AsmConstraint* outs, u32 nout, Operand* bound_outs, const AsmConstraint* ins, u32 nin, Operand* bound_ins, @@ -5719,15 +5869,10 @@ static void aa_asm_block_native(NativeTarget* t, const char* tmpl, AANativeTarget* a = aa_of(t); SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; static const NativeAsmNativeHooks hooks = { - .opk_reg = AA64_INLINE_OPK_REG, - .opcls_fp = AA64_INLINE_OPCLS_FP, .panic = aa_asm_native_panic, .bound_reg = aa_asm_bound_reg, .bound_mem = aa_asm_bound_mem, .mem_base = aa_asm_native_mem_base_hook, - .stage_reg = aa_asm_native_stage_reg_hook, - .load_loc_to_reg = aa_asm_native_load_loc_hook, - .store_reg_to_loc = aa_asm_native_store_loc_hook, .run_template = aa_asm_native_run_template_hook, }; native_asm_bind_native_operands(t, loc, tmpl, outs, nout, out_locs, ins, nin, diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c @@ -15,9 +15,10 @@ * POP {r7, pc} ; restore r7 + return (Thumb bit from stacked lr) * * The NDT value-cache pool is the caller-saved arg registers r0..r3 (Lever 1: - * args materialize directly into their ABI registers); r12 (IP) is the emit - * scratch; r7 is the frame pointer. Callee-saved r4..r11 are reserved from the - * -O0 pool (the optimizer allocates over them at -O1). */ + * args materialize directly into their ABI registers); r12 (IP) is its O0 + * scratch and a backend-private hook temporary; r7 is the frame pointer. + * Callee-saved r4..r11 are reserved from the -O0 pool (the optimizer allocates + * over them at -O1). */ #include <string.h> #include "abi/abi.h" @@ -80,9 +81,9 @@ typedef struct Arm32NativeTarget { u8 is_variadic; NativeFrameSlot sret_ptr_slot; /* Lazily-allocated 4-byte home for an indirect call target when the call has - * outgoing stack args. At -O0 the two emit scratch (IP, LR) are both consumed - * staging stack args (IP = arg value, LR = large-offset address), so the callee - * cannot survive in a scratch register across staging: it is spilled here and + * outgoing stack args. At -O0 the two backend staging registers (IP for the + * value, LR for a large-offset address) may both be consumed, so the callee + * cannot remain in either register across staging: it is spilled here and * reloaded just before BLX. NATIVE_FRAME_SLOT_NONE until first needed. */ NativeFrameSlot callee_spill_slot; u32 func_start; @@ -212,14 +213,14 @@ static void arm_emit_pop(MCEmitter* mc, u32 reglist) { * (Lever 1 via api_pack_call_args_in_order). The -O0 cache flushes at every * call/branch/return, so caller-saved suffices and the prologue needs no * callee-save spills. */ -static const Reg arm_int_allocable[] = {0u, 1u, 2u, 3u}; +static const Reg arm_ndt_int_allocable[] = {0u, 1u, 2u, 3u}; static const Reg arm_int_scratch[] = {ARM_SCRATCH}; -/* The -O1 optimizer's native-emit pass needs two scratch registers (e.g. to - * store a spilled value through a spilled-pointer address). The -O0 NDT must NOT - * take LR as an operand scratch (LR is the backend's dedicated div/staging temp, - * and arm_emit_global_addr/copy_bytes use it), so LR is exposed only here, on the - * optimizer path, where operands are allocated registers (never LR). */ -static const Reg arm_int_opt_scratch[] = {ARM_SCRATCH, ARM_TMP}; +/* O1 operand temporaries are the caller-saved low registers. They are excluded + * from persistent allocation and every Thumb2 operand hook accepts them. IP + * and LR remain backend-private: many hooks use those two internally, so + * 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 NativePhysRegInfo arm_int_phys[] = { ARM_PHYS_ARG(0u, 0u), ARM_PHYS_ARG(1u, 1u), @@ -228,7 +229,7 @@ static const NativePhysRegInfo arm_int_phys[] = { ARM_PHYS_CALLEE(6u), ARM_PHYS_RESERVED(7u), /* fp */ ARM_PHYS_CALLEE(8u), ARM_PHYS_CALLEE(9u), ARM_PHYS_CALLEE(10u), ARM_PHYS_CALLEE(11u), - ARM_PHYS_RESERVED(12u), /* ip = scratch */ + ARM_PHYS_RESERVED(12u), /* ip = O0/backend temp */ ARM_PHYS_RESERVED(13u), /* sp */ ARM_PHYS_RESERVED(14u), /* lr */ ARM_PHYS_RESERVED(15u), /* pc */ @@ -278,12 +279,16 @@ static int arm_asm_constraint_reg(const NativeRegInfo* ri, const char* body, static const NativeAllocClassInfo arm_classes[] = { {.cls = NATIVE_REG_INT, - .allocable = arm_int_allocable, - .nallocable = sizeof arm_int_allocable / sizeof arm_int_allocable[0], + .ndt_allocable = arm_ndt_int_allocable, + .ndt_allocable_count = + sizeof arm_ndt_int_allocable / sizeof arm_ndt_int_allocable[0], .scratch = arm_int_scratch, .nscratch = sizeof arm_int_scratch / sizeof arm_int_scratch[0], - .opt_scratch = arm_int_opt_scratch, - .opt_nscratch = sizeof arm_int_opt_scratch / sizeof arm_int_opt_scratch[0], + .emit_temps = arm_int_emit_temps, + .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], + .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) */ @@ -377,6 +382,7 @@ static int arm_addr_legal(NativeTarget* t, const NativeAddr* addr, (void)mem; if (!addr) return 0; /* Base + 12-bit positive or 8-bit signed offset, no scaled index in v1. */ + if (addr->base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) return 0; if (addr->index_kind != NATIVE_ADDR_INDEX_NONE) return 0; return addr->offset >= -255 && addr->offset <= 4095; } @@ -477,17 +483,19 @@ static void arm_emit_global_addr(Arm32NativeTarget* a, u32 rd, ObjSymId sym, /* The unified memory-access address layer (ARM32.md §1) lives here. Every * load/store/copy resolves a NativeAddr to a legal (base register, in-range - * displacement) through ONE function, arm_resolve_mem, with ONE scratch- + * displacement) through ONE function, arm_resolve_mem, with ONE temporary- * reservation invariant asserted in one place. * - * Scratch budget: two registers are reserved as emit scratch in the function - * body — IP (r12, ARM_SCRATCH) and LR (r14, ARM_TMP); r7 (FP) holds the frame - * anchor and is never a scratch. A single access needs at most one - * address-staging scratch, distinct from the transfer register `rt`. The picker + * Backend budget: IP (r12, ARM_SCRATCH) and LR (r14, ARM_TMP) are reserved for + * hook-internal staging; they are deliberately not O1 operand locations. r7 + * (FP) holds the frame anchor and is never temporary storage. A single access + * needs at most one address-staging register, distinct from transfer register + * `rt`. The picker * draws it from {IP,LR} minus {rt} minus a caller `pinned` mask (held bases the * access must preserve) and panics if none is free, so the invariant is explicit * rather than relying on incidental liveness. */ static void arm_emit_base_off(Arm32NativeTarget* a, u32 rd, u32 base, i32 off); +static void arm_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr); /* True if every byte of a `span`-byte access at signed displacement `off` fits * the Thumb-2 single-granule immediate forms (T4 negative -255..0, T3 positive @@ -684,6 +692,7 @@ static void arm_emit_mem(Arm32NativeTarget* a, int is_load, NativeLoc reg, NativeAddr addr, MemAccess mem) { u32 rt = loc_reg(reg); u32 size = mem.size ? mem.size : native_type_size(&a->base, reg.type); + if (size == 0u) arm_panic(a, "zero-width memory access"); if (size > 8u) arm_panic(a, "memory access wider than 8 bytes"); if (size == 8u) { u32 hi = (rt + 1u) & 0xfu; @@ -693,6 +702,63 @@ static void arm_emit_mem(Arm32NativeTarget* a, int is_load, NativeLoc reg, arm_emit_mem_one(a, is_load, hi, r.base, r.off + 4, 4u); return; } + if (size != 1u && size != 2u && size != 4u) { + NativePartChunkIter it = native_part_chunks(size, 4u); + KitCgTypeId lane_type = builtin_id(KIT_CG_BUILTIN_I32); + u32 carrier_mask = 1u << rt; + u32 addr_inputs = 0u; + u32 chunk_off, chunk_size; + u32 tmp = REG_NONE; + ArmMemAddr r; + if (size > 4u) carrier_mask |= 1u << ((rt + 1u) & 0xfu); + if (addr.base_kind == NATIVE_ADDR_BASE_REG) + addr_inputs |= 1u << (addr.base.reg & 0xfu); + if (addr.index_kind == NATIVE_ADDR_INDEX_REG) + addr_inputs |= 1u << (addr.index.reg & 0xfu); + if (is_load && (carrier_mask & addr_inputs)) { + /* A single LDR may use the destination as its base (`ldr r0, [r0]`). + * An exact part is a sequence, so doing that would destroy the address + * before its later chunks. Retain the complete effective address in a + * private register first; avoid every address input because load_addr + * still needs them while it constructs the indexed form. */ + u32 addr_reg = arm_pick_scratch(a, 0xffu, carrier_mask | addr_inputs); + NativeLoc addr_loc = + native_loc_reg(lane_type, NATIVE_REG_INT, (Reg)addr_reg); + arm_load_addr(&a->base, addr_loc, addr); + r.base = addr_reg; + r.off = 0; + } else { + /* Resolve once for the full span. Besides avoiding repeated global/far + * address formation, this gives every chunk one stable base and makes + * scratch ownership explicit: a later pack temporary must avoid r.base. */ + r = arm_resolve_mem(a, &addr, 0xffu, size, carrier_mask); + } + /* ABIArgPart.size is exact even when the carrier is a 32-bit register. + * Split 3/5/6/7-byte parts into bounded 4/2/1-byte accesses and pack each + * chunk into the appropriate low/high carrier lane. This boundary is also + * used by generic call-result moves, so no caller can silently widen a + * semantic part into an adjacent frame object. */ + while (native_part_chunk_next(&it, &chunk_off, &chunk_size)) { + u32 lane_reg = (rt + chunk_off / 4u) & 0xfu; + u32 lane_shift = (chunk_off & 3u) * 8u; + u32 transfer_reg = lane_reg; + if (lane_shift) { + if (tmp == REG_NONE) + tmp = arm_pick_scratch(a, 0xffu, + carrier_mask | (1u << (r.base & 0xfu))); + transfer_reg = tmp; + if (!is_load) + arm_emit_t32(a->base.mc, + arm_shift_imm(1u, tmp, lane_reg, lane_shift)); + } + arm_emit_mem_one(a, is_load, transfer_reg, r.base, + r.off + (i32)chunk_off, chunk_size); + if (is_load && lane_shift) + arm_emit_t32(a->base.mc, + arm_orr_reg_lsl(lane_reg, lane_reg, tmp, lane_shift)); + } + return; + } { ArmMemAddr r = arm_resolve_mem(a, &addr, rt, size, 0u); arm_emit_mem_one(a, is_load, rt, r.base, r.off, size); @@ -775,7 +841,7 @@ static void arm_emit_base_off(Arm32NativeTarget* a, u32 rd, u32 base, i32 off) { * base (the resolver guarantees a staged base reg differs from its source); * only when rd aliases base (load_addr's address-of-an-indexed-REG case) * fall back to the reserved ARM_TMP, which is dead in that context. */ - u32 tmp = (rd != base) ? rd : ARM_TMP; + u32 tmp = (rd != base) ? rd : arm_pick_scratch(a, 0xffu, 1u << base); arm_emit_load_u32(a, tmp, (u32)(off < 0 ? -off : off)); arm_emit_t32(mc, off < 0 ? arm_sub_reg(rd, base, tmp) : arm_add_reg(rd, base, tmp)); @@ -800,6 +866,26 @@ static void arm_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) { } else if (addr.base_kind == NATIVE_ADDR_BASE_FRAME) { NativeFrameSlotEntry* s = native_frame_slot_at(&a->frame, addr.base.frame); arm_emit_base_off(a, rd, ARM_FP, -(i32)s->off + addr.offset); + } else if (addr.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) { + NativeAddr load; + NativeLoc value; + MemAccess mem; + if (!addr.base_type) + arm_panic(a, "frame-value base has no exact type"); + memset(&load, 0, sizeof load); + load.base_kind = NATIVE_ADDR_BASE_FRAME; + load.base.frame = addr.base.frame; + load.base_type = addr.base_type; + memset(&value, 0, sizeof value); + value.kind = NATIVE_LOC_REG; + value.cls = NATIVE_REG_INT; + value.type = addr.base_type; + value.v.reg = rd; + memset(&mem, 0, sizeof mem); + mem.type = value.type; + mem = native_mem_for_type(t, value.type, 0); + arm_emit_mem(a, 1, value, load, mem); + arm_emit_base_off(a, rd, rd, addr.offset); } else if (addr.base_kind == NATIVE_ADDR_BASE_REG) { arm_emit_base_off(a, rd, addr.base.reg & 0xfu, addr.offset); } else { @@ -808,6 +894,27 @@ static void arm_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) { if (addr.index_kind == NATIVE_ADDR_INDEX_REG) { u32 idx = addr.index.reg & 0xfu; arm_emit_t32(mc, arm_add_reg_lsl(rd, rd, idx, addr.log2_scale)); + } else if (addr.index_kind == NATIVE_ADDR_INDEX_FRAME_VALUE) { + NativeAddr load; + NativeLoc value; + MemAccess mem; + u32 idx = arm_pick_scratch(a, rd, 1u << rd); + if (!addr.index_type) + arm_panic(a, "frame-value index has no exact type"); + memset(&load, 0, sizeof load); + load.base_kind = NATIVE_ADDR_BASE_FRAME; + load.base.frame = addr.index.frame; + load.base_type = addr.index_type; + memset(&value, 0, sizeof value); + value.kind = NATIVE_LOC_REG; + value.cls = NATIVE_REG_INT; + value.type = addr.index_type; + value.v.reg = idx; + memset(&mem, 0, sizeof mem); + mem.type = value.type; + mem = native_mem_for_type(t, value.type, 0); + arm_emit_mem(a, 1, value, load, mem); + arm_emit_t32(mc, arm_add_reg_lsl(rd, rd, idx, addr.log2_scale)); } else if (addr.index_kind != NATIVE_ADDR_INDEX_NONE) { arm_panic(a, "unsupported address index kind in load_addr"); } @@ -1434,15 +1541,16 @@ static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, * frameless leaf): a leaf with no callee-saves, no body slots, no outgoing * args, no sret/variadic and only register params needs no frame record, no r7 * anchor and no SUB sp. It cannot, however, return through a bare `BX lr`: - * arm32 is register-starved and uses lr (ARM_TMP) pervasively as an emit-time - * scratch — global-address staging in arm_resolve_mem, the `__builtin_*_overflow` - * sequences, popcount, bitfield insert, the byte-copy granule engine — any of - * which a leaf body may hit (e.g. `static int g; return g;` stages &g into lr, + * arm32 is register-starved and uses LR (ARM_TMP) pervasively as a + * backend-private temporary — global-address staging in arm_resolve_mem, + * `__builtin_*_overflow` sequences, popcount, bitfield insert, and the + * byte-copy granule engine. A leaf body may hit any of these (e.g. + * `static int g; return g;` stages &g into LR, * after which a bare `BX lr` would branch to &g). So the tier still saves and * restores the return address with a single-register `PUSH {lr}` / `POP {pc}`, * leaving the body free to clobber lr; it just skips the r7 frame record and * the stack reservation. (aa64/rv64 keep their return-address register out of - * the scratch pool, so their frameless leaves skip even this push; arm32 + * the backend temporary bank, so their frameless leaves skip even this push; arm32 * cannot.) Inline asm cannot occur here (asm_block is not installed). */ a->slim_prologue = kf && kf->is_leaf && !kf->reads_frame && cs_mask == 0 && !a->frame.has_alloca && a->frame.cum_off == 0 && @@ -1513,8 +1621,8 @@ static void arm_func_end(NativeTarget* t) { mc_label_place(mc, a->epilogue_label); if (a->slim_prologue) /* Minimal leaf: only lr was saved (PUSH {lr}); pop it straight into pc. This - * restores the return address the body may have clobbered using lr as an - * emit scratch — see the func_begin slim_prologue note. */ + * restores the return address the body may have clobbered using LR as a + * backend-private temporary — see the func_begin slim_prologue note. */ arm_emit_pop(mc, 1u << 15u); /* POP {pc} (16-bit, 0xbd00) */ else arm_emit_frame_restore(a, /*to_pc=*/1); @@ -1824,26 +1932,30 @@ static u32 arm_call_stack_bytes(NativeTarget* t, const NativeCallDesc* desc) { static void arm_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src, u32 src_offset, u32 size) { + NativeAddr addr; /* Load `size` bytes at src_offset of `src` into register `dst`. */ if (src.kind == NATIVE_LOC_REG) { arm_move(t, dst, src); return; } + if (src.kind == NATIVE_LOC_IMM) { + i64 part; + if (!native_loc_imm_part(src, src_offset, size, &part)) + arm_panic(arm_of(t), "invalid immediate argument part"); + arm_load_imm(t, dst, part); + return; + } + if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_VALUE) { + if (src_offset != 0u || size != t->c->target.ptr_size || + !native_loc_address_value(src, &addr)) + arm_panic(arm_of(t), "split or invalid address-value argument"); + arm_load_addr(t, dst, addr); + return; + } { - NativeAddr addr; MemAccess mem; - memset(&addr, 0, sizeof addr); - if (src.kind == NATIVE_LOC_FRAME) { - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = src.v.frame; - addr.offset = (i32)src_offset; - } else if (src.kind == NATIVE_LOC_STACK) { - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = src.v.stack.slot; - addr.offset = src.v.stack.offset + (i32)src_offset; - } else { + if (!native_loc_storage_addr(src, (i32)src_offset, &addr)) arm_panic(arm_of(t), "unsupported arg source location"); - } addr.base_type = dst.type; memset(&mem, 0, sizeof mem); mem.type = dst.type; @@ -1877,17 +1989,8 @@ static void arm_store_outgoing(NativeTarget* t, u32 stack_off, NativeLoc src, * to pass a large INDIRECT aggregate argument by reference). */ static void arm_addr_of_loc(NativeTarget* t, NativeLoc dst, NativeLoc src) { NativeAddr addr; - memset(&addr, 0, sizeof addr); - if (src.kind == NATIVE_LOC_FRAME) { - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = src.v.frame; - } else if (src.kind == NATIVE_LOC_STACK) { - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = src.v.stack.slot; - addr.offset = src.v.stack.offset; - } else { + if (!native_loc_storage_addr(src, 0, &addr)) arm_panic(arm_of(t), "address-of non-memory location"); - } addr.base_type = dst.type; arm_load_addr(t, dst, addr); } @@ -1920,6 +2023,7 @@ static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, ? 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; plan->callee = desc->callee; @@ -1946,12 +2050,12 @@ static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, * register (r4..r11, or the chosen staging reg) needs no move. * * The exception is the -O0 (single-pass) call with outgoing stack args: staging - * those args consumes BOTH emit scratch (IP for the value, LR for a large-offset - * store address), so the callee cannot survive in LR. Spill it to its frame - * home now — before any arg move clobbers its register — and reload just before - * BLX (arm_emit_call). The known-frame (-O1) path keeps the register form: its - * optimizer-allocated callee already survives, and its frame is final (no body- - * time slot allocation). */ + * those args may consume BOTH backend temporaries (IP for the value, LR for a + * large-offset store address), so the callee cannot survive in LR. Spill it + * to its frame home now — before any arg move clobbers its register — and + * reload just before BLX (arm_emit_call). The known-frame (-O1) path keeps + * the register form: its optimizer-allocated callee already survives, and + * its frame is final (no body-time slot allocation). */ if (plan->callee.kind == NATIVE_LOC_REG && (NativeAllocClass)plan->callee.cls == NATIVE_REG_INT) { u32 cr = plan->callee.v.reg & 0xfu; @@ -1976,6 +2080,22 @@ static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, } } } + + /* An indirect sibling call lives in IP across argument marshalling because + * r0-r11 are restored by the tail epilogue and LR is the stack-part carrier. + * A three-byte part needs both private registers: LR carries the part while + * IP packs its final byte (and a far outgoing address can need IP as its + * resolved base). Preserve the callee with one balanced temporary stack save + * for the entire no-call marshalling interval, making both private registers + * available to the exact-width boundary. Tail stack arguments are addressed + * through FP into the incoming window, never through the temporarily shifted + * SP; CFA is FP-anchored. There are no compiler-time exits between this PUSH + * and its matching POP below. */ + if (tail && plan->callee.kind == NATIVE_LOC_REG && + loc_reg(plan->callee) == ARM_SCRATCH) { + arm_emit_push(t->mc, 1u << ARM_SCRATCH); + tail_callee_saved = 1; + } { u32 next_int = (abi && abi->has_sret) ? 1u : 0u; u32 stack = 0, nmoves = 0, i, p; @@ -2026,12 +2146,10 @@ static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, else stack = align_up_u32(stack, 4u); if (desc->args[i].kind == NATIVE_LOC_REG) { /* Source already in a register: store it straight to the outgoing - * slot. Staging through the arg scratch (ip) would clobber a live - * value the optimizer parked there — under register pressure it - * spills/rematerializes arg values into the opt scratch regs (ip/lr, - * see pass_lower scratch_for), so a register arg whose source is ip - * was lost when an earlier stack arg's staging overwrote it (the - * many-int variadic O1 miscompile). */ + * slot. Routing it through backend-private IP is unnecessary and + * would add an independent lifetime while other call operands are + * still leased. Keeping the source location intact also preserves + * the instruction-scoped ownership contract. */ arm_store_outgoing(t, stack, desc->args[i], part->size, tail); } else { NativeLoc tmp = native_loc_reg(lty, NATIVE_REG_INT, arg_stage); @@ -2058,6 +2176,7 @@ static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, } } } + if (tail_callee_saved) arm_emit_pop(t->mc, 1u << ARM_SCRATCH); if (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) { u32 nr = 0, ni = 0, p; for (p = 0; p < abi->ret.nparts; ++p) { @@ -2122,7 +2241,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 emit scratch for arg staging. Arg staging is + * (arm_plan_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; @@ -2183,6 +2302,37 @@ static void arm_plan_ret(NativeTarget* t, const CGFuncDesc* fd, } if (value && abi && abi->ret.kind == ABI_ARG_DIRECT) { u32 ni = 0, p; + int exact_parts = 0; + for (p = 0; p < abi->ret.nparts; ++p) { + const ABIArgPart* part = &abi->ret.parts[p]; + if (part->size != 1u && part->size != 2u && part->size != 4u && + part->size != 8u) { + exact_parts = 1; + break; + } + } + + /* A return-plan register destination makes the generic write phase + * materialize its source using the scalar carrier type. For a three-byte + * AAPCS aggregate tail that carrier is I32, so the exact mem.size=3 on the + * final move arrives too late: materialization has already read four bytes. + * Future 5/6/7-byte parts have the same mismatch. Once any such part is + * present, fill every return register here in ABI order through the shared + * exact-width argument boundary. arm_load_part uses only backend-private + * temporaries, preserving return registers populated by earlier parts. */ + if (exact_parts) { + for (p = 0; p < abi->ret.nparts; ++p) { + const ABIArgPart* part = &abi->ret.parts[p]; + KitCgTypeId pty = arm_part_scalar_type(part); + NativeLoc dst = + native_loc_reg(pty, NATIVE_REG_INT, (Reg)(ni++)); + arm_load_part(t, dst, *value, part->src_offset, part->size); + } + *out_rets = NULL; + *out_nrets = 0; + return; + } + for (p = 0; p < abi->ret.nparts; ++p) { const ABIArgPart* part = &abi->ret.parts[p]; /* Per-lane type: an i64/double return is two i32 lanes in r0:r1; sizing @@ -2533,11 +2683,12 @@ static void arm_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size, * routes 8-byte _Atomic to the spinlock libcall (atomic_lock_free_max=4), so * only <=4-byte ops reach here. * - * Register budget: lr (ARM_TMP) holds the access ADDRESS. The NDT keeps the - * value operands in the r0..r3 pool OR in ip (ARM_SCRATCH, its materialization - * scratch), so the atomic sequences must NOT use ip as a temp — they would - * clobber an operand that happens to live there. STREX additionally needs its - * status, value and base registers mutually distinct. + * Register budget: LR (ARM_TMP) holds the access address. On the shared O0 + * path, a value operand may occupy IP (ARM_SCRATCH); on O1, operands arrive in + * allocated r4..r11 or instruction-scoped r0..r3 locations and IP/LR remain + * backend-private. The core must therefore keep IP unavailable for its own + * extra state so it is safe on both paths. STREX additionally needs its status, + * value and base registers mutually distinct. * * The rmw/cas loops therefore borrow callee-saved registers for the private * temporaries (new value + STREX status). At -O1 the optimizer freely allocates @@ -2545,8 +2696,8 @@ static void arm_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size, * across the whole callee-saved set, so a fixed r4/r5 would alias an operand. The * borrow is therefore OPERAND-AWARE (arm_atomic_borrow): it picks free callee- * saved registers clear of the live operand+base set, guaranteeing base + all - * operands + the STREX temporaries are mutually distinct (ARM32.md §2 "operand- - * aware scratch budget"). The picked registers are push/pop-balanced so the + * operands + the STREX temporaries are mutually distinct. The picked + * registers are push/pop-balanced so the * caller's callee-saved values are preserved (free at -O0, safe at -O1). */ /* Pick `n` distinct callee-saved temporaries (r4,r5,r6,r8,r9,r10,r11 — fp/r7 is @@ -2583,10 +2734,9 @@ static int arm_order_release(KitCgMemOrder o) { * the LDREX/STREX sequence then uses as the base. */ static u32 arm_atomic_addr_reg(Arm32NativeTarget* a, NativeAddr addr) { /* Use the pointer's own register when the address already is one (the -O1 - * case: the optimizer collapses &x into a register distinct from the atomic - * value operands). Materializing into LR would collide with a value operand - * the optimizer staged in LR (an emit scratch). Otherwise (a FRAME/GLOBAL - * address, e.g. -O0) materialize into LR, which holds no operand there. */ + * case: location MIR keeps the allocated address component explicit). + * Otherwise (a FRAME/GLOBAL address, e.g. -O0) materialize into + * backend-private LR, which holds no operand there. */ if (addr.base_kind == NATIVE_ADDR_BASE_REG && addr.offset == 0 && addr.index_kind == NATIVE_ADDR_INDEX_NONE) return addr.base.reg & 0xfu; @@ -2847,9 +2997,10 @@ static void arm_va_arg_core(Arm32NativeTarget* a, NativeLoc dst, NativeAddr ap, MCEmitter* mc = t->mc; KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32); u32 sz = native_type_size(t, type); - /* The cursor must survive the value load (dst = [cursor]) and the +4 update, so - * it cannot alias `dst` — at -O1 the result is staged in a reserved scratch - * (IP, or LR), so pick the other reserved reg for the cursor. */ + /* The cursor must survive the value load (dst = [cursor]) and the +4 update, + * so it cannot alias `dst`. O1 destinations come from the scoped r0..r3 bank; + * the O0 direct path may use IP, so choose LR only for that case and IP + * otherwise. */ u32 cur_reg = (dst.kind == NATIVE_LOC_REG && loc_reg(dst) == ARM_SCRATCH) ? ARM_TMP : ARM_SCRATCH; @@ -2891,7 +3042,8 @@ static NativeAddr arm_va_addr_from_ptr(NativeLoc ap_ptr) { /* The destination memory of a value-location: a wide (i64/double) va_arg result * is memory-resident, so the optimizer hands its storage (a frame/stack slot, or * an explicit address), NOT a pointer in a register. Convert it to a NativeAddr - * the byte-copy can write through. (Mirrors riscv's rv_loc_addr.) */ + * the byte-copy can write through. (Mirrors RISC-V's storage-address + * resolver.) */ static NativeAddr arm_loc_addr(Arm32NativeTarget* a, NativeLoc loc) { NativeAddr addr; memset(&addr, 0, sizeof addr); @@ -2950,9 +3102,9 @@ static void arm_emit_setcc(MCEmitter* mc, u32 rd, u32 cc) { /* The 32-bit `__builtin_*_overflow` family. dsts = [value, overflow]; args = * [a, b]. i64 overflow is lowered to lane ops by the cg layer (arith.c), so the - * backend only sees the 32-bit forms. lr/ip are the transient scratch (lr is - * reserved; ip is the materialization scratch, free here since the operands are - * already in their NDT registers). */ + * backend only sees the 32-bit forms. LR/IP are backend-private temporaries on + * O1; on O0, IP's operand value has already been consumed before these internal + * sequences use it. */ /* nd_intrinsic forwards an immediate argument as NATIVE_LOC_IMM (not in a * register). A value-consuming intrinsic needs it in a register: materialize it * into `into` (an output register, free to reuse as an input) and return the reg @@ -3080,7 +3232,7 @@ static void arm_intrinsic(NativeTarget* t, IntrinKind kind, u32 width = native_type_size(t, dsts[0].type); NativeLoc a0 = arm_intrin_arg(a, args[0], loc_reg(dsts[0])); /* Use the 32-bit REV.W/REV16.W forms: the dst may be a high register - * (the IP codegen scratch at -O0, r8..r11 at -O1) that the 16-bit T1 + * (IP at -O0 or r4..r11 when allocated at -O1) that the 16-bit T1 * encoding cannot represent. */ if (width == 4u) { arm_emit_t32(mc, arm_rev_w(loc_reg(dsts[0]), loc_reg(a0))); @@ -3360,8 +3512,8 @@ static void arm_direct_load_operand_to_reg(NativeDirectTarget* d, Operand op, static void arm_direct_load_address_to_reg(NativeDirectTarget* d, Operand op, NativeLoc dst) { /* For an "m" memory operand: a LOCAL's address is its frame home; an INDIRECT - * (*(base+ofs)) names an address held in a frame slot, so materialize_addr - * loads the pointer first (load_addr has no FRAME_VALUE base form). */ + * (*(base+ofs)) names an address held in a frame slot; load_addr materializes + * that typed FRAME_VALUE through backend-owned temporaries. */ d->native->load_addr(d->native, dst, arm_direct_materialize_addr(d, op)); } static void arm_direct_store_reg_to_operand(NativeDirectTarget* d, Operand op, @@ -3460,10 +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 ip (scratch), fp, sp, lr, pc from the operand allocator. */ - .scratch_int = (1u << ARM_SCRATCH) | (1u << ARM_FP) | (1u << 13u) | - (1u << 14u) | (1u << 15u), - .scratch_fp = 0u, + /* 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, @@ -3483,9 +3636,7 @@ static void arm_direct_asm_block(NativeDirectTarget* d, const char* tmpl, &hooks); } -/* Convert an inline-asm input location — a computed address, or a frame/stack - * home — to a NativeAddr. Shared by the "m" (bind the address) and the staged - * register (load the value) input paths. */ +/* Convert a concrete memory-constraint location to a NativeAddr. */ static NativeAddr arm_asm_in_loc_addr(NativeTarget* t, NativeLoc loc, KitCgTypeId type) { NativeAddr ma; @@ -3504,20 +3655,19 @@ static NativeAddr arm_asm_in_loc_addr(NativeTarget* t, NativeLoc loc, return ma; } -/* Reserve the next scratch register to stage an inline-asm operand into: IP (the - * operand scratch) first, then LR. The native-emit asm path runs with both free - * (the optimizer allocates asm operands to r0..r11 and never to IP/LR), so two - * staged operands are available — enough for the operand shapes the corpus - * exercises. A third staged operand is a clean diagnostic rather than a clobber. */ -static Reg arm_asm_stage_reg(NativeTarget* t, u32* nstage) { +/* Reserve the next backend-private memory-base register: IP first, then LR. + * The optimized emitter never presents IP/LR as asm operands, so both are free + * here. */ +static Reg arm_asm_mem_base_reg(NativeTarget* t, u32* nbase) { Reg r; - if (*nstage == 0u) + if (*nbase == 0u) r = (Reg)ARM_SCRATCH; - else if (*nstage == 1u) + else if (*nbase == 1u) r = (Reg)ARM_TMP; else - arm_asm_panic_at(t->c, arm_of(t)->loc, "too many staged inline-asm operands"); - (*nstage)++; + arm_asm_panic_at(t->c, arm_of(t)->loc, + "too many memory inline-asm operands"); + (*nbase)++; return r; } @@ -3528,7 +3678,7 @@ static void arm_asm_native_panic(NativeTarget* t, SrcLoc loc, static Reg arm_asm_native_mem_base_hook(NativeTarget* t, SrcLoc loc, NativeLoc src, u32* ntmp) { - Reg base = arm_asm_stage_reg(t, ntmp); + Reg base = arm_asm_mem_base_reg(t, ntmp); NativeAddr ma = arm_asm_in_loc_addr(t, src, src.type); (void)loc; arm_load_addr( @@ -3537,50 +3687,6 @@ static Reg arm_asm_native_mem_base_hook(NativeTarget* t, SrcLoc loc, return base; } -static Reg arm_asm_native_stage_reg_hook(NativeTarget* t, SrcLoc loc, - NativeAllocClass cls, u32* ntmp, - u32* nstage_int, u32* nstage_fp) { - (void)loc; - (void)nstage_int; - (void)nstage_fp; - if (cls != NATIVE_REG_INT) - arm_asm_panic_at(t->c, arm_of(t)->loc, - "floating-point asm operands are unsupported"); - return arm_asm_stage_reg(t, ntmp); -} - -static void arm_asm_native_load_loc_hook(NativeTarget* t, SrcLoc loc, - NativeLoc src, NativeLoc dst) { - (void)loc; - if (dst.cls != NATIVE_REG_INT) - arm_asm_panic_at(t->c, arm_of(t)->loc, - "floating-point asm operands are unsupported"); - if (src.kind == NATIVE_LOC_REG) { - if (src.v.reg != dst.v.reg || src.cls != dst.cls) arm_move(t, dst, src); - return; - } - if (src.kind == NATIVE_LOC_IMM) { - arm_load_imm(t, dst, src.v.imm); - return; - } - arm_load(t, dst, arm_asm_in_loc_addr(t, src, dst.type), - native_mem_for_type(t, dst.type, native_type_size(t, dst.type))); -} - -static void arm_asm_native_store_loc_hook(NativeTarget* t, SrcLoc loc, - NativeLoc dst, NativeLoc src) { - (void)loc; - if (src.cls != NATIVE_REG_INT) - arm_asm_panic_at(t->c, arm_of(t)->loc, - "floating-point asm operands are unsupported"); - if (dst.kind == NATIVE_LOC_REG) { - if (dst.v.reg != src.v.reg || dst.cls != src.cls) arm_move(t, dst, src); - return; - } - arm_store(t, arm_asm_in_loc_addr(t, dst, src.type), src, - native_mem_for_type(t, src.type, native_type_size(t, src.type))); -} - static void arm_asm_native_run_template_hook( NativeTarget* t, const char* tmpl, const AsmConstraint* outs, u32 nout, Operand* bound_outs, const AsmConstraint* ins, u32 nin, Operand* bound_ins, @@ -3599,15 +3705,10 @@ static void arm_asm_block_native(NativeTarget* t, const char* tmpl, const Sym* clobbers, u32 nclob) { SrcLoc loc = arm_of(t)->loc; static const NativeAsmNativeHooks hooks = { - .opk_reg = ARM32_INLINE_OPK_REG, - .opcls_fp = ARM32_INLINE_OPCLS_FP, .panic = arm_asm_native_panic, .bound_reg = arm_asm_bound_reg, .bound_mem = arm_asm_bound_mem, .mem_base = arm_asm_native_mem_base_hook, - .stage_reg = arm_asm_native_stage_reg_hook, - .load_loc_to_reg = arm_asm_native_load_loc_hook, - .store_reg_to_loc = arm_asm_native_store_loc_hook, .run_template = arm_asm_native_run_template_hook, }; native_asm_bind_native_operands(t, loc, tmpl, outs, nout, out_locs, ins, nin, diff --git a/src/arch/mc.c b/src/arch/mc.c @@ -238,6 +238,11 @@ ObjSymId mc_label_symbol(MCEmitter* m, MCLabel id) { li->block_sym = obj_symbol(m->obj, name, SB_LOCAL, SK_NOTYPE, li->placed ? li->sec_id : OBJ_SEC_NONE, li->placed ? (u64)li->offset : 0u, 0); + /* A block-address symbol is a relocation anchor within its function, not a + * separately collectible/reorderable content unit. This distinction is + * material on Mach-O, where every ordinary symbol under + * MH_SUBSECTIONS_VIA_SYMBOLS otherwise starts a new atom. */ + obj_symbol_set_atom_subordinate(m->obj, li->block_sym, 1); return li->block_sym; } diff --git a/src/arch/native_target.h b/src/arch/native_target.h @@ -147,19 +147,41 @@ typedef struct NativeAllocClassInfo { u8 cls; /* NativeAllocClass */ u8 pad[3]; - const Reg* allocable; - u32 nallocable; - + /* NativeDirectTarget (-O0) value-cache register policy. Optimized + * allocation is described independently by NATIVE_REG_ALLOCABLE rows in + * `phys`; neither path may infer its value bank from the other. */ + const Reg* ndt_allocable; + u32 ndt_allocable_count; + + /* NativeDirectTarget (-O0) value-cache temporary policy only. Optimized + * emission must not infer operand registers from this list; it owns the + * independently declared, instruction-scoped `emit_temps` bank below. */ const Reg* scratch; u32 nscratch; - /* Optional native-emit scratch for the -O1 optimizer (pass_native_emit) when - * it needs a different set than the -O0 NDT `scratch`. opt_nscratch == 0 means - * the optimizer reuses `scratch`. Register-starved backends (arm32) expose a - * second reserved register here (one the NDT must not take as an operand - * scratch, e.g. a dedicated backend div/staging temp). */ - const Reg* opt_scratch; - u32 opt_nscratch; + /* Registers owned exclusively by an -O1 native-emission instruction scope. + * They are operand-facing temporaries, not MIR values and not backend-private + * implementation registers. The set is explicit rather than inherited from + * the -O0 cache's `scratch` policy: those two clients have different + * lifetimes and may require disjoint banks (notably ARM32). */ + const Reg* emit_temps; + u32 nemit_temps; + + /* Backend-internal registers that may be loaned specifically while binding + * a register-only inline-asm block. They are otherwise unavailable to the + * optimizer emitter. The asm staging planner uses them only when the block + * has no memory constraint (whose address binder may need the same bank). */ + const Reg* asm_temps; + u32 nasm_temps; + + /* 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 + * in dead O1 allocation registers whose ABI preservation is established + * (caller-saved, or a callee-saved register present in the known frame) + * across adjacent instructions; O1 allocation-register preservation is + * governed by machine_op_clobbers below. */ + u32 emit_cache_mask; const NativePhysRegInfo* phys; u32 nphys; @@ -176,7 +198,7 @@ struct NativeRegInfo { const NativeAllocClassInfo* classes; u32 nclasses; - /* True when this register model's scratch + caller-saved-allocable pool is + /* True when this register model's scratch + caller-saved NDT cache pool is * rich enough that the single-pass (-O0) NativeDirectTarget never needs to * fall back to a callee-saved register under pressure. When set, NDT is * enforced caller-saved-only (nd_scratch_acquire skips callee-saved regs; @@ -199,11 +221,11 @@ struct NativeRegInfo { /* True when this backend honors MF_SEXT_LOAD by emitting a sign-extending * load that fills the whole register (aa64 ldrsb/ldrsh -> X), so the shared - * -O0 NDT may record load_sext on the loaded local and elide the following + * Native codegen may record load_sext on the loaded local and elide the following * CV_SEXT. Clear (the default) on backends whose narrow load zero-extends * regardless of the flag (x86-64, riscv64 today): there the convert must run, * so the flag is a no-op and the extend is emitted normally. */ - u8 ndt_load_sext; + u8 load_sext; /* Map a register name to its (Reg, class). `name` is the raw spelling * ("rax", "x8", "a7"); the caller resolves any Sym to its bytes first so this @@ -238,13 +260,21 @@ typedef enum NativeLocKind { NATIVE_LOC_STACK, NATIVE_LOC_IMM, NATIVE_LOC_GLOBAL, + /* Address value of a concrete frame slot. Distinct from NATIVE_LOC_FRAME, + * which is the scalar value stored in the slot, and NATIVE_LOC_ADDR, which + * is the scalar value addressed by an effective-address expression. */ + NATIVE_LOC_FRAME_ADDR, NATIVE_LOC_ADDR, } NativeLocKind; typedef enum NativeAddrBaseKind { NATIVE_ADDR_BASE_NONE, + /* Pointer value already held in a physical register. */ NATIVE_ADDR_BASE_REG, + /* Address of the frame slot itself: FP + slot offset. */ NATIVE_ADDR_BASE_FRAME, + /* Scalar pointer value stored in a frame slot. base_type is mandatory and + * is the exact value type, independent of the colored slot descriptor. */ NATIVE_ADDR_BASE_FRAME_VALUE, NATIVE_ADDR_BASE_GLOBAL, } NativeAddrBaseKind; @@ -252,6 +282,8 @@ typedef enum NativeAddrBaseKind { typedef enum NativeAddrIndexKind { NATIVE_ADDR_INDEX_NONE, NATIVE_ADDR_INDEX_REG, + /* Scalar index value stored in a frame slot. index_type is mandatory and is + * the exact value type, independent of the colored slot descriptor. */ NATIVE_ADDR_INDEX_FRAME_VALUE, } NativeAddrIndexKind; @@ -388,10 +420,13 @@ typedef struct NativeFramePatchState { /* 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). Built by the optimizer from an instruction; - * the descriptor keeps the backend from depending on the optimizer IR. */ + * rax/rdx, variable shifts use cl, and a bitfield read-modify-write borrows + * fixed temporaries). Built by the optimizer from an instruction; the + * descriptor keeps the backend from depending on the optimizer IR. */ typedef enum NativeMachineOpKind { NATIVE_MOP_BINOP, + NATIVE_MOP_BITFIELD_LOAD, + NATIVE_MOP_BITFIELD_STORE, NATIVE_MOP_VA_START, NATIVE_MOP_VA_ARG, NATIVE_MOP_ATOMIC_CAS, @@ -419,6 +454,11 @@ typedef struct NativeMachineOp { typedef struct NativeCallDesc { KitCgTypeId fn_type; NativeLoc callee; + /* Each argument is a complete semantic value. REG/IMM carry scalar bits; + * FRAME/STACK/ADDR name dereferenceable storage; GLOBAL/FRAME_ADDR are + * scalar address values and must classify as one pointer-sized ABI part. + * Backends use native_loc_addr_role plus the checked converters below rather + * than inferring value-vs-storage semantics from union fields. */ const NativeLoc* args; const NativeLoc* results; u32 nargs; @@ -492,14 +532,18 @@ struct NativeTarget { * sxtw/uxtw #scale]`), honoring NativeAddr.index_ext. */ int (*can_fold_shift_into_alu)(NativeTarget*); int (*can_fold_extend_into_addr)(NativeTarget*); - /* Optional. Report the physical registers the target's encoding of `op` - * clobbers as a side effect (not its declared operands/results), one bitmask - * per NativeAllocClass. The optimizer keeps values live ACROSS the - * instruction out of these registers, so the backend may use them freely (x86 - * idiv writes rax/rdx; a variable shift uses cl; atomics use rax/rcx/rdx). - * Return non-zero if any register is clobbered, 0 otherwise (the common, - * unconstrained case). NULL means no instruction clobbers fixed registers - * (aa64/rv64). */ + /* Optional, but exhaustive when present. Report every optimizer-visible + * physical register the target's encoding of `op` clobbers as a side effect + * (not its declared operands/results), one bitmask per NativeAllocClass. + * This includes registers that can be MIR hard homes and cache-enabled emit + * temps whose retained spill value must be invalidated. Pure backend-private + * temporaries are reserved, never cached, and do not appear here. This single + * contract lets regalloc keep live values out of fixed clobbers and lets the + * emitter retain clean spill values safely. Examples include x86 idiv + * (rax/rdx), variable shift (rcx), bitfield RMW and atomics (rax/rcx/rdx), + * and format-dependent TLS sequences. Return non-zero if any register is + * 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]); @@ -586,14 +630,22 @@ struct NativeTarget { void (*emit)(NativeTarget*, const NativeInst*); /* All instruction-emission hooks require caller-selected legal physical - * operands. In particular, dst values are NATIVE_LOC_REG, arithmetic sources - * are NATIVE_LOC_REG or target-legal immediates, and memory base/index - * registers in NativeAddr must already be materialized. NativeTarget may - * validate and assert, but it must not allocate registers. */ + * value operands. In particular, dst values are NATIVE_LOC_REG and + * arithmetic sources are NATIVE_LOC_REG or target-legal immediates. + * NativeAddr may retain typed FRAME_VALUE base/index components; the backend + * materializes those exact-width values only through its declared internal + * registers. It must not allocate or expose a new IR-visible register. */ void (*move)(NativeTarget*, NativeLoc dst_reg, NativeLoc src_reg); void (*load_imm)(NativeTarget*, NativeLoc dst_reg, i64 imm); void (*load_const)(NativeTarget*, NativeLoc dst_reg, ConstBytes); + /* Address materialization must accept dst_reg aliasing a BASE_REG component + * while still consuming any distinct index before the destination write. + * The emitter never requests dst_reg aliasing the index itself. */ void (*load_addr)(NativeTarget*, NativeLoc dst_reg, NativeAddr addr); + /* A one-register scalar load from a plain BASE_REG address must accept + * dst_reg aliasing that base: the effective address is consumed before the + * destination is written. This lets the target-neutral emitter reuse an + * instruction-leased address register under maximal temporary pressure. */ void (*load)(NativeTarget*, NativeLoc dst_reg, NativeAddr addr, MemAccess); void (*store)(NativeTarget*, NativeAddr addr, NativeLoc src_reg, MemAccess); void (*tls_addr_of)(NativeTarget*, NativeLoc dst_reg, ObjSymId sym, @@ -602,6 +654,8 @@ struct NativeTarget { AggregateAccess); void (*set_bytes)(NativeTarget*, NativeAddr dst, NativeLoc byte_value, AggregateAccess); + /* Like load(), a one-register bitfield load must consume a plain BASE_REG + * address before writing dst_reg and accept those registers aliasing. */ void (*bitfield_load)(NativeTarget*, NativeLoc dst_reg, NativeAddr record_addr, BitFieldAccess); void (*bitfield_store)(NativeTarget*, NativeAddr record_addr, @@ -632,14 +686,24 @@ 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*); - /* `value` is the single returned local's location, or NULL for a void - * return. out_rets/out_nrets describe the ABI parts of that one value. */ + /* 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 (*ret)(NativeTarget*); + /* Like load(), a one-register atomic load must consume a plain BASE_REG + * address before writing dst and accept those registers aliasing. Backends + * already materialize any stronger atomic addressing constraint into their + * private scratch before the value load. */ void (*atomic_load)(NativeTarget*, NativeLoc dst, NativeAddr addr, MemAccess, KitCgMemOrder); void (*atomic_store)(NativeTarget*, NativeAddr addr, NativeLoc src, MemAccess, @@ -730,6 +794,147 @@ static inline NativeLoc native_loc_stack(KitCgTypeId type, NativeFrameSlot slot, .v.stack = {.slot = slot, .offset = offset}}; } +/* A NativeLoc that structurally contains an address has one of two disjoint + * semantic roles. Storage locations are dereferenced to obtain a value; + * address recipes are themselves scalar pointer values. Keep the role in the + * shared contract so ABI marshallers cannot accidentally turn `&slot` into a + * load from slot (or turn a stored scalar into its address). */ +typedef enum NativeLocAddrRole { + NATIVE_LOC_ADDR_ROLE_NONE, + NATIVE_LOC_ADDR_ROLE_STORAGE, + NATIVE_LOC_ADDR_ROLE_VALUE, +} NativeLocAddrRole; + +static inline NativeLocAddrRole native_loc_addr_role(NativeLoc loc) { + switch ((NativeLocKind)loc.kind) { + case NATIVE_LOC_FRAME: + case NATIVE_LOC_STACK: + case NATIVE_LOC_ADDR: + return NATIVE_LOC_ADDR_ROLE_STORAGE; + case NATIVE_LOC_GLOBAL: + case NATIVE_LOC_FRAME_ADDR: + return NATIVE_LOC_ADDR_ROLE_VALUE; + default: + return NATIVE_LOC_ADDR_ROLE_NONE; + } +} + +/* Resolve storage that may be dereferenced. `offset` selects bytes within the + * stored object (including an existing STACK/ADDR displacement). */ +static inline int native_loc_storage_addr(NativeLoc loc, i32 offset, + NativeAddr* out) { + NativeAddr addr = {0}; + if (!out || native_loc_addr_role(loc) != NATIVE_LOC_ADDR_ROLE_STORAGE) + return 0; + switch ((NativeLocKind)loc.kind) { + case NATIVE_LOC_FRAME: + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = loc.v.frame; + addr.base_type = loc.type; + break; + case NATIVE_LOC_STACK: + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = loc.v.stack.slot; + addr.base_type = loc.type; + addr.offset = loc.v.stack.offset; + break; + case NATIVE_LOC_ADDR: + addr = loc.v.addr; + break; + default: + return 0; + } + addr.offset += offset; + *out = addr; + return 1; +} + +/* Resolve a scalar address recipe. There is intentionally no part offset: + * GLOBAL/FRAME_ADDR are one pointer value, not byte-addressable aggregate + * storage. A call marshaller that tries to split one must reject the shape. */ +static inline int native_loc_address_value(NativeLoc loc, NativeAddr* out) { + NativeAddr addr = {0}; + if (!out || native_loc_addr_role(loc) != NATIVE_LOC_ADDR_ROLE_VALUE) + return 0; + switch ((NativeLocKind)loc.kind) { + case NATIVE_LOC_FRAME_ADDR: + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = loc.v.frame; + addr.base_type = loc.type; + break; + case NATIVE_LOC_GLOBAL: + addr.base_kind = NATIVE_ADDR_BASE_GLOBAL; + addr.base.global.sym = loc.v.global.sym; + addr.base.global.addend = loc.v.global.addend; + addr.base_type = loc.type; + break; + default: + return 0; + } + *out = addr; + return 1; +} + +/* Iterate the exact bytes carried by one ABI part as native load/store widths. + * ABIArgPart.size is a semantic byte count, not the rounded size of its + * register or stack carrier: a six-byte aggregate part must transfer 4+2 + * bytes, never one 8-byte access that can cross the source/destination object. + * + * Chunks are returned from low to high byte offsets, greedily choosing the + * largest power-of-two width no greater than max_chunk. All native targets are + * little-endian today, so the byte offset is also the register shift in bytes + * when packing/unpacking a part. Physical ABI stack-slot sizing remains the + * backend's separate responsibility. */ +typedef struct NativePartChunkIter { + u32 total; + u32 offset; + u32 max_chunk; +} NativePartChunkIter; + +static inline NativePartChunkIter native_part_chunks(u32 total, + u32 max_chunk) { + NativePartChunkIter it; + it.total = total; + it.offset = 0; + it.max_chunk = max_chunk; + return it; +} + +static inline int native_part_chunk_next(NativePartChunkIter* it, + u32* offset_out, u32* size_out) { + u32 chunk = 1u; + u32 remaining; + if (!it || !offset_out || !size_out || it->offset >= it->total || + it->max_chunk == 0u) + return 0; + remaining = it->total - it->offset; + while (chunk <= remaining / 2u && chunk <= it->max_chunk / 2u) chunk <<= 1; + *offset_out = it->offset; + *size_out = chunk; + it->offset += chunk; + return 1; +} + +/* Select the raw byte lane used for one ABI part of an immediate scalar. The + * complete value is only i64-wide, so bounds are explicit. Offset zero keeps + * the producer's signed numeric value (preserving established narrow-scalar + * extension behavior); later parts are bit slices and are zero-extended. */ +static inline int native_loc_imm_part(NativeLoc loc, u32 offset, u32 size, + i64* out) { + u64 bits, mask; + if (!out || loc.kind != NATIVE_LOC_IMM || size == 0u || size > 8u || + offset >= 8u || size > 8u - offset) + return 0; + if (offset == 0u) { + *out = loc.v.imm; + return 1; + } + bits = (u64)loc.v.imm >> (offset * 8u); + mask = size == 8u ? ~(u64)0 : (((u64)1u << (size * 8u)) - 1u); + *out = (i64)(bits & mask); + return 1; +} + static inline int native_loc_is_fp(NativeLoc loc) { return (NativeAllocClass)loc.cls == NATIVE_REG_FP; } diff --git a/src/arch/riscv/native.c b/src/arch/riscv/native.c @@ -36,12 +36,12 @@ #include "obj/obj.h" enum { - RV_TMP0 = 5u, /* t0: emit-internal scratch (reserved, never allocable) */ - RV_TMP1 = 6u, /* t1: emit-internal scratch */ - RV_TMP2 = 7u, /* t2: emit-internal scratch (reserved in phys table) */ - RV_TMP3 = 28u, /* t3: emit-internal scratch (reserved in phys table) */ - RV_FTMP0 = 0u, /* ft0: emit-internal FP scratch */ - RV_FTMP1 = 1u, /* ft1: emit-internal FP scratch */ + RV_TMP0 = 5u, /* t0: backend-private temp (reserved, never allocable) */ + RV_TMP1 = 6u, /* t1: backend-private temp */ + RV_TMP2 = 7u, /* t2: backend/asm temp (reserved in phys table) */ + RV_TMP3 = 28u, /* t3: backend/asm temp (reserved in phys table) */ + RV_FTMP0 = 0u, /* ft0: backend/asm FP temp */ + RV_FTMP1 = 1u, /* ft1: backend/asm FP temp */ RV_FA0 = 10u, /* fa0..fa7 = f10..f17 (FP arg/return registers) */ RV_FA7 = 17u, /* Single-pass (-O0) tcc-style prologue. The frame-independent entry @@ -369,6 +369,39 @@ static u32 loc_size32(NativeTarget* t, NativeLoc loc) { return native_type_size(t, loc.type); } +static u32 rv_internal_tmp(RvNativeTarget* a, u32 avoid_mask) { + static const u8 temps[] = {RV_TMP1, RV_TMP0, RV_TMP2, RV_TMP3}; + for (u32 i = 0; i < sizeof temps / sizeof temps[0]; ++i) + if (!(avoid_mask & (1u << temps[i]))) return temps[i]; + rv_panic(a, "no backend temporary for frame component"); +} + +/* Load a typed scalar value from a frame slot. Address components retain their + * own type because spill-slot coloring may reuse a wider descriptor; loading + * the descriptor width would consume neighboring bytes. Far s0 offsets are + * materialized through a distinct backend-owned register. */ +static void rv_load_frame_component(RvNativeTarget* a, u32 dst, + NativeFrameSlot slot, KitCgTypeId type, + u32 avoid_mask) { + RvNativeSlot* s = rv_slot_get(a, slot); + i32 off = rv_s0_off_slot(s); + u32 size; + if (!type) rv_panic(a, "frame-value component has no exact type"); + size = native_type_size(&a->base, type); + if (fits_i12(off)) { + rv64_emit32(a->base.mc, + enc_int_load(a->variant, size, 0, dst, RV_S0, off)); + return; + } + { + u32 tmp = rv_internal_tmp(a, avoid_mask | (1u << dst) | (1u << RV_S0)); + rv_emit_load_imm(a->variant, a->base.mc, 1, tmp, (i64)off); + rv64_emit32(a->base.mc, rv_add(tmp, RV_S0, tmp)); + rv64_emit32(a->base.mc, + enc_int_load(a->variant, size, 0, dst, tmp, 0)); + } +} + /* The original rv_is_64 predicate exactly, but sourcing the width from the * cached descriptor: `size >= 8 || (xlen==64 && is_ptr)`. The is_ptr clause * keeps the rv32 4-byte-pointer distinction (a stamped 4-byte pointer caches @@ -413,17 +446,18 @@ static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0x1fu; } .spill_cost = 0u, \ .copy_cost = 0u} -/* t0..t3 (x5,x6,x7,x28) are emit-internal scratch (RV_TMP0..RV_TMP3), reserved - * and never handed to the allocator or driver. t4/t5/t6 are the native-emit - * scratch pool (disjoint from the emit temps so a hook can never clobber an - * operand parked there). Three are needed, not two: a load/store arch must - * materialize a scaled-index address into a scratch (collapse_addr_to_reg, e.g. - * `s->arr[i].field`), and the worst case — a 3-operand binop, or such an access - * — has three native-emit scratches live at once. aarch64 reserves three for +/* t0..t3 (x5,x6,x7,x28) are backend-private hook/asm temporaries + * (RV_TMP0..RV_TMP3), reserved and never exposed as optimizer operand + * locations. t4/t5/t6 form the O1 instruction-scoped operand-temp bank, + * disjoint from backend temporaries so a hook cannot overwrite a leased + * operand. Three are needed, not two: a load/store target can need an address + * materialization alongside a 3-operand operation. aarch64 declares three for * the same reason; x64 needs only two because it folds the index into its - * addressing mode. + * addressing mode. Location MIR retains spills as frame locations until the + * consuming instruction opens this lease scope. * - * rv_int_allocable is the NDT (-O0) value-cache / scratch pool and is read only + * rv_ndt_int_allocable is the NDT (-O0) value-cache / scratch pool and is read + * only * by NativeDirectTarget; the optimizer allocates over the phys[] ALLOCABLE flags * (s1..s11, callee-saved, saved by its -O1 prologue) instead. The NDT pool is * the caller-saved argument registers a0..a7: the -O0 cache flushes to frame @@ -433,9 +467,10 @@ static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0x1fu; } * RV_PROLOGUE_WORDS region) from ever overflowing on large/variadic frames the * way callee-save offsets would. Incoming args are spilled to homes at entry * before any body op, so caching in a0..a7 cannot clobber a live parameter. */ -static const Reg rv_int_allocable[] = {10u, 11u, 12u, 13u, 14u, - 15u, 16u, 17u}; /* a0..a7 */ +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 NativePhysRegInfo rv_int_phys[] = { RV_PHYS_INT_RESERVED(0u), /* zero */ @@ -445,7 +480,7 @@ static const NativePhysRegInfo rv_int_phys[] = { 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 (emit) */ + RV_PHYS_INT_RESERVED(7u), /* t2 = TMP2 (backend/asm) */ RV_PHYS_INT_RESERVED(8u), /* s0/fp */ RV_PHYS_INT_CALLEE(9u), /* s1 */ RV_PHYS_INT_ARG(10u, 0u), RV_PHYS_INT_ARG(11u, 1u), @@ -457,10 +492,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 (emit) */ - RV_PHYS_INT_RESERVED(29u), /* t4 = native-emit scratch */ - RV_PHYS_INT_RESERVED(30u), /* t5 = native-emit scratch */ - RV_PHYS_INT_RESERVED(31u), /* t6 = native-emit scratch (3rd) */ + 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 */ }; #define RV_PHYS_FP_ARG(r, idx) \ @@ -502,18 +537,19 @@ static const NativePhysRegInfo rv_int_phys[] = { * overflowing. fa0..fa7 come FIRST (mirroring the int pool's a0..a7): * nd_cache_alloc scans the pool in order, so fronting the arg registers lets a * producer materialize a call's fp args directly into their ABI registers - * (Lever 1, via api_pack_call_args_in_order). ft0/ft1 reserved as emit-internal - * scratch; ft2/ft3 driver scratch. */ -static const Reg rv_fp_allocable[] = {10u, 11u, 12u, 13u, 14u, 15u, - 16u, 17u, 4u, 5u, 6u, 7u, - 28u, 29u, 30u, 31u}; /* fa0-7,ft4-7,ft8-11 */ + * (Lever 1, via api_pack_call_args_in_order). ft0/ft1 are backend/asm + * temporaries; ft2/ft3 form the O1 operand-temp bank and O0 scratch policy. */ +static const Reg rv_ndt_fp_allocable[] = { + 10u, 11u, 12u, 13u, 14u, 15u, 16u, 17u, 4u, 5u, 6u, 7u, + 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 NativePhysRegInfo rv_fp_phys[] = { RV_PHYS_FP_RESERVED(0u), /* ft0 = FTMP0 */ RV_PHYS_FP_RESERVED(1u), /* ft1 = FTMP1 */ - RV_PHYS_FP_RESERVED(2u), /* ft2 = scratch */ - RV_PHYS_FP_RESERVED(3u), /* ft3 = scratch */ + RV_PHYS_FP_RESERVED(2u), /* ft2 = O1 operand temp / O0 scratch */ + RV_PHYS_FP_RESERVED(3u), /* ft3 = O1 operand temp / O0 scratch */ RV_PHYS_FP_CALLER(4u), RV_PHYS_FP_CALLER(5u), RV_PHYS_FP_CALLER(6u), RV_PHYS_FP_CALLER(7u), RV_PHYS_FP_CALLEE(8u), RV_PHYS_FP_CALLEE(9u), RV_PHYS_FP_ARG(10u, 0u), RV_PHYS_FP_ARG(11u, 1u), RV_PHYS_FP_ARG(12u, 2u), @@ -528,10 +564,16 @@ static const NativePhysRegInfo rv_fp_phys[] = { static const NativeAllocClassInfo rv_classes[] = { {.cls = NATIVE_REG_INT, - .allocable = rv_int_allocable, - .nallocable = sizeof rv_int_allocable / sizeof rv_int_allocable[0], + .ndt_allocable = rv_ndt_int_allocable, + .ndt_allocable_count = + sizeof rv_ndt_int_allocable / sizeof rv_ndt_int_allocable[0], .scratch = rv_int_scratch, .nscratch = sizeof rv_int_scratch / sizeof rv_int_scratch[0], + .emit_temps = rv_int_scratch, + .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], + .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) */ @@ -540,14 +582,20 @@ static const NativeAllocClassInfo rv_classes[] = { .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). t4/t5 are the - * driver scratch pool (reserved-from-alloc but listed in scratch[]). */ + /* 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)}, {.cls = NATIVE_REG_FP, - .allocable = rv_fp_allocable, - .nallocable = sizeof rv_fp_allocable / sizeof rv_fp_allocable[0], + .ndt_allocable = rv_ndt_fp_allocable, + .ndt_allocable_count = + sizeof rv_ndt_fp_allocable / sizeof rv_ndt_fp_allocable[0], .scratch = rv_fp_scratch, .nscratch = sizeof rv_fp_scratch / sizeof rv_fp_scratch[0], + .emit_temps = rv_fp_scratch, + .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], + .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) */ @@ -764,7 +812,8 @@ static u32 rv_fold_index(RvNativeTarget* a, u32 base, u32 idx, u8 log2_scale) { /* Resolve any NativeAddr to a base register + imm12 offset. RISC-V has no * indexed load/store, so an index is folded into RV_TMP0 via Zba; far offsets - * and FRAME/FRAME_VALUE/GLOBAL bases are materialized into RV_TMP0/RV_TMP1. */ + * and GLOBAL or exact-typed FRAME_VALUE components are materialized in the + * backend-private RV_TMP0/RV_TMP1 bank. Plain FRAME bases remain s0-relative. */ static void rv_resolve_mem_addr(RvNativeTarget* a, const NativeAddr* addr, u32* base_out, i32* off_out) { MCEmitter* mc = a->base.mc; @@ -782,8 +831,8 @@ static void rv_resolve_mem_addr(RvNativeTarget* a, const NativeAddr* addr, break; } case NATIVE_ADDR_BASE_FRAME_VALUE: { - RvNativeSlot* s = rv_slot_get(a, addr->base.frame); - rv64_emit32(mc, rv_ld_ptr(a->variant, RV_TMP0, RV_S0, rv_s0_off_slot(s))); + rv_load_frame_component(a, RV_TMP0, addr->base.frame, addr->base_type, + 0u); base = RV_TMP0; off = addr->offset; break; @@ -800,13 +849,15 @@ static void rv_resolve_mem_addr(RvNativeTarget* a, const NativeAddr* addr, if (addr->index_kind == NATIVE_ADDR_INDEX_REG) { base = rv_fold_index(a, base, addr->index.reg & 0x1fu, addr->log2_scale); } else if (addr->index_kind == NATIVE_ADDR_INDEX_FRAME_VALUE) { - RvNativeSlot* s = rv_slot_get(a, addr->index.frame); - rv64_emit32(mc, rv_ld_ptr(a->variant, RV_TMP1, RV_S0, rv_s0_off_slot(s))); - base = rv_fold_index(a, base, RV_TMP1, addr->log2_scale); + u32 idx = rv_internal_tmp(a, 1u << base); + rv_load_frame_component(a, idx, addr->index.frame, addr->index_type, + 1u << base); + base = rv_fold_index(a, base, idx, addr->log2_scale); } if (!fits_i12(off)) { - rv_emit_load_imm(a->variant, mc, 1, RV_TMP1, (i64)off); - rv64_emit32(mc, rv_add(RV_TMP0, base, RV_TMP1)); + u32 tmp = rv_internal_tmp(a, 1u << base); + rv_emit_load_imm(a->variant, mc, 1, tmp, (i64)off); + rv64_emit32(mc, rv_add(RV_TMP0, base, tmp)); base = RV_TMP0; off = 0; } @@ -814,6 +865,105 @@ static void rv_resolve_mem_addr(RvNativeTarget* a, const NativeAddr* addr, *off_out = off; } +/* Choose a backend-private integer register for an exact-width ABI transfer. + * TMP0/TMP1 are considered last because address resolution normally owns that + * pair; callers that choose before resolution therefore exclude them by + * passing them as explicit avoids. */ +static u32 rv_part_tmp(RvNativeTarget* a, u32 avoid_a, u32 avoid_b, + u32 avoid_c) { + static const u8 temps[] = {RV_TMP2, RV_TMP3, RV_TMP1, RV_TMP0}; + u32 i; + for (i = 0; i < (u32)(sizeof temps / sizeof temps[0]); ++i) { + u32 r = temps[i]; + if (r != avoid_a && r != avoid_b && r != avoid_c) return r; + } + rv_panic(a, "no temporary for exact-width ABI part"); + return RV_TMP2; +} + +static u32 rv_addr_reg(const NativeAddr* addr, int index) { + if (!index && addr->base_kind == NATIVE_ADDR_BASE_REG) + return addr->base.reg & 0x1fu; + if (index && addr->index_kind == NATIVE_ADDR_INDEX_REG) + return addr->index.reg & 0x1fu; + return REG_NONE; +} + +/* TMP2/TMP3 are not borrowed by rv_resolve_mem_addr, so a packed store may + * retain its source in either one while TMP0/TMP1 materialize the address. */ +static u32 rv_part_preserved_tmp(RvNativeTarget* a, const NativeAddr* addr) { + u32 base = rv_addr_reg(addr, 0); + u32 index = rv_addr_reg(addr, 1); + if (RV_TMP2 != base && RV_TMP2 != index) return RV_TMP2; + if (RV_TMP3 != base && RV_TMP3 != index) return RV_TMP3; + rv_panic(a, "exact-width ABI address consumes transfer temporaries"); + return RV_TMP2; +} + +/* RISC-V has no 3/5/6/7-byte scalar load/store. ABI parts of those semantic + * sizes still occupy one packed integer register, so move them with bounded + * low-to-high 4/2/1 (or RV32 2/1) accesses and explicitly pack/unpack the + * register bits. This keeps every memory access within the semantic object; + * the separately rounded ABI carrier/stack-slot size is not an access width. */ +static void rv_emit_exact_int_part(RvNativeTarget* a, int is_load, u32 reg, + NativeAddr addr, u32 size) { + const RiscvVariant* v = a->variant; + MCEmitter* mc = a->base.mc; + NativePartChunkIter chunks = native_part_chunks(size, v->ptr_bytes); + u32 base; + i32 off; + u32 chunk_off, chunk_size; + + if (!is_load) { + /* Preserve the packed source before address resolution, which may borrow + * TMP0/TMP1. Avoid any explicit address components as well: a plain + * BASE_REG address must remain intact until it has been resolved. */ + u32 value = rv_part_preserved_tmp(a, &addr); + u32 consumed = 0u; + if (value != reg) rv64_emit32(mc, rv_addi(value, reg, 0)); + rv_resolve_mem_addr(a, &addr, &base, &off); + if (!fits_i12((i64)off + (i64)size - 1)) { + u32 stable = rv_part_tmp(a, value, base, REG_NONE); + rv_emit_addr_adjust(v, mc, stable, base, off); + base = stable; + off = 0; + } + while (native_part_chunk_next(&chunks, &chunk_off, &chunk_size)) { + if (chunk_off != consumed) { + rv64_emit32(mc, rv_srli(value, value, + (chunk_off - consumed) * 8u)); + consumed = chunk_off; + } + rv64_emit32(mc, + enc_int_store(v, chunk_size, value, base, + off + (i32)chunk_off)); + } + return; + } + + rv_resolve_mem_addr(a, &addr, &base, &off); + if (base == reg || !fits_i12((i64)off + (i64)size - 1)) { + u32 stable = rv_part_tmp(a, reg, base, REG_NONE); + rv_emit_addr_adjust(v, mc, stable, base, off); + base = stable; + off = 0; + } + { + u32 piece = rv_part_tmp(a, reg, base, REG_NONE); + int first = 1; + while (native_part_chunk_next(&chunks, &chunk_off, &chunk_size)) { + u32 dst = first ? reg : piece; + rv64_emit32(mc, enc_int_load(v, chunk_size, 0, dst, base, + off + (i32)chunk_off)); + if (!first) { + rv64_emit32(mc, rv_slli(piece, piece, chunk_off * 8u)); + rv64_emit32(mc, rv_or(reg, reg, piece)); + } + first = 0; + } + } +} + /* Central load/store primitive. is_load: 1 load into reg, 0 store reg to mem. */ static void rv_emit_mem(RvNativeTarget* a, int is_load, NativeLoc reg, @@ -826,6 +976,12 @@ static void rv_emit_mem(RvNativeTarget* a, int is_load, NativeLoc reg, u32 base; i32 off; + if (!fp && sz != 0u && sz <= a->variant->ptr_bytes && + (sz & (sz - 1u)) != 0u) { + rv_emit_exact_int_part(a, is_load, r, addr, sz); + return; + } + rv_resolve_mem_addr(a, &addr, &base, &off); if (fp) { rv64_emit32( @@ -900,8 +1056,7 @@ static void rv_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) { off = 0; } else if (addr.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) { /* Load the pointer stored in the frame slot, then add the offset. */ - RvNativeSlot* s = rv_slot_get(a, addr.base.frame); - rv64_emit32(mc, rv_ld_ptr(a->variant, rd, RV_S0, rv_s0_off_slot(s))); + rv_load_frame_component(a, rd, addr.base.frame, addr.base_type, 0u); base = rd; off = addr.offset; } else if (addr.base_kind == NATIVE_ADDR_BASE_FRAME) { @@ -914,11 +1069,21 @@ static void rv_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) { } else { rv_panic(a, "unsupported address base in load_addr"); } - /* Fold any index via Zba sh{1,2,3}add (index << scale) + base. */ - if (addr.index_kind == NATIVE_ADDR_INDEX_REG) { - u32 idx = addr.index.reg & 0x1fu; + /* Fold any index via Zba sh{1,2,3}add (index << scale) + base. Resolve the + * byte offset before loading a frame index so the far-offset helper's + * scratch cannot overwrite that index. */ + if (addr.index_kind == NATIVE_ADDR_INDEX_REG || + addr.index_kind == NATIVE_ADDR_INDEX_FRAME_VALUE) { + u32 idx; if (off != 0 || base != rd) rv_emit_addr_adjust(a->variant, mc, rd, base, off); + if (addr.index_kind == NATIVE_ADDR_INDEX_REG) { + idx = addr.index.reg & 0x1fu; + } else { + idx = rv_internal_tmp(a, 1u << rd); + rv_load_frame_component(a, idx, addr.index.frame, addr.index_type, + 1u << rd); + } switch (addr.log2_scale) { case 0: rv64_emit32(mc, rv_add(rd, rd, idx)); @@ -2224,30 +2389,12 @@ static u32 rv_call_stack_bytes(NativeTarget* t, const NativeCallDesc* desc) { return rv_call_stack_size(t, desc); } -/* Resolve a NativeLoc to an addressable NativeAddr (frame/stack/addr). */ -static NativeAddr rv_loc_addr(RvNativeTarget* a, NativeLoc loc, u32 offset) { +/* Resolve a dereferenceable NativeLoc to its storage address. */ +static NativeAddr rv_storage_addr(RvNativeTarget* a, NativeLoc loc, + u32 offset) { NativeAddr addr; - memset(&addr, 0, sizeof addr); - switch ((NativeLocKind)loc.kind) { - case NATIVE_LOC_FRAME: - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = loc.v.frame; - addr.base_type = loc.type; - addr.offset = (i32)offset; - return addr; - case NATIVE_LOC_STACK: - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = loc.v.stack.slot; - addr.base_type = loc.type; - addr.offset = loc.v.stack.offset + (i32)offset; - return addr; - case NATIVE_LOC_ADDR: - addr = loc.v.addr; - addr.offset += (i32)offset; - return addr; - default: - rv_panic(a, "location is not addressable"); - } + if (!native_loc_storage_addr(loc, (i32)offset, &addr)) + rv_panic(a, "location is not storage"); return addr; } @@ -2258,16 +2405,26 @@ static void rv_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src, rv_move(t, dst, src); return; } - if (src.kind == NATIVE_LOC_FRAME || src.kind == NATIVE_LOC_STACK || - src.kind == NATIVE_LOC_ADDR) { - NativeAddr addr = rv_loc_addr(a, src, offset); + if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_VALUE) { + NativeAddr addr; + if (offset != 0u || size != t->c->target.ptr_size || + !native_loc_address_value(src, &addr)) + rv_panic(a, "split or invalid address-value argument"); + rv_load_addr(t, dst, addr); + return; + } + if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_STORAGE) { + NativeAddr addr = rv_storage_addr(a, src, offset); addr.base_type = dst.type; rv_emit_mem(a, 1, dst, addr, native_mem_for_type(t, dst.type, size)); return; } if (src.kind == NATIVE_LOC_IMM) { + i64 part; + if (!native_loc_imm_part(src, offset, size, &part)) + rv_panic(a, "invalid immediate argument part"); rv_emit_load_imm(a->variant, t->mc, loc_is_64(t, dst) ? 1u : 0u, - loc_reg(dst), src.v.imm); + loc_reg(dst), part); return; } rv_panic(a, "unsupported part source"); @@ -2276,9 +2433,8 @@ static void rv_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src, static void rv_store_part(NativeTarget* t, NativeLoc dst, NativeLoc src, u32 offset, u32 size) { RvNativeTarget* a = rv_of(t); - if (dst.kind == NATIVE_LOC_FRAME || dst.kind == NATIVE_LOC_STACK || - dst.kind == NATIVE_LOC_ADDR) { - NativeAddr addr = rv_loc_addr(a, dst, offset); + if (native_loc_addr_role(dst) == NATIVE_LOC_ADDR_ROLE_STORAGE) { + NativeAddr addr = rv_storage_addr(a, dst, offset); addr.base_type = src.type; rv_emit_mem(a, 0, src, addr, native_mem_for_type(t, src.type, size)); return; @@ -2291,7 +2447,7 @@ static void rv_store_part(NativeTarget* t, NativeLoc dst, NativeLoc src, } static void rv_addr_of_loc(NativeTarget* t, NativeLoc dst, NativeLoc src) { - NativeAddr addr = rv_loc_addr(rv_of(t), src, 0); + NativeAddr addr = rv_storage_addr(rv_of(t), src, 0); rv_load_addr(t, dst, addr); } @@ -2315,6 +2471,50 @@ static void rv_store_outgoing_part(NativeTarget* t, int tail_call, rv_emit_mem(rv_of(t), 0, src, addr, native_mem_for_type(t, src.type, size)); } +/* Copy exactly the semantic bytes of a stack-routed value. The caller owns + * the separately rounded physical ABI slot and advances its stack cursor by + * that carrier size; padding is never treated as readable source storage. */ +static void rv_store_outgoing_value_bytes(NativeTarget* t, int tail_call, + u32 stack_off, NativeLoc src, + u32 value_size) { + RvNativeTarget* a = rv_of(t); + NativeLoc tmp = native_loc_reg(src.type, NATIVE_REG_INT, RV_TMP0); + u32 off = 0; + while (off < value_size) { + u32 chunk = value_size - off; + if (chunk > a->variant->ptr_bytes) chunk = a->variant->ptr_bytes; + rv_load_part(t, tmp, src, off, chunk); + rv_store_outgoing_part(t, tail_call, stack_off + off, tmp, chunk); + off += chunk; + } +} + +/* Give every indirect target one lifetime that is disjoint from argument + * transport. `ra` is reserved from allocation and from the t0-t3 backend + * temporary bank, while its incoming value is already saved in the function + * frame at any real call site. A normal JALR may read and write ra in the same + * instruction. A tail site copies this parked value to t1 immediately before + * teardown restores the incoming ra. + * + * Lowering normally presents REG/GLOBAL, but NativeCallDesc permits scalar + * address values and dereferenceable storage too. Route every non-global shape + * through the same checked part boundary so FRAME/STACK/ADDR/FRAME_ADDR/IMM + * remain valid without acquiring an optimizer-owned register. NONE is kept for + * planning-only queries that never emit a call. */ +static NativeLoc rv_stage_indirect_callee(NativeTarget* t, NativeLoc callee) { + RvNativeTarget* a = rv_of(t); + NativeLoc parked; + if (callee.kind == NATIVE_LOC_NONE || callee.kind == NATIVE_LOC_GLOBAL) + return callee; + if (callee.kind == NATIVE_LOC_REG && + (NativeAllocClass)callee.cls != NATIVE_REG_INT) + rv_panic(a, "indirect callee is not in an integer register"); + if (!callee.type) rv_panic(a, "indirect callee has no pointer type"); + parked = native_loc_reg(callee.type, NATIVE_REG_INT, RV_RA); + rv_load_part(t, parked, callee, 0, a->variant->ptr_bytes); + return parked; +} + /* NativeTarget bind_param: route incoming param (ABI loc) into dst. */ static void rv_bind_native_param(NativeTarget* t, const CGParamDesc* p, NativeLoc dst) { @@ -2413,7 +2613,7 @@ static void rv_emit_one_arg_move(NativeTarget* t, const NativeArgMove* m) { } /* Parallel-copy register arg moves via the shared scheduler; cycles break - * through the int/fp emit scratch (t1 / ft1). */ + * through backend-private t1/ft1. */ static void rv_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves, u32 n) { NativeArgShuffle s; @@ -2449,15 +2649,7 @@ static void rv_plan_call(NativeTarget* t, const NativeCallDesc* desc, plan->stack_arg_size = rv_call_stack_size(t, desc); if (plan->stack_arg_size > a->frame.max_outgoing) a->frame.max_outgoing = plan->stack_arg_size; - /* Indirect callee in an arg register would be clobbered by arg loads. */ - if (plan->callee.kind == NATIVE_LOC_REG && - (NativeAllocClass)plan->callee.cls == NATIVE_REG_INT && - plan->callee.v.reg >= RV_A0 && plan->callee.v.reg <= RV_A7) { - NativeLoc scratch = - native_loc_reg(plan->callee.type, NATIVE_REG_INT, RV_TMP0); - rv_move(t, scratch, plan->callee); - plan->callee = scratch; - } + plan->callee = rv_stage_indirect_callee(t, plan->callee); { /* sret returns pass the hidden destination pointer as the implicit first * integer argument (a0), so the real args start at a1. */ @@ -2472,15 +2664,10 @@ static void rv_plan_call(NativeTarget* t, const NativeCallDesc* desc, abi && abi->variadic && abi->vararg_on_stack && i >= abi->nparams; if (ai->kind == ABI_ARG_IGNORE) continue; if (force_stack) { - NativeLoc tmpreg = - native_loc_reg(desc->args[i].type, NATIVE_REG_INT, RV_TMP0); - u32 slot = a->variant->gp_slot_bytes; /* xlen-word: 8 lp64d / 4 ilp32 */ - u32 n = rv_class_stack_size(a->variant, ai), off = 0; - while (off < n) { - rv_load_part(t, tmpreg, desc->args[i], off, slot); - rv_store_outgoing_part(t, tail, stack + off, tmpreg, slot); - off += slot; - } + u32 value_size = native_type_size(t, desc->args[i].type); + u32 n = rv_class_stack_size(a->variant, ai); + rv_store_outgoing_value_bytes(t, tail, stack, desc->args[i], + value_size); stack += n; continue; } @@ -2585,11 +2772,10 @@ static void rv_emit_tail_site(NativeTarget* t, NativeLoc callee) { u32 n_int = rv_collect_int_saves(a, int_regs); u32 n_fp = rv_collect_fp_saves(a, fp_regs); i32 i; - /* Stage an indirect callee into a reserved scratch (t1) BEFORE the teardown: - * regalloc parks the function pointer in a callee-saved register so it - * survives arg marshalling, and the callee-save / s0 / ra restores below - * would otherwise overwrite it. t1 is reserved (never allocable) and - * untouched by the restore loop (which only uses t0 for far offsets). */ + /* Stage the target parked in ra into a reserved scratch BEFORE teardown: the + * callee-save / s0 / ra restores below would otherwise overwrite it. t1 is + * reserved (never allocable) and untouched by the restore loop (which only + * uses t0 for far offsets). */ if (indirect) rv64_emit32(mc, rv_addi(RV_TMP1, loc_reg(callee), 0)); /* Restore callee-saves before tearing the frame down (O1 path; none at -O0). * Their save offsets are s0-relative via rv_save_off, so the restore is @@ -2655,7 +2841,7 @@ static void rv_plan_ret(NativeTarget* t, const CGFuncDesc* fd, dst_addr.base_kind = NATIVE_ADDR_BASE_REG; dst_addr.base.reg = RV_TMP1; dst_addr.base_type = value->type; - src_addr = rv_loc_addr(a, *value, 0); + src_addr = rv_storage_addr(a, *value, 0); src_addr.base_type = value->type; memset(&access, 0, sizeof access); access.type = value->type; @@ -2674,18 +2860,16 @@ static void rv_plan_ret(NativeTarget* t, const CGFuncDesc* fd, part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT; KitCgTypeId pty = rv_part_scalar_type(part); Reg rreg = cls == NATIVE_REG_FP ? RV_FA0 + nf++ : RV_A0 + ni++; - rets[nr].src = *value; - if (rets[nr].src.kind == NATIVE_LOC_FRAME) - rets[nr].src = - native_loc_stack(pty, value->v.frame, (i32)part->src_offset); - else if (rets[nr].src.kind == NATIVE_LOC_STACK) { - rets[nr].src.v.stack.offset += (i32)part->src_offset; - rets[nr].src.type = pty; - } - rets[nr].dst = native_loc_reg(pty, cls, rreg); - rets[nr].mem = native_mem_for_type(t, pty, part->size); - nr++; + NativeLoc dst = native_loc_reg(pty, cls, rreg); + /* Returning a memory-homed aggregate through the generic move phase + * loses ABIArgPart.size while materializing its scalar carrier type (a + * six-byte part becomes an eight-byte load). Marshal here through the + * same exact-width boundary as outgoing arguments instead. */ + rv_load_part(t, dst, *value, part->src_offset, part->size); } + *out_rets = NULL; + *out_nrets = 0; + return; } else if (value) { rets[0].src = *value; rets[0].dst = native_loc_reg(value->type, NATIVE_REG_INT, RV_A0); @@ -3093,7 +3277,7 @@ static void rv_va_arg_native(NativeTarget* t, NativeLoc dst, NativeLoc ap_ptr, KitCgTypeId type) { RvNativeTarget* a = rv_of(t); if (rv_va_arg_is_wide(t, type)) { - rv_va_arg_wide(a, rv_loc_addr(a, dst, 0), rv_va_addr_from_ptr(ap_ptr), + rv_va_arg_wide(a, rv_storage_addr(a, dst, 0), rv_va_addr_from_ptr(ap_ptr), native_type_size(t, type)); return; } @@ -3554,6 +3738,27 @@ static void rv_intrinsic(NativeTarget* t, IntrinKind kind, } rv_panic(a, "unsupported compiler intrinsic"); } + +/* Optimized-hook fixed effects. RISC-V arithmetic, bitfields, aggregate + * memory operations, atomics, varargs and Local-Exec TLS use only declared + * operands/results plus the reserved t0..t3 / ft0..ft3 backend temporaries. + * A syscall is the one exception: argument placement writes the Linux syscall + * 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; + 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; +} + /* ============================ inline asm ============================ */ _Noreturn static void rv_asm_panic_at(Compiler* c, SrcLoc loc, @@ -3638,7 +3843,7 @@ static void rv_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers, * 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 t-temps that aren't emit scratch. */ + /* 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. */ @@ -3835,10 +4040,9 @@ static RvAsmSavedClobber* rv_asm_save_callee_clobbers(RvNativeTarget* a, } /* ---- NativeTarget (optimizer) asm hook ---- - * The optimizer pre-allocated every operand register and arranged surrounding - * data flow, so this binds pre-allocated registers to the template and only - * materializes memory-operand bases into the reserved scratch + spills the - * callee-saved registers the asm clobbers. */ + * The optimized emitter owns register placement, staging, and output + * writeback. This hook binds its concrete locations and only materializes + * memory-constraint bases in backend-private registers. */ static NativeAddr rv_asm_loc_to_addr(RvNativeTarget* a, SrcLoc loc, NativeLoc src) { @@ -3887,61 +4091,6 @@ static Reg rv_asm_native_mem_base(RvNativeTarget* a, SrcLoc loc, NativeLoc src, return dst; } -static u32 rv_asm_reg_mem_size(RvNativeTarget* a, NativeAllocClass cls, - KitCgTypeId type) { - u32 sz = native_type_size(&a->base, type); - if (cls == NATIVE_REG_INT && sz > a->variant->ptr_bytes) - sz = a->variant->ptr_bytes; - return sz; -} - -static Reg rv_asm_stage_reg(RvNativeTarget* a, SrcLoc loc, NativeAllocClass cls, - u32* nint, u32* nfp) { - static const Reg int_regs[] = {RV_TMP2, RV_TMP3}; - static const Reg fp_regs[] = {RV_FTMP0, RV_FTMP1}; - if (cls == NATIVE_REG_FP) { - if (*nfp >= (u32)(sizeof fp_regs / sizeof fp_regs[0])) - rv_asm_panic_at(a->base.c, loc, "too many staged fp asm operands"); - return fp_regs[(*nfp)++]; - } - if (*nint >= (u32)(sizeof int_regs / sizeof int_regs[0])) - rv_asm_panic_at(a->base.c, loc, "too many staged integer asm operands"); - return int_regs[(*nint)++]; -} - -static void rv_asm_load_loc_to_reg(RvNativeTarget* a, SrcLoc loc, NativeLoc src, - NativeLoc dst) { - NativeTarget* t = &a->base; - NativeAllocClass cls = (NativeAllocClass)dst.cls; - if (src.kind == NATIVE_LOC_REG) { - if (src.v.reg != dst.v.reg || src.cls != dst.cls) t->move(t, dst, src); - return; - } - if (src.kind == NATIVE_LOC_IMM) { - if (cls != NATIVE_REG_INT) - rv_asm_panic_at(t->c, loc, - "floating-point immediate asm input is unsupported"); - t->load_imm(t, dst, src.v.imm); - return; - } - rv_emit_mem( - a, 1, dst, rv_asm_loc_to_addr(a, loc, src), - native_mem_for_type(t, dst.type, rv_asm_reg_mem_size(a, cls, dst.type))); -} - -static void rv_asm_store_reg_to_loc(RvNativeTarget* a, SrcLoc loc, - NativeLoc dst, NativeLoc src) { - NativeTarget* t = &a->base; - NativeAllocClass cls = (NativeAllocClass)src.cls; - if (dst.kind == NATIVE_LOC_REG) { - if (dst.v.reg != src.v.reg || dst.cls != src.cls) t->move(t, dst, src); - return; - } - rv_emit_mem( - a, 0, src, rv_asm_loc_to_addr(a, loc, dst), - native_mem_for_type(t, src.type, rv_asm_reg_mem_size(a, cls, src.type))); -} - static void rv_asm_native_panic(NativeTarget* t, SrcLoc loc, const char* msg) { rv_asm_panic_at(t->c, loc, msg); } @@ -3951,23 +4100,6 @@ static Reg rv_asm_native_mem_base_hook(NativeTarget* t, SrcLoc loc, return rv_asm_native_mem_base(rv_of(t), loc, src, ntmp); } -static Reg rv_asm_native_stage_reg_hook(NativeTarget* t, SrcLoc loc, - NativeAllocClass cls, u32* ntmp, - u32* nstage_int, u32* nstage_fp) { - (void)ntmp; - return rv_asm_stage_reg(rv_of(t), loc, cls, nstage_int, nstage_fp); -} - -static void rv_asm_native_load_loc_hook(NativeTarget* t, SrcLoc loc, - NativeLoc src, NativeLoc dst) { - rv_asm_load_loc_to_reg(rv_of(t), loc, src, dst); -} - -static void rv_asm_native_store_loc_hook(NativeTarget* t, SrcLoc loc, - NativeLoc dst, NativeLoc src) { - rv_asm_store_reg_to_loc(rv_of(t), loc, dst, src); -} - static void rv_asm_native_run_template_hook( NativeTarget* t, const char* tmpl, const AsmConstraint* outs, u32 nout, Operand* bound_outs, const AsmConstraint* ins, u32 nin, Operand* bound_ins, @@ -3987,15 +4119,10 @@ static void rv_asm_block_native(NativeTarget* t, const char* tmpl, RvNativeTarget* a = rv_of(t); SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; static const NativeAsmNativeHooks hooks = { - .opk_reg = RV64_INLINE_OPK_REG, - .opcls_fp = RV64_INLINE_OPCLS_FP, .panic = rv_asm_native_panic, .bound_reg = rv_asm_bound_reg, .bound_mem = rv_asm_bound_mem, .mem_base = rv_asm_native_mem_base_hook, - .stage_reg = rv_asm_native_stage_reg_hook, - .load_loc_to_reg = rv_asm_native_load_loc_hook, - .store_reg_to_loc = rv_asm_native_store_loc_hook, .run_template = rv_asm_native_run_template_hook, }; native_asm_bind_native_operands(t, loc, tmpl, outs, nout, out_locs, ins, nin, @@ -4026,6 +4153,7 @@ NativeTarget* rv64_native_target_new(Compiler* c, ObjBuilder* obj, t->class_for_type = native_class_for_type_fp_le8; t->imm_legal = rv_imm_legal; t->addr_legal = rv_addr_legal; + t->machine_op_clobbers = rv_machine_op_clobbers; t->func_begin = rv_func_begin; t->func_begin_known_frame = rv_func_begin_known_frame; t->note_frame_state = NULL; @@ -4248,13 +4376,13 @@ 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 emit scratch (t0/t1/t2/t3), sp/gp/tp/zero/ra and the frame - * pointer so the operand allocator never hands them out. */ - .scratch_int = (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), - .scratch_fp = + /* 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, diff --git a/src/arch/x64/native.c b/src/arch/x64/native.c @@ -14,14 +14,15 @@ * The single-pass (-O0) prologue reserves a NOP placeholder patched in func_end * once max_outgoing and callee-saves are known. * - * Register model. INT scratch (never allocable, never driver scratch): R10 and - * R11 — the emit paths' fixed temporaries. FP scratch: XMM14 and XMM15. RSP/RBP - * are reserved (stack/frame pointers). RAX is reserved too (return value, the - * div/mul implicit operand), but it is NOT an emit temp, so inline asm may pin - * an operand to it (the Linux syscall idiom) — see x64_asm_operand_reg_ok. - * The driver scratch pool is R8/R9 (int) and XMM4/XMM5 (fp), caller-saved on - * both SysV and Win64 and disjoint from the emit temps so a hook never clobbers - * an operand parked there. Scratch registers are reserved from allocation. + * Register model. R10/R11 and XMM14/XMM15 are backend-private hook/asm + * temporaries and are never optimizer operand locations. RSP/RBP are reserved + * stack/frame pointers. RAX is reserved too (return value and div/mul implicit + * operand), but is not in either temporary bank, so inline asm may pin an + * operand to it (the Linux syscall idiom) — see x64_asm_operand_reg_ok. The O1 + * instruction-scoped operand bank is R8/R9 (int) and XMM4/XMM5 (fp), + * caller-saved on both SysV and Win64; the O0 direct target happens to use the + * same registers through its independent `scratch` policy. Both banks are + * reserved from persistent allocation. * Callee-saved set is resolved per-OS via x64_abi_for_os at runtime (the * legality masks below are SysV's, the conservative superset that both ABIs' * allocators respect — Win64's extra callee-saves RDI/RSI/xmm6-15 only shrink @@ -49,10 +50,10 @@ #include "obj/obj.h" enum { - X64_TMP_INT = X64_R10, /* emit-internal int scratch (reserved) */ - X64_TMP_INT2 = X64_R11, /* emit-internal int scratch (reserved) */ - X64_TMP_FP = X64_XMM0 + 14, /* emit-internal fp scratch (reserved) */ - X64_TMP_FP2 = X64_XMM15, /* emit-internal fp scratch (reserved) */ + X64_TMP_INT = X64_R10, /* backend-private int temp (reserved) */ + X64_TMP_INT2 = X64_R11, /* backend-private int temp (reserved) */ + X64_TMP_FP = X64_XMM0 + 14, /* backend-private fp temp (reserved) */ + X64_TMP_FP2 = X64_XMM15, /* backend-private fp temp (reserved) */ X64_MAX_REG_ARG_MOVES = 16u, /* Deferred entry register-binds (-O1): bounded by simultaneously-live * register-homed param parts, i.e. the allocable register count. */ @@ -93,6 +94,7 @@ 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 */ @@ -129,6 +131,11 @@ 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; @@ -163,10 +170,12 @@ static u32 loc_size32(NativeTarget* t, NativeLoc loc) { return native_type_size(t, loc.type); } -/* The original x64_is_64 predicate exactly, but sourcing the width from the - * cached descriptor: `size >= 8 || is_ptr`. Byte-identical. */ +/* Select the physical integer carrier width. Native scalar sizes are + * 1/2/4/8, while a small direct aggregate may legitimately remain packed in a + * register under the -O0 cache; its 5/6/7-byte representation needs a 64-bit + * move even though its semantic size is below eight bytes. */ static int loc_is_64(NativeTarget* t, NativeLoc loc) { - return loc_size32(t, loc) >= 8u || cg_type_is_ptr(t->c, loc.type); + return loc_size32(t, loc) > 4u || cg_type_is_ptr(t->c, loc.type); } static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0xfu; } @@ -232,11 +241,12 @@ static void emit_jcc_rel32(MCEmitter* mc, u32 cc, MCLabel l); * (the fixed-size save region covers them). RSI/RDI are safe to cache: they have * no implicit x86 use — unlike RCX (shift count) and RAX/RDX (div/mul), which * are therefore excluded — and incoming args are spilled to frame homes at entry - * before any body op runs. R8/R9 are the driver scratch pool; R10/R11 are emit - * scratch (reserved); RAX is reserved (return / div-mul, asm-pinnable). */ -static const Reg x64_int_allocable[] = {X64_RSI, X64_RDI, X64_R13, X64_R14, - X64_R15}; + * before any body op runs. R8/R9 are also the O1 operand-temp bank; R10/R11 are + * backend-private (reserved); RAX is reserved (return/div-mul, asm-pinnable). */ +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 NativePhysRegInfo x64_int_phys[] = { X64_PHYS_INT_RESERVED(X64_RAX), /* return / div-mul (asm-pinnable) */ @@ -247,10 +257,10 @@ static const NativePhysRegInfo x64_int_phys[] = { 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), /* driver scratch */ - X64_PHYS_INT_ARG_RESERVED(X64_R9), /* driver scratch */ - X64_PHYS_INT_RESERVED(X64_R10), /* emit scratch */ - X64_PHYS_INT_RESERVED(X64_R11), /* emit scratch */ + 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_CALLEE(X64_R13), X64_PHYS_INT_CALLEE(X64_R14), @@ -298,12 +308,14 @@ static const NativePhysRegInfo x64_int_phys[] = { * materialize directly into the ABI arg registers (Lever 1), then xmm6..xmm11. * xmm0/xmm1 are also the fp ret regs but ndt_result_reg_stable is OFF, so the * fp result still moves out of xmm0 via the post-call path (often a no-op). - * xmm4/xmm5 stay driver scratch; xmm14/xmm15 are emit scratch. */ -static const Reg x64_fp_allocable[] = { + * xmm4/xmm5 form the O1 operand-temp bank (and O0 scratch policy); + * xmm14/xmm15 are backend-private. */ +static const Reg x64_ndt_fp_allocable[] = { 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 Reg x64_fp_scratch[] = {X64_XMM4, X64_XMM5}; +static const Reg x64_fp_asm_temps[] = {X64_TMP_FP, X64_TMP_FP2}; static const NativePhysRegInfo x64_fp_phys[] = { X64_PHYS_FP_ARG_RET(X64_XMM0), X64_PHYS_FP_ARG_RET(X64_XMM1), @@ -313,16 +325,22 @@ static const NativePhysRegInfo x64_fp_phys[] = { 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), - X64_PHYS_FP_RESERVED(X64_XMM0 + 14), /* emit scratch */ - X64_PHYS_FP_RESERVED(X64_XMM15), /* emit scratch */ + X64_PHYS_FP_RESERVED(X64_XMM0 + 14), /* backend-private temp */ + X64_PHYS_FP_RESERVED(X64_XMM15), /* backend-private temp */ }; static const NativeAllocClassInfo x64_classes[] = { {.cls = NATIVE_REG_INT, - .allocable = x64_int_allocable, - .nallocable = sizeof x64_int_allocable / sizeof x64_int_allocable[0], + .ndt_allocable = x64_ndt_int_allocable, + .ndt_allocable_count = + sizeof x64_ndt_int_allocable / sizeof x64_ndt_int_allocable[0], .scratch = x64_int_scratch, .nscratch = sizeof x64_int_scratch / sizeof x64_int_scratch[0], + .emit_temps = x64_int_scratch, + .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], + .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) */ @@ -336,15 +354,21 @@ static const NativeAllocClassInfo x64_classes[] = { .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 driver scratch; r10/r11 emit scratch */ + /* 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)}, {.cls = NATIVE_REG_FP, - .allocable = x64_fp_allocable, - .nallocable = sizeof x64_fp_allocable / sizeof x64_fp_allocable[0], + .ndt_allocable = x64_ndt_fp_allocable, + .ndt_allocable_count = + sizeof x64_ndt_fp_allocable / sizeof x64_ndt_fp_allocable[0], .scratch = x64_fp_scratch, .nscratch = sizeof x64_fp_scratch / sizeof x64_fp_scratch[0], + .emit_temps = x64_fp_scratch, + .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], + .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. */ @@ -352,7 +376,7 @@ static const NativeAllocClassInfo x64_classes[] = { .callee_saved_mask = 0u, .arg_mask = 0xffu, /* xmm0..xmm7 */ .ret_mask = (1u << X64_XMM0) | (1u << X64_XMM1), - /* xmm4/xmm5 driver scratch; xmm14/xmm15 emit scratch. */ + /* 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)}, @@ -389,9 +413,9 @@ static int x64_asm_operand_reg_ok(const NativeRegInfo* ri, NativeAllocClass cls, (void)ri; if (cls == NATIVE_REG_INT) { switch (reg) { - /* RAX is reserved but not an emit temp, so it is a legal asm pin (the - * Linux syscall number/return register). R8/R9 are driver scratch and - * R10/R11 are emit scratch, so those stay excluded. */ + /* RAX is reserved but not an emitter temp, so it is a legal asm pin (the + * Linux syscall number/return register). R8/R9 are optimizer operand + * temps and R10/R11 backend/asm temps, so those stay excluded. */ case X64_RAX: case X64_RBX: case X64_RCX: @@ -580,8 +604,9 @@ static void x64_emit_global_lea(NativeTarget* t, u32 dst_reg, ObjSymId sym, } } -/* Resolve a NativeAddr to (base, index, log2_scale, off). Materializes - * FRAME/FRAME_VALUE/GLOBAL bases into the supplied scratch register. */ +/* Resolve a NativeAddr to (base, index, log2_scale, off). Plain FRAME bases + * remain rbp-relative. GLOBAL and FRAME_VALUE components use backend-private + * fixed temps; each FRAME_VALUE load uses its exact base_type/index_type. */ static u32 x64_resolve_addr(X64NativeTarget* a, const NativeAddr* addr, u32 scratch, u32* idx_out, u32* scale_out, i32* off_out) { @@ -601,7 +626,10 @@ static u32 x64_resolve_addr(X64NativeTarget* a, const NativeAddr* addr, } case NATIVE_ADDR_BASE_FRAME_VALUE: { X64NativeSlot* s = x64_slot_get(a, addr->base.frame); - emit_mov_load(t->mc, 8, 0, scratch, X64_RBP, -(i32)s->off); + u32 size; + if (!addr->base_type) x64_panic(a, "frame-value base has no exact type"); + size = native_type_size(t, addr->base_type); + emit_mov_load(t->mc, size, 0, scratch, X64_RBP, -(i32)s->off); base = scratch; off = addr->offset; break; @@ -620,8 +648,13 @@ static u32 x64_resolve_addr(X64NativeTarget* a, const NativeAddr* addr, *scale_out = addr->log2_scale; } else if (addr->index_kind == NATIVE_ADDR_INDEX_FRAME_VALUE) { X64NativeSlot* s = x64_slot_get(a, addr->index.frame); - emit_mov_load(t->mc, 8, 0, X64_TMP_INT2, X64_RBP, -(i32)s->off); - *idx_out = X64_TMP_INT2; + u32 size; + u32 index_tmp = base == X64_TMP_INT2 ? X64_TMP_INT : X64_TMP_INT2; + if (!addr->index_type) + x64_panic(a, "frame-value index has no exact type"); + size = native_type_size(t, addr->index_type); + emit_mov_load(t->mc, size, 0, index_tmp, X64_RBP, -(i32)s->off); + *idx_out = index_tmp; *scale_out = addr->log2_scale; } else { *idx_out = REG_NONE; @@ -633,6 +666,10 @@ static u32 x64_resolve_addr(X64NativeTarget* a, const NativeAddr* addr, /* ============================ memory ============================ */ +static int x64_native_part_width(u32 size); +static u32 x64_addr_to_base_reg(X64NativeTarget* a, NativeAddr addr, + u32 scratch); + /* Central load/store primitive. is_load: 1 load into reg, 0 store reg to mem. * Materializes the address through X64_TMP_INT2 (r11) for non-reg bases. */ static void x64_emit_mem(X64NativeTarget* a, int is_load, NativeLoc reg, @@ -645,6 +682,9 @@ static void x64_emit_mem(X64NativeTarget* a, int is_load, NativeLoc reg, u32 base, idx, scale; i32 off; + if (!fp && !x64_native_part_width(sz)) + x64_panic(a, "raw integer memory width is not native"); + /* Global base: fold into a single rip-relative access when local. */ if (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL && addr.index_kind == NATIVE_ADDR_INDEX_NONE && @@ -725,6 +765,147 @@ static void x64_emit_mem(X64NativeTarget* a, int is_load, NativeLoc reg, } } +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); +} + +/* 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) { + 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); + 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_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. */ +static void x64_load_int_part(X64NativeTarget* a, NativeLoc dst, + NativeAddr addr, MemAccess mem, u32 size) { + NativePartChunkIter chunks; + NativeTarget* t = &a->base; + Reg rd = (Reg)loc_reg(dst); + Reg scratch; + NativeLoc tmp; + u32 chunk_off, chunk_size; + int first = 1; + + if (x64_native_part_width(size)) { + mem.size = size; + x64_emit_mem(a, 1, dst, addr, mem); + return; + } + 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); + tmp = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, + scratch); + 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; + x64_emit_mem(a, 1, chunk_dst, chunk_addr, mem); + if (!first) { + emit_shift_imm(t->mc, 1, X64_SHIFT_SUB_SHL, scratch, + (u8)(chunk_off * 8u)); + emit_alu_rr(t->mc, 1, X64_OPC_ALU_OR, rd, scratch); + } + first = 0; + } +} + +/* Store exactly the semantic bytes carried in an integer ABI register. The + * first chunk uses the source directly; later chunks use a shifted private + * copy, preserving both the source register and adjacent destination bytes. */ +static void x64_store_int_part(X64NativeTarget* a, NativeAddr addr, + NativeLoc src, MemAccess mem, u32 size) { + NativePartChunkIter chunks; + NativeTarget* t = &a->base; + Reg rs = (Reg)loc_reg(src); + Reg scratch; + NativeLoc tmp; + u32 chunk_off, chunk_size, shifted = 0; + int copied = 0; + + if (x64_native_part_width(size)) { + mem.size = size; + x64_emit_mem(a, 0, src, addr, mem); + return; + } + 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"); + tmp = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, + scratch); + chunks = native_part_chunks(size, 8u); + while (native_part_chunk_next(&chunks, &chunk_off, &chunk_size)) { + NativeAddr chunk_addr = addr; + NativeLoc chunk_src = src; + chunk_addr.offset += (i32)chunk_off; + if (chunk_off != 0u) { + if (!copied) { + emit_mov_rr(t->mc, 1, scratch, rs); + copied = 1; + } + emit_shift_imm(t->mc, 1, X64_SHIFT_SUB_SHR, scratch, + (u8)((chunk_off - shifted) * 8u)); + shifted = chunk_off; + chunk_src = tmp; + } + mem.size = chunk_size; + x64_emit_mem(a, 0, chunk_src, chunk_addr, mem); + } +} + /* ============================ moves / data ============================ */ static void x64_move(NativeTarget* t, NativeLoc dst, NativeLoc src) { @@ -751,6 +932,19 @@ static void x64_move(NativeTarget* t, NativeLoc dst, NativeLoc src) { emit_mov_rr(mc, loc_is_64(t, dst) ? 1 : 0, rd, rs); } +/* Preserve an indirect call target below rsp while its arguments are + * marshalled. CFA is rbp-based, so this transient balanced pair needs no CFI + * adjustment. High registers use the PUSH/POP opcode's REX.B extension. */ +static void x64_push_reg(MCEmitter* mc, Reg reg) { + if (reg & 8u) emit1(mc, X64_REX_BASE | X64_REX_B); + emit1(mc, X64_OPC_PUSH_R | (reg & 7u)); +} + +static void x64_pop_reg(MCEmitter* mc, Reg reg) { + if (reg & 8u) emit1(mc, X64_REX_BASE | X64_REX_B); + emit1(mc, X64_OPC_POP_R | (reg & 7u)); +} + static void x64_load_imm(NativeTarget* t, NativeLoc dst, i64 imm) { x64_emit_load_imm(t->mc, loc_is_64(t, dst) ? 1 : 0, loc_reg(dst), imm); } @@ -800,11 +994,19 @@ static void x64_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) { static void x64_load(NativeTarget* t, NativeLoc dst, NativeAddr addr, MemAccess mem) { - x64_emit_mem(x64_of(t), 1, dst, addr, mem); + u32 size = mem.size ? mem.size : loc_size32(t, dst); + if (!x64_native_part_width(size) && size <= 8u) + x64_load_int_part(x64_of(t), dst, addr, mem, size); + else + x64_emit_mem(x64_of(t), 1, dst, addr, mem); } static void x64_store(NativeTarget* t, NativeAddr addr, NativeLoc src, MemAccess mem) { - x64_emit_mem(x64_of(t), 0, src, addr, mem); + u32 size = mem.size ? mem.size : loc_size32(t, src); + if (!x64_native_part_width(size) && size <= 8u) + x64_store_int_part(x64_of(t), addr, src, mem, size); + else + x64_emit_mem(x64_of(t), 0, src, addr, mem); } /* Resolve an addressable NativeAddr to a bare base register (no index, off 0) @@ -837,52 +1039,41 @@ static u32 x64_addr_to_base_reg(X64NativeTarget* a, NativeAddr addr, return scratch; } -/* copy_bytes: resolve dst into r11 and src into rax (both bare pointers), then - * unrolled granule copy through rdx. dst is resolved first (its base may live - * in r11 from a FRAME_VALUE load) and src second so the two never alias. */ +/* Normalize one address at a time, load through R10, preserve the granule in + * XMM14 while the destination is normalized, then restore it to R10 for the + * store. This keeps both effective addresses and the transfer value entirely + * in the backend-private R10/R11/XMM14 bank; no allocator/cache-visible fixed + * register is touched. */ static void x64_copy_bytes(NativeTarget* t, NativeAddr dst, NativeAddr src, AggregateAccess access) { X64NativeTarget* a = x64_of(t); - /* Copy chunk by chunk (8/4/2/1) through the value scratch rax, letting - * x64_emit_mem resolve each address with its own scratch (r11). Uses only the - * reserved emit scratch (rax/r11) — no ad-hoc allocable temp (previously - * rdx), which the optimizer may have live across the copy. */ - KitCgTypeId tys[4]; + MCEmitter* mc = t->mc; u32 n = access.size, i = 0; - tys[0] = builtin_id(KIT_CG_BUILTIN_I64); - tys[1] = builtin_id(KIT_CG_BUILTIN_I32); - tys[2] = builtin_id(KIT_CG_BUILTIN_I16); - tys[3] = builtin_id(KIT_CG_BUILTIN_I8); while (i < n) { u32 rem = n - i, s; - KitCgTypeId ty; NativeAddr sa = src, da = dst; - NativeLoc val; - MemAccess mem; + u32 sb, db; if (rem >= 8u) { s = 8u; - ty = tys[0]; } else if (rem >= 4u) { s = 4u; - ty = tys[1]; } else if (rem >= 2u) { s = 2u; - ty = tys[2]; } else { s = 1u; - ty = tys[3]; } sa.offset += (i32)i; - sa.base_type = ty; da.offset += (i32)i; - da.base_type = ty; - val = native_loc_reg(ty, NATIVE_REG_INT, X64_TMP_INT); - memset(&mem, 0, sizeof mem); - mem.type = ty; - mem.size = s; - mem.align = s; - x64_emit_mem(a, 1, val, sa, mem); /* rax = [src + i] */ - x64_emit_mem(a, 0, val, da, mem); /* [dst + i] = rax */ + sb = x64_addr_to_base_reg(a, sa, X64_TMP_INT2); + emit_mov_load(mc, s, 0, X64_TMP_INT, sb, 0); + emit_sse_rr_w(mc, 0x66, 0x6E, s == 8u, X64_TMP_FP, X64_TMP_INT); + db = x64_addr_to_base_reg(a, da, X64_TMP_INT2); + if (db == X64_TMP_INT) { + emit_mov_rr(mc, 1, X64_TMP_INT2, db); + db = X64_TMP_INT2; + } + emit_sse_rr_w(mc, 0x66, 0x7E, s == 8u, X64_TMP_FP, X64_TMP_INT); + emit_mov_store(mc, s, X64_TMP_INT, db, 0); i += s; } } @@ -893,34 +1084,43 @@ static void x64_set_bytes(NativeTarget* t, NativeAddr dst, NativeLoc byte_value, MCEmitter* mc = t->mc; u32 dr = x64_addr_to_base_reg(a, dst, X64_TMP_INT2); u32 n = access.size, i = 0; - /* Broadcast the byte across 8 bytes into rax. */ + /* R10 holds the broadcast value and R11 the resolved destination. Keep both + * entirely backend-private: the variable case broadcasts the low byte in + * XMM14 (punpcklbw/punpcklwd/punpckldq), then moves the qword to R10. */ + if (dr == X64_TMP_INT) { + emit_mov_rr(mc, 1, X64_TMP_INT2, dr); + dr = X64_TMP_INT2; + } if (byte_value.kind == NATIVE_LOC_IMM) { u8 b = (u8)(byte_value.v.imm & 0xffu); u64 b64 = b; b64 |= b64 << 8; b64 |= b64 << 16; b64 |= b64 << 32; - x64_emit_load_imm(mc, 1, X64_RAX, (i64)b64); + x64_emit_load_imm(mc, 1, X64_TMP_INT, (i64)b64); } else { - /* Replicate the low byte of a register via multiply by 0x0101..01. */ - x64_emit_load_imm(mc, 1, X64_R11, (i64)0x0101010101010101ll); - emit_mov_rr(mc, 1, X64_RAX, loc_reg(byte_value)); - emit_imul_rr(mc, 1, X64_RAX, X64_R11); + emit_mov_rr(mc, 0, X64_TMP_INT, loc_reg(byte_value)); + emit_alu_imm32(mc, 0, X64_ALU_SUB_AND, X64_TMP_INT, 0xff); + emit_sse_rr_w(mc, 0x66, 0x6E, 0, X64_TMP_FP, X64_TMP_INT); + emit_sse_rr(mc, 0x66, 0x60, X64_TMP_FP, X64_TMP_FP); + emit_sse_rr(mc, 0x66, 0x61, X64_TMP_FP, X64_TMP_FP); + emit_sse_rr(mc, 0x66, 0x62, X64_TMP_FP, X64_TMP_FP); + emit_sse_rr_w(mc, 0x66, 0x7E, 1, X64_TMP_FP, X64_TMP_INT); } while (i + 8u <= n) { - emit_mov_store(mc, 8, X64_RAX, dr, (i32)i); + emit_mov_store(mc, 8, X64_TMP_INT, dr, (i32)i); i += 8u; } while (i + 4u <= n) { - emit_mov_store(mc, 4, X64_RAX, dr, (i32)i); + emit_mov_store(mc, 4, X64_TMP_INT, dr, (i32)i); i += 4u; } while (i + 2u <= n) { - emit_mov_store(mc, 2, X64_RAX, dr, (i32)i); + emit_mov_store(mc, 2, X64_TMP_INT, dr, (i32)i); i += 2u; } while (i < n) { - emit_mov_store(mc, 1, X64_RAX, dr, (i32)i); + emit_mov_store(mc, 1, X64_TMP_INT, dr, (i32)i); i += 1u; } } @@ -964,8 +1164,22 @@ static void x64_bitfield_store(NativeTarget* t, NativeAddr ra, NativeLoc src, u32 src_reg = loc_reg(src); u32 base; ra.offset += (i32)bf.storage_offset; - /* Stabilize the base into r11 before consuming rax/rcx/rdx scratch. */ + /* Resolve the address before borrowing the fixed value temporaries. A bare + * register address may be returned unchanged, so explicitly stabilize it in + * r11 when the read-modify-write is about to clobber that register. */ base = x64_addr_to_base_reg(a, ra, X64_TMP_INT2); + if (base == X64_RAX || base == X64_RCX || base == X64_RDX) { + emit_mov_rr(mc, 1, X64_TMP_INT2, base); + base = X64_TMP_INT2; + } + /* A dying source may legally occupy one of the fixed clobber registers. + * Preserve it after address resolution (which may transiently use r10 for a + * spilled index) and before loading the storage/mask into rax/rcx. RDX stays + * live until it has been copied to RCX below, so it needs no staging. */ + if (src_reg == X64_RAX || src_reg == X64_RCX) { + emit_mov_rr(mc, w, X64_TMP_INT, src_reg); + src_reg = X64_TMP_INT; + } /* rax = storage; rax &= ~mask. */ emit_mov_load(mc, storage_bytes, 0, X64_RAX, base, 0); x64_emit_load_imm(mc, w, X64_RCX, (i64)~mask); @@ -1031,17 +1245,20 @@ static void x64_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc aop, /* Division: rax/rdx implicit; divisor must avoid rax/rdx. */ if (op == BO_SDIV || op == BO_UDIV || op == BO_SREM || op == BO_UREM) { u32 rb; - if (ra != X64_RAX) emit_mov_rr(mc, w, X64_RAX, ra); if (b_imm) { x64_emit_load_imm(mc, w, X64_R11, imm); rb = X64_R11; } else { rb = loc_reg(bop); + /* Preserve the divisor before placing the dividend in RAX. This is a + * real parallel-copy edge: a dying divisor may be in RAX while the + * dividend is elsewhere, or in RDX before CQO/XOR clears it. */ if (rb == X64_RAX || rb == X64_RDX) { emit_mov_rr(mc, w, X64_R11, rb); rb = X64_R11; } } + if (ra != X64_RAX) emit_mov_rr(mc, w, X64_RAX, ra); if (op == BO_SDIV || op == BO_SREM) { emit_cqo_or_cdq(mc, w); emit_f7_rm(mc, w, X64_F7_SUB_IDIV, rb); @@ -1069,6 +1286,16 @@ static void x64_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc aop, } { u32 rb = loc_reg(bop); + /* RCX cannot simultaneously hold a distinct result and the shift + * count. Compute through backend-private R10, then publish the result + * after the instruction has consumed CL. */ + if (rd == X64_RCX && (ra != X64_RCX || rb != X64_RCX)) { + emit_mov_rr(mc, w, X64_TMP_INT, ra); + if (rb != X64_RCX) emit_mov_rr(mc, 0, X64_RCX, rb); + emit_shift_cl(mc, w, sub, X64_TMP_INT); + emit_mov_rr(mc, w, X64_RCX, X64_TMP_INT); + return; + } /* Place the count in cl and the value in dst. Stage the count through * r11 first so neither move clobbers the other when the value already * sits in rcx or the count sits in dst. (The optimizer additionally @@ -1398,10 +1625,10 @@ static void x64_convert(NativeTarget* t, ConvKind k, NativeLoc dst, emit_jmp_rel32(mc, L_done); mc_label_place(mc, L_high); emit_mov_rr(mc, 1, X64_R11, rs); - emit_mov_rr(mc, 1, X64_RAX, rs); - emit_alu_imm8(mc, 1, X64_ALU_SUB_AND, X64_RAX, 1); + emit_mov_rr(mc, 1, X64_TMP_INT, rs); + emit_alu_imm8(mc, 1, X64_ALU_SUB_AND, X64_TMP_INT, 1); emit_shift_imm(mc, 1, X64_SHIFT_SUB_SHR, X64_R11, 1); - emit_alu_rr(mc, 1, X64_OPC_ALU_OR, X64_R11, X64_RAX); + emit_alu_rr(mc, 1, X64_OPC_ALU_OR, X64_R11, X64_TMP_INT); emit_sse_rr_w(mc, prefix, 0x2A, 1, rd, X64_R11); emit_sse_rr(mc, prefix, 0x58, rd, rd); mc_label_place(mc, L_done); @@ -1543,9 +1770,10 @@ static void x64_cmp_branch(NativeTarget* t, CmpOp op, NativeLoc aop, if (fp) { /* Materialize the 0/1 result, then branch on nonzero. */ NativeLoc tmp = - native_loc_reg(builtin_id(KIT_CG_BUILTIN_I32), NATIVE_REG_INT, X64_RAX); + native_loc_reg(builtin_id(KIT_CG_BUILTIN_I32), NATIVE_REG_INT, + X64_TMP_INT); x64_cmp(t, op, tmp, aop, bop); - emit_test_self(mc, 0, X64_RAX); + emit_test_self(mc, 0, X64_TMP_INT); emit_jcc_rel32(mc, X64_CC_NE, l); return; } @@ -1803,11 +2031,13 @@ 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 @@ -2229,50 +2459,71 @@ static u32 x64_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type, return x64_call_stack_size(t, &d); } -/* Resolve a NativeLoc to an addressable NativeAddr (frame/stack/addr). */ -static NativeAddr x64_loc_addr(X64NativeTarget* a, NativeLoc loc, u32 offset) { +/* Resolve a dereferenceable NativeLoc to its storage address. */ +static NativeAddr x64_storage_addr(X64NativeTarget* a, NativeLoc loc, + u32 offset) { NativeAddr addr; - memset(&addr, 0, sizeof addr); - switch ((NativeLocKind)loc.kind) { - case NATIVE_LOC_FRAME: - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = loc.v.frame; - addr.base_type = loc.type; - addr.offset = (i32)offset; - return addr; - case NATIVE_LOC_STACK: - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = loc.v.stack.slot; - addr.base_type = loc.type; - addr.offset = loc.v.stack.offset + (i32)offset; - return addr; - case NATIVE_LOC_ADDR: - addr = loc.v.addr; - addr.offset += (i32)offset; - return addr; - default: - x64_panic(a, "location is not addressable"); - } + if (!native_loc_storage_addr(loc, (i32)offset, &addr)) + x64_panic(a, "location is not storage"); return addr; } +static void x64_move_part_reg(NativeTarget* t, NativeLoc dst, NativeLoc src, + u32 size) { + MCEmitter* mc = t->mc; + int dfp = native_loc_is_fp(dst), sfp = native_loc_is_fp(src); + u32 rd = loc_reg(dst), rs = loc_reg(src); + if (dfp && sfp) { + if (size != 4u && size != 8u) + x64_panic(x64_of(t), "invalid FP register part width"); + if (rd != rs) emit_sse_rr(mc, sse_scalar_prefix(size), 0x10, rd, rs); + return; + } + if (dfp && !sfp) { + if (size != 4u && size != 8u) + x64_panic(x64_of(t), "invalid GPR-to-FP part width"); + emit_sse_rr_w(mc, 0x66, 0x6E, size == 8u, rd, rs); + return; + } + if (!dfp && sfp) { + if (size != 4u && size != 8u) + x64_panic(x64_of(t), "invalid FP-to-GPR part width"); + emit_sse_rr_w(mc, 0x66, 0x7E, size == 8u, rs, rd); + return; + } + if (rd != rs) emit_mov_rr(mc, size > 4u, rd, rs); +} + static void x64_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src, u32 offset, u32 size) { X64NativeTarget* a = x64_of(t); if (src.kind == NATIVE_LOC_REG) { - x64_move(t, dst, src); + x64_move_part_reg(t, dst, src, size); + return; + } + if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_VALUE) { + NativeAddr addr; + if (offset != 0u || size != t->c->target.ptr_size || + !native_loc_address_value(src, &addr)) + x64_panic(a, "split or invalid address-value argument"); + x64_load_addr(t, dst, addr); return; } - if (src.kind == NATIVE_LOC_FRAME || src.kind == NATIVE_LOC_STACK || - src.kind == NATIVE_LOC_ADDR) { - NativeAddr addr = x64_loc_addr(a, src, offset); + if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_STORAGE) { + NativeAddr addr = x64_storage_addr(a, src, offset); + MemAccess mem = native_mem_for_type(t, dst.type, size); addr.base_type = dst.type; - x64_emit_mem(a, 1, dst, addr, native_mem_for_type(t, dst.type, size)); + if (!x64_native_part_width(size) && size <= 8u) + x64_load_int_part(a, dst, addr, mem, size); + else + x64_emit_mem(a, 1, dst, addr, mem); return; } if (src.kind == NATIVE_LOC_IMM) { - x64_emit_load_imm(t->mc, loc_is_64(t, dst) ? 1 : 0, loc_reg(dst), - src.v.imm); + i64 part; + if (!native_loc_imm_part(src, offset, size, &part)) + x64_panic(a, "invalid immediate argument part"); + x64_emit_load_imm(t->mc, size > 4u, loc_reg(dst), part); return; } x64_panic(a, "unsupported part source"); @@ -2281,22 +2532,25 @@ static void x64_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src, static void x64_store_part(NativeTarget* t, NativeLoc dst, NativeLoc src, u32 offset, u32 size) { X64NativeTarget* a = x64_of(t); - if (dst.kind == NATIVE_LOC_FRAME || dst.kind == NATIVE_LOC_STACK || - dst.kind == NATIVE_LOC_ADDR) { - NativeAddr addr = x64_loc_addr(a, dst, offset); + if (native_loc_addr_role(dst) == NATIVE_LOC_ADDR_ROLE_STORAGE) { + NativeAddr addr = x64_storage_addr(a, dst, offset); + MemAccess mem = native_mem_for_type(t, src.type, size); addr.base_type = src.type; - x64_emit_mem(a, 0, src, addr, native_mem_for_type(t, src.type, size)); + if (!x64_native_part_width(size) && size <= 8u) + x64_store_int_part(a, addr, src, mem, size); + else + x64_emit_mem(a, 0, src, addr, mem); return; } if (dst.kind == NATIVE_LOC_REG) { - x64_move(t, dst, src); + x64_move_part_reg(t, dst, src, size); return; } x64_panic(a, "unsupported part destination"); } static void x64_addr_of_loc(NativeTarget* t, NativeLoc dst, NativeLoc src) { - NativeAddr addr = x64_loc_addr(x64_of(t), src, 0); + NativeAddr addr = x64_storage_addr(x64_of(t), src, 0); x64_load_addr(t, dst, addr); } @@ -2317,9 +2571,47 @@ static void x64_store_outgoing_part(NativeTarget* t, int tail_call, addr.offset = (i32)(16u + stack_off); } else { addr.base.reg = X64_RSP; - addr.offset = (i32)stack_off; + /* 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)); + } + { + MemAccess mem = native_mem_for_type(t, src.type, size); + if (!x64_native_part_width(size) && size <= 8u) + x64_store_int_part(a, addr, src, mem, size); + else + x64_emit_mem(a, 0, src, addr, mem); + } +} + +/* A stack carrier contains the same byte representation as aggregate storage, + * 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, + const ABIArgPart* part) { + NativeAllocClass cls = + part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT; + KitCgTypeId pty = x64_part_scalar_type(part); + Reg tmp = cls == NATIVE_REG_FP ? X64_TMP_FP : X64_TMP_INT; + NativeLoc tmpreg = native_loc_reg(pty, cls, tmp); + + if (!x64_native_part_width(part->size) && part->size <= 8u && + native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_STORAGE) { + NativePartChunkIter chunks = native_part_chunks(part->size, 8u); + 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, + chunk_size); + } + return; } - x64_emit_mem(a, 0, src, addr, native_mem_for_type(t, src.type, size)); + + x64_load_part(t, tmpreg, src, part->src_offset, part->size); + x64_store_outgoing_part(t, tail_call, stack_off, tmpreg, part->size); } /* NativeTarget bind_param: route incoming param (ABI loc) into dst. */ @@ -2482,7 +2774,7 @@ static void x64_bind_native_param(NativeTarget* t, const CGParamDesc* p, /* Flush the deferred register-destination param binds as a parallel copy (the * shared scheduler breaks any cycle the allocator's rotation created through - * the int/fp emit scratch). Frame-dst and indirect binds were emitted eagerly + * backend-private int/fp cycle temporaries). Frame-dst and indirect binds were emitted eagerly * in bind_param — they only read incoming registers, so they precede this. */ static void x64_bind_params_end(NativeTarget* t) { X64NativeTarget* a = x64_of(t); @@ -2586,14 +2878,18 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, plan->clobber_mask[c] = x64_clobber_mask(aregs, (NativeAllocClass)c); plan->return_mask[c] = x64_return_mask(abi, (NativeAllocClass)c); } - /* Indirect callee in a clobbered/arg register would be lost; stage in r11. */ - if (plan->callee.kind == NATIVE_LOC_REG && - (NativeAllocClass)plan->callee.cls == NATIVE_REG_INT && - plan->callee.v.reg != X64_R11) { - NativeLoc scratch = - native_loc_reg(plan->callee.type, NATIVE_REG_INT, X64_R11); - x64_move(t, scratch, plan->callee); - plan->callee = scratch; + /* 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 + * of its producer register and every allowed NativeLoc argument shape. */ + 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; + plan->callee = native_loc_reg(plan->callee.type, NATIVE_REG_INT, X64_R11); } { u32 next_int = (abi && abi->has_sret) ? 1u : 0u; @@ -2629,12 +2925,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]; - NativeAllocClass cls = - part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT; - Reg tmp = cls == NATIVE_REG_FP ? X64_TMP_FP : X64_TMP_INT; - NativeLoc tmpreg = native_loc_reg(desc->args[i].type, cls, tmp); - x64_load_part(t, tmpreg, desc->args[i], part->src_offset, part->size); - x64_store_outgoing_part(t, tail, stack, tmpreg, part->size); + x64_copy_outgoing_part(t, tail, stack, desc->args[i], part); stack += 8u; } continue; @@ -2668,10 +2959,7 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, m->size = part->size; x64_sync_slot(aregs, &next_int, &next_fp); } else { - Reg tmp = cls == NATIVE_REG_FP ? X64_TMP_FP : X64_TMP_INT; - NativeLoc tmpreg = native_loc_reg(desc->args[i].type, cls, tmp); - x64_load_part(t, tmpreg, desc->args[i], part->src_offset, part->size); - x64_store_outgoing_part(t, tail, stack, tmpreg, part->size); + x64_copy_outgoing_part(t, tail, stack, desc->args[i], part); stack += 8u; x64_sync_slot(aregs, &next_int, &next_fp); } @@ -2682,7 +2970,7 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, x64_emit_reg_arg_moves( t, moves, nmoves, (plan->callee.kind == NATIVE_LOC_REG && plan->callee.v.reg == X64_R11) - ? X64_TMP_INT + ? X64_RAX : X64_TMP_INT2); if (abi && abi->has_sret) { /* sret pointer in the first int-arg reg. A tail call forwards the @@ -2698,6 +2986,10 @@ 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) { + x64_pop_reg(t->mc, X64_R11); + a->call_callee_parked = 0; + } } /* Return value receipt. */ if (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) { @@ -2812,7 +3104,7 @@ static void x64_plan_ret(NativeTarget* t, const CGFuncDesc* fd, dst_addr.base_kind = NATIVE_ADDR_BASE_REG; dst_addr.base.reg = X64_R11; dst_addr.base_type = value->type; - src_addr = x64_loc_addr(a, *value, 0); + src_addr = x64_storage_addr(a, *value, 0); src_addr.base_type = value->type; memset(&access, 0, sizeof access); access.type = value->type; @@ -2830,6 +3122,33 @@ static void x64_plan_ret(NativeTarget* t, const CGFuncDesc* fd, u32 ni = 0, nf = 0; static const u32 ret_int_regs[2] = {X64_RAX, X64_RDX}; u16 p; + int exact_parts = 0; + for (p = 0; p < abi->ret.nparts; ++p) + if (abi->ret.parts[p].cls == ABI_CLASS_INT && + !x64_native_part_width(abi->ret.parts[p].size)) + exact_parts = 1; + + /* Generic write_phase materializes a register destination from its scalar + * type, so an I64 carrier for a 3/5/6/7-byte aggregate part would widen the + * source load to eight bytes. Once one exact part is present, marshal every + * return part here in ABI order: this preserves already-filled return + * registers and keeps each memory read bounded by ABIArgPart.size. */ + if (exact_parts) { + for (p = 0; p < abi->ret.nparts; ++p) { + const ABIArgPart* part = &abi->ret.parts[p]; + NativeAllocClass cls = + part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT; + KitCgTypeId pty = x64_part_scalar_type(part); + Reg rreg = cls == NATIVE_REG_FP ? (Reg)(X64_XMM0 + nf++) + : (Reg)ret_int_regs[ni++]; + NativeLoc dst = native_loc_reg(pty, cls, rreg); + x64_load_part(t, dst, *value, part->src_offset, part->size); + } + *out_rets = NULL; + *out_nrets = 0; + return; + } + for (p = 0; p < abi->ret.nparts; ++p) { const ABIArgPart* part = &abi->ret.parts[p]; NativeAllocClass cls = @@ -2886,14 +3205,15 @@ static void x64_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size, } emit_u32le(mc, (u32)aligned); } else { - /* rax = (size + 15) & ~15; sub rsp, rax. */ - emit_lea(mc, X64_RAX, rsz, 15); - emit_rex(mc, 1, 0, 0, X64_RAX); + /* r10 = (size + 15) & ~15; sub rsp, r10. */ + emit_lea(mc, X64_TMP_INT, rsz, 15); + emit_rex(mc, 1, 0, 0, X64_TMP_INT); { - u8 buf[3] = {X64_OPC_ALU_IMM8, modrm(3u, X64_ALU_SUB_AND, X64_RAX), 0xF0}; + u8 buf[3] = {X64_OPC_ALU_IMM8, + modrm(3u, X64_ALU_SUB_AND, X64_TMP_INT), 0xF0}; mc_emit_bytes(mc, buf, 3); } - emit_alu_rr(mc, 1, X64_OPC_ALU_SUB, X64_RSP, X64_RAX); + emit_alu_rr(mc, 1, X64_OPC_ALU_SUB, X64_RSP, X64_TMP_INT); } a->frame.has_alloca = 1; /* lea dst, [rsp + max_outgoing] — disp32 patched in func_end. */ @@ -3163,15 +3483,21 @@ static void x64_atomic_store(NativeTarget* t, NativeAddr addr, NativeLoc src, u32 base = x64_atomic_base(a, addr); u32 sr = loc_reg(src); if (mo == KIT_CG_MO_SEQ_CST) { - /* xchg [mem], r11 implicitly fences. Stage src in rax (r11 holds base). */ - if (sr != X64_RAX) emit_mov_rr(mc, w, X64_RAX, sr); + /* XCHG writes the prior memory value into its register operand. Stage the + * semantic source in backend-private R10 so that mutation is invisible to + * the allocator; R11 holds a materialized address. */ + if (base == X64_TMP_INT) { + emit_mov_rr(mc, 1, X64_TMP_INT2, base); + base = X64_TMP_INT2; + } + if (sr != X64_TMP_INT) emit_mov_rr(mc, w, X64_TMP_INT, sr); emit_lock_prefix(mc); - emit_rex(mc, w, X64_RAX, 0, base); + emit_rex(mc, w, X64_TMP_INT, 0, base); { u8 op = 0x87; /* xchg r/m, r */ mc_emit_bytes(mc, &op, 1); } - emit_mem_operand(mc, X64_RAX, base, 0); + emit_mem_operand(mc, X64_TMP_INT, base, 0); return; } emit_mov_store(mc, sz, sr, base, 0); @@ -3189,8 +3515,8 @@ static void x64_atomic_rmw(NativeTarget* t, KitCgAtomicOp op, NativeLoc dst, u32 vr = loc_reg(val); (void)mo; /* LOCK ops are full barriers. */ /* The rmw uses fixed rax (prior), rcx (new), rdx (val); the optimizer may - * have materialized the address into one of them, so keep it out (r11 is the - * int emit scratch, never an allocated operand). Stage before rdx is loaded. + * have materialized the address into one of them, so keep it out (R11 is + * backend-private, never an allocated operand). Stage before RDX is loaded. */ if (base == X64_RAX || base == X64_RCX || base == X64_RDX) { emit_mov_rr(mc, 1, X64_TMP_INT2, base); @@ -3272,8 +3598,8 @@ static void x64_atomic_cas(NativeTarget* t, NativeLoc prior, NativeLoc ok, (void)success; (void)failure; /* cmpxchg uses fixed rax (expected) and rcx (desired). The optimizer may have - * materialized the address into either; keep it out of both (r11 is the int - * emit scratch, never an allocated operand). */ + * materialized the address into either; keep it out of both (R11 is + * backend-private, never an allocated operand). */ if (base == X64_RAX || base == X64_RCX) { emit_mov_rr(mc, 1, X64_TMP_INT2, base); base = X64_TMP_INT2; @@ -3321,10 +3647,7 @@ static void x64_fence(NativeTarget* t, KitCgMemOrder mo) { * object. */ /* Resolve a va_list address into `scratch`, materializing it there if it is not - * already, so the va field-value scratch registers (rax / r10 / rdx) never - * alias it. At -O1 the optimizer may place the va_list pointer in any register - * — including those — and the va code would then clobber the pointer - * mid-sequence. */ + * already, so the backend-private field-value register never aliases it. */ static u32 x64_va_base(X64NativeTarget* a, NativeAddr ap, u32 scratch) { u32 base = x64_addr_to_base_reg(a, ap, scratch); if (base != scratch) { @@ -3364,24 +3687,26 @@ static void x64_va_start_core(X64NativeTarget* a, NativeAddr ap) { if (a->abi->shadow_space) { /* Win64: *ap = rbp + 16 + named_int*8 + named_stack. */ u32 first = 16u + a->next_param_int * 8u + a->next_param_stack; - emit_lea(mc, X64_RAX, X64_RBP, (i32)first); - emit_mov_store(mc, 8, X64_RAX, ap_base, 0); + emit_lea(mc, X64_TMP_INT, X64_RBP, (i32)first); + emit_mov_store(mc, 8, X64_TMP_INT, ap_base, 0); return; } { X64NativeSlot* rs = x64_slot_get(a, a->reg_save_slot); /* gp_offset = next_param_int * 8 */ - x64_emit_load_imm(mc, 0, X64_RAX, (i64)(a->next_param_int * 8u)); - emit_mov_store(mc, 4, X64_RAX, ap_base, 0); + x64_emit_load_imm(mc, 0, X64_TMP_INT, (i64)(a->next_param_int * 8u)); + emit_mov_store(mc, 4, X64_TMP_INT, ap_base, 0); /* fp_offset = 48 + next_param_fp * 16 */ - x64_emit_load_imm(mc, 0, X64_RAX, (i64)(48u + a->next_param_fp * 16u)); - emit_mov_store(mc, 4, X64_RAX, ap_base, 4); + x64_emit_load_imm(mc, 0, X64_TMP_INT, + (i64)(48u + a->next_param_fp * 16u)); + emit_mov_store(mc, 4, X64_TMP_INT, ap_base, 4); /* overflow_arg_area = rbp + 16 + next_param_stack */ - emit_lea(mc, X64_RAX, X64_RBP, (i32)(16u + a->next_param_stack)); - emit_mov_store(mc, 8, X64_RAX, ap_base, 8); + emit_lea(mc, X64_TMP_INT, X64_RBP, + (i32)(16u + a->next_param_stack)); + emit_mov_store(mc, 8, X64_TMP_INT, ap_base, 8); /* reg_save_area = rbp - reg_save_slot.off */ - emit_lea(mc, X64_RAX, X64_RBP, -(i32)rs->off); - emit_mov_store(mc, 8, X64_RAX, ap_base, 16); + emit_lea(mc, X64_TMP_INT, X64_RBP, -(i32)rs->off); + emit_mov_store(mc, 8, X64_TMP_INT, ap_base, 16); } } @@ -3393,15 +3718,15 @@ static void x64_va_arg_core(X64NativeTarget* a, NativeLoc dst, NativeAddr ap, int is_fp = native_loc_is_fp(dst); u32 dr = loc_reg(dst); u32 ap_base = x64_va_base(a, ap, X64_TMP_INT2); /* r11 */ - /* GPR scratch for the offset/address arithmetic. For integer results the - * destination is itself a (throwaway) scratch GPR — pass_native_emit fetches - * va_arg into a scratch and copies it to the real destination afterward — so + /* GPR temporary for the offset/address arithmetic. For integer results the + * destination is itself a throwaway operand temp — pass_native_emit fetches + * va_arg into a scoped register and writes it to the real destination — so * we reuse `dr` and touch no allocable register at all. FP results keep their - * value in an XMM register, so they borrow the reserved RAX emit scratch. + * value in an XMM register, so they borrow backend-private R10. * Either way only r11 (ap_base) and `gp` are used: the va_list fields are * advanced in memory (x64_add_mem_imm) and the reg-save base is folded in * with x64_add_reg_mem, so no third register is needed. */ - u32 gp = is_fp ? X64_RAX : dr; + u32 gp = is_fp ? X64_TMP_INT : dr; if (a->abi->shadow_space) { /* Win64: gp = *ap; load dr from [gp]; *ap += 8. */ emit_mov_load(mc, 8, 0, gp, ap_base, 0); @@ -3452,8 +3777,9 @@ static void x64_va_copy_core(X64NativeTarget* a, NativeAddr dst_ap, NativeAddr src_ap) { NativeTarget* t = &a->base; MCEmitter* mc = t->mc; - /* Resolve dst into r11, src into rax, and copy each qword through the fp emit - * scratch xmm14. Uses only reserved emit scratch (r11/rax/xmm14), so the + /* Resolve dst into R11, src into R10, and copy each qword through + * backend-private XMM14. Uses only reserved backend registers + * (R11/R10/XMM14), so the * optimizer's register choice for a va_list pointer can never be clobbered * and no allocable GPR (previously rdx) is consumed. */ u32 dst_base = x64_va_base(a, dst_ap, X64_TMP_INT2); @@ -3770,23 +4096,23 @@ static void x64_intrinsic(NativeTarget* t, IntrinKind kind, i = n; /* copy high-to-low so an overlapping dst > src is safe */ while (i >= 8u) { i -= 8u; - emit_mov_load(mc, 8, 0, X64_RAX, sr, (i32)i); - emit_mov_store(mc, 8, X64_RAX, dr, (i32)i); + emit_mov_load(mc, 8, 0, X64_TMP_INT, sr, (i32)i); + emit_mov_store(mc, 8, X64_TMP_INT, dr, (i32)i); } while (i >= 4u) { i -= 4u; - emit_mov_load(mc, 4, 0, X64_RAX, sr, (i32)i); - emit_mov_store(mc, 4, X64_RAX, dr, (i32)i); + emit_mov_load(mc, 4, 0, X64_TMP_INT, sr, (i32)i); + emit_mov_store(mc, 4, X64_TMP_INT, dr, (i32)i); } while (i >= 2u) { i -= 2u; - emit_mov_load(mc, 2, 0, X64_RAX, sr, (i32)i); - emit_mov_store(mc, 2, X64_RAX, dr, (i32)i); + emit_mov_load(mc, 2, 0, X64_TMP_INT, sr, (i32)i); + emit_mov_store(mc, 2, X64_TMP_INT, dr, (i32)i); } while (i >= 1u) { i -= 1u; - emit_mov_load(mc, 1, 0, X64_RAX, sr, (i32)i); - emit_mov_store(mc, 1, X64_RAX, dr, (i32)i); + emit_mov_load(mc, 1, 0, X64_TMP_INT, sr, (i32)i); + emit_mov_store(mc, 1, X64_TMP_INT, dr, (i32)i); } return; } @@ -4110,7 +4436,11 @@ static X64AsmSavedClobber* x64_asm_save_callee_clobbers(X64NativeTarget* a, return saved; } -/* ---- NativeTarget (optimizer) asm hook ---- */ +/* ---- NativeTarget (optimizer) asm hook ---- + * + * Register placement, staging, and output writeback belong to the optimized + * emitter. This hook binds those concrete locations and only materializes + * memory-constraint bases in backend-private registers. */ static NativeAddr x64_asm_loc_to_addr(X64NativeTarget* a, SrcLoc loc, NativeLoc src) { @@ -4157,50 +4487,6 @@ static Reg x64_asm_native_mem_base(X64NativeTarget* a, SrcLoc loc, return dst; } -static Reg x64_asm_stage_reg(X64NativeTarget* a, SrcLoc loc, - NativeAllocClass cls, u32* nint, u32* nfp) { - static const Reg int_regs[] = {X64_TMP_INT, X64_TMP_INT2}; - static const Reg fp_regs[] = {X64_TMP_FP, X64_TMP_FP2}; - if (cls == NATIVE_REG_FP) { - if (*nfp >= (u32)(sizeof fp_regs / sizeof fp_regs[0])) - x64_asm_panic_at(a->base.c, loc, "too many staged fp asm operands"); - return fp_regs[(*nfp)++]; - } - if (*nint >= (u32)(sizeof int_regs / sizeof int_regs[0])) - x64_asm_panic_at(a->base.c, loc, "too many staged integer asm operands"); - return int_regs[(*nint)++]; -} - -static void x64_asm_load_loc_to_reg(X64NativeTarget* a, SrcLoc loc, - NativeLoc src, NativeLoc dst) { - NativeTarget* t = &a->base; - NativeAllocClass cls = (NativeAllocClass)dst.cls; - if (src.kind == NATIVE_LOC_REG) { - if (src.v.reg != dst.v.reg || src.cls != dst.cls) t->move(t, dst, src); - return; - } - if (src.kind == NATIVE_LOC_IMM) { - if (cls != NATIVE_REG_INT) - x64_asm_panic_at(t->c, loc, - "floating-point immediate asm input is unsupported"); - t->load_imm(t, dst, src.v.imm); - return; - } - x64_emit_mem(a, 1, dst, x64_asm_loc_to_addr(a, loc, src), - native_mem_for_type(t, dst.type, loc_size32(t, dst))); -} - -static void x64_asm_store_reg_to_loc(X64NativeTarget* a, SrcLoc loc, - NativeLoc dst, NativeLoc src) { - NativeTarget* t = &a->base; - if (dst.kind == NATIVE_LOC_REG) { - if (dst.v.reg != src.v.reg || dst.cls != src.cls) t->move(t, dst, src); - return; - } - x64_emit_mem(a, 0, src, x64_asm_loc_to_addr(a, loc, dst), - native_mem_for_type(t, src.type, loc_size32(t, src))); -} - static void x64_asm_native_panic(NativeTarget* t, SrcLoc loc, const char* msg) { x64_asm_panic_at(t->c, loc, msg); @@ -4211,23 +4497,6 @@ static Reg x64_asm_native_mem_base_hook(NativeTarget* t, SrcLoc loc, return x64_asm_native_mem_base(x64_of(t), loc, src, ntmp); } -static Reg x64_asm_native_stage_reg_hook(NativeTarget* t, SrcLoc loc, - NativeAllocClass cls, u32* ntmp, - u32* nstage_int, u32* nstage_fp) { - (void)nstage_int; - return x64_asm_stage_reg(x64_of(t), loc, cls, ntmp, nstage_fp); -} - -static void x64_asm_native_load_loc_hook(NativeTarget* t, SrcLoc loc, - NativeLoc src, NativeLoc dst) { - x64_asm_load_loc_to_reg(x64_of(t), loc, src, dst); -} - -static void x64_asm_native_store_loc_hook(NativeTarget* t, SrcLoc loc, - NativeLoc dst, NativeLoc src) { - x64_asm_store_reg_to_loc(x64_of(t), loc, dst, src); -} - static void x64_asm_native_run_template_hook( NativeTarget* t, const char* tmpl, const AsmConstraint* outs, u32 nout, Operand* bound_outs, const AsmConstraint* ins, u32 nin, Operand* bound_ins, @@ -4247,15 +4516,10 @@ static void x64_asm_block_native(NativeTarget* t, const char* tmpl, X64NativeTarget* a = x64_of(t); SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0}; static const NativeAsmNativeHooks hooks = { - .opk_reg = X64_INLINE_OPK_REG, - .opcls_fp = X64_INLINE_OPCLS_FP, .panic = x64_asm_native_panic, .bound_reg = x64_asm_bound_reg, .bound_mem = x64_asm_bound_mem, .mem_base = x64_asm_native_mem_base_hook, - .stage_reg = x64_asm_native_stage_reg_hook, - .load_loc_to_reg = x64_asm_native_load_loc_hook, - .store_reg_to_loc = x64_asm_native_store_loc_hook, .run_template = x64_asm_native_run_template_hook, }; native_asm_bind_native_operands(t, loc, tmpl, outs, nout, out_locs, ins, nin, @@ -4274,9 +4538,9 @@ static void x64_set_loc(NativeTarget* t, SrcLoc loc) { * 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 * (remainder/sign); a variable shift uses cl; cmpxchg/xadd loops use - * rax/rcx/rdx; an FP va_arg borrows rax for the gp/fp offset (an integer va_arg - * does the offset arithmetic in its own destination register, so it clobbers - * nothing). */ + * rax/rcx/rdx; bitfield store uses the same trio for its read-modify-write. + * 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]) { mask[0] = mask[1] = mask[2] = 0; @@ -4310,22 +4574,21 @@ static int x64_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, default: return 0; } + case NATIVE_MOP_BITFIELD_LOAD: + /* The only non-destination temporary is backend-private R11. */ + return 0; + case NATIVE_MOP_BITFIELD_STORE: + mask[NATIVE_REG_INT] = + (1u << X64_RAX) | (1u << X64_RCX) | (1u << X64_RDX); + return 1; case NATIVE_MOP_ATOMIC_CAS: case NATIVE_MOP_ATOMIC_RMW: mask[NATIVE_REG_INT] = (1u << X64_RAX) | (1u << X64_RCX) | (1u << X64_RDX); return 1; case NATIVE_MOP_VA_START: - /* x64_va_start_core materializes the va_list field values through RAX - * (the ap pointer itself lands in the reserved r11 scratch). RAX is the - * return register, so the allocator may otherwise keep a live value there - * across the op. */ - mask[NATIVE_REG_INT] = (1u << X64_RAX); - return 1; case NATIVE_MOP_VA_ARG: - if (!op->result_is_fp) return 0; - mask[NATIVE_REG_INT] = (1u << X64_RAX); - return 1; + return 0; case NATIVE_MOP_INTRINSIC: /* The unsigned multiply-overflow intrinsic emits a one-operand MUL, whose * rdx:rax product clobbers both registers. The signed variant uses a @@ -4342,10 +4605,13 @@ static int x64_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op, 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) | (1u << X64_R10) | (1u << X64_R11); + (1u << X64_R9); return 1; } return 0; @@ -4570,13 +4836,14 @@ 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 emit scratch (r10,r11), driver scratch (r8,r9), rax (reserved; - * only self-allocated here when explicitly pinned), and sp/bp. */ - .scratch_int = (1u << X64_RAX) | (1u << X64_R11) | (1u << X64_RSP) | - (1u << X64_RBP) | (1u << X64_R8) | (1u << X64_R9) | - (1u << X64_R10), - .scratch_fp = (1u << X64_XMM4) | (1u << X64_XMM5) | - (1u << (X64_XMM0 + 14)) | (1u << X64_XMM15), + /* 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, diff --git a/src/build/trace.c b/src/build/trace.c @@ -333,8 +333,10 @@ static int emit_child_row(KitWriter* out, const uint8_t id[BUILD_HASH_LEN]) { return emit_bytes(out, "\n"); } -static int emit_sorted_children(const uint8_t (*rows)[BUILD_HASH_LEN], size_t n, - KitWriter* out, char* err, size_t errcap) { +/* Use a flat byte view here: strict C11 cannot add const through a + * pointer-to-array conversion (that becomes well-defined only in C23). */ +static int emit_sorted_children(const uint8_t* rows, size_t n, KitWriter* out, + char* err, size_t errcap) { const uint8_t* prev = NULL; size_t emitted = 0; if (n && !rows) return set_err(err, errcap, "missing child rows"); @@ -343,7 +345,7 @@ static int emit_sorted_children(const uint8_t (*rows)[BUILD_HASH_LEN], size_t n, const uint8_t* best = NULL; size_t i; for (i = 0; i < n; ++i) { - const uint8_t* cur = rows[i]; + const uint8_t* cur = rows + i * BUILD_HASH_LEN; int after_prev = !prev || id_cmp(prev, cur) < 0; if (after_prev && (!best || id_cmp(cur, best) < 0)) best = cur; } @@ -486,8 +488,8 @@ int build_deepset_emit(const BuildDeepSet* t, KitWriter* out, char* err, if (emit_sorted_blobs(t->blobs, t->n_blobs, out, err, errcap) != BUILD_OK) return BUILD_ERR; - if (emit_sorted_children(t->children, t->n_children, out, err, errcap) != - BUILD_OK) + if (emit_sorted_children(t->children ? t->children[0] : NULL, t->n_children, + out, err, errcap) != BUILD_OK) return BUILD_ERR; return kit_writer_status(out) == KIT_OK ? BUILD_OK : BUILD_ERR; } diff --git a/src/cg/call.c b/src/cg/call.c @@ -84,9 +84,10 @@ void api_pack_call_arg(KitCg* g, CGLocal* out, KitCgTypeId fty, u32 idx) { * sub-array so the LIFO pop yields arg0 first, then pop+materialize forward. * * Why forward: an argument materialized into a register lands in the first free - * -O0 cache register, and the cache pool front-loads the ABI argument registers - * (x0,x1,.. — see aa_int_allocable). Producing arg0 first therefore lands it in - * x0, arg1 in x1, .. so the per-call parallel-copy collapses to no-ops, instead + * -O0 cache register, and the target's `ndt_allocable` order front-loads the + * ABI argument registers (x0,x1,.. on aa64). Producing arg0 first therefore + * lands it in x0, arg1 in x1, .. so the per-call parallel-copy collapses to + * no-ops, instead * of the reverse order leaving each value in the *wrong* arg register and * forcing a permutation shuffle. Matches tcc's eager left-to-right argument * emission. diff --git a/src/cg/cgir.h b/src/cg/cgir.h @@ -267,7 +267,7 @@ typedef enum MemFlag { * the whole register) instead of a zero-extending load plus a CV_SEXT convert. * Non-bitfield integer loads only. A backend that does not honor it ignores * the bit and emits a plain load; the shared -O0 NDT only elides the following - * CV_SEXT when the backend advertises NativeRegInfo.ndt_load_sext, so leaving + * CV_SEXT when the backend advertises NativeRegInfo.load_sext, so leaving * the convert in place stays correct there. */ MF_SEXT_LOAD = 1u << 6, } MemFlag; diff --git a/src/cg/native_asm.c b/src/cg/native_asm.c @@ -158,28 +158,6 @@ NativeAsmRegPinStatus native_asm_resolve_pin(NativeTarget* t, Sym reg, return NATIVE_ASM_REG_PIN_OK; } -NativeAsmPinnedLoc native_asm_prepare_pinned_loc(NativeTarget* t, Sym reg, - const char* constraint, - KitCgTypeId type, - NativeLoc loc) { - NativeAsmPinnedLoc out; - NativeAsmRegPin pin; - memset(&out, 0, sizeof out); - out.loc = loc; - out.pin_status = native_asm_resolve_pin(t, reg, constraint, &pin); - if (out.pin_status == NATIVE_ASM_REG_PIN_ABSENT) return out; - out.has_pin = 1u; - if (out.pin_status != NATIVE_ASM_REG_PIN_OK) return out; - if (loc.kind != NATIVE_LOC_REG) { - out.loc = native_loc_reg(type, pin.cls, pin.reg); - out.needs_stage = 1u; - return out; - } - if ((Reg)loc.v.reg != pin.reg || (NativeAllocClass)loc.cls != pin.cls) - out.wrong_reg = 1u; - return out; -} - const char* native_asm_pin_status_message(NativeAsmRegPinStatus st) { switch (st) { case NATIVE_ASM_REG_PIN_ABSENT: @@ -234,8 +212,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->scratch_int; - used_fp = clob_fp | h->scratch_fp; + used_int = clob_int | h->reserved_int_mask; + used_fp = clob_fp | h->reserved_fp_mask; for (i = 0; i < nout; ++i) { const char* body = native_asm_constraint_body(outs[i].str); @@ -260,14 +238,19 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, &used_fp); if (info.cls == NATIVE_REG_FP) { used_fp |= 1u << reg; - if (info.fixed_reg != REG_NONE) clob_fp |= 1u << reg; + /* Binding itself overwrites the staging register, independently of + * whether the template names it as a clobber. Include every bound + * register in the preservation set so a target may safely draw a + * high-arity direct-asm operand from its callee-saved pool. */ + clob_fp |= 1u << reg; } else { used_int |= 1u << reg; - if (info.fixed_reg != REG_NONE) clob_int |= 1u << reg; + clob_int |= 1u << reg; } 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); + clob_int |= 1u << reg; h->bound_mem(&bound_outs[i], type, reg); } else { h->panic(d, "unsupported output constraint"); @@ -308,10 +291,10 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, &used_fp); if (info.cls == NATIVE_REG_FP) { used_fp |= 1u << reg; - if (info.fixed_reg != REG_NONE) clob_fp |= 1u << reg; + clob_fp |= 1u << reg; } else { used_int |= 1u << reg; - if (info.fixed_reg != REG_NONE) clob_int |= 1u << reg; + clob_int |= 1u << reg; } h->bound_reg(&bound_ins[i], type, info.cls, reg); } else if (body[0] == 'i') { @@ -320,6 +303,7 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, 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); + clob_int |= 1u << reg; h->bound_mem(&bound_ins[i], type, reg); } else { h->panic(d, "unsupported input constraint"); @@ -373,16 +357,32 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, for (i = nsaved; i > 0; --i) h->restore_one(d, saved, i - 1u); } -static void native_asm_bind_native_one(NativeTarget* t, SrcLoc loc, - Operand* out, const char* constraint, - KitCgTypeId type, NativeLoc src, - u32* ntmp, - const NativeAsmNativeHooks* h) { - const char* body = native_asm_constraint_body(constraint); +static void native_asm_validate_native_pin( + NativeTarget* t, SrcLoc loc, const AsmConstraint* constraint, NativeLoc src, + const NativeAsmNativeHooks* h) { + NativeAsmRegPin pin; + NativeAsmRegPinStatus status; + if (!constraint->reg) return; + status = native_asm_resolve_pin(t, constraint->reg, constraint->str, &pin); + if (status != NATIVE_ASM_REG_PIN_OK) + h->panic(t, loc, native_asm_pin_status_message(status)); + if (src.kind != NATIVE_LOC_REG || src.cls != (u8)pin.cls || + src.v.reg != pin.reg) + h->panic(t, loc, "hard-register asm operand in wrong register"); +} + +static void native_asm_bind_native_one( + NativeTarget* t, SrcLoc loc, Operand* out, + const AsmConstraint* constraint, KitCgTypeId type, NativeLoc src, + u32* ntmp, const NativeAsmNativeHooks* h) { + const char* body = native_asm_constraint_body(constraint->str); NativeAsmConstraintInfo info; - if (native_asm_constraint_reg_info(t, constraint, &info)) { + native_asm_validate_native_pin(t, loc, constraint, src, h); + if (native_asm_constraint_reg_info(t, constraint->str, &info)) { if (src.kind != NATIVE_LOC_REG) h->panic(t, loc, "register asm operand not in a register"); + if (src.cls != (u8)info.cls) + h->panic(t, loc, "register asm operand has wrong register class"); if (info.fixed_reg != REG_NONE && info.fixed_reg != (Reg)src.v.reg) h->panic(t, loc, "fixed-register asm operand in wrong register"); if (info.allowed_mask && @@ -412,37 +412,12 @@ void native_asm_bind_native_operands( Compiler* c = t->c; Operand* bound_outs = nout ? arena_zarray(c->tu, Operand, nout) : NULL; Operand* bound_ins = nin ? arena_zarray(c->tu, Operand, nin) : NULL; - u8* staged_outs = nout ? arena_zarray(c->tu, u8, nout) : NULL; - u32 ntmp = 0, nstage_int = 0, nstage_fp = 0; + u32 ntmp = 0; for (u32 i = 0; i < nout; ++i) { KitCgTypeId type = outs[i].type ? outs[i].type : out_locs[i].type; - NativeLoc outloc = out_locs[i]; - NativeAsmConstraintInfo info; - NativeAsmPinnedLoc pinned = - native_asm_prepare_pinned_loc(t, outs[i].reg, outs[i].str, type, outloc); - if (pinned.has_pin) { - if (pinned.pin_status != NATIVE_ASM_REG_PIN_OK) - h->panic(t, loc, native_asm_pin_status_message(pinned.pin_status)); - if (pinned.wrong_reg) - h->panic(t, loc, "hard-register asm operand in wrong register"); - outloc = pinned.loc; - if (pinned.needs_stage) { - staged_outs[i] = 1u; - if (outs[i].dir == KIT_CG_ASM_INOUT) - h->load_loc_to_reg(t, loc, out_locs[i], outloc); - } - } else if (native_asm_constraint_reg_info(t, outs[i].str, &info) && - info.allowed_mask == 0 && outloc.kind != NATIVE_LOC_REG) { - Reg r = - h->stage_reg(t, loc, info.cls, &ntmp, &nstage_int, &nstage_fp); - outloc = native_loc_reg(type, info.cls, r); - staged_outs[i] = 1u; - if (outs[i].dir == KIT_CG_ASM_INOUT) - h->load_loc_to_reg(t, loc, out_locs[i], outloc); - } - native_asm_bind_native_one(t, loc, &bound_outs[i], outs[i].str, type, - outloc, &ntmp, h); + native_asm_bind_native_one(t, loc, &bound_outs[i], &outs[i], type, + out_locs[i], &ntmp, h); } for (u32 i = 0; i < nin; ++i) { @@ -455,45 +430,20 @@ void native_asm_bind_native_operands( h->panic(t, loc, "matching constraint out of range"); if (native_asm_constraint_early(outs[matched].str)) h->panic(t, loc, "matching input names early-clobber output"); + if (out_locs[matched].kind != NATIVE_LOC_REG || + in_locs[i].kind != NATIVE_LOC_REG || + out_locs[matched].cls != in_locs[i].cls || + out_locs[matched].v.reg != in_locs[i].v.reg) + h->panic(t, loc, "matching asm operands are in different registers"); bound_ins[i] = bound_outs[matched]; continue; } type = ins[i].type ? ins[i].type : in_locs[i].type; inloc = in_locs[i]; - { - NativeAsmConstraintInfo info; - NativeAsmPinnedLoc pinned = - native_asm_prepare_pinned_loc(t, ins[i].reg, ins[i].str, type, inloc); - if (pinned.has_pin) { - if (pinned.pin_status != NATIVE_ASM_REG_PIN_OK) - h->panic(t, loc, native_asm_pin_status_message(pinned.pin_status)); - if (pinned.wrong_reg) - h->panic(t, loc, "hard-register asm operand in wrong register"); - inloc = pinned.loc; - if (pinned.needs_stage) - h->load_loc_to_reg(t, loc, in_locs[i], inloc); - } else if (native_asm_constraint_reg_info(t, ins[i].str, &info) && - info.allowed_mask == 0 && inloc.kind != NATIVE_LOC_REG) { - Reg r = - h->stage_reg(t, loc, info.cls, &ntmp, &nstage_int, &nstage_fp); - inloc = native_loc_reg(type, info.cls, r); - h->load_loc_to_reg(t, loc, in_locs[i], inloc); - } - } - native_asm_bind_native_one(t, loc, &bound_ins[i], ins[i].str, type, inloc, + native_asm_bind_native_one(t, loc, &bound_ins[i], &ins[i], type, inloc, &ntmp, h); } h->run_template(t, tmpl, outs, nout, bound_outs, ins, nin, bound_ins, clobbers, nclob); - - for (u32 i = 0; i < nout; ++i) { - NativeAllocClass cls; - NativeLoc src; - if (!staged_outs || !staged_outs[i]) continue; - if (bound_outs[i].kind != h->opk_reg) continue; - cls = bound_outs[i].pad[0] == h->opcls_fp ? NATIVE_REG_FP : NATIVE_REG_INT; - src = native_loc_reg(bound_outs[i].type, cls, (Reg)bound_outs[i].v.local); - h->store_reg_to_loc(t, loc, out_locs[i], src); - } } diff --git a/src/cg/native_asm.h b/src/cg/native_asm.h @@ -64,14 +64,6 @@ typedef struct NativeAsmConstraintInfo { u32 allowed_mask; /* 0 means any valid register in cls. */ } NativeAsmConstraintInfo; -typedef struct NativeAsmPinnedLoc { - NativeLoc loc; - NativeAsmRegPinStatus pin_status; - u8 has_pin; - u8 needs_stage; - u8 wrong_reg; -} NativeAsmPinnedLoc; - int native_asm_constraint_reg_info(NativeTarget* t, const char* constraint, NativeAsmConstraintInfo* out); int native_asm_constraint_is_reg(NativeTarget* t, const char* constraint); @@ -83,10 +75,6 @@ int native_asm_constraint_is_reg(NativeTarget* t, const char* constraint); NativeAsmRegPinStatus native_asm_resolve_pin(NativeTarget* t, Sym reg, const char* constraint, NativeAsmRegPin* out); -NativeAsmPinnedLoc native_asm_prepare_pinned_loc(NativeTarget* t, Sym reg, - const char* constraint, - KitCgTypeId type, - NativeLoc loc); const char* native_asm_pin_status_message(NativeAsmRegPinStatus st); int native_asm_constraint_reg_class(const char* constraint, NativeAllocClass* cls_out); @@ -108,11 +96,11 @@ int native_asm_constraint_reg_class(const char* constraint, typedef struct NativeDirectTarget NativeDirectTarget; typedef struct NativeAsmDirectHooks { - /* Registers the operand allocator must never hand out (emit scratch, ABI - * fixed regs, frame pointer, ...), seeded into the used masks alongside the - * clobber masks. Per-arch. */ - u32 scratch_int; - u32 scratch_fp; + /* 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; @@ -134,9 +122,10 @@ typedef struct NativeAsmDirectHooks { * naming). */ void (*clobber_masks)(Compiler* c, SrcLoc loc, const Sym* clobbers, u32 nclob, u32* int_mask, u32* fp_mask); - /* Spill the callee-saved registers the asm clobbers, returning an opaque, - * block-lived save record plus its element count. restore_one undoes element - * idx; the driver replays them in reverse. */ + /* Spill the callee-saved registers the asm clobbers or the direct binder + * uses for operand/address staging, returning an opaque, block-lived save + * record plus its element count. restore_one undoes element idx; the driver + * replays them in reverse. */ void* (*save_callee_clobbers)(NativeDirectTarget* d, u32 int_mask, u32 fp_mask, u32* nsaved_out); void (*restore_one)(NativeDirectTarget* d, void* saved, u32 idx); @@ -168,28 +157,17 @@ void native_asm_bind_direct_operands(NativeDirectTarget* d, const char* tmpl, /* ---- Optimized NativeTarget inline-asm binding ---- * - * The optimizer allocates the operand locations before final native emission. - * This shared driver validates those locations against the same - * NativeAsmConstraintInfo used by regalloc, stages frame-resident unrestricted - * register operands through reserved scratch registers, binds memory operands to - * concrete base registers, runs the arch assembler, and stores staged outputs - * back. */ + * The optimizer's native emitter owns register placement, staging, and output + * writeback. This shared driver is deliberately narrower: it validates and + * binds the concrete register/immediate locations supplied by the emitter, + * materializes memory-operand bases through the backend, and runs the arch + * assembler. */ typedef struct NativeAsmNativeHooks { - u8 opk_reg; - u8 opcls_fp; - u8 pad[2]; - void (*panic)(NativeTarget* t, SrcLoc loc, const char* msg); void (*bound_reg)(Operand* out, KitCgTypeId type, NativeAllocClass cls, Reg reg); void (*bound_mem)(Operand* out, KitCgTypeId type, Reg base); Reg (*mem_base)(NativeTarget* t, SrcLoc loc, NativeLoc src, u32* ntmp); - Reg (*stage_reg)(NativeTarget* t, SrcLoc loc, NativeAllocClass cls, u32* ntmp, - u32* nstage_int, u32* nstage_fp); - void (*load_loc_to_reg)(NativeTarget* t, SrcLoc loc, NativeLoc src, - NativeLoc dst); - void (*store_reg_to_loc)(NativeTarget* t, SrcLoc loc, NativeLoc dst, - NativeLoc src); void (*run_template)(NativeTarget* t, const char* tmpl, const AsmConstraint* outs, u32 nout, Operand* bound_outs, const AsmConstraint* ins, u32 nin, Operand* bound_ins, diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c @@ -186,8 +186,8 @@ static u32 nd_acquirable_mask(const NativeAllocClassInfo* ci) { u32 m = 0u, i; for (i = 0; i < ci->nscratch; ++i) if (ci->scratch[i] < 32u) m |= 1u << ci->scratch[i]; - for (i = 0; i < ci->nallocable; ++i) - if (ci->allocable[i] < 32u) m |= 1u << ci->allocable[i]; + for (i = 0; i < ci->ndt_allocable_count; ++i) + if (ci->ndt_allocable[i] < 32u) m |= 1u << ci->ndt_allocable[i]; return m; } @@ -248,8 +248,8 @@ static Reg nd_scratch_acquire_span(NativeDirectTarget* d, NativeAllocClass cls, } return r; } - regs = ci->allocable; - nregs = ci->nallocable; + regs = ci->ndt_allocable; + nregs = ci->ndt_allocable_count; } /* Under pressure, evict the LRU non-pinned cached local (spilling it to its * home) and reuse its register as a scratch temporary. A wide run evicts every @@ -263,8 +263,8 @@ static Reg nd_scratch_acquire_span(NativeDirectTarget* d, NativeAllocClass cls, return r; } } else { - const Reg* alloc = ci->allocable; - for (u32 i = 0; i < ci->nallocable; ++i) { + const Reg* alloc = ci->ndt_allocable; + for (u32 i = 0; i < ci->ndt_allocable_count; ++i) { Reg r = alloc[i]; u32 k; int run_ok = 1; @@ -697,8 +697,8 @@ static Reg nd_pick_cache_victim(NativeDirectTarget* d, NativeAllocClass cls) { const NativeAllocClassInfo* ci = nd_class_info(d, cls); Reg best = REG_NONE; u32 best_use = 0; - for (u32 i = 0; i < ci->nallocable; ++i) { - Reg r = ci->allocable[i]; + for (u32 i = 0; i < ci->ndt_allocable_count; ++i) { + Reg r = ci->ndt_allocable[i]; u32 use; if (r >= 32u) continue; if (d->reg_owner[cls][r] == CG_LOCAL_NONE) continue; @@ -1786,7 +1786,7 @@ static void nd_load(CgTarget* t, Operand dst, Operand addr, MemAccess mem) { d->native->load(d->native, reg, naddr, mem); nd_dst_writeback(d, dst, reg); /* A narrow integer load fills the whole register: a plain ldrb/ldrh/ldr-w - * zero-extends, and (when the backend advertises ndt_load_sext and the access + * zero-extends, and (when the backend advertises load_sext and the access * is MF_SEXT_LOAD) a sign-extending ldrsb/ldrsh sign-extends. Either way a * subsequent matching widen (CV_ZEXT / CV_SEXT) is a no-op, so record which * one this load produced — register-resident only — so nd_convert can drop the @@ -1797,7 +1797,7 @@ static void nd_load(CgTarget* t, Operand dst, Operand addr, MemAccess mem) { NativeDirectLocal* l = nd_local(d, dst.v.local); if (l->reg == reg.v.reg) { if ((mem.flags & MF_SEXT_LOAD) && d->reg_info && - d->reg_info->ndt_load_sext) + d->reg_info->load_sext) l->load_sext = 1; else l->load_zext = 1; @@ -2597,8 +2597,8 @@ CgTarget* native_direct_target_new(Compiler* c, ObjBuilder* obj, u32 mask, n = 0; if (!ci) continue; mask = nd_caller_saved_mask(d, (NativeAllocClass)cls); - for (u32 i = 0; i < ci->nallocable; ++i) { - Reg r = ci->allocable[i]; + for (u32 i = 0; i < ci->ndt_allocable_count; ++i) { + Reg r = ci->ndt_allocable[i]; if (r >= 32u) continue; if (caller_only && !(mask & (1u << r))) continue; d->cache_pool[cls][n++] = r; diff --git a/src/cg/native_direct_target.h b/src/cg/native_direct_target.h @@ -50,7 +50,7 @@ typedef struct NativeDirectLocal { u8 load_zext; /* The cached register holds this local's value SIGN-extended to the full * register, from an MF_SEXT_LOAD narrow load that the backend honored with a - * sign-extending load (aa64 ldrsb/ldrsh, gated on ndt_load_sext). While set, a + * sign-extending load (aa64 ldrsb/ldrsh, gated on load_sext). While set, a * CV_SEXT widening it to any wider integer is a no-op and is elided by * nd_convert. Set only in nd_load; cleared exactly like load_zext (a spill + * plain reload would zero-extend, so clearing it forces the real extend to run diff --git a/src/link/link_relocatable.c b/src/link/link_relocatable.c @@ -183,6 +183,7 @@ static ObjSymId rel_copy_symbol(Linker* l, ObjBuilder* out, if (id == OBJ_SYM_NONE) compiler_panic(l->c, SRCLOC_NONE, "link -r: oom copying symbol"); if (s->flags) obj_symbol_set_flags(out, id, s->flags); + if (s->atom_subordinate) obj_symbol_set_atom_subordinate(out, id, 1); obj_sym_mark_referenced(out, id); return id; } diff --git a/src/obj/macho/emit.c b/src/obj/macho/emit.c @@ -471,6 +471,8 @@ void emit_macho(Compiler* c, ObjBuilder* ob, Writer* w) { * field) are already excluded by read_macho before stashing, * so a plain OR can't double-count. */ n_desc |= s->flags; + if (s->atom_subordinate && (type & N_TYPE) == N_SECT) + n_desc |= N_ALT_ENTRY; { ObjAtomId aid = obj_atom_find_symbol(ob, e.id); const ObjAtom* atom = obj_atom_get(ob, aid); diff --git a/src/obj/macho/macho.h b/src/obj/macho/macho.h @@ -233,6 +233,7 @@ typedef struct MachNlist64 { #define N_NO_DEAD_STRIP 0x0020u #define N_WEAK_REF 0x0040u #define N_WEAK_DEF 0x0080u +#define N_ALT_ENTRY 0x0200u #define REFERENCE_FLAG_UNDEFINED_NON_LAZY 0x0u #define REFERENCE_FLAG_UNDEFINED_LAZY 0x1u diff --git a/src/obj/macho/read.c b/src/obj/macho/read.c @@ -635,7 +635,7 @@ ObjBuilder* read_macho(Compiler* c, const char* name, const u8* data, (SymKind)kind, sec_id, value, size, cmnalign); obj_sym_mark_referenced(ob, id); if ((mh_flags & MH_SUBSECTIONS_VIA_SYMBOLS) && type_field == N_SECT && - sec_id != OBJ_SEC_NONE) { + sec_id != OBJ_SEC_NONE && !(n_desc & N_ALT_ENTRY)) { MAtomCand* ac = &atom_cands[natom_cands++]; ac->sec = sec_id; ac->sym = id; @@ -654,8 +654,11 @@ ObjBuilder* read_macho(Compiler* c, const char* name, const u8* data, * back in. */ u16 desc_pass = n_desc; desc_pass &= (u16) ~(N_WEAK_DEF | N_WEAK_REF); + if (type_field == N_SECT) desc_pass &= (u16)~N_ALT_ENTRY; if (kind == SK_COMMON) desc_pass &= 0x00ff; /* drop align field */ if (desc_pass) obj_symbol_set_flags(ob, id, desc_pass); + if (type_field == N_SECT && (n_desc & N_ALT_ENTRY)) + obj_symbol_set_atom_subordinate(ob, id, 1); sym_macho_to_obj[i] = id; } diff --git a/src/obj/obj.c b/src/obj/obj.c @@ -742,6 +742,7 @@ static ObjSymId obj_symbol_make(ObjBuilder* ob, Sym name, SymBind bind, s->value = value; s->size = size; s->common_align = common_align; + s->atom_subordinate = 0; /* First-wins: record the lowest id for this name so obj_symbol_find returns * the same symbol the linear scan would. Later same-name symbols (legal for * STB_LOCAL) do not overwrite. */ @@ -821,6 +822,15 @@ void obj_symbol_set_flags(ObjBuilder* ob, ObjSymId id, u16 flags) { s->flags = flags; } +void obj_symbol_set_atom_subordinate(ObjBuilder* ob, ObjSymId id, + int subordinate) { + ObjSym* s; + if (id == OBJ_SYM_NONE) return; + s = Symbols_at(&ob->symbols, id); + if (!s) return; + s->atom_subordinate = subordinate ? 1u : 0u; +} + void obj_reloc(ObjBuilder* ob, ObjSecId section_id, u32 offset, RelocKind kind, ObjSymId sym, i64 addend) { obj_reloc_ex(ob, section_id, offset, kind, sym, addend, 1, 0); diff --git a/src/obj/obj.h b/src/obj/obj.h @@ -399,6 +399,10 @@ typedef struct ObjSym { * is also folded into the sweep, so emit-time symbol loops only need to * check `removed`. */ u8 removed; + /* This symbol names an address subordinate to the preceding content unit + * rather than starting a separately collectible/reorderable atom. Mach-O + * writes this semantic as N_ALT_ENTRY; other formats may ignore it. */ + u8 atom_subordinate; } ObjSym; typedef struct ObjGroup { @@ -630,6 +634,7 @@ int obj_weak_alias_at(const ObjBuilder*, u32 i, ObjSymId* sym_out, * to use the same field for their own pass-through; the contract is * "bits go in / same bits come out", not a shared semantic. */ void obj_symbol_set_flags(ObjBuilder*, ObjSymId, u16 flags); +void obj_symbol_set_atom_subordinate(ObjBuilder*, ObjSymId, int subordinate); /* ---- read side (linker, file emitters, objdump) ---- */ u32 obj_section_count(const ObjBuilder*); diff --git a/src/opt/cg_ir_lower.c b/src/opt/cg_ir_lower.c @@ -539,7 +539,11 @@ static void materialize_frame_base(CgIrLower* l, u32 block, CGLocal local, OptLocalMap* m = local_map(l, local, loc); if (m->storage.kind == CG_LOCAL_STORAGE_REG) return; if (materialized_reg_exists(l, local, CG_IR_MAT_BASE)) return; - PReg r = ir_alloc_preg(l->f, m->type, RC_INT); + KitCgTypeId base_type = + cg_type_is_ptr(l->c, m->type) ? m->type : cg_type_ptr_to(l->c, m->type); + if (!base_type) + lower_panic(l, loc, "cannot construct indirect frame-base pointer type"); + PReg r = ir_alloc_preg(l->f, base_type, RC_INT); OptOperand ops[2]; ops[1] = opt_frame_operand(m); if (cg_type_is_ptr(l->c, m->type)) { @@ -549,7 +553,7 @@ static void materialize_frame_base(CgIrLower* l, u32 block, CGLocal local, memset(&ops[0], 0, sizeof ops[0]); ops[0].kind = OPK_REG; ops[0].cls = RC_INT; - ops[0].type = m->type; + ops[0].type = base_type; ops[0].v.reg = (Reg)r; ld->opnds = dup_opt_ops(l, ops, 2); ld->nopnds = 2; @@ -566,12 +570,12 @@ static void materialize_frame_base(CgIrLower* l, u32 block, CGLocal local, memset(&ops[0], 0, sizeof ops[0]); ops[0].kind = OPK_REG; ops[0].cls = RC_INT; - ops[0].type = m->type; + ops[0].type = base_type; ops[0].v.reg = (Reg)r; ao->opnds = dup_opt_ops(l, ops, 2); ao->nopnds = 2; ao->def = (Val)r; - ao->type = m->type; + ao->type = base_type; } remember_materialized_reg(l, local, CG_IR_MAT_BASE, (Reg)r, loc); } @@ -659,10 +663,13 @@ static OptOperand lower_operand_addr(CgIrLower* l, const Operand* in, out.cls = RC_INT; out.v.ind.base = resolve_materialized_reg(l, in->v.ind.base, CG_IR_MAT_BASE, loc); + out.v.ind.base_type = opt_reg_type(l->f, (PReg)out.v.ind.base); out.v.ind.index = REG_NONE; - if (in->v.ind.index != CG_LOCAL_NONE) + if (in->v.ind.index != CG_LOCAL_NONE) { out.v.ind.index = resolve_materialized_reg(l, in->v.ind.index, CG_IR_MAT_INDEX, loc); + out.v.ind.index_type = opt_reg_type(l->f, (PReg)out.v.ind.index); + } out.v.ind.log2_scale = in->v.ind.log2_scale; out.v.ind.ofs = in->v.ind.ofs; return out; diff --git a/src/opt/ir.h b/src/opt/ir.h @@ -63,8 +63,36 @@ typedef enum OptOperandKind { OPT_OPK_GLOBAL = OPK_GLOBAL, OPT_OPK_INDIRECT = OPK_INDIRECT, OPT_OPK_REG = 0xf0u, + /* Post-allocation rematerialization recipe: the value is the address of a + * frame slot, not the scalar stored in that slot. Semantic HIR never carries + * this kind; location MIR and native emission own it. */ + OPT_OPK_FRAME_ADDR = 0xf1u, + /* Post-allocation scalar spill home. Unlike OPK_LOCAL (semantic frame + * storage/lvalue), this denotes the value stored in the frame slot. */ + OPT_OPK_STACK = 0xf2u, } OptOperandKind; #define OPK_REG OPT_OPK_REG +#define OPK_FRAME_ADDR OPT_OPK_FRAME_ADDR +#define OPK_STACK OPT_OPK_STACK + +/* Location kinds for the components of an OPK_INDIRECT effective address. + * HIR is zero-initialized and therefore remains register-only. MIR may name a + * spilled pointer/index by its stable frame home, or retain a cheap frame + * address rematerialization recipe, without inventing a scratch-register + * value whose lifetime leaks across instructions. */ +typedef enum OptIndirectLocKind { + OPT_INDIRECT_REG = 0, + OPT_INDIRECT_FRAME = 1, + OPT_INDIRECT_FRAME_ADDR = 2, +} OptIndirectLocKind; + +typedef enum OptOperandFlag { + /* Transient walker annotation, never stored in IR. It lets callbacks that + * lower or classify recipes distinguish an embedded effective-address + * component from a first-class scalar operand without changing the compact + * callback ABI. */ + OPT_OPERAND_WALK_INDIRECT_PART = 1u << 0, +} OptOperandFlag; /* Operand riders (O1-PATTERNS L7/L8). Default 0 = no rider, so every existing * operand and every backend that does not implement the fold keeps behaving as @@ -86,7 +114,7 @@ typedef struct OptOperand { * emitted as the shifted-register form (`add xD,xA,xS,lsl #shift`). 0 = the * register is used as-is. Only stamped on a binop's second source operand. */ u8 shift; - u8 pad[1]; + u8 flags; /* OptOperandFlag; stored operands normally have no flags */ KitCgTypeId type; union { i64 imm; @@ -105,7 +133,17 @@ typedef struct OptOperand { * value that the addressing mode widens to 64 bits with the recorded * extend (`[Xbase, Wm, sxtw #log2_scale]`). NONE = full-width X index. */ u8 index_ext; + /* OptIndirectLocKind for `base` and `index`. */ + u8 base_kind; + u8 index_kind; i32 ofs; + /* The component value types travel with location MIR. Spill-slot + * coloring is intentionally allowed to reuse a physical frame slot for + * unrelated values, so the slot descriptor cannot be used to recover a + * spilled base/index type after allocation. HIR initializes these from + * the source PRegs; MIR preserves them while changing only location. */ + KitCgTypeId base_type; + KitCgTypeId index_type; } ind; } v; } OptOperand; @@ -388,7 +426,7 @@ typedef struct IRBitFieldAux { /* One per-class clobber bitmask set; see Func.inst_clobbers. */ typedef u32 OptInstClobberMask[OPT_REG_CLASSES]; -#define OPT_MAX_SCRATCH_REGS 4u +#define OPT_MAX_EMIT_TEMPS 4u typedef struct IRGepAux { KitCgTypeId base_type; @@ -450,6 +488,17 @@ typedef struct IRCasAux { KitCgMemOrder failure; } IRCasAux; +typedef struct IRAsmRegRequirement { + /* A register constraint resolved once at the HIR -> native-policy boundary. + * It remains an instruction-local placement request; regalloc never turns it + * into a persistent MIR location. */ + u32 allowed_mask; /* 0 means any operand register in `cls` */ + i32 fixed_reg; /* -1 means no fixed register */ + u8 cls; /* RegClass */ + u8 present; /* constraint requires a register */ + u8 pad[2]; +} IRAsmRegRequirement; + typedef struct IRAsmAux { const char* tmpl; AsmConstraint* outs; @@ -464,14 +513,10 @@ typedef struct IRAsmAux { u32 clobber_abi_sets; /* Filled by opt_machinize from backend register-name resolution. */ u32 clobber_mask[OPT_REG_CLASSES]; - i32* out_fixed_regs; /* nout, -1 when unconstrained */ - i32* in_fixed_regs; /* nin, -1 when unconstrained */ - u8* out_fixed_cls; /* RegClass, parallel to out_fixed_regs */ - u8* in_fixed_cls; /* RegClass, parallel to in_fixed_regs */ - u32* out_allowed_masks; /* nout, 0 when the whole class is allowed */ - u32* in_allowed_masks; /* nin, 0 when the whole class is allowed */ - u8* out_allowed_cls; /* RegClass, parallel to out_allowed_masks */ - u8* in_allowed_cls; /* RegClass, parallel to in_allowed_masks */ + IRAsmRegRequirement* out_reg_reqs; /* nout */ + IRAsmRegRequirement* in_reg_reqs; /* nin */ + u8 has_memory_constraint; + u8 pad[3]; } IRAsmAux; typedef struct IRIntrinAux { @@ -584,6 +629,7 @@ typedef struct Block { typedef struct MFunc { Block* blocks; u32 nblocks; + u32 blocks_cap; u32 entry; u32* emit_order; u32 emit_order_n; @@ -619,21 +665,17 @@ typedef struct OptPRegInfo { u32 live_block_freq; u32 live_across_call_freq; u32 spill_cost; - i32 tied_hard_reg; /* -1 means no fixed/tied physical register need. */ Reg hard_reg; FrameSlot spill_slot; u8 alloc_kind; /* OptAllocKind */ u8 cls; /* RegClass */ i8 preferred_hard_reg; /* soft hint for allocator; -1 = no hint */ u8 pad[1]; - u32 forbidden_hard_regs; /* bit r means PReg may not allocate hard reg r. */ - u32 allowed_hard_regs; /* 0 means unrestricted; otherwise positive mask. */ - /* Subset of forbidden_hard_regs that comes from a fixed-register machine - * clobber (an instruction live across this value destroys reg r — see - * Func.inst_clobbers). Unlike soft forbids, the return-register hint must not - * clear these: a value cannot live in a register clobbered within its range. - */ - u32 clobbered_hard_regs; + /* Authoritative correctness constraint. Bit r means this unsplit PReg may + * not be allocated to hard register r because an instruction boundary in + * its live range reserves or destroys that register. ABI placement + * preferences are independent soft hints and never mutate this mask. */ + u32 forbidden_hard_regs; } OptPRegInfo; typedef enum OptUseKind { @@ -733,8 +775,9 @@ typedef struct Func { u64 opt_coalesce_merges; InstId next_inst_id; /* Per-instruction fixed-register clobbers (one bitmask per reg class), - * indexed by InstId, sized [next_inst_id]. Built in opt_machinize_native from - * the target's machine_op_clobbers hook; consulted by the allocator + * indexed by InstId, sized [next_inst_id]. Derived from the target's + * machine_op_clobbers hook by opt_machinize_native, then refreshed at the + * final pre-allocation HIR-shape boundary. Consulted by the allocator * (pass_lower) to keep values live across an instruction out of the registers * its encoding destroys (x86 idiv → rax/rdx, etc.). NULL when no instruction * clobbers. */ @@ -763,8 +806,12 @@ typedef struct Func { u32 opt_hard_reg_count[OPT_REG_CLASSES]; CGPhysRegInfo opt_phys_regs[OPT_REG_CLASSES][OPT_MAX_HARD_REGS]; u32 opt_phys_reg_count[OPT_REG_CLASSES]; - Reg opt_scratch_regs[OPT_REG_CLASSES][OPT_MAX_SCRATCH_REGS]; - u32 opt_scratch_reg_count[OPT_REG_CLASSES]; + Reg emit_temp_regs[OPT_REG_CLASSES][OPT_MAX_EMIT_TEMPS]; + u32 emit_temp_reg_count[OPT_REG_CLASSES]; + /* Backend-private inline-asm staging registers. Kept as a mask because MIR + * verification only needs to enforce that they never become value + * locations; final asm emission leases the ordered descriptor list. */ + u32 asm_temp_mask[OPT_REG_CLASSES]; u32 opt_caller_saved[OPT_REG_CLASSES]; /* bit r set if hard reg r is caller-saved */ u32 opt_callee_saved[OPT_REG_CLASSES]; diff --git a/src/opt/ir_print.c b/src/opt/ir_print.c @@ -201,6 +201,16 @@ static void dump_operand(Writer* w, const Operand* op) { strbuf_put_u64(&sb, (u64)(unsigned)op->v.frame_slot); dump_sb(w, &sb); break; + case OPK_STACK: + strbuf_puts(&sb, "stack#"); + strbuf_put_u64(&sb, (u64)(unsigned)op->v.frame_slot); + dump_sb(w, &sb); + break; + case OPK_FRAME_ADDR: + strbuf_puts(&sb, "&frame#"); + strbuf_put_u64(&sb, (u64)(unsigned)op->v.frame_slot); + dump_sb(w, &sb); + break; case OPK_GLOBAL: strbuf_puts(&sb, "global#"); strbuf_put_u64(&sb, (u64)(unsigned)op->v.global.sym); @@ -209,8 +219,28 @@ static void dump_operand(Writer* w, const Operand* op) { break; case OPK_INDIRECT: strbuf_putc(&sb, '['); - strbuf_putc(&sb, 'v'); + if (op->v.ind.base_kind == OPT_INDIRECT_FRAME) + strbuf_puts(&sb, "frame-value#"); + else if (op->v.ind.base_kind == OPT_INDIRECT_FRAME_ADDR) + strbuf_puts(&sb, "frame#"); + else + strbuf_putc(&sb, 'v'); strbuf_put_u64(&sb, (u64)(unsigned)op->v.ind.base); + if (op->v.ind.index_kind != OPT_INDIRECT_REG || + op->v.ind.index != (Reg)REG_NONE) { + strbuf_puts(&sb, "+"); + if (op->v.ind.index_kind == OPT_INDIRECT_FRAME) + strbuf_puts(&sb, "frame-value#"); + else if (op->v.ind.index_kind == OPT_INDIRECT_FRAME_ADDR) + strbuf_puts(&sb, "frame#"); + else + strbuf_putc(&sb, 'v'); + strbuf_put_u64(&sb, (u64)(unsigned)op->v.ind.index); + if (op->v.ind.log2_scale) { + strbuf_puts(&sb, "<<"); + strbuf_put_u64(&sb, op->v.ind.log2_scale); + } + } strbuf_put_i64_plus(&sb, (i64)(int)op->v.ind.ofs); strbuf_putc(&sb, ']'); dump_sb(w, &sb); diff --git a/src/opt/mir_clone.c b/src/opt/mir_clone.c @@ -0,0 +1,338 @@ +#include <string.h> + +#include "opt/opt_internal.h" + +/* + * MIR-owned containers form an independently mutable graph. The arena owns + * both graphs for the lifetime of Func, while the temporary destination Func + * view continues to share immutable function-wide context. No container that + * an optimizer pass may rewrite is allowed to point into HIR. Leaf descriptors + * explicitly left shared below (types, ABI descriptions, symbol/string storage, + * and constant byte payloads) are immutable compiler-owned data. + */ + +static u32 clone_cap(u32 count, u32 cap) { return cap < count ? count : cap; } + +static void clone_abi_value(Arena* arena, CGABIValue* dst, + const CGABIValue* src) { + *dst = *src; + if (!src->parts || !src->nparts) return; + dst->parts = arena_array(arena, CGABIPart, src->nparts); + memcpy(dst->parts, src->parts, sizeof(dst->parts[0]) * src->nparts); +} + +static IRCallAux* clone_call_aux(Arena* arena, const IRCallAux* src) { + IRCallAux* dst = arena_new(arena, IRCallAux); + *dst = *src; + + if (src->desc.args && src->desc.nargs) { + dst->desc.args = arena_array(arena, CGABIValue, src->desc.nargs); + for (u32 i = 0; i < src->desc.nargs; ++i) + clone_abi_value(arena, &dst->desc.args[i], &src->desc.args[i]); + } + clone_abi_value(arena, &dst->desc.ret, &src->desc.ret); + + if (src->plan.args && src->plan.nargs) { + dst->plan.args = arena_array(arena, CGCallPlanMove, src->plan.nargs); + memcpy(dst->plan.args, src->plan.args, + sizeof(dst->plan.args[0]) * src->plan.nargs); + } + if (src->plan.rets && src->plan.nrets) { + dst->plan.rets = arena_array(arena, CGCallPlanRet, src->plan.nrets); + memcpy(dst->plan.rets, src->plan.rets, + sizeof(dst->plan.rets[0]) * src->plan.nrets); + } + if (src->results && src->nresults) { + dst->results = arena_array(arena, Val, src->nresults); + memcpy(dst->results, src->results, + sizeof(dst->results[0]) * src->nresults); + } + return dst; +} + +static IRRetAux* clone_ret_aux(Arena* arena, const IRRetAux* src) { + IRRetAux* dst = arena_new(arena, IRRetAux); + *dst = *src; + clone_abi_value(arena, &dst->val, &src->val); + return dst; +} + +static IRAsmAux* clone_asm_aux(Arena* arena, const IRAsmAux* src) { + IRAsmAux* dst = arena_new(arena, IRAsmAux); + *dst = *src; + +#define CLONE_ASM_ARRAY(field, T, count) \ + do { \ + if (src->field && (count)) { \ + dst->field = arena_array(arena, T, (count)); \ + memcpy(dst->field, src->field, sizeof(dst->field[0]) * (count)); \ + } \ + } while (0) + + CLONE_ASM_ARRAY(outs, AsmConstraint, src->nout); + CLONE_ASM_ARRAY(ins, AsmConstraint, src->nin); + CLONE_ASM_ARRAY(clobbers, Sym, src->nclob); + CLONE_ASM_ARRAY(out_ops, Operand, src->nout); + CLONE_ASM_ARRAY(in_ops, Operand, src->nin); + CLONE_ASM_ARRAY(out_reg_reqs, IRAsmRegRequirement, src->nout); + CLONE_ASM_ARRAY(in_reg_reqs, IRAsmRegRequirement, src->nin); + +#undef CLONE_ASM_ARRAY + return dst; +} + +static IRIntrinAux* clone_intrin_aux(Arena* arena, const IRIntrinAux* src) { + IRIntrinAux* dst = arena_new(arena, IRIntrinAux); + *dst = *src; + if (src->dsts && src->ndst) { + dst->dsts = arena_array(arena, Operand, src->ndst); + memcpy(dst->dsts, src->dsts, sizeof(dst->dsts[0]) * src->ndst); + } + if (src->args && src->narg) { + dst->args = arena_array(arena, Operand, src->narg); + memcpy(dst->args, src->args, sizeof(dst->args[0]) * src->narg); + } + if (src->result_vals && src->ndst) { + dst->result_vals = arena_array(arena, Val, src->ndst); + memcpy(dst->result_vals, src->result_vals, + sizeof(dst->result_vals[0]) * src->ndst); + } + return dst; +} + +static int inst_uses_aux(IROp op) { + switch (op) { + case IR_PARAM_DECL: + case IR_TLS_ADDR_OF: + case IR_AGG_COPY: + case IR_AGG_SET: + case IR_BITFIELD_LOAD: + case IR_BITFIELD_STORE: + case IR_CALL: + case IR_PHI: + case IR_SWITCH: + case IR_INDIRECT_BRANCH: + case IR_LOCAL_STATIC_DATA_BEGIN: + case IR_LOCAL_STATIC_DATA_WRITE: + case IR_LOCAL_STATIC_DATA_LABEL_ADDR: + case IR_RET: + case IR_SCOPE_BEGIN: + case IR_ATOMIC_LOAD: + case IR_ATOMIC_STORE: + case IR_ATOMIC_RMW: + case IR_ATOMIC_CAS: + case IR_ASM_BLOCK: + case IR_INTRINSIC: + return 1; + default: + return 0; + } +} + +static void* clone_inst_aux(Arena* arena, const Inst* src) { + if (!src->extra.aux) return NULL; + +#define CLONE_PLAIN_AUX(T) \ + do { \ + const T* src_aux = (const T*)src->extra.aux; \ + T* dst_aux = arena_new(arena, T); \ + *dst_aux = *src_aux; \ + return dst_aux; \ + } while (0) + + switch ((IROp)src->op) { + case IR_PARAM_DECL: { + const IRParamDeclAux* src_aux = + (const IRParamDeclAux*)src->extra.aux; + IRParamDeclAux* dst_aux = arena_new(arena, IRParamDeclAux); + *dst_aux = *src_aux; + if (src_aux->desc.incoming && src_aux->desc.nincoming) { + CGABIPart* incoming = + arena_array(arena, CGABIPart, src_aux->desc.nincoming); + memcpy(incoming, src_aux->desc.incoming, + sizeof(incoming[0]) * src_aux->desc.nincoming); + dst_aux->desc.incoming = incoming; + } + return dst_aux; + } + case IR_TLS_ADDR_OF: + CLONE_PLAIN_AUX(IRTlsAux); + case IR_AGG_COPY: + case IR_AGG_SET: + CLONE_PLAIN_AUX(IRAggAux); + case IR_BITFIELD_LOAD: + case IR_BITFIELD_STORE: + CLONE_PLAIN_AUX(IRBitFieldAux); + case IR_CALL: + return clone_call_aux(arena, (const IRCallAux*)src->extra.aux); + case IR_PHI: { + const IRPhiAux* src_aux = (const IRPhiAux*)src->extra.aux; + IRPhiAux* dst_aux = arena_new(arena, IRPhiAux); + *dst_aux = *src_aux; + if (src_aux->pred_blocks && src_aux->npreds) { + dst_aux->pred_blocks = arena_array(arena, u32, src_aux->npreds); + memcpy(dst_aux->pred_blocks, src_aux->pred_blocks, + sizeof(dst_aux->pred_blocks[0]) * src_aux->npreds); + } + if (src_aux->pred_vals && src_aux->npreds) { + dst_aux->pred_vals = arena_array(arena, Val, src_aux->npreds); + memcpy(dst_aux->pred_vals, src_aux->pred_vals, + sizeof(dst_aux->pred_vals[0]) * src_aux->npreds); + } + return dst_aux; + } + case IR_SWITCH: { + const IRSwitchAux* src_aux = (const IRSwitchAux*)src->extra.aux; + IRSwitchAux* dst_aux = arena_new(arena, IRSwitchAux); + *dst_aux = *src_aux; + if (src_aux->cases && src_aux->ncases) { + dst_aux->cases = + arena_array(arena, IRSwitchAuxCase, src_aux->ncases); + memcpy(dst_aux->cases, src_aux->cases, + sizeof(dst_aux->cases[0]) * src_aux->ncases); + } + return dst_aux; + } + case IR_INDIRECT_BRANCH: { + const IRIndirectAux* src_aux = (const IRIndirectAux*)src->extra.aux; + IRIndirectAux* dst_aux = arena_new(arena, IRIndirectAux); + *dst_aux = *src_aux; + if (src_aux->targets && src_aux->ntargets) { + dst_aux->targets = arena_array(arena, u32, src_aux->ntargets); + memcpy(dst_aux->targets, src_aux->targets, + sizeof(dst_aux->targets[0]) * src_aux->ntargets); + } + return dst_aux; + } + case IR_LOCAL_STATIC_DATA_BEGIN: + CLONE_PLAIN_AUX(CgIrLocalStaticBeginAux); + case IR_LOCAL_STATIC_DATA_WRITE: + /* The data payload is an immutable byte string owned by the semantic IR; + * only the mutable descriptor object is graph-local. */ + CLONE_PLAIN_AUX(CgIrLocalStaticWriteAux); + case IR_LOCAL_STATIC_DATA_LABEL_ADDR: + CLONE_PLAIN_AUX(CgIrLocalStaticLabelAux); + case IR_RET: + return clone_ret_aux(arena, (const IRRetAux*)src->extra.aux); + case IR_SCOPE_BEGIN: + CLONE_PLAIN_AUX(IRScopeAux); + case IR_ATOMIC_LOAD: + case IR_ATOMIC_STORE: + case IR_ATOMIC_RMW: + CLONE_PLAIN_AUX(IRAtomicAux); + case IR_ATOMIC_CAS: + CLONE_PLAIN_AUX(IRCasAux); + case IR_ASM_BLOCK: + return clone_asm_aux(arena, (const IRAsmAux*)src->extra.aux); + case IR_INTRINSIC: + return clone_intrin_aux(arena, (const IRIntrinAux*)src->extra.aux); + default: + return NULL; + } + +#undef CLONE_PLAIN_AUX +} + +static void clone_inst(Arena* arena, Inst* dst, const Inst* src) { + *dst = *src; + if (src->defs && src->ndefs) { + dst->defs = arena_array(arena, Val, src->ndefs); + memcpy(dst->defs, src->defs, sizeof(dst->defs[0]) * src->ndefs); + } + if (src->opnds && src->nopnds) { + dst->opnds = arena_array(arena, Operand, src->nopnds); + memcpy(dst->opnds, src->opnds, sizeof(dst->opnds[0]) * src->nopnds); + } + if (inst_uses_aux((IROp)src->op)) + dst->extra.aux = clone_inst_aux(arena, src); +} + +static Inst* cloned_inst_by_id(Func* f, InstId id) { + if (id == INST_ID_NONE) return NULL; + for (u32 b = 0; b < f->nblocks; ++b) { + Block* block = &f->blocks[b]; + for (u32 i = 0; i < block->ninsts; ++i) + if (block->insts[i].id == id) return &block->insts[i]; + } + return NULL; +} + +void opt_mir_clone_func(Func* dst, const Func* src) { + Arena* arena; + u32 blocks_cap; + u32 frame_slots_cap; + u32 emit_order_cap; + + if (!dst || !src) return; + *dst = *src; + arena = src->arena; + dst->mir = NULL; + dst->opt_rewritten = 0; + + blocks_cap = clone_cap(src->nblocks, src->blocks_cap); + dst->blocks = arena_zarray(arena, Block, blocks_cap ? blocks_cap : 1u); + for (u32 b = 0; b < src->nblocks; ++b) { + const Block* src_block = &src->blocks[b]; + Block* dst_block = &dst->blocks[b]; + u32 inst_cap; + *dst_block = *src_block; + + inst_cap = clone_cap(src_block->ninsts, src_block->cap); + dst_block->insts = + arena_zarray(arena, Inst, inst_cap ? inst_cap : 1u); + for (u32 i = 0; i < src_block->ninsts; ++i) + clone_inst(arena, &dst_block->insts[i], &src_block->insts[i]); + + if (src_block->preds && src_block->npreds) { + dst_block->preds = arena_array(arena, u32, src_block->npreds); + memcpy(dst_block->preds, src_block->preds, + sizeof(dst_block->preds[0]) * src_block->npreds); + } + if (src_block->succ && src_block->succ_cap) { + dst_block->succ = arena_array(arena, u32, src_block->succ_cap); + memcpy(dst_block->succ, src_block->succ, + sizeof(dst_block->succ[0]) * src_block->succ_cap); + } + } + + frame_slots_cap = clone_cap(src->nframe_slots, src->frame_slots_cap); + if (src->frame_slots && frame_slots_cap) { + dst->frame_slots = arena_zarray(arena, IRFrameSlot, frame_slots_cap); + memcpy(dst->frame_slots, src->frame_slots, + sizeof(dst->frame_slots[0]) * src->nframe_slots); + } + + emit_order_cap = clone_cap(src->emit_order_n, src->emit_order_cap); + if (src->emit_order && emit_order_cap) { + dst->emit_order = arena_array(arena, u32, emit_order_cap); + memcpy(dst->emit_order, src->emit_order, + sizeof(dst->emit_order[0]) * src->emit_order_n); + } + + /* spill_slot_for may add a call-save home during rewriting. Keep that + * allocation bookkeeping private to the lowering view. */ + if (src->preg_info && opt_reg_count(src)) { + u32 nregs = opt_reg_count(src); + dst->preg_info = arena_array(arena, OptPRegInfo, nregs); + memcpy(dst->preg_info, src->preg_info, + sizeof(dst->preg_info[0]) * nregs); + } + + if (src->scope_aux_inst && src->scopes_cap) { + dst->scope_aux_inst = arena_zarray(arena, Inst*, src->scopes_cap); + for (u32 i = 0; i < src->nscopes; ++i) { + const Inst* scope = src->scope_aux_inst[i]; + if (scope) dst->scope_aux_inst[i] = cloned_inst_by_id(dst, scope->id); + } + } + + /* Def-use entries contain direct Operand pointers into HIR. They are an + * analysis cache, not part of the MIR graph, and must never cross this + * boundary. */ + dst->opt_uses = NULL; + dst->opt_nuses = 0; + dst->opt_uses_cap = 0; + dst->opt_first_use_by_val = NULL; + dst->opt_first_use_by_val_cap = 0; + dst->opt_valid_analyses = 0; +} diff --git a/src/opt/opt.c b/src/opt/opt.c @@ -132,14 +132,17 @@ static void opt_registry_add(OptImpl* o, CgIrFunc* f) { } static void opt_dbg_dump(OptImpl* o, Func* f, const char* tag) { + Func view; + Func* graph = f; const char* s = kit_debug_getenv("KIT_DUMP"); KitWriter* w = NULL; size_t len = 0; const uint8_t* bytes; if (!s) return; if (strcmp(s, "1") != 0 && strcmp(s, tag) != 0) return; + if (opt_mir_view(f, &view)) graph = &view; kit_writer_mem(o->c->ctx->heap, &w); - opt_ir_dump(f, w); + opt_ir_dump(graph, w); bytes = kit_writer_mem_bytes(w, &len); diag_emit(o->c->ctx->diag, KIT_DIAG_NOTE, f->desc.loc, "DUMP %s:\n%.*s", tag, (int)len, (const char*)bytes); @@ -253,6 +256,15 @@ static void opt_o1_native_finish(OptImpl* o, Func* f, int cfg_dirty) { metrics_scope_end(o->c, "opt.o1.hoist_loop_consts"); opt_verify(f, "o1-hoist-loop-consts"); + /* Machinization derives fixed-register effects from instruction shape. The + * loop-immediate pass above can turn an immediate shift into a register shift + * (x64 then uses/clobbers RCX), and the hoister is the last HIR shape mutator + * before allocation. Refresh once at this explicit boundary so liveness and + * regalloc consume effects for the final HIR, not the pre-transform form. */ + metrics_scope_begin(o->c, "opt.machine_effects.refresh"); + opt_refresh_machine_clobbers(f, o->native); + metrics_scope_end(o->c, "opt.machine_effects.refresh"); + metrics_scope_begin(o->c, "opt.live_blocks.pre_dde"); memset(&live, 0, sizeof live); opt_live_blocks(f, &live); @@ -301,6 +313,9 @@ static void opt_o1_native_finish(OptImpl* o, Func* f, int cfg_dirty) { metrics_scope_begin(o->c, "opt.post_ra.jump_cleanup_layout"); opt_mir_jump_cleanup(f, OPT_JUMP_CLEANUP_LAYOUT); metrics_scope_end(o->c, "opt.post_ra.jump_cleanup_layout"); + metrics_scope_begin(o->c, "opt.post_ra.layout.verify"); + opt_mir_verify(f, "post-mir-layout"); + metrics_scope_end(o->c, "opt.post_ra.layout.verify"); opt_dbg_dump(o, f, "pre-emit"); metrics_scope_begin(o->c, "opt.emit"); diff --git a/src/opt/opt.h b/src/opt/opt.h @@ -65,6 +65,10 @@ void opt_jump_opt(Func*); /* ----- lowering / backend prep (per Func, run before NativeTarget emission) * ----- */ void opt_machinize_native(Func*, NativeTarget* target); +/* Rebuild target-derived per-instruction fixed-register effects after an HIR + * shape-changing pass. The O1 pipeline calls this at its final HIR boundary, + * before liveness and allocation consume those effects. */ +void opt_refresh_machine_clobbers(Func*, NativeTarget* target); void opt_build_loop_tree(Func*); typedef struct OptBitset { diff --git a/src/opt/opt_internal.h b/src/opt/opt_internal.h @@ -9,6 +9,22 @@ typedef struct OptHardRegSet { u32 cls[OPT_REG_CLASSES]; } OptHardRegSet; +/* Canonical physical-register effects of one instruction. `uses` and `defs` + * are the registers named by the instruction/ABI operands; `clobbers` are + * implicit kills (call ABI, inline asm, and target machine constraints). + * Emission temporaries are private to native emission and never appear here. + * Keeping defs separate from clobbers matters to DCE: an implicit kill is not + * a produced value. */ +typedef struct OptRegEffects { + OptHardRegSet uses; + OptHardRegSet defs; + OptHardRegSet clobbers; + /* Per-register explicit-use multiplicity, saturated at two. This keeps + * combine's single-use decisions in the same canonical operand model (and + * preserves the double-reference case `[r + r*scale]`). */ + u8 use_count[OPT_REG_CLASSES][OPT_MAX_HARD_REGS]; +} OptRegEffects; + typedef struct OptHardBlockLive { OptHardRegSet live_in; OptHardRegSet live_out; @@ -148,12 +164,32 @@ int opt_edge_is_fallthrough(Func*, u32 pred, u32 succ); u32 opt_split_edge(Func*, u32 pred, u32 succ); int opt_val_in_inst_defs(const Inst*, Val); +/* One authority for the scalar types which may occupy an indirect index. + * Address synthesis and MIR verification must agree on this boundary. */ +int opt_indirect_index_type_valid(Func*, KitCgTypeId); +/* Canonical role of a direct instruction operand. Unlike legacy def-id + * inference, this remains valid after PRegs have been rewritten to physical + * registers or frame locations. Aux operands are classified by the central + * walker below. */ +int opt_inst_operand_is_def(const Inst*, u32 index); void opt_walk_operand(Func*, Inst*, Operand*, int is_def, OptOperandWalkFn, void*); void opt_walk_abivalue(Func*, Inst*, CGABIValue*, int storage_def, OptOperandWalkFn, void*); void opt_walk_inst_operands(Func*, Inst*, OptOperandWalkFn, void*); +/* Build the independently mutable post-allocation graph used by MIR lowering. + * Mutable instruction, operand, aux, and CFG storage never aliases HIR. */ +void opt_mir_clone_func(Func* dst, const Func* src); + +/* Present the independently owned MIR graph through the ordinary Func pass + * interface. The view deliberately drops HIR pointer-based analyses; callers + * must rebuild any analysis they need over MIR. Passes which may replace graph + * storage commit those fields, plus any extended instruction-id namespace, + * back through opt_mir_commit. */ +int opt_mir_view(Func* f, Func* out); +void opt_mir_commit(Func* f, const Func* view); + /* Make room for k new instructions starting at index `at` in block `bl`. * Returns a pointer to the first new slot (zero-initialized). Grows the * block's inst array (doubling) as needed. */ @@ -177,8 +213,8 @@ int opt_hard_empty(const OptHardRegSet*); int opt_hard_intersects(const OptHardRegSet*, const OptHardRegSet*); void opt_hard_live_step(OptHardRegSet* live, const OptHardRegSet* use, const OptHardRegSet* def); -void opt_hard_inst_use_def(Func*, const Inst*, OptHardRegSet* use, - OptHardRegSet* def); +void opt_inst_reg_effects(Func*, const Inst*, OptRegEffects*); +void opt_reg_effect_kills(const OptRegEffects*, OptHardRegSet*); OptHardBlockLive* opt_maybe_build_hard_live(Func*); OptHardRegSet opt_hard_live_out_for_block(const OptHardBlockLive*); int opt_block_live_out_has_phys_reg(Func*, const OptHardBlockLive*, u32 block, diff --git a/src/opt/opt_util.c b/src/opt/opt_util.c @@ -10,40 +10,129 @@ int opt_val_in_inst_defs(const Inst* in, Val v) { return 0; } -static int opt_operand_index_is_def(const Inst* in, u32 i) { - if (!in || i >= in->nopnds || in->opnds[i].kind != OPK_REG) return 0; - if (!opt_val_in_inst_defs(in, (Val)in->opnds[i].v.reg)) return 0; +int opt_indirect_index_type_valid(Func* f, KitCgTypeId type) { + u64 size; + if (!f || !f->c || type == KIT_CG_TYPE_NONE || + !cg_type_is_int(f->c, type) || + opt_value_reg_class(f->c, type) != RC_INT) + return 0; + size = cg_type_size(f->c, type); + return size != 0u && size <= f->c->target.ptr_size; +} + +int opt_inst_operand_is_def(const Inst* in, u32 i) { + if (!in || i >= in->nopnds) return 0; switch ((IROp)in->op) { + case IR_LOAD_IMM: + case IR_LOAD_CONST: + case IR_TLS_ADDR_OF: + case IR_LOAD_LABEL_ADDR: + case IR_COPY: + case IR_CONVERT: + case IR_UNOP: + case IR_VA_ARG: + case IR_LOAD: + case IR_ADDR_OF: + case IR_BITFIELD_LOAD: + case IR_ATOMIC_LOAD: + case IR_BINOP: + case IR_CMP: + case IR_ALLOCA: + case IR_ATOMIC_RMW: + return i == 0u; case IR_ATOMIC_CAS: - return i == 0 || i == 1; + return i == 0u || i == 1u; + default: + return 0; + } +} + +static KitCgTypeId indirect_frame_type(Func* f, FrameSlot slot, + KitCgTypeId fallback) { + if (f && slot != FRAME_SLOT_NONE && slot <= f->nframe_slots) { + KitCgTypeId type = f->frame_slots[slot - 1u].type; + if (type) return type; + } + return fallback; +} + +static void indirect_part_operand(Func* f, const Operand* owner, u32 value, + u8 kind, KitCgTypeId component_type, + Operand* out) { + *out = *owner; + out->cls = RC_INT; + out->flags |= OPT_OPERAND_WALK_INDIRECT_PART; + if (component_type) out->type = component_type; + switch ((OptIndirectLocKind)kind) { + case OPT_INDIRECT_REG: + out->kind = OPK_REG; + out->v.reg = (Reg)value; + if (!component_type && (PReg)value < opt_reg_count(f) && + opt_reg_type(f, (PReg)value)) + out->type = opt_reg_type(f, (PReg)value); + return; + case OPT_INDIRECT_FRAME: + out->kind = OPK_STACK; + out->v.frame_slot = (FrameSlot)value; + if (!component_type) + out->type = indirect_frame_type(f, (FrameSlot)value, owner->type); + return; + case OPT_INDIRECT_FRAME_ADDR: + out->kind = OPK_FRAME_ADDR; + out->v.frame_slot = (FrameSlot)value; + return; + } +} + +static void indirect_part_commit(Func* f, Inst* in, Operand* owner, + Operand* part, u32* value, u8* kind, + KitCgTypeId* component_type) { + if (component_type) *component_type = part->type; + switch ((OptOperandKind)part->kind) { + case OPK_REG: + *value = part->v.reg; + *kind = OPT_INDIRECT_REG; + return; + case OPK_STACK: + *value = part->v.frame_slot; + *kind = OPT_INDIRECT_FRAME; + return; + case OPK_FRAME_ADDR: + *value = part->v.frame_slot; + *kind = OPT_INDIRECT_FRAME_ADDR; + return; default: - return i == 0; + compiler_panic(f->c, in ? in->loc : (SrcLoc){0, 0, 0}, + "opt operand walk: invalid indirect location kind %u", + (unsigned)part->kind); } + (void)owner; } void opt_walk_operand(Func* f, Inst* in, Operand* op, int is_def, OptOperandWalkFn fn, void* ctx) { if (!op || !fn) return; - if (op->kind == OPK_REG) { + if (op->kind != OPK_INDIRECT) { fn(f, in, op, is_def, ctx); } else if (op->kind == OPK_INDIRECT) { - Operand base = *op; - base.kind = OPK_REG; - base.cls = RC_INT; - base.v.reg = op->v.ind.base; - if ((PReg)base.v.reg < opt_reg_count(f) && opt_reg_type(f, base.v.reg)) - base.type = opt_reg_type(f, base.v.reg); + Operand base; + /* Expose the complete address owner as well as its scalar components. + * Location-aware consumers (notably MIR verification) need to validate + * the tuple; register-rewrite callbacks simply ignore non-register kinds. */ + fn(f, in, op, 0, ctx); + indirect_part_operand(f, op, op->v.ind.base, op->v.ind.base_kind, + op->v.ind.base_type, &base); fn(f, in, &base, 0, ctx); - op->v.ind.base = base.v.reg; - if (op->v.ind.index != (Reg)REG_NONE) { - Operand idx = *op; - idx.kind = OPK_REG; - idx.cls = RC_INT; - idx.v.reg = op->v.ind.index; - if ((PReg)idx.v.reg < opt_reg_count(f) && opt_reg_type(f, idx.v.reg)) - idx.type = opt_reg_type(f, idx.v.reg); + indirect_part_commit(f, in, op, &base, &op->v.ind.base, + &op->v.ind.base_kind, &op->v.ind.base_type); + if (op->v.ind.index_kind != OPT_INDIRECT_REG || + op->v.ind.index != (Reg)REG_NONE) { + Operand idx; + indirect_part_operand(f, op, op->v.ind.index, op->v.ind.index_kind, + op->v.ind.index_type, &idx); fn(f, in, &idx, 0, ctx); - op->v.ind.index = idx.v.reg; + indirect_part_commit(f, in, op, &idx, &op->v.ind.index, + &op->v.ind.index_kind, &op->v.ind.index_type); } } } @@ -59,11 +148,11 @@ void opt_walk_abivalue(Func* f, Inst* in, CGABIValue* v, int storage_def, void opt_walk_inst_operands(Func* f, Inst* in, OptOperandWalkFn fn, void* ctx) { if (!in || !fn) return; for (u32 i = 0; i < in->nopnds; ++i) { - int is_def = opt_operand_index_is_def(in, i); + int is_def = opt_inst_operand_is_def(in, i); if (!is_def) opt_walk_operand(f, in, &in->opnds[i], 0, fn, ctx); } for (u32 i = 0; i < in->nopnds; ++i) { - int is_def = opt_operand_index_is_def(in, i); + int is_def = opt_inst_operand_is_def(in, i); if (is_def) opt_walk_operand(f, in, &in->opnds[i], 1, fn, ctx); } diff --git a/src/opt/pass_addr_fold.c b/src/opt/pass_addr_fold.c @@ -560,12 +560,14 @@ static void addr_cse_remap_op(Func* f, Inst* in, Operand* op, int is_def, void* ctx) { const PReg* remap = (const PReg*)ctx; PReg p; - (void)f; (void)in; (void)is_def; if (!op || op->kind != OPK_REG) return; p = (PReg)op->v.reg; - if (p != PREG_NONE && p != 0 && remap[p] != 0) op->v.reg = remap[p]; + if (p != PREG_NONE && p != 0 && remap[p] != 0) { + op->v.reg = remap[p]; + op->type = opt_reg_type(f, remap[p]); + } } static void addr_cse_apply_to_inst(Func* f, Inst* in, const PReg* remap) { diff --git a/src/opt/pass_coalesce.c b/src/opt/pass_coalesce.c @@ -144,32 +144,12 @@ static int group_conflicts(const CoalesceCtx* c, PReg ra, PReg rb, PReg allow_a, return 0; } -static int hard_reg_possible(Func* f, u8 cls, u32 forbidden, u32 allowed) { +static int hard_reg_possible(Func* f, u8 cls, u32 forbidden) { for (u32 i = 0; i < f->opt_hard_reg_count[cls]; ++i) { Reg r = f->opt_hard_regs[cls][i]; if (r >= 32) continue; - if (allowed && (allowed & (1u << r)) == 0) continue; if ((forbidden & (1u << r)) == 0) return 1; } - if (allowed) { - for (Reg r = 0; r < 32; ++r) { - if ((allowed & (1u << r)) == 0) continue; - if (forbidden & (1u << r)) continue; - int in_hard = 0; - for (u32 i = 0; i < f->opt_hard_reg_count[cls]; ++i) { - if (f->opt_hard_regs[cls][i] == r) { - in_hard = 1; - break; - } - } - if (in_hard) continue; - for (u32 i = 0; i < f->opt_phys_reg_count[cls]; ++i) { - const CGPhysRegInfo* pi = &f->opt_phys_regs[cls][i]; - if (pi->reg == r && (pi->flags & CG_REG_RESERVED) == 0) return 1; - } - } - return 0; - } return f->opt_hard_reg_count[cls] == 0; } @@ -178,32 +158,15 @@ static int group_constraints_compatible(const CoalesceCtx* c, PReg ra, Func* f = c->f; u8 cls = opt_reg_cls(f, ra); KitCgTypeId type = opt_reg_type(f, ra); - i32 tied = -1; u32 forbidden = 0; - u32 allowed = 0; for (PReg v = 1; v < opt_reg_count(f); ++v) { PReg r = coalesce_find(f, v); if (r != ra && r != rb) continue; if (opt_reg_cls(f, v) != cls || opt_reg_type(f, v) != type) return 0; const OptPRegInfo* vi = &f->preg_info[v]; forbidden |= vi->forbidden_hard_regs; - if (vi->allowed_hard_regs) { - if (allowed) { - allowed &= vi->allowed_hard_regs; - if (!allowed) return 0; - } else { - allowed = vi->allowed_hard_regs; - } - } - if (vi->tied_hard_reg >= 0) { - if (tied >= 0 && tied != vi->tied_hard_reg) return 0; - tied = vi->tied_hard_reg; - } } - if (tied >= 0 && tied < 32 && (forbidden & (1u << (Reg)tied))) return 0; - if (tied >= 0 && tied < 32 && allowed && (allowed & (1u << (Reg)tied)) == 0) - return 0; - return hard_reg_possible(f, cls, forbidden, allowed); + return hard_reg_possible(f, cls, forbidden); } static void coalesce_union(Func* f, PReg a, PReg b) { @@ -373,11 +336,9 @@ typedef struct LinCoalState { PReg* members; /* per root: PReg members of the coalesce group (capped) */ u32 nmembers; u32 forbidden; /* OR of forbidden_hard_regs over the group */ - u32 allowed; /* intersection of nonzero allowed masks; 0 if none */ - u8 has_allowed; /* a member contributed a positive allowed mask */ u8 init; /* state has been populated for this root */ - i32 tied; /* single tied hard reg, or -1 */ u8 cls; + u8 pad[2]; KitCgTypeId type; } LinCoalState; @@ -410,29 +371,7 @@ static int lin_constraints_ok(Func* f, const LinCoalState* sa, const LinCoalState* sb) { if (sa->cls != sb->cls || sa->type != sb->type) return 0; u32 forbidden = sa->forbidden | sb->forbidden; - u32 allowed = 0; - u8 has_allowed = 0; - if (sa->has_allowed && sb->has_allowed) { - allowed = sa->allowed & sb->allowed; - has_allowed = 1; - if (!allowed) return 0; - } else if (sa->has_allowed) { - allowed = sa->allowed; - has_allowed = 1; - } else if (sb->has_allowed) { - allowed = sb->allowed; - has_allowed = 1; - } - i32 tied = sa->tied; - if (sb->tied >= 0) { - if (tied >= 0 && tied != sb->tied) return 0; - tied = sb->tied; - } - if (tied >= 0 && tied < 32 && (forbidden & (1u << (Reg)tied))) return 0; - if (tied >= 0 && tied < 32 && has_allowed && - (allowed & (1u << (Reg)tied)) == 0) - return 0; - return hard_reg_possible(f, sa->cls, forbidden, has_allowed ? allowed : 0u); + return hard_reg_possible(f, sa->cls, forbidden); } void opt_coalesce_linear(Func* f, const OptLiveRangeSet* ranges) { @@ -482,10 +421,7 @@ void opt_coalesce_linear(Func* f, const OptLiveRangeSet* ranges) { st[v].members = NULL; st[v].nmembers = 0; st[v].forbidden = 0; - st[v].allowed = 0; - st[v].has_allowed = 0; st[v].init = 0; - st[v].tied = -1; st[v].cls = 0; st[v].type = 0; } @@ -500,11 +436,6 @@ void opt_coalesce_linear(Func* f, const OptLiveRangeSet* ranges) { st[v].members[0] = v; st[v].nmembers = 1; st[v].forbidden = vi->forbidden_hard_regs; - if (vi->allowed_hard_regs) { - st[v].allowed = vi->allowed_hard_regs; - st[v].has_allowed = 1; - } - st[v].tied = vi->tied_hard_reg; st[v].cls = opt_reg_cls(f, v); st[v].type = opt_reg_type(f, v); } @@ -531,15 +462,6 @@ void opt_coalesce_linear(Func* f, const OptLiveRangeSet* ranges) { for (u32 m = 0; m < st[orr].nmembers; ++m) st[nr].members[st[nr].nmembers++] = st[orr].members[m]; st[nr].forbidden |= st[orr].forbidden; - if (st[orr].has_allowed) { - if (st[nr].has_allowed) - st[nr].allowed &= st[orr].allowed; - else { - st[nr].allowed = st[orr].allowed; - st[nr].has_allowed = 1; - } - } - if (st[orr].tied >= 0) st[nr].tied = st[orr].tied; st[orr].nmembers = 0; /* folded into nr */ ++f->opt_coalesce_merges; } diff --git a/src/opt/pass_combine.c b/src/opt/pass_combine.c @@ -37,6 +37,20 @@ static int frame_slot_is_spill(Func* f, FrameSlot fs) { return f->frame_slots[fs - 1u].kind == FS_SPILL; } +typedef enum SpillAccessKind { + SPILL_ACCESS_NONE = 0, + SPILL_ACCESS_LOAD, + SPILL_ACCESS_STORE, +} SpillAccessKind; + +typedef struct SpillAccess { + SpillAccessKind kind; + FrameSlot slot; + u32 size; + u16 addr_space; + const Operand* value; +} SpillAccess; + /* W6 address-offset profitability gate: is `ofs` an offset the target can keep * inline in a load/store (so folding it into an indirect whose base producer * survives does not force an emit-time address build)? Conservatively keyed on @@ -49,55 +63,51 @@ static int combine_offset_fold_ok(NativeTarget* target, KitCgTypeId type, return target->imm_legal(target, NATIVE_IMM_ADDR_OFFSET, 0, type, ofs); } -static int spill_local_slot(Func* f, const Operand* addr, const MemAccess* mem, - FrameSlot* out) { - if (!addr || addr->kind != OPK_LOCAL) return 0; - if (opt_mem_observable(mem)) return 0; - if (mem->alias.kind != ALIAS_LOCAL) return 0; - if (mem->alias.v.local_id != (i32)addr->v.frame_slot) return 0; - if (!frame_slot_is_spill(f, addr->v.frame_slot)) return 0; - *out = addr->v.frame_slot; - return 1; -} +/* Normalize location-MIR stack copies to the one spill-access vocabulary + * consumed by compaction and stack DSE. Spill traffic exists only after + * allocation and deliberately uses IR_COPY + OPK_STACK: recognizing semantic + * IR_LOAD/IR_STORE + OPK_LOCAL here would collapse a spill value back into an + * addressable source-language local and violate the HIR/MIR boundary. */ +static int direct_spill_access(Func* f, const Inst* in, SpillAccess* out) { + SpillAccess access; + const Operand* stack = NULL; + memset(&access, 0, sizeof access); + if (!f || !in || (IROp)in->op != IR_COPY || in->nopnds < 2u || + !in->opnds) + return 0; -static int same_spill_access(Func* f, const Inst* a, const Inst* b, - FrameSlot* slot_out) { - FrameSlot as = FRAME_SLOT_NONE; - FrameSlot bs = FRAME_SLOT_NONE; - if (!spill_local_slot(f, &a->opnds[0], &a->extra.mem, &as)) return 0; - if (!spill_local_slot(f, &b->opnds[0], &b->extra.mem, &bs)) return 0; - if (as != bs) return 0; - if (a->extra.mem.size != b->extra.mem.size) return 0; - if (a->extra.mem.addr_space != b->extra.mem.addr_space) return 0; - if (slot_out) *slot_out = as; - return 1; -} + if (in->opnds[0].kind == OPK_STACK && + in->opnds[1].kind != OPK_STACK) { + access.kind = SPILL_ACCESS_STORE; + stack = &in->opnds[0]; + access.value = &in->opnds[1]; + } else if (in->opnds[1].kind == OPK_STACK && + in->opnds[0].kind != OPK_STACK) { + access.kind = SPILL_ACCESS_LOAD; + stack = &in->opnds[1]; + access.value = &in->opnds[0]; + } else { + return 0; + } -static int load_spill_slot(Func* f, const Inst* in, FrameSlot* slot_out) { - if ((IROp)in->op != IR_LOAD || in->nopnds < 2) return 0; - return spill_local_slot(f, &in->opnds[1], &in->extra.mem, slot_out); -} + if (!frame_slot_is_spill(f, stack->v.frame_slot)) return 0; + { + u64 size = cg_type_size(f->c, stack->type); + if (!size || size > UINT32_MAX) return 0; + access.slot = stack->v.frame_slot; + access.size = (u32)size; + } + access.addr_space = 0u; -static int store_spill_slot(Func* f, const Inst* in, FrameSlot* slot_out) { - if ((IROp)in->op != IR_STORE || in->nopnds < 2) return 0; - return spill_local_slot(f, &in->opnds[0], &in->extra.mem, slot_out); + if (!access.size || access.slot == FRAME_SLOT_NONE || !access.value) + return 0; + if (out) *out = access; + return 1; } -static int same_spill_slot_and_size(Func* f, const Inst* a, const Inst* b) { - FrameSlot as = FRAME_SLOT_NONE; - FrameSlot bs = FRAME_SLOT_NONE; - if ((IROp)a->op == IR_LOAD) { - if (!load_spill_slot(f, a, &as)) return 0; - } else if (!store_spill_slot(f, a, &as)) { - return 0; - } - if ((IROp)b->op == IR_LOAD) { - if (!load_spill_slot(f, b, &bs)) return 0; - } else if (!store_spill_slot(f, b, &bs)) { - return 0; - } - return as == bs && a->extra.mem.size == b->extra.mem.size && - a->extra.mem.addr_space == b->extra.mem.addr_space; +static int same_spill_shape(const SpillAccess* a, const SpillAccess* b) { + return a && b && a->slot == b->slot && a->size == b->size && + a->addr_space == b->addr_space; } static int same_phys_reg(const Operand* a, const Operand* b) { @@ -105,208 +115,65 @@ static int same_phys_reg(const Operand* a, const Operand* b) { a->cls == b->cls && a->v.reg == b->v.reg; } -/* Count register references inside a single operand. An OPK_INDIRECT can - * reference the queried register twice (e.g. `[r4 + r4 * 4]`); we return the - * true count so single-use accounting is exact. */ -static int count_operand_phys_uses(const Operand* op, const Operand* r) { +static int operand_uses_phys_reg(const Operand* op, const Operand* r) { if (!op || !r || r->kind != OPK_REG) return 0; if (op->kind == OPK_REG) return op->cls == r->cls && op->v.reg == r->v.reg ? 1 : 0; if (op->kind == OPK_INDIRECT) { if (r->cls != RC_INT) return 0; - int n = 0; - if (op->v.ind.base == r->v.reg) ++n; - if (op->v.ind.index != (Reg)REG_NONE && op->v.ind.index == r->v.reg) ++n; - return n; + return (op->v.ind.base_kind == OPT_INDIRECT_REG && + op->v.ind.base == r->v.reg) || + (op->v.ind.index_kind == OPT_INDIRECT_REG && + op->v.ind.index != (Reg)REG_NONE && + op->v.ind.index == r->v.reg); } return 0; } -static int operand_uses_phys_reg(const Operand* op, const Operand* r) { - return count_operand_phys_uses(op, r) > 0; +static int inst_uses_phys_reg(Func* f, const Inst* in, const Operand* reg) { + OptRegEffects effects; + if (!reg || reg->kind != OPK_REG || reg->cls >= OPT_REG_CLASSES || + reg->v.reg >= OPT_MAX_HARD_REGS) + return 0; + opt_inst_reg_effects(f, in, &effects); + return effects.use_count[reg->cls][reg->v.reg]; } -static int abi_uses_phys_reg(const CGABIValue* v, const Operand* r) { - int n = 0; - if (!v) return 0; - n += count_operand_phys_uses(&v->storage, r); - for (u32 i = 0; i < v->nparts; ++i) - n += count_operand_phys_uses(&v->parts[i].op, r); - return n; +static int hard_set_has_reg(const OptHardRegSet* set, const Operand* reg) { + if (!set || !reg || reg->kind != OPK_REG || + reg->cls >= OPT_REG_CLASSES || reg->v.reg >= OPT_MAX_HARD_REGS) + return 0; + return (set->cls[reg->cls] & (1u << reg->v.reg)) != 0; } -static int inst_uses_phys_reg(const Inst* in, const Operand* r) { - int n = 0; - switch ((IROp)in->op) { - case IR_COPY: - case IR_CONVERT: - case IR_UNOP: - case IR_VA_ARG: - if (in->nopnds >= 2) n += count_operand_phys_uses(&in->opnds[1], r); - break; - case IR_LOAD: - case IR_ADDR_OF: - case IR_BITFIELD_LOAD: - case IR_ATOMIC_LOAD: - if (in->nopnds >= 2) n += count_operand_phys_uses(&in->opnds[1], r); - break; - case IR_BINOP: - case IR_CMP: - if (in->nopnds >= 2) n += count_operand_phys_uses(&in->opnds[1], r); - if (in->nopnds >= 3) n += count_operand_phys_uses(&in->opnds[2], r); - break; - case IR_STORE: - case IR_AGG_COPY: - case IR_AGG_SET: - case IR_BITFIELD_STORE: - case IR_VA_COPY: - if (in->nopnds >= 1) n += count_operand_phys_uses(&in->opnds[0], r); - if (in->nopnds >= 2) n += count_operand_phys_uses(&in->opnds[1], r); - break; - case IR_CALL: { - IRCallAux* aux = (IRCallAux*)in->extra.aux; - if (!aux) break; - if (aux->use_plan_replay) { - n += count_operand_phys_uses(&aux->plan.callee, r); - for (u32 i = 0; i < aux->plan.nargs; ++i) - n += count_operand_phys_uses(&aux->plan.args[i].src, r); - } else { - n += count_operand_phys_uses(&aux->desc.callee, r); - for (u32 i = 0; i < aux->desc.nargs; ++i) - n += abi_uses_phys_reg(&aux->desc.args[i], r); - } - break; - } - case IR_CMP_BRANCH: - case IR_CONDBR: - case IR_SWITCH: - case IR_INDIRECT_BRANCH: - for (u32 i = 0; i < in->nopnds; ++i) - n += count_operand_phys_uses(&in->opnds[i], r); - break; - case IR_RET: { - IRRetAux* aux = (IRRetAux*)in->extra.aux; - if (aux && aux->present) n += abi_uses_phys_reg(&aux->val, r); - break; - } - case IR_SCOPE_BEGIN: - break; - case IR_ALLOCA: - if (in->nopnds >= 2) n += count_operand_phys_uses(&in->opnds[1], r); - break; - case IR_VA_START: - case IR_VA_END: - if (in->nopnds >= 1) n += count_operand_phys_uses(&in->opnds[0], r); - break; - case IR_ATOMIC_STORE: - if (in->nopnds >= 1) n += count_operand_phys_uses(&in->opnds[0], r); - if (in->nopnds >= 2) n += count_operand_phys_uses(&in->opnds[1], r); - break; - case IR_ATOMIC_RMW: - if (in->nopnds >= 2) n += count_operand_phys_uses(&in->opnds[1], r); - if (in->nopnds >= 3) n += count_operand_phys_uses(&in->opnds[2], r); - break; - case IR_ATOMIC_CAS: - if (in->nopnds >= 3) n += count_operand_phys_uses(&in->opnds[2], r); - if (in->nopnds >= 4) n += count_operand_phys_uses(&in->opnds[3], r); - if (in->nopnds >= 5) n += count_operand_phys_uses(&in->opnds[4], r); - break; - case IR_ASM_BLOCK: { - IRAsmAux* aux = (IRAsmAux*)in->extra.aux; - if (!aux) break; - for (u32 i = 0; i < aux->nin; ++i) - n += count_operand_phys_uses(&aux->in_ops[i], r); - break; - } - case IR_INTRINSIC: { - IRIntrinAux* aux = (IRIntrinAux*)in->extra.aux; - if (!aux) break; - for (u32 i = 0; i < aux->narg; ++i) - n += count_operand_phys_uses(&aux->args[i], r); - break; - } - default: - break; - } - return n; +static int inst_defines_phys_reg(Func* f, const Inst* in, const Operand* reg) { + OptRegEffects effects; + opt_inst_reg_effects(f, in, &effects); + return hard_set_has_reg(&effects.defs, reg); } -static int abi_defines_phys_reg(const CGABIValue* v, const Operand* r) { - int n = 0; - if (!v) return 0; - if (same_phys_reg(&v->storage, r)) ++n; - for (u32 i = 0; i < v->nparts; ++i) - if (same_phys_reg(&v->parts[i].op, r)) ++n; - return n; +static int inst_clobbers_phys_reg(Func* f, const Inst* in, + const Operand* reg) { + OptRegEffects effects; + opt_inst_reg_effects(f, in, &effects); + return hard_set_has_reg(&effects.clobbers, reg); } -static int inst_defines_phys_reg(const Inst* in, const Operand* r) { - if (!r || r->kind != OPK_REG) return 0; - switch ((IROp)in->op) { - case IR_LOAD_IMM: - case IR_LOAD_CONST: - case IR_LOAD_LABEL_ADDR: - case IR_COPY: - case IR_LOAD: - case IR_ADDR_OF: - case IR_TLS_ADDR_OF: - case IR_BITFIELD_LOAD: - case IR_BINOP: - case IR_UNOP: - case IR_CMP: - case IR_CONVERT: - case IR_ALLOCA: - case IR_VA_ARG: - case IR_ATOMIC_LOAD: - case IR_ATOMIC_RMW: - return in->nopnds >= 1 && same_phys_reg(&in->opnds[0], r); - case IR_CALL: { - IRCallAux* aux = (IRCallAux*)in->extra.aux; - if (!aux) return 0; - if (aux->use_plan_replay) { - for (u32 i = 0; i < aux->plan.nargs; ++i) - if (aux->plan.args[i].dst_kind == CG_CALL_PLAN_REG && - r->cls == aux->plan.args[i].cls && - r->v.reg == aux->plan.args[i].dst_reg) - return 1; - for (u32 i = 0; i < aux->plan.nrets; ++i) - if ((r->cls == aux->plan.rets[i].cls && - r->v.reg == aux->plan.rets[i].src_reg) || - same_phys_reg(&aux->plan.rets[i].dst, r)) - return 1; - return 0; - } - return abi_defines_phys_reg(&aux->desc.ret, r); - } - case IR_ATOMIC_CAS: - return (in->nopnds >= 1 && same_phys_reg(&in->opnds[0], r)) || - (in->nopnds >= 2 && same_phys_reg(&in->opnds[1], r)); - case IR_ASM_BLOCK: { - IRAsmAux* aux = (IRAsmAux*)in->extra.aux; - if (!aux) return 0; - for (u32 i = 0; i < aux->nout; ++i) - if (same_phys_reg(&aux->out_ops[i], r)) return 1; - if (r->cls < OPT_REG_CLASSES && r->v.reg < 32 && - (aux->clobber_mask[r->cls] & (1u << r->v.reg))) - return 1; - return 0; - } - case IR_INTRINSIC: { - IRIntrinAux* aux = (IRIntrinAux*)in->extra.aux; - if (!aux) return 0; - for (u32 i = 0; i < aux->ndst; ++i) - if (same_phys_reg(&aux->dsts[i], r)) return 1; - return 0; - } - default: - return 0; - } +static int inst_kills_phys_reg(Func* f, const Inst* in, const Operand* reg) { + OptRegEffects effects; + OptHardRegSet kills; + opt_inst_reg_effects(f, in, &effects); + opt_reg_effect_kills(&effects, &kills); + return hard_set_has_reg(&kills, reg); } /* True if `in` may write to memory. Used to invalidate memory-reading * producers (IR_LOAD) when the consumer is past an intervening write. */ static int inst_writes_memory(const Inst* in) { switch ((IROp)in->op) { + case IR_COPY: + return in->nopnds >= 1u && in->opnds && + in->opnds[0].kind == OPK_STACK; case IR_STORE: case IR_AGG_COPY: case IR_AGG_SET: @@ -325,6 +192,9 @@ static int inst_writes_memory(const Inst* in) { static int inst_reads_memory(const Inst* in) { switch ((IROp)in->op) { + case IR_COPY: + return in->nopnds >= 2u && in->opnds && + in->opnds[1].kind == OPK_STACK; case IR_LOAD: case IR_BITFIELD_LOAD: case IR_ATOMIC_LOAD: @@ -408,9 +278,9 @@ typedef struct AddrCseEntry { /* W5 (O1.md): local (same-block) redundant-load + pure-compute CSE. Two small * most-recent-wins rings: * - * - LOAD ring: the most-recently-loaded `{address, size, addr_space}` and the - * register that received the value. A later plain (non-observable, - * non-bit-field) IR_LOAD of the IDENTICAL address+shape, with the producer + * - LOAD ring: the most-recently-loaded `{address, result, MemAccess}` and + * the register that received the value. A later plain (non-observable, + * non-bit-field) IR_LOAD of the IDENTICAL semantic shape, with the producer * register still live and NO intervening memory write, is rewritten to an * IR_COPY off the producer. Aliasing is the whole game: ANY memory write * (inst_writes_memory: store/aggregate/atomic/call/asm/intrinsic) forgets @@ -418,12 +288,12 @@ typedef struct AddrCseEntry { * loads always preserves the reload. Volatile/atomic loads * (opt_mem_observable) are never recorded or reused. * - * - COMPUTE ring: the most-recent pure IR_BINOP/IR_UNOP `{op, operands}` and - * its destination register. A later identical pure compute whose inputs are - * unchanged since (and whose producer is still live) is rewritten to an - * IR_COPY off the producer. Pure computes touch no memory, so only register - * redefinitions/clobbers invalidate them (tracked via ctx->last_def, which a - * clobber barrier bumps for every register). + * - COMPUTE ring: the most-recent pure IR_BINOP/IR_UNOP `{op, mode, result, + * operands}` and its destination register. A later identical pure compute + * whose inputs are unchanged since (and whose producer is still live) is + * rewritten to an IR_COPY off the producer. Pure computes touch no memory, + * so only register redefinitions/clobbers invalidate them (tracked via + * ctx->last_def, which a clobber barrier bumps for every register). * * Both rings are bounded → O(1) per inst → the pass stays linear. Each rewrite * is the broader, map-based same-block form distinct from the adjacent @@ -435,14 +305,16 @@ typedef struct LoadCseEntry { i32 inst_idx; /* producing IR_LOAD index in this BB, -1 if empty */ Operand dst; /* the register that received the loaded value (OPK_REG) */ Operand addr; /* the load's address operand (OPK_INDIRECT/LOCAL/GLOBAL) */ - u32 size; /* MemAccess size */ - u16 addr_space; + KitCgTypeId result_type; + MemAccess mem; } LoadCseEntry; typedef struct ComputeCseEntry { i32 inst_idx; /* producing IR_BINOP/IR_UNOP index in this BB, -1 if empty */ Operand dst; /* the result register (OPK_REG) */ u16 op; /* IROp (IR_BINOP / IR_UNOP) */ + u16 flags; /* per-instruction semantic mode */ + KitCgTypeId result_type; i64 sub; /* extra.imm = BinOp/UnOp selector */ Operand a; /* operand 1 */ Operand b; /* operand 2 (kind == 0 sentinel for a unary compute) */ @@ -468,8 +340,8 @@ typedef struct CombineCtx { /* W6 cmp-immediate tracking: the constant currently held by each integer * hard register, if known. const_valid is a per-RC_INT-reg bit set when a * reaching IR_LOAD_IMM defined that register and nothing since has redefined - * or clobbered it (incl. call/asm/intrinsic clobbers, tracked precisely via - * opt_hard_inst_use_def). Lets a `cmp wN, wM` whose wM holds a small constant + * or clobbered it (incl. call/asm/machine clobbers, tracked precisely via + * opt_inst_reg_effects). Lets a `cmp wN, wM` whose wM holds a small constant * fold to `cmp wN, #k`, even across a call that preserves wM (a callee-saved * reg). Integer-only: cmp immediates are integer. */ i64 const_val[OPT_MAX_HARD_REGS]; @@ -493,67 +365,25 @@ static void ctx_reset(CombineCtx* ctx) { ctx->block_change_p = 0; } -/* True if `in` is a barrier that conservatively invalidates all prior - * producers in this BB (modelled after the old find_single_direct_use, which - * treats every IR_CALL as a clobber regardless of explicit ABI describes). */ -static int inst_is_clobber_barrier(const Inst* in) { - switch ((IROp)in->op) { - case IR_CALL: - case IR_ASM_BLOCK: - case IR_INTRINSIC: - return 1; - default: - return 0; +static void ctx_record_kills(CombineCtx* ctx, const OptHardRegSet* kills, + i32 i) { + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) { + u32 mask = kills->cls[c]; + for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r) + if (mask & (1u << r)) ctx->last_def[c][r] = i; } } -static void ctx_record_reg_def(CombineCtx* ctx, const Operand* op, i32 i) { - if (op->kind != OPK_REG) return; - if (op->cls >= OPT_REG_CLASSES || op->v.reg >= OPT_MAX_HARD_REGS) return; - ctx->last_def[op->cls][op->v.reg] = i; -} - -/* After processing inst at index `i`, mark each register it defines (incl. - * implicit clobbers from CALL / ASM / INTRINSIC) as defined here. Mark - * memory if this inst writes. - * - * Walks the inst's destination operand(s) directly rather than probing every - * (cls, reg) pair. The barrier case (CALL/ASM/INTRINSIC) keeps a bulk mark - * because explicit defs/clobbers aren't always populated in IRCallAux at - * this point in the pipeline. */ +/* Record the canonical explicit definitions and implicit clobbers after one + * instruction. Calls and asm no longer erase every producer: only the ABI or + * target-declared registers are invalidated. */ static void ctx_record(CombineCtx* ctx, const Inst* in, i32 i) { + OptRegEffects effects; + OptHardRegSet kills; if (inst_writes_memory(in)) ctx->last_mem_def = i; - if (inst_is_clobber_barrier(in)) { - for (u32 c = 0; c < OPT_REG_CLASSES; ++c) - for (u32 r = 0; r < OPT_MAX_HARD_REGS; ++r) ctx->last_def[c][r] = i; - return; - } - switch ((IROp)in->op) { - case IR_LOAD_IMM: - case IR_LOAD_CONST: - case IR_LOAD_LABEL_ADDR: - case IR_COPY: - case IR_LOAD: - case IR_ADDR_OF: - case IR_TLS_ADDR_OF: - case IR_BITFIELD_LOAD: - case IR_BINOP: - case IR_UNOP: - case IR_CMP: - case IR_CONVERT: - case IR_ALLOCA: - case IR_VA_ARG: - case IR_ATOMIC_LOAD: - case IR_ATOMIC_RMW: - if (in->nopnds >= 1) ctx_record_reg_def(ctx, &in->opnds[0], i); - break; - case IR_ATOMIC_CAS: - if (in->nopnds >= 1) ctx_record_reg_def(ctx, &in->opnds[0], i); - if (in->nopnds >= 2) ctx_record_reg_def(ctx, &in->opnds[1], i); - break; - default: - break; - } + opt_inst_reg_effects(ctx->f, in, &effects); + opt_reg_effect_kills(&effects, &kills); + ctx_record_kills(ctx, &kills, i); } /* Lookup the producer of (cls, reg) in this BB, if any. Returns -1 if no @@ -577,8 +407,7 @@ static i32 ctx_prev_def_before(const CombineCtx* ctx, const Operand* reg, if (!reg || reg->kind != OPK_REG) return -1; for (i32 j = before_idx - 1; j >= 0; --j) { const Inst* prev = &ctx->bl->insts[j]; - if (inst_is_clobber_barrier(prev) || inst_defines_phys_reg(prev, reg)) - return j; + if (inst_kills_phys_reg(ctx->f, prev, reg)) return j; } return -1; } @@ -606,21 +435,6 @@ static void ctx_restore_removed_def(CombineCtx* ctx, const Operand* reg, * Without this live-range scoping, reuse of a scratch physreg later in the * block (`sxtw x12, ...; mov x13, x12; mov x12, ...`) makes every fold look * multi-use and combine rejects almost everything. */ -/* True if `in` redefines or clobbers `def`'s physical register. Consults the - * instruction's precise def/clobber set (the same one liveness uses) rather - * than a blanket clobber-barrier test: a call clobbers only its caller-saved - * set, so a value held in a callee-saved register survives it. Treating every - * call as killing every register made `killed` spuriously true and let combine - * skip the cross-block live-out check, deleting a still-live def. */ -static int inst_kills_phys_reg(Func* f, const Inst* in, const Operand* def) { - OptHardRegSet use, d; - if (!def || def->kind != OPK_REG || def->cls >= OPT_REG_CLASSES || - def->v.reg >= 32) - return 0; - opt_hard_inst_use_def(f, in, &use, &d); - return (d.cls[def->cls] & (1u << def->v.reg)) != 0; -} - static int count_uses_in_live_range(Func* f, const Block* bl, i32 prod_idx, const Operand* def, int* killed_in_block_out) { @@ -628,7 +442,7 @@ static int count_uses_in_live_range(Func* f, const Block* bl, i32 prod_idx, int killed = 0; for (i32 i = prod_idx + 1; i < (i32)bl->ninsts; ++i) { const Inst* in = &bl->insts[i]; - n += inst_uses_phys_reg(in, def); + n += inst_uses_phys_reg(f, in, def); if (inst_kills_phys_reg(f, in, def)) { killed = 1; break; @@ -638,14 +452,14 @@ static int count_uses_in_live_range(Func* f, const Block* bl, i32 prod_idx, return n; } -static int use_after_clobber_before_redef(const Block* bl, i32 prod_idx, - const Operand* def) { +static int use_after_clobber_before_redef(Func* f, const Block* bl, + i32 prod_idx, const Operand* def) { int saw_clobber = 0; for (i32 i = prod_idx + 1; i < (i32)bl->ninsts; ++i) { const Inst* in = &bl->insts[i]; - if (saw_clobber && inst_uses_phys_reg(in, def)) return 1; - if (inst_defines_phys_reg(in, def)) return 0; - if (inst_is_clobber_barrier(in)) saw_clobber = 1; + if (saw_clobber && inst_uses_phys_reg(f, in, def)) return 1; + if (inst_defines_phys_reg(f, in, def)) return 0; + if (inst_clobbers_phys_reg(f, in, def)) saw_clobber = 1; } return 0; } @@ -800,6 +614,13 @@ static int first_return_reg(Func* f, u8 cls, Reg* out) { return 0; } +static int reg_is_emit_temp(Func* f, u8 cls, Reg reg) { + if (!f || cls >= OPT_REG_CLASSES || reg >= OPT_MAX_HARD_REGS) return 0; + for (u32 i = 0; i < f->emit_temp_reg_count[cls]; ++i) + if (f->emit_temp_regs[cls][i] == reg) return 1; + return 0; +} + static int ret_scalar_storage(CGABIValue* v, Operand** out) { if (!v || v->storage.kind != OPK_REG) return 0; if (v->nparts > 1) return 0; @@ -807,21 +628,44 @@ static int ret_scalar_storage(CGABIValue* v, Operand** out) { return 1; } -static int is_target_scratch_reg(Func* f, u8 cls, Reg reg) { - if (!f || cls >= OPT_REG_CLASSES) return 0; - for (u32 i = 0; i < f->opt_scratch_reg_count[cls]; ++i) { - if (f->opt_scratch_regs[cls][i] == reg) return 1; - } - return 0; -} - /* ---- Rewrite 1: substitute producer source into uses ---- */ -/* Set the indirect's base or index to `new_reg`. */ -static void set_indirect_field(Operand* ind, Reg old_reg, Reg new_reg) { - if (ind->v.ind.base == old_reg) ind->v.ind.base = new_reg; - if (ind->v.ind.index != (Reg)REG_NONE && ind->v.ind.index == old_reg) - ind->v.ind.index = new_reg; +/* Retarget a direct register use through a physical copy. The register is a + * location; the rest of the operand belongs to the consumer and describes how + * that location is interpreted there. In particular, same-register-class + * conversions can become IR_COPY, so replacing the whole operand can narrow a + * compare or ABI move. It would also discard consumer riders such as `shift`. + */ +static int retarget_reg_use(Operand* use, const Operand* def, + const Operand* src) { + if (!use || !def || !src || use->kind != OPK_REG || + src->kind != OPK_REG || !same_phys_reg(use, def) || + use->cls != src->cls) + return 0; + use->v.reg = src->v.reg; + return 1; +} + +/* Substitute a register-valued address component without confusing a MIR + * frame-slot id for a hard-register number. Only physical identity follows a + * copy: the component type belongs to the use and records how those bits are + * interpreted there. This matters when a same-width conversion became a + * copy (pointer source consumed as an integer ptrdiff index, for example). */ +static int set_indirect_reg(Operand* ind, Reg old_reg, const Operand* src) { + int n = 0; + if (!ind || !src || src->kind != OPK_REG) return 0; + if (ind->v.ind.base_kind == OPT_INDIRECT_REG && + ind->v.ind.base == old_reg) { + ind->v.ind.base = src->v.reg; + ++n; + } + if (ind->v.ind.index_kind == OPT_INDIRECT_REG && + ind->v.ind.index != (Reg)REG_NONE && + ind->v.ind.index == old_reg) { + ind->v.ind.index = src->v.reg; + ++n; + } + return n; } /* Substitute `def` -> `src` in a single aux register slot (no slot-index @@ -829,17 +673,10 @@ static void set_indirect_field(Operand* ind, Reg old_reg, Reg new_reg) { * register). Returns 1 if rewritten. */ static int subst_one_aux_operand(Operand* op, const Operand* def, const Operand* src) { - if (op->kind == OPK_REG && same_phys_reg(op, def)) { - *op = *src; - return 1; - } + if (retarget_reg_use(op, def, src)) return 1; if (op->kind == OPK_INDIRECT && src->kind == OPK_REG && def->cls == RC_INT && - src->cls == RC_INT && - (op->v.ind.base == def->v.reg || - (op->v.ind.index != (Reg)REG_NONE && op->v.ind.index == def->v.reg))) { - set_indirect_field(op, def->v.reg, src->v.reg); - return 1; - } + src->cls == RC_INT) + return set_indirect_reg(op, def->v.reg, src) != 0; return 0; } @@ -866,21 +703,24 @@ static int subst_consumer_operands(Inst* in, const Operand* def, /* Direct OPK_REG substitution: requires the slot to be on the whitelist. */ if (op->kind == OPK_REG && same_phys_reg(op, def) && combine_subst_slot(in, oi, kind, copy_imm_ok)) { - *op = *src; - ++n; - continue; + if (kind == SK_REG) { + if (retarget_reg_use(op, def, src)) { + ++n; + continue; + } + } else { + *op = *src; + ++n; + continue; + } } /* Indirect base/index substitution: only OPK_REG src may land here. The * substitution only changes which register computes the address, not the * value being stored — safe for IR_STORE / IR_ATOMIC_STORE / * IR_BITFIELD_STORE / IR_AGG_COPY / IR_AGG_SET as well. */ if (op->kind == OPK_INDIRECT && kind == SK_REG && src->kind == OPK_REG && - def->kind == OPK_REG && def->cls == RC_INT && src->cls == RC_INT && - (op->v.ind.base == def->v.reg || - (op->v.ind.index != (Reg)REG_NONE && op->v.ind.index == def->v.reg))) { - set_indirect_field(op, def->v.reg, src->v.reg); - ++n; - } + def->kind == OPK_REG && def->cls == RC_INT && src->cls == RC_INT) + n += set_indirect_reg(op, def->v.reg, src); } /* Aux register uses: IR_CALL plan/desc args + callee. SK_REG only — @@ -929,8 +769,6 @@ static int try_substitute_for_reg(CombineCtx* ctx, Inst* in, i32 i, u8 cls, if (ctx_def_changed_since(ctx, prod->opnds[1].cls, prod->opnds[1].v.reg, prod_idx)) return 0; - if (is_target_scratch_reg(ctx->f, prod->opnds[1].cls, prod->opnds[1].v.reg)) - return 0; kind = SK_REG; src_op = prod->opnds[1]; } else if (pop == IR_LOAD_IMM) { @@ -1009,10 +847,12 @@ static void try_substitute_aux_operand(CombineCtx* ctx, Inst* in, i32 i, if (!seen_mark(seen, op->cls, op->v.reg)) *any |= try_substitute_for_reg(ctx, in, i, op->cls, op->v.reg); } else if (op->kind == OPK_INDIRECT) { - if (op->v.ind.base != (Reg)REG_NONE && + if (op->v.ind.base_kind == OPT_INDIRECT_REG && + op->v.ind.base != (Reg)REG_NONE && !seen_mark(seen, RC_INT, op->v.ind.base)) *any |= try_substitute_for_reg(ctx, in, i, RC_INT, op->v.ind.base); - if (op->v.ind.index != (Reg)REG_NONE && + if (op->v.ind.index_kind == OPT_INDIRECT_REG && + op->v.ind.index != (Reg)REG_NONE && !seen_mark(seen, RC_INT, op->v.ind.index)) *any |= try_substitute_for_reg(ctx, in, i, RC_INT, op->v.ind.index); } @@ -1069,7 +909,8 @@ static int try_addr_synth_one_op(CombineCtx* ctx, Inst* in, i32 i, /* (a/c) base producer is IR_BINOP IADD. reg+reg synthesizes a base+index * pair (requires no existing index, sub-rule a); reg+imm folds the immediate * into ofs (sub-rule c, ok with or without an existing index). */ - if (op->v.ind.base != (Reg)REG_NONE) { + if (op->v.ind.base_kind == OPT_INDIRECT_REG && + op->v.ind.base != (Reg)REG_NONE) { Reg b = op->v.ind.base; i32 prod_idx = ctx_producer_of(ctx, RC_INT, b); if (prod_idx >= 0 && prod_idx < i) { @@ -1089,19 +930,24 @@ static int try_addr_synth_one_op(CombineCtx* ctx, Inst* in, i32 i, ctx->f, ctx->hard_live, ctx->bl->id, &prod_def)); Operand lhs = prod->opnds[1]; Operand rhs = prod->opnds[2]; - int has_no_index = (op->v.ind.index == (Reg)REG_NONE); + int has_no_index = + op->v.ind.index_kind == OPT_INDIRECT_REG && + op->v.ind.index == (Reg)REG_NONE; /* reg + reg: base = lhs, index = rhs, scale=0. Needs an empty index * slot — cannot stack two indices. This synthesizes a new index, a * structural change, so keep the single-use guard: with other live * uses the add must stay, and we'd add an index without retiring it. */ if (single_use && has_no_index && lhs.kind == OPK_REG && rhs.kind == OPK_REG && lhs.cls == RC_INT && rhs.cls == RC_INT && + opt_indirect_index_type_valid(ctx->f, rhs.type) && !producer_def_aliases_source(&prod_def, &lhs) && !producer_def_aliases_source(&prod_def, &rhs) && !ctx_def_changed_since(ctx, RC_INT, lhs.v.reg, prod_idx) && !ctx_def_changed_since(ctx, RC_INT, rhs.v.reg, prod_idx)) { op->v.ind.base = lhs.v.reg; + op->v.ind.base_type = lhs.type; op->v.ind.index = rhs.v.reg; + op->v.ind.index_type = rhs.type; op->v.ind.log2_scale = 0; any = 1; } @@ -1127,6 +973,7 @@ static int try_addr_synth_one_op(CombineCtx* ctx, Inst* in, i32 i, (single_use || combine_offset_fold_ok(ctx->target, op->type, sum))) { op->v.ind.base = lhs.v.reg; + op->v.ind.base_type = lhs.type; op->v.ind.ofs = (i32)sum; any = 1; } @@ -1142,6 +989,7 @@ static int try_addr_synth_one_op(CombineCtx* ctx, Inst* in, i32 i, (single_use || combine_offset_fold_ok(ctx->target, op->type, sum))) { op->v.ind.base = rhs.v.reg; + op->v.ind.base_type = rhs.type; op->v.ind.ofs = (i32)sum; any = 1; } @@ -1151,7 +999,8 @@ static int try_addr_synth_one_op(CombineCtx* ctx, Inst* in, i32 i, } /* (b) index producer is IR_BINOP ISHL reg, imm with scale=0. */ - if (op->v.ind.index != (Reg)REG_NONE && op->v.ind.log2_scale == 0) { + if (op->v.ind.index_kind == OPT_INDIRECT_REG && + op->v.ind.index != (Reg)REG_NONE && op->v.ind.log2_scale == 0) { Reg idx = op->v.ind.index; i32 prod_idx = ctx_producer_of(ctx, RC_INT, idx); if (prod_idx >= 0 && prod_idx < i) { @@ -1178,6 +1027,7 @@ static int try_addr_synth_one_op(CombineCtx* ctx, Inst* in, i32 i, !ctx_def_changed_since(ctx, RC_INT, prod->opnds[1].v.reg, prod_idx)) { op->v.ind.index = prod->opnds[1].v.reg; + op->v.ind.index_type = prod->opnds[1].type; op->v.ind.log2_scale = (u8)sh; any = 1; } @@ -1193,7 +1043,8 @@ static int try_addr_synth_one_op(CombineCtx* ctx, Inst* in, i32 i, * the shift into log2_scale and then this absorbs the sxtw producing that * index. Gated behind the target capability: only a backend that emits the * extended-register addressing form is handed an index_ext rider. */ - if (op->v.ind.index != (Reg)REG_NONE && + if (op->v.ind.index_kind == OPT_INDIRECT_REG && + op->v.ind.index != (Reg)REG_NONE && op->v.ind.index_ext == OPT_IDX_EXT_NONE && ctx->target && ctx->target->can_fold_extend_into_addr && ctx->target->can_fold_extend_into_addr(ctx->target)) { @@ -1222,6 +1073,7 @@ static int try_addr_synth_one_op(CombineCtx* ctx, Inst* in, i32 i, !ctx_def_changed_since(ctx, RC_INT, prod->opnds[1].v.reg, prod_idx)) { op->v.ind.index = prod->opnds[1].v.reg; + op->v.ind.index_type = prod->opnds[1].type; op->v.ind.index_ext = sign_p ? (u8)OPT_IDX_EXT_SXTW : (u8)OPT_IDX_EXT_UXTW; any = 1; @@ -1346,8 +1198,11 @@ static int try_sink(CombineCtx* ctx, Inst* in, i32 i) { if (p->kind == OPK_REG) { if (ctx_def_changed_since(ctx, p->cls, p->v.reg, prod_idx)) return 0; } else if (p->kind == OPK_INDIRECT) { - if (ctx_def_changed_since(ctx, RC_INT, p->v.ind.base, prod_idx)) return 0; - if (p->v.ind.index != (Reg)REG_NONE && + if (p->v.ind.base_kind == OPT_INDIRECT_REG && + ctx_def_changed_since(ctx, RC_INT, p->v.ind.base, prod_idx)) + return 0; + if (p->v.ind.index_kind == OPT_INDIRECT_REG && + p->v.ind.index != (Reg)REG_NONE && ctx_def_changed_since(ctx, RC_INT, p->v.ind.index, prod_idx)) return 0; } @@ -1359,7 +1214,8 @@ static int try_sink(CombineCtx* ctx, Inst* in, i32 i) { * copy that originally wrote it. */ for (i32 j = prod_idx + 1; j < i; ++j) { Inst* mid = &ctx->bl->insts[j]; - if (inst_uses_phys_reg(mid, &dst) || inst_defines_phys_reg(mid, &dst)) + if (inst_uses_phys_reg(ctx->f, mid, &dst) || + inst_kills_phys_reg(ctx->f, mid, &dst)) return 0; } @@ -1370,7 +1226,8 @@ static int try_sink(CombineCtx* ctx, Inst* in, i32 i) { int uses_total = count_uses_in_live_range(ctx->f, ctx->bl, prod_idx, &src, &killed); if (uses_total != 1) return 0; - if (killed && use_after_clobber_before_redef(ctx->bl, prod_idx, &src)) + if (killed && + use_after_clobber_before_redef(ctx->f, ctx->bl, prod_idx, &src)) return 0; if (!killed && opt_block_live_out_has_phys_reg(ctx->f, ctx->hard_live, ctx->bl->id, &src)) @@ -1385,7 +1242,6 @@ static int try_sink(CombineCtx* ctx, Inst* in, i32 i) { prod->opnds[2] = tmp; } prod->opnds[0] = dst; - if (prod->def != VAL_NONE) prod->def = (Val)dst.v.reg; /* Update last-def: producer no longer defines src, now defines dst. If src * had an earlier reaching definition in the block, keep it visible for @@ -1438,18 +1294,16 @@ static int try_combine_exts(CombineCtx* ctx, Inst* in, i32 i) { } /* L6 (O1-PATTERNS): the SIGNED mirror of the ZEXT-of-load fold above. A - * narrow integer load fills the ENTIRE destination register on every target - * (native_direct_target.c:1700): a flagged sign-extending load (MF_SEXT_LOAD, - * `ldrsb`/`ldrsh`) sign-extends to register width, and a plain load - * (`ldrb`/`ldrh`) zero-extends to register width. A subsequent SXTB/SXTH/SXTW - * convert is then a no-op in two shapes — drop it to an IR_COPY (copy-prop + - * DCE retire it; worst case a same-cost register move): + * narrow integer load fills the ENTIRE destination register. A backend that + * advertises load_sext honors MF_SEXT_LOAD with a sign-extending load + * (`ldrsb`/`ldrsh`); every backend's plain load (`ldrb`/`ldrh`) zero-extends. + * A subsequent SXTB/SXTH/SXTW convert is then a no-op in two shapes — drop it + * to an IR_COPY (copy-prop + DCE retire it; worst case a same-cost move): * - * (a) SEXT of a sign-extending load of equal-or-smaller width - * (`mem.size <= sb`): the load already replicated the loaded sign bit - * across the whole register, so re-sign-extending from `sb >= mem.size` - * bytes re-reads bits that are already that sign extension -> identity. - * (`ldrsb x; sxtb w` / `ldrsb x; sxtw x` — yyjson's signature.) + * (a) SEXT of a sign-extending load of equal-or-smaller width, on a target + * that honors MF_SEXT_LOAD (`mem.size <= sb`): the load already + * replicated the loaded sign bit across the whole register, so + * re-sign-extending from `sb >= mem.size` bytes is an identity. * * (b) SEXT of a ZERO-extending load whose value cannot have the sign bit set * in the widened position (`mem.size < sb`, STRICT): the loaded value @@ -1463,7 +1317,10 @@ static int try_combine_exts(CombineCtx* ctx, Inst* in, i32 i) { prod->extra.mem.size && prod->nopnds >= 1 && same_reg_operand(&prod->opnds[0], &in->opnds[1])) { int sext_load = (prod->extra.mem.flags & MF_SEXT_LOAD) != 0; - if ((sext_load && prod->extra.mem.size <= sb) || + int load_sign_extends = + sext_load && ctx->target && ctx->target->regs && + ctx->target->regs->load_sext; + if ((load_sign_extends && prod->extra.mem.size <= sb) || (!sext_load && prod->extra.mem.size < sb)) { in->op = IR_COPY; in->nopnds = 2; /* opnds[0]=dst, opnds[1]=src already in place */ @@ -1793,11 +1650,6 @@ static int try_addr_of_cse(CombineCtx* ctx, Inst* in, i32 i) { continue; /* A copy of the producer reg into self is a no-op the dst already holds. */ if (same_phys_reg(&in->opnds[0], &e->dst)) continue; - /* Native emission may reuse a reserved scratch between MIR instructions - * while materializing an immediate or frame operand. Such clobbers are - * deliberately invisible to MIR liveness, so extending an address value - * held in scratch to this later copy is unsound. */ - if (is_target_scratch_reg(ctx->f, e->dst.cls, e->dst.v.reg)) continue; /* Rewrite `addr_of rD, <addr>` into `copy rD, rP`. */ in->op = (u16)IR_COPY; in->opnds[1] = e->dst; @@ -1810,11 +1662,13 @@ static int try_addr_of_cse(CombineCtx* ctx, Inst* in, i32 i) { /* ---- Rewrite 8 (W5): local same-block redundant-load elimination ---- */ -/* Two load-address operands name the same address iff identical kind+payload. - * For an OPK_INDIRECT both the base, index, scale, AND offset must match; the - * register-unchanged-since check is applied separately by the caller. */ +/* Two load-address operands name the same typed address iff their kind and + * payload agree. For an OPK_INDIRECT both the base, index, scale, AND offset + * must match; the register-unchanged-since check is applied separately by the + * caller. */ static int same_load_addr_operand(const Operand* a, const Operand* b) { - if (a->kind != b->kind) return 0; + if (a->kind != b->kind || a->cls != b->cls || a->type != b->type) + return 0; switch (a->kind) { case OPK_LOCAL: return a->v.frame_slot == b->v.frame_slot; @@ -1824,6 +1678,10 @@ static int same_load_addr_operand(const Operand* a, const Operand* b) { case OPK_INDIRECT: return a->v.ind.base == b->v.ind.base && a->v.ind.index == b->v.ind.index && + a->v.ind.base_kind == b->v.ind.base_kind && + a->v.ind.index_kind == b->v.ind.index_kind && + a->v.ind.base_type == b->v.ind.base_type && + a->v.ind.index_type == b->v.ind.index_type && a->v.ind.log2_scale == b->v.ind.log2_scale && a->v.ind.index_ext == b->v.ind.index_ext && a->v.ind.ofs == b->v.ind.ofs; @@ -1832,6 +1690,55 @@ static int same_load_addr_operand(const Operand* a, const Operand* b) { } } +/* Alias roots are optimizer metadata rather than part of the bytes returned by + * a load, but keeping them in this deliberately conservative local key avoids + * letting CSE erase a distinction made by an upstream alias classifier. */ +static int same_load_alias_root(const AliasRoot* a, const AliasRoot* b) { + if (a->kind != b->kind) return 0; + switch ((AliasKind)a->kind) { + case ALIAS_UNKNOWN: + case ALIAS_HEAP: + return 1; + case ALIAS_LOCAL: + return a->v.local_id == b->v.local_id; + case ALIAS_GLOBAL: + return a->v.global == b->v.global; + case ALIAS_PARAM: + return a->v.param_idx == b->v.param_idx; + case ALIAS_STRING: + return a->v.string_id == b->v.string_id; + default: + return 0; + } +} + +/* Compare explicit fields rather than struct bytes: MemAccess contains padding + * and a tagged union, so memcmp would make the key depend on inactive storage. + * Even fields that usually do not alter the returned bits (alignment and alias + * classification) stay in this bounded key. That keeps reuse conservative and + * aligned with the richer memory key used by O2 GVN. */ +static int same_load_mem_access(const MemAccess* a, const MemAccess* b) { + return a->type == b->type && a->size == b->size && a->align == b->align && + a->flags == b->flags && a->addr_space == b->addr_space && + a->bf_offset == b->bf_offset && a->bf_width == b->bf_width && + a->bf_signed == b->bf_signed && + same_load_alias_root(&a->alias, &b->alias); +} + +/* Physical register identity is intentionally absent: CSE expects distinct + * destinations. The value interpretation carried by the instruction and its + * destination operand must nevertheless agree before a producer can be copied + * into the later destination. */ +static int same_cse_result_shape(const Inst* in, KitCgTypeId result_type, + const Operand* dst) { + const Operand* current; + if (!in || !dst || in->nopnds < 1) return 0; + current = &in->opnds[0]; + return current->kind == OPK_REG && dst->kind == OPK_REG && + in->type == current->type && result_type == dst->type && + in->type == result_type && current->cls == dst->cls; +} + /* A plain, CSE-eligible IR_LOAD: dst is a register, address is a * direct/indirect memory operand, and the access is neither observable * (volatile/atomic) nor a bit-field rider. */ @@ -1840,13 +1747,10 @@ static int load_is_cseable(const Inst* in) { if (in->opnds[0].kind != OPK_REG) return 0; const Operand* addr = &in->opnds[1]; /* W5 owns pointer-deref (OPK_INDIRECT) and global (OPK_GLOBAL) loads — the - * `*p` / global-variable same-block reload. Direct frame-slot loads - * (OPK_LOCAL spill/local reloads) are deliberately EXCLUDED: that traffic is - * the domain of the adjacent spill compaction in opt_combine_compact_block - * (store/load, load/load, load/store, store/store pairs) and the linear-scan - * spill machinery, which the doc says to keep distinct from W5. Reusing a - * spill reload here both duplicates that machinery and is unsound against the - * way the spill/coalesce path may re-home a frame slot, so it stays out. */ + * `*p` / global-variable same-block reload. Semantic OPK_LOCAL traffic stays + * out of this narrow CSE, while location-MIR spills are represented as + * IR_COPY/OPK_STACK and owned by opt_combine_compact_block. Reusing either + * here would duplicate its alias/location authority. */ if (addr->kind != OPK_INDIRECT && addr->kind != OPK_GLOBAL) return 0; if (opt_mem_observable(&in->extra.mem)) return 0; if (in->extra.mem.bf_width != 0) return 0; /* bit-field rider */ @@ -1860,8 +1764,12 @@ static int load_is_cseable(const Inst* in) { * happened AT the recorded load's own index. */ if (in->opnds[0].cls == RC_INT && addr->kind == OPK_INDIRECT) { Reg d = in->opnds[0].v.reg; - if (addr->v.ind.base == d) return 0; - if (addr->v.ind.index != (Reg)REG_NONE && addr->v.ind.index == d) return 0; + if (addr->v.ind.base_kind == OPT_INDIRECT_REG && + addr->v.ind.base == d) + return 0; + if (addr->v.ind.index_kind == OPT_INDIRECT_REG && + addr->v.ind.index != (Reg)REG_NONE && addr->v.ind.index == d) + return 0; } return 1; } @@ -1872,10 +1780,12 @@ static int load_is_cseable(const Inst* in) { static int load_addr_regs_unchanged(const CombineCtx* ctx, const Operand* addr, i32 since_idx) { if (addr->kind != OPK_INDIRECT) return 1; - if (addr->v.ind.base != (Reg)REG_NONE && + if (addr->v.ind.base_kind == OPT_INDIRECT_REG && + addr->v.ind.base != (Reg)REG_NONE && ctx_def_changed_since(ctx, RC_INT, addr->v.ind.base, since_idx)) return 0; - if (addr->v.ind.index != (Reg)REG_NONE && + if (addr->v.ind.index_kind == OPT_INDIRECT_REG && + addr->v.ind.index != (Reg)REG_NONE && ctx_def_changed_since(ctx, RC_INT, addr->v.ind.index, since_idx)) return 0; return 1; @@ -1887,20 +1797,19 @@ static void load_cse_record(CombineCtx* ctx, const Inst* in, i32 i) { e->inst_idx = i; e->dst = in->opnds[0]; e->addr = in->opnds[1]; - e->size = in->extra.mem.size; - e->addr_space = in->extra.mem.addr_space; + e->result_type = in->type; + e->mem = in->extra.mem; ctx->load_cse_next = (ctx->load_cse_next + 1u) % COMBINE_LOAD_CSE_SLOTS; } static int try_local_load_cse(CombineCtx* ctx, Inst* in, i32 i) { if (!load_is_cseable(in)) return 0; const Operand* addr = &in->opnds[1]; - u32 size = in->extra.mem.size; - u16 as = in->extra.mem.addr_space; for (u32 k = 0; k < COMBINE_LOAD_CSE_SLOTS; ++k) { const LoadCseEntry* e = &ctx->load_cse[k]; if (e->inst_idx < 0 || e->inst_idx >= i) continue; - if (e->size != size || e->addr_space != as) continue; + if (!same_cse_result_shape(in, e->result_type, &e->dst)) continue; + if (!same_load_mem_access(&e->mem, &in->extra.mem)) continue; if (!same_load_addr_operand(&e->addr, addr)) continue; /* ALIASING: any memory write between the earlier load and now invalidates * reuse. ctx->last_mem_def is the index of the most-recent memory writer @@ -1921,13 +1830,9 @@ static int try_local_load_cse(CombineCtx* ctx, Inst* in, i32 i) { /* Same-class reuse only (an int load into an FP reg, or vice versa, would * need a cross-class move, not a same-class copy). */ if (in->opnds[0].cls != e->dst.cls) continue; - /* Never reuse a value that lives in a target scratch register: native emit - * repurposes the scratch regs to materialize OPK_LOCAL operands between MIR - * instructions (invisible to MIR-level liveness), so a new copy off such a - * register can read a clobbered value. */ - if (is_target_scratch_reg(ctx->f, e->dst.cls, e->dst.v.reg)) continue; /* Rewrite `load rD, [addr]` into `copy rD, rP`. */ in->op = (u16)IR_COPY; + in->flags = 0; in->opnds[1] = e->dst; in->nopnds = 2; ctx->block_change_p = 1; @@ -1960,9 +1865,11 @@ static int same_compute_operand(const Operand* a, const Operand* b) { /* The L7 shift rider is part of the operand's value: `x2` and `x2,lsl#2` * are different inputs, so a ridered and a riderless operand must not be * treated as the same compute (else CSE would drop the shift). */ - return a->cls == b->cls && a->v.reg == b->v.reg && a->shift == b->shift; + return a->cls == b->cls && a->v.reg == b->v.reg && + a->type == b->type && a->shift == b->shift; case OPK_IMM: - return a->v.imm == b->v.imm && a->type == b->type; + return a->cls == b->cls && a->v.imm == b->v.imm && + a->type == b->type; default: return 0; } @@ -1982,6 +1889,8 @@ static void compute_cse_record(CombineCtx* ctx, const Inst* in, i32 i) { e->inst_idx = i; e->dst = in->opnds[0]; e->op = in->op; + e->flags = in->flags; + e->result_type = in->type; e->sub = in->extra.imm; e->a = in->opnds[1]; if ((IROp)in->op == IR_BINOP) @@ -2000,7 +1909,10 @@ static int try_local_compute_cse(CombineCtx* ctx, Inst* in, i32 i) { for (u32 k = 0; k < COMBINE_COMPUTE_CSE_SLOTS; ++k) { const ComputeCseEntry* e = &ctx->compute_cse[k]; if (e->inst_idx < 0 || e->inst_idx >= i) continue; - if (e->op != in->op || e->sub != in->extra.imm) continue; + if (e->op != in->op || e->flags != in->flags || + e->sub != in->extra.imm) + continue; + if (!same_cse_result_shape(in, e->result_type, &e->dst)) continue; if (!same_compute_operand(&e->a, a)) continue; if (op == IR_BINOP) { if (!same_compute_operand(&e->b, b)) continue; @@ -2020,15 +1932,9 @@ static int try_local_compute_cse(CombineCtx* ctx, Inst* in, i32 i) { /* Self-copy is a no-op; skip. Same class only. */ if (same_phys_reg(&in->opnds[0], &e->dst)) continue; if (in->opnds[0].cls != e->dst.cls) continue; - /* Never reuse a value that lives in a target scratch register: the - * post-combine native emit repurposes the scratch regs to materialize - * OPK_LOCAL operands BETWEEN MIR instructions (invisible to MIR-level - * liveness), so extending such a register's live range to a new copy reads - * a clobbered value. (Same hazard the copy-prop path guards in - * try_substitute_for_reg.) */ - if (is_target_scratch_reg(ctx->f, e->dst.cls, e->dst.v.reg)) continue; /* Rewrite the recompute into `copy rD, rP`. */ in->op = (u16)IR_COPY; + in->flags = 0; in->opnds[1] = e->dst; in->nopnds = 2; in->extra.imm = 0; @@ -2049,6 +1955,7 @@ static int try_ret_retarget(Func* f, Block* bl, i32 i) { Reg ret_reg = REG_NONE; if (!aux || !aux->present || !ret_scalar_storage(&aux->val, &ret_op) || !first_return_reg(f, ret_op->cls, &ret_reg) || ret_reg == (Reg)REG_NONE || + reg_is_emit_temp(f, ret_op->cls, ret_reg) || ret_reg == ret_op->v.reg) return 0; Inst* producer = &bl->insts[i - 1u]; @@ -2141,7 +2048,7 @@ static int try_fold_const_convert(CombineCtx* ctx, Inst* in, i32 i) { * We track, per integer hard register, the constant a reaching IR_LOAD_IMM put * there (const_val / const_valid). On a cmp/cmp_branch we replace an immediate- * slot register operand holding a target-legal constant with that immediate. - * Tracking is invalidated precisely (opt_hard_inst_use_def gives each inst's + * Tracking is invalidated precisely (opt_inst_reg_effects gives each inst's * def + clobber set), so a constant in a callee-saved register survives calls * that preserve it. Linear: O(1) bookkeeping per inst. */ @@ -2149,11 +2056,13 @@ static int try_fold_const_convert(CombineCtx* ctx, Inst* in, i32 i) { * inst at index `i`. Must run for EVERY inst (called from the forward driver), * so the tracking stays sound across barriers and redefinitions. */ static void ctx_track_const(CombineCtx* ctx, const Inst* in) { - OptHardRegSet use, def; - opt_hard_inst_use_def(ctx->f, in, &use, &def); + OptRegEffects effects; + OptHardRegSet kills; + opt_inst_reg_effects(ctx->f, in, &effects); + opt_reg_effect_kills(&effects, &kills); /* Any register this inst defines or clobbers no longer holds its old known * constant. (RC_INT only — cmp immediates are integer.) */ - ctx->const_valid &= ~def.cls[RC_INT]; + ctx->const_valid &= ~kills.cls[RC_INT]; /* A load_imm of an integer hard register records the new constant. */ if ((IROp)in->op == IR_LOAD_IMM && in->nopnds >= 1 && in->opnds[0].kind == OPK_REG && in->opnds[0].cls == RC_INT && @@ -2222,18 +2131,24 @@ static int opt_combine_fold_block(Func* f, Block* bl, if (enable_o1_combine_rewrites && try_ret_retarget(f, bl, i)) { ctx.block_change_p = 1; - /* The producer's def changed; update ctx for the producer at i-1. */ + /* The producer's destination changed. Remove every old effect no longer + * present, then record its canonical post-rewrite effects. */ Inst* prev = &bl->insts[i - 1]; - Operand probe; - memset(&probe, 0, sizeof probe); - probe.kind = OPK_REG; + OptRegEffects effects; + OptHardRegSet kills; + opt_inst_reg_effects(f, prev, &effects); + opt_reg_effect_kills(&effects, &kills); for (u8 c = 0; c < OPT_REG_CLASSES; ++c) { - probe.cls = c; for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r) { - probe.v.reg = r; if (ctx.last_def[c][r] == i - 1 && - !inst_defines_phys_reg(prev, &probe)) + !(kills.cls[c] & (1u << r))) { + Operand probe; + memset(&probe, 0, sizeof probe); + probe.kind = OPK_REG; + probe.cls = c; + probe.v.reg = r; ctx_restore_removed_def(&ctx, &probe, i - 1); + } } } ctx_record(&ctx, prev, i - 1); @@ -2315,9 +2230,15 @@ static int opt_combine_compact_block(Func* f, Block* bl) { if (w) { Inst* prev = &bl->insts[w - 1u]; - if ((IROp)prev->op == IR_STORE && (IROp)in->op == IR_LOAD && - same_spill_slot_and_size(f, prev, in) && - same_reg_operand(&prev->opnds[1], &in->opnds[0])) { + SpillAccess prev_access; + SpillAccess in_access; + int prev_is_spill = direct_spill_access(f, prev, &prev_access); + int in_is_spill = direct_spill_access(f, in, &in_access); + if (prev_is_spill && in_is_spill && + prev_access.kind == SPILL_ACCESS_STORE && + in_access.kind == SPILL_ACCESS_LOAD && + same_spill_shape(&prev_access, &in_access) && + same_reg_operand(prev_access.value, in_access.value)) { changed = 1; continue; } @@ -2333,33 +2254,41 @@ static int opt_combine_compact_block(Func* f, Block* bl) { * rX is dead with the slot unread, W8 stack-DSE + mir_dce retire the * store. Guard: only when the stored value is a register of the load * dst's class (an immediate/odd-class store is left as a real reload). */ - if ((IROp)prev->op == IR_STORE && (IROp)in->op == IR_LOAD && - same_spill_slot_and_size(f, prev, in) && prev->nopnds >= 2 && - in->nopnds >= 2 && prev->opnds[1].kind == OPK_REG && - in->opnds[0].kind == OPK_REG && - prev->opnds[1].cls == in->opnds[0].cls) { + if (prev_is_spill && in_is_spill && + prev_access.kind == SPILL_ACCESS_STORE && + in_access.kind == SPILL_ACCESS_LOAD && + same_spill_shape(&prev_access, &in_access) && + prev_access.value->kind == OPK_REG && + in_access.value->kind == OPK_REG && + prev_access.value->cls == in_access.value->cls) { in->op = (u16)IR_COPY; - in->opnds[1] = prev->opnds[1]; + in->opnds[1] = *prev_access.value; in->nopnds = 2; memset(&in->extra, 0, sizeof in->extra); changed = 1; bl->insts[w++] = *in; continue; } - if ((IROp)prev->op == IR_LOAD && (IROp)in->op == IR_STORE && - same_spill_slot_and_size(f, prev, in) && - same_reg_operand(&prev->opnds[0], &in->opnds[1])) { + if (prev_is_spill && in_is_spill && + prev_access.kind == SPILL_ACCESS_LOAD && + in_access.kind == SPILL_ACCESS_STORE && + same_spill_shape(&prev_access, &in_access) && + same_reg_operand(prev_access.value, in_access.value)) { changed = 1; continue; } - if ((IROp)prev->op == IR_LOAD && (IROp)in->op == IR_LOAD && - same_spill_slot_and_size(f, prev, in) && - same_reg_operand(&prev->opnds[0], &in->opnds[0])) { + if (prev_is_spill && in_is_spill && + prev_access.kind == SPILL_ACCESS_LOAD && + in_access.kind == SPILL_ACCESS_LOAD && + same_spill_shape(&prev_access, &in_access) && + same_reg_operand(prev_access.value, in_access.value)) { changed = 1; continue; } - if ((IROp)prev->op == IR_STORE && (IROp)in->op == IR_STORE && - same_spill_access(f, prev, in, NULL)) { + if (prev_is_spill && in_is_spill && + prev_access.kind == SPILL_ACCESS_STORE && + in_access.kind == SPILL_ACCESS_STORE && + same_spill_shape(&prev_access, &in_access)) { bl->insts[w - 1u] = *in; changed = 1; continue; @@ -2377,8 +2306,8 @@ static int opt_combine_compact_block(Func* f, Block* bl) { * O1.md W8. A forward MIR scan deletes a spill store when a later store in the * same block fully overwrites the same stack slot before any possible read. * Deliberately narrower than the parked O2 DSE: V1 handles exact direct frame - * spill stores only (IR_STORE of OPK_LOCAL(slot) with ALIAS_LOCAL == slot, - * !observable, no bit-field rider, exact same {slot,size,addr_space}). + * spill stores only (location-MIR IR_COPY/OPK_STACK, exact same + * {slot,size,addr_space}). * * Fast design (no per-block hash table, no O(nslots) clear): dense per-FrameSlot * side arrays plus a `gen` counter that is bumped at block start AND on any @@ -2399,21 +2328,43 @@ typedef struct StackDseState { u32 gen; } StackDseState; -/* A direct, DSE-eligible spill store: IR_STORE of a frame-local spill slot, - * not observable (volatile/atomic), aliasing exactly that local, no bit-field - * rider. Fills *slot. */ -static int dse_direct_spill_store(Func* f, const Inst* in, FrameSlot* slot) { - if (!store_spill_slot(f, in, slot)) return 0; - if (in->extra.mem.bf_width != 0) return 0; /* bit-field rider: bail */ +/* A direct location-MIR spill store (IR_COPY into OPK_STACK). */ +static int dse_direct_spill_store(Func* f, const Inst* in, + SpillAccess* access) { + SpillAccess found; + if (!direct_spill_access(f, in, &found) || + found.kind != SPILL_ACCESS_STORE) + return 0; + if (access) *access = found; return 1; } -/* A direct read of a spill slot (the read side that must preserve a pending - * store). Mirrors load_spill_slot but also covers bit-field/atomic loads of the - * slot defensively; those route through the barrier path anyway, but a plain - * IR_LOAD of the slot is the common case to clear. */ -static int dse_direct_spill_load(Func* f, const Inst* in, FrameSlot* slot) { - return load_spill_slot(f, in, slot); +typedef struct StackDseOperandCtx { + StackDseState* state; + int forget_defs; +} StackDseOperandCtx; + +/* Every OPK_STACK use is a real read of its spill home, including locations + * nested in an indirect address or an aux descriptor. A non-candidate stack + * definition also ends the simple pending-store chain. Use the canonical + * operand walker so DSE cannot silently acquire a narrower view of MIR than + * liveness/emission. For a candidate IR_COPY store, its one destination is the + * new pending store and is therefore retained; any nested source use is still + * forgotten before that store is recorded. */ +static void stack_dse_forget_operand(Func* f, Inst* in, Operand* op, + int is_def, void* arg) { + StackDseOperandCtx* ctx = (StackDseOperandCtx*)arg; + StackDseState* st = ctx ? ctx->state : NULL; + FrameSlot slot; + u32 si; + (void)f; + (void)in; + if (!st || !op || op->kind != OPK_STACK || (is_def && !ctx->forget_defs)) + return; + slot = op->v.frame_slot; + if (slot == FRAME_SLOT_NONE || slot > st->nslots) return; + si = slot - 1u; + if (st->seen_gen[si] == st->gen) st->seen_gen[si] = st->gen - 1u; } static void stack_dse_block(Func* f, Block* bl, StackDseState* st, @@ -2424,15 +2375,19 @@ static void stack_dse_block(Func* f, Block* bl, StackDseState* st, Inst* in = &bl->insts[i]; if ((IROp)in->op == IR_NOP) continue; - FrameSlot slot = FRAME_SLOT_NONE; + SpillAccess access; + int candidate_store = dse_direct_spill_store(f, in, &access); + StackDseOperandCtx operand_ctx; + operand_ctx.state = st; + operand_ctx.forget_defs = !candidate_store; + opt_walk_inst_operands(f, in, stack_dse_forget_operand, &operand_ctx); /* Accepted direct spill store: try to kill a prior pending store of the * exact same key, then record this one. */ - if (dse_direct_spill_store(f, in, &slot) && slot >= 1u && - slot <= st->nslots) { - u32 sz = in->extra.mem.size; - u16 as = in->extra.mem.addr_space; - u32 si = slot - 1u; + if (candidate_store && access.slot >= 1u && access.slot <= st->nslots) { + u32 sz = access.size; + u16 as = access.addr_space; + u32 si = access.slot - 1u; if (st->seen_gen[si] == st->gen) { if (st->last_size[si] == sz && st->last_aspace[si] == as) { /* Same {slot,size,addr_space}: the prior store is fully overwritten @@ -2457,30 +2412,24 @@ static void stack_dse_block(Func* f, Block* bl, StackDseState* st, continue; } - /* Accepted direct spill load: clears the pending store for that slot (it is - * read, so not dead). */ - if (dse_direct_spill_load(f, in, &slot) && slot >= 1u && - slot <= st->nslots) { - u32 si = slot - 1u; - if (st->seen_gen[si] == st->gen) st->seen_gen[si] = st->gen - 1u; - continue; - } - - /* A plain memory read that is NOT a direct spill load cannot alias any - * pending spill slot: FS_SPILL slots are allocator-private and never - * address-taken, so no computed pointer can reach them. Such a load (a - * struct field, a global, a heap deref) is therefore transparent to this - * spill-only DSE — do not bump gen for it. (A direct spill load was already - * handled above.) */ - if (inst_reads_memory(in)) continue; - + /* The canonical operand walk above already consumed every explicit spill + * read, including stack-valued arithmetic, aux operands, and spilled + * indirect components. Any remaining plain memory read cannot alias an + * FS_SPILL slot: those homes are allocator-private and never address-taken. + * A struct-field/global/heap read is therefore transparent to this + * spill-only DSE. */ /* Any other memory writer is a full barrier: a non-spill / non-direct store * (handled per-slot above only for direct spill stores), an aggregate op, a * call/asm/intrinsic, or an atomic/volatile access. These could, in * principle, touch state this narrow pass does not model, so forget every * pending store in O(1) by bumping gen. (Plain register-only insts — * binop/copy/convert/load_imm/etc. — are transparent.) */ - if (inst_writes_memory(in)) ++st->gen; + if (inst_writes_memory(in)) { + ++st->gen; + continue; + } + + if (inst_reads_memory(in)) continue; } } diff --git a/src/opt/pass_dce.c b/src/opt/pass_dce.c @@ -20,7 +20,9 @@ static int opt_inst_writes_frame_local(const Inst* in) { case IR_UNOP: case IR_CMP: case IR_CONVERT: - return in->nopnds > 0 && in->opnds[0].kind == OPK_LOCAL; + return in->nopnds > 0 && + (in->opnds[0].kind == OPK_LOCAL || + in->opnds[0].kind == OPK_STACK); default: return 0; } @@ -184,19 +186,22 @@ void opt_dce(Func* f) { for (u32 ri = bl->ninsts; ri > 0; --ri) { u32 i = ri - 1u; Inst* in = &bl->insts[i]; - OptHardRegSet use, def; + OptRegEffects effects; + OptHardRegSet kills; if ((IROp)in->op == IR_NOP) continue; - opt_hard_inst_use_def(f, in, &use, &def); - if (!opt_inst_has_side_effect(f, in) && !opt_hard_empty(&def) && - !opt_hard_intersects(&def, &live)) { + opt_inst_reg_effects(f, in, &effects); + opt_reg_effect_kills(&effects, &kills); + if (!opt_inst_has_side_effect(f, in) && + !opt_hard_empty(&effects.defs) && + !opt_hard_intersects(&effects.defs, &live)) { continue; } - if (!opt_inst_has_side_effect(f, in) && opt_hard_empty(&def) && - in->nopnds == 0) { + if (!opt_inst_has_side_effect(f, in) && + opt_hard_empty(&effects.defs) && in->nopnds == 0) { continue; } new_insts[w++] = *in; - opt_hard_live_step(&live, &use, &def); + opt_hard_live_step(&live, &effects.uses, &kills); } for (u32 i = 0; i < w / 2; ++i) { Inst tmp = new_insts[i]; diff --git a/src/opt/pass_hard_live.c b/src/opt/pass_hard_live.c @@ -3,19 +3,6 @@ #include "core/arena.h" #include "opt/opt_internal.h" -u32 opt_call_clobber_mask_for(Func* f, const Inst* in, u8 cls) { - if (cls >= OPT_REG_CLASSES) return 0; - if (in && (IROp)in->op == IR_CALL) { - IRCallAux* aux = (IRCallAux*)in->extra.aux; - if (aux && aux->plan_valid) return aux->plan.clobber_mask[cls]; - } - return f->opt_caller_saved[cls]; -} -static void hard_add(OptHardRegSet* s, u8 cls, Reg r) { - if (cls >= OPT_REG_CLASSES || r >= 32) return; - s->cls[cls] |= 1u << r; -} - int opt_hard_empty(const OptHardRegSet* s) { for (u32 c = 0; c < OPT_REG_CLASSES; ++c) if (s->cls[c]) return 0; @@ -51,164 +38,6 @@ void opt_hard_live_step(OptHardRegSet* live, const OptHardRegSet* use, live->cls[c] = (live->cls[c] & ~def->cls[c]) | use->cls[c]; } -static void hard_use_operand(OptHardRegSet* s, const Operand* op) { - if (!op) return; - if (op->kind == OPK_REG) { - hard_add(s, op->cls, op->v.reg); - } else if (op->kind == OPK_INDIRECT) { - hard_add(s, RC_INT, op->v.ind.base); - if (op->v.ind.index != (Reg)REG_NONE) hard_add(s, RC_INT, op->v.ind.index); - } -} - -static void hard_def_operand(OptHardRegSet* s, const Operand* op) { - if (op && op->kind == OPK_REG) hard_add(s, op->cls, op->v.reg); -} - -static void hard_def_clobber_mask(OptHardRegSet* s, const u32* masks) { - if (!masks) return; - for (u32 c = 0; c < OPT_REG_CLASSES; ++c) s->cls[c] |= masks[c]; -} - -static void hard_use_abivalue(OptHardRegSet* use, const CGABIValue* v) { - if (!v) return; - hard_use_operand(use, &v->storage); - for (u32 i = 0; i < v->nparts; ++i) hard_use_operand(use, &v->parts[i].op); -} - -static void hard_def_abivalue(OptHardRegSet* def, const CGABIValue* v) { - if (!v) return; - hard_def_operand(def, &v->storage); - for (u32 i = 0; i < v->nparts; ++i) hard_def_operand(def, &v->parts[i].op); -} - -void opt_hard_inst_use_def(Func* f, const Inst* in, OptHardRegSet* use, - OptHardRegSet* def) { - memset(use, 0, sizeof *use); - memset(def, 0, sizeof *def); - switch ((IROp)in->op) { - case IR_LOAD_IMM: - case IR_LOAD_CONST: - case IR_TLS_ADDR_OF: - if (in->nopnds >= 1) hard_def_operand(def, &in->opnds[0]); - break; - case IR_COPY: - case IR_CONVERT: - case IR_UNOP: - case IR_VA_ARG: - if (in->nopnds >= 1) hard_def_operand(def, &in->opnds[0]); - if (in->nopnds >= 2) hard_use_operand(use, &in->opnds[1]); - break; - case IR_LOAD: - case IR_ADDR_OF: - case IR_BITFIELD_LOAD: - case IR_ATOMIC_LOAD: - if (in->nopnds >= 1) hard_def_operand(def, &in->opnds[0]); - if (in->nopnds >= 2) hard_use_operand(use, &in->opnds[1]); - break; - case IR_BINOP: - case IR_CMP: - if (in->nopnds >= 1) hard_def_operand(def, &in->opnds[0]); - if (in->nopnds >= 2) hard_use_operand(use, &in->opnds[1]); - if (in->nopnds >= 3) hard_use_operand(use, &in->opnds[2]); - break; - case IR_STORE: - case IR_AGG_COPY: - case IR_AGG_SET: - case IR_BITFIELD_STORE: - case IR_VA_COPY: - if (in->nopnds >= 1) hard_use_operand(use, &in->opnds[0]); - if (in->nopnds >= 2) hard_use_operand(use, &in->opnds[1]); - break; - case IR_CALL: { - IRCallAux* aux = (IRCallAux*)in->extra.aux; - if (!aux) break; - if (aux->use_plan_replay) { - hard_use_operand(use, &aux->plan.callee); - for (u32 i = 0; i < aux->plan.nargs; ++i) { - hard_use_operand(use, &aux->plan.args[i].src); - if (aux->plan.args[i].dst_kind == CG_CALL_PLAN_REG) - hard_add(def, aux->plan.args[i].cls, aux->plan.args[i].dst_reg); - } - for (u32 i = 0; i < aux->plan.nrets; ++i) { - hard_add(def, aux->plan.rets[i].cls, aux->plan.rets[i].src_reg); - hard_def_operand(def, &aux->plan.rets[i].dst); - } - } else { - hard_use_operand(use, &aux->desc.callee); - for (u32 i = 0; i < aux->desc.nargs; ++i) - hard_use_abivalue(use, &aux->desc.args[i]); - hard_def_abivalue(def, &aux->desc.ret); - } - for (u32 c = 0; c < OPT_REG_CLASSES; ++c) { - u32 idx = c; - u32 mask = opt_call_clobber_mask_for(f, in, (u8)idx); - u32 old = def->cls[idx]; - def->cls[idx] = old | mask; - } - break; - } - case IR_CMP_BRANCH: - case IR_CONDBR: - case IR_SWITCH: - case IR_INDIRECT_BRANCH: - for (u32 i = 0; i < in->nopnds; ++i) hard_use_operand(use, &in->opnds[i]); - break; - case IR_LOAD_LABEL_ADDR: - if (in->nopnds >= 1) hard_def_operand(def, &in->opnds[0]); - break; - case IR_RET: { - IRRetAux* aux = (IRRetAux*)in->extra.aux; - if (aux && aux->present) hard_use_abivalue(use, &aux->val); - break; - } - case IR_SCOPE_BEGIN: - break; - case IR_ALLOCA: - if (in->nopnds >= 1) hard_def_operand(def, &in->opnds[0]); - if (in->nopnds >= 2) hard_use_operand(use, &in->opnds[1]); - break; - case IR_VA_START: - case IR_VA_END: - if (in->nopnds >= 1) hard_use_operand(use, &in->opnds[0]); - break; - case IR_ATOMIC_STORE: - if (in->nopnds >= 1) hard_use_operand(use, &in->opnds[0]); - if (in->nopnds >= 2) hard_use_operand(use, &in->opnds[1]); - break; - case IR_ATOMIC_RMW: - if (in->nopnds >= 1) hard_def_operand(def, &in->opnds[0]); - if (in->nopnds >= 2) hard_use_operand(use, &in->opnds[1]); - if (in->nopnds >= 3) hard_use_operand(use, &in->opnds[2]); - break; - case IR_ATOMIC_CAS: - if (in->nopnds >= 1) hard_def_operand(def, &in->opnds[0]); - if (in->nopnds >= 2) hard_def_operand(def, &in->opnds[1]); - if (in->nopnds >= 3) hard_use_operand(use, &in->opnds[2]); - if (in->nopnds >= 4) hard_use_operand(use, &in->opnds[3]); - if (in->nopnds >= 5) hard_use_operand(use, &in->opnds[4]); - break; - case IR_ASM_BLOCK: { - IRAsmAux* aux = (IRAsmAux*)in->extra.aux; - if (!aux) break; - for (u32 i = 0; i < aux->nin; ++i) hard_use_operand(use, &aux->in_ops[i]); - for (u32 i = 0; i < aux->nout; ++i) - hard_def_operand(def, &aux->out_ops[i]); - hard_def_clobber_mask(def, aux->clobber_mask); - break; - } - case IR_INTRINSIC: { - IRIntrinAux* aux = (IRIntrinAux*)in->extra.aux; - if (!aux) break; - for (u32 i = 0; i < aux->narg; ++i) hard_use_operand(use, &aux->args[i]); - for (u32 i = 0; i < aux->ndst; ++i) hard_def_operand(def, &aux->dsts[i]); - break; - } - default: - break; - } -} - static void hard_live_blocks(Func* f, OptHardBlockLive* live) { for (u32 b = 0; b < f->nblocks; ++b) { Block* bl = &f->blocks[b]; @@ -216,12 +45,15 @@ static void hard_live_blocks(Func* f, OptHardBlockLive* live) { memset(&seen_def, 0, sizeof seen_def); memset(&live[b], 0, sizeof live[b]); for (u32 i = 0; i < bl->ninsts; ++i) { - OptHardRegSet use, def; - opt_hard_inst_use_def(f, &bl->insts[i], &use, &def); + OptRegEffects effects; + OptHardRegSet kills; + opt_inst_reg_effects(f, &bl->insts[i], &effects); + opt_reg_effect_kills(&effects, &kills); for (u32 c = 0; c < OPT_REG_CLASSES; ++c) - live[b].live_use.cls[c] |= use.cls[c] & ~seen_def.cls[c]; - hard_or(&seen_def, &def); - hard_or(&live[b].live_def, &def); + live[b].live_use.cls[c] |= + effects.uses.cls[c] & ~seen_def.cls[c]; + hard_or(&seen_def, &kills); + hard_or(&live[b].live_def, &kills); } } diff --git a/src/opt/pass_lower.c b/src/opt/pass_lower.c @@ -161,49 +161,42 @@ static u32 live_update_refs_before_active(u64* live, u32 active_words, static void forbid_preg_reg(Func* f, PReg v, u8 cls, Reg r) { if (v == PREG_NONE || v == 0 || v >= opt_reg_count(f) || - cls >= OPT_REG_CLASSES || r >= 32) + cls >= OPT_REG_CLASSES || r >= OPT_MAX_HARD_REGS) return; if (opt_reg_cls(f, v) != cls) return; f->preg_info[v].forbidden_hard_regs |= 1u << r; } -static void apply_fixed_asm_operand(Func* f, Operand* op, i32 fixed, - u8 fixed_cls) { - if (!op || op->kind != OPK_REG || fixed < 0) return; - PReg v = (PReg)op->v.reg; - if (v == PREG_NONE || v == 0 || v >= opt_reg_count(f)) return; - if (fixed_cls >= OPT_REG_CLASSES || opt_reg_cls(f, v) != fixed_cls) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, "opt asm: fixed register class mismatch"); - } - f->preg_info[v].tied_hard_reg = fixed; -} - -static void apply_allowed_asm_operand(Func* f, Operand* op, u32 allowed, - u8 allowed_cls) { - u32 hard_mask = 0; - if (!op || op->kind != OPK_REG || !allowed) return; - PReg v = (PReg)op->v.reg; - if (v == PREG_NONE || v == 0 || v >= opt_reg_count(f)) return; - if (allowed_cls >= OPT_REG_CLASSES || opt_reg_cls(f, v) != allowed_cls) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, "opt asm: allowed register class mismatch"); - } - for (u32 i = 0; i < f->opt_hard_reg_count[allowed_cls]; ++i) { - Reg r = f->opt_hard_regs[allowed_cls][i]; - if (r < 32) hard_mask |= 1u << r; - } - if (f->preg_info[v].allowed_hard_regs) { - u32 both = f->preg_info[v].allowed_hard_regs & allowed; - if (!both) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, "opt asm: conflicting allowed register sets"); - } - f->preg_info[v].allowed_hard_regs = both; +static void reserve_asm_requirement(Func* f, + const IRAsmRegRequirement* req, u64* use, + u64* def, u64* live_after) { + u32 mask; + if (!f || !f->preg_info || !req || !req->present) return; + if (req->cls >= OPT_REG_CLASSES) + compiler_panic(f->c, (SrcLoc){0, 0, 0}, + "opt asm: invalid register requirement class"); + if (req->fixed_reg >= 0) { + if ((u32)req->fixed_reg >= OPT_MAX_HARD_REGS) + compiler_panic(f->c, (SrcLoc){0, 0, 0}, + "opt asm: invalid fixed register requirement"); + mask = 1u << (u32)req->fixed_reg; } else { - f->preg_info[v].allowed_hard_regs = allowed; + mask = req->allowed_mask; + } + /* Generic whole-class constraints need no reservation: ordinary allocated + * operands already satisfy them, while spills use the emitter's private temp + * bank. Fixed/subset constraints reserve their candidate registers only at + * this instruction. With an unsplit allocator the implementation is a + * conservative per-value forbid, but placement ownership remains local: the + * operand itself is staged rather than pinned for its whole live range. */ + if (!mask) return; + for (PReg v = 1; v < opt_reg_count(f); ++v) { + if (opt_reg_cls(f, v) != req->cls || + (!bit_has(use, v) && !bit_has(def, v) && + !(live_after && bit_has(live_after, v)))) + continue; + f->preg_info[v].forbidden_hard_regs |= mask; } - f->preg_info[v].forbidden_hard_regs |= hard_mask & ~allowed; } static void apply_asm_register_constraints(Func* f, Inst* in, u64* use, @@ -211,27 +204,17 @@ static void apply_asm_register_constraints(Func* f, Inst* in, u64* use, IRAsmAux* aux = (IRAsmAux*)in->extra.aux; if (!aux || !f->preg_info) return; - for (u32 i = 0; i < aux->nout; ++i) { - i32 fixed = aux->out_fixed_regs ? aux->out_fixed_regs[i] : -1; - u8 cls = aux->out_fixed_cls ? aux->out_fixed_cls[i] : 0; - u32 allowed = aux->out_allowed_masks ? aux->out_allowed_masks[i] : 0; - u8 allowed_cls = aux->out_allowed_cls ? aux->out_allowed_cls[i] : 0; - apply_allowed_asm_operand(f, &aux->out_ops[i], allowed, allowed_cls); - apply_fixed_asm_operand(f, &aux->out_ops[i], fixed, cls); - } - for (u32 i = 0; i < aux->nin; ++i) { - i32 fixed = aux->in_fixed_regs ? aux->in_fixed_regs[i] : -1; - u8 cls = aux->in_fixed_cls ? aux->in_fixed_cls[i] : 0; - u32 allowed = aux->in_allowed_masks ? aux->in_allowed_masks[i] : 0; - u8 allowed_cls = aux->in_allowed_cls ? aux->in_allowed_cls[i] : 0; - apply_allowed_asm_operand(f, &aux->in_ops[i], allowed, allowed_cls); - apply_fixed_asm_operand(f, &aux->in_ops[i], fixed, cls); - } + for (u32 i = 0; i < aux->nout; ++i) + reserve_asm_requirement(f, aux->out_reg_reqs ? &aux->out_reg_reqs[i] : NULL, + use, def, live_after); + for (u32 i = 0; i < aux->nin; ++i) + reserve_asm_requirement(f, aux->in_reg_reqs ? &aux->in_reg_reqs[i] : NULL, + use, def, live_after); for (u32 cls = 0; cls < OPT_REG_CLASSES; ++cls) { u32 mask = aux->clobber_mask[cls]; if (!mask) continue; - for (Reg r = 0; r < 32; ++r) { + for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r) { if ((mask & (1u << r)) == 0) continue; for (PReg v = 1; v < opt_reg_count(f); ++v) { if (!bit_has(use, v) && !bit_has(def, v) && @@ -257,17 +240,13 @@ static void apply_machine_reg_clobbers(Func* f, Inst* in, u64* def, for (u32 cls = 0; cls < OPT_REG_CLASSES; ++cls) { u32 mask = f->inst_clobbers[in->id][cls]; if (!mask) continue; - for (Reg r = 0; r < 32; ++r) { + for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r) { if ((mask & (1u << r)) == 0) continue; for (PReg v = 1; v < opt_reg_count(f); ++v) { if (!(live_after && bit_has(live_after, v)) || bit_has(def, v)) continue; if ((u8)opt_reg_cls(f, v) != (u8)cls) continue; f->preg_info[v].forbidden_hard_regs |= 1u << r; - /* Record this as a hard clobber so the return-register hint won't later - * clear the forbid and place the value in a register the instruction - * destroys (see set_preg_pref_to_ret_reg). */ - f->preg_info[v].clobbered_hard_regs |= 1u << r; } } } @@ -285,17 +264,22 @@ static int phys_arg_reg_for_index(Func* f, u8 cls, u32 abi_index, Reg* out) { return 0; } -static int hard_available(Func* f, u8 cls, Reg r); - static int is_caller_saved(Func* f, u8 cls, Reg r) { - if (cls >= OPT_REG_CLASSES || r >= 32) return 0; + if (cls >= OPT_REG_CLASSES || r >= OPT_MAX_HARD_REGS) return 0; return (f->opt_caller_saved[cls] & (1u << r)) != 0; } +static int is_emit_temp(Func* f, u8 cls, Reg r) { + if (!f || cls >= OPT_REG_CLASSES || r >= OPT_MAX_HARD_REGS) return 0; + for (u32 i = 0; i < f->emit_temp_reg_count[cls]; ++i) + if (f->emit_temp_regs[cls][i] == r) return 1; + return 0; +} + static Reg first_ret_reg(Func* f, u8 cls) { if (!f || cls >= OPT_REG_CLASSES) return REG_NONE; u32 mask = f->opt_ret_regs[cls]; - for (Reg r = 0; r < 32; ++r) + for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r) if (mask & (1u << r)) return r; return REG_NONE; } @@ -307,20 +291,18 @@ static void set_preg_pref_to_ret_reg(Func* f, const Operand* op) { u8 cls = f->preg_info[v].cls; if (cls >= OPT_REG_CLASSES) return; Reg hint = first_ret_reg(f, cls); - if (hint == REG_NONE || hint >= 32) return; - /* Don't override a real pin. */ - if (f->preg_info[v].tied_hard_reg >= 0) return; + if (hint == REG_NONE || hint >= OPT_MAX_HARD_REGS || + is_emit_temp(f, cls, hint)) + return; /* A value live across an instruction that clobbers the ret reg cannot live * there; skip the hint so the allocator places it elsewhere and the return * copy moves it into the ret reg (e.g. an accumulator returned past a va_arg, * which uses rax). */ - if (f->preg_info[v].clobbered_hard_regs & (1u << hint)) return; - /* The hint reg may not be in opt_hard_regs (e.g. x0 on aa64 is reserved - * as the ABI ret reg, outside aa_int_allocable); the allocator's - * preferred-reg branch will still consider it via the unit-overlap - * precision check. Clear any leftover soft forbid bit so the hint isn't - * silently blocked. */ - f->preg_info[v].forbidden_hard_regs &= ~(1u << hint); + if (f->preg_info[v].forbidden_hard_regs & (1u << hint)) return; + /* The hint reg may not be in opt_hard_regs (e.g. x0 on aa64 is reserved as + * the ABI return register, outside the NATIVE_REG_ALLOCABLE bank); the + * allocator's preferred-reg branch can still consider it. A hard forbid + * above always wins. */ f->preg_info[v].preferred_hard_reg = (i8)hint; } @@ -336,14 +318,14 @@ static void set_preg_pref_for_abivalue(Func* f, const CGABIValue* v) { * arg reg for the i-th call argument). */ static void set_preg_pref_to_arg_reg(Func* f, const Operand* op, Reg hint) { if (!op || op->kind != OPK_REG) return; - if (hint == REG_NONE || hint >= 32) return; + if (hint == REG_NONE || hint >= OPT_MAX_HARD_REGS) return; PReg v = (PReg)op->v.reg; if (v == PREG_NONE || v == 0 || v >= opt_reg_count(f)) return; u8 cls = f->preg_info[v].cls; if (cls >= OPT_REG_CLASSES) return; - if (f->preg_info[v].tied_hard_reg >= 0) return; + if (is_emit_temp(f, cls, hint)) return; if (f->preg_info[v].preferred_hard_reg >= 0) return; - f->preg_info[v].forbidden_hard_regs &= ~(1u << hint); + if (f->preg_info[v].forbidden_hard_regs & (1u << hint)) return; f->preg_info[v].preferred_hard_reg = (i8)hint; } @@ -405,10 +387,10 @@ static void set_preg_pref_for_params(Func* f) { if (*counter < 8u && phys_arg_reg_for_index(f, cls, *counter, &hint)) { PReg v = (PReg)p->storage.v.reg; if (v != PREG_NONE && v != 0 && v < opt_reg_count(f) && - f->preg_info[v].cls == cls && f->preg_info[v].tied_hard_reg < 0 && + f->preg_info[v].cls == cls && f->preg_info[v].preferred_hard_reg < 0 && hint != REG_NONE && - hint < 32) { - f->preg_info[v].forbidden_hard_regs &= ~(1u << hint); + hint < OPT_MAX_HARD_REGS && !is_emit_temp(f, cls, hint) && + !(f->preg_info[v].forbidden_hard_regs & (1u << hint))) { f->preg_info[v].preferred_hard_reg = (i8)hint; } } @@ -473,9 +455,9 @@ static void set_preg_pref_for_call_args(Func* f, const CGCallDesc* desc) { * copy then emits `mov x0, x8`. Propagating the hint from v_arg to v_value * lets both land at x0 and turns the copy into an identity move that * combine elides. Walks insts; for each IR_COPY whose def has a hint and - * whose single OPK_REG source operand has none, propagate (and clear the - * source's forbid for the hint reg). Safe because the copy itself dies - * once both sides share the reg. */ + * whose single OPK_REG source operand has none, propagate when that placement + * remains legal. Safe because the copy itself dies once both sides share the + * register. */ static void propagate_hint_through_copies(Func* f) { if (!f || !f->preg_info) return; for (u32 b = 0; b < f->nblocks; ++b) { @@ -492,16 +474,12 @@ static void propagate_hint_through_copies(Func* f) { if (src == PREG_NONE || src == 0 || src >= opt_reg_count(f)) continue; i8 dst_pref = f->preg_info[dst].preferred_hard_reg; if (dst_pref < 0) continue; - if (f->preg_info[src].tied_hard_reg >= 0) continue; if (f->preg_info[src].preferred_hard_reg >= 0) continue; if (f->preg_info[dst].cls != f->preg_info[src].cls) continue; - /* Don't propagate the hint into a value clobbered out of that register by - * a machine instruction live across it (e.g. a loop accumulator returned - * past a va_arg/idiv): it cannot live there. Leave it allocated - * elsewhere; the copy moves it into the hinted register. */ - if (f->preg_info[src].clobbered_hard_regs & (1u << (Reg)dst_pref)) + /* A boundary forbid always outranks the copy-placement preference (for + * example a loop accumulator returned past va_arg/idiv). */ + if (f->preg_info[src].forbidden_hard_regs & (1u << (Reg)dst_pref)) continue; - f->preg_info[src].forbidden_hard_regs &= ~(1u << (Reg)dst_pref); f->preg_info[src].preferred_hard_reg = dst_pref; } } @@ -561,13 +539,6 @@ static void apply_abi_aliasing_hints(Func* f) { typedef struct OptAllocator OptAllocator; -static int hard_available(Func* f, u8 cls, Reg r) { - if (cls >= OPT_REG_CLASSES) return 0; - for (u32 i = 0; i < f->opt_hard_reg_count[cls]; ++i) - if (f->opt_hard_regs[cls][i] == r) return 1; - return 0; -} - static const CGPhysRegInfo* phys_info_for(Func* f, u8 cls, Reg r) { if (cls >= OPT_REG_CLASSES) return NULL; for (u32 i = 0; i < f->opt_phys_reg_count[cls]; ++i) @@ -603,7 +574,9 @@ static void spill_slot_add_priority(Func* f, FrameSlot slot, u32 cost) { s->priority = sum < s->priority ? 0xffffffffu : sum; /* saturate on overflow */ } -static u32 hard_loc_bit(u8 cls, Reg r) { return ((u32)cls * 32u) + (u32)r; } +static u32 hard_loc_bit(u8 cls, Reg r) { + return ((u32)cls * OPT_MAX_HARD_REGS) + (u32)r; +} typedef struct OptAllocGroupInfo { PReg root; @@ -612,9 +585,7 @@ typedef struct OptAllocGroupInfo { u32 live_across_call_freq; /* sum over group members (W3-coalesce aware) */ u32 first; u32 last; - i32 tied_hard_reg; u32 forbidden_hard_regs; - u32 allowed_hard_regs; u8 cls; u8 pad[3]; } OptAllocGroupInfo; @@ -636,7 +607,7 @@ typedef struct OptAllocator { u64* used_locs; u32 point_count; u32 loc_words; /* width of one row, in u64 words */ - u32 hard_loc_bits; /* OPT_REG_CLASSES * 32 */ + u32 hard_loc_bits; /* OPT_REG_CLASSES * OPT_MAX_HARD_REGS */ /* Stack slot table (parallel arrays). */ FrameSlot* stack_slots; @@ -741,9 +712,6 @@ static u32 hard_reg_alloc_score(Func* f, const OptAllocator* a, static int alloc_candidate_higher(const OptAllocCandidate* a, const OptAllocCandidate* b) { - int a_tied = a->gi.tied_hard_reg >= 0; - int b_tied = b->gi.tied_hard_reg >= 0; - if (a_tied != b_tied) return a_tied > b_tied; if (a->gi.spill_cost != b->gi.spill_cost) return a->gi.spill_cost > b->gi.spill_cost; if (a->gi.live_length != b->gi.live_length) @@ -811,7 +779,6 @@ static void alloc_group_info(Func* f, const OptAllocator* a, memset(out, 0, sizeof *out); out->root = root; out->first = (u32)~0u; - out->tied_hard_reg = -1; out->cls = f->preg_info[root].cls; for (PReg v = alloc_group_first_member(a, root); v != 0; v = alloc_group_next_member(a, root, v)) { @@ -824,46 +791,27 @@ static void alloc_group_info(Func* f, const OptAllocator* a, if (first < out->first) out->first = first; if (vi->last_pos > out->last) out->last = vi->last_pos; out->forbidden_hard_regs |= vi->forbidden_hard_regs; - if (vi->allowed_hard_regs) { - if (out->allowed_hard_regs) { - u32 both = out->allowed_hard_regs & vi->allowed_hard_regs; - if (!both) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, - "opt asm: conflicting allowed register sets"); - } - out->allowed_hard_regs = both; - } else { - out->allowed_hard_regs = vi->allowed_hard_regs; - } - } - if (vi->tied_hard_reg >= 0) out->tied_hard_reg = vi->tied_hard_reg; } if (out->first == (u32)~0u) out->first = 0; } -static void opt_init_preg_info_from_ranges(Func* f, - const OptLiveRangeSet* ranges) { - OptPRegInfo* old = f->preg_info; +/* Rebuild allocation metadata exclusively from the current HIR and its live + * ranges. OptPRegInfo and OptLoc are products of this pass, never inputs: all + * hard constraints and ABI preferences are derived immediately after this + * reset. Carrying either table across a rerun makes removed clobbers, + * constraints, hints, and spill locations silently authoritative. */ +static void opt_rebuild_preg_info_from_ranges(Func* f, + const OptLiveRangeSet* ranges) { OptPRegInfo* info = arena_zarray(f->arena, OptPRegInfo, opt_reg_count(f) ? opt_reg_count(f) : 1u); + f->preg_locs = NULL; for (PReg v = 0; v < opt_reg_count(f); ++v) { - i32 tied = old ? old[v].tied_hard_reg : -1; - u32 forbidden = old ? old[v].forbidden_hard_regs : 0; - u32 allowed = old ? old[v].allowed_hard_regs : 0; - u32 clobbered = old ? old[v].clobbered_hard_regs : 0; - u32 old_frequency = old ? old[v].frequency : 0; - i8 pref = old ? old[v].preferred_hard_reg : (i8)-1; OptPRegInfo* vi = &info[v]; - vi->tied_hard_reg = tied; - vi->preferred_hard_reg = pref; + vi->preferred_hard_reg = (i8)-1; vi->hard_reg = REG_NONE; vi->spill_slot = FRAME_SLOT_NONE; vi->alloc_kind = OPT_ALLOC_NONE; vi->cls = opt_reg_cls(f, v); - vi->forbidden_hard_regs = forbidden; - vi->allowed_hard_regs = allowed; - vi->clobbered_hard_regs = clobbered; if (!ranges || v == PREG_NONE || v == 0 || ranges->first_range_by_preg[v] == OPT_RANGE_NONE) { continue; @@ -885,7 +833,6 @@ static void opt_init_preg_info_from_ranges(Func* f, vi->live_across_call_freq = ranges->live_across_call_freq_by_preg[v]; vi->spill_cost = ranges->spill_cost_by_preg[v]; vi->frequency = vi->spill_cost; - if (old_frequency > vi->frequency) vi->frequency = old_frequency; } f->preg_info = info; } @@ -1106,7 +1053,7 @@ static void opt_assign_ranges(Func* f, const OptLiveRangeSet* ranges, OptAllocator* a) { memset(a, 0, sizeof *a); a->point_count = ranges->point_count ? ranges->point_count : 1u; - a->hard_loc_bits = OPT_REG_CLASSES * 32u; + a->hard_loc_bits = OPT_REG_CLASSES * OPT_MAX_HARD_REGS; a->loc_words = alloc_loc_words_for_bits(a->hard_loc_bits); a->used_locs = arena_zarray(f->arena, u64, (u64)a->point_count * a->loc_words); @@ -1146,52 +1093,12 @@ static void opt_assign_ranges(Func* f, const OptLiveRangeSet* ranges, u8 cls = gi.cls; alloc_compute_group_conflicts(f, a, ranges, v); - if (gi.tied_hard_reg >= 0) { - Reg fixed = (Reg)gi.tied_hard_reg; - /* Machinize has already validated inline-asm hard-register pins against - * the target's operand-register policy. Some legal pins are ABI registers - * outside the standard allocable set (aa64 x0, rv64 a7), so the allocator - * accepts validated physical registers here and relies on the - * conflict/clobber checks below for placement correctness. */ - if (!hard_available(f, cls, fixed) && !phys_info_for(f, cls, fixed)) { - SrcLoc loc = {0, 0, 0}; - compiler_panic( - f->c, loc, - "opt regalloc: fixed hard reg %u unavailable in class %u", - (unsigned)fixed, (unsigned)cls); - } - if (fixed < 32 && (gi.forbidden_hard_regs & (1u << fixed))) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, - "opt regalloc: fixed hard reg %u is clobbered", - (unsigned)fixed); - } - if (fixed >= 32 || (gi.allowed_hard_regs && - (gi.allowed_hard_regs & (1u << fixed)) == 0)) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, - "opt regalloc: fixed hard reg %u violates asm " - "constraint", - (unsigned)fixed); - } - u32 bit = hard_loc_bit(cls, fixed); - if (fixed >= 32 || alloc_group_conflicts_bit(a, bit)) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, "opt regalloc: conflicting fixed hard reg %u", - (unsigned)fixed); - } - alloc_assign_group_hard(f, a, ranges, v, fixed); - continue; - } - int found = 0; Reg best = REG_NONE; u32 best_score = 0xffffffffu; for (u32 r = 0; r < f->opt_hard_reg_count[cls]; ++r) { Reg hr = f->opt_hard_regs[cls][r]; - if (hr >= 32) continue; - if (gi.allowed_hard_regs && (gi.allowed_hard_regs & (1u << hr)) == 0) - continue; + if (hr >= OPT_MAX_HARD_REGS) continue; if (gi.forbidden_hard_regs & (1u << hr)) continue; u32 bit = hard_loc_bit(cls, hr); if (alloc_group_conflicts_bit(a, bit)) continue; @@ -1202,28 +1109,10 @@ static void opt_assign_ranges(Func* f, const OptLiveRangeSet* ranges, best_score = score; } } - if (gi.allowed_hard_regs) { - for (Reg hr = 0; hr < 32; ++hr) { - const CGPhysRegInfo* pi; - if ((gi.allowed_hard_regs & (1u << hr)) == 0) continue; - if (hard_available(f, cls, hr)) continue; - if (gi.forbidden_hard_regs & (1u << hr)) continue; - pi = phys_info_for(f, cls, hr); - if (!pi || (pi->flags & CG_REG_RESERVED)) continue; - u32 bit = hard_loc_bit(cls, hr); - if (alloc_group_conflicts_bit(a, bit)) continue; - u32 score = hard_reg_alloc_score(f, a, vi, hr); - if (!found || score < best_score) { - found = 1; - best = hr; - best_score = score; - } - } - } - /* Also consider the preferred hard reg if it's outside the standard - * allocable set (e.g. x0 on aa64: reserved as the ABI ret reg, not in - * aa_int_allocable). Used by apply_abi_aliasing_hints to let an IR_CALL - * result PReg or IR_RET value PReg land directly in x0, eliding the + /* Also consider the preferred hard reg if it's outside the standard O1 + * allocation bank (e.g. x0 on aa64: reserved as the ABI return register, + * without NATIVE_REG_ALLOCABLE). Used by apply_abi_aliasing_hints to let + * an IR_CALL result PReg or IR_RET value PReg land directly in x0, eliding the * materialization move emit_call/emit_ret would otherwise emit. Only * short-lived PRegs benefit: a value live across a call cannot survive in * a caller-saved hint reg, and this is the *only* path that can reach an @@ -1233,12 +1122,12 @@ static void opt_assign_ranges(Func* f, const OptLiveRangeSet* ranges, * below would otherwise take the hint reg regardless of score, parking a * cross-call value in x0 where it collides with the next call's result. */ if (vi->preferred_hard_reg >= 0 && + !is_emit_temp(f, cls, (Reg)vi->preferred_hard_reg) && !(gi.live_across_call_freq && is_caller_saved(f, cls, (Reg)vi->preferred_hard_reg))) { Reg hint = (Reg)vi->preferred_hard_reg; int already_tried = 0; - if (hint < 32 && - (!gi.allowed_hard_regs || (gi.allowed_hard_regs & (1u << hint)))) { + if (hint < OPT_MAX_HARD_REGS) { for (u32 r = 0; r < f->opt_hard_reg_count[cls]; ++r) { if (f->opt_hard_regs[cls][r] == hint) { already_tried = 1; @@ -1297,10 +1186,6 @@ static void opt_assign_ranges(Func* f, const OptLiveRangeSet* ranges, } if (found) { alloc_assign_group_hard(f, a, ranges, v, best); - } else if (gi.allowed_hard_regs) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, - "opt regalloc: no hard register satisfies asm constraint"); } else { /* gi.spill_cost is the group's aggregated spill-traffic metric * (alloc_group_info sums each member's frequency/spill_cost). Thread it to @@ -1395,22 +1280,11 @@ static void out_prepend_inst(Func* f, RewriteOut* out, const Inst* in) { *dst = *in; } -static MemAccess spill_mem(Func* f, PReg v) { - MemAccess m; - memset(&m, 0, sizeof m); - m.type = opt_reg_type(f, v); - m.size = type_size_fallback(f, opt_reg_type(f, v)); - m.align = m.size >= 8 ? 8 : m.size; - m.alias.kind = ALIAS_LOCAL; - m.alias.v.local_id = (i32)spill_slot_for(f, v); - return m; -} - static Operand spill_addr(Func* f, PReg v) { Operand o; memset(&o, 0, sizeof o); - o.kind = OPK_LOCAL; - o.cls = RC_INT; + o.kind = OPK_STACK; + o.cls = opt_preg_loc_cls(f, v); o.type = opt_reg_type(f, v); o.v.frame_slot = spill_slot_for(f, v); return o; @@ -1427,38 +1301,29 @@ static Operand hard_operand(Func* f, PReg v) { } static void append_store_preg(Func* f, RewriteList* out, PReg v) { - Inst* st = list_push(f, out, IR_STORE); + Inst* st = list_push(f, out, IR_COPY); st->opnds = arena_array(f->arena, Operand, 2); st->opnds[0] = spill_addr(f, v); st->opnds[1] = hard_operand(f, v); st->nopnds = 2; - st->extra.mem = spill_mem(f, v); } static void append_load_preg(Func* f, RewriteList* out, PReg v) { - Inst* ld = list_push(f, out, IR_LOAD); + Inst* ld = list_push(f, out, IR_COPY); ld->opnds = arena_array(f->arena, Operand, 2); ld->opnds[0] = hard_operand(f, v); ld->opnds[1] = spill_addr(f, v); ld->nopnds = 2; - ld->extra.mem = spill_mem(f, v); -} - -static Reg scratch_for(Func* f, u8 cls, u32* next) { - u32 n = f->opt_scratch_reg_count[cls]; - if (!n) return REG_NONE; - Reg r = f->opt_scratch_regs[cls][*next % n]; - ++*next; - return r; } /* --------------------------------------------------------------------------- * Rematerialization (doc/plan/O1.md W2). * * A spilled value whose single def is *cheaper to recompute than to reload* is - * recomputed at each use instead of reloaded from its slot; the spill store is - * dropped entirely (the original def then writes a dead scratch, removed by - * mir_dce). The v1 set is restricted to input-less producers whose recompute is + * carried as a location recipe at each use instead of occupying a spill home; + * the original producer is omitted from MIR. Native emission realizes the + * recipe inside the consuming instruction's temporary scope. The v1 set is + * restricted to input-less producers whose recompute is * <= reload cost, so the recompute is valid at any use site: * - IR_LOAD_IMM whose immediate materializes in <= 2 instructions * (movz, or movz;movk -- i.e. <= 2 nonzero 16-bit halfwords); @@ -1522,45 +1387,25 @@ static void remat_count_def(Func* f, Inst* in, Operand* op, int is_def, /* Linear pre-pass: record remat_def[v] iff v has exactly one def and that def * is in the v1 set. One walk to count defs + snapshot candidate defs, one PReg * sweep to drop multiply-defined entries. */ -/* Drop `op`'s PReg from the remat set (used to force a spill store + slot reload - * instead of a recompute-into-scratch). */ +/* Drop `op`'s PReg from the remat set. */ static void remat_exclude_operand(RematInfo* ri, const Operand* op) { if (!op || op->kind != OPK_REG) return; PReg v = (PReg)op->v.reg; if (v != PREG_NONE && v != 0 && v < ri->nregs) ri->remat_def[v] = NULL; } -/* Exclude every value used as a call argument from rematerialization. ALL of a - * call's arguments are live simultaneously at the call, but a spilled remat arg - * recomputes into the round-robin opt-scratch registers - * (rewrite_call_arg_operand). With more simultaneously-live spilled args than - * scratch registers — arm32 has only ip/lr yet up to four register args — the - * round-robin aliases them and they clobber each other (the many-int variadic - * O1 miscompile). Keeping these out of the remat set preserves their spill - * store, so the arg rewrite reads each one from its own slot. Other targets have - * enough registers that call args rarely spill, so this is a no-op there. */ -static void remat_exclude_call_args(Func* f, RematInfo* ri) { - for (u32 b = 0; b < f->nblocks; ++b) { - Block* bl = &f->blocks[b]; - for (u32 i = 0; i < bl->ninsts; ++i) { - Inst* in = &bl->insts[i]; - IRCallAux* aux; - if ((IROp)in->op != IR_CALL) continue; - aux = (IRCallAux*)in->extra.aux; - if (!aux) continue; - if (aux->use_plan_replay) { - for (u32 k = 0; k < aux->plan.nargs; ++k) - remat_exclude_operand(ri, &aux->plan.args[k].src); - } else { - for (u32 k = 0; k < aux->desc.nargs; ++k) { - CGABIValue* av = (CGABIValue*)&aux->desc.args[k]; - remat_exclude_operand(ri, &av->storage); - for (u32 p = 0; p < av->nparts; ++p) - remat_exclude_operand(ri, (Operand*)&av->parts[p].op); - } - } - } - } +/* Effective-address components have a deliberately compact reg/frame encoding, + * not the full scalar recipe union. Keep an immediate/frame-address producer + * in its stable spill home when it is used as an embedded base or index. The + * walker marks these transient component operands explicitly. */ +static void remat_exclude_indirect_use(Func* f, Inst* in, Operand* op, + int is_def, void* arg) { + (void)f; + (void)in; + if (is_def || !op || op->kind != OPK_REG || + !(op->flags & OPT_OPERAND_WALK_INDIRECT_PART)) + return; + remat_exclude_operand((RematInfo*)arg, op); } static void opt_mark_remat(Func* f, RematInfo* ri) { @@ -1593,7 +1438,11 @@ static void opt_mark_remat(Func* f, RematInfo* ri) { } for (u32 v = 0; v < nregs; ++v) if (def_count[v] != 1u) ri->remat_def[v] = NULL; - remat_exclude_call_args(f, ri); + for (u32 b = 0; b < f->nblocks; ++b) { + Block* bl = &f->blocks[b]; + for (u32 i = 0; i < bl->ninsts; ++i) + opt_walk_inst_operands(f, &bl->insts[i], remat_exclude_indirect_use, ri); + } } static Inst* remat_def_for(const RematInfo* ri, PReg v) { @@ -1603,43 +1452,41 @@ static Inst* remat_def_for(const RematInfo* ri, PReg v) { } typedef struct RewriteCtx { - RewriteList* before; - RewriteList* after; const RematInfo* remat; - u32 next_scratch[OPT_REG_CLASSES]; } RewriteCtx; -/* Shared "materialize this spilled PReg at this use" helper. Every spilled-use - * rewrite path routes through this. When v rematerializes, clone its recorded - * def into `before` with dst = scratch; otherwise append an IR_LOAD from the - * slot. The caller has already pointed the operand at `scratch`. */ -static void remat_or_reload_use(Func* f, RewriteCtx* c, Operand* op, PReg v, - Reg scratch) { - Inst* def = remat_def_for(c->remat, v); - if (def) { - Inst* re = list_push(f, c->before, (IROp)def->op); - InstId id = re->id; - *re = *def; - re->id = id; - re->opnds = arena_array(f->arena, Operand, def->nopnds ? def->nopnds : 1u); - if (def->nopnds) - memcpy(re->opnds, def->opnds, sizeof(Operand) * def->nopnds); - re->opnds[0] = *op; /* dst already rewritten to scratch */ - re->def = (Val)scratch; - re->type = op->type; - return; +static int remat_operand_for(const RematInfo* ri, PReg v, Operand* out) { + const Inst* def = remat_def_for(ri, v); + if (!def || !out) return 0; + memset(out, 0, sizeof *out); + switch ((IROp)def->op) { + case IR_LOAD_IMM: + out->kind = OPK_IMM; + out->cls = def->nopnds ? def->opnds[0].cls : RC_INT; + out->type = def->type ? def->type + : (def->nopnds ? def->opnds[0].type : 0); + out->v.imm = def->extra.imm; + return 1; + case IR_ADDR_OF: + if (def->nopnds < 2 || def->opnds[1].kind != OPK_LOCAL) return 0; + out->kind = OPK_FRAME_ADDR; + out->cls = RC_INT; + out->type = def->type ? def->type : def->opnds[0].type; + out->v.frame_slot = def->opnds[1].v.frame_slot; + return 1; + default: + return 0; } - Inst* ld = list_push(f, c->before, IR_LOAD); - ld->opnds = arena_array(f->arena, Operand, 2); - ld->opnds[0] = *op; - ld->opnds[1] = spill_addr(f, v); - ld->nopnds = 2; - ld->extra.mem = spill_mem(f, v); } +/* Translate one virtual value to its stable allocated location. A spill is a + * frame operand, not a synthetic reload into an emitter-reserved register. + * Cheap input-less values remain recipes and are materialized inside the + * consumer's native-emission temp scope. */ static void rewrite_one_operand(Func* f, Inst* owner, Operand* op, int is_def, void* arg) { RewriteCtx* c = (RewriteCtx*)arg; + (void)owner; if (op->kind != OPK_REG) return; PReg v = (PReg)op->v.reg; if (v == PREG_NONE || v == 0 || v >= opt_reg_count(f)) return; @@ -1649,107 +1496,12 @@ static void rewrite_one_operand(Func* f, Inst* owner, Operand* op, int is_def, return; } if (alloc_kind != OPT_ALLOC_SPILL) return; - u8 cls = opt_preg_loc_cls(f, v); - Reg scratch = scratch_for(f, cls, &c->next_scratch[cls]); - if (scratch == (Reg)REG_NONE) { - SrcLoc loc = owner ? owner->loc : (SrcLoc){0, 0, 0}; - compiler_panic(f->c, loc, - "opt rewrite: no scratch register for spilled class %u", - (unsigned)cls); - } - op->v.reg = scratch; - if (!is_def) { - remat_or_reload_use(f, c, op, v, scratch); - } else if (remat_def_for(c->remat, v)) { - /* Every use of this PReg rematerializes (the producer is input-less and all - * three spilled-use paths route through remat_or_reload_use), so no frame - * use of the slot remains -- drop the spill store. The original def now - * writes a dead scratch that mir_dce removes. */ - } else { - Inst* st = list_push(f, c->after, IR_STORE); - st->opnds = arena_array(f->arena, Operand, 2); - st->opnds[0] = spill_addr(f, v); - st->opnds[1] = *op; - st->nopnds = 2; - st->extra.mem = spill_mem(f, v); - } -} - -/* Spilled call-arg use. Without remat the slot becomes a direct OPK_LOCAL frame - * operand; with remat we recompute into a scratch so the dropped spill store - * leaves no dangling frame read. */ -static void rewrite_call_arg_operand(Func* f, Operand* op, RewriteCtx* ctx) { - if (!op || op->kind != OPK_REG) return; - PReg v = (PReg)op->v.reg; - if (v == PREG_NONE || v == 0 || v >= opt_reg_count(f)) return; - u8 alloc_kind = opt_preg_alloc_kind(f, v); - if (alloc_kind == OPT_ALLOC_HARD) { - op->v.reg = opt_preg_hard_reg(f, v); - return; - } - if (alloc_kind != OPT_ALLOC_SPILL) return; - if (remat_def_for(ctx->remat, v)) { - u8 cls = opt_preg_loc_cls(f, v); - Reg scratch = scratch_for(f, cls, &ctx->next_scratch[cls]); - if (scratch == (Reg)REG_NONE) - compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt rewrite: no scratch register for spilled class %u", - (unsigned)cls); - op->v.reg = scratch; - remat_or_reload_use(f, ctx, op, v, scratch); + if (!is_def && !(op->flags & OPT_OPERAND_WALK_INDIRECT_PART) && + remat_operand_for(c->remat, v, op)) return; - } *op = spill_addr(f, v); } -static void rewrite_store_value_operand(Func* f, Inst* owner, Operand* op, - RewriteCtx* ctx) { - PReg v; - u8 alloc_kind; - if (!op || op->kind != OPK_REG) return; - v = (PReg)op->v.reg; - if (v == PREG_NONE || v == 0 || v >= opt_reg_count(f)) return; - alloc_kind = opt_preg_alloc_kind(f, v); - if (alloc_kind == OPT_ALLOC_HARD) { - op->v.reg = opt_preg_hard_reg(f, v); - return; - } - if (alloc_kind == OPT_ALLOC_SPILL) { - /* A remat candidate must recompute into a scratch (the slot may no longer - * be written); a non-remat spill may fold into a direct frame operand. */ - if (remat_def_for(ctx->remat, v)) { - rewrite_one_operand(f, owner, op, 0, ctx); - return; - } - *op = spill_addr(f, v); - return; - } - rewrite_one_operand(f, owner, op, 0, ctx); -} - -static void rewrite_call_arg_indirect_base(Func* f, Inst* owner, Operand* op, - RewriteCtx* ctx) { - if (!op || op->kind != OPK_INDIRECT) return; - Operand base = *op; - base.kind = OPK_REG; - base.cls = RC_INT; - base.v.reg = op->v.ind.base; - rewrite_one_operand(f, owner, &base, 0, ctx); - op->v.ind.base = base.v.reg; -} - -static void rewrite_call_arg_value(Func* f, Inst* owner, CGABIValue* v, - RewriteCtx* ctx) { - if (!v) return; - rewrite_call_arg_indirect_base(f, owner, &v->storage, ctx); - rewrite_call_arg_operand(f, &v->storage, ctx); - for (u32 i = 0; i < v->nparts; ++i) { - Operand* op = (Operand*)&v->parts[i].op; - rewrite_call_arg_indirect_base(f, owner, op, ctx); - rewrite_call_arg_operand(f, op, ctx); - } -} - typedef struct RewriteCallSaveCtx { Func* f; RewriteList* out; @@ -1766,7 +1518,7 @@ static void rewrite_call_save_one(PReg v, void* arg) { if (opt_preg_alloc_kind(f, v) != OPT_ALLOC_HARD) return; u8 cls = opt_preg_loc_cls(f, v); Reg hr = opt_preg_hard_reg(f, v); - if (cls >= OPT_REG_CLASSES || hr >= 32u) return; + if (cls >= OPT_REG_CLASSES || hr >= OPT_MAX_HARD_REGS) return; if ((opt_call_clobber_mask_for(f, c->call, cls) & (1u << hr)) == 0) return; if (c->emit_restore) append_load_preg(f, c->out, v); @@ -1810,6 +1562,16 @@ static PReg* rewrite_collect_call_save_pregs(Func* f, u32* count_out) { return pregs; } +static int rewrite_elides_remat_producer(Func* f, const Inst* in, + const RematInfo* remat) { + PReg v; + if (!in || !remat_inst_is_candidate(f, in)) return 0; + v = (PReg)in->def; + if (v == PREG_NONE || v == 0 || v >= opt_reg_count(f)) return 0; + return opt_preg_alloc_kind(f, v) == OPT_ALLOC_SPILL && + remat_def_for(remat, v) != NULL; +} + static void rewrite_func(Func* f, const OptLiveInfo* live_info) { u32 words = live_info ? live_info->words : f->opt_live_words; if (!words) words = bit_words(opt_reg_count(f)); @@ -1829,9 +1591,7 @@ static void rewrite_func(Func* f, const OptLiveInfo* live_info) { Block* bl = &f->blocks[b]; RewriteOut out; out_init(f, &out, bl->ninsts + 16u); - RewriteList before, after, call_saves, call_restores; - memset(&before, 0, sizeof before); - memset(&after, 0, sizeof after); + RewriteList call_saves, call_restores; memset(&call_saves, 0, sizeof call_saves); memset(&call_restores, 0, sizeof call_restores); live_active_words = live_copy_block_out_active(live_info, b, live, words, @@ -1847,53 +1607,20 @@ static void rewrite_func(Func* f, const OptLiveInfo* live_info) { } refs_reset(&refs); opt_walk_inst_operands(f, &in, refs_collect, &refs); - list_reset(&before); - list_reset(&after); list_reset(&call_saves); list_reset(&call_restores); - RewriteCtx ctx; - memset(&ctx, 0, sizeof ctx); - ctx.before = &before; - ctx.after = &after; - ctx.remat = &remat; - if ((IROp)in.op == IR_CALL) { - IRCallAux* aux = (IRCallAux*)in.extra.aux; - if (aux) { - if (aux->use_plan_replay) { - rewrite_one_operand(f, &in, &aux->plan.callee, 0, &ctx); - for (u32 k = 0; k < aux->plan.nargs; ++k) { - rewrite_call_arg_indirect_base(f, &in, &aux->plan.args[k].src, - &ctx); - rewrite_call_arg_operand(f, &aux->plan.args[k].src, &ctx); - } - for (u32 k = 0; k < aux->plan.nrets; ++k) - rewrite_one_operand(f, &in, &aux->plan.rets[k].dst, 1, &ctx); - } else { - rewrite_one_operand(f, &in, &aux->desc.callee, 0, &ctx); - for (u32 k = 0; k < aux->desc.nargs; ++k) - rewrite_call_arg_value(f, &in, (CGABIValue*)&aux->desc.args[k], - &ctx); - opt_walk_abivalue(f, &in, &aux->desc.ret, 1, rewrite_one_operand, - &ctx); - } - } - } else if ((IROp)in.op == IR_STORE && in.nopnds >= 2) { - opt_walk_operand(f, &in, &in.opnds[0], 0, rewrite_one_operand, &ctx); - rewrite_store_value_operand(f, &in, &in.opnds[1], &ctx); - } else { - opt_walk_inst_operands(f, &in, rewrite_one_operand, &ctx); - } + RewriteCtx ctx = {&remat}; + int elide = rewrite_elides_remat_producer(f, &in, &remat); + opt_walk_inst_operands(f, &in, rewrite_one_operand, &ctx); if ((IROp)in.op == IR_CALL) { append_live_call_saves(f, &call_saves, &in, live, live_active_words, &refs, call_save_pregs, ncall_save_pregs, 0); append_live_call_saves(f, &call_restores, &in, live, live_active_words, &refs, call_save_pregs, ncall_save_pregs, 1); } - out_prepend_list_reverse(f, &out, &after); out_prepend_list_reverse(f, &out, &call_restores); - out_prepend_inst(f, &out, &in); + if (!elide) out_prepend_inst(f, &out, &in); out_prepend_list_reverse(f, &out, &call_saves); - out_prepend_list_reverse(f, &out, &before); live_active_words = live_update_refs_before_active(live, live_active_words, words, &refs); f->opt_rewrite_live_words_touched += refs.nuses + refs.ndefs; @@ -1902,33 +1629,41 @@ static void rewrite_func(Func* f, const OptLiveInfo* live_info) { bl->ninsts = out.cap - out.start; bl->cap = bl->ninsts; } + + /* MIR operands are locations, not SSA values. Keep the HIR definition + * metadata on the untouched source graph and make the representation + * boundary explicit here: post-allocation passes derive register effects + * solely from instruction semantics and location operands. */ + for (u32 b = 0; b < f->nblocks; ++b) { + Block* bl = &f->blocks[b]; + for (u32 i = 0; i < bl->ninsts; ++i) { + bl->insts[i].def = VAL_NONE; + bl->insts[i].ndefs = 0; + bl->insts[i].defs = NULL; + } + } f->opt_rewritten = 1; } void opt_lower_to_mir(Func* f, const OptLiveInfo* live_info) { if (!f) return; - Func phys = *f; - phys.blocks = f->blocks; - phys.opt_rewritten = 0; - phys.mir = NULL; + Func phys; + opt_mir_clone_func(&phys, f); rewrite_func(&phys, live_info); MFunc* m = arena_zarray(f->arena, MFunc, 1); m->blocks = phys.blocks; m->nblocks = phys.nblocks; + m->blocks_cap = phys.blocks_cap; m->entry = phys.entry; m->emit_order_n = phys.emit_order_n; - m->emit_order_cap = phys.emit_order_n; - m->emit_order = - arena_array(f->arena, u32, phys.emit_order_n ? phys.emit_order_n : 1u); - if (phys.emit_order_n) - memcpy(m->emit_order, phys.emit_order, sizeof(u32) * phys.emit_order_n); + m->emit_order_cap = phys.emit_order_cap; + m->emit_order = phys.emit_order; f->mir = m; - f->blocks = phys.blocks; - f->nblocks = phys.nblocks; - f->blocks_cap = phys.blocks_cap; + /* frame slots and instruction ids are function-wide emission metadata. The + * HIR graph itself remains untouched and virtual; MIR owns phys.blocks. */ f->frame_slots = phys.frame_slots; f->nframe_slots = phys.nframe_slots; f->frame_slots_cap = phys.frame_slots_cap; @@ -2136,17 +1871,9 @@ static void opt_regalloc_place(Func* f, OptLiveInfo* live_out) { opt_live_blocks(f, &live); OptLiveRangeSet ranges; opt_live_ranges_build(f, &live, &ranges); - opt_init_preg_info_from_ranges(f, &ranges); + opt_rebuild_preg_info_from_ranges(f, &ranges); opt_apply_asm_constraints_from_live(f, &live); apply_abi_aliasing_hints(f); - /* The ABI hint passes clear forbid bits to steer values toward ABI registers - * (ret reg, arg regs). Restore the hard machine-clobber forbids afterward so - * no allocation path (normal, preferred-reg, or two-address group) can place - * a value in a register that an instruction live across it destroys. */ - if (f->preg_info) - for (PReg v = 1; v < opt_reg_count(f); ++v) - f->preg_info[v].forbidden_hard_regs |= - f->preg_info[v].clobbered_hard_regs; /* Linear move coalescing (O1.md W3): populate the union-find that * alloc_coalesce_root/alloc_group_member consult so the allocator merges * copy-related values onto one location. No O(n^2) conflict matrix — see diff --git a/src/opt/pass_machinize.c b/src/opt/pass_machinize.c @@ -15,44 +15,53 @@ static int native_resolve_reg(NativeTarget* target, Slice name, Reg* out, return 0; } +static void asm_prepare_reg_requirement(Func* f, NativeTarget* target, + const AsmConstraint* constraint, + IRAsmRegRequirement* req) { + NativeAsmRegPin pin; + NativeAsmRegPinStatus status; + NativeAsmConstraintInfo info; + memset(req, 0, sizeof *req); + req->fixed_reg = -1; + + status = native_asm_resolve_pin(target, constraint->reg, constraint->str, + &pin); + if (status == NATIVE_ASM_REG_PIN_OK) { + req->present = 1; + req->cls = (u8)pin.cls; + req->fixed_reg = (i32)pin.reg; + return; + } + if (status != NATIVE_ASM_REG_PIN_ABSENT) + compiler_panic(f->c, (SrcLoc){0, 0, 0}, "opt asm: %s", + native_asm_pin_status_message(status)); + + memset(&info, 0, sizeof info); + if (!native_asm_constraint_reg_info(target, constraint->str, &info)) return; + req->present = 1; + req->cls = (u8)info.cls; + req->allowed_mask = info.allowed_mask; + if (info.fixed_reg != REG_NONE) req->fixed_reg = (i32)info.fixed_reg; +} + static void asm_prepare_constraints(Func* f, NativeTarget* target, IRAsmAux* aux) { if (!aux) return; for (u32 c = 0; c < OPT_REG_CLASSES; ++c) aux->clobber_mask[c] = 0; - if (aux->nout && !aux->out_fixed_regs) { - aux->out_fixed_regs = arena_array(f->arena, i32, aux->nout); - aux->out_fixed_cls = arena_zarray(f->arena, u8, aux->nout); - } - if (aux->nin && !aux->in_fixed_regs) { - aux->in_fixed_regs = arena_array(f->arena, i32, aux->nin); - aux->in_fixed_cls = arena_zarray(f->arena, u8, aux->nin); - } - if (aux->nout && !aux->out_allowed_masks) { - aux->out_allowed_masks = arena_zarray(f->arena, u32, aux->nout); - aux->out_allowed_cls = arena_zarray(f->arena, u8, aux->nout); - } - if (aux->nin && !aux->in_allowed_masks) { - aux->in_allowed_masks = arena_zarray(f->arena, u32, aux->nin); - aux->in_allowed_cls = arena_zarray(f->arena, u8, aux->nin); - } - for (u32 i = 0; i < aux->nout; ++i) { - aux->out_fixed_regs[i] = -1; - aux->out_fixed_cls[i] = 0; - aux->out_allowed_masks[i] = 0; - aux->out_allowed_cls[i] = 0; - } - for (u32 i = 0; i < aux->nin; ++i) { - aux->in_fixed_regs[i] = -1; - aux->in_fixed_cls[i] = 0; - aux->in_allowed_masks[i] = 0; - aux->in_allowed_cls[i] = 0; - } + aux->has_memory_constraint = 0; + if (aux->nout && !aux->out_reg_reqs) + aux->out_reg_reqs = + arena_zarray(f->arena, IRAsmRegRequirement, aux->nout); + if (aux->nin && !aux->in_reg_reqs) + aux->in_reg_reqs = + arena_zarray(f->arena, IRAsmRegRequirement, aux->nin); for (u32 i = 0; i < aux->nclob; ++i) { Reg r; RegClass cls; Slice nm = pool_slice(f->c->global, aux->clobbers[i]); if (native_resolve_reg(target, nm, &r, &cls) != 0) continue; - if ((u32)cls < OPT_REG_CLASSES && r < 32) aux->clobber_mask[cls] |= 1u << r; + if ((u32)cls < OPT_REG_CLASSES && r < OPT_MAX_HARD_REGS) + aux->clobber_mask[cls] |= 1u << r; } if (aux->clobber_abi_sets) { u32 int_mask, fp_mask; @@ -64,54 +73,16 @@ static void asm_prepare_constraints(Func* f, NativeTarget* target, aux->clobber_mask[NATIVE_REG_FP] |= fp_mask; } for (u32 i = 0; i < aux->nout; ++i) { - NativeAsmRegPin pin; - NativeAsmRegPinStatus st = native_asm_resolve_pin(target, aux->outs[i].reg, - aux->outs[i].str, &pin); - if (st == NATIVE_ASM_REG_PIN_OK) { - aux->out_fixed_regs[i] = (i32)pin.reg; - aux->out_fixed_cls[i] = (u8)pin.cls; - continue; - } - if (st != NATIVE_ASM_REG_PIN_ABSENT) { - compiler_panic(f->c, (SrcLoc){0, 0, 0}, "opt asm: %s", - native_asm_pin_status_message(st)); - } - NativeAsmConstraintInfo info; - if (native_asm_constraint_reg_info(target, aux->outs[i].str, &info)) { - if (info.allowed_mask) { - aux->out_allowed_masks[i] = info.allowed_mask; - aux->out_allowed_cls[i] = (u8)info.cls; - } - if (info.fixed_reg != REG_NONE) { - aux->out_fixed_regs[i] = (i32)info.fixed_reg; - aux->out_fixed_cls[i] = (u8)info.cls; - } - } + asm_prepare_reg_requirement(f, target, &aux->outs[i], + &aux->out_reg_reqs[i]); + if (native_asm_constraint_body(aux->outs[i].str)[0] == 'm') + aux->has_memory_constraint = 1; } for (u32 i = 0; i < aux->nin; ++i) { - NativeAsmRegPin pin; - NativeAsmRegPinStatus st = - native_asm_resolve_pin(target, aux->ins[i].reg, aux->ins[i].str, &pin); - if (st == NATIVE_ASM_REG_PIN_OK) { - aux->in_fixed_regs[i] = (i32)pin.reg; - aux->in_fixed_cls[i] = (u8)pin.cls; - continue; - } - if (st != NATIVE_ASM_REG_PIN_ABSENT) { - compiler_panic(f->c, (SrcLoc){0, 0, 0}, "opt asm: %s", - native_asm_pin_status_message(st)); - } - NativeAsmConstraintInfo info; - if (native_asm_constraint_reg_info(target, aux->ins[i].str, &info)) { - if (info.allowed_mask) { - aux->in_allowed_masks[i] = info.allowed_mask; - aux->in_allowed_cls[i] = (u8)info.cls; - } - if (info.fixed_reg != REG_NONE) { - aux->in_fixed_regs[i] = (i32)info.fixed_reg; - aux->in_fixed_cls[i] = (u8)info.cls; - } - } + asm_prepare_reg_requirement(f, target, &aux->ins[i], + &aux->in_reg_reqs[i]); + if (native_asm_constraint_body(aux->ins[i].str)[0] == 'm') + aux->has_memory_constraint = 1; } } @@ -121,7 +92,8 @@ static void machinize_reset(Func* f, NativeTarget* target) { for (u32 c = 0; c < OPT_REG_CLASSES; ++c) { f->opt_hard_reg_count[c] = 0; f->opt_phys_reg_count[c] = 0; - f->opt_scratch_reg_count[c] = 0; + f->emit_temp_reg_count[c] = 0; + f->asm_temp_mask[c] = 0; f->opt_caller_saved[c] = 0; f->opt_callee_saved[c] = 0; f->opt_reserved_regs[c] = 0; @@ -141,19 +113,124 @@ 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; - if (cls >= OPT_REG_CLASSES) return; + u32 known_mask = 0; + u32 emit_mask; + u32 asm_mask; + 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]); + } f->opt_caller_saved[cls] = native_target_caller_saved_mask(target, (NativeAllocClass)cls); f->opt_callee_saved[cls] = native_target_callee_saved_mask(target, (NativeAllocClass)cls); - f->opt_reserved_regs[cls] = ci->reserved_mask; + if (emit_mask & ~f->opt_caller_saved[cls]) + compiler_panic(f->c, (SrcLoc){0, 0, 0}, + "opt_machinize: class %u emit temp bank is not entirely " + "caller-saved", + (unsigned)cls); + f->opt_reserved_regs[cls] = ci->reserved_mask | 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; - for (u32 i = 0; - i < ci->nphys && f->opt_phys_reg_count[cls] < OPT_MAX_HARD_REGS; ++i) { + 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]++]; memset(dst, 0, sizeof *dst); @@ -165,28 +242,37 @@ static void collect_class(Func* f, NativeTarget* target, f->opt_hard_reg_count[cls] < OPT_MAX_HARD_REGS) f->opt_hard_regs[cls][f->opt_hard_reg_count[cls]++] = src->reg; } - /* The optimizer's native-emit scratch: opt_scratch overrides scratch when a - * backend needs a different (usually larger) set at -O1 than the -O0 NDT. */ - const Reg* scr = ci->opt_nscratch ? ci->opt_scratch : ci->scratch; - u32 nscr = ci->opt_nscratch ? ci->opt_nscratch : ci->nscratch; - for (u32 i = 0; i < nscr && i < OPT_MAX_SCRATCH_REGS; ++i) - f->opt_scratch_regs[cls][f->opt_scratch_reg_count[cls]++] = scr[i]; + for (u32 i = 0; i < ci->nemit_temps && i < OPT_MAX_EMIT_TEMPS; ++i) + f->emit_temp_regs[cls][f->emit_temp_reg_count[cls]++] = + ci->emit_temps[i]; } static void machinize_collect_regs(Func* f, NativeTarget* target) { + u32 seen = 0; if (!target || !target->regs) return; - for (u32 i = 0; i < target->regs->nclasses; ++i) + 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"); + for (u32 i = 0; i < target->regs->nclasses; ++i) { + u32 cls = target->regs->classes[i].cls; + if (cls >= OPT_REG_CLASSES || (seen & (1u << cls))) + compiler_panic(f->c, (SrcLoc){0, 0, 0}, + "opt_machinize: duplicate/invalid register class %u", + (unsigned)cls); + seen |= 1u << cls; collect_class(f, target, &target->regs->classes[i]); + } } static void machinize_check_overlap(Func* f) { for (u32 c = 0; c < OPT_REG_CLASSES; ++c) { for (u32 i = 0; i < f->opt_hard_reg_count[c]; ++i) { Reg hr = f->opt_hard_regs[c][i]; - for (u32 s = 0; s < f->opt_scratch_reg_count[c]; ++s) { - if (f->opt_scratch_regs[c][s] == hr) { + for (u32 s = 0; s < f->emit_temp_reg_count[c]; ++s) { + if (f->emit_temp_regs[c][s] == hr) { compiler_panic(f->c, (SrcLoc){0, 0, 0}, - "opt_machinize: hard reg %u overlaps scratch reg " + "opt_machinize: hard reg %u overlaps emit temp " "in class %u", (unsigned)hr, (unsigned)c); } @@ -195,13 +281,45 @@ static void machinize_check_overlap(Func* f) { } } -/* Record, per instruction, the physical registers the target's encoding - * clobbers as a side effect (x86 idiv → rax/rdx, variable shift → cl, atomics, - * va_arg), so the allocator keeps values live across them out of those - * registers. The target reports this through machine_op_clobbers; a NULL hook - * means no instruction has fixed-register clobbers and the side table stays - * empty. */ -static void machinize_inst_clobbers(Func* f, NativeTarget* target) { +static u32 machinize_known_phys_mask(const Func* f, u32 cls) { + u32 mask = 0; + if (!f || cls >= OPT_REG_CLASSES) return 0; + for (u32 i = 0; i < f->opt_phys_reg_count[cls]; ++i) { + Reg reg = f->opt_phys_regs[cls][i].reg; + if (reg < OPT_MAX_HARD_REGS) mask |= 1u << reg; + } + return mask; +} + +static void machinize_validate_clobber_masks(Func* f, const Inst* in, + const u32* masks) { + for (u32 cls = 0; cls < OPT_REG_CLASSES; ++cls) { + u32 unknown = masks[cls] & ~machinize_known_phys_mask(f, cls); + if (unknown) + compiler_panic( + f->c, in->loc, + "opt_machinize: class %u machine clobber mask names unknown " + "physical register %#x", + (unsigned)cls, (unsigned)unknown); + } +} + +/* Record, per instruction, every optimizer-visible register the target's + * encoding clobbers as a side effect (division, variable shifts, bitfield RMW, + * atomics, intrinsics, and format-dependent TLS among the current cases). The + * allocator keeps live-across MIR values out of those registers, and native + * emission uses the same exhaustive effect to invalidate clean frame-cache + * entries in cache-capable emit temps. Pure backend-private registers stay out + * of the table. A NULL hook promises no such clobbers, so the derived side + * table stays empty. */ +void opt_refresh_machine_clobbers(Func* f, NativeTarget* target) { + if (!f || !target) return; + /* Target effects are derived from the current instruction shapes, not + * persistent IR state. Replace the table on every refresh so a shape-changing + * pass, target change, or instruction-id growth cannot retain stale effects. + * The arena owns the superseded allocation. */ + f->inst_clobbers = NULL; + f->inst_clobbers_cap = 0; if (!target->machine_op_clobbers || !f->next_inst_id) return; for (u32 b = 0; b < f->nblocks; ++b) { Block* bl = &f->blocks[b]; @@ -209,6 +327,7 @@ static void machinize_inst_clobbers(Func* f, NativeTarget* target) { Inst* in = &bl->insts[i]; NativeMachineOp mop; u32 mask[NATIVE_CALL_PLAN_CLASSES]; + int has_clobbers; memset(&mop, 0, sizeof mop); switch ((IROp)in->op) { case IR_BINOP: @@ -217,6 +336,12 @@ static void machinize_inst_clobbers(Func* f, NativeTarget* target) { mop.second_is_reg = (u8)(in->nopnds > 2u && in->opnds[2].kind == OPK_REG); break; + case IR_BITFIELD_LOAD: + mop.kind = NATIVE_MOP_BITFIELD_LOAD; + break; + case IR_BITFIELD_STORE: + mop.kind = NATIVE_MOP_BITFIELD_STORE; + break; case IR_VA_START: mop.kind = NATIVE_MOP_VA_START; break; @@ -245,7 +370,9 @@ static void machinize_inst_clobbers(Func* f, NativeTarget* target) { continue; } mask[0] = mask[1] = mask[2] = 0; - if (!target->machine_op_clobbers(target, &mop, mask)) continue; + has_clobbers = target->machine_op_clobbers(target, &mop, mask); + machinize_validate_clobber_masks(f, in, mask); + if (!has_clobbers) continue; if (in->id == INST_ID_NONE) continue; if (!f->inst_clobbers) { f->inst_clobbers_cap = f->next_inst_id; @@ -264,5 +391,5 @@ void opt_machinize_native(Func* f, NativeTarget* target) { machinize_prepare_insts(f, target); machinize_collect_regs(f, target); machinize_check_overlap(f); - machinize_inst_clobbers(f, target); + opt_refresh_machine_clobbers(f, target); } diff --git a/src/opt/pass_mir.c b/src/opt/pass_mir.c @@ -1,102 +1,575 @@ #include "opt/opt_internal.h" -static int mir_view(Func* f, Func* out) { +int opt_mir_view(Func* f, Func* out) { if (!f || !f->mir) return 0; *out = *f; out->blocks = f->mir->blocks; out->nblocks = f->mir->nblocks; + out->blocks_cap = f->mir->blocks_cap; out->entry = f->mir->entry; out->emit_order = f->mir->emit_order; out->emit_order_n = f->mir->emit_order_n; out->emit_order_cap = f->mir->emit_order_cap; out->opt_rewritten = 1; + /* HIR def-use records contain direct pointers into HIR operands. MIR passes + * must rebuild any analysis they need over this physical graph. */ + out->opt_uses = NULL; + out->opt_nuses = 0; + out->opt_uses_cap = 0; + out->opt_first_use_by_val = NULL; + out->opt_first_use_by_val_cap = 0; + out->opt_valid_analyses = 0; + /* Scope lookup is a recording/semantic cache of direct Inst pointers into + * HIR. Structured scopes have already been lowered before MIR exists, and a + * physical pass must never inherit pointers back into the semantic graph. */ + out->scope_aux_inst = NULL; + out->nscopes = 0; + out->scopes_cap = 0; out->mir = NULL; return 1; } -static void mir_commit(Func* f, const Func* view) { +void opt_mir_commit(Func* f, const Func* view) { if (!f || !f->mir || !view) return; f->mir->blocks = view->blocks; f->mir->nblocks = view->nblocks; + f->mir->blocks_cap = view->blocks_cap; f->mir->entry = view->entry; f->mir->emit_order = view->emit_order; f->mir->emit_order_n = view->emit_order_n; f->mir->emit_order_cap = view->emit_order_cap; + /* Instruction ids are function-wide side-table keys, but a physical pass + * allocates them through its independently mutable Func view. Commit that + * namespace with the graph so later MIR analyses can size their tables for + * every instruction the pass inserted. */ + f->next_inst_id = view->next_inst_id; } -static void mir_verify_reg(Func* f, const char* stage, Reg r, u8 cls, - const char* what) { - if (r == (Reg)REG_NONE || cls >= OPT_REG_CLASSES || r >= OPT_MAX_HARD_REGS) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, "opt MIR verify %s: bad %s cls%u reg%u", - stage ? stage : "?", what, (unsigned)cls, (unsigned)r); +typedef struct MirVerifyCtx { + const char* stage; + /* opt_walk_operand presents an indirect owner immediately followed by + * transient scalar views of its base and (when present) index. Track that + * sequence so a stored OPT_OPERAND_WALK_INDIRECT_PART flag cannot masquerade + * as a legitimate walker annotation. */ + const Operand* indirect; + u8 indirect_part; /* 1 = base, 2 = index */ +} MirVerifyCtx; + +static void mir_fail(Func* f, const Inst* in, const char* stage, + const char* msg, u32 a, u32 b) { + compiler_panic(f->c, in ? in->loc : (SrcLoc){0, 0, 0}, + "opt MIR verify %s: %s [op %u, inst %u] (%u, %u)", + stage ? stage : "?", msg, in ? (unsigned)in->op : 0u, + in ? (unsigned)in->id : 0u, (unsigned)a, (unsigned)b); +} + +static int mir_pow2(u32 n) { return n && !(n & (n - 1u)); } + +static int mir_type_known(Func* f, KitCgTypeId type) { + return type != KIT_CG_TYPE_NONE && cg_type_get(f->c, type) != NULL; +} + +static int mir_type_valid(Func* f, KitCgTypeId type) { + return mir_type_known(f, type) && + kit_cg_type_is_sized((KitCompiler*)f->c, type) && + cg_type_size(f->c, type) != 0 && cg_type_align(f->c, type) != 0; +} + +static int mir_type_scalar(Func* f, KitCgTypeId type) { + const CgType* ty; + if (!mir_type_valid(f, type)) return 0; + ty = cg_type_get(f->c, type); + switch (ty->kind) { + case KIT_CG_TYPE_BOOL: + case KIT_CG_TYPE_INT: + case KIT_CG_TYPE_FLOAT: + case KIT_CG_TYPE_PTR: + case KIT_CG_TYPE_ENUM: + case KIT_CG_TYPE_VARARG_STATE: + return 1; + default: + return 0; } } +static int mir_type_address_component(Func* f, KitCgTypeId type) { + return mir_type_scalar(f, type) && + opt_value_reg_class(f->c, type) == RC_INT && + cg_type_size(f->c, type) == f->c->target.ptr_size; +} + +static int mir_reg_known(Func* f, Reg r, u8 cls) { + if (cls >= OPT_REG_CLASSES || r == (Reg)REG_NONE || + r >= OPT_MAX_HARD_REGS) + return 0; + for (u32 i = 0; i < f->opt_phys_reg_count[cls]; ++i) { + const CGPhysRegInfo* phys = &f->opt_phys_regs[cls][i]; + if (phys->reg == r && phys->cls == cls) return 1; + } + return 0; +} + +static u32 mir_known_reg_mask(Func* f, u8 cls) { + u32 mask = 0; + if (!f || cls >= OPT_REG_CLASSES) return 0; + for (u32 i = 0; i < f->opt_phys_reg_count[cls]; ++i) { + Reg r = f->opt_phys_regs[cls][i].reg; + if (r < OPT_MAX_HARD_REGS) mask |= 1u << r; + } + return mask; +} + +static int mir_reg_is_emit_temp(Func* f, Reg r, u8 cls) { + if (cls >= OPT_REG_CLASSES) return 0; + for (u32 i = 0; i < f->emit_temp_reg_count[cls]; ++i) + if (f->emit_temp_regs[cls][i] == r) return 1; + return 0; +} + +static int mir_reg_is_asm_temp(Func* f, Reg r, u8 cls) { + return cls < OPT_REG_CLASSES && r < OPT_MAX_HARD_REGS && + (f->asm_temp_mask[cls] & (1u << r)) != 0; +} + +static void mir_verify_reg(Func* f, const Inst* in, const char* stage, Reg r, + u8 cls, const char* what) { + if (!mir_reg_known(f, r, cls)) + mir_fail(f, in, stage, what, cls, (u32)r); + if (mir_reg_is_emit_temp(f, r, cls)) + mir_fail(f, in, stage, "emitter-owned temp escaped into MIR", cls, + (u32)r); + if (mir_reg_is_asm_temp(f, r, cls)) + mir_fail(f, in, stage, "asm-internal temp escaped into MIR", cls, + (u32)r); +} + +static IRFrameSlot* mir_frame_slot(Func* f, const Inst* in, const char* stage, + FrameSlot id, const char* what) { + if (id == FRAME_SLOT_NONE || id > f->nframe_slots) + mir_fail(f, in, stage, what, id, f->nframe_slots); + return &f->frame_slots[id - 1u]; +} + +static void mir_verify_slot_contains(Func* f, const Inst* in, + const char* stage, + const IRFrameSlot* slot, + KitCgTypeId type, const char* what) { + u64 size; + if (!mir_type_valid(f, type)) + mir_fail(f, in, stage, what, type, slot ? slot->id : 0u); + size = cg_type_size(f->c, type); + if (!slot || size > slot->size) + mir_fail(f, in, stage, what, slot ? slot->id : 0u, type); +} + +static void mir_verify_slot_fits(Func* f, const Inst* in, const char* stage, + const IRFrameSlot* slot, KitCgTypeId type, + const char* what) { + mir_verify_slot_contains(f, in, stage, slot, type, what); + if (cg_type_align(f->c, type) > slot->align) + mir_fail(f, in, stage, what, slot->id, type); +} + +static void mir_verify_frame_table(Func* f, const char* stage) { + const u16 allowed_flags = + (u16)(FSF_ADDR_TAKEN | FSF_MEMORY_REQUIRED | FSF_VOLATILE); + for (u32 i = 0; i < f->nframe_slots; ++i) { + IRFrameSlot* slot = &f->frame_slots[i]; + if (slot->id != i + 1u) + mir_fail(f, NULL, stage, "non-canonical frame slot id", slot->id, + i + 1u); + if (slot->kind != FS_LOCAL && slot->kind != FS_PARAM && + slot->kind != FS_SPILL) + mir_fail(f, NULL, stage, "invalid optimizer frame slot kind", slot->id, + slot->kind); + if (!mir_type_valid(f, slot->type)) + mir_fail(f, NULL, stage, "invalid frame slot type", slot->id, + slot->type); + if (!slot->size || !mir_pow2(slot->align)) + mir_fail(f, NULL, stage, "invalid frame slot size/alignment", slot->size, + slot->align); + if (cg_type_size(f->c, slot->type) > slot->size || + cg_type_align(f->c, slot->type) > slot->align) + mir_fail(f, NULL, stage, "frame slot does not fit descriptor type", + slot->id, slot->type); + if (slot->flags & ~allowed_flags) + mir_fail(f, NULL, stage, "invalid frame slot flags", slot->id, + slot->flags); + if (slot->kind == FS_SPILL) { + if (slot->flags) + mir_fail(f, NULL, stage, "spill slot carries semantic flags", slot->id, + slot->flags); + } else if (slot->priority) { + mir_fail(f, NULL, stage, "semantic frame slot carries spill priority", + slot->id, slot->priority); + } + } +} + +static void mir_verify_asm_requirement(Func* f, const Inst* in, + const char* stage, + const IRAsmRegRequirement* req, + const Operand* op) { + u32 known; + if (!req->present) { + if (req->allowed_mask || req->fixed_reg != -1 || req->cls) + mir_fail(f, in, stage, "non-canonical absent asm requirement", + req->allowed_mask, (u32)req->fixed_reg); + return; + } + if (req->cls >= OPT_REG_CLASSES || !op || !mir_type_scalar(f, op->type) || + opt_value_reg_class(f->c, op->type) != req->cls) + mir_fail(f, in, stage, "invalid asm requirement class/type", req->cls, + op ? op->type : 0u); + known = mir_known_reg_mask(f, req->cls); + if (req->allowed_mask & ~known) + mir_fail(f, in, stage, "asm allowed mask names unknown register", + req->cls, req->allowed_mask & ~known); + if (req->fixed_reg < -1 || req->fixed_reg >= (i32)OPT_MAX_HARD_REGS || + (req->fixed_reg >= 0 && + !(known & (1u << (u32)req->fixed_reg)))) + mir_fail(f, in, stage, "asm fixed requirement names unknown register", + req->cls, (u32)req->fixed_reg); + if (req->fixed_reg >= 0 && req->allowed_mask && + !(req->allowed_mask & (1u << (u32)req->fixed_reg))) + mir_fail(f, in, stage, "asm fixed register violates allowed mask", + req->cls, (u32)req->fixed_reg); +} + +static void mir_verify_asm_aux(Func* f, const Inst* in, const char* stage) { + const IRAsmAux* aux = (const IRAsmAux*)in->extra.aux; + if (!aux) + mir_fail(f, in, stage, "asm instruction has no descriptor", 0, 0); + if ((aux->nout != 0u) != (aux->outs && aux->out_ops && aux->out_reg_reqs) || + (aux->nin != 0u) != (aux->ins && aux->in_ops && aux->in_reg_reqs) || + (aux->nclob != 0u) != (aux->clobbers != NULL) || + aux->has_memory_constraint > 1u) + mir_fail(f, in, stage, "inconsistent asm descriptor arrays", aux->nout, + aux->nin); + for (u32 cls = 0; cls < OPT_REG_CLASSES; ++cls) + if (aux->clobber_mask[cls] & ~mir_known_reg_mask(f, (u8)cls)) + mir_fail(f, in, stage, "asm clobber mask names unknown register", cls, + aux->clobber_mask[cls]); + for (u32 i = 0; i < aux->nout; ++i) + mir_verify_asm_requirement(f, in, stage, &aux->out_reg_reqs[i], + &aux->out_ops[i]); + for (u32 i = 0; i < aux->nin; ++i) + mir_verify_asm_requirement(f, in, stage, &aux->in_reg_reqs[i], + &aux->in_ops[i]); +} + +static void mir_verify_scalar_location(Func* f, Inst* in, Operand* op, + int is_def, MirVerifyCtx* ctx) { + IRFrameSlot* slot; + if (op->flags) + mir_fail(f, in, ctx->stage, "stored operand has transient flags", op->kind, + op->flags); + if (op->shift) { + if (op->kind != OPK_REG || is_def || op->cls != RC_INT || + op->shift > 4u || (IROp)in->op != IR_BINOP || in->nopnds != 3u || + op != &in->opnds[2]) + mir_fail(f, in, ctx->stage, "invalid shifted-register rider", op->kind, + op->shift); + } + switch ((OptOperandKind)op->kind) { + case OPK_IMM: + /* A zero operand is the canonical absent ABI value (notably a void call + * result). Every present immediate must still carry a real scalar type. */ + if (op->type == KIT_CG_TYPE_NONE) { + if (op->cls || op->v.imm) + mir_fail(f, in, ctx->stage, "malformed absent immediate", op->cls, + (u32)op->v.imm); + } else if (!mir_type_scalar(f, op->type) || op->cls != RC_INT) { + mir_fail(f, in, ctx->stage, "invalid immediate type/class", op->type, + op->cls); + } + return; + case OPK_GLOBAL: + /* A symbol-address recipe may retain an incomplete array or function + * referent type. Native emission needs the symbol identity and address + * class, not sized storage; size/alignment checks belong to operations + * that actually access the referent. */ + if (!mir_type_known(f, op->type) || op->cls != RC_INT) + mir_fail(f, in, ctx->stage, "invalid global type/class", op->type, + op->cls); + return; + case OPK_LOCAL: + if (op->cls != RC_INT) + mir_fail(f, in, ctx->stage, "frame local has non-address class", + op->cls, op->v.frame_slot); + slot = mir_frame_slot(f, in, ctx->stage, op->v.frame_slot, + "invalid local frame slot"); + if (slot->kind != FS_LOCAL && slot->kind != FS_PARAM) + mir_fail(f, in, ctx->stage, "local operand names allocator spill", + op->v.frame_slot, slot->kind); + /* A semantic local may be a typed subobject view into packed storage. + * Its access alignment lives in MemAccess and may be lower than the + * viewed type's natural alignment; only containment is required here. + * Allocator spills below retain the stronger alignment invariant. */ + mir_verify_slot_contains(f, in, ctx->stage, slot, op->type, + "local operand type does not fit frame slot"); + return; + case OPK_STACK: + if (!mir_type_scalar(f, op->type) || op->cls >= OPT_REG_CLASSES || + opt_value_reg_class(f->c, op->type) != op->cls) + mir_fail(f, in, ctx->stage, "invalid spill value type/class", op->type, + op->cls); + slot = mir_frame_slot(f, in, ctx->stage, op->v.frame_slot, + "invalid spill frame slot"); + if (slot->kind != FS_SPILL) + mir_fail(f, in, ctx->stage, "spill value names semantic frame slot", + op->v.frame_slot, slot->kind); + mir_verify_slot_fits(f, in, ctx->stage, slot, op->type, + "spill value type does not fit frame slot"); + return; + case OPK_FRAME_ADDR: + if (is_def || op->cls != RC_INT || !cg_type_is_ptr(f->c, op->type) || + !mir_type_address_component(f, op->type)) + mir_fail(f, in, ctx->stage, "invalid frame-address recipe", op->type, + op->cls); + slot = mir_frame_slot(f, in, ctx->stage, op->v.frame_slot, + "invalid frame-address slot"); + if (slot->kind != FS_LOCAL && slot->kind != FS_PARAM) + mir_fail(f, in, ctx->stage, "frame-address recipe names spill slot", + op->v.frame_slot, slot->kind); + return; + case OPK_REG: + if (!mir_type_scalar(f, op->type) || op->cls >= OPT_REG_CLASSES || + opt_value_reg_class(f->c, op->type) != op->cls) + mir_fail(f, in, ctx->stage, "invalid register value type/class", + op->type, op->cls); + mir_verify_reg(f, in, ctx->stage, op->v.reg, op->cls, + "unknown physical register operand"); + return; + case OPK_INDIRECT: + mir_fail(f, in, ctx->stage, "nested indirect operand", op->kind, 0); + return; + default: + mir_fail(f, in, ctx->stage, "unknown operand kind", op->kind, 0); + return; + } +} + +static void mir_verify_indirect_component(Func* f, Inst* in, Operand* part, + int is_def, MirVerifyCtx* ctx) { + const Operand* owner = ctx->indirect; + u32 value; + u8 kind; + KitCgTypeId type; + u8 expect_kind; + if (!owner || (ctx->indirect_part != 1u && ctx->indirect_part != 2u)) + mir_fail(f, in, ctx->stage, "unexpected indirect component", part->kind, + part->flags); + if (part->flags != OPT_OPERAND_WALK_INDIRECT_PART || is_def || part->shift || + part->cls != RC_INT) + mir_fail(f, in, ctx->stage, "malformed indirect component view", + part->flags, part->kind); + if (ctx->indirect_part == 1u) { + value = owner->v.ind.base; + kind = owner->v.ind.base_kind; + type = owner->v.ind.base_type; + } else { + value = owner->v.ind.index; + kind = owner->v.ind.index_kind; + type = owner->v.ind.index_type; + } + expect_kind = kind == OPT_INDIRECT_REG + ? OPK_REG + : (kind == OPT_INDIRECT_FRAME ? OPK_STACK + : OPK_FRAME_ADDR); + if (part->kind != expect_kind || part->type != type || + (part->kind == OPK_REG ? (u32)part->v.reg + : (u32)part->v.frame_slot) != value) + mir_fail(f, in, ctx->stage, "indirect component view disagrees with owner", + ctx->indirect_part, part->kind); + + if (ctx->indirect_part == 1u && + !(owner->v.ind.index_kind == OPT_INDIRECT_REG && + owner->v.ind.index == (Reg)REG_NONE)) { + ctx->indirect_part = 2u; + } else { + ctx->indirect = NULL; + ctx->indirect_part = 0; + } +} + +static void mir_verify_indirect(Func* f, Inst* in, Operand* op, + MirVerifyCtx* ctx) { + IRFrameSlot* slot; + int has_index; + int address_only = (IROp)in->op == IR_ADDR_OF && in->nopnds >= 2u && + op == &in->opnds[1]; + if (op->flags || op->shift || op->cls != RC_INT) + mir_fail(f, in, ctx->stage, "invalid indirect owner shape", op->flags, + op->shift); + /* IR_ADDR_OF computes the identity of its lvalue and may therefore retain an + * incomplete flexible-array referent. Instructions that dereference the + * recipe still require sized storage. */ + if (!(address_only ? mir_type_known(f, op->type) + : mir_type_valid(f, op->type))) + mir_fail(f, in, ctx->stage, "invalid indirect owner type", op->type, + (u32)address_only); + if (op->v.ind.base_kind > OPT_INDIRECT_FRAME_ADDR) + mir_fail(f, in, ctx->stage, "invalid indirect base location kind", + op->v.ind.base_kind, op->v.ind.base); + if (!mir_type_address_component(f, op->v.ind.base_type)) + mir_fail(f, in, ctx->stage, "invalid indirect base type", + op->v.ind.base_type, op->v.ind.base_kind); + switch ((OptIndirectLocKind)op->v.ind.base_kind) { + case OPT_INDIRECT_REG: + mir_verify_reg(f, in, ctx->stage, op->v.ind.base, RC_INT, + "unknown indirect base register"); + break; + case OPT_INDIRECT_FRAME: + slot = mir_frame_slot(f, in, ctx->stage, (FrameSlot)op->v.ind.base, + "invalid indirect base spill slot"); + if (slot->kind != FS_SPILL) + mir_fail(f, in, ctx->stage, "indirect base value names semantic slot", + op->v.ind.base, slot->kind); + mir_verify_slot_fits(f, in, ctx->stage, slot, op->v.ind.base_type, + "indirect base type does not fit spill slot"); + break; + case OPT_INDIRECT_FRAME_ADDR: + slot = mir_frame_slot(f, in, ctx->stage, (FrameSlot)op->v.ind.base, + "invalid indirect frame-address base slot"); + if (slot->kind != FS_LOCAL && slot->kind != FS_PARAM) + mir_fail(f, in, ctx->stage, + "indirect frame-address base names spill slot", + op->v.ind.base, slot->kind); + break; + } + + if (op->v.ind.index_kind > OPT_INDIRECT_FRAME_ADDR) + mir_fail(f, in, ctx->stage, "invalid indirect index location kind", + op->v.ind.index_kind, op->v.ind.index); + has_index = !(op->v.ind.index_kind == OPT_INDIRECT_REG && + op->v.ind.index == (Reg)REG_NONE); + if (!has_index) { + if (op->v.ind.index_type != KIT_CG_TYPE_NONE || op->v.ind.log2_scale || + op->v.ind.index_ext != OPT_IDX_EXT_NONE) + mir_fail(f, in, ctx->stage, "non-canonical absent indirect index", + op->v.ind.index_type, op->v.ind.log2_scale); + } else { + if (!opt_indirect_index_type_valid(f, op->v.ind.index_type)) + mir_fail(f, in, ctx->stage, "invalid indirect index type", + op->v.ind.index_type, op->v.ind.index_kind); + switch ((OptIndirectLocKind)op->v.ind.index_kind) { + case OPT_INDIRECT_REG: + mir_verify_reg(f, in, ctx->stage, op->v.ind.index, RC_INT, + "unknown indirect index register"); + break; + case OPT_INDIRECT_FRAME: + slot = mir_frame_slot(f, in, ctx->stage, (FrameSlot)op->v.ind.index, + "invalid indirect index spill slot"); + if (slot->kind != FS_SPILL) + mir_fail(f, in, ctx->stage, + "indirect index value names semantic slot", + op->v.ind.index, slot->kind); + mir_verify_slot_fits(f, in, ctx->stage, slot, op->v.ind.index_type, + "indirect index type does not fit spill slot"); + break; + case OPT_INDIRECT_FRAME_ADDR: + mir_fail(f, in, ctx->stage, + "frame address cannot be an indirect index", + op->v.ind.index, op->v.ind.index_type); + break; + } + } + if (op->v.ind.log2_scale > 3u) + mir_fail(f, in, ctx->stage, "invalid indirect scale", + op->v.ind.log2_scale, op->v.ind.index); + if (op->v.ind.index_ext > OPT_IDX_EXT_UXTW || + (op->v.ind.index_ext != OPT_IDX_EXT_NONE && + (!has_index || op->v.ind.index_kind == OPT_INDIRECT_FRAME_ADDR || + cg_type_size(f->c, op->v.ind.index_type) != 4u))) + mir_fail(f, in, ctx->stage, "invalid indirect index extension", + op->v.ind.index_ext, op->v.ind.index_type); + + ctx->indirect = op; + ctx->indirect_part = 1u; +} + static void mir_verify_operand(Func* f, Inst* in, Operand* op, int is_def, void* arg) { - (void)in; - (void)is_def; - const char* stage = (const char*)arg; + MirVerifyCtx* ctx = (MirVerifyCtx*)arg; if (!op) return; - if (op->kind == OPK_REG) { - mir_verify_reg(f, stage, op->v.reg, op->cls, "register operand"); - } else if (op->kind == OPK_INDIRECT) { - mir_verify_reg(f, stage, op->v.ind.base, RC_INT, "indirect base"); - if (op->v.ind.index != (Reg)REG_NONE) - mir_verify_reg(f, stage, op->v.ind.index, RC_INT, "indirect index"); - } else if (op->kind == OPK_LOCAL) { - if (op->v.frame_slot == FRAME_SLOT_NONE || - op->v.frame_slot > f->nframe_slots) { - SrcLoc loc = {0, 0, 0}; - compiler_panic(f->c, loc, "opt MIR verify %s: bad frame slot %u", - stage ? stage : "?", (unsigned)op->v.frame_slot); - } + if (ctx->indirect) { + mir_verify_indirect_component(f, in, op, is_def, ctx); + return; + } + if (op->flags) + mir_fail(f, in, ctx->stage, "stored operand has transient flags", op->kind, + op->flags); + if (op->kind == OPK_INDIRECT) { + mir_verify_indirect(f, in, op, ctx); + return; } + mir_verify_scalar_location(f, in, op, is_def, ctx); } void opt_mir_verify(Func* f, const char* stage) { Func v; - if (!mir_view(f, &v)) return; + if (!opt_mir_view(f, &v)) return; + mir_verify_frame_table(&v, stage); for (u32 b = 0; b < v.nblocks; ++b) { Block* bl = &v.blocks[b]; + if (bl->id != b) + mir_fail(&v, NULL, stage, "non-canonical MIR block id", bl->id, b); for (u32 i = 0; i < bl->ninsts; ++i) { Inst* in = &bl->insts[i]; + MirVerifyCtx ctx; + memset(&ctx, 0, sizeof ctx); + ctx.stage = stage; if ((IROp)in->op == IR_PHI) { - SrcLoc loc = in->loc; - compiler_panic(f->c, loc, "opt MIR verify %s: phi survived lowering", - stage ? stage : "?"); + mir_fail(&v, in, stage, "phi survived lowering", b, i); + } + if ((IROp)in->op > IR_INTRINSIC) + mir_fail(&v, in, stage, "invalid MIR opcode", in->op, in->id); + if ((in->nopnds != 0u) != (in->opnds != NULL)) + mir_fail(&v, in, stage, "inconsistent MIR operand array", in->nopnds, + in->opnds != NULL); + if (in->def != VAL_NONE || in->ndefs != 0u || in->defs != NULL) + mir_fail(&v, in, stage, "stale semantic definition metadata", in->def, + in->ndefs); + if ((IROp)in->op == IR_PARAM_DECL) { + if (in->nopnds) + mir_fail(&v, in, stage, "MIR parameter marker carries operands", b, + i); + continue; } - if ((IROp)in->op == IR_PARAM_DECL) continue; - opt_walk_inst_operands(&v, in, mir_verify_operand, (void*)stage); + if ((IROp)in->op == IR_ASM_BLOCK) mir_verify_asm_aux(&v, in, stage); + opt_walk_inst_operands(&v, in, mir_verify_operand, &ctx); + if (ctx.indirect) + mir_fail(&v, in, stage, "incomplete indirect component walk", + ctx.indirect_part, in->id); } } } void opt_mir_combine(Func* f, NativeTarget* target) { Func v; - if (!mir_view(f, &v)) return; + if (!opt_mir_view(f, &v)) return; opt_combine(&v, target); - mir_commit(f, &v); + opt_mir_commit(f, &v); } void opt_mir_dce(Func* f) { Func v; - if (!mir_view(f, &v)) return; + if (!opt_mir_view(f, &v)) return; opt_dce(&v); - mir_commit(f, &v); + opt_mir_commit(f, &v); } void opt_mir_build_cfg(Func* f) { Func v; - if (!mir_view(f, &v)) return; + if (!opt_mir_view(f, &v)) return; opt_build_cfg(&v); - mir_commit(f, &v); + opt_mir_commit(f, &v); } void opt_mir_jump_cleanup(Func* f, OptJumpCleanupStage stage) { Func v; - if (!mir_view(f, &v)) return; + if (!opt_mir_view(f, &v)) return; opt_jump_cleanup(&v, stage); - mir_commit(f, &v); + opt_mir_commit(f, &v); } diff --git a/src/opt/pass_native_emit.c b/src/opt/pass_native_emit.c @@ -1,5 +1,6 @@ #include <string.h> +#include "cg/native_asm.h" #include "cg/type.h" #include "core/metrics.h" #include "core/pool.h" @@ -17,6 +18,8 @@ #undef CGCallPlanRet #undef CGScopeDesc +typedef struct NativeEmitTempScope NativeEmitTempScope; + typedef struct NativeEmitCtx { Compiler* c; Func* f; @@ -34,8 +37,41 @@ typedef struct NativeEmitCtx { * branch-to-epilogue: func_end places the epilogue label at the very next * position, so the branch would just jump to the next 4 bytes. */ u8 emitting_terminal_ret; + /* Instruction-scoped materialization state. NULL outside emit_inst (notably + * during frame planning/parameter binding). */ + NativeEmitTempScope* temps; + /* Physical registers live after each MIR instruction, indexed by InstId. + * Used only to scavenge dead caller-saved registers after the dedicated temp + * bank is exhausted. */ + OptHardRegSet* live_after_by_inst; + u32 live_after_cap; + /* Clean frame-value forwarding cache, indexed by physical register. A + * nonzero slot means the register holds the exact value already stored in + * that private FS_SPILL slot. Memory is always authoritative. */ + FrameSlot frame_cache_slot[OPT_REG_CLASSES][OPT_MAX_HARD_REGS]; + KitCgTypeId frame_cache_type[OPT_REG_CLASSES][OPT_MAX_HARD_REGS]; + u32 frame_cache_held[OPT_REG_CLASSES]; + /* Callee-saved registers selected by the pre-frame asm staging plan. The + * actual instruction-local allocator is deterministic and chooses the same + * registers during emission; these masks make their ABI preservation part of + * the known frame instead of allocating save slots mid-body. */ + u32 asm_stage_callee_used[OPT_REG_CLASSES]; + /* Callee-saved registers already represented in the known frame, from MIR + * allocation or asm staging. A dead register in this set may safely retain a + * clean spill value without creating a new ABI preservation obligation. */ + u32 callee_saved_used[OPT_REG_CLASSES]; } NativeEmitCtx; +static void frame_cache_clear(NativeEmitCtx* e); +static void frame_cache_drop_reg(NativeEmitCtx* e, u32 cls, Reg reg); +static void frame_cache_invalidate_masks(NativeEmitCtx* e, + const u32* masks); +static NativeLoc frame_cache_take_slot(NativeEmitCtx* e, FrameSlot slot, + NativeLoc source, + NativeAllocClass cls, + KitCgTypeId type, Reg avoid_a, + Reg avoid_b); + static _Noreturn void emit_panic(NativeEmitCtx* e, SrcLoc loc, const char* msg) { compiler_panic(e->c, loc, "opt native emit: %s", msg); @@ -184,6 +220,16 @@ static NativeLoc loc_frame(KitCgTypeId type, NativeAllocClass cls, return loc; } +static NativeLoc loc_frame_addr(KitCgTypeId type, NativeFrameSlot slot) { + NativeLoc loc; + memset(&loc, 0, sizeof loc); + loc.kind = NATIVE_LOC_FRAME_ADDR; + loc.cls = NATIVE_REG_INT; + loc.type = type; + loc.v.frame = slot; + return loc; +} + static NativeLoc loc_imm(KitCgTypeId type, i64 imm) { NativeLoc loc; memset(&loc, 0, sizeof loc); @@ -210,45 +256,444 @@ static int loc_same_frame(NativeLoc a, NativeLoc b) { a.v.frame == b.v.frame; } -static Reg scratch_reg(NativeEmitCtx* e, NativeAllocClass cls, Reg a, Reg b, - SrcLoc loc) { +#define NATIVE_EMIT_TEMP_CACHE 16u + +typedef struct NativeEmitTempCacheEntry { + NativeLoc source; + NativeLoc result; +} NativeEmitTempCacheEntry; + +struct NativeEmitTempScope { + NativeEmitCtx* emit; + const Inst* inst; + u8 allow_asm_temps; + u8 pad[3]; + u32 leased[OPT_REG_CLASSES]; + u32 unavailable[OPT_REG_CLASSES]; + /* Definition registers that currently hold a cached stack source consumed + * by this same instruction. They may be used in place as source+destination + * even though the normal avoidance set contains the destination register. */ + u32 cache_def_reuse[OPT_REG_CLASSES]; + NativeEmitTempCacheEntry cache[NATIVE_EMIT_TEMP_CACHE]; + u32 ncache; +}; + +typedef struct NativeEmitTempMark { + u32 leased[OPT_REG_CLASSES]; + u32 ncache; +} NativeEmitTempMark; + +typedef struct FrameCacheNeedCtx { + NativeEmitCtx* emit; + u32 regs[OPT_REG_CLASSES]; +} FrameCacheNeedCtx; + +static void frame_cache_collect_needed(const Inst* in, + FrameCacheNeedCtx* need) { + NativeEmitCtx* e = need->emit; + for (u32 i = 0; in && i < in->nopnds; ++i) { + const OptOperand* op = &in->opnds[i]; + if (opt_inst_operand_is_def(in, i) || op->kind != OPK_STACK) continue; + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) + for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r) + if ((e->frame_cache_held[c] & (1u << r)) && + e->frame_cache_slot[c][r] == op->v.frame_slot && + e->frame_cache_type[c][r] == op->type) + need->regs[c] |= 1u << r; + } +} + +static int temp_loc_equal(NativeLoc a, NativeLoc b) { + if (a.kind != b.kind || a.cls != b.cls || a.type != b.type) return 0; + switch ((NativeLocKind)a.kind) { + case NATIVE_LOC_NONE: + return 1; + case NATIVE_LOC_REG: + return a.v.reg == b.v.reg; + case NATIVE_LOC_FRAME: + case NATIVE_LOC_FRAME_ADDR: + return a.v.frame == b.v.frame; + case NATIVE_LOC_STACK: + return a.v.stack.slot == b.v.stack.slot && + a.v.stack.offset == b.v.stack.offset; + case NATIVE_LOC_IMM: + return a.v.imm == b.v.imm; + case NATIVE_LOC_GLOBAL: + return a.v.global.sym == b.v.global.sym && + a.v.global.addend == b.v.global.addend; + case NATIVE_LOC_ADDR: + return memcmp(&a.v.addr, &b.v.addr, sizeof a.v.addr) == 0; + } + return 0; +} + +static void temp_scope_begin(NativeEmitCtx* e, NativeEmitTempScope* scope, + const Inst* in) { + OptRegEffects effects; + FrameCacheNeedCtx needed; + u32 live_after[OPT_REG_CLASSES]; + u32 invalidate[OPT_REG_CLASSES]; + memset(scope, 0, sizeof *scope); + memset(&needed, 0, sizeof needed); + memset(live_after, 0, sizeof live_after); + scope->emit = e; + scope->inst = in; + if (in && in->id != INST_ID_NONE && in->id < e->live_after_cap) + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) { + live_after[c] = e->live_after_by_inst[in->id].cls[c]; + scope->unavailable[c] = live_after[c]; + } + opt_inst_reg_effects(e->f, in, &effects); + needed.emit = e; + frame_cache_collect_needed(in, &needed); + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) { + u32 clobbers = effects.clobbers.cls[c]; + /* Call clobbers happen after argument/callee staging. A call-local temp is + * consumed by the parallel-copy plan before emit_call and may therefore + * live in a caller-saved register; forbidding the entire ABI clobber set + * makes high-pressure calls impossible precisely when every callee-saved + * allocation register is live across them. Calls clear the block cache + * below, so this exception cannot preserve a stale value past the call. */ + if (in && (IROp)in->op == IR_CALL) clobbers = 0u; + scope->unavailable[c] |= + effects.uses.cls[c] | effects.defs.cls[c] | clobbers; + scope->cache_def_reuse[c] = + needed.regs[c] & effects.defs.cls[c] & + ~(effects.uses.cls[c] | effects.clobbers.cls[c]); + invalidate[c] = scope->unavailable[c] & ~scope->cache_def_reuse[c]; + } + if (in && ((IROp)in->op == IR_CALL || (IROp)in->op == IR_ASM_BLOCK)) + frame_cache_clear(e); + else + frame_cache_invalidate_masks(e, invalidate); + e->temps = scope; +} + +static NativeEmitTempMark temp_scope_mark(NativeEmitCtx* e) { + NativeEmitTempMark mark; + memset(&mark, 0, sizeof mark); + if (!e->temps) return mark; + memcpy(mark.leased, e->temps->leased, sizeof mark.leased); + mark.ncache = e->temps->ncache; + return mark; +} + +static void temp_scope_rewind(NativeEmitCtx* e, NativeEmitTempMark mark) { + if (!e->temps) return; + memcpy(e->temps->leased, mark.leased, sizeof mark.leased); + e->temps->ncache = mark.ncache; +} + +/* Forget every instruction-local materialization fact whose value lived in + * `reg`. This does not release the register: a caller that overwrote a cached + * value in place still owns the new result through the existing lease. */ +static int temp_scope_drop_cached_reg(NativeEmitCtx* e, + NativeAllocClass cls, Reg reg) { u32 c = (u32)cls; - if (c < OPT_REG_CLASSES) { - for (u32 i = 0; i < e->f->opt_scratch_reg_count[c]; ++i) { - Reg r = e->f->opt_scratch_regs[c][i]; - if (r != a && r != b) return r; + u32 out = 0; + int owned = 0; + if (!e->temps || c >= OPT_REG_CLASSES || reg >= OPT_MAX_HARD_REGS) return 0; + for (u32 i = 0; i < e->temps->ncache; ++i) { + NativeEmitTempCacheEntry entry = e->temps->cache[i]; + if (entry.result.kind == NATIVE_LOC_REG && entry.result.cls == (u8)cls && + entry.result.v.reg == reg) { + owned = 1; + continue; } + if (out != i) e->temps->cache[out] = entry; + ++out; } - emit_panic(e, loc, "no scratch register for native emission"); + if (!owned) return 0; + e->temps->ncache = out; + e->temps->cache_def_reuse[c] &= ~(1u << reg); + return 1; +} + +/* Relinquish a register only when this instruction acquired it as a cached + * materialization. Earlier ABI-phase destinations are leased without a cache + * entry and therefore remain protected. */ +static void temp_scope_release_cached_reg(NativeEmitCtx* e, + NativeAllocClass cls, Reg reg) { + u32 c = (u32)cls; + if (!temp_scope_drop_cached_reg(e, cls, reg)) return; + e->temps->leased[c] &= ~(1u << reg); +} + +/* End one ordering phase inside a compound native operation. A call consumes + * all argument/callee staging before it clobbers caller-saved registers; none + * of those leases or materialization facts may be observed by return + * writeback. The instruction scope (and its hard-reg avoidance set) remains + * active for the post-call phase. */ +static void temp_scope_phase_barrier(NativeEmitCtx* e) { + if (!e->temps) return; + memset(e->temps->leased, 0, sizeof e->temps->leased); + e->temps->ncache = 0; +} + +/* Keep a completed register destination owned until the current compound + * operation reaches its next ordering barrier. write_loc deliberately + * rewinds the temporary leases used to perform one move; without this separate + * phase lease, a later move could reuse (and overwrite) an earlier ABI + * argument or return register while materializing its own source. */ +static void temp_scope_hold_loc(NativeEmitCtx* e, NativeLoc loc) { + u32 c; + Reg r; + if (!e->temps || loc.kind != NATIVE_LOC_REG) return; + c = (u32)loc.cls; + r = loc.v.reg; + if (c >= OPT_REG_CLASSES || r >= OPT_MAX_HARD_REGS) + emit_panic(e, e->temps->inst ? e->temps->inst->loc : (SrcLoc){0, 0, 0}, + "invalid phase-owned register"); + frame_cache_drop_reg(e, c, r); + e->temps->leased[c] |= 1u << r; +} + +static void temp_scope_end(NativeEmitCtx* e, NativeEmitTempScope* scope) { + if (e->temps != scope) + emit_panic(e, scope->inst ? scope->inst->loc : (SrcLoc){0, 0, 0}, + "temporary scope mismatch"); +#ifndef NDEBUG + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) + if (scope->leased[c] & e->frame_cache_held[c]) + emit_panic(e, scope->inst ? scope->inst->loc : (SrcLoc){0, 0, 0}, + "temporary lease escaped into frame cache"); +#endif + memset(scope->leased, 0, sizeof scope->leased); + scope->ncache = 0; + e->temps = NULL; +} + +static int temp_reg_available(NativeEmitCtx* e, NativeAllocClass cls, Reg r, + Reg a, Reg b) { + u32 c = (u32)cls; + u32 bit; + if (r == a || r == b || r >= 32u || c >= OPT_REG_CLASSES) return 0; + bit = 1u << r; + if (!e->temps) return 1; + return !(e->temps->leased[c] & bit) && + !(e->temps->unavailable[c] & bit) && + !(e->frame_cache_held[c] & bit); } -static int scratch_available(NativeEmitCtx* e, NativeAllocClass cls, Reg a, - Reg b) { +static Reg temp_acquire(NativeEmitCtx* e, NativeAllocClass cls, Reg a, Reg b, + SrcLoc loc) { u32 c = (u32)cls; if (c < OPT_REG_CLASSES) { - for (u32 i = 0; i < e->f->opt_scratch_reg_count[c]; ++i) { - Reg r = e->f->opt_scratch_regs[c][i]; - if (r != a && r != b) return 1; + /* Prefer the target-declared O1 operand-temp bank. These registers are + * excluded from allocation, but still respect explicit/implicit instruction + * clobbers and other leases in this scope. */ + for (u32 i = 0; i < e->f->emit_temp_reg_count[c]; ++i) { + Reg r = e->f->emit_temp_regs[c][i]; + if (!temp_reg_available(e, cls, r, a, b)) continue; + if (e->temps) e->temps->leased[c] |= 1u << r; + return r; + } + /* A dead caller-saved allocation register is an equally local temporary: + * it needs no prologue save and liveness proves its value is dead before + * we overwrite it. This is the pressure escape hatch for two-temp targets + * and for ARM32, whose backend-internal LR must not be leased. */ + if (e->temps) { + for (u32 i = 0; i < e->f->opt_hard_reg_count[c]; ++i) { + Reg r = e->f->opt_hard_regs[c][i]; + if (r >= 32u || !(e->f->opt_caller_saved[c] & (1u << r))) continue; + if (!temp_reg_available(e, cls, r, a, b)) continue; + e->temps->leased[c] |= 1u << r; + return r; + } + } + /* A clean cache entry is always evictable: its frame store is already + * authoritative. Prefer losing the forwarding opportunity over failing a + * real instruction's temporary demand. */ + if (e->temps) { + for (u32 i = 0; i < e->f->emit_temp_reg_count[c]; ++i) { + Reg r = e->f->emit_temp_regs[c][i]; + u32 bit = r < 32u ? 1u << r : 0u; + if (!bit || !(e->frame_cache_held[c] & bit) || r == a || r == b || + (e->temps->leased[c] & bit) || + (e->temps->unavailable[c] & bit)) + continue; + frame_cache_drop_reg(e, c, r); + e->temps->leased[c] |= bit; + return r; + } } } - return 0; + compiler_panic( + e->c, loc, + "opt native emit: no instruction-local native temporary " + "(class=%u op=%u inst=%u leased=0x%08x unavailable=0x%08x " + "cached=0x%08x avoid=%u/%u)", + (unsigned)c, + e->temps && e->temps->inst ? (unsigned)e->temps->inst->op : 0u, + e->temps && e->temps->inst ? (unsigned)e->temps->inst->id : 0u, + e->temps && c < OPT_REG_CLASSES ? (unsigned)e->temps->leased[c] : 0u, + e->temps && c < OPT_REG_CLASSES ? (unsigned)e->temps->unavailable[c] + : 0u, + c < OPT_REG_CLASSES ? (unsigned)e->frame_cache_held[c] : 0u, + (unsigned)a, (unsigned)b); +} + +static int asm_temp_reg_ok(NativeEmitCtx* e, NativeAllocClass cls, Reg r, + u32 allowed_mask) { + if (r >= 32u || (allowed_mask && !(allowed_mask & (1u << r)))) return 0; + return !e->target->regs || !e->target->regs->asm_operand_reg_ok || + e->target->regs->asm_operand_reg_ok(e->target->regs, cls, r); +} + +static const NativeAllocClassInfo* emit_class_info(NativeEmitCtx* e, + NativeAllocClass cls) { + if (!e->target->regs) return NULL; + for (u32 i = 0; i < e->target->regs->nclasses; ++i) + if (e->target->regs->classes[i].cls == (u8)cls) + return &e->target->regs->classes[i]; + return NULL; } -/* True iff `r` is one of this class's reserved scratch registers. Such a register - * is never assigned to an allocator-live value: when it appears as an address's - * base it holds only a transient (a reloaded spilled base or a materialized - * frame/global address) that is dead after the current access. */ -static int reg_is_scratch(NativeEmitCtx* e, NativeAllocClass cls, Reg r) { +static void frame_cache_drop_reg(NativeEmitCtx* e, u32 cls, Reg reg) { + if (cls >= OPT_REG_CLASSES || reg >= OPT_MAX_HARD_REGS) return; + e->frame_cache_slot[cls][reg] = FRAME_SLOT_NONE; + e->frame_cache_type[cls][reg] = 0; + e->frame_cache_held[cls] &= ~(1u << reg); +} + +static void frame_cache_clear(NativeEmitCtx* e) { + memset(e->frame_cache_slot, 0, sizeof e->frame_cache_slot); + memset(e->frame_cache_type, 0, sizeof e->frame_cache_type); + memset(e->frame_cache_held, 0, sizeof e->frame_cache_held); +} + +static void frame_cache_invalidate_masks(NativeEmitCtx* e, + const u32* masks) { + if (!masks) return; + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) { + u32 killed = e->frame_cache_held[c] & masks[c]; + for (Reg r = 0; killed && r < 32u; ++r) + if (killed & (1u << r)) { + frame_cache_drop_reg(e, c, r); + killed &= ~(1u << r); + } + } +} + +static u32 frame_cache_retainable_mask(NativeEmitCtx* e, + NativeAllocClass cls) { + const NativeAllocClassInfo* ci = emit_class_info(e, cls); u32 c = (u32)cls; - if (r == (Reg)REG_NONE || c >= OPT_REG_CLASSES) return 0; - for (u32 i = 0; i < e->f->opt_scratch_reg_count[c]; ++i) - if (e->f->opt_scratch_regs[c][i] == r) return 1; + u32 mask; + u32 preserved_allocable; + if (!ci || c >= OPT_REG_CLASSES) return 0u; + mask = ci->emit_cache_mask; + preserved_allocable = native_target_caller_saved_mask(e->target, cls); + preserved_allocable |= e->callee_saved_used[c]; + /* An allocable register is a legal persistent cache only when acquisition + * proved it dead and its ABI preservation is already established: + * caller-saved until the next call boundary, or a callee-save already present + * in the known frame because MIR/asm uses it. The NativeTarget contract + * requires ordinary hooks to preserve such registers unless the current + * instruction's exhaustive machine_op_clobbers effect says otherwise; + * calls/asm clear the cache at their phase boundary. */ + for (u32 i = 0; i < e->f->opt_hard_reg_count[c]; ++i) { + Reg r = e->f->opt_hard_regs[c][i]; + if (r < 32u && (preserved_allocable & (1u << r))) mask |= 1u << r; + } + return mask; +} + +/* Acquire a lease satisfying an inline-asm register constraint. Unlike the + * general temporary picker this may name a fixed ABI register (for example + * x86 "a"). Regalloc has already kept values live across the asm out of that + * register; the availability check enforces that contract at the boundary. */ +static Reg temp_acquire_asm(NativeEmitCtx* e, NativeAllocClass cls, Reg fixed, + u32 allowed_mask, SrcLoc loc) { + u32 c = (u32)cls; + if (!e->temps || c >= OPT_REG_CLASSES) + emit_panic(e, loc, "asm register staging outside temporary scope"); + if (fixed != (Reg)REG_NONE) { + if (!asm_temp_reg_ok(e, cls, fixed, allowed_mask) || + !temp_reg_available(e, cls, fixed, REG_NONE, REG_NONE)) + emit_panic(e, loc, "fixed asm staging register is unavailable"); + e->temps->leased[c] |= 1u << fixed; + return fixed; + } + for (u32 i = 0; i < e->f->emit_temp_reg_count[c]; ++i) { + Reg r = e->f->emit_temp_regs[c][i]; + /* The O1 operand-temp bank is deliberately hidden from user + * named-register requirements and allocator homes, but it is valid as an + * internally substituted unrestricted asm operand. Restricted masks still + * apply. */ + if (r >= 32u || (allowed_mask && !(allowed_mask & (1u << r))) || + !temp_reg_available(e, cls, r, REG_NONE, REG_NONE)) + continue; + e->temps->leased[c] |= 1u << r; + return r; + } + if (e->temps->allow_asm_temps) { + const NativeAllocClassInfo* ci = emit_class_info(e, cls); + for (u32 i = 0; ci && i < ci->nasm_temps; ++i) { + Reg r = ci->asm_temps[i]; + if (r >= 32u || (allowed_mask && !(allowed_mask & (1u << r))) || + !temp_reg_available(e, cls, r, REG_NONE, REG_NONE)) + continue; + e->temps->leased[c] |= 1u << r; + return r; + } + } + /* An asm boundary may use every target-declared asm register, not just the + * allocator's general-purpose subset. This is what makes high-arity blocks + * possible without assigning a fixed register to an entire PReg live range. + * A setup prepass records any selected callee-saved registers in the known + * frame, so choosing one here remains ABI-safe. */ + for (u32 i = 0; i < e->f->opt_phys_reg_count[c]; ++i) { + Reg r = e->f->opt_phys_regs[c][i].reg; + if (!asm_temp_reg_ok(e, cls, r, allowed_mask) || + !temp_reg_available(e, cls, r, REG_NONE, REG_NONE)) + continue; + e->temps->leased[c] |= 1u << r; + return r; + } + compiler_panic(e->c, loc, + "opt native emit: no register available for asm operand " + "staging (class=%u leased=0x%08x unavailable=0x%08x " + "allowed=0x%08x)", + (unsigned)c, (unsigned)e->temps->leased[c], + (unsigned)e->temps->unavailable[c], + (unsigned)allowed_mask); +} + +/* A leased register is owned by the current emission scope and may therefore + * be overwritten in place. An ordinary OPK_REG may still be live after the + * instruction even when the result itself is frame-resident, so reusing it as + * the result would silently clobber an allocator-owned value. */ +static int temp_reg_is_leased(NativeEmitCtx* e, NativeAllocClass cls, Reg r) { + u32 c = (u32)cls; + return e->temps && c < OPT_REG_CLASSES && r < 32u && + (e->temps->leased[c] & (1u << r)) != 0; +} + +static int temp_available(NativeEmitCtx* e, NativeAllocClass cls, Reg a, + Reg b) { + u32 c = (u32)cls; + if (c < OPT_REG_CLASSES) { + for (u32 i = 0; i < e->f->emit_temp_reg_count[c]; ++i) { + Reg r = e->f->emit_temp_regs[c][i]; + if (temp_reg_available(e, cls, r, a, b)) return 1; + } + if (e->temps) { + for (u32 i = 0; i < e->f->opt_hard_reg_count[c]; ++i) { + Reg r = e->f->opt_hard_regs[c][i]; + if (r < 32u && (e->f->opt_caller_saved[c] & (1u << r)) && + temp_reg_available(e, cls, r, a, b)) + return 1; + } + } + } return 0; } -static NativeLoc scratch_loc(NativeEmitCtx* e, KitCgTypeId type, - NativeAllocClass cls, Reg a, Reg b, SrcLoc loc) { - return loc_reg(type, cls, scratch_reg(e, cls, a, b, loc)); +static NativeLoc temp_loc_acquire(NativeEmitCtx* e, KitCgTypeId type, + NativeAllocClass cls, Reg a, Reg b, + SrcLoc loc) { + return loc_reg(type, cls, temp_acquire(e, cls, a, b, loc)); } static NativeFrameSlot map_slot(NativeEmitCtx* e, NativeFrameSlot slot, @@ -276,6 +721,10 @@ static NativeAddr addr_from_loc(NativeEmitCtx* e, NativeLoc loc, addr.base_kind = NATIVE_ADDR_BASE_FRAME; addr.base.frame = loc.v.frame; return addr; + case NATIVE_LOC_FRAME_ADDR: + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = loc.v.frame; + return addr; case NATIVE_LOC_STACK: addr.base_kind = NATIVE_ADDR_BASE_FRAME; addr.base.frame = loc.v.stack.slot; @@ -309,20 +758,90 @@ static NativeAddr addr_from_operand(NativeEmitCtx* e, const OptOperand* op, addr.base_kind = NATIVE_ADDR_BASE_FRAME; addr.base.frame = map_slot(e, op->v.frame_slot, loc); return addr; + case OPT_OPK_FRAME_ADDR: + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = map_slot(e, op->v.frame_slot, loc); + return addr; + case OPT_OPK_STACK: + addr.base_type = op->type; + { + NativeFrameSlot mapped = map_slot(e, op->v.frame_slot, loc); + NativeAllocClass cls = class_for_type(e, addr.base_type); + NativeLoc held = frame_cache_take_slot( + e, op->v.frame_slot, loc_frame(addr.base_type, cls, mapped), cls, + addr.base_type, REG_NONE, REG_NONE); + if (held.kind == NATIVE_LOC_REG) { + addr.base_kind = NATIVE_ADDR_BASE_REG; + addr.cls = held.cls; + addr.base.reg = held.v.reg; + } else { + addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE; + addr.base.frame = mapped; + } + } + return addr; case OPT_OPK_GLOBAL: addr.base_kind = NATIVE_ADDR_BASE_GLOBAL; addr.base.global.sym = op->v.global.sym; addr.base.global.addend = op->v.global.addend; return addr; case OPT_OPK_INDIRECT: - addr.base_kind = NATIVE_ADDR_BASE_REG; addr.cls = NATIVE_REG_INT; - addr.base.reg = op->v.ind.base; - addr.index_kind = op->v.ind.index == (Reg)REG_NONE - ? NATIVE_ADDR_INDEX_NONE - : NATIVE_ADDR_INDEX_REG; + addr.base_type = op->v.ind.base_type; + if (!addr.base_type) + emit_panic(e, loc, "indirect base has no location type"); + if (op->v.ind.base_kind == OPT_INDIRECT_FRAME) { + { + NativeFrameSlot mapped = map_slot(e, op->v.ind.base, loc); + NativeLoc held = frame_cache_take_slot( + e, op->v.ind.base, + loc_frame(addr.base_type, NATIVE_REG_INT, mapped), + NATIVE_REG_INT, addr.base_type, REG_NONE, REG_NONE); + if (held.kind == NATIVE_LOC_REG) { + addr.base_kind = NATIVE_ADDR_BASE_REG; + addr.base.reg = held.v.reg; + } else { + addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE; + addr.base.frame = mapped; + } + } + } else if (op->v.ind.base_kind == OPT_INDIRECT_FRAME_ADDR) { + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = map_slot(e, op->v.ind.base, loc); + } else { + addr.base_kind = NATIVE_ADDR_BASE_REG; + addr.base.reg = op->v.ind.base; + } addr.index_cls = NATIVE_REG_INT; - addr.index.reg = op->v.ind.index; + if (op->v.ind.index_kind == OPT_INDIRECT_FRAME) { + addr.index_type = op->v.ind.index_type; + if (!addr.index_type) + emit_panic(e, loc, "indirect index has no location type"); + { + NativeFrameSlot mapped = map_slot(e, op->v.ind.index, loc); + NativeLoc held = frame_cache_take_slot( + e, op->v.ind.index, + loc_frame(addr.index_type, NATIVE_REG_INT, mapped), + NATIVE_REG_INT, addr.index_type, REG_NONE, REG_NONE); + if (held.kind == NATIVE_LOC_REG) { + addr.index_kind = NATIVE_ADDR_INDEX_REG; + addr.index.reg = held.v.reg; + } else { + addr.index_kind = NATIVE_ADDR_INDEX_FRAME_VALUE; + addr.index.frame = mapped; + } + } + } else if (op->v.ind.index_kind == OPT_INDIRECT_FRAME_ADDR) { + emit_panic(e, loc, "frame address cannot be an indirect index"); + } else if (op->v.ind.index == (Reg)REG_NONE) { + addr.index_kind = NATIVE_ADDR_INDEX_NONE; + } else { + addr.index_type = op->v.ind.index_type; + if (!addr.index_type) + emit_panic(e, loc, "indirect index has no location type"); + addr.index_kind = NATIVE_ADDR_INDEX_REG; + addr.index.reg = op->v.ind.index; + } addr.log2_scale = op->v.ind.log2_scale; /* L8 extend rider: OPT_IDX_EXT_* and NATIVE_ADDR_IDX_EXT_* share the * 0=NONE/1=SXTW/2=UXTW encoding. Only set by the addr-fold recognition @@ -367,7 +886,7 @@ static NativeAddr pointer_addr_from_operand(NativeEmitCtx* e, return addr; } cls = class_for_type(e, op->type); - r = scratch_reg(e, cls, avoid_a, avoid_b, loc); + r = temp_acquire(e, cls, avoid_a, avoid_b, loc); memset(&frame, 0, sizeof frame); frame.base_kind = NATIVE_ADDR_BASE_FRAME; frame.base.frame = map_slot(e, op->v.frame_slot, loc); @@ -379,6 +898,28 @@ static NativeAddr pointer_addr_from_operand(NativeEmitCtx* e, addr.base.reg = r; return addr; } + case OPT_OPK_FRAME_ADDR: + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = map_slot(e, op->v.frame_slot, loc); + return addr; + case OPT_OPK_STACK: + addr.cls = class_for_type(e, op->type); + addr.base_type = op->type; + { + NativeFrameSlot mapped = map_slot(e, op->v.frame_slot, loc); + NativeAllocClass cls = (NativeAllocClass)addr.cls; + NativeLoc held = frame_cache_take_slot( + e, op->v.frame_slot, loc_frame(addr.base_type, cls, mapped), cls, + addr.base_type, avoid_a, avoid_b); + if (held.kind == NATIVE_LOC_REG) { + addr.base_kind = NATIVE_ADDR_BASE_REG; + addr.base.reg = held.v.reg; + } else { + addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE; + addr.base.frame = mapped; + } + } + return addr; case OPT_OPK_GLOBAL: addr.base_kind = NATIVE_ADDR_BASE_GLOBAL; addr.base.global.sym = op->v.global.sym; @@ -408,31 +949,37 @@ static Reg addr_index_reg(const NativeAddr* addr) { static void collapse_addr_to_reg(NativeEmitCtx* e, NativeAddr* addr, SrcLoc loc) { - /* Materialize the full address into a reserved scratch register. We normally - * must not reuse the base register as the destination: the register allocator + /* Materialize the full address into an instruction-local temporary. We + * normally must not reuse the base register as the destination: physical MIR * may keep that value live past this memory op (e.g. a pointer stored into * several of its own fields and then returned), so an in-place `add base, * base, #off` would corrupt it. Avoid both base and index so load_addr can * still read them. * - * Exception (the only case where no third scratch exists): when both base and - * index are themselves reserved scratch registers, the address consumed the - * entire scratch budget and there is nothing left to avoid them with. A - * scratch base holds only a transient dead after this access, so folding the - * index into it in place is sound and frees the other scratch for the caller. - * load_addr reads the base before adding the (scaled) index, so dst==base is - * correct; dst==index would clobber the index before use — base only. */ + * If no third temporary is available and the base is itself a scoped + * temporary, that base is transient and may be updated in place. load_addr + * reads the base before adding the scaled index, so dst==base is sound; + * dst==index would clobber the index before use and is never selected. If the + * base is caller-owned, temp_acquire reports exhaustion instead of silently + * changing its value. */ Reg base = addr_base_reg(addr); Reg index = addr_index_reg(addr); Reg r; - if (scratch_available(e, NATIVE_REG_INT, base, index)) - r = scratch_reg(e, NATIVE_REG_INT, base, index, loc); - else if (reg_is_scratch(e, NATIVE_REG_INT, base)) + if (temp_available(e, NATIVE_REG_INT, base, index)) + r = temp_acquire(e, NATIVE_REG_INT, base, index, loc); + else if (base != index && temp_reg_is_leased(e, NATIVE_REG_INT, base)) r = base; else - r = scratch_reg(e, NATIVE_REG_INT, base, index, loc); /* diagnoses cleanly */ + r = temp_acquire(e, NATIVE_REG_INT, base, index, loc); /* diagnoses cleanly */ NativeLoc dst = loc_reg(addr->base_type, NATIVE_REG_INT, r); e->target->load_addr(e->target, dst, *addr); + /* load_addr replaced any prior value fact attached to its destination. The + * result remains leased; only the old cache identity is invalid now. */ + temp_scope_drop_cached_reg(e, NATIVE_REG_INT, r); + if (base != r) + temp_scope_release_cached_reg(e, NATIVE_REG_INT, base); + if (index != r) + temp_scope_release_cached_reg(e, NATIVE_REG_INT, index); memset(addr, 0, sizeof *addr); addr->base_kind = NATIVE_ADDR_BASE_REG; addr->cls = NATIVE_REG_INT; @@ -463,6 +1010,11 @@ static NativeLoc loc_from_operand(NativeEmitCtx* e, const OptOperand* op, case OPT_OPK_LOCAL: return loc_frame(op->type, class_for_type(e, op->type), map_slot(e, op->v.frame_slot, loc)); + case OPT_OPK_STACK: + return loc_frame(op->type, class_for_type(e, op->type), + map_slot(e, op->v.frame_slot, loc)); + case OPT_OPK_FRAME_ADDR: + return loc_frame_addr(op->type, map_slot(e, op->v.frame_slot, loc)); case OPT_OPK_INDIRECT: { NativeLoc out = loc_none(); out.kind = NATIVE_LOC_ADDR; @@ -482,56 +1034,306 @@ static NativeLoc materialize(NativeEmitCtx* e, NativeLoc src, NativeAddr addr; MemAccess mem; if (src.kind == NATIVE_LOC_REG) return src; - dst = scratch_loc(e, type ? type : src.type, cls, avoid_a, avoid_b, loc); + if (e->temps) { + KitCgTypeId want_type = type ? type : src.type; + for (u32 i = 0; i < e->temps->ncache; ++i) { + NativeEmitTempCacheEntry* entry = &e->temps->cache[i]; + Reg cached_reg = entry->result.v.reg; + u32 cached_bit = cached_reg < 32u ? 1u << cached_reg : 0u; + int avoided = cached_reg == avoid_a || cached_reg == avoid_b; + if (entry->result.cls != (u8)cls || entry->result.type != want_type || + (avoided && + !(e->temps->cache_def_reuse[(u32)cls] & cached_bit))) + continue; + if (temp_loc_equal(entry->source, src)) return entry->result; + } + } + dst = temp_loc_acquire(e, type ? type : src.type, cls, avoid_a, avoid_b, + loc); switch ((NativeLocKind)src.kind) { case NATIVE_LOC_IMM: e->target->load_imm(e->target, dst, src.v.imm); - return dst; + break; case NATIVE_LOC_GLOBAL: addr = addr_from_loc(e, src, loc); e->target->load_addr(e->target, dst, addr); - return dst; + break; + case NATIVE_LOC_FRAME_ADDR: + addr = addr_from_loc(e, src, loc); + e->target->load_addr(e->target, dst, addr); + break; case NATIVE_LOC_FRAME: case NATIVE_LOC_STACK: case NATIVE_LOC_ADDR: addr = addr_from_loc(e, src, loc); mem = mem_for_type(e->c, dst.type); e->target->load(e->target, dst, addr, mem); - return dst; + break; default: emit_panic(e, loc, "cannot materialize location"); } + if (e->temps && e->temps->ncache < NATIVE_EMIT_TEMP_CACHE) { + NativeEmitTempCacheEntry* entry = &e->temps->cache[e->temps->ncache++]; + entry->source = src; + entry->result = dst; + } + return dst; +} + +static int frame_cache_slot_is_exact_spill(NativeEmitCtx* e, + const OptOperand* op, + MemAccess mem) { + const IRFrameSlot* slot; + if (!op || op->kind != OPK_STACK || op->v.frame_slot == FRAME_SLOT_NONE || + op->v.frame_slot > e->f->nframe_slots) + return 0; + slot = &e->f->frame_slots[op->v.frame_slot - 1u]; + /* Spill-slot coloring may reuse one slot for different, non-overlapping type + * IDs. Exactness is the stored bit width; the cache entry separately records + * the current value's type/class and only answers an identical use. */ + return slot->kind == FS_SPILL && slot->size >= mem.size && + mem.size == type_size_or(e->c, op->type, 0u); +} + +static void frame_cache_invalidate_slot(NativeEmitCtx* e, FrameSlot slot) { + if (slot == FRAME_SLOT_NONE) return; + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) + for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r) + if (e->frame_cache_slot[c][r] == slot) + frame_cache_drop_reg(e, c, r); +} + +static int frame_cache_types_compatible(NativeEmitCtx* e, + KitCgTypeId cached, + KitCgTypeId requested) { + (void)e; + return cached == requested; +} + +static NativeLoc frame_cache_take_slot(NativeEmitCtx* e, FrameSlot slot, + NativeLoc source, + NativeAllocClass cls, + KitCgTypeId type, Reg avoid_a, + Reg avoid_b) { + u32 c = (u32)cls; + if (!e->temps || slot == FRAME_SLOT_NONE || c >= OPT_REG_CLASSES) + return loc_none(); + for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r) { + u32 bit = 1u << r; + NativeLoc result; + int avoided = r == avoid_a || r == avoid_b; + if (!(e->frame_cache_held[c] & bit) || + (avoided && !(e->temps->cache_def_reuse[c] & bit)) || + e->frame_cache_slot[c][r] != slot || + !frame_cache_types_compatible(e, e->frame_cache_type[c][r], type) || + ((e->temps->unavailable[c] & bit) && + !(e->temps->cache_def_reuse[c] & bit))) + continue; + frame_cache_drop_reg(e, c, r); + e->temps->leased[c] |= bit; + result = loc_reg(type, cls, r); + if (e->temps->ncache < NATIVE_EMIT_TEMP_CACHE) { + NativeEmitTempCacheEntry* entry = + &e->temps->cache[e->temps->ncache++]; + entry->source = source; + entry->result = result; + } + return result; + } + return loc_none(); +} + +static NativeLoc frame_cache_take(NativeEmitCtx* e, const OptOperand* op, + NativeLoc source, NativeAllocClass cls, + KitCgTypeId type, Reg avoid_a, Reg avoid_b) { + if (!op || op->kind != OPK_STACK) return loc_none(); + return frame_cache_take_slot(e, op->v.frame_slot, source, cls, type, + avoid_a, avoid_b); +} + +static void frame_cache_note_store(NativeEmitCtx* e, const OptOperand* op, + NativeLoc src, MemAccess mem) { + NativeAllocClass cls; + u32 c, bit; + Reg r; + int retainable; + if (!op || op->kind != OPK_STACK) return; + frame_cache_invalidate_slot(e, op->v.frame_slot); + if (!e->temps || src.kind != NATIVE_LOC_REG || + !frame_cache_slot_is_exact_spill(e, op, mem)) + return; + cls = (NativeAllocClass)src.cls; + c = (u32)cls; + r = src.v.reg; + if (c >= OPT_REG_CLASSES || r >= OPT_MAX_HARD_REGS) return; + bit = 1u << r; + /* Cross-instruction retention follows the explicit NativeTarget preservation + * contract: declared cache-enabled emission temps plus liveness-proven dead + * O1 allocation registers + * whose caller/callee-save preservation is established, with every + * exceptional fixed clobber represented by the current instruction's + * exhaustive machine effect. */ + retainable = (frame_cache_retainable_mask(e, cls) & bit) != 0; + if (!retainable) return; + frame_cache_drop_reg(e, c, r); + e->temps->leased[c] &= ~bit; /* transfer scope ownership to block cache */ + e->frame_cache_slot[c][r] = op->v.frame_slot; + e->frame_cache_type[c][r] = op->type; + e->frame_cache_held[c] |= bit; +} + +static NativeLoc materialize_operand(NativeEmitCtx* e, const OptOperand* op, + NativeAllocClass cls, KitCgTypeId type, + Reg avoid_a, Reg avoid_b, SrcLoc loc) { + NativeLoc source = loc_from_operand(e, op, loc); + NativeLoc cached = frame_cache_take(e, op, source, cls, + type ? type : source.type, avoid_a, + avoid_b); + if (cached.kind != NATIVE_LOC_NONE) return cached; + return materialize(e, source, cls, type, avoid_a, avoid_b, loc); } static void write_loc(NativeEmitCtx* e, NativeLoc dst, NativeLoc src, MemAccess mem, SrcLoc loc) { NativeAddr addr; NativeLoc tmp; - if (dst.kind == NATIVE_LOC_NONE) return; - if (loc_same_frame(dst, src)) return; + NativeEmitTempMark mark = temp_scope_mark(e); + if (dst.kind == NATIVE_LOC_NONE) goto done; + if (loc_same_frame(dst, src)) goto done; if (dst.kind == NATIVE_LOC_REG) { if (src.kind == NATIVE_LOC_REG) { if (dst.v.reg != src.v.reg || dst.cls != src.cls) e->target->move(e->target, dst, src); - return; + goto done; } /* An immediate goes straight into the destination register; routing it - * through a scratch and then moving would cost an extra instruction. */ + * through an instruction-local temp and then moving would cost an extra + * instruction. */ if (src.kind == NATIVE_LOC_IMM) { e->target->load_imm(e->target, dst, src.v.imm); - return; + goto done; } tmp = materialize(e, src, (NativeAllocClass)dst.cls, dst.type, dst.v.reg, REG_NONE, loc); if (tmp.v.reg != dst.v.reg || tmp.cls != dst.cls) e->target->move(e->target, dst, tmp); - return; + goto done; } addr = addr_from_loc(e, dst, loc); if (src.kind != NATIVE_LOC_REG) src = materialize(e, src, (NativeAllocClass)dst.cls, dst.type, REG_NONE, REG_NONE, loc); e->target->store(e->target, addr, src, mem); +done: + temp_scope_rewind(e, mark); +} + +/* Execute one move in a multi-move ABI phase and retain its register + * destination. The backend call/return planners 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, + MemAccess mem, SrcLoc loc) { + write_loc(e, dst, src, mem, loc); + temp_scope_hold_loc(e, dst); +} + +static void write_operand(NativeEmitCtx* e, const OptOperand* dst, + NativeLoc src, MemAccess mem, SrcLoc loc) { + NativeLoc real_dst = loc_from_operand(e, dst, loc); + /* Keep the register that actually reaches a spill store. Besides avoiding a + * second materialization in write_loc, this lets ownership transfer from the + * instruction lease to the clean block cache. Constants and copies into a + * spill home are common producers and otherwise lose their forwarding fact + * at this abstraction boundary. */ + if (dst && dst->kind == OPK_STACK && src.kind != NATIVE_LOC_REG && + !loc_same_frame(real_dst, src)) + src = materialize(e, src, (NativeAllocClass)real_dst.cls, real_dst.type, + REG_NONE, REG_NONE, loc); + write_loc(e, real_dst, src, mem, loc); + frame_cache_note_store(e, dst, src, mem); +} + +/* Place a value directly in an already-leased register. This is the boundary + * operation used for fixed/constrained asm operands: going through materialize + * would acquire an unrelated temporary and then copy, which both wastes a + * register and can make a satisfiable high-arity asm look impossible. */ +static void load_loc_into_reg(NativeEmitCtx* e, NativeLoc dst, NativeLoc src, + SrcLoc loc) { + NativeAddr addr; + if (dst.kind != NATIVE_LOC_REG) + emit_panic(e, loc, "staging destination is not a register"); + switch ((NativeLocKind)src.kind) { + case NATIVE_LOC_REG: + if (src.v.reg != dst.v.reg || src.cls != dst.cls) + e->target->move(e->target, dst, src); + return; + case NATIVE_LOC_IMM: + e->target->load_imm(e->target, dst, src.v.imm); + return; + case NATIVE_LOC_GLOBAL: + case NATIVE_LOC_FRAME_ADDR: + addr = addr_from_loc(e, src, loc); + e->target->load_addr(e->target, dst, addr); + return; + case NATIVE_LOC_FRAME: + case NATIVE_LOC_STACK: + case NATIVE_LOC_ADDR: + addr = addr_from_loc(e, src, loc); + e->target->load(e->target, dst, addr, mem_for_type(e->c, dst.type)); + return; + default: + emit_panic(e, loc, "cannot stage asm operand in a register"); + } +} + +static int asm_reg_loc_satisfies(NativeEmitCtx* e, NativeLoc loc, + NativeAllocClass cls, Reg fixed, + u32 allowed_mask) { + if (loc.kind != NATIVE_LOC_REG || loc.cls != (u8)cls) return 0; + if (fixed != (Reg)REG_NONE && loc.v.reg != fixed) return 0; + return asm_temp_reg_ok(e, cls, loc.v.reg, allowed_mask); +} + +static int asm_reg_requirement(NativeEmitCtx* e, + const IRAsmRegRequirement* req, + NativeAllocClass* cls, Reg* fixed, + u32* allowed, SrcLoc loc) { + if (!req || !req->present) return 0; + if (req->cls >= OPT_REG_CLASSES || req->fixed_reg < -1 || + req->fixed_reg >= (i32)OPT_MAX_HARD_REGS) + emit_panic(e, loc, "invalid pre-resolved asm register requirement"); + *cls = (NativeAllocClass)req->cls; + *fixed = req->fixed_reg < 0 ? REG_NONE : (Reg)req->fixed_reg; + *allowed = req->allowed_mask; + return 1; +} + +/* Localize an asm register constraint at the instruction boundary. An + * unrestricted allocated register may already satisfy it; fixed/subset + * requirements are normally fulfilled by scoped staging. No emitter-owned + * register becomes a MIR home. */ +static NativeLoc asm_stage_reg_operand(NativeEmitCtx* e, + const AsmConstraint* constraint, + const IRAsmRegRequirement* req, + NativeLoc real, int load_value, + int* staged, SrcLoc loc) { + NativeAllocClass cls; + KitCgTypeId type; + Reg fixed; + u32 allowed; + Reg r; + NativeLoc out; + if (staged) *staged = 0; + if (!asm_reg_requirement(e, req, &cls, &fixed, &allowed, loc)) + return real; + type = constraint->type ? constraint->type : real.type; + if (asm_reg_loc_satisfies(e, real, cls, fixed, allowed)) return real; + r = temp_acquire_asm(e, cls, fixed, allowed, loc); + out = loc_reg(type, cls, r); + if (load_value) load_loc_into_reg(e, out, real, loc); + if (staged) *staged = 1; + return out; } /* For an arithmetic / compare source operand: keep it as an immediate when it @@ -543,9 +1345,8 @@ static NativeLoc operand_imm_or_reg(NativeEmitCtx* e, const OptOperand* op, if (op->kind == OPK_IMM && e->target->imm_legal && e->target->imm_legal(e->target, use, sub, op->type, op->v.imm)) return loc_imm(op->type, op->v.imm); - return materialize(e, loc_from_operand(e, op, loc), - class_for_type(e, op->type), op->type, avoid_a, avoid_b, - loc); + return materialize_operand(e, op, class_for_type(e, op->type), op->type, + avoid_a, avoid_b, loc); } static Reg loc_avoid_reg(NativeLoc l) { @@ -562,6 +1363,73 @@ static int type_is_aggregate_or_large(NativeEmitCtx* e, KitCgTypeId type) { type_size_or(e->c, type, 8u) > e->c->target.ptr_size); } +/* Acquire the register that receives a scalar memory load whose MIR result has + * a frame home. If the address already consumes every available value temp, + * first collapse it to one plain register. A leased integer base is then dead + * after address evaluation and may receive the loaded scalar in place: every + * NativeTarget load hook consumes a simple effective address before writing + * its destination. Never apply this to an allocator-owned base, an FP result, + * or a value wider than one machine register. */ +static NativeLoc acquire_frame_load_result(NativeEmitCtx* e, + KitCgTypeId type, + NativeAddr* addr, SrcLoc loc) { + NativeAllocClass cls = class_for_type(e, type); + Reg base = addr_base_reg(addr); + Reg index = addr_index_reg(addr); + if (temp_available(e, cls, base, index)) + return temp_loc_acquire(e, type, cls, base, index, loc); + + collapse_addr_to_reg(e, addr, loc); + base = addr_base_reg(addr); + if (cls == NATIVE_REG_INT && + type_size_or(e->c, type, e->c->target.ptr_size) <= + e->c->target.ptr_size && + addr->base_kind == NATIVE_ADDR_BASE_REG && + addr->index_kind == NATIVE_ADDR_INDEX_NONE && addr->offset == 0 && + temp_reg_is_leased(e, NATIVE_REG_INT, base)) + return loc_reg(type, cls, base); + + return temp_loc_acquire(e, type, cls, base, addr_index_reg(addr), loc); +} + +/* Materialize an address whose MIR destination has a frame home. A normal + * free temp receives the address directly. Under full pressure, collapse the + * expression into its leased base and return that already-materialized value; + * collapse_addr_to_reg refuses to overwrite an allocator-owned base. */ +static NativeLoc emit_frame_address_result(NativeEmitCtx* e, + KitCgTypeId type, + NativeAddr* addr, SrcLoc loc) { + NativeAllocClass cls = class_for_type(e, type); + Reg base = addr_base_reg(addr); + Reg index = addr_index_reg(addr); + NativeLoc dst; + if (temp_available(e, cls, base, index)) { + dst = temp_loc_acquire(e, type, cls, base, index, loc); + e->target->load_addr(e->target, dst, *addr); + return dst; + } + collapse_addr_to_reg(e, addr, loc); + base = addr_base_reg(addr); + if (cls != NATIVE_REG_INT || + !temp_reg_is_leased(e, NATIVE_REG_INT, base)) + emit_panic(e, loc, "materialized address has no scoped integer owner"); + return loc_reg(type, cls, base); +} + +/* Materialize a scalar value while an effective address remains live. If + * the address consumes the whole value-temp bank, reduce it to one scoped + * base first; collapse releases cache-owned components that are dead after + * address evaluation. The value then explicitly avoids the surviving base + * and index, keeping the backend hook's address and data roles disjoint. */ +static NativeLoc materialize_operand_away_from_addr( + NativeEmitCtx* e, const OptOperand* op, NativeAddr* addr, SrcLoc loc) { + NativeAllocClass cls = class_for_type(e, op->type); + if (!temp_available(e, cls, addr_base_reg(addr), addr_index_reg(addr))) + collapse_addr_to_reg(e, addr, loc); + return materialize_operand(e, op, cls, op->type, addr_base_reg(addr), + addr_index_reg(addr), loc); +} + /* Copy an aggregate / oversized value between two memory locations. dst and * src must be addressable (frame/global/indirect/reg-as-pointer); used for * IR_COPY/IR_LOAD/IR_STORE whose value type cannot move through one register. @@ -730,16 +1598,24 @@ 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 + * argument phase. Real backends marshal register arguments inside + * plan_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. */ + 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); for (u32 i = 0; i < plan.nargs; ++i) - write_loc(e, plan.args[i].dst, plan.args[i].src, plan.args[i].mem, in->loc); - if (plan.callee.kind != NATIVE_LOC_REG && - plan.callee.kind != NATIVE_LOC_GLOBAL) - plan.callee = materialize(e, plan.callee, NATIVE_REG_INT, plan.callee.type, - REG_NONE, REG_NONE, in->loc); + write_phase_loc(e, plan.args[i].dst, plan.args[i].src, plan.args[i].mem, + in->loc); e->target->emit_call(e->target, &plan); + temp_scope_phase_barrier(e); for (u32 i = 0; i < plan.nrets; ++i) - write_loc(e, plan.rets[i].dst, plan.rets[i].src, plan.rets[i].mem, in->loc); + write_phase_loc(e, plan.rets[i].dst, plan.rets[i].src, plan.rets[i].mem, + in->loc); if (result_slot && final_result.kind != NATIVE_LOC_NONE) { KitCgTypeId rty = aux->desc.ret.type; NativeLoc tmp = loc_frame(rty, class_for_type(e, rty), result_slot); @@ -784,7 +1660,7 @@ static void emit_ret(NativeEmitCtx* e, Inst* in, const CGFuncDesc* fd) { } e->target->plan_ret(e->target, fd, values, &rets, &nrets); for (u32 i = 0; i < nrets; ++i) - write_loc(e, rets[i].dst, rets[i].src, rets[i].mem, in->loc); + 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 * inst emitted: func_end will place the epilogue label at mc_pos right * after this, so the branch would jump to the next 4 bytes. The actual @@ -793,8 +1669,8 @@ static void emit_ret(NativeEmitCtx* e, Inst* in, const CGFuncDesc* fd) { if (!e->emitting_terminal_ret) e->target->ret(e->target); } -static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, - const CGFuncDesc* fd) { +static void emit_inst_body(NativeEmitCtx* e, u32 block, u32 order_index, + Inst* in, const CGFuncDesc* fd) { NativeLoc dst, a, b, src, tmp; NativeAddr addr, addr2; Reg dst_reg; @@ -813,16 +1689,23 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, return; case IR_LOAD_IMM: dst = loc_from_operand(e, &in->opnds[0], in->loc); - write_loc(e, dst, loc_imm(in->opnds[0].type, in->extra.imm), - mem_for_type(e->c, in->opnds[0].type), in->loc); + write_operand(e, &in->opnds[0], + loc_imm(in->opnds[0].type, in->extra.imm), + mem_for_type(e->c, in->opnds[0].type), in->loc); return; - case IR_LOAD_CONST: - dst = loc_from_operand(e, &in->opnds[0], in->loc); + case IR_LOAD_CONST: { + NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc); + dst = real; if (dst.kind != NATIVE_LOC_REG) - dst = materialize(e, dst, class_for_type(e, in->opnds[0].type), - in->opnds[0].type, REG_NONE, REG_NONE, in->loc); + dst = temp_loc_acquire(e, in->opnds[0].type, + class_for_type(e, in->opnds[0].type), REG_NONE, + REG_NONE, in->loc); e->target->load_const(e->target, dst, in->extra.cbytes); + if (real.kind != NATIVE_LOC_REG) + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; + } case IR_COPY: if (type_is_aggregate_or_large(e, in->opnds[0].type)) { emit_agg_move(e, addr_from_operand(e, &in->opnds[0], in->loc), @@ -832,7 +1715,15 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, } dst = loc_from_operand(e, &in->opnds[0], in->loc); src = loc_from_operand(e, &in->opnds[1], in->loc); - write_loc(e, dst, src, mem_for_type(e->c, in->opnds[0].type), in->loc); + if (in->opnds[1].kind == OPK_STACK) { + NativeLoc cached = frame_cache_take( + e, &in->opnds[1], src, + class_for_type(e, in->opnds[1].type), in->opnds[1].type, + REG_NONE, REG_NONE); + if (cached.kind != NATIVE_LOC_NONE) src = cached; + } + write_operand(e, &in->opnds[0], src, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; case IR_LOAD: if (type_is_aggregate_or_large(e, in->opnds[0].type)) { @@ -847,14 +1738,9 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, if (dst.kind == NATIVE_LOC_REG) { e->target->load(e->target, dst, addr, in->extra.mem); } else { - if (!scratch_available(e, class_for_type(e, in->opnds[0].type), - addr_base_reg(&addr), addr_index_reg(&addr))) - collapse_addr_to_reg(e, &addr, in->loc); - tmp = scratch_loc(e, in->opnds[0].type, - class_for_type(e, in->opnds[0].type), - addr_base_reg(&addr), addr_index_reg(&addr), in->loc); + tmp = acquire_frame_load_result(e, in->opnds[0].type, &addr, in->loc); e->target->load(e->target, tmp, addr, in->extra.mem); - write_loc(e, dst, tmp, in->extra.mem, in->loc); + write_operand(e, &in->opnds[0], tmp, in->extra.mem, in->loc); } return; case IR_STORE: @@ -868,29 +1754,26 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, legalize_addr(e, &addr, in->extra.mem, in->loc); src = loc_from_operand(e, &in->opnds[1], in->loc); /* Storing a constant 0 from the hardware zero register avoids - * materializing 0 into a scratch first (e.g. `strb wzr, [..]` rather than - * `movz w9,0; strb w9, [..]`). */ + * materializing 0 into an operand temp first (e.g. `strb wzr, [..]` + * rather than `movz w9,0; strb w9, [..]`). */ if (src.kind == NATIVE_LOC_IMM && src.v.imm == 0 && e->target->has_store_zero_reg && class_for_type(e, in->opnds[1].type) == NATIVE_REG_INT) src = loc_reg(in->opnds[1].type, NATIVE_REG_INT, e->target->store_zero_reg); /* Source register aliases the address base/index (e.g. `*p = (T)p`). - * Collapse the address into a scratch register: collapse_addr_to_reg - * selects a scratch distinct from both base and index — hence distinct - * from `src` — so the store reads `src` and writes through the fresh - * scratch with no alias. This stays entirely in registers; the frame is - * fully planned before emission, so emit never allocates a slot here. */ + * Collapse the address into an instruction-local temp: + * collapse_addr_to_reg selects one distinct from both base and index — + * hence distinct from `src` — so the store reads `src` and writes through + * the fresh temp with no alias. This stays entirely in registers; the + * frame is fully planned before emission, so emit never allocates a slot + * here. */ if (src.kind == NATIVE_LOC_REG && (src.v.reg == addr_base_reg(&addr) || src.v.reg == addr_index_reg(&addr))) collapse_addr_to_reg(e, &addr, in->loc); if (src.kind != NATIVE_LOC_REG) { - if (!scratch_available(e, class_for_type(e, in->opnds[1].type), - addr_base_reg(&addr), addr_index_reg(&addr))) - collapse_addr_to_reg(e, &addr, in->loc); - src = materialize(e, src, class_for_type(e, in->opnds[1].type), - in->opnds[1].type, addr_base_reg(&addr), - addr_index_reg(&addr), in->loc); + src = materialize_operand_away_from_addr(e, &in->opnds[1], &addr, + in->loc); } e->target->store(e->target, addr, src, in->extra.mem); return; @@ -898,22 +1781,29 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc); addr = addr_from_operand(e, &in->opnds[1], in->loc); dst = real; - if (dst.kind != NATIVE_LOC_REG) - dst = scratch_loc(e, in->opnds[0].type, - class_for_type(e, in->opnds[0].type), REG_NONE, - REG_NONE, in->loc); - e->target->load_addr(e->target, dst, addr); - if (real.kind != NATIVE_LOC_REG) - write_loc(e, real, dst, mem_for_type(e->c, in->opnds[0].type), in->loc); + if (dst.kind != NATIVE_LOC_REG || + (addr.index_kind == NATIVE_ADDR_INDEX_REG && + dst.v.reg == addr.index.reg)) { + dst = emit_frame_address_result(e, in->opnds[0].type, &addr, in->loc); + } else { + e->target->load_addr(e->target, dst, addr); + } + if (!temp_loc_equal(real, dst)) + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; } case IR_TLS_ADDR_OF: { IRTlsAux* aux = (IRTlsAux*)in->extra.aux; - dst = loc_from_operand(e, &in->opnds[0], in->loc); + NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc); + dst = real; if (dst.kind != NATIVE_LOC_REG) - dst = materialize(e, dst, NATIVE_REG_INT, in->opnds[0].type, REG_NONE, + dst = temp_loc_acquire(e, in->opnds[0].type, NATIVE_REG_INT, REG_NONE, REG_NONE, in->loc); e->target->tls_addr_of(e->target, dst, aux->sym, aux->addend); + if (real.kind != NATIVE_LOC_REG) + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; } case IR_AGG_COPY: { @@ -924,6 +1814,8 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, e, &in->opnds[1], in->loc, addr.base_kind == NATIVE_ADDR_BASE_REG ? addr.base.reg : REG_NONE, REG_NONE); + legalize_addr(e, &addr, aux->access.mem, in->loc); + legalize_addr(e, &addr2, aux->access.mem, in->loc); e->target->copy_bytes(e->target, addr, addr2, aux->access); return; } @@ -931,46 +1823,46 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, IRAggAux* aux = (IRAggAux*)in->extra.aux; addr = pointer_addr_from_operand(e, &in->opnds[0], in->loc, REG_NONE, REG_NONE); + legalize_addr(e, &addr, aux->access.mem, in->loc); src = loc_from_operand(e, &in->opnds[1], in->loc); if (src.kind != NATIVE_LOC_REG) { - if (!scratch_available(e, NATIVE_REG_INT, addr_base_reg(&addr), - addr_index_reg(&addr))) - collapse_addr_to_reg(e, &addr, in->loc); - src = materialize(e, src, NATIVE_REG_INT, in->opnds[1].type, - addr_base_reg(&addr), addr_index_reg(&addr), in->loc); + src = materialize_operand_away_from_addr(e, &in->opnds[1], &addr, + in->loc); } e->target->set_bytes(e->target, addr, src, aux->access); return; } case IR_BITFIELD_LOAD: { IRBitFieldAux* aux = (IRBitFieldAux*)in->extra.aux; - dst = loc_from_operand(e, &in->opnds[0], in->loc); + NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc); + dst = real; addr = addr_from_operand(e, &in->opnds[1], in->loc); + legalize_addr(e, &addr, aux->access.storage, in->loc); if (dst.kind != NATIVE_LOC_REG) - dst = materialize(e, dst, class_for_type(e, in->opnds[0].type), - in->opnds[0].type, REG_NONE, REG_NONE, in->loc); + dst = acquire_frame_load_result(e, in->opnds[0].type, &addr, + in->loc); e->target->bitfield_load(e->target, dst, addr, aux->access); + if (real.kind != NATIVE_LOC_REG) + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; } case IR_BITFIELD_STORE: { IRBitFieldAux* aux = (IRBitFieldAux*)in->extra.aux; addr = addr_from_operand(e, &in->opnds[0], in->loc); + legalize_addr(e, &addr, aux->access.storage, in->loc); src = loc_from_operand(e, &in->opnds[1], in->loc); /* The field store is a read-modify-write that keeps the address live * across the value placement, so the value must not be materialized into * the address's base/index register. Mirror IR_STORE/IR_AGG_SET: when no - * other scratch is free, collapse the address to a single scratch first, + * other operand temp is free, collapse the address to one temp first, * then materialize the value avoiding the address regs. Without this, a - * spilled pointer reloaded into scratch[0] and a value reusing scratch[0] - * collide — the value `mov`s over the address, nulling it (SIGSEGV in - * optimized SQLite). */ + * spilled pointer reloaded into the first temp and a value reusing that + * temp collide — the value `mov`s over the address, nulling it (SIGSEGV + * in optimized SQLite). */ if (src.kind != NATIVE_LOC_REG) { - if (!scratch_available(e, class_for_type(e, in->opnds[1].type), - addr_base_reg(&addr), addr_index_reg(&addr))) - collapse_addr_to_reg(e, &addr, in->loc); - src = materialize(e, src, class_for_type(e, in->opnds[1].type), - in->opnds[1].type, addr_base_reg(&addr), - addr_index_reg(&addr), in->loc); + src = materialize_operand_away_from_addr(e, &in->opnds[1], &addr, + in->loc); } e->target->bitfield_store(e->target, addr, src, aux->access); return; @@ -979,9 +1871,10 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, dst = loc_from_operand(e, &in->opnds[0], in->loc); dst_reg = dst.kind == NATIVE_LOC_REG ? dst.v.reg : REG_NONE; b = loc_from_operand(e, &in->opnds[2], in->loc); - a = materialize(e, loc_from_operand(e, &in->opnds[1], in->loc), - class_for_type(e, in->opnds[1].type), in->opnds[1].type, - dst_reg, loc_avoid_reg(b), in->loc); + a = materialize_operand(e, &in->opnds[1], + class_for_type(e, in->opnds[1].type), + in->opnds[1].type, dst_reg, loc_avoid_reg(b), + in->loc); b = operand_imm_or_reg(e, &in->opnds[2], NATIVE_IMM_BINOP, (u32)in->extra.imm, a.v.reg, dst_reg, in->loc); /* L7 shift rider: a single-use SHL folded into this binop's second @@ -990,62 +1883,85 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, * pass only stamps it for targets advertising can_fold_shift_into_alu, * so a backend ignoring NativeLoc.shift never receives a nonzero rider. */ if (b.kind == NATIVE_LOC_REG) b.shift = in->opnds[2].shift; - if (dst.kind != NATIVE_LOC_REG) - dst = scratch_loc(e, in->opnds[0].type, - class_for_type(e, in->opnds[0].type), a.v.reg, - loc_avoid_reg(b), in->loc); + if (dst.kind != NATIVE_LOC_REG) { + NativeAllocClass dst_cls = class_for_type(e, in->opnds[0].type); + if (a.kind == NATIVE_LOC_REG && a.cls == (u8)dst_cls && + temp_reg_is_leased(e, dst_cls, a.v.reg)) + dst = loc_reg(in->opnds[0].type, dst_cls, a.v.reg); + else + dst = temp_loc_acquire(e, in->opnds[0].type, dst_cls, a.v.reg, + loc_avoid_reg(b), in->loc); + } e->target->binop(e->target, (BinOp)in->extra.imm, dst, a, b); if (in->opnds[0].kind != OPK_REG) - write_loc(e, loc_from_operand(e, &in->opnds[0], in->loc), dst, - mem_for_type(e->c, in->opnds[0].type), in->loc); + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; case IR_UNOP: dst = loc_from_operand(e, &in->opnds[0], in->loc); dst_reg = dst.kind == NATIVE_LOC_REG ? dst.v.reg : REG_NONE; - a = materialize(e, loc_from_operand(e, &in->opnds[1], in->loc), - class_for_type(e, in->opnds[1].type), in->opnds[1].type, - dst_reg, REG_NONE, in->loc); - if (dst.kind != NATIVE_LOC_REG) - dst = scratch_loc(e, in->opnds[0].type, - class_for_type(e, in->opnds[0].type), a.v.reg, - REG_NONE, in->loc); + a = materialize_operand(e, &in->opnds[1], + class_for_type(e, in->opnds[1].type), + in->opnds[1].type, dst_reg, REG_NONE, in->loc); + if (dst.kind != NATIVE_LOC_REG) { + NativeAllocClass dst_cls = class_for_type(e, in->opnds[0].type); + if (a.kind == NATIVE_LOC_REG && a.cls == (u8)dst_cls && + temp_reg_is_leased(e, dst_cls, a.v.reg)) + dst = loc_reg(in->opnds[0].type, dst_cls, a.v.reg); + else + dst = temp_loc_acquire(e, in->opnds[0].type, dst_cls, a.v.reg, + REG_NONE, + in->loc); + } e->target->unop(e->target, (UnOp)in->extra.imm, dst, a); if (in->opnds[0].kind != OPK_REG) - write_loc(e, loc_from_operand(e, &in->opnds[0], in->loc), dst, - mem_for_type(e->c, in->opnds[0].type), in->loc); + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; case IR_CMP: dst = loc_from_operand(e, &in->opnds[0], in->loc); dst_reg = dst.kind == NATIVE_LOC_REG ? dst.v.reg : REG_NONE; b = loc_from_operand(e, &in->opnds[2], in->loc); - a = materialize(e, loc_from_operand(e, &in->opnds[1], in->loc), - class_for_type(e, in->opnds[1].type), in->opnds[1].type, - dst_reg, loc_avoid_reg(b), in->loc); + a = materialize_operand(e, &in->opnds[1], + class_for_type(e, in->opnds[1].type), + in->opnds[1].type, dst_reg, loc_avoid_reg(b), + in->loc); b = operand_imm_or_reg(e, &in->opnds[2], NATIVE_IMM_CMP, (u32)in->extra.imm, a.v.reg, dst_reg, in->loc); - if (dst.kind != NATIVE_LOC_REG) - dst = scratch_loc(e, in->opnds[0].type, - class_for_type(e, in->opnds[0].type), a.v.reg, - loc_avoid_reg(b), in->loc); + if (dst.kind != NATIVE_LOC_REG) { + NativeAllocClass dst_cls = class_for_type(e, in->opnds[0].type); + if (a.kind == NATIVE_LOC_REG && a.cls == (u8)dst_cls && + temp_reg_is_leased(e, dst_cls, a.v.reg)) + dst = loc_reg(in->opnds[0].type, dst_cls, a.v.reg); + else + dst = temp_loc_acquire(e, in->opnds[0].type, dst_cls, a.v.reg, + loc_avoid_reg(b), in->loc); + } e->target->cmp(e->target, (CmpOp)in->extra.imm, dst, a, b); if (in->opnds[0].kind != OPK_REG) - write_loc(e, loc_from_operand(e, &in->opnds[0], in->loc), dst, - mem_for_type(e->c, in->opnds[0].type), in->loc); + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; case IR_CONVERT: dst = loc_from_operand(e, &in->opnds[0], in->loc); dst_reg = dst.kind == NATIVE_LOC_REG ? dst.v.reg : REG_NONE; - src = materialize(e, loc_from_operand(e, &in->opnds[1], in->loc), - class_for_type(e, in->opnds[1].type), in->opnds[1].type, - dst_reg, REG_NONE, in->loc); - if (dst.kind != NATIVE_LOC_REG) - dst = scratch_loc(e, in->opnds[0].type, - class_for_type(e, in->opnds[0].type), src.v.reg, - REG_NONE, in->loc); + src = materialize_operand(e, &in->opnds[1], + class_for_type(e, in->opnds[1].type), + in->opnds[1].type, dst_reg, REG_NONE, in->loc); + if (dst.kind != NATIVE_LOC_REG) { + NativeAllocClass dst_cls = class_for_type(e, in->opnds[0].type); + if (src.kind == NATIVE_LOC_REG && src.cls == (u8)dst_cls && + temp_reg_is_leased(e, dst_cls, src.v.reg)) + dst = loc_reg(in->opnds[0].type, dst_cls, src.v.reg); + else + dst = temp_loc_acquire(e, in->opnds[0].type, dst_cls, src.v.reg, + REG_NONE, + in->loc); + } e->target->convert(e->target, (ConvKind)in->extra.imm, dst, src); if (in->opnds[0].kind != OPK_REG) - write_loc(e, loc_from_operand(e, &in->opnds[0], in->loc), dst, - mem_for_type(e->c, in->opnds[0].type), in->loc); + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; case IR_CALL: emit_call(e, in); @@ -1059,9 +1975,10 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, ? e->f->emit_order[order_index + 1u] : UINT32_MAX; b = loc_from_operand(e, &in->opnds[1], in->loc); - a = materialize(e, loc_from_operand(e, &in->opnds[0], in->loc), - class_for_type(e, in->opnds[0].type), in->opnds[0].type, - REG_NONE, loc_avoid_reg(b), in->loc); + a = materialize_operand(e, &in->opnds[0], + class_for_type(e, in->opnds[0].type), + in->opnds[0].type, REG_NONE, loc_avoid_reg(b), + in->loc); b = operand_imm_or_reg(e, &in->opnds[1], NATIVE_IMM_CMP, (u32)in->extra.imm, a.v.reg, REG_NONE, in->loc); e->target->cmp_branch( @@ -1074,20 +1991,20 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, } case IR_SWITCH: { IRSwitchAux* aux = (IRSwitchAux*)in->extra.aux; - /* Pin the selector in a register ONCE before the chain. */ - NativeLoc sel = - materialize(e, loc_from_operand(e, &in->opnds[0], in->loc), - class_for_type(e, in->opnds[0].type), in->opnds[0].type, - REG_NONE, REG_NONE, in->loc); + /* Lease the selector in one register for the whole chain. */ + NativeLoc sel = materialize_operand( + e, &in->opnds[0], class_for_type(e, in->opnds[0].type), + in->opnds[0].type, REG_NONE, REG_NONE, in->loc); /* Each case value is interpreted at the selector type (matches the CG * fallback chain in cg/native_direct_target.c). Route each through the * same NATIVE_IMM_CMP immediate-or-register logic IR_CMP_BRANCH uses, so * a legal compare immediate becomes `cmp sel, #k` with no per-case * materialization; an illegal immediate (e.g. too large for the target's - * compare encoding) falls back to load_imm into a scratch that avoids the - * pinned selector register. */ + * compare encoding) falls back to load_imm into a temp that avoids the + * leased selector register. */ KitCgTypeId case_type = aux && aux->selector_type ? aux->selector_type : in->opnds[0].type; + NativeEmitTempMark case_mark = temp_scope_mark(e); for (u32 i = 0; aux && i < aux->ncases; ++i) { OptOperand case_op; NativeLoc imm; @@ -1099,6 +2016,7 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, sel.v.reg, REG_NONE, in->loc); e->target->cmp_branch(e->target, CMP_EQ, sel, imm, ensure_label(e, aux->cases[i].block, in->loc)); + temp_scope_rewind(e, case_mark); } if (aux) e->target->jump(e->target, @@ -1112,21 +2030,26 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, : NULL; for (u32 i = 0; aux && i < aux->ntargets; ++i) labels[i] = ensure_label(e, aux->targets[i], in->loc); - src = materialize(e, loc_from_operand(e, &in->opnds[0], in->loc), - NATIVE_REG_INT, in->opnds[0].type, REG_NONE, REG_NONE, - in->loc); + src = materialize_operand(e, &in->opnds[0], NATIVE_REG_INT, + in->opnds[0].type, REG_NONE, REG_NONE, + in->loc); e->target->indirect_branch(e->target, src, labels, aux ? aux->ntargets : 0u); return; } - case IR_LOAD_LABEL_ADDR: - dst = loc_from_operand(e, &in->opnds[0], in->loc); + case IR_LOAD_LABEL_ADDR: { + NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc); + dst = real; if (dst.kind != NATIVE_LOC_REG) - dst = materialize(e, dst, NATIVE_REG_INT, in->opnds[0].type, REG_NONE, + dst = temp_loc_acquire(e, in->opnds[0].type, NATIVE_REG_INT, REG_NONE, REG_NONE, in->loc); e->target->load_label_addr(e->target, dst, ensure_label(e, (u32)in->extra.imm, in->loc)); + if (real.kind != NATIVE_LOC_REG) + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; + } case IR_LOCAL_STATIC_DATA_BEGIN: { CgIrLocalStaticBeginAux* aux = (CgIrLocalStaticBeginAux*)in->extra.aux; emit_local_static_begin(e, aux ? &aux->desc : NULL, in->loc); @@ -1154,38 +2077,44 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, case IR_RET: emit_ret(e, in, fd); return; - case IR_ALLOCA: - dst = loc_from_operand(e, &in->opnds[0], in->loc); - src = materialize(e, loc_from_operand(e, &in->opnds[1], in->loc), - NATIVE_REG_INT, in->opnds[1].type, REG_NONE, REG_NONE, - in->loc); + case IR_ALLOCA: { + NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc); + dst = real; + src = materialize_operand(e, &in->opnds[1], NATIVE_REG_INT, + in->opnds[1].type, REG_NONE, REG_NONE, + in->loc); if (dst.kind != NATIVE_LOC_REG) - dst = scratch_loc(e, in->opnds[0].type, NATIVE_REG_INT, src.v.reg, + dst = temp_loc_acquire(e, in->opnds[0].type, NATIVE_REG_INT, src.v.reg, REG_NONE, in->loc); e->target->alloca_(e->target, dst, src, (u32)in->extra.imm); + if (real.kind != NATIVE_LOC_REG) + write_operand(e, &in->opnds[0], dst, + mem_for_type(e->c, in->opnds[0].type), in->loc); return; + } case IR_ATOMIC_LOAD: { IRAtomicAux* aux = (IRAtomicAux*)in->extra.aux; dst = loc_from_operand(e, &in->opnds[0], in->loc); addr = pointer_addr_from_operand(e, &in->opnds[1], in->loc, REG_NONE, REG_NONE); + legalize_addr(e, &addr, aux->mem, in->loc); if (dst.kind != NATIVE_LOC_REG) - dst = scratch_loc(e, in->opnds[0].type, - class_for_type(e, in->opnds[0].type), REG_NONE, - REG_NONE, in->loc); + dst = acquire_frame_load_result(e, in->opnds[0].type, &addr, + in->loc); e->target->atomic_load(e->target, dst, addr, aux->mem, aux->mo); if (in->opnds[0].kind != OPK_REG) - write_loc(e, loc_from_operand(e, &in->opnds[0], in->loc), dst, aux->mem, - in->loc); + write_operand(e, &in->opnds[0], dst, aux->mem, in->loc); return; } case IR_ATOMIC_STORE: { IRAtomicAux* aux = (IRAtomicAux*)in->extra.aux; addr = pointer_addr_from_operand(e, &in->opnds[0], in->loc, REG_NONE, REG_NONE); - src = materialize(e, loc_from_operand(e, &in->opnds[1], in->loc), - class_for_type(e, in->opnds[1].type), in->opnds[1].type, - REG_NONE, REG_NONE, in->loc); + legalize_addr(e, &addr, aux->mem, in->loc); + src = loc_from_operand(e, &in->opnds[1], in->loc); + if (src.kind != NATIVE_LOC_REG) + src = materialize_operand_away_from_addr(e, &in->opnds[1], &addr, + in->loc); e->target->atomic_store(e->target, addr, src, aux->mem, aux->mo); return; } @@ -1194,18 +2123,18 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, dst = loc_from_operand(e, &in->opnds[0], in->loc); addr = pointer_addr_from_operand(e, &in->opnds[1], in->loc, REG_NONE, REG_NONE); - src = materialize(e, loc_from_operand(e, &in->opnds[2], in->loc), - class_for_type(e, in->opnds[2].type), in->opnds[2].type, - REG_NONE, REG_NONE, in->loc); + legalize_addr(e, &addr, aux->mem, in->loc); + src = materialize_operand(e, &in->opnds[2], + class_for_type(e, in->opnds[2].type), + in->opnds[2].type, REG_NONE, REG_NONE, in->loc); if (dst.kind != NATIVE_LOC_REG) - dst = scratch_loc(e, in->opnds[0].type, + dst = temp_loc_acquire(e, in->opnds[0].type, class_for_type(e, in->opnds[0].type), src.v.reg, REG_NONE, in->loc); e->target->atomic_rmw(e->target, (KitCgAtomicOp)aux->op, dst, addr, src, aux->mem, aux->mo); if (in->opnds[0].kind != OPK_REG) - write_loc(e, loc_from_operand(e, &in->opnds[0], in->loc), dst, aux->mem, - in->loc); + write_operand(e, &in->opnds[0], dst, aux->mem, in->loc); return; } case IR_ATOMIC_CAS: { @@ -1217,77 +2146,76 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, ok = loc_from_operand(e, &in->opnds[1], in->loc); addr = pointer_addr_from_operand(e, &in->opnds[2], in->loc, REG_NONE, REG_NONE); - expected = materialize(e, loc_from_operand(e, &in->opnds[3], in->loc), - class_for_type(e, in->opnds[3].type), - in->opnds[3].type, REG_NONE, REG_NONE, in->loc); - desired = - materialize(e, loc_from_operand(e, &in->opnds[4], in->loc), - class_for_type(e, in->opnds[4].type), in->opnds[4].type, - expected.v.reg, REG_NONE, in->loc); + legalize_addr(e, &addr, aux->mem, in->loc); + expected = materialize_operand( + e, &in->opnds[3], class_for_type(e, in->opnds[3].type), + in->opnds[3].type, REG_NONE, REG_NONE, in->loc); + desired = materialize_operand( + e, &in->opnds[4], class_for_type(e, in->opnds[4].type), + in->opnds[4].type, expected.v.reg, REG_NONE, in->loc); if (dst.kind != NATIVE_LOC_REG) - dst = scratch_loc(e, in->opnds[0].type, + dst = temp_loc_acquire(e, in->opnds[0].type, class_for_type(e, in->opnds[0].type), expected.v.reg, desired.v.reg, in->loc); if (ok.kind != NATIVE_LOC_REG) - ok = scratch_loc(e, in->opnds[1].type, + ok = temp_loc_acquire(e, in->opnds[1].type, class_for_type(e, in->opnds[1].type), dst.v.reg, expected.v.reg, in->loc); e->target->atomic_cas(e->target, dst, ok, addr, expected, desired, aux->mem, aux->success, aux->failure); if (in->opnds[0].kind != OPK_REG) - write_loc(e, loc_from_operand(e, &in->opnds[0], in->loc), dst, aux->mem, - in->loc); + write_operand(e, &in->opnds[0], dst, aux->mem, in->loc); if (in->opnds[1].kind != OPK_REG) - write_loc(e, loc_from_operand(e, &in->opnds[1], in->loc), ok, - mem_for_type(e->c, in->opnds[1].type), in->loc); + write_operand(e, &in->opnds[1], ok, + mem_for_type(e->c, in->opnds[1].type), in->loc); return; } case IR_VA_START: { - NativeLoc ap = materialize(e, loc_from_operand(e, &in->opnds[0], in->loc), - NATIVE_REG_INT, in->opnds[0].type, REG_NONE, - REG_NONE, in->loc); + NativeLoc ap = materialize_operand(e, &in->opnds[0], NATIVE_REG_INT, + in->opnds[0].type, REG_NONE, REG_NONE, + in->loc); e->target->va_start_(e->target, ap); return; } case IR_VA_END: { - NativeLoc ap = materialize(e, loc_from_operand(e, &in->opnds[0], in->loc), - NATIVE_REG_INT, in->opnds[0].type, REG_NONE, - REG_NONE, in->loc); + NativeLoc ap = materialize_operand(e, &in->opnds[0], NATIVE_REG_INT, + in->opnds[0].type, REG_NONE, REG_NONE, + in->loc); e->target->va_end_(e->target, ap); return; } case IR_VA_COPY: { - NativeLoc d = materialize(e, loc_from_operand(e, &in->opnds[0], in->loc), - NATIVE_REG_INT, in->opnds[0].type, REG_NONE, - REG_NONE, in->loc); - NativeLoc s = materialize(e, loc_from_operand(e, &in->opnds[1], in->loc), - NATIVE_REG_INT, in->opnds[1].type, d.v.reg, - REG_NONE, in->loc); + NativeLoc d = materialize_operand(e, &in->opnds[0], NATIVE_REG_INT, + in->opnds[0].type, REG_NONE, REG_NONE, + in->loc); + NativeLoc s = materialize_operand(e, &in->opnds[1], NATIVE_REG_INT, + in->opnds[1].type, d.v.reg, REG_NONE, + in->loc); e->target->va_copy_(e->target, d, s); return; } case IR_VA_ARG: { KitCgTypeId ty = in->opnds[0].type; - NativeLoc ap = materialize(e, loc_from_operand(e, &in->opnds[1], in->loc), - NATIVE_REG_INT, in->opnds[1].type, REG_NONE, - REG_NONE, in->loc); + NativeLoc ap = materialize_operand(e, &in->opnds[1], NATIVE_REG_INT, + in->opnds[1].type, REG_NONE, REG_NONE, + in->loc); NativeLoc res; if (type_is_aggregate_or_large(e, ty)) { /* A value too wide for one register (an 8-byte i64/double on a 32-bit - * target, or an aggregate) can't pass through a scratch register; hand - * the target its memory destination so it can copy the value directly. - */ + * target, or an aggregate) cannot pass through one instruction-local + * register; hand the target its memory destination so it can copy the + * value directly. */ e->target->va_arg_(e->target, loc_from_operand(e, &in->opnds[0], in->loc), ap, ty); return; } /* The result must land in a register distinct from the va_list pointer; - * fetch into a scratch register, then write to the real destination. */ - res = scratch_loc(e, ty, class_for_type(e, ty), ap.v.reg, REG_NONE, + * fetch into an instruction-local temp, then write to the real + * destination. */ + res = temp_loc_acquire(e, ty, class_for_type(e, ty), ap.v.reg, REG_NONE, in->loc); e->target->va_arg_(e->target, res, ap, ty); - write_loc(e, loc_from_operand(e, &in->opnds[0], in->loc), res, - mem_for_type(e->c, ty), in->loc); + write_operand(e, &in->opnds[0], res, mem_for_type(e->c, ty), in->loc); return; } case IR_ASM_BLOCK: { @@ -1295,23 +2223,61 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, NativeLoc* out_locs = aux && aux->nout ? arena_array(e->f->arena, NativeLoc, aux->nout) : NULL; + NativeLoc* real_out_locs = + aux && aux->nout + ? arena_array(e->f->arena, NativeLoc, aux->nout) + : NULL; NativeLoc* in_locs = aux && aux->nin ? arena_array(e->f->arena, NativeLoc, aux->nin) : NULL; - /* The optimizer has already allocated registers for the asm operands and - * placed the input values / consumes the output values through the normal - * use/def data flow. We only convert each operand to its NativeLoc; the - * NativeTarget hook binds the pre-allocated registers to the template and - * saves/restores any callee-saved registers the asm clobbers. */ - for (u32 i = 0; aux && i < aux->nout; ++i) - out_locs[i] = loc_from_operand(e, &aux->out_ops[i], in->loc); - for (u32 i = 0; aux && i < aux->nin; ++i) - in_locs[i] = loc_from_operand(e, &aux->in_ops[i], in->loc); + int* staged_outs = + aux && aux->nout ? arena_zarray(e->f->arena, int, aux->nout) : NULL; + if (e->temps) + e->temps->allow_asm_temps = + (u8)!(aux && aux->has_memory_constraint); + /* Register constraints are an instruction-local placement requirement, + * not a property of the value's whole live range. Keep regalloc's chosen + * hard register when it satisfies the constraint; otherwise stage the + * value through a scoped lease. The backend asm binder validates and + * binds concrete register operands; only backend-private materialization + * for memory constraints remains on its side of the boundary. */ + for (u32 i = 0; aux && i < aux->nout; ++i) { + real_out_locs[i] = + loc_from_operand(e, &aux->out_ops[i], in->loc); + out_locs[i] = asm_stage_reg_operand( + e, &aux->outs[i], + aux->out_reg_reqs ? &aux->out_reg_reqs[i] : NULL, + real_out_locs[i], + aux->outs[i].dir == KIT_CG_ASM_INOUT, &staged_outs[i], in->loc); + } + for (u32 i = 0; aux && i < aux->nin; ++i) { + NativeLoc real = loc_from_operand(e, &aux->in_ops[i], in->loc); + int matched = native_asm_match_index(aux->ins[i].str); + if (matched >= 0) { + if ((u32)matched >= aux->nout || + out_locs[matched].kind != NATIVE_LOC_REG) + emit_panic(e, in->loc, "invalid matching asm operand"); + if (native_asm_constraint_early(aux->outs[matched].str)) + emit_panic(e, in->loc, + "matching input names early-clobber output"); + load_loc_into_reg(e, out_locs[matched], real, in->loc); + in_locs[i] = out_locs[matched]; + } else { + in_locs[i] = asm_stage_reg_operand( + e, &aux->ins[i], + aux->in_reg_reqs ? &aux->in_reg_reqs[i] : NULL, real, 1, NULL, + in->loc); + } + } e->target->asm_block(e->target, aux ? aux->tmpl : "", aux ? aux->outs : NULL, aux ? aux->nout : 0, out_locs, aux ? aux->ins : NULL, aux ? aux->nin : 0, in_locs, aux ? aux->clobbers : NULL, aux ? aux->nclob : 0); + for (u32 i = 0; aux && i < aux->nout; ++i) + if (staged_outs[i]) + write_operand(e, &aux->out_ops[i], out_locs[i], + mem_for_type(e->c, out_locs[i].type), in->loc); return; } case IR_BREAK_TO: @@ -1328,22 +2294,41 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, NativeLoc* dsts = aux && aux->ndst ? arena_array(e->f->arena, NativeLoc, aux->ndst) : NULL; + NativeLoc* real_dsts = + aux && aux->ndst + ? arena_array(e->f->arena, NativeLoc, aux->ndst) + : NULL; NativeLoc* args = aux && aux->narg ? arena_array(e->f->arena, NativeLoc, aux->narg) : NULL; - for (u32 i = 0; aux && i < aux->ndst; ++i) - dsts[i] = loc_from_operand(e, &aux->dsts[i], in->loc); for (u32 i = 0; aux && i < aux->narg; ++i) { if (aux->args[i].kind == OPK_IMM) { args[i] = loc_from_operand(e, &aux->args[i], in->loc); } else { - args[i] = materialize(e, loc_from_operand(e, &aux->args[i], in->loc), - class_for_type(e, aux->args[i].type), - aux->args[i].type, REG_NONE, REG_NONE, in->loc); + args[i] = materialize_operand( + e, &aux->args[i], class_for_type(e, aux->args[i].type), + aux->args[i].type, REG_NONE, REG_NONE, in->loc); } } + for (u32 i = 0; aux && i < aux->ndst; ++i) { + NativeAllocClass cls = class_for_type(e, aux->dsts[i].type); + real_dsts[i] = loc_from_operand(e, &aux->dsts[i], in->loc); + dsts[i] = real_dsts[i]; + if (dsts[i].kind != NATIVE_LOC_NONE && + dsts[i].kind != NATIVE_LOC_REG) + dsts[i] = temp_loc_acquire(e, aux->dsts[i].type, cls, REG_NONE, + REG_NONE, + in->loc); + } e->target->intrinsic(e->target, aux->kind, dsts, aux->ndst, args, aux->narg); + for (u32 i = 0; aux && i < aux->ndst; ++i) { + if (real_dsts[i].kind == NATIVE_LOC_NONE || + real_dsts[i].kind == NATIVE_LOC_REG) + continue; + write_operand(e, &aux->dsts[i], dsts[i], + mem_for_type(e->c, aux->dsts[i].type), in->loc); + } return; } default: @@ -1351,6 +2336,14 @@ static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, } } +static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in, + const CGFuncDesc* fd) { + NativeEmitTempScope scope; + temp_scope_begin(e, &scope, in); + emit_inst_body(e, block, order_index, in, fd); + temp_scope_end(e, &scope); +} + static int native_emit_terminates(const Inst* in) { if (!in) return 0; switch ((IROp)in->op) { @@ -1376,6 +2369,10 @@ static int native_emit_terminates(const Inst* in) { static void emit_block(NativeEmitCtx* e, u32 block, u32 order_index, const CGFuncDesc* fd) { if (block >= e->f->nblocks) return; + /* No cache fact crosses a control-flow edge. Even a single-predecessor block + * may be entered through a layout-independent branch, so block entry is the + * simple, explicit ownership boundary. */ + frame_cache_clear(e); if (!e->label_placed[block]) { e->label_placed[block] = 1u; e->target->label_place(e->target, @@ -1433,13 +2430,95 @@ static u32 compute_callee_saved_used(NativeEmitCtx* e, u32* used, u32 cap) { nclasses = ri->nclasses < cap ? ri->nclasses : cap; for (u32 i = 0; i < ri->nclasses; ++i) { const NativeAllocClassInfo* ci = &ri->classes[i]; - if (ci->cls < cap) + if (ci->cls < cap) { used[ci->cls] &= native_target_callee_saved_mask(t, (NativeAllocClass)ci->cls); + used[ci->cls] |= e->asm_stage_callee_used[ci->cls]; + } } return nclasses; } +static void compute_emit_live_after(NativeEmitCtx* e) { + OptHardBlockLive* blocks = opt_maybe_build_hard_live(e->f); + e->live_after_cap = e->f->next_inst_id; + if (!e->live_after_cap) return; + e->live_after_by_inst = + arena_zarray(e->f->arena, OptHardRegSet, e->live_after_cap); + for (u32 b = 0; b < e->f->nblocks; ++b) { + Block* bl = &e->f->blocks[b]; + OptHardRegSet live = + opt_hard_live_out_for_block(blocks ? &blocks[b] : NULL); + for (u32 ri = bl->ninsts; ri > 0; --ri) { + Inst* in = &bl->insts[ri - 1u]; + OptRegEffects effects; + OptHardRegSet kills; + if (in->id != INST_ID_NONE && in->id < e->live_after_cap) + e->live_after_by_inst[in->id] = live; + opt_inst_reg_effects(e->f, in, &effects); + opt_reg_effect_kills(&effects, &kills); + opt_hard_live_step(&live, &effects.uses, &kills); + } + } +} + +static int asm_mir_reg_satisfies(NativeEmitCtx* e, const OptOperand* op, + NativeAllocClass cls, Reg fixed, + u32 allowed) { + NativeLoc loc; + if (!op || op->kind != OPK_REG) return 0; + loc = loc_reg(op->type, (NativeAllocClass)op->cls, op->v.reg); + return asm_reg_loc_satisfies(e, loc, cls, fixed, allowed); +} + +/* Dry-run the instruction-local asm allocator before frame finalization. The + * same candidate order and unavailable masks are used by final emission, so + * this pass need only retain the ABI-relevant result: which callee-saved + * registers the staging leases will overwrite. */ +static void plan_asm_staging(NativeEmitCtx* e) { + memset(e->asm_stage_callee_used, 0, sizeof e->asm_stage_callee_used); + for (u32 b = 0; b < e->f->nblocks; ++b) { + Block* bl = &e->f->blocks[b]; + for (u32 i = 0; i < bl->ninsts; ++i) { + Inst* in = &bl->insts[i]; + IRAsmAux* aux; + NativeEmitTempScope scope; + if ((IROp)in->op != IR_ASM_BLOCK) continue; + aux = (IRAsmAux*)in->extra.aux; + if (!aux) continue; + temp_scope_begin(e, &scope, in); + scope.allow_asm_temps = (u8)!aux->has_memory_constraint; + for (u32 k = 0; k < aux->nout; ++k) { + NativeAllocClass cls; + Reg fixed; + u32 allowed; + if (!asm_reg_requirement( + e, aux->out_reg_reqs ? &aux->out_reg_reqs[k] : NULL, &cls, + &fixed, &allowed, in->loc) || + asm_mir_reg_satisfies(e, &aux->out_ops[k], cls, fixed, allowed)) + continue; + (void)temp_acquire_asm(e, cls, fixed, allowed, in->loc); + } + for (u32 k = 0; k < aux->nin; ++k) { + NativeAllocClass cls; + Reg fixed; + u32 allowed; + if (native_asm_match_index(aux->ins[k].str) >= 0) continue; + if (!asm_reg_requirement( + e, aux->in_reg_reqs ? &aux->in_reg_reqs[k] : NULL, &cls, + &fixed, &allowed, in->loc) || + asm_mir_reg_satisfies(e, &aux->in_ops[k], cls, fixed, allowed)) + continue; + (void)temp_acquire_asm(e, cls, fixed, allowed, in->loc); + } + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) + e->asm_stage_callee_used[c] |= + scope.leased[c] & e->f->opt_callee_saved[c]; + temp_scope_end(e, &scope); + } + } +} + /* Plan the complete call frame before any code is emitted, then hand it to the * backend via func_begin_known_frame so the prologue is emitted final. The * optimizer knows everything the frame needs after register allocation and MIR @@ -1468,6 +2547,9 @@ static void plan_frame(NativeEmitCtx* e, const CGFuncDesc* fd) { nclasses = t->reserve_callee_saves ? compute_callee_saved_used(e, used, EMIT_MAX_REG_CLASSES) : 0u; + memset(e->callee_saved_used, 0, sizeof e->callee_saved_used); + for (u32 c = 0; c < nclasses && c < OPT_REG_CLASSES; ++c) + e->callee_saved_used[c] = used[c]; /* Outgoing-arg area = max stack-arg bytes over all calls; also note alloca. */ for (u32 b = 0; b < e->f->nblocks; ++b) { @@ -1487,10 +2569,10 @@ static void plan_frame(NativeEmitCtx* e, const CGFuncDesc* fd) { } else if ((IROp)in->op == IR_ASM_BLOCK) { /* Inline asm may clobber the return-address register or the red zone * opaquely; disqualifies the frame-eliding tiers (see has_asm). Its - * callee-saved register clobbers and hard-register operand pins are - * equally opaque to the operand scan below; count them now so the - * backend can fold them into the saved set (collected into a single Sym - * list in a second pass below). */ + * callee-saved register clobbers and named-register operand + * requirements are equally opaque to the operand scan below; count + * them now so the backend can fold them into the saved set (collected + * into a single Sym list in a second pass below). */ IRAsmAux* aux = (IRAsmAux*)in->extra.aux; has_asm = 1; if (aux) { @@ -1520,10 +2602,10 @@ static void plan_frame(NativeEmitCtx* e, const CGFuncDesc* fd) { } } } - /* Gather the union of every asm block's clobber names and hard-register - * operand pins for known-frame callee-save reservation. Keep the raw names: - * each backend's frame planner uses its clobber parser so diagnostics and - * ABI exclusions match final emission. */ + /* Gather the union of every asm block's clobber names and named-register + * operand requirements for known-frame callee-save reservation. Keep the raw + * names: each backend's frame planner uses its clobber parser so diagnostics + * and ABI exclusions match final emission. */ if (nasm_clob) { u32 n = 0; asm_clobbers = arena_array(e->f->arena, Sym, nasm_clob); @@ -1700,20 +2782,7 @@ void opt_emit_native(Compiler* c, Func* f, NativeTarget* target) { CGFuncDesc fd; if (!f || !target) return; memset(&e, 0, sizeof e); - if (f->mir) { - view = *f; - view.blocks = f->mir->blocks; - view.nblocks = f->mir->nblocks; - view.entry = f->mir->entry; - view.emit_order = f->mir->emit_order; - view.emit_order_n = f->mir->emit_order_n; - view.emit_order_cap = f->mir->emit_order_cap; - view.opt_rewritten = 1; - view.mir = NULL; - e.f = &view; - } else { - e.f = f; - } + e.f = opt_mir_view(f, &view) ? &view : f; e.c = c; e.target = target; metrics_scope_begin(c, "opt.native_emit.setup"); @@ -1722,6 +2791,8 @@ void opt_emit_native(Compiler* c, Func* f, NativeTarget* target) { arena_zarray(e.f->arena, u8, e.f->nblocks ? e.f->nblocks : 1u); for (u32 i = 0; i < e.f->nblocks; ++i) e.labels[i] = MC_LABEL_NONE; fd = semantic_func_desc(&e); + compute_emit_live_after(&e); + plan_asm_staging(&e); metrics_scope_end(c, "opt.native_emit.setup"); metrics_scope_begin(c, "opt.native_emit.func_begin"); diff --git a/src/opt/pass_o2.c b/src/opt/pass_o2.c @@ -781,6 +781,9 @@ static u64 gvn_key_hash(const GvnKey* k) { h = gvn_mix_u64(h, k->ops[i].v.ind.index); h = gvn_mix_u64(h, (u64)(i64)k->ops[i].v.ind.ofs); h = gvn_mix_u64(h, k->ops[i].v.ind.log2_scale); + h = gvn_mix_u64(h, k->ops[i].v.ind.index_ext); + h = gvn_mix_u64(h, k->ops[i].v.ind.base_type); + h = gvn_mix_u64(h, k->ops[i].v.ind.index_type); } else { h = gvn_mix_u64(h, k->ops[i].kind == OPK_REG ? k->ops[i].v.reg : (u64)k->ops[i].v.imm); @@ -795,7 +798,10 @@ static int gvn_operand_key_equal(const GvnOperandKey* a, if (a->kind == OPK_INDIRECT) return a->v.ind.base == b->v.ind.base && a->v.ind.index == b->v.ind.index && a->v.ind.ofs == b->v.ind.ofs && - a->v.ind.log2_scale == b->v.ind.log2_scale; + a->v.ind.log2_scale == b->v.ind.log2_scale && + a->v.ind.index_ext == b->v.ind.index_ext && + a->v.ind.base_type == b->v.ind.base_type && + a->v.ind.index_type == b->v.ind.index_type; if (a->kind == OPK_REG) return a->v.reg == b->v.reg; return a->v.imm == b->v.imm; } @@ -923,12 +929,15 @@ static int gvn_make_addr_operand_key(GvnCtx* ctx, const Operand* op, Val base = gvn_find(ctx, (Val)op->v.ind.base); if (base == VAL_NONE || base >= ctx->f->nvals) return 0; out->v.ind.base = base; + out->v.ind.base_type = ctx->f->val_type[base]; out->v.ind.index = VAL_NONE; if (op->v.ind.index != REG_NONE) { Val index = gvn_find(ctx, (Val)op->v.ind.index); if (index == VAL_NONE || index >= ctx->f->nvals) return 0; out->v.ind.index = index; + out->v.ind.index_type = ctx->f->val_type[index]; out->v.ind.log2_scale = op->v.ind.log2_scale; + out->v.ind.index_ext = op->v.ind.index_ext; } out->v.ind.ofs = op->v.ind.ofs; return 1; @@ -1263,7 +1272,13 @@ static int gvn_operand_key_less(const GvnOperandKey* a, if (a->v.ind.index != b->v.ind.index) return a->v.ind.index < b->v.ind.index; if (a->v.ind.ofs != b->v.ind.ofs) return a->v.ind.ofs < b->v.ind.ofs; - return a->v.ind.log2_scale < b->v.ind.log2_scale; + if (a->v.ind.log2_scale != b->v.ind.log2_scale) + return a->v.ind.log2_scale < b->v.ind.log2_scale; + if (a->v.ind.index_ext != b->v.ind.index_ext) + return a->v.ind.index_ext < b->v.ind.index_ext; + if (a->v.ind.base_type != b->v.ind.base_type) + return a->v.ind.base_type < b->v.ind.base_type; + return a->v.ind.index_type < b->v.ind.index_type; } if (a->kind == OPK_REG) return a->v.reg < b->v.reg; return a->v.imm < b->v.imm; @@ -1325,9 +1340,11 @@ static void gvn_replace_one_use(Func* f, const OptUse* use, Val repl) { break; case OPT_USE_INDIRECT_BASE: use->operand->v.ind.base = (Reg)repl; + use->operand->v.ind.base_type = f->val_type[repl]; break; case OPT_USE_INDIRECT_INDEX: use->operand->v.ind.index = (Reg)repl; + use->operand->v.ind.index_type = f->val_type[repl]; break; case OPT_USE_PHI_INPUT: { IRPhiAux* aux = (IRPhiAux*)in->extra.aux; diff --git a/src/opt/pass_ssa.c b/src/opt/pass_ssa.c @@ -283,24 +283,13 @@ static Val reg_stack_top(RegRenameCtx* ctx, Reg r) { } static void reg_replace_use(RegRenameCtx* ctx, Operand* op) { - if (!op) return; - if (op->kind == OPK_REG) { - Reg r = op->v.reg; - Val v = reg_stack_top(ctx, r); - if (v == VAL_NONE) return; - op->v.reg = (Reg)v; - op->type = ctx->f->val_type[v]; - op->cls = ctx->f->val_cls[v]; - } else if (op->kind == OPK_INDIRECT) { - Reg r = op->v.ind.base; - Val v = reg_stack_top(ctx, r); - if (v != VAL_NONE) op->v.ind.base = (Reg)v; - if (op->v.ind.index != (Reg)REG_NONE) { - Reg ri = op->v.ind.index; - Val vi = reg_stack_top(ctx, ri); - if (vi != VAL_NONE) op->v.ind.index = (Reg)vi; - } - } + if (!op || op->kind != OPK_REG) return; + Reg r = op->v.reg; + Val v = reg_stack_top(ctx, r); + if (v == VAL_NONE) return; + op->v.reg = (Reg)v; + op->type = ctx->f->val_type[v]; + op->cls = ctx->f->val_cls[v]; } static Val reg_define_operand(RegRenameCtx* ctx, u32 b, u32 i, Inst* in, diff --git a/src/opt/reg_effects.c b/src/opt/reg_effects.c @@ -0,0 +1,152 @@ +#include <string.h> + +#include "opt/opt_internal.h" + +static void effect_add(OptHardRegSet* set, u8 cls, Reg reg) { + if (!set || cls >= OPT_REG_CLASSES || reg >= OPT_MAX_HARD_REGS) return; + set->cls[cls] |= 1u << reg; +} + +static void effect_add_use(OptRegEffects* effects, u8 cls, Reg reg) { + if (!effects || cls >= OPT_REG_CLASSES || reg >= OPT_MAX_HARD_REGS) return; + effect_add(&effects->uses, cls, reg); + if (effects->use_count[cls][reg] < 2u) + ++effects->use_count[cls][reg]; +} + +static void effect_use_operand(OptRegEffects* effects, const Operand* op) { + if (!effects || !op) return; + if (op->kind == OPK_REG) { + effect_add_use(effects, op->cls, op->v.reg); + } else if (op->kind == OPK_INDIRECT) { + if (op->v.ind.base_kind == OPT_INDIRECT_REG) + effect_add_use(effects, RC_INT, op->v.ind.base); + if (op->v.ind.index_kind == OPT_INDIRECT_REG && + op->v.ind.index != (Reg)REG_NONE) + effect_add_use(effects, RC_INT, op->v.ind.index); + } +} + +static void effect_def_operand(OptRegEffects* effects, const Operand* op) { + if (effects && op && op->kind == OPK_REG) + effect_add(&effects->defs, op->cls, op->v.reg); +} + +static void effect_use_abivalue(OptRegEffects* effects, const CGABIValue* v) { + if (!v) return; + effect_use_operand(effects, &v->storage); + for (u32 i = 0; i < v->nparts; ++i) + effect_use_operand(effects, &v->parts[i].op); +} + +static void effect_def_abivalue(OptRegEffects* effects, const CGABIValue* v) { + if (!v) return; + effect_def_operand(effects, &v->storage); + for (u32 i = 0; i < v->nparts; ++i) + effect_def_operand(effects, &v->parts[i].op); +} + +static void effect_add_masks(OptHardRegSet* set, const u32* masks) { + if (!set || !masks) return; + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) set->cls[c] |= masks[c]; +} + +u32 opt_call_clobber_mask_for(Func* f, const Inst* in, u8 cls) { + if (!f || cls >= OPT_REG_CLASSES) return 0; + if (in && (IROp)in->op == IR_CALL) { + const IRCallAux* aux = (const IRCallAux*)in->extra.aux; + if (aux && aux->plan_valid) return aux->plan.clobber_mask[cls]; + } + return f->opt_caller_saved[cls]; +} + +static void effect_explicit_operands(Func* f, const Inst* in, + OptRegEffects* effects) { + for (u32 i = 0; i < in->nopnds; ++i) { + if (opt_inst_operand_is_def(in, i)) + effect_def_operand(effects, &in->opnds[i]); + else + effect_use_operand(effects, &in->opnds[i]); + } + + /* Direct operand roles come from opt_inst_operand_is_def above. Only + * register-bearing auxiliary descriptions need instruction-specific work. */ + switch ((IROp)in->op) { + case IR_CALL: { + const IRCallAux* aux = (const IRCallAux*)in->extra.aux; + if (aux && aux->use_plan_replay) { + effect_use_operand(effects, &aux->plan.callee); + for (u32 i = 0; i < aux->plan.nargs; ++i) { + effect_use_operand(effects, &aux->plan.args[i].src); + if (aux->plan.args[i].dst_kind == CG_CALL_PLAN_REG) + effect_add(&effects->defs, aux->plan.args[i].cls, + aux->plan.args[i].dst_reg); + } + for (u32 i = 0; i < aux->plan.nrets; ++i) { + effect_add(&effects->defs, aux->plan.rets[i].cls, + aux->plan.rets[i].src_reg); + effect_def_operand(effects, &aux->plan.rets[i].dst); + } + } else if (aux) { + effect_use_operand(effects, &aux->desc.callee); + for (u32 i = 0; i < aux->desc.nargs; ++i) + effect_use_abivalue(effects, &aux->desc.args[i]); + effect_def_abivalue(effects, &aux->desc.ret); + } + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) + effects->clobbers.cls[c] |= + opt_call_clobber_mask_for(f, in, (u8)c); + break; + } + case IR_RET: { + const IRRetAux* aux = (const IRRetAux*)in->extra.aux; + if (aux && aux->present) effect_use_abivalue(effects, &aux->val); + break; + } + case IR_ASM_BLOCK: { + const IRAsmAux* aux = (const IRAsmAux*)in->extra.aux; + if (!aux) break; + for (u32 i = 0; i < aux->nin; ++i) + effect_use_operand(effects, &aux->in_ops[i]); + for (u32 i = 0; i < aux->nout; ++i) + effect_def_operand(effects, &aux->out_ops[i]); + effect_add_masks(&effects->clobbers, aux->clobber_mask); + break; + } + case IR_INTRINSIC: { + const IRIntrinAux* aux = (const IRIntrinAux*)in->extra.aux; + if (!aux) break; + for (u32 i = 0; i < aux->narg; ++i) + effect_use_operand(effects, &aux->args[i]); + for (u32 i = 0; i < aux->ndst; ++i) + effect_def_operand(effects, &aux->dsts[i]); + break; + } + default: + break; + } +} + +static void effect_add_machine_clobbers(Func* f, const Inst* in, + OptRegEffects* effects) { + if (!f->inst_clobbers || in->id == INST_ID_NONE || + in->id >= f->inst_clobbers_cap) + return; + effect_add_masks(&effects->clobbers, f->inst_clobbers[in->id]); +} + +void opt_inst_reg_effects(Func* f, const Inst* in, OptRegEffects* effects) { + if (!effects) return; + memset(effects, 0, sizeof *effects); + if (!f || !in) return; + effect_explicit_operands(f, in, effects); + effect_add_machine_clobbers(f, in, effects); +} + +void opt_reg_effect_kills(const OptRegEffects* effects, OptHardRegSet* kills) { + if (!kills) return; + memset(kills, 0, sizeof *kills); + if (!effects) return; + for (u32 c = 0; c < OPT_REG_CLASSES; ++c) + kills->cls[c] = effects->defs.cls[c] | effects->clobbers.cls[c]; +} diff --git a/test/arch/arm32_inline_test.c b/test/arch/arm32_inline_test.c @@ -3,6 +3,8 @@ #include <stdint.h> #include <string.h> +#include <kit/disasm.h> + #include "inline_public_test.h" static void arm32_body(KitCompiler* c, KitCg* cg, KitCgTypeId i64_ty) { @@ -22,6 +24,80 @@ 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); } +/* 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, + KitCgTypeId i64_ty) { + KitCgTypeId i32_ty = kit_cg_type_builtin(c, KIT_CG_BUILTIN_I32); + KitCgAsmOperand ins[5]; + KitCgLocal locals[5]; + KitCgLocalAttrs attrs; + KitCgMemAccess mem; + uint32_t i; + (void)i64_ty; + + memset(&attrs, 0, sizeof attrs); + memset(&mem, 0, sizeof mem); + mem.type = i32_ty; + mem.align = kit_cg_type_align(c, i32_ty); + for (i = 0; i < 5u; ++i) { + locals[i] = kit_cg_local(cg, i32_ty, attrs); + kit_cg_push_int(cg, i + 1u, i32_ty); + kit_cg_local_write(cg, locals[i], mem); + } + for (i = 0; i < 5u; ++i) { + kit_cg_local_read(cg, locals[i], mem); + ins[i] = it_asm_op(c, "r", NULL, i32_ty, KIT_CG_ASM_IN); + } + it_inline_asm(c, cg, "", NULL, 0, ins, 5, NULL, 0); +} + +static int arm32_is_r4_mem(const KitInsn* insn, const char* mnemonic) { + size_t n = strlen(mnemonic); + return insn->mnemonic.s && insn->mnemonic.len >= n && + memcmp(insn->mnemonic.s, mnemonic, n) == 0 && insn->operands.s && + insn->operands.len >= 2u && insn->operands.s[0] == 'r' && + insn->operands.s[1] == '4'; +} + +static int arm32_has_generic_r4_preservation(InlineTestEnv* env, + const InlineText* text) { + KitTargetOptions opts; + KitTarget* target = NULL; + KitDisasmContext dc; + KitDisasmIter* it = NULL; + KitInsn insn; + uint32_t loads_after_save = 0; + int saw_save = 0; + int ok = 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 (!saw_save && arm32_is_r4_mem(&insn, "str")) { + saw_save = 1; + } else if (saw_save && arm32_is_r4_mem(&insn, "ldr")) { + loads_after_save++; + } + } + /* The first load stages operand five; the second restores caller r4. */ + ok = saw_save && loads_after_save >= 2u; + +done: + if (it) kit_disasm_iter_free(it); + kit_target_free(target); + return ok; +} + static int arm32_emit_file_asm(InlineTestEnv* env, const char* src, InlineText* text) { KitCompiler* c = NULL; @@ -199,6 +275,17 @@ int main(void) { IT_EXPECT(&env, arm32_check_thumb_func(&env), "arm32 .thumb_func symbol regression failed: %s", env.last_diag); + IT_EXPECT(&env, + it_emit_text(&env, KIT_ARCH_ARM_32, + "arm32_generic_callee_saved_operand", + arm32_generic_callee_saved_operand, &text), + "failed to emit generic callee-saved asm operand: %s", + env.last_diag); + if (text.data) + IT_EXPECT(&env, arm32_has_generic_r4_preservation(&env, &text), + "generic r4 asm operand was not saved and restored"); + it_text_close(&text); + if (env.fails) { fprintf(stderr, "%d failure(s)\n", env.fails); return 1; diff --git a/test/arch/x64_inline_test.c b/test/arch/x64_inline_test.c @@ -5,6 +5,37 @@ #include "inline_public_test.h" +/* Collect the architectural XMM registers named by `ucomisd %xmmN,%xmmN`. + * The inline case below emits one such non-destructive compare per input. A + * full mask therefore proves that all sixteen simultaneous constraints were + * bound independently; merely compiling an empty template cannot detect an + * accidental many-to-one placement. */ +static uint32_t x64_self_ucomisd_bindings(const uint8_t* p, size_t n, + uint32_t* count_out) { + uint32_t mask = 0; + uint32_t count = 0; + for (size_t i = 0; i + 3u < n; ++i) { + size_t op = i; + uint8_t rex = 0; + uint8_t modrm; + uint32_t lhs; + uint32_t rhs; + if (p[op++] != 0x66u) continue; + if (op < n && p[op] >= 0x40u && p[op] <= 0x4fu) rex = p[op++]; + if (op + 2u >= n || p[op] != 0x0fu || p[op + 1u] != 0x2eu) + continue; + modrm = p[op + 2u]; + if ((modrm & 0xc0u) != 0xc0u) continue; + lhs = ((modrm >> 3u) & 7u) | ((rex & 4u) ? 8u : 0u); + rhs = (modrm & 7u) | ((rex & 1u) ? 8u : 0u); + if (lhs != rhs) continue; + mask |= 1u << lhs; + ++count; + } + if (count_out) *count_out = count; + return mask; +} + static void x64_body(KitCompiler* c, KitCg* cg, KitCgTypeId i64_ty) { KitCgAsmOperand imm; (void)i64_ty; @@ -60,6 +91,14 @@ static void x64_push_local_inputs(KitCompiler* c, KitCg* cg, KitCgTypeId ty, } static void x64_optimized_stage(KitCompiler* c, KitCg* cg, KitCgTypeId i64_ty) { + static const char xmm_use_template[] = + "ucomisd %0, %0 ; ucomisd %1, %1 ; ucomisd %2, %2 ; " + "ucomisd %3, %3 ; ucomisd %4, %4 ; ucomisd %5, %5 ; " + "ucomisd %6, %6 ; ucomisd %7, %7 ; ucomisd %8, %8 ; " + "ucomisd %9, %9 ; ucomisd %10, %10 ; ucomisd %11, %11 ; " + "ucomisd %12, %12 ; ucomisd %13, %13 ; ucomisd %14, %14 ; " + "ucomisd %15, %15"; + static const char* const cc_clobber[] = {"cc"}; KitCgTypeId f64_ty = kit_cg_type_builtin(c, KIT_CG_BUILTIN_F64); KitCgAsmOperand ins[18]; KitCgAsmOperand outs[6]; @@ -70,8 +109,12 @@ static void x64_optimized_stage(KitCompiler* c, KitCg* cg, KitCgTypeId i64_ty) { x64_push_local_inputs(c, cg, i64_ty, "q", ins, 14); it_inline_asm(c, cg, "", NULL, 0, ins, 14, NULL, 0); - x64_push_local_inputs(c, cg, f64_ty, "x", ins, 18); - it_inline_asm(c, cg, "", NULL, 0, ins, 18, NULL, 0); + /* Sixteen is the architectural XMM register count. The old spill-to-scratch + * rewrite silently aliased simultaneous operands. Reference every input in + * the emitted template so the byte-level check can prove that each binding + * is distinct; an empty template only proved that planning did not panic. */ + x64_push_local_inputs(c, cg, f64_ty, "x", ins, 16); + it_inline_asm(c, cg, xmm_use_template, NULL, 0, ins, 16, cc_clobber, 1); for (uint32_t i = 0; i < 6u; ++i) outs[i] = it_asm_op(c, i & 1u ? "=x" : "=r", NULL, @@ -127,10 +170,20 @@ int main(void) { { InlineText opt; + uint32_t xmm_compare_count = 0; + uint32_t xmm_bindings = 0; IT_EXPECT(&env, it_emit_text_opt(&env, KIT_ARCH_X86_64, "x64_optimized_stage", x64_optimized_stage, 1, &opt), "failed to emit optimized x64 inline-asm staging case"); + if (opt.data) + xmm_bindings = x64_self_ucomisd_bindings( + opt.data, opt.len, &xmm_compare_count); + IT_EXPECT(&env, + xmm_compare_count == 16u && xmm_bindings == 0xffffu, + "sixteen simultaneous XMM inputs were not independently bound " + "(compares=%u mask=%#x)", + xmm_compare_count, xmm_bindings); it_text_close(&opt); } diff --git a/test/cg/native_direct_target_test.c b/test/cg/native_direct_target_test.c @@ -79,21 +79,21 @@ typedef struct MockNative { static const Reg mock_int_scratch[] = {1u, 2u, 3u}; static const Reg mock_fp_scratch[] = {4u, 5u}; -static const Reg mock_int_allocable[] = {1u, 2u, 3u, 6u}; -static const Reg mock_fp_allocable[] = {4u, 5u, 6u}; +static const Reg mock_ndt_int_allocable[] = {1u, 2u, 3u, 6u}; +static const Reg mock_ndt_fp_allocable[] = {4u, 5u, 6u}; static const NativeAllocClassInfo mock_classes[] = { {.cls = NATIVE_REG_INT, - .allocable = mock_int_allocable, - .nallocable = 4, + .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}, {.cls = NATIVE_REG_FP, - .allocable = mock_fp_allocable, - .nallocable = 3, + .ndt_allocable = mock_ndt_fp_allocable, + .ndt_allocable_count = 3, .scratch = mock_fp_scratch, .nscratch = 2, .caller_saved_mask = (1u << 4) | (1u << 5), @@ -419,11 +419,12 @@ static void test_frame_locals_scratch_storeback_and_branches(void) { EXPECT(count_event(&native, EV_LOAD_IMM) == 3, "two explicit immediates plus cmp imm materialization expected"); /* With the local register cache, a/b/sum live in registers across the - * straight-line compute run: the binop reloads neither operand. They are - * spilled at the branch flush (EV_STORE), and sum is reloaded only after that - * flush — once for the cmp_branch and once for the return. */ - EXPECT(count_event(&native, EV_LOAD) == 2, - "sum is reloaded from its home for the cmp_branch and the return"); + * straight-line compute run. cmp_branch pins sum before flushing, so the + * compare needs no reload; only the later return reloads it from its home. */ + EXPECT(count_event(&native, EV_LOAD) == 1, + "sum is reloaded from its home only for the return " + "(loads=%d)", + count_event(&native, EV_LOAD)); EXPECT(count_event(&native, EV_STORE) >= 3, "results should store back to frame homes"); EXPECT(count_event(&native, EV_BINOP) == 1, "expected one native binop"); @@ -472,8 +473,9 @@ static void test_call_barrier_storeback_and_max_outgoing(void) { "call planning should track max outgoing stack size"); EXPECT(count_event(&native, EV_PLAN_CALL) == 1, "expected one call plan"); EXPECT(count_event(&native, EV_EMIT_CALL) == 1, "expected one emitted call"); - EXPECT(count_event(&native, EV_STORE) >= 3, - "arg setup and result should store through native writes"); + EXPECT(count_event(&native, EV_STORE) == 2, + "selective call setup should emit only its required stores (stores=%d)", + count_event(&native, EV_STORE)); t->func_end(t); tc_fini(&tc); } @@ -506,8 +508,10 @@ static void test_b_cached_pointer_base_not_reloaded(void) { EXPECT(li >= 0 && native.events[li].a == NATIVE_ADDR_BASE_REG, "dereference addresses the live cache register for p"); } - EXPECT(count_event(&native, EV_STORE) == 1, - "p is not spilled; only the load result is written to d's home"); + EXPECT(count_event(&native, EV_STORE) == 0, + "p stays live and the load result remains in the write-back cache " + "(stores=%d)", + count_event(&native, EV_STORE)); t->func_end(t); tc_fini(&tc); } @@ -549,9 +553,9 @@ static void test_b_cache_survives_store(void) { tc_fini(&tc); } -/* A volatile access must still flush the cache (it may observe memory) and emit - * the volatile barrier — the escape argument does not apply. */ -static void test_b_volatile_load_flushes(void) { +/* A volatile access emits the ordering barrier but does not flush unrelated, + * non-escaped locals: foreign memory cannot alias their private frame homes. */ +static void test_b_volatile_load_preserves_private_cache(void) { TestCtx tc; MockNative native; CgTarget* t; @@ -571,8 +575,10 @@ static void test_b_volatile_load_flushes(void) { EXPECT((native.barrier_flags & NATIVE_DIRECT_BARRIER_VOLATILE) != 0, "volatile access emits a volatile barrier"); - EXPECT(count_event(&native, EV_STORE) >= 2, - "volatile flush spills the dirty cached locals (p and a)"); + EXPECT(count_event(&native, EV_STORE) == 0, + "volatile foreign-memory access preserves private cached locals " + "(stores=%d)", + count_event(&native, EV_STORE)); t->func_end(t); tc_fini(&tc); } @@ -616,7 +622,7 @@ int main(void) { test_call_barrier_storeback_and_max_outgoing(); test_b_cached_pointer_base_not_reloaded(); test_b_cache_survives_store(); - test_b_volatile_load_flushes(); + test_b_volatile_load_preserves_private_cache(); test_b_call_still_flushes(); if (g_u.fails) { fprintf(stderr, "%d/%d checks failed\n", g_u.fails, g_u.checks); diff --git a/test/lib/exec_bare.sh b/test/lib/exec_bare.sh @@ -347,7 +347,7 @@ SECTIONS { } EOF "$EXEC_BARE_KIT" as -target riscv64-none-elf \ - -march=rv64imafd_zicsr_zifencei -mabi=lp64 \ + -march=rv64imafd_zicsr_zifencei -mabi=lp64d \ -o "$dir/start.o" "$dir/start.S" 2>"$dir/as.err" } diff --git a/test/macho/kit-roundtrip-macho.c b/test/macho/kit-roundtrip-macho.c @@ -2,12 +2,13 @@ * reader, then re-emit via the public object builder API. Round-trip oracle * for the Mach-O writer. * - * Usage: kit-roundtrip-macho <in.o> <out.o> + * Usage: kit-roundtrip-macho [--require-atom-subordinate] <in.o> <out.o> * * Behavior: kit_obj_open detects and parses the input into a KitObjFile; - * kit_obj_file_builder exposes the already-finalized builder; and - * kit_obj_builder_emit writes the canonical re-emit to out.o. Diagnostics - * go to stderr via the libc heap + stderr diag sink. */ + * kit_obj_file_builder exposes the already-finalized builder; subordinate + * symbols are checked against their owning atoms; and kit_obj_builder_emit + * writes the canonical re-emit to out.o. Diagnostics go to stderr via the + * libc heap + stderr diag sink. */ #include <fcntl.h> #include <kit/core.h> @@ -19,6 +20,8 @@ #include <sys/stat.h> #include <unistd.h> +#include "obj/obj.h" + static void* heap_alloc(KitHeap* h, size_t n, size_t a) { (void)h; (void)a; @@ -98,13 +101,73 @@ static int write_file(const char* path, const uint8_t* data, size_t len) { return 0; } +static int verify_atom_subordinates(KitObjBuilder* ob, int require_one) { + ObjSymIter* it = obj_symiter_new(ob); + ObjSymEntry e; + unsigned count = 0; + int failed = 0; + if (!it) { + fprintf(stderr, "error: cannot inspect Mach-O symbols\n"); + return -1; + } + while (obj_symiter_next(it, &e)) { + const ObjSym* sym = e.sym; + const ObjAtom* atom; + ObjAtomId aid; + if (!sym || sym->removed || !sym->atom_subordinate) continue; + ++count; + if (sym->section_id == OBJ_SEC_NONE || sym->kind == SK_UNDEF || + sym->kind == SK_COMMON || sym->kind == SK_ABS) { + fprintf(stderr, + "error: atom-subordinate symbol %u is not a section " + "definition\n", + (unsigned)e.id); + failed = 1; + continue; + } + aid = obj_atom_find_symbol(ob, e.id); + atom = obj_atom_get(ob, aid); + if (!atom) { + fprintf(stderr, + "error: atom-subordinate symbol %u has no owning atom\n", + (unsigned)e.id); + failed = 1; + } + for (aid = 1; aid < obj_atom_count(ob); ++aid) { + atom = obj_atom_get(ob, aid); + if (atom && !atom->removed && atom->signature == e.id) { + fprintf(stderr, + "error: atom-subordinate symbol %u was reconstructed as an " + "independent atom\n", + (unsigned)e.id); + failed = 1; + break; + } + } + } + obj_symiter_free(it); + if (require_one && count == 0) { + fprintf(stderr, "error: no atom-subordinate symbol survived roundtrip\n"); + failed = 1; + } + return failed ? -1 : 0; +} + int main(int argc, char** argv) { - if (argc != 3) { - fprintf(stderr, "usage: kit-roundtrip-macho <in.o> <out.o>\n"); + int require_subordinate = 0; + int argi = 1; + if (argc > 1 && strcmp(argv[1], "--require-atom-subordinate") == 0) { + require_subordinate = 1; + ++argi; + } + if (argc - argi != 2) { + fprintf(stderr, + "usage: kit-roundtrip-macho [--require-atom-subordinate] " + "<in.o> <out.o>\n"); return 2; } - const char* in_path = argv[1]; - const char* out_path = argv[2]; + const char* in_path = argv[argi]; + const char* out_path = argv[argi + 1]; uint8_t* in_data = NULL; size_t in_len = 0; @@ -144,6 +207,11 @@ int main(int argc, char** argv) { free(in_data); return 1; } + if (verify_atom_subordinates(ob, require_subordinate) != 0) { + kit_obj_free(obj); + free(in_data); + return 1; + } KitWriter* w = NULL; if (kit_writer_mem(&g_heap, &w) != KIT_OK || !w) { diff --git a/test/opt/aa64_apple_varargs.sh b/test/opt/aa64_apple_varargs.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# AArch64 call-boundary regression: +# - Apple unnamed aggregate stack slots keep semantic bytes separate from +# their rounded physical carriers (Six: 6 in 8, Twelve: 12 in 16). +# - an indirect target initially in x0 survives exact-width stack-argument +# marshalling, which owns the backend-private x16/x17 pair. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +KIT="${KIT:-$ROOT/build/kit}" +WORK="$ROOT/build/test/opt/aa64_apple_varargs" +SRC="$WORK/fixture.c" +mkdir -p "$WORK" + +cat > "$SRC" <<'EOF' +typedef struct Six { unsigned char b[6]; } Six; +typedef struct Twelve { unsigned char b[12]; } Twelve; + +__attribute__((noinline)) void variadic_sink(int tag, ...) { + volatile int seen = tag; + (void)seen; +} + +__attribute__((noinline)) void pass_apple_varargs(Six six, Twelve twelve) { + variadic_sink(7, six, twelve); +} + +typedef int (*Indirect)(int, int, int, int, int, int, int, int, Six); + +__attribute__((noinline)) int call_indirect(Indirect fn, Six six) { + return fn(1, 2, 3, 4, 5, 6, 7, 8, six); +} + +__attribute__((noinline)) static int check_indirect( + int a, int b, int c, int d, int e, int f, int g, int h, Six six) { + return a == 1 && b == 2 && c == 3 && d == 4 && e == 5 && f == 6 && + g == 7 && h == 8 && six.b[0] == 9 && six.b[1] == 10 && + six.b[2] == 11 && six.b[3] == 12 && six.b[4] == 13 && + six.b[5] == 14; +} + +int test_main(void) { + Six six = {{9, 10, 11, 12, 13, 14}}; + return call_indirect(check_indirect, six) ? 0 : 1; +} +EOF + +fail() { + printf 'aa64 Apple-varargs regression FAILED: %s\n' "$1" >&2 + if [ -n "${2:-}" ] && [ -f "$2" ]; then + sed 's/^/ | /' "$2" >&2 + fi + exit 1 +} + +slice_fn() { + local name="$1" input="$2" output="$3" + awk -v name="$name" ' + $0 ~ "<_?" name ">:" { in_fn = 1; next } + /^[0-9a-f]+ <[^>]+>:/ { in_fn = 0 } + in_fn { print } + ' "$input" > "$output" +} + +for opt in O0 O1; do + obj="$WORK/apple.$opt.o" + dis="$WORK/apple.$opt.dis" + body="$WORK/apple.$opt.pass.dis" + "$KIT" build-obj -target aarch64-apple-darwin "-$opt" -c "$SRC" -o "$obj" \ + > "$WORK/apple.$opt.compile.out" 2>&1 + "$KIT" objdump -d "$obj" > "$dis" 2>&1 + slice_fn pass_apple_varargs "$dis" "$body" + [ -s "$body" ] || fail "$opt pass_apple_varargs missing" "$dis" + + # Six is the <=8 control: it is read at six bytes, zero-extended, and the + # complete 8-byte carrier is stored. Twelve occupies the following 16-byte + # carrier, but only its live 8+4 bytes may be read or written. + grep -Eq 'str[[:space:]]+x[0-9]+, \[sp\]' "$body" || + fail "$opt missing zero-padded Six carrier store" "$body" + grep -Eq 'str[[:space:]]+x[0-9]+, \[sp, #8\]' "$body" || + fail "$opt missing first eight bytes of Twelve" "$body" + grep -Eq 'str[[:space:]]+w[0-9]+, \[sp, #16\]' "$body" || + fail "$opt missing four-byte Twelve tail" "$body" + if grep -Eq 'str[[:space:]]+x[0-9]+, \[sp, #16\]' "$body"; then + fail "$opt widened Twelve tail into carrier padding" "$body" + fi + xloads="$(grep -Ec '(ldr|ldur)[[:space:]]+x[0-9]+, \[x29' "$body" || true)" + [ "$xloads" -eq 1 ] || + fail "$opt expected one 8-byte Twelve source load, got $xloads" "$body" +done + +# O1 keeps fn in x0. The target must be saved before the Six stack writes use +# x16/x17, then moved into x16 only after those writes are complete. +call_body="$WORK/apple.O1.call.dis" +slice_fn call_indirect "$WORK/apple.O1.dis" "$call_body" +[ -s "$call_body" ] || fail 'O1 call_indirect missing' "$WORK/apple.O1.dis" +stash_line="$(grep -n 'mov[[:space:]]\+x30, x0' "$call_body" | head -n1 | cut -d: -f1 || true)" +stack_line="$(grep -n 'str[[:space:]]\+w[0-9]\+, \[sp\]' "$call_body" | head -n1 | cut -d: -f1 || true)" +reload_line="$(grep -n 'mov[[:space:]]\+x16, x30' "$call_body" | head -n1 | cut -d: -f1 || true)" +call_line="$(grep -n 'blr[[:space:]]\+x16' "$call_body" | head -n1 | cut -d: -f1 || true)" +if [ -z "$stash_line" ] || [ -z "$stack_line" ] || [ -z "$reload_line" ] || + [ -z "$call_line" ] || [ "$stash_line" -ge "$stack_line" ] || + [ "$stack_line" -ge "$reload_line" ] || [ "$reload_line" -ge "$call_line" ]; then + fail 'indirect target lifetime is not stash -> stack args -> reload -> blr' \ + "$call_body" +fi + +# Execute the indirect-call oracle when this host can run AArch64 natively. +case "$(uname -m 2>/dev/null)" in + arm64|aarch64) + if "$KIT" run -O1 -e test_main "$SRC" > "$WORK/native.run.out" 2>&1; then + rc=0 + else + rc=$? + fi + [ "$rc" -eq 0 ] || fail "native indirect call exited $rc" "$WORK/native.run.out" + ;; +esac + +printf 'aa64-apple-varargs: ok\n' diff --git a/test/opt/arm32_emit_temp_pressure.c b/test/opt/arm32_emit_temp_pressure.c @@ -0,0 +1,15 @@ +/* Keep many scalar values live while emitting three-operand arithmetic. ARM32 + * allocates the values in r4-r11; the native emitter must satisfy spill-heavy + * operands from its explicit r0-r3 temp bank without borrowing IP/LR or + * exposing those temporaries in MIR. */ +long arm32_emit_temp_pressure(long s) { + long a = s + 1, b = s + 2, c = s + 3, d = s + 4; + long e = s + 5, f = s + 6, g = s + 7, h = s + 8; + long i = s + 9, j = s + 10, k = s + 11, l = s + 12; + long m = s + 13, n = s + 14, o = s + 15, p = s + 16; + long products = a * b + c * d + e * f + g * h + i * j + k * l + m * n + o * p; + long differences = + a - b + c - d + e - f + g - h + i - j + k - l + m - n + o - p; + long bits = a ^ b ^ c ^ d ^ e ^ f ^ g ^ h ^ i ^ j ^ k ^ l ^ m ^ n ^ o ^ p; + return products + differences + bits + a + p; +} diff --git a/test/opt/arm32_emit_temp_pressure.sh b/test/opt/arm32_emit_temp_pressure.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +KIT="${KIT:-$ROOT/build/kit}" +WORK="$ROOT/build/test/opt/arm32_emit_temp_pressure" +SRC="$ROOT/test/opt/arm32_emit_temp_pressure.c" +mkdir -p "$WORK" + +if ! "$KIT" cc -O1 -target arm-none-eabi -mcpu=cortex-m3 \ + -mfloat-abi=soft -ffreestanding -std=c11 -c "$SRC" \ + -o "$WORK/pressure.o" > "$WORK/compile.out" 2>&1; then + sed 's/^/ | /' "$WORK/compile.out" >&2 + exit 1 +fi + +if [ ! -s "$WORK/pressure.o" ]; then + printf 'arm32_emit_temp_pressure: compiler produced no object\n' >&2 + exit 1 +fi +printf 'arm32_emit_temp_pressure: OK\n' diff --git a/test/opt/combine_cse_test.c b/test/opt/combine_cse_test.c @@ -0,0 +1,517 @@ +#include <stdio.h> +#include <string.h> + +#include "lib/kit_unit.h" +#include "opt/opt_internal.h" + +typedef struct CseFixture { + KitCompiler* kc; + Func* f; + u32 block; + KitCgTypeId i8_type; + KitCgTypeId i32_type; + KitCgTypeId i64_type; +} CseFixture; + +static Operand reg_op(Reg reg, KitCgTypeId type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_REG; + op.cls = RC_INT; + op.type = type; + op.v.reg = reg; + return op; +} + +static Operand global_op(ObjSymId sym, KitCgTypeId type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_GLOBAL; + op.cls = RC_INT; + op.type = type; + op.v.global.sym = sym; + return op; +} + +static Operand stack_op(FrameSlot slot, KitCgTypeId type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_STACK; + op.cls = RC_INT; + op.type = type; + op.v.frame_slot = slot; + return op; +} + +static int fixture_init(KitUnit* u, CseFixture* fx) { + CGFuncDesc desc; + memset(fx, 0, sizeof *fx); + if (kit_unit_compiler_new( + u, kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &fx->kc) != KIT_OK || + !fx->kc) + return 0; + + fx->i8_type = kit_cg_type_builtin(fx->kc, KIT_CG_BUILTIN_I8); + fx->i32_type = kit_cg_type_builtin(fx->kc, KIT_CG_BUILTIN_I32); + fx->i64_type = kit_cg_type_builtin(fx->kc, KIT_CG_BUILTIN_I64); + memset(&desc, 0, sizeof desc); + desc.fn_type = fx->i64_type; + desc.result_type = fx->i64_type; + fx->f = ir_func_new((Compiler*)fx->kc, &desc); + if (!fx->f) return 0; + fx->block = ir_block_new(fx->f); + fx->f->entry = fx->block; + return 1; +} + +static Inst* emit_load(CseFixture* fx, Reg dst, KitCgTypeId result_type, + MemAccess mem) { + Inst* in = ir_emit(fx->f, fx->block, IR_LOAD); + in->type = result_type; + in->nopnds = 2; + in->opnds = arena_array(fx->f->arena, Operand, 2); + in->opnds[0] = reg_op(dst, result_type); + in->opnds[1] = global_op(1u, mem.type); + in->extra.mem = mem; + return in; +} + +static Inst* emit_add(CseFixture* fx, Reg dst, KitCgTypeId result_type, + KitCgTypeId lhs_type, KitCgTypeId rhs_type, + u8 rhs_shift) { + Inst* in = ir_emit(fx->f, fx->block, IR_BINOP); + in->type = result_type; + in->nopnds = 3; + in->opnds = arena_array(fx->f->arena, Operand, 3); + in->opnds[0] = reg_op(dst, result_type); + in->opnds[1] = reg_op(4u, lhs_type); + in->opnds[2] = reg_op(5u, rhs_type); + in->opnds[2].shift = rhs_shift; + in->extra.imm = BO_IADD; + return in; +} + +static Inst* emit_store(CseFixture* fx, Reg src, KitCgTypeId type, u32 size) { + Inst* in = ir_emit(fx->f, fx->block, IR_STORE); + in->nopnds = 2; + in->opnds = arena_array(fx->f->arena, Operand, 2); + in->opnds[0] = global_op(2u, type); + in->opnds[1] = reg_op(src, type); + in->extra.mem.type = type; + in->extra.mem.size = size; + in->extra.mem.align = size; + in->extra.mem.alias.kind = ALIAS_GLOBAL; + in->extra.mem.alias.v.global = 2u; + return in; +} + +static FrameSlot add_spill_slot(CseFixture* fx, KitCgTypeId type, u32 size, + u32 align) { + FrameSlotDesc desc; + memset(&desc, 0, sizeof desc); + desc.type = type; + desc.size = size; + desc.align = align; + desc.kind = FS_SPILL; + return ir_frame_slot_new(fx->f, &desc); +} + +static Inst* emit_stack_copy(CseFixture* fx, Operand dst, Operand src) { + Inst* in = ir_emit(fx->f, fx->block, IR_COPY); + in->type = dst.type; + in->nopnds = 2u; + in->opnds = arena_array(fx->f->arena, Operand, 2u); + in->opnds[0] = dst; + in->opnds[1] = src; + return in; +} + +static Inst* find_inst(CseFixture* fx, InstId id) { + Block* block = &fx->f->blocks[fx->block]; + for (u32 i = 0; i < block->ninsts; ++i) + if (block->insts[i].id == id) return &block->insts[i]; + return NULL; +} + +static u32 count_op(const CseFixture* fx, IROp op) { + const Block* block = &fx->f->blocks[fx->block]; + u32 count = 0; + for (u32 i = 0; i < block->ninsts; ++i) + if ((IROp)block->insts[i].op == op) ++count; + return count; +} + +static int is_copy_between(const Inst* in, Reg dst, Reg src, + KitCgTypeId type) { + return in && (IROp)in->op == IR_COPY && in->flags == 0u && + in->type == type && in->nopnds == 2u && in->opnds != NULL && + in->opnds[0].kind == OPK_REG && in->opnds[0].cls == RC_INT && + in->opnds[0].type == type && in->opnds[0].v.reg == dst && + in->opnds[1].kind == OPK_REG && in->opnds[1].cls == RC_INT && + in->opnds[1].type == type && in->opnds[1].v.reg == src; +} + +static int has_reg_destination(const Inst* in, IROp op, Reg dst, + KitCgTypeId type) { + return in && (IROp)in->op == op && in->type == type && in->nopnds >= 1u && + in->opnds != NULL && in->opnds[0].kind == OPK_REG && + in->opnds[0].cls == RC_INT && in->opnds[0].type == type && + in->opnds[0].v.reg == dst; +} + +static int is_store_from(const Inst* in, Reg src, KitCgTypeId type) { + return in && (IROp)in->op == IR_STORE && in->nopnds == 2u && + in->opnds != NULL && in->opnds[1].kind == OPK_REG && + in->opnds[1].cls == RC_INT && in->opnds[1].type == type && + in->opnds[1].v.reg == src; +} + +static MemAccess byte_mem(const CseFixture* fx) { + MemAccess mem; + memset(&mem, 0, sizeof mem); + mem.type = fx->i8_type; + mem.size = 1u; + mem.align = 1u; + mem.alias.kind = ALIAS_GLOBAL; + mem.alias.v.global = 1u; + return mem; +} + +static void exact_load_key_is_reused(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for exact load CSE test"); + return; + } + MemAccess mem = byte_mem(&fx); + InstId producer_id = emit_load(&fx, 1u, fx.i32_type, mem)->id; + InstId replacement_id = emit_load(&fx, 2u, fx.i32_type, mem)->id; + /* Make r2 observable. Combine must forward this memory write to the earlier + * loaded value, while any surviving r2 definition remains a valid copy. */ + InstId store_id = emit_store(&fx, 2u, fx.i32_type, 4u)->id; + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_LOAD) == 1u, + "identical loads were not CSE'd"); + { + Inst* producer = find_inst(&fx, producer_id); + Inst* replacement = find_inst(&fx, replacement_id); + Inst* store = find_inst(&fx, store_id); + CU_EXPECT(u, + has_reg_destination(producer, IR_LOAD, 1u, fx.i32_type) && + is_store_from(store, 1u, fx.i32_type), + "load CSE did not preserve r2 as the destination and reuse " + "the value produced in r1 at its consumer"); + CU_EXPECT(u, + !replacement || + is_copy_between(replacement, 2u, 1u, fx.i32_type), + "surviving load-CSE replacement does not define r2 from r1"); + } + kit_compiler_free(fx.kc); +} + +static void load_flags_are_part_of_key(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for load flag CSE test"); + return; + } + MemAccess plain = byte_mem(&fx); + MemAccess sext = plain; + sext.flags |= MF_SEXT_LOAD; + emit_load(&fx, 1u, fx.i32_type, plain); + emit_load(&fx, 2u, fx.i32_type, sext); + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_LOAD) == 2u, + "load CSE discarded MF_SEXT_LOAD semantics"); + kit_compiler_free(fx.kc); +} + +static void load_result_type_is_part_of_key(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for load result CSE test"); + return; + } + MemAccess mem = byte_mem(&fx); + emit_load(&fx, 1u, fx.i32_type, mem); + emit_load(&fx, 2u, fx.i64_type, mem); + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_LOAD) == 2u, + "load CSE reused a differently typed result"); + kit_compiler_free(fx.kc); +} + +static void load_access_type_is_part_of_key(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for access type CSE test"); + return; + } + MemAccess byte = byte_mem(&fx); + MemAccess other = byte; + other.type = fx.i32_type; + emit_load(&fx, 1u, fx.i32_type, byte); + emit_load(&fx, 2u, fx.i32_type, other); + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_LOAD) == 2u, + "load CSE ignored the MemAccess object type"); + kit_compiler_free(fx.kc); +} + +static void load_alignment_is_part_of_key(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for alignment CSE test"); + return; + } + MemAccess byte = byte_mem(&fx); + MemAccess aligned = byte; + aligned.align = 4u; + emit_load(&fx, 1u, fx.i32_type, byte); + emit_load(&fx, 2u, fx.i32_type, aligned); + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_LOAD) == 2u, + "load CSE ignored MemAccess alignment metadata"); + kit_compiler_free(fx.kc); +} + +static void exact_compute_key_is_reused(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for exact compute CSE test"); + return; + } + InstId producer_id = + emit_add(&fx, 1u, fx.i64_type, fx.i64_type, fx.i64_type, 0u)->id; + InstId replacement_id = + emit_add(&fx, 2u, fx.i64_type, fx.i64_type, fx.i64_type, 0u)->id; + InstId store_id = emit_store(&fx, 2u, fx.i64_type, 8u)->id; + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_BINOP) == 1u, + "identical computes were not CSE'd"); + { + Inst* producer = find_inst(&fx, producer_id); + Inst* replacement = find_inst(&fx, replacement_id); + Inst* store = find_inst(&fx, store_id); + CU_EXPECT(u, + has_reg_destination(producer, IR_BINOP, 1u, fx.i64_type) && + is_store_from(store, 1u, fx.i64_type), + "compute CSE did not preserve r2 as the destination and reuse " + "the value produced in r1 at its consumer"); + CU_EXPECT(u, + !replacement || + is_copy_between(replacement, 2u, 1u, fx.i64_type), + "surviving compute-CSE replacement does not define r2 from r1"); + } + kit_compiler_free(fx.kc); +} + +static void location_mir_spill_uses_block_dse(KitUnit* u) { + CseFixture fx; + FrameSlot slot; + + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for spill-use DSE guard"); + return; + } + slot = add_spill_slot(&fx, fx.i64_type, 8u, 8u); + { + InstId first_id = + emit_stack_copy(&fx, stack_op(slot, fx.i64_type), + reg_op(1u, fx.i64_type)) + ->id; + Inst* use = emit_add(&fx, 2u, fx.i64_type, fx.i64_type, fx.i64_type, 0u); + use->opnds[1] = stack_op(slot, fx.i64_type); + emit_stack_copy(&fx, stack_op(slot, fx.i64_type), + reg_op(3u, fx.i64_type)); + opt_combine(fx.f, NULL); + CU_EXPECT(u, find_inst(&fx, first_id) != NULL, + "spill DSE crossed a direct OPK_STACK value use"); + } + kit_compiler_free(fx.kc); + + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, + "compiler allocation failed for indirect spill-use DSE guard"); + return; + } + slot = add_spill_slot(&fx, fx.i64_type, 8u, 8u); + { + InstId first_id = + emit_stack_copy(&fx, stack_op(slot, fx.i64_type), + reg_op(1u, fx.i64_type)) + ->id; + MemAccess mem = byte_mem(&fx); + Inst* load = emit_load(&fx, 2u, fx.i64_type, mem); + memset(&load->opnds[1], 0, sizeof load->opnds[1]); + load->opnds[1].kind = OPK_INDIRECT; + load->opnds[1].cls = RC_INT; + load->opnds[1].type = fx.i64_type; + load->opnds[1].v.ind.base_kind = OPT_INDIRECT_FRAME; + load->opnds[1].v.ind.base = slot; + load->opnds[1].v.ind.base_type = fx.i64_type; + load->opnds[1].v.ind.index_kind = OPT_INDIRECT_REG; + load->opnds[1].v.ind.index = (Reg)REG_NONE; + emit_stack_copy(&fx, stack_op(slot, fx.i64_type), + reg_op(3u, fx.i64_type)); + opt_combine(fx.f, NULL); + CU_EXPECT(u, find_inst(&fx, first_id) != NULL, + "spill DSE crossed an indirect frame-component use"); + } + kit_compiler_free(fx.kc); +} + +static void compute_result_type_is_part_of_key(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for compute result CSE test"); + return; + } + emit_add(&fx, 1u, fx.i32_type, fx.i64_type, fx.i64_type, 0u); + emit_add(&fx, 2u, fx.i64_type, fx.i64_type, fx.i64_type, 0u); + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_BINOP) == 2u, + "compute CSE reused a differently typed destination"); + kit_compiler_free(fx.kc); +} + +static void compute_reg_input_type_is_part_of_key(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for compute input CSE test"); + return; + } + emit_add(&fx, 1u, fx.i64_type, fx.i64_type, fx.i32_type, 0u); + emit_add(&fx, 2u, fx.i64_type, fx.i64_type, fx.i64_type, 0u); + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_BINOP) == 2u, + "compute CSE ignored a register operand's type"); + kit_compiler_free(fx.kc); +} + +static void compute_shift_rider_is_part_of_key(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for compute rider CSE test"); + return; + } + emit_add(&fx, 1u, fx.i64_type, fx.i64_type, fx.i64_type, 0u); + emit_add(&fx, 2u, fx.i64_type, fx.i64_type, fx.i64_type, 2u); + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_BINOP) == 2u, + "compute CSE ignored a shifted-register rider"); + kit_compiler_free(fx.kc); +} + +static void compute_semantic_flags_are_part_of_key(KitUnit* u) { + CseFixture fx; + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for compute flag CSE test"); + return; + } + Inst* portable = emit_add(&fx, 1u, fx.i64_type, fx.i64_type, fx.i64_type, + 0u); + Inst* target = emit_add(&fx, 2u, fx.i64_type, fx.i64_type, fx.i64_type, 0u); + portable->extra.imm = BO_SHL; + target->extra.imm = BO_SHL; + target->flags = CG_IR_INST_TARGET_SHIFT_EDGES; + + opt_combine(fx.f, NULL); + CU_EXPECT(u, count_op(&fx, IR_BINOP) == 2u, + "compute CSE ignored per-instruction semantic flags"); + kit_compiler_free(fx.kc); +} + +static void location_mir_spill_copies_are_compacted(KitUnit* u) { + CseFixture fx; + FrameSlot slot; + Inst* replacement; + InstId replacement_id; + + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for spill-copy forwarding"); + return; + } + slot = add_spill_slot(&fx, fx.i64_type, 8u, 8u); + emit_stack_copy(&fx, stack_op(slot, fx.i64_type), + reg_op(1u, fx.i64_type)); + replacement_id = + emit_stack_copy(&fx, reg_op(2u, fx.i64_type), + stack_op(slot, fx.i64_type)) + ->id; + opt_combine(fx.f, NULL); + replacement = find_inst(&fx, replacement_id); + CU_EXPECT(u, + replacement && + is_copy_between(replacement, 2u, 1u, fx.i64_type), + "location-MIR spill reload was not forwarded from the stored " + "register"); + kit_compiler_free(fx.kc); + + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for spill-copy elision"); + return; + } + slot = add_spill_slot(&fx, fx.i64_type, 8u, 8u); + emit_stack_copy(&fx, stack_op(slot, fx.i64_type), + reg_op(1u, fx.i64_type)); + replacement_id = + emit_stack_copy(&fx, reg_op(1u, fx.i64_type), + stack_op(slot, fx.i64_type)) + ->id; + opt_combine(fx.f, NULL); + CU_EXPECT(u, find_inst(&fx, replacement_id) == NULL, + "location-MIR same-register spill reload survived compaction"); + kit_compiler_free(fx.kc); + + if (!fixture_init(u, &fx)) { + CU_EXPECT(u, 0, "compiler allocation failed for spill overwrite DSE"); + return; + } + slot = add_spill_slot(&fx, fx.i64_type, 8u, 8u); + { + InstId first_id = + emit_stack_copy(&fx, stack_op(slot, fx.i64_type), + reg_op(1u, fx.i64_type)) + ->id; + InstId second_id = + emit_stack_copy(&fx, stack_op(slot, fx.i64_type), + reg_op(2u, fx.i64_type)) + ->id; + opt_combine(fx.f, NULL); + CU_EXPECT(u, + find_inst(&fx, first_id) == NULL && + find_inst(&fx, second_id) != NULL, + "location-MIR spill overwrite did not retire the dead store"); + } + kit_compiler_free(fx.kc); +} + +int main(void) { + KitUnit u; + kit_unit_init(&u); + exact_load_key_is_reused(&u); + load_flags_are_part_of_key(&u); + load_result_type_is_part_of_key(&u); + load_access_type_is_part_of_key(&u); + load_alignment_is_part_of_key(&u); + exact_compute_key_is_reused(&u); + compute_result_type_is_part_of_key(&u); + compute_reg_input_type_is_part_of_key(&u); + compute_shift_rider_is_part_of_key(&u); + compute_semantic_flags_are_part_of_key(&u); + location_mir_spill_copies_are_compacted(&u); + location_mir_spill_uses_block_dse(&u); + kit_unit_summary(&u, "combine-cse"); + return kit_unit_status(&u); +} diff --git a/test/opt/frame_value_backend_test.c b/test/opt/frame_value_backend_test.c @@ -0,0 +1,766 @@ +/* Real-backend regression for typed FRAME_VALUE address components. + * + * Spill-slot coloring is allowed to reuse a physical frame slot across values + * with different type IDs. NativeAddr therefore carries base_type/index_type + * as the value-width authority: a backend must not infer a reload width from + * the (possibly wider) slot descriptor. Build two deliberately-wide i64 + * slots, consume them as a pointer base plus an i32 scaled index, and inspect + * only the body bytes emitted by each native backend. + * + * This also pins the RV64 load_addr path: it used to load the frame base but + * silently omit an INDEX_FRAME_VALUE. The final sh2add proves the index + * participates in the address. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "arch/aa64/aa64.h" +#include "arch/arch.h" +#include "arch/arm32/arm32.h" +#include "arch/riscv/rv64.h" +#include "arch/x64/isa.h" +#include "arch/x64/x64.h" +#include "cg/type.h" +#include "core/pool.h" +#include "lib/kit_unit.h" +#include "obj/obj.h" + +typedef NativeTarget *(*NativeCtor)(Compiler *, ObjBuilder *, MCEmitter *); + +typedef struct DecodedInsn { + char mnemonic[20]; + char operands[96]; +} DecodedInsn; + +typedef struct BackendCase { + KitArchKind arch; + const char *name; + NativeCtor ctor; + Reg dst_reg; + u8 ptr_size; +} BackendCase; + +static void slice_copy(KitSlice src, char *dst, size_t cap) { + size_t n = src.len < cap - 1u ? src.len : cap - 1u; + if (n && src.s) + memcpy(dst, src.s, n); + dst[n] = '\0'; +} + +static u32 decode_body(KitUnit *unit, Compiler *c, ObjBuilder *obj, + ObjSecId text, u32 begin, u32 end, DecodedInsn *out, + u32 cap, const char *tag) { + const Section *sec = obj_section_get(obj, text); + ArchDisasm *dis = NULL; + u8 *flat = NULL; + u32 size; + u32 pos = begin; + u32 nout = 0; + + CU_CHECK(unit, sec != NULL, "%s: missing text section", tag); + if (!sec) + return 0; + size = buf_pos(&sec->bytes); + CU_CHECK(unit, begin <= end && end <= size, + "%s: invalid body range [%u,%u) in %u-byte text", tag, begin, end, + size); + if (begin > end || end > size) + return 0; + flat = size ? (u8 *)malloc(size) : NULL; + CU_CHECK(unit, size == 0u || flat != NULL, + "%s: could not allocate flattened text", tag); + if (size && !flat) + return 0; + if (size) + buf_flatten(&sec->bytes, flat); + dis = arch_disasm_new(c); + CU_CHECK(unit, dis != NULL, "%s: could not create disassembler", tag); + if (!dis) { + free(flat); + return 0; + } + + while (pos < end) { + KitInsn insn; + u32 used; + memset(&insn, 0, sizeof insn); + used = arch_disasm_decode(dis, flat + pos, end - pos, pos, &insn); + CU_CHECK(unit, used != 0u, "%s: undecodable body byte at offset %u", tag, + pos); + if (!used) + break; + CU_CHECK(unit, nout < cap, "%s: body exceeds %u decoded instructions", tag, + cap); + if (nout < cap) { + slice_copy(insn.mnemonic, out[nout].mnemonic, sizeof out[nout].mnemonic); + slice_copy(insn.operands, out[nout].operands, sizeof out[nout].operands); + ++nout; + } + pos += used; + } + CU_EXPECT(unit, pos == end, "%s: decoder stopped at %u, body ends at %u", tag, + pos, end); + arch_disasm_free(dis); + free(flat); + return nout; +} + +static int has_insn(const DecodedInsn *insns, u32 ninsns, const char *mnemonic, + const char *operand) { + for (u32 i = 0; i < ninsns; ++i) { + if (mnemonic && !strstr(insns[i].mnemonic, mnemonic)) + continue; + if (operand && !strstr(insns[i].operands, operand)) + continue; + return 1; + } + return 0; +} + +static i32 find_insn(const DecodedInsn *insns, u32 ninsns, + const char *mnemonic, const char *operand) { + for (u32 i = 0; i < ninsns; ++i) { + if (mnemonic && !strstr(insns[i].mnemonic, mnemonic)) + continue; + if (operand && !strstr(insns[i].operands, operand)) + continue; + return (i32)i; + } + return -1; +} + +static u32 count_insns(const DecodedInsn *insns, u32 ninsns, + const char *mnemonic) { + u32 count = 0; + for (u32 i = 0; i < ninsns; ++i) + if (strstr(insns[i].mnemonic, mnemonic)) + ++count; + return count; +} + +static u32 collect_relocs_to(const ObjBuilder *obj, ObjSecId section, + u32 begin, u32 end, ObjSymId sym, Reloc *out, + u32 cap) { + u32 n = 0; + for (u32 i = 0; i < obj_reloc_total(obj); ++i) { + const Reloc *r = obj_reloc_at(obj, i); + if (!r || r->removed || r->section_id != section || r->offset < begin || + r->offset >= end || r->sym != sym) + continue; + if (n < cap) + out[n] = *r; + ++n; + } + return n; +} + +static void check_location_address_roles(KitUnit *unit) { + NativeLoc storage = {0}; + NativeLoc frame_addr = {0}; + NativeLoc global = {0}; + NativeLoc imm = {0}; + NativeAddr addr; + i64 part = 0; + + storage.kind = NATIVE_LOC_STACK; + storage.type = 11u; + storage.v.stack.slot = 7u; + storage.v.stack.offset = 5; + CU_EXPECT(unit, + native_loc_addr_role(storage) == NATIVE_LOC_ADDR_ROLE_STORAGE && + native_loc_storage_addr(storage, 3, &addr) && + addr.base_kind == NATIVE_ADDR_BASE_FRAME && + addr.base.frame == 7u && addr.offset == 8, + "STACK must resolve only as offset storage"); + CU_EXPECT(unit, !native_loc_address_value(storage, &addr), + "storage location was accepted as an address value"); + + frame_addr.kind = NATIVE_LOC_FRAME_ADDR; + frame_addr.type = 12u; + frame_addr.v.frame = 9u; + CU_EXPECT(unit, + native_loc_addr_role(frame_addr) == NATIVE_LOC_ADDR_ROLE_VALUE && + native_loc_address_value(frame_addr, &addr) && + addr.base_kind == NATIVE_ADDR_BASE_FRAME && + addr.base.frame == 9u && addr.offset == 0, + "FRAME_ADDR must resolve only as a scalar address recipe"); + CU_EXPECT(unit, !native_loc_storage_addr(frame_addr, 0, &addr), + "frame address value was accepted as storage"); + + global.kind = NATIVE_LOC_GLOBAL; + global.type = 13u; + global.v.global.sym = 17u; + global.v.global.addend = 37; + CU_EXPECT(unit, + native_loc_address_value(global, &addr) && + addr.base_kind == NATIVE_ADDR_BASE_GLOBAL && + addr.base.global.sym == 17u && + addr.base.global.addend == 37, + "GLOBAL address recipe lost its symbol/addend"); + CU_EXPECT(unit, !native_loc_storage_addr(global, 0, &addr), + "global address value was accepted as storage"); + + imm.kind = NATIVE_LOC_IMM; + imm.v.imm = (i64)0x1122334455667788ull; + CU_EXPECT(unit, + native_loc_imm_part(imm, 4u, 4u, &part) && + (u64)part == 0x11223344u, + "immediate ABI part selection returned the wrong high word"); + CU_EXPECT(unit, !native_loc_imm_part(imm, 7u, 2u, &part), + "immediate ABI part selection accepted an out-of-bounds lane"); +} + +static void check_typed_frame_address(KitUnit *unit, const BackendCase *tc) { + KitTargetSpec spec = kit_unit_target(tc->arch, KIT_OS_LINUX, KIT_OBJ_ELF); + KitCompiler *kit_c = NULL; + Compiler *c; + ObjBuilder *obj = NULL; + MCEmitter *mc = NULL; + NativeTarget *native; + ObjSecId text, data; + ObjSymId sym, global_sym; + KitCgTypeId void_type, i8_type, i32_type, i64_type, ptr_type, tiny3_type, + block8_type, fn_type, call_fn_type, wide_call_fn_type, + exact_call_fn_type, forced_vararg_fn_type; + KitCgFuncParam call_param, exact_call_param; + KitCgFuncResult result; + KitCgFuncSig sig, call_sig, forced_vararg_sig; + CGFuncDesc func; + NativeFrameSlotDesc slot_descs[2]; + NativeKnownFrameDesc frame; + NativeFrameSlot slots[2]; + NativeAddr addr; + NativeLoc dst; + NativeLoc call_arg; + NativeLoc exact_arg; + NativeCallDesc call_desc; + NativeCallPlan call_plan; + DecodedInsn body[12]; + DecodedInsn call_body[8]; + DecodedInsn global_body[12]; + DecodedInsn imm_body[12]; + DecodedInsn exact_call_body[24]; + DecodedInsn forced_vararg_body[32]; + DecodedInsn copy_frame_value_body[16]; + DecodedInsn set_frame_value_body[24]; + DecodedInsn indexed_frame_value_body[12]; + Reloc global_relocs[4]; + u32 body_begin, body_end, nbody; + u32 call_begin, call_end, ncall; + u32 global_begin, global_end, nglobal, nglobal_relocs; + u32 imm_begin = 0, imm_end = 0, nimm = 0; + u32 exact_call_begin = 0, exact_call_end = 0, nexact_call = 0; + u32 forced_vararg_begin = 0, forced_vararg_end = 0, nforced_vararg = 0; + u32 copy_frame_value_begin = 0, copy_frame_value_end = 0, + ncopy_frame_value = 0; + u32 set_frame_value_begin = 0, set_frame_value_end = 0, + nset_frame_value = 0; + u32 indexed_frame_value_begin = 0, indexed_frame_value_end = 0, + nindexed_frame_value = 0; + + spec.ptr_size = tc->ptr_size; + spec.ptr_align = tc->ptr_size; + CU_CHECK(unit, kit_unit_compiler_new(unit, spec, &kit_c) == KIT_OK && kit_c, + "%s: compiler allocation failed", tc->name); + if (!kit_c) + return; + c = (Compiler *)kit_c; + obj = obj_new(c); + CU_CHECK(unit, obj != NULL, "%s: object allocation failed", tc->name); + if (!obj) + goto done; + mc = mc_new(c, obj); + CU_CHECK(unit, mc != NULL, "%s: MC allocation failed", tc->name); + if (!mc) + goto done; + native = tc->ctor(c, obj, mc); + CU_CHECK(unit, native != NULL, "%s: native target allocation failed", + tc->name); + if (!native) + goto done; + + text = obj_section(obj, pool_intern_slice(c->global, SLICE_LIT(".text")), + SEC_TEXT, SF_EXEC | SF_ALLOC, 16u); + data = obj_section(obj, pool_intern_slice(c->global, SLICE_LIT(".data")), + SEC_DATA, SF_WRITE | SF_ALLOC, 16u); + sym = obj_symbol(obj, pool_intern_slice(c->global, SLICE_LIT("frame_value")), + SB_GLOBAL, SK_FUNC, text, 0u, 0u); + global_sym = obj_symbol( + obj, pool_intern_slice(c->global, SLICE_LIT("address_value_global")), + SB_LOCAL, SK_OBJ, data, 0u, 64u); + void_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + i8_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I8); + i32_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I32); + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr(kit_c, void_type, 0u); + tiny3_type = kit_cg_type_array(kit_c, i8_type, 3u); + block8_type = kit_cg_type_array(kit_c, i8_type, 8u); + memset(&result, 0, sizeof result); + result.type = void_type; + memset(&sig, 0, sizeof sig); + sig.result = result; + sig.call_conv = KIT_CG_CC_TARGET_C; + fn_type = kit_cg_type_func(kit_c, sig); + memset(&call_param, 0, sizeof call_param); + call_param.type = ptr_type; + memset(&call_sig, 0, sizeof call_sig); + call_sig.result = result; + call_sig.params = &call_param; + call_sig.nparams = 1u; + call_sig.call_conv = KIT_CG_CC_TARGET_C; + call_fn_type = kit_cg_type_func(kit_c, call_sig); + call_param.type = i64_type; + wide_call_fn_type = kit_cg_type_func(kit_c, call_sig); + memset(&exact_call_param, 0, sizeof exact_call_param); + exact_call_param.type = tiny3_type; + call_sig.params = &exact_call_param; + exact_call_fn_type = kit_cg_type_func(kit_c, call_sig); + memset(&forced_vararg_sig, 0, sizeof forced_vararg_sig); + forced_vararg_sig.result = result; + forced_vararg_sig.call_conv = KIT_CG_CC_TARGET_C; + forced_vararg_sig.abi_variadic = true; + forced_vararg_fn_type = kit_cg_type_func(kit_c, forced_vararg_sig); + + memset(&func, 0, sizeof func); + func.sym = sym; + func.text_section_id = text; + func.fn_type = fn_type; + func.result_type = void_type; + + /* Both colored homes advertise i64/8. The address contract below is the + * authority: pointer-width base, four-byte index. */ + memset(slot_descs, 0, sizeof slot_descs); + for (u32 i = 0; i < 2u; ++i) { + slot_descs[i].type = i64_type; + slot_descs[i].size = 8u; + slot_descs[i].align = 8u; + slot_descs[i].kind = NATIVE_FRAME_SLOT_SPILL; + } + memset(&frame, 0, sizeof frame); + frame.slots = slot_descs; + frame.nslots = 2u; + /* The RV-only forced-vararg construction below owns one rounded 16-byte + * outgoing region. Reserve it before known-frame finalization just as the + * optimizer's frame pre-pass does for a real call. */ + if (tc->arch == KIT_ARCH_RV64) frame.max_outgoing = 16u; + /* Keep a real frame record on every target so frame-relative body reloads + * have a settled anchor independent of leaf-frame elision. */ + frame.is_leaf = 0u; + native->func_begin_known_frame(native, &func, &frame, slots); + + memset(&addr, 0, sizeof addr); + addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE; + addr.base.frame = slots[0]; + addr.base_type = ptr_type; + addr.cls = NATIVE_REG_INT; + addr.index_kind = NATIVE_ADDR_INDEX_FRAME_VALUE; + addr.index.frame = slots[1]; + addr.index_type = i32_type; + addr.index_cls = NATIVE_REG_INT; + addr.log2_scale = 2u; + addr.offset = 12; + dst = native_loc_reg(ptr_type, NATIVE_REG_INT, tc->dst_reg); + body_begin = mc_pos(mc); + native->load_addr(native, dst, addr); + body_end = mc_pos(mc); + + /* A post-allocation rematerialization may present `&local` directly to the + * ABI marshaller. It is an address value, not the scalar stored in the + * frame slot, and must not require an optimizer-owned staging register. */ + memset(&call_arg, 0, sizeof call_arg); + call_arg.kind = NATIVE_LOC_FRAME_ADDR; + call_arg.cls = NATIVE_REG_INT; + call_arg.type = ptr_type; + call_arg.v.frame = slots[0]; + memset(&call_desc, 0, sizeof call_desc); + call_desc.fn_type = call_fn_type; + call_desc.args = &call_arg; + call_desc.nargs = 1u; + memset(&call_plan, 0, sizeof call_plan); + call_begin = mc_pos(mc); + native->plan_call(native, &call_desc, &call_plan); + call_end = mc_pos(mc); + + /* A GLOBAL NativeLoc is the scalar address value `&symbol + addend`, never + * the bytes stored at that address. Exercise the ABI boundary directly: + * ordinary C currently materializes globals before constructing its call + * descriptor, so a source-level call cannot pin this lower-level contract. */ + memset(&call_arg, 0, sizeof call_arg); + call_arg.kind = NATIVE_LOC_GLOBAL; + call_arg.cls = NATIVE_REG_INT; + call_arg.type = ptr_type; + call_arg.v.global.sym = global_sym; + call_arg.v.global.addend = 37; + memset(&call_desc, 0, sizeof call_desc); + call_desc.fn_type = call_fn_type; + call_desc.args = &call_arg; + call_desc.nargs = 1u; + memset(&call_plan, 0, sizeof call_plan); + global_begin = mc_pos(mc); + native->plan_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 + * NativeLoc immediate denotes the complete scalar, so each lane must select + * its own source bits instead of rematerializing the low word twice. */ + if (tc->arch == KIT_ARCH_ARM_32) { + memset(&call_arg, 0, sizeof call_arg); + call_arg.kind = NATIVE_LOC_IMM; + call_arg.cls = NATIVE_REG_INT; + call_arg.type = i64_type; + call_arg.v.imm = (i64)0x1122334455667788ull; + memset(&call_desc, 0, sizeof call_desc); + call_desc.fn_type = wide_call_fn_type; + call_desc.args = &call_arg; + call_desc.nargs = 1u; + memset(&call_plan, 0, sizeof call_plan); + imm_begin = mc_pos(mc); + native->plan_call(native, &call_desc, &call_plan); + imm_end = mc_pos(mc); + } + + /* x64 lowerers may legally hand the ABI boundary an exact-width aggregate + * in NATIVE_LOC_ADDR storage whose explicit base is also its destination ABI + * register. Keep a register-indirect callee live across that whole marshal: + * the 3-byte argument forces address stabilization through r11, which was + * formerly also the parked call target. This backend-level construction pins + * the allowed shape even when current source lowering chooses a frame home. */ + if (tc->arch == KIT_ARCH_X86_64) { + memset(&exact_arg, 0, sizeof exact_arg); + exact_arg.kind = NATIVE_LOC_ADDR; + exact_arg.cls = NATIVE_REG_INT; + exact_arg.type = tiny3_type; + exact_arg.v.addr.base_kind = NATIVE_ADDR_BASE_REG; + exact_arg.v.addr.base.reg = X64_RDI; + exact_arg.v.addr.base_type = ptr_type; + exact_arg.v.addr.cls = NATIVE_REG_INT; + memset(&call_desc, 0, sizeof call_desc); + call_desc.fn_type = exact_call_fn_type; + call_desc.callee = native_loc_reg(ptr_type, NATIVE_REG_INT, X64_RCX); + call_desc.args = &exact_arg; + 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->emit_call(native, &call_plan); + exact_call_end = mc_pos(mc); + } + + /* RISC-V does not currently select vararg_on_stack, but it still implements + * the shared ABI trait and must not encode a latent rounded-slot overread. + * Synthesize that trait for one three-byte unnamed argument: the physical + * carrier is eight bytes (and the whole outgoing area rounds to 16), while + * source and destination accesses must remain exactly 2+1 bytes. Keep the + * indirect target in a STACK home at the same time: current lowering usually + * materializes that shape before this boundary, but NativeCallDesc permits it + * and the backend must park it outside the exact/address temporary bank. */ + if (tc->arch == KIT_ARCH_RV64) { + const ABIFuncInfo* classified = + abi_cg_func_info(c->abi, forced_vararg_fn_type); + ABIFuncInfo* synthetic = (ABIFuncInfo*)classified; + u8 saved_vararg_on_stack; + CU_CHECK(unit, synthetic != NULL, + "rv64: variadic function classification failed"); + if (synthetic) { + saved_vararg_on_stack = synthetic->vararg_on_stack; + synthetic->vararg_on_stack = 1u; + exact_arg = native_loc_stack(tiny3_type, slots[0], 0); + memset(&call_desc, 0, sizeof call_desc); + call_desc.fn_type = forced_vararg_fn_type; + call_desc.callee = native_loc_stack(ptr_type, slots[1], 0); + call_desc.args = &exact_arg; + 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->emit_call(native, &call_plan); + forced_vararg_end = mc_pos(mc); + synthetic->vararg_on_stack = saved_vararg_on_stack; + } + } + + /* The AArch64 aggregate primitives carry their payload in x16. A + * FRAME_VALUE destination must therefore materialize its pointer through a + * different address scratch. Otherwise both copy_bytes and widened + * set_bytes degenerate to `str x16, [x16]`, storing the pointer value rather + * than the aggregate payload. Keep this at the backend boundary so the + * contract stays independent of optimizer register-allocation choices. */ + if (tc->arch == KIT_ARCH_ARM_64) { + AggregateAccess access; + NativeAddr frame_value_dst; + NativeAddr frame_src; + NativeLoc fill; + MemAccess indexed_mem; + memset(&access, 0, sizeof access); + access.type = block8_type; + access.size = 8u; + access.align = 8u; + access.mem.type = block8_type; + access.mem.size = 8u; + access.mem.align = 8u; + memset(&frame_value_dst, 0, sizeof frame_value_dst); + frame_value_dst.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE; + frame_value_dst.base.frame = slots[0]; + frame_value_dst.base_type = ptr_type; + frame_value_dst.cls = NATIVE_REG_INT; + memset(&frame_src, 0, sizeof frame_src); + frame_src.base_kind = NATIVE_ADDR_BASE_FRAME; + frame_src.base.frame = slots[1]; + frame_src.base_type = ptr_type; + frame_src.cls = NATIVE_REG_INT; + fill = native_loc_reg(i8_type, NATIVE_REG_INT, 9u); + + copy_frame_value_begin = mc_pos(mc); + native->copy_bytes(native, frame_value_dst, frame_src, access); + copy_frame_value_end = mc_pos(mc); + set_frame_value_begin = mc_pos(mc); + native->set_bytes(native, frame_value_dst, fill, access); + set_frame_value_end = mc_pos(mc); + + /* A register index remains live until the final addressing-mode + * instruction. Pin x16 as that index and require base materialization to + * select x17 even when the store payload itself uses neither scratch. */ + frame_value_dst.index_kind = NATIVE_ADDR_INDEX_REG; + frame_value_dst.index.reg = 16u; + frame_value_dst.index_type = i64_type; + frame_value_dst.index_cls = NATIVE_REG_INT; + memset(&indexed_mem, 0, sizeof indexed_mem); + indexed_mem.type = i64_type; + indexed_mem.size = 8u; + indexed_mem.align = 8u; + indexed_frame_value_begin = mc_pos(mc); + native->store(native, frame_value_dst, + native_loc_reg(i64_type, NATIVE_REG_INT, 9u), indexed_mem); + indexed_frame_value_end = mc_pos(mc); + } + native->func_end(native); + + memset(body, 0, sizeof body); + nbody = decode_body(unit, c, obj, text, body_begin, body_end, body, + (u32)(sizeof body / sizeof body[0]), tc->name); + memset(call_body, 0, sizeof call_body); + ncall = decode_body(unit, c, obj, text, call_begin, call_end, call_body, + (u32)(sizeof call_body / sizeof call_body[0]), tc->name); + memset(global_body, 0, sizeof global_body); + nglobal = decode_body(unit, c, obj, text, global_begin, global_end, + global_body, + (u32)(sizeof global_body / sizeof global_body[0]), + tc->name); + memset(global_relocs, 0, sizeof global_relocs); + nglobal_relocs = collect_relocs_to( + obj, text, global_begin, global_end, global_sym, global_relocs, + (u32)(sizeof global_relocs / sizeof global_relocs[0])); + if (tc->arch == KIT_ARCH_ARM_32) { + memset(imm_body, 0, sizeof imm_body); + nimm = decode_body(unit, c, obj, text, imm_begin, imm_end, imm_body, + (u32)(sizeof imm_body / sizeof imm_body[0]), tc->name); + } + if (tc->arch == KIT_ARCH_X86_64) { + memset(exact_call_body, 0, sizeof exact_call_body); + nexact_call = decode_body( + unit, c, obj, text, exact_call_begin, exact_call_end, exact_call_body, + (u32)(sizeof exact_call_body / sizeof exact_call_body[0]), tc->name); + } + if (tc->arch == KIT_ARCH_RV64) { + memset(forced_vararg_body, 0, sizeof forced_vararg_body); + nforced_vararg = decode_body( + unit, c, obj, text, forced_vararg_begin, forced_vararg_end, + forced_vararg_body, + (u32)(sizeof forced_vararg_body / sizeof forced_vararg_body[0]), + tc->name); + } + if (tc->arch == KIT_ARCH_ARM_64) { + memset(copy_frame_value_body, 0, sizeof copy_frame_value_body); + ncopy_frame_value = decode_body( + unit, c, obj, text, copy_frame_value_begin, copy_frame_value_end, + copy_frame_value_body, + (u32)(sizeof copy_frame_value_body / sizeof copy_frame_value_body[0]), + "aa64-copy-frame-value"); + memset(set_frame_value_body, 0, sizeof set_frame_value_body); + nset_frame_value = decode_body( + unit, c, obj, text, set_frame_value_begin, set_frame_value_end, + set_frame_value_body, + (u32)(sizeof set_frame_value_body / sizeof set_frame_value_body[0]), + "aa64-set-frame-value"); + memset(indexed_frame_value_body, 0, sizeof indexed_frame_value_body); + nindexed_frame_value = decode_body( + unit, c, obj, text, indexed_frame_value_begin, + indexed_frame_value_end, indexed_frame_value_body, + (u32)(sizeof indexed_frame_value_body / + sizeof indexed_frame_value_body[0]), + "aa64-indexed-frame-value"); + } + CU_EXPECT(unit, nbody >= 3u, "%s: expected typed base/index/add sequence", + tc->name); + CU_EXPECT(unit, ncall != 0u, + "%s: frame-address call argument emitted no materialization", + tc->name); + CU_EXPECT(unit, nglobal != 0u, + "%s: global-address call argument emitted no materialization", + tc->name); + + switch (tc->arch) { + case KIT_ARCH_X86_64: + CU_EXPECT(unit, has_insn(body, nbody, "mov", "r11d"), + "x64: i32 frame index was not loaded through r11d"); + CU_EXPECT(unit, has_insn(body, nbody, "lea", NULL), + "x64: scaled frame index was not folded into LEA"); + CU_EXPECT(unit, + nglobal_relocs == 1u && global_relocs[0].kind == R_PC32 && + global_relocs[0].addend == 33, + "x64: GLOBAL call arg lost symbol or +37 addend"); + CU_EXPECT(unit, has_insn(global_body, nglobal, "lea", NULL), + "x64: GLOBAL call arg was not materialized with LEA"); + CU_EXPECT(unit, nglobal == 1u, + "x64: GLOBAL call arg unexpectedly dereferenced storage"); + { + i32 push = find_insn(exact_call_body, nexact_call, "push", "rcx"); + i32 exact_load = + find_insn(exact_call_body, nexact_call, "mov", "(%r11)"); + i32 pop = find_insn(exact_call_body, nexact_call, "pop", "r11"); + i32 call = find_insn(exact_call_body, nexact_call, "call", "r11"); + CU_EXPECT(unit, push >= 0, + "x64: indirect callee was not parked before marshalling"); + CU_EXPECT(unit, exact_load > push, + "x64: exact address argument did not consume stabilized r11"); + CU_EXPECT(unit, pop > exact_load, + "x64: indirect callee was restored before argument transport"); + CU_EXPECT(unit, call > pop, + "x64: restored indirect callee was not the call target"); + } + break; + case KIT_ARCH_ARM_64: + CU_EXPECT(unit, has_insn(body, nbody, "ldr", "w17"), + "aa64: i32 frame index was not loaded through w17"); + CU_EXPECT(unit, has_insn(body, nbody, "add", "lsl #2"), + "aa64: scaled frame index was not folded into the address"); + CU_EXPECT(unit, + nglobal_relocs == 2u && + global_relocs[0].kind == R_AARCH64_ADR_PREL_PG_HI21 && + global_relocs[1].kind == R_AARCH64_ADD_ABS_LO12_NC && + global_relocs[0].addend == 37 && + global_relocs[1].addend == 37, + "aa64: GLOBAL call arg lost symbol or +37 addend"); + CU_EXPECT(unit, nglobal == 2u, + "aa64: GLOBAL call arg unexpectedly dereferenced storage"); + CU_EXPECT(unit, + has_insn(copy_frame_value_body, ncopy_frame_value, "str", + "x16, [x17") && + !has_insn(copy_frame_value_body, ncopy_frame_value, "str", + "x16, [x16"), + "aa64: aggregate copy clobbered x16 while materializing its " + "FRAME_VALUE destination"); + CU_EXPECT(unit, + has_insn(set_frame_value_body, nset_frame_value, "str", + "x16, [x17") && + !has_insn(set_frame_value_body, nset_frame_value, "str", + "x16, [x16"), + "aa64: widened aggregate set clobbered x16 while materializing " + "its FRAME_VALUE destination"); + CU_EXPECT(unit, + has_insn(indexed_frame_value_body, nindexed_frame_value, "ldr", + "x17") && + has_insn(indexed_frame_value_body, nindexed_frame_value, + "str", "x9, [x17, x16"), + "aa64: FRAME_VALUE base materialization clobbered its x16 " + "register index"); + break; + case KIT_ARCH_RV64: + CU_EXPECT(unit, has_insn(body, nbody, "lw", "t1"), + "rv64: i32 frame index was not loaded with lw"); + /* The RV decoder does not yet describe Zba, so it intentionally renders + * sh2add a0,t1,a0 as its exact raw word. Pinning that word is stronger + * than counting instructions: the old dropped-index path had no fourth + * instruction at all. */ + CU_EXPECT(unit, has_insn(body, nbody, ".word", "0x20a34533"), + "rv64: frame index was dropped from load_addr"); + CU_EXPECT(unit, + nglobal_relocs == 1u && + global_relocs[0].kind == R_RV_PCREL_HI20, + "rv64: GLOBAL call arg did not retain its symbol relocation"); + CU_EXPECT(unit, has_insn(global_body, nglobal, "addi", "37"), + "rv64: GLOBAL call arg lost its +37 addend"); + CU_EXPECT(unit, nglobal == 3u && + !has_insn(global_body, nglobal, "ld", NULL) && + !has_insn(global_body, nglobal, "lw", NULL), + "rv64: GLOBAL call arg unexpectedly dereferenced storage"); + { + i32 park = find_insn(forced_vararg_body, nforced_vararg, "ld", "ra"); + i32 exact_load = + find_insn(forced_vararg_body, nforced_vararg, "lhu", NULL); + i32 exact_store = + find_insn(forced_vararg_body, nforced_vararg, "sh", NULL); + i32 call = + find_insn(forced_vararg_body, nforced_vararg, "jalr", "ra, 0(ra)"); + CU_EXPECT(unit, park >= 0, + "rv64: indirect callee was not parked outside transport temps"); + CU_EXPECT(unit, exact_load > park && + has_insn(forced_vararg_body, nforced_vararg, "lbu", + NULL), + "rv64: 3-byte forced vararg did not use exact 2+1 loads"); + CU_EXPECT(unit, exact_store > exact_load && + has_insn(forced_vararg_body, nforced_vararg, "sb", + NULL), + "rv64: 3-byte forced vararg did not use exact 2+1 stores"); + CU_EXPECT(unit, + count_insns(forced_vararg_body, nforced_vararg, "ld") == 1u && + !has_insn(forced_vararg_body, nforced_vararg, "sd", NULL), + "rv64: rounded vararg carrier became a semantic access width"); + CU_EXPECT(unit, call > exact_store, + "rv64: parked indirect target did not survive argument transport"); + } + break; + case KIT_ARCH_ARM_32: + CU_EXPECT(unit, count_insns(body, nbody, "ldr") == 2u, + "arm32: exact-width base/index should require two word loads"); + CU_EXPECT(unit, has_insn(body, nbody, "add", "lsl #2"), + "arm32: scaled frame index was not folded into the address"); + CU_EXPECT(unit, + nglobal_relocs == 2u && + global_relocs[0].kind == R_ARM_THM_MOVW_ABS_NC && + global_relocs[1].kind == R_ARM_THM_MOVT_ABS, + "arm32: GLOBAL call arg did not retain MOVW/MOVT relocations"); + CU_EXPECT(unit, has_insn(global_body, nglobal, "add", "37"), + "arm32: GLOBAL call arg lost its +37 addend"); + CU_EXPECT(unit, nglobal == 3u && + !has_insn(global_body, nglobal, "ldr", NULL), + "arm32: GLOBAL call arg unexpectedly dereferenced storage"); + CU_EXPECT(unit, nimm >= 4u, + "arm32: split i64 immediate did not materialize both lanes"); + CU_EXPECT(unit, has_insn(imm_body, nimm, "movw", "r0, #30600") && + has_insn(imm_body, nimm, "movt", "r0, #21862"), + "arm32: i64 immediate low lane is not 0x55667788"); + CU_EXPECT(unit, has_insn(imm_body, nimm, "movw", "r1, #13124") && + has_insn(imm_body, nimm, "movt", "r1, #4386"), + "arm32: i64 immediate high lane is not 0x11223344"); + break; + default: + CU_EXPECT(unit, 0, "%s: unhandled architecture", tc->name); + break; + } + +done: + if (mc) + mc_free(mc); + if (obj) + obj_free(obj); + kit_compiler_free(kit_c); +} + +int main(void) { + static const BackendCase cases[] = { + {KIT_ARCH_X86_64, "x64", x64_native_target_new, 0u, 8u}, + {KIT_ARCH_ARM_64, "aa64", aa64_native_target_new, 0u, 8u}, + {KIT_ARCH_RV64, "rv64", rv64_native_target_new, 10u, 8u}, + {KIT_ARCH_ARM_32, "arm32", arm32_native_target_new, 0u, 4u}, + }; + KitUnit unit; + kit_unit_init(&unit); + check_location_address_roles(&unit); + for (u32 i = 0; i < (u32)(sizeof cases / sizeof cases[0]); ++i) + check_typed_frame_address(&unit, &cases[i]); + kit_unit_summary(&unit, "frame-value-backends"); + return kit_unit_status(&unit); +} diff --git a/test/opt/location_mir_test.c b/test/opt/location_mir_test.c @@ -0,0 +1,1162 @@ +#include <stdio.h> +#include <string.h> + +#include <kit/frontend.h> + +#include "lib/kit_unit.h" +#include "opt/opt_internal.h" + +typedef struct WalkSummary { + FrameSlot stack_uses[4]; + u32 nstack_uses; + u32 reserved_regs_seen; + u32 reserved_mask[OPT_REG_CLASSES]; +} WalkSummary; + +typedef void (*PanicAction)(void*); + +typedef struct PanicInvocation { + PanicAction action; + void* arg; +} PanicInvocation; + +static KitStatus run_panicking_action(KitCompiler* kc, void* arg) { + PanicInvocation* call = (PanicInvocation*)arg; + (void)kc; + call->action(call->arg); + return KIT_OK; +} + +static int expect_compiler_panic(KitUnit* u, Compiler* c, PanicAction action, + void* arg, const char* expected) { + PanicInvocation call; + KitStatus status; + call.action = action; + call.arg = arg; + u->last_diag[0] = '\0'; + ++u->suppress_fatal; + status = kit_frontend_run((KitCompiler*)c, run_panicking_action, &call); + --u->suppress_fatal; + return status == KIT_ERR && strstr(u->last_diag, expected) != NULL; +} + +static Operand reg_op(PReg reg, KitCgTypeId type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_REG; + op.cls = RC_INT; + op.type = type; + op.v.reg = reg; + return op; +} + +static Operand indirect_op(PReg base, PReg index, KitCgTypeId base_type, + KitCgTypeId index_type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_INDIRECT; + op.cls = RC_INT; + op.type = base_type; + op.v.ind.base = base; + op.v.ind.index = index; + op.v.ind.log2_scale = 3; + op.v.ind.base_type = base_type; + op.v.ind.index_type = index_type; + return op; +} + +static FrameSlot spill_slot(Func* f, KitCgTypeId type) { + FrameSlotDesc desc; + memset(&desc, 0, sizeof desc); + desc.type = type; + desc.size = 8; + desc.align = 8; + desc.kind = FS_SPILL; + return ir_frame_slot_new(f, &desc); +} + +static FrameSlot local_slot(Func* f, KitCgTypeId type, u32 size, u32 align) { + FrameSlotDesc desc; + memset(&desc, 0, sizeof desc); + desc.type = type; + desc.size = size; + desc.align = align; + desc.kind = FS_LOCAL; + return ir_frame_slot_new(f, &desc); +} + +static void set_stack_loc(Func* f, PReg reg, FrameSlot slot) { + f->preg_locs[reg].kind = OPT_LOC_STACK; + f->preg_locs[reg].cls = RC_INT; + f->preg_locs[reg].spill_slot = slot; + f->preg_info[reg].alloc_kind = OPT_ALLOC_SPILL; + f->preg_info[reg].cls = RC_INT; + f->preg_info[reg].spill_slot = slot; +} + +static void set_hard_loc(Func* f, PReg reg, Reg hard_reg) { + f->preg_locs[reg].kind = OPT_LOC_HARD; + f->preg_locs[reg].cls = RC_INT; + f->preg_locs[reg].hard_reg = hard_reg; + f->preg_info[reg].alloc_kind = OPT_ALLOC_HARD; + f->preg_info[reg].cls = RC_INT; + f->preg_info[reg].hard_reg = hard_reg; +} + +typedef struct MachinizeInvocation { + Func* f; + NativeTarget* target; +} MachinizeInvocation; + +static void run_machinize(void* arg) { + MachinizeInvocation* call = (MachinizeInvocation*)arg; + opt_machinize_native(call->f, call->target); +} + +static void run_mir_verify(void* arg) { + opt_mir_verify((Func*)arg, "negative-temp-escape"); +} + +static void machinize_rejects_cross_owner_registers(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + CGFuncDesc desc; + Func* f; + NativePhysRegInfo phys[2]; + NativeAllocClassInfo cls; + NativeRegInfo regs; + NativeTarget target; + MachinizeInvocation call; + Reg emit_temp = 1u; + Reg asm_temp = 2u; + + 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 machinize ownership test"); + if (!kc) return; + c = (Compiler*)kc; + memset(&desc, 0, sizeof desc); + desc.fn_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + f = ir_func_new(c, &desc); + + memset(phys, 0, sizeof phys); + phys[0].reg = emit_temp; + phys[0].cls = NATIVE_REG_INT; + phys[0].flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED; + phys[1].reg = asm_temp; + phys[1].cls = NATIVE_REG_INT; + phys[1].flags = NATIVE_REG_CALLER_SAVED | NATIVE_REG_RESERVED; + memset(&cls, 0, sizeof cls); + cls.cls = NATIVE_REG_INT; + cls.emit_temps = &emit_temp; + 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; + memset(&target, 0, sizeof target); + target.c = c; + target.regs = &regs; + call.f = f; + call.target = &target; + + CU_EXPECT(u, + expect_compiler_panic(u, c, run_machinize, &call, + "emit temp 1 is allocable"), + "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; + cls.asm_temps = &asm_temp; + cls.nasm_temps = 1u; + cls.reserved_mask = 1u << emit_temp; + CU_EXPECT(u, + expect_compiler_panic(u, c, run_machinize, &call, + "asm temp bank is not reserved"), + "machinize accepted an asm-internal temp outside the reserved " + "register owner"); + + kit_compiler_free(kc); +} + +static void mir_verify_rejects_private_temp_escape(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + KitCgTypeId i64_type; + CGFuncDesc desc; + Func* f; + PReg value; + Inst* load; + u32 entry; + Reg temp = 9u; + + 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 MIR temp-escape test"); + if (!kc) return; + c = (Compiler*)kc; + i64_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + memset(&desc, 0, sizeof desc); + desc.fn_type = i64_type; + desc.result_type = i64_type; + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + value = ir_alloc_preg(f, i64_type, RC_INT); + f->preg_locs = arena_zarray(f->arena, OptLoc, f->npregs); + f->preg_info = arena_zarray(f->arena, OptPRegInfo, f->npregs); + set_hard_loc(f, value, temp); + f->opt_phys_regs[RC_INT][0].reg = temp; + f->opt_phys_regs[RC_INT][0].cls = RC_INT; + f->opt_phys_reg_count[RC_INT] = 1u; + f->emit_temp_regs[RC_INT][0] = temp; + f->emit_temp_reg_count[RC_INT] = 1u; + f->opt_reserved_regs[RC_INT] = 1u << temp; + + load = ir_emit(f, entry, IR_LOAD_IMM); + load->type = i64_type; + load->def = value; + load->extra.imm = 42; + load->nopnds = 1u; + load->opnds = arena_array(f->arena, Operand, 1u); + load->opnds[0] = reg_op(value, i64_type); + opt_lower_to_mir(f, NULL); + CU_EXPECT(u, f->mir != NULL, + "temp-escape fixture did not lower to location MIR"); + if (f->mir) { + CU_EXPECT(u, + expect_compiler_panic(u, c, run_mir_verify, f, + "emitter-owned temp escaped into MIR"), + "MIR verifier accepted an emitter-owned temporary as a " + "persistent value"); + + f->emit_temp_reg_count[RC_INT] = 0u; + f->asm_temp_mask[RC_INT] = 1u << temp; + CU_EXPECT(u, + expect_compiler_panic(u, c, run_mir_verify, f, + "asm-internal temp escaped into MIR"), + "MIR verifier accepted an asm-internal temporary as a " + "persistent value"); + } + + kit_compiler_free(kc); +} + +static void mir_commit_propagates_instruction_namespace(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + CGFuncDesc desc; + Func* f; + Func view; + InstId committed; + + 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 MIR commit contract test"); + if (!kc) return; + c = (Compiler*)kc; + memset(&desc, 0, sizeof desc); + desc.fn_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + f = ir_func_new(c, &desc); + f->mir = arena_zarray(f->arena, MFunc, 1u); + + CU_EXPECT(u, opt_mir_view(f, &view), + "MIR commit fixture did not produce a physical view"); + committed = f->next_inst_id + 7u; + view.next_inst_id = committed; + opt_mir_commit(f, &view); + CU_EXPECT(u, f->next_inst_id == committed, + "MIR commit lost the physical graph's instruction-id namespace"); + + kit_compiler_free(kc); +} + +static Inst* find_inst(Block* block, InstId id) { + for (u32 i = 0; i < block->ninsts; ++i) + if (block->insts[i].id == id) + return &block->insts[i]; + return NULL; +} + +static void summarize_location(Func* f, Inst* in, Operand* op, int is_def, + void* arg) { + WalkSummary* summary = (WalkSummary*)arg; + (void)f; + (void)in; + if (op->kind == OPK_STACK && !is_def && + summary->nstack_uses < + (u32)(sizeof summary->stack_uses / sizeof summary->stack_uses[0])) { + summary->stack_uses[summary->nstack_uses++] = op->v.frame_slot; + } + if (op->kind == OPK_REG && op->cls < OPT_REG_CLASSES && op->v.reg < 32u && + (summary->reserved_mask[op->cls] & (1u << op->v.reg))) { + ++summary->reserved_regs_seen; + } +} + +static void spilled_values_remain_locations(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + KitCgTypeId i64_type, ptr_type; + CGFuncDesc desc; + PReg bin_dst, bin_lhs, bin_rhs, load_dst, addr_base, addr_index; + FrameSlot bin_dst_slot, bin_lhs_slot, bin_rhs_slot; + FrameSlot addr_base_slot, addr_index_slot; + u32 entry; + Inst* bin; + Inst* load; + InstId bin_id; + InstId load_id; + Block* hir_blocks; + Inst* hir_insts; + Operand* hir_bin_opnds; + Operand* hir_load_opnds; + u32* hir_succ; + Block* mir_block; + WalkSummary all; + WalkSummary address; + Func view; + + CU_EXPECT(u, + kit_unit_compiler_new( + u, kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed"); + if (!kc) + return; + c = (Compiler*)kc; + i64_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kc, kit_cg_type_builtin(kc, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.fn_type = i64_type; + desc.result_type = i64_type; + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + + bin_dst = ir_alloc_preg(f, i64_type, RC_INT); + bin_lhs = ir_alloc_preg(f, i64_type, RC_INT); + bin_rhs = ir_alloc_preg(f, i64_type, RC_INT); + load_dst = ir_alloc_preg(f, i64_type, RC_INT); + addr_base = ir_alloc_preg(f, ptr_type, RC_INT); + addr_index = ir_alloc_preg(f, i64_type, RC_INT); + + bin_dst_slot = spill_slot(f, i64_type); + bin_lhs_slot = spill_slot(f, i64_type); + bin_rhs_slot = spill_slot(f, i64_type); + /* Spill coloring may reuse a physical slot across unrelated type IDs. Keep + * intentionally mismatched descriptor types here: location MIR must carry + * the base/index value types rather than reconstructing them from the slot. */ + addr_base_slot = spill_slot(f, i64_type); + addr_index_slot = spill_slot(f, ptr_type); + + f->preg_locs = arena_zarray(f->arena, OptLoc, f->npregs); + f->preg_info = arena_zarray(f->arena, OptPRegInfo, f->npregs); + set_stack_loc(f, bin_dst, bin_dst_slot); + set_stack_loc(f, bin_lhs, bin_lhs_slot); + set_stack_loc(f, bin_rhs, bin_rhs_slot); + set_hard_loc(f, load_dst, 1); + set_stack_loc(f, addr_base, addr_base_slot); + set_stack_loc(f, addr_index, addr_index_slot); + + /* These are emitter-private temporaries. They may be selected while native + * code is emitted, but must never become values in post-regalloc MIR. */ + f->emit_temp_regs[RC_INT][0] = 9; + f->emit_temp_regs[RC_INT][1] = 10; + f->emit_temp_regs[RC_INT][2] = 11; + f->emit_temp_reg_count[RC_INT] = 3; + f->opt_reserved_regs[RC_INT] = (1u << 9) | (1u << 10) | (1u << 11); + + bin = ir_emit(f, entry, IR_BINOP); + bin->type = i64_type; + bin->def = bin_dst; + bin->extra.imm = BO_IADD; + bin->nopnds = 3; + bin->opnds = arena_array(f->arena, Operand, bin->nopnds); + bin->opnds[0] = reg_op(bin_dst, i64_type); + bin->opnds[1] = reg_op(bin_lhs, i64_type); + bin->opnds[2] = reg_op(bin_rhs, i64_type); + bin_id = bin->id; + + load = ir_emit(f, entry, IR_LOAD); + load->type = i64_type; + load->def = load_dst; + load->nopnds = 2; + load->opnds = arena_array(f->arena, Operand, load->nopnds); + load->opnds[0] = reg_op(load_dst, i64_type); + load->opnds[1] = indirect_op(addr_base, addr_index, ptr_type, i64_type); + load->extra.mem.type = i64_type; + load->extra.mem.size = 8; + load->extra.mem.align = 8; + load_id = load->id; + + /* Capture the semantic HIR allocation after the last ir_emit (which may + * grow the block's instruction array). Lowering must build a physically + * separate MIR graph and leave each virtual operand in this graph intact. */ + hir_blocks = f->blocks; + hir_insts = f->blocks[entry].insts; + hir_bin_opnds = find_inst(&f->blocks[entry], bin_id)->opnds; + hir_load_opnds = find_inst(&f->blocks[entry], load_id)->opnds; + hir_succ = f->blocks[entry].succ; + + opt_lower_to_mir(f, NULL); + CU_EXPECT(u, f->mir != NULL, "lowering did not produce MIR"); + if (!f->mir) { + kit_compiler_free(kc); + return; + } + mir_block = &f->mir->blocks[entry]; + bin = find_inst(mir_block, bin_id); + load = find_inst(mir_block, load_id); + + CU_EXPECT(u, f->blocks == hir_blocks && f->blocks != f->mir->blocks, + "lowering did not preserve a distinct HIR block graph"); + CU_EXPECT(u, + f->mir->blocks_cap == f->blocks_cap && + opt_mir_view(f, &view) && view.blocks == f->mir->blocks && + view.blocks_cap == f->mir->blocks_cap && view.mir == NULL && + view.scope_aux_inst == NULL && view.nscopes == 0, + "canonical MIR view retained stale HIR graph metadata"); + CU_EXPECT(u, + f->blocks[entry].insts == hir_insts && + f->mir->blocks[entry].insts != hir_insts, + "lowering aliases HIR and MIR instruction arrays"); + CU_EXPECT(u, + f->blocks[entry].succ == hir_succ && + f->mir->blocks[entry].succ != hir_succ, + "lowering aliases HIR and MIR CFG arrays"); + { + Inst* hir_bin = find_inst(&f->blocks[entry], bin_id); + Inst* hir_load = find_inst(&f->blocks[entry], load_id); + CU_EXPECT(u, + hir_bin && hir_bin->opnds == hir_bin_opnds && + hir_bin->opnds[0].kind == OPK_REG && + hir_bin->opnds[0].v.reg == bin_dst && + hir_bin->opnds[1].v.reg == bin_lhs && + hir_bin->opnds[2].v.reg == bin_rhs && bin && + bin->opnds != hir_bin_opnds, + "location rewrite changed or aliased HIR binop operands"); + CU_EXPECT(u, + hir_load && hir_load->opnds == hir_load_opnds && + hir_load->opnds[0].kind == OPK_REG && + hir_load->opnds[0].v.reg == load_dst && + hir_load->opnds[1].kind == OPK_INDIRECT && + hir_load->opnds[1].v.ind.base == addr_base && + hir_load->opnds[1].v.ind.index == addr_index && load && + load->opnds != hir_load_opnds, + "location rewrite changed or aliased HIR address operands"); + } + + CU_EXPECT(u, f->opt_rewrite_inserted_insts == 0, + "location lowering inserted %llu reload/store instructions", + (unsigned long long)f->opt_rewrite_inserted_insts); + CU_EXPECT(u, mir_block->ninsts == 2, + "location MIR should preserve two source instructions, got %u", + mir_block->ninsts); + CU_EXPECT(u, bin != NULL && bin->op == IR_BINOP, + "original binop did not survive location lowering"); + CU_EXPECT(u, load != NULL && load->op == IR_LOAD, + "original indirect load did not survive location lowering"); + CU_EXPECT(u, + load && load->opnds[1].kind == OPK_INDIRECT && + load->opnds[1].v.ind.base_kind == OPT_INDIRECT_FRAME && + load->opnds[1].v.ind.index_kind == OPT_INDIRECT_FRAME && + load->opnds[1].v.ind.base_type == ptr_type && + load->opnds[1].v.ind.index_type == i64_type, + "indirect component location/type provenance was not preserved"); + + /* OPK_STACK is the post-allocation scalar-value location. OPK_LOCAL remains + * a semantic frame lvalue/address, so accepting it here would collapse the + * exact boundary this test protects. */ + if (bin && bin->nopnds == 3) { + CU_EXPECT(u, + bin->opnds[0].kind == OPK_STACK && + bin->opnds[0].v.frame_slot == bin_dst_slot, + "spilled scalar def is not its stable frame location"); + CU_EXPECT(u, + bin->opnds[1].kind == OPK_STACK && + bin->opnds[1].v.frame_slot == bin_lhs_slot, + "first spilled scalar use is not its stable frame location"); + CU_EXPECT(u, + bin->opnds[2].kind == OPK_STACK && + bin->opnds[2].v.frame_slot == bin_rhs_slot, + "second spilled scalar use is not its stable frame location"); + } else { + CU_EXPECT(u, 0, "lowered binop has the wrong operand shape"); + } + + memset(&address, 0, sizeof address); + address.reserved_mask[RC_INT] = f->opt_reserved_regs[RC_INT]; + if (load) + opt_walk_inst_operands(f, load, summarize_location, &address); + CU_EXPECT(u, + address.nstack_uses == 2 && + address.stack_uses[0] == addr_base_slot && + address.stack_uses[1] == addr_index_slot, + "spilled indirect base/index are not stable frame locations"); + + memset(&all, 0, sizeof all); + for (u32 cls = 0; cls < OPT_REG_CLASSES; ++cls) + all.reserved_mask[cls] = f->opt_reserved_regs[cls]; + for (u32 i = 0; i < mir_block->ninsts; ++i) + opt_walk_inst_operands(f, &mir_block->insts[i], summarize_location, &all); + CU_EXPECT(u, all.reserved_regs_seen == 0, + "MIR exposes %u emitter-private register operands", + all.reserved_regs_seen); + + kit_compiler_free(kc); +} + +static void spilled_frame_address_remains_recipe(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + KitCgTypeId i32_type, ptr_type; + CGFuncDesc desc; + PReg address, copy; + FrameSlot value_slot, address_spill, copy_spill; + u32 entry; + Inst* addr; + Inst* move; + InstId addr_id; + InstId move_id; + + CU_EXPECT(u, + kit_unit_compiler_new( + u, kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed for frame-address recipe test"); + if (!kc) return; + c = (Compiler*)kc; + i32_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I32); + ptr_type = kit_cg_type_ptr(kc, i32_type, 0); + + memset(&desc, 0, sizeof desc); + desc.fn_type = ptr_type; + desc.result_type = ptr_type; + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + + address = ir_alloc_preg(f, ptr_type, RC_INT); + copy = ir_alloc_preg(f, ptr_type, RC_INT); + value_slot = local_slot(f, i32_type, 4, 4); + address_spill = spill_slot(f, ptr_type); + copy_spill = spill_slot(f, ptr_type); + f->preg_locs = arena_zarray(f->arena, OptLoc, f->npregs); + f->preg_info = arena_zarray(f->arena, OptPRegInfo, f->npregs); + set_stack_loc(f, address, address_spill); + set_stack_loc(f, copy, copy_spill); + + addr = ir_emit(f, entry, IR_ADDR_OF); + addr_id = addr->id; + addr->type = ptr_type; + addr->def = address; + addr->nopnds = 2; + addr->opnds = arena_zarray(f->arena, Operand, addr->nopnds); + addr->opnds[0] = reg_op(address, ptr_type); + addr->opnds[1].kind = OPK_LOCAL; + addr->opnds[1].cls = RC_INT; + addr->opnds[1].type = i32_type; + addr->opnds[1].v.frame_slot = value_slot; + + move = ir_emit(f, entry, IR_COPY); + move_id = move->id; + move->type = ptr_type; + move->def = copy; + move->nopnds = 2; + move->opnds = arena_zarray(f->arena, Operand, move->nopnds); + move->opnds[0] = reg_op(copy, ptr_type); + move->opnds[1] = reg_op(address, ptr_type); + + opt_lower_to_mir(f, NULL); + opt_mir_combine(f, NULL); + opt_mir_dce(f); + opt_mir_jump_cleanup(f, OPT_JUMP_CLEANUP_CFG); + opt_mir_build_cfg(f); + opt_mir_jump_cleanup(f, OPT_JUMP_CLEANUP_LAYOUT); + CU_EXPECT(u, f->mir != NULL, "frame-address lowering did not produce MIR"); + if (f->mir) { + Block* block = &f->mir->blocks[entry]; + Inst* mir_addr = find_inst(block, addr_id); + Inst* mir_move = find_inst(block, move_id); + CU_EXPECT(u, mir_addr == NULL, + "rematerialized frame-address producer survived MIR lowering"); + CU_EXPECT(u, block->ninsts == 1 && mir_move && mir_move->nopnds == 2, + "frame-address recipe lowering has the wrong instruction shape"); + CU_EXPECT(u, + mir_move && mir_move->opnds[0].kind == OPK_STACK && + mir_move->opnds[0].v.frame_slot == copy_spill, + "frame-address copy destination is not its spill home"); + CU_EXPECT(u, + mir_move && mir_move->opnds[1].kind == OPK_FRAME_ADDR && + mir_move->opnds[1].v.frame_slot == value_slot, + "frame-address recipe was rewritten as an unrelated value"); + } + addr = find_inst(&f->blocks[entry], addr_id); + move = find_inst(&f->blocks[entry], move_id); + CU_EXPECT(u, + addr && addr->opnds[1].kind == OPK_LOCAL && + addr->opnds[1].v.frame_slot == value_slot && move && + move->opnds[1].kind == OPK_REG && + move->opnds[1].v.reg == address, + "frame-address location lowering mutated semantic HIR"); + + kit_compiler_free(kc); +} + +static void call_aux_is_independent(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + KitCgTypeId i64_type; + CGFuncDesc desc; + PReg callee_reg, arg_reg, arg_part_reg, ret_reg, ret_part_reg; + IRCallAux* hir_aux; + IRCallAux* mir_aux; + Inst* call; + InstId call_id; + u32 entry; + + CU_EXPECT(u, + kit_unit_compiler_new( + u, kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed for call-aux clone test"); + if (!kc) return; + c = (Compiler*)kc; + i64_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + + memset(&desc, 0, sizeof desc); + desc.fn_type = i64_type; + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + + callee_reg = ir_alloc_preg(f, i64_type, RC_INT); + arg_reg = ir_alloc_preg(f, i64_type, RC_INT); + arg_part_reg = ir_alloc_preg(f, i64_type, RC_INT); + ret_reg = ir_alloc_preg(f, i64_type, RC_INT); + ret_part_reg = ir_alloc_preg(f, i64_type, RC_INT); + f->preg_locs = arena_zarray(f->arena, OptLoc, f->npregs); + f->preg_info = arena_zarray(f->arena, OptPRegInfo, f->npregs); + set_hard_loc(f, callee_reg, 1); + set_hard_loc(f, arg_reg, 2); + set_hard_loc(f, arg_part_reg, 3); + set_hard_loc(f, ret_reg, 4); + set_hard_loc(f, ret_part_reg, 5); + + call = ir_emit(f, entry, IR_CALL); + call_id = call->id; + call->def = ret_reg; + hir_aux = arena_znew(f->arena, IRCallAux); + hir_aux->desc.callee = reg_op(callee_reg, i64_type); + hir_aux->desc.nargs = 1; + hir_aux->desc.args = arena_zarray(f->arena, CGABIValue, 1); + hir_aux->desc.args[0].type = i64_type; + hir_aux->desc.args[0].storage = reg_op(arg_reg, i64_type); + hir_aux->desc.args[0].nparts = 1; + hir_aux->desc.args[0].parts = arena_zarray(f->arena, CGABIPart, 1); + hir_aux->desc.args[0].parts[0].op = reg_op(arg_part_reg, i64_type); + hir_aux->desc.ret.type = i64_type; + hir_aux->desc.ret.storage = reg_op(ret_reg, i64_type); + hir_aux->desc.ret.nparts = 1; + hir_aux->desc.ret.parts = arena_zarray(f->arena, CGABIPart, 1); + hir_aux->desc.ret.parts[0].op = reg_op(ret_part_reg, i64_type); + hir_aux->plan.callee = reg_op(callee_reg, i64_type); + hir_aux->plan.nargs = 2; + hir_aux->plan.args = + arena_zarray(f->arena, CGCallPlanMove, hir_aux->plan.nargs); + hir_aux->plan.args[0].src = reg_op(arg_reg, i64_type); + hir_aux->plan.args[0].dst = reg_op(arg_part_reg, i64_type); + hir_aux->plan.args[0].src_kind = CG_CALL_PLAN_SRC_VALUE; + hir_aux->plan.args[0].dst_kind = CG_CALL_PLAN_REG; + hir_aux->plan.args[0].dst_reg = 3; + hir_aux->plan.args[1].src = reg_op(arg_part_reg, i64_type); + hir_aux->plan.args[1].dst_kind = CG_CALL_PLAN_STACK; + hir_aux->plan.args[1].stack_offset = 24; + hir_aux->plan.nrets = 2; + hir_aux->plan.rets = + arena_zarray(f->arena, CGCallPlanRet, hir_aux->plan.nrets); + hir_aux->plan.rets[0].dst = reg_op(ret_reg, i64_type); + hir_aux->plan.rets[0].src_reg = 4; + hir_aux->plan.rets[1].dst = reg_op(ret_part_reg, i64_type); + hir_aux->plan.rets[1].src_reg = 5; + hir_aux->plan_valid = 1; + hir_aux->nresults = 1; + hir_aux->results = arena_array(f->arena, Val, 1); + hir_aux->results[0] = ret_reg; + call->extra.aux = hir_aux; + + opt_lower_to_mir(f, NULL); + call = f->mir ? find_inst(&f->mir->blocks[entry], call_id) : NULL; + mir_aux = call ? (IRCallAux*)call->extra.aux : NULL; + CU_EXPECT(u, call && mir_aux && mir_aux != hir_aux, + "call aux object aliases HIR after MIR lowering"); + CU_EXPECT(u, + mir_aux && mir_aux->desc.args != hir_aux->desc.args && + mir_aux->desc.args[0].parts != hir_aux->desc.args[0].parts && + mir_aux->desc.ret.parts != hir_aux->desc.ret.parts && + mir_aux->plan.args != hir_aux->plan.args && + mir_aux->plan.rets != hir_aux->plan.rets && + mir_aux->results != hir_aux->results, + "call aux nested arrays alias HIR after MIR lowering"); + CU_EXPECT(u, + hir_aux->desc.callee.v.reg == callee_reg && + hir_aux->desc.args[0].storage.v.reg == arg_reg && + hir_aux->desc.args[0].parts[0].op.v.reg == arg_part_reg && + hir_aux->desc.ret.storage.v.reg == ret_reg && + hir_aux->desc.ret.parts[0].op.v.reg == ret_part_reg && + mir_aux && mir_aux->desc.callee.v.reg == 1 && + mir_aux->desc.args[0].storage.v.reg == 2 && + mir_aux->desc.args[0].parts[0].op.v.reg == 3 && + mir_aux->desc.ret.storage.v.reg == 4 && + mir_aux->desc.ret.parts[0].op.v.reg == 5, + "call operand rewrite crossed the HIR/MIR ownership boundary"); + CU_EXPECT(u, + mir_aux && mir_aux->plan.nargs == 2 && mir_aux->plan.nrets == 2 && + mir_aux->plan.args[0].src.v.reg == arg_reg && + mir_aux->plan.args[1].stack_offset == 24 && + mir_aux->plan.rets[0].dst.v.reg == ret_reg && + mir_aux->plan.rets[1].src_reg == 5, + "call plan arrays did not survive MIR cloning"); + + if (mir_aux) { + mir_aux->plan.args[0].src.kind = OPK_IMM; + mir_aux->plan.args[0].src.v.imm = 91; + mir_aux->plan.args[1].stack_offset = 88; + mir_aux->plan.rets[0].dst.v.reg = 27; + mir_aux->plan.rets[1].src_reg = 28; + } + CU_EXPECT(u, + hir_aux->plan.args[0].src.kind == OPK_REG && + hir_aux->plan.args[0].src.v.reg == arg_reg && + hir_aux->plan.args[1].stack_offset == 24 && + hir_aux->plan.rets[0].dst.v.reg == ret_reg && + hir_aux->plan.rets[1].src_reg == 5, + "mutating MIR call-plan arrays changed semantic HIR"); + + hir_aux->plan.args[1].stack_offset = 104; + hir_aux->plan.rets[1].src_reg = 30; + CU_EXPECT(u, + mir_aux && mir_aux->plan.args[1].stack_offset == 88 && + mir_aux->plan.rets[1].src_reg == 28, + "mutating HIR call-plan arrays changed location MIR"); + + kit_compiler_free(kc); +} + +static void structured_aux_arrays_are_independent(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + KitCgTypeId i64_type; + CGFuncDesc desc; + PReg dst0, dst1, arg0, arg1; + Inst* phi; + Inst* sw; + Inst* indirect; + Inst* intrin; + InstId phi_id, switch_id, indirect_id, intrin_id; + IRPhiAux* hir_phi; + IRPhiAux* mir_phi; + IRSwitchAux* hir_switch; + IRSwitchAux* mir_switch; + IRIndirectAux* hir_indirect; + IRIndirectAux* mir_indirect; + IRIntrinAux* hir_intrin; + IRIntrinAux* mir_intrin; + u32 entry; + + CU_EXPECT(u, + kit_unit_compiler_new( + u, kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed for structured-aux clone test"); + if (!kc) return; + c = (Compiler*)kc; + i64_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + + memset(&desc, 0, sizeof desc); + desc.fn_type = i64_type; + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + + dst0 = ir_alloc_preg(f, i64_type, RC_INT); + dst1 = ir_alloc_preg(f, i64_type, RC_INT); + arg0 = ir_alloc_preg(f, i64_type, RC_INT); + arg1 = ir_alloc_preg(f, i64_type, RC_INT); + f->preg_locs = arena_zarray(f->arena, OptLoc, f->npregs); + f->preg_info = arena_zarray(f->arena, OptPRegInfo, f->npregs); + set_hard_loc(f, dst0, 8); + set_hard_loc(f, dst1, 9); + set_hard_loc(f, arg0, 10); + set_hard_loc(f, arg1, 11); + + phi = ir_emit(f, entry, IR_PHI); + phi_id = phi->id; + hir_phi = arena_znew(f->arena, IRPhiAux); + hir_phi->npreds = 2; + hir_phi->pred_blocks = arena_array(f->arena, u32, hir_phi->npreds); + hir_phi->pred_vals = arena_array(f->arena, Val, hir_phi->npreds); + hir_phi->pred_blocks[0] = 3; + hir_phi->pred_blocks[1] = 4; + hir_phi->pred_vals[0] = 13; + hir_phi->pred_vals[1] = 14; + phi->extra.aux = hir_phi; + + sw = ir_emit(f, entry, IR_SWITCH); + switch_id = sw->id; + hir_switch = arena_znew(f->arena, IRSwitchAux); + hir_switch->ncases = 2; + hir_switch->cases = + arena_zarray(f->arena, IRSwitchAuxCase, hir_switch->ncases); + hir_switch->cases[0].value = 21; + hir_switch->cases[0].block = 5; + hir_switch->cases[1].value = 22; + hir_switch->cases[1].block = 6; + sw->extra.aux = hir_switch; + + indirect = ir_emit(f, entry, IR_INDIRECT_BRANCH); + indirect_id = indirect->id; + hir_indirect = arena_znew(f->arena, IRIndirectAux); + hir_indirect->ntargets = 2; + hir_indirect->targets = arena_array(f->arena, u32, hir_indirect->ntargets); + hir_indirect->targets[0] = 7; + hir_indirect->targets[1] = 8; + indirect->extra.aux = hir_indirect; + + intrin = ir_emit(f, entry, IR_INTRINSIC); + intrin_id = intrin->id; + hir_intrin = arena_znew(f->arena, IRIntrinAux); + hir_intrin->kind = INTRIN_SADD_OVERFLOW; + hir_intrin->ndst = 2; + hir_intrin->narg = 2; + hir_intrin->dsts = arena_zarray(f->arena, Operand, hir_intrin->ndst); + hir_intrin->args = arena_zarray(f->arena, Operand, hir_intrin->narg); + hir_intrin->result_vals = arena_array(f->arena, Val, hir_intrin->ndst); + hir_intrin->dsts[0] = reg_op(dst0, i64_type); + hir_intrin->dsts[1] = reg_op(dst1, i64_type); + hir_intrin->args[0] = reg_op(arg0, i64_type); + hir_intrin->args[1] = reg_op(arg1, i64_type); + hir_intrin->result_vals[0] = 31; + hir_intrin->result_vals[1] = 32; + intrin->extra.aux = hir_intrin; + + opt_lower_to_mir(f, NULL); + phi = f->mir ? find_inst(&f->mir->blocks[entry], phi_id) : NULL; + sw = f->mir ? find_inst(&f->mir->blocks[entry], switch_id) : NULL; + indirect = + f->mir ? find_inst(&f->mir->blocks[entry], indirect_id) : NULL; + intrin = f->mir ? find_inst(&f->mir->blocks[entry], intrin_id) : NULL; + mir_phi = phi ? (IRPhiAux*)phi->extra.aux : NULL; + mir_switch = sw ? (IRSwitchAux*)sw->extra.aux : NULL; + mir_indirect = indirect ? (IRIndirectAux*)indirect->extra.aux : NULL; + mir_intrin = intrin ? (IRIntrinAux*)intrin->extra.aux : NULL; + + CU_EXPECT(u, + mir_phi && mir_phi != hir_phi && + mir_phi->pred_blocks != hir_phi->pred_blocks && + mir_phi->pred_vals != hir_phi->pred_vals, + "phi aux arrays alias semantic HIR"); + CU_EXPECT(u, + mir_switch && mir_switch != hir_switch && + mir_switch->cases != hir_switch->cases, + "switch case array aliases semantic HIR"); + CU_EXPECT(u, + mir_indirect && mir_indirect != hir_indirect && + mir_indirect->targets != hir_indirect->targets, + "indirect-branch target array aliases semantic HIR"); + CU_EXPECT(u, + mir_intrin && mir_intrin != hir_intrin && + mir_intrin->dsts != hir_intrin->dsts && + mir_intrin->args != hir_intrin->args && + mir_intrin->result_vals != hir_intrin->result_vals, + "intrinsic aux arrays alias semantic HIR"); + CU_EXPECT(u, + mir_phi && mir_phi->pred_blocks[0] == 3 && + mir_phi->pred_vals[1] == 14 && mir_switch && + mir_switch->cases[1].value == 22 && + mir_switch->cases[1].block == 6 && mir_indirect && + mir_indirect->targets[1] == 8 && mir_intrin && + mir_intrin->result_vals[0] == 31 && + mir_intrin->dsts[0].v.reg == 8 && + mir_intrin->dsts[1].v.reg == 9 && + mir_intrin->args[0].v.reg == 10 && + mir_intrin->args[1].v.reg == 11, + "structured aux payloads did not survive location-MIR cloning"); + + if (mir_phi) { + mir_phi->pred_blocks[0] = 103; + mir_phi->pred_vals[0] = 113; + } + if (mir_switch) { + mir_switch->cases[0].value = 121; + mir_switch->cases[0].block = 105; + } + if (mir_indirect) mir_indirect->targets[0] = 107; + if (mir_intrin) { + mir_intrin->dsts[0].kind = OPK_IMM; + mir_intrin->dsts[0].v.imm = 131; + mir_intrin->args[0].kind = OPK_IMM; + mir_intrin->args[0].v.imm = 132; + mir_intrin->result_vals[0] = 133; + } + CU_EXPECT(u, + hir_phi->pred_blocks[0] == 3 && hir_phi->pred_vals[0] == 13 && + hir_switch->cases[0].value == 21 && + hir_switch->cases[0].block == 5 && + hir_indirect->targets[0] == 7 && + hir_intrin->dsts[0].kind == OPK_REG && + hir_intrin->dsts[0].v.reg == dst0 && + hir_intrin->args[0].kind == OPK_REG && + hir_intrin->args[0].v.reg == arg0 && + hir_intrin->result_vals[0] == 31, + "mutating MIR structured aux arrays changed semantic HIR"); + + hir_phi->pred_blocks[1] = 204; + hir_phi->pred_vals[1] = 214; + hir_switch->cases[1].value = 222; + hir_switch->cases[1].block = 206; + hir_indirect->targets[1] = 208; + hir_intrin->dsts[1].v.reg = dst0; + hir_intrin->args[1].v.reg = arg0; + hir_intrin->result_vals[1] = 232; + CU_EXPECT(u, + mir_phi && mir_phi->pred_blocks[1] == 4 && + mir_phi->pred_vals[1] == 14 && mir_switch && + mir_switch->cases[1].value == 22 && + mir_switch->cases[1].block == 6 && mir_indirect && + mir_indirect->targets[1] == 8 && mir_intrin && + mir_intrin->dsts[1].v.reg == 9 && + mir_intrin->args[1].v.reg == 11 && + mir_intrin->result_vals[1] == 32, + "mutating HIR structured aux arrays changed location MIR"); + + kit_compiler_free(kc); +} + +static void asm_aux_resolved_metadata_is_independent(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + KitCgTypeId i64_type; + CGFuncDesc desc; + PReg out_reg, in_reg; + IRAsmAux* hir_aux; + IRAsmAux* mir_aux; + Inst* asm_in; + InstId asm_id; + FrameSlot memory_slot; + FrameSlotDesc memory_desc; + Sym out_name, in_name, clobber_name; + u32 entry; + + CU_EXPECT(u, + kit_unit_compiler_new( + u, kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed for asm-aux clone test"); + if (!kc) return; + c = (Compiler*)kc; + i64_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + out_name = kit_sym_intern(kc, KIT_SLICE_LIT("out")); + in_name = kit_sym_intern(kc, KIT_SLICE_LIT("in")); + clobber_name = kit_sym_intern(kc, KIT_SLICE_LIT("x12")); + + memset(&desc, 0, sizeof desc); + desc.fn_type = i64_type; + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + + out_reg = ir_alloc_preg(f, i64_type, RC_INT); + in_reg = ir_alloc_preg(f, i64_type, RC_INT); + f->preg_locs = arena_zarray(f->arena, OptLoc, f->npregs); + f->preg_info = arena_zarray(f->arena, OptPRegInfo, f->npregs); + set_hard_loc(f, out_reg, 6); + set_hard_loc(f, in_reg, 7); + memset(&memory_desc, 0, sizeof memory_desc); + memory_desc.type = i64_type; + memory_desc.size = 8; + memory_desc.align = 8; + memory_desc.kind = FS_LOCAL; + memory_slot = ir_frame_slot_new(f, &memory_desc); + + asm_in = ir_emit(f, entry, IR_ASM_BLOCK); + asm_id = asm_in->id; + asm_in->def = out_reg; + hir_aux = arena_znew(f->arena, IRAsmAux); + hir_aux->tmpl = "add %0, %1"; + hir_aux->nout = 1; + hir_aux->nin = 2; + hir_aux->nclob = 1; + hir_aux->outs = arena_zarray(f->arena, AsmConstraint, hir_aux->nout); + hir_aux->ins = arena_zarray(f->arena, AsmConstraint, hir_aux->nin); + hir_aux->clobbers = arena_array(f->arena, Sym, hir_aux->nclob); + hir_aux->out_ops = arena_zarray(f->arena, Operand, hir_aux->nout); + hir_aux->in_ops = arena_zarray(f->arena, Operand, hir_aux->nin); + hir_aux->out_reg_reqs = + arena_zarray(f->arena, IRAsmRegRequirement, hir_aux->nout); + hir_aux->in_reg_reqs = + arena_zarray(f->arena, IRAsmRegRequirement, hir_aux->nin); + hir_aux->outs[0].str = "=r"; + hir_aux->outs[0].name = out_name; + hir_aux->outs[0].type = i64_type; + hir_aux->outs[0].dir = KIT_CG_ASM_OUT; + hir_aux->ins[0].str = "r"; + hir_aux->ins[0].name = in_name; + hir_aux->ins[0].type = i64_type; + hir_aux->ins[0].dir = KIT_CG_ASM_IN; + hir_aux->ins[1].str = "m"; + hir_aux->ins[1].type = i64_type; + hir_aux->ins[1].dir = KIT_CG_ASM_IN; + hir_aux->clobbers[0] = clobber_name; + hir_aux->out_ops[0] = reg_op(out_reg, i64_type); + hir_aux->in_ops[0] = reg_op(in_reg, i64_type); + hir_aux->in_ops[1].kind = OPK_LOCAL; + hir_aux->in_ops[1].cls = RC_INT; + hir_aux->in_ops[1].type = i64_type; + hir_aux->in_ops[1].v.frame_slot = memory_slot; + hir_aux->out_reg_reqs[0].allowed_mask = 0x000000f0u; + hir_aux->out_reg_reqs[0].fixed_reg = 6; + hir_aux->out_reg_reqs[0].cls = RC_INT; + hir_aux->out_reg_reqs[0].present = 1; + hir_aux->in_reg_reqs[0].allowed_mask = 0x0000ff00u; + hir_aux->in_reg_reqs[0].fixed_reg = -1; + hir_aux->in_reg_reqs[0].cls = RC_INT; + hir_aux->in_reg_reqs[0].present = 1; + hir_aux->in_reg_reqs[1].fixed_reg = -1; + hir_aux->clobber_mask[RC_INT] = (1u << 3) | (1u << 12); + hir_aux->clobber_mask[RC_FP] = 1u << 9; + hir_aux->clobber_mask[RC_VEC] = 1u << 2; + hir_aux->clobber_abi_sets = KIT_CG_ASM_CLOBBER_ABI_CALLER_SAVED; + hir_aux->has_memory_constraint = 1; + asm_in->extra.aux = hir_aux; + + opt_lower_to_mir(f, NULL); + asm_in = f->mir ? find_inst(&f->mir->blocks[entry], asm_id) : NULL; + mir_aux = asm_in ? (IRAsmAux*)asm_in->extra.aux : NULL; + CU_EXPECT(u, asm_in && mir_aux && mir_aux != hir_aux, + "asm aux object aliases HIR after MIR lowering"); + CU_EXPECT(u, + mir_aux && mir_aux->outs != hir_aux->outs && + mir_aux->ins != hir_aux->ins && + mir_aux->clobbers != hir_aux->clobbers && + mir_aux->out_ops != hir_aux->out_ops && + mir_aux->in_ops != hir_aux->in_ops && + mir_aux->out_reg_reqs != hir_aux->out_reg_reqs && + mir_aux->in_reg_reqs != hir_aux->in_reg_reqs, + "asm aux nested mutable arrays alias HIR"); + CU_EXPECT(u, + mir_aux && + memcmp(mir_aux->clobber_mask, hir_aux->clobber_mask, + sizeof hir_aux->clobber_mask) == 0 && + mir_aux->clobber_abi_sets == hir_aux->clobber_abi_sets && + mir_aux->has_memory_constraint == + hir_aux->has_memory_constraint && + memcmp(mir_aux->out_reg_reqs, hir_aux->out_reg_reqs, + sizeof hir_aux->out_reg_reqs[0]) == 0 && + memcmp(mir_aux->in_reg_reqs, hir_aux->in_reg_reqs, + sizeof hir_aux->in_reg_reqs[0] * hir_aux->nin) == 0, + "resolved asm placement metadata did not survive MIR cloning"); + CU_EXPECT(u, + hir_aux->out_ops[0].kind == OPK_REG && + hir_aux->out_ops[0].v.reg == out_reg && + hir_aux->in_ops[0].kind == OPK_REG && + hir_aux->in_ops[0].v.reg == in_reg && mir_aux && + mir_aux->out_ops[0].kind == OPK_REG && + mir_aux->out_ops[0].v.reg == 6 && + mir_aux->in_ops[0].kind == OPK_REG && + mir_aux->in_ops[0].v.reg == 7 && + hir_aux->in_ops[1].kind == OPK_LOCAL && + hir_aux->in_ops[1].v.frame_slot == memory_slot && + mir_aux->in_ops[1].kind == OPK_LOCAL && + mir_aux->in_ops[1].v.frame_slot == memory_slot, + "asm operand rewrite crossed the HIR/MIR ownership boundary"); + + if (mir_aux) { + mir_aux->out_reg_reqs[0].allowed_mask = 1u << 20; + mir_aux->out_reg_reqs[0].fixed_reg = 20; + mir_aux->in_reg_reqs[0].present = 0; + mir_aux->clobber_mask[RC_INT] = 1u << 22; + mir_aux->has_memory_constraint = 0; + mir_aux->outs[0].name = in_name; + mir_aux->ins[0].str = "m"; + mir_aux->clobbers[0] = out_name; + mir_aux->out_ops[0].v.reg = 20; + mir_aux->in_ops[0].kind = OPK_IMM; + mir_aux->in_ops[0].v.imm = 42; + } + CU_EXPECT(u, + hir_aux->out_reg_reqs[0].allowed_mask == 0x000000f0u && + hir_aux->out_reg_reqs[0].fixed_reg == 6 && + hir_aux->in_reg_reqs[0].present == 1 && + hir_aux->clobber_mask[RC_INT] == + ((1u << 3) | (1u << 12)) && + hir_aux->has_memory_constraint == 1 && + hir_aux->outs[0].name == out_name && + strcmp(hir_aux->ins[0].str, "r") == 0 && + hir_aux->clobbers[0] == clobber_name && + hir_aux->out_ops[0].kind == OPK_REG && + hir_aux->out_ops[0].v.reg == out_reg && + hir_aux->in_ops[0].kind == OPK_REG && + hir_aux->in_ops[0].v.reg == in_reg, + "mutating MIR asm metadata changed the semantic HIR graph"); + + /* Prove independence in the other direction as well: the already-mutated + * MIR requirements and operands must not observe later HIR changes. */ + hir_aux->out_reg_reqs[0].allowed_mask = 1u; + hir_aux->in_ops[0].v.reg = out_reg; + CU_EXPECT(u, + mir_aux && mir_aux->out_reg_reqs[0].allowed_mask == (1u << 20) && + mir_aux->in_ops[0].kind == OPK_IMM && + mir_aux->in_ops[0].v.imm == 42, + "mutating HIR asm metadata changed the location MIR graph"); + + kit_compiler_free(kc); +} + +int main(void) { + KitUnit u; + kit_unit_init(&u); + machinize_rejects_cross_owner_registers(&u); + mir_verify_rejects_private_temp_escape(&u); + mir_commit_propagates_instruction_namespace(&u); + spilled_values_remain_locations(&u); + spilled_frame_address_remains_recipe(&u); + call_aux_is_independent(&u); + structured_aux_arrays_are_independent(&u); + asm_aux_resolved_metadata_is_independent(&u); + kit_unit_summary(&u, "location-mir"); + return kit_unit_status(&u); +} diff --git a/test/opt/macho_block_alt_entry.sh b/test/opt/macho_block_alt_entry.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Mach-O MH_SUBSECTIONS_VIA_SYMBOLS regression: relocation-anchor symbols for +# switch/label blocks must be N_ALT_ENTRY symbols pinned inside their function, +# not independently collectible atoms. Otherwise --gc-sections can retain the +# function's entry atom while dropping a directly-branched-to switch dispatch +# or common return block (intra-function branches are already resolved and do +# not carry linker relocations). +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +KIT="${KIT:-$ROOT/build/kit}" +ROUNDTRIP="${ROUNDTRIP:-$ROOT/build/test/kit-roundtrip-macho}" +WORK="$ROOT/build/test/opt/macho_block_alt_entry" +mkdir -p "$WORK" + +cat > "$WORK/dispatch.c" <<'EOF' +int dispatch(unsigned x) { + if (x < 256) return 7; + switch (x) { + case 256: return 11; + case 257: return 13; + case 258: return 17; + case 259: return 19; + case 260: return 23; + case 261: return 29; + case 262: return 31; + case 263: return 37; + case 264: return 41; + default: return 43; + } +} +EOF + +cat > "$WORK/main.c" <<'EOF' +extern int dispatch(unsigned); +int main(void) { return dispatch(123) == 7 ? 0 : 1; } +EOF + +COMMON=(-target aarch64-apple-darwin -O1 -ffunction-sections + -fdata-sections -ffreestanding) +"$KIT" cc "${COMMON[@]}" -c "$WORK/dispatch.c" -o "$WORK/dispatch.o" \ + > "$WORK/dispatch.cc.out" 2>&1 +"$KIT" cc "${COMMON[@]}" -c "$WORK/main.c" -o "$WORK/main.o" \ + > "$WORK/main.cc.out" 2>&1 + +# Read and re-emit the defining object before linking. This makes the test +# cover both halves of the format boundary: Mach-O read must collapse pinned +# block symbols into their owner atom, and emit must preserve the subordinate +# semantic as N_ALT_ENTRY on the second object too. +"$ROUNDTRIP" --require-atom-subordinate "$WORK/dispatch.o" \ + "$WORK/dispatch.roundtrip.o" > "$WORK/dispatch.roundtrip.out" 2>&1 + +# Force the serialized Mach-O reader/linker path: compiling and linking in one +# driver invocation can keep the original ObjBuilder and would not exercise +# MH_SUBSECTIONS_VIA_SYMBOLS reconstruction. +"$KIT" cc -target aarch64-apple-darwin -nostdlib -Wl,--gc-sections \ + -Wl,-e,main "$WORK/main.o" "$WORK/dispatch.roundtrip.o" -o "$WORK/linked" \ + > "$WORK/link.out" 2>&1 +"$KIT" objdump -d "$WORK/linked" > "$WORK/linked.dis" 2>&1 + +# The case body and indirect table dispatch live after block-label boundaries. +# Without N_ALT_ENTRY they are collected and the surviving fast-path branches +# outside __text; retaining both instructions pins the full function content. +if ! grep -Eq '\bmovz[[:space:]]+w0, 0xb\b' "$WORK/linked.dis"; then + printf 'macho_block_alt_entry FAILED: case block was GC-collected\n' >&2 + sed 's/^/ | /' "$WORK/linked.dis" >&2 + exit 1 +fi +if ! grep -Eq '\bbr[[:space:]]+x[0-9]+\b' "$WORK/linked.dis"; then + printf 'macho_block_alt_entry FAILED: switch dispatch was GC-collected\n' >&2 + sed 's/^/ | /' "$WORK/linked.dis" >&2 + exit 1 +fi + +printf 'macho_block_alt_entry: OK (block anchors stay pinned through Mach-O GC)\n' diff --git a/test/opt/native_emit_frame_dst_test.c b/test/opt/native_emit_frame_dst_test.c @@ -0,0 +1,1751 @@ +#include <stdio.h> +#include <string.h> + +#include "lib/kit_unit.h" +#include "opt/opt.h" + +/* NativeTarget's vtable was declared against the semantic descriptor before + * opt/ir.h introduced its optimizer-private compatibility macro. Keep the + * callback signature on that original boundary; use OptCGFuncDesc explicitly + * when constructing the optimizer Func below. */ +#undef CGFuncDesc + +typedef struct MockNative { + NativeTarget base; + KitUnit *unit; + NativeAllocClassInfo classes[2]; + NativeRegInfo regs; + Reg int_temps[4]; + NativeCallPlanMove call_arg; + NativeCallPlanRet call_ret; + NativeCallPlanMove phase_call_args[2]; + NativeCallPlanRet ret_parts[2]; + u32 callback_mask; + u32 writeback_slots; + u32 loads; + u32 load_addrs; + u32 frame_loads; + u32 reg_address_loads; + u32 in_place_reg_address_loads; + u32 reg_address_stores; + u32 in_place_reg_address_bitfield_loads; + u32 atomic_loads; + u32 atomic_stores; + Reg frame_load_dsts[16]; + u32 nframe_load_dsts; + u32 reg_values[32]; + u32 phase_callee_value; + u32 phase_arg_values[2]; + u32 calls; + u8 report_bitfield_store_clobber; + u8 plan_two_arg_indirect_call; + u8 plan_two_part_ret; + u8 reject_indexed_addr; + MCLabel next_label; +} MockNative; + +enum { + SAW_LOAD_CONST = 1u << 0, + SAW_TLS_ADDR = 1u << 1, + SAW_BITFIELD_LOAD = 1u << 2, + SAW_LABEL_ADDR = 1u << 3, + SAW_ALLOCA = 1u << 4, + SAW_INTRINSIC = 1u << 5, + SAW_BINOP = 1u << 6, + SAW_ATOMIC_RMW = 1u << 7, + SAW_ATOMIC_CAS = 1u << 8, + SAW_BITFIELD_STORE = 1u << 9, +}; + +static MockNative *mock_of(NativeTarget *target) { + return (MockNative *)target; +} + +static void expect_reg(MockNative *mock, NativeLoc loc, const char *what) { + CU_EXPECT(mock->unit, loc.kind == NATIVE_LOC_REG, + "%s must receive a physical register, got location kind %u", what, + (unsigned)loc.kind); +} + +static NativeAllocClass mock_class_for_type(NativeTarget *target, + KitCgTypeId type) { + return cg_type_is_float(target->c, type) ? NATIVE_REG_FP : NATIVE_REG_INT; +} + +static int mock_addr_legal(NativeTarget *target, const NativeAddr *addr, + MemAccess mem) { + (void)mem; + return !mock_of(target)->reject_indexed_addr || + addr->index_kind == NATIVE_ADDR_INDEX_NONE; +} + +static void mock_func_begin_known_frame(NativeTarget *target, + const CGFuncDesc *desc, + const NativeKnownFrameDesc *frame, + NativeFrameSlot *out_slots) { + (void)target; + (void)desc; + for (u32 i = 0; i < frame->nslots; ++i) + out_slots[i] = i + 1u; +} + +static void mock_func_end(NativeTarget *target) { (void)target; } + +static MCLabel mock_label_new(NativeTarget *target) { + return ++mock_of(target)->next_label; +} + +static void mock_label_place(NativeTarget *target, MCLabel label) { + (void)target; + (void)label; +} + +static void mock_load_imm(NativeTarget *target, NativeLoc dst, i64 imm) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "load_imm destination"); + if (dst.v.reg < 32u) mock->reg_values[dst.v.reg] = (u32)imm; +} + +static void mock_move(NativeTarget *target, NativeLoc dst, NativeLoc src) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "move destination"); + expect_reg(mock, src, "move source"); + if (dst.v.reg < 32u && src.v.reg < 32u) + mock->reg_values[dst.v.reg] = mock->reg_values[src.v.reg]; +} + +static void mock_load_const(NativeTarget *target, NativeLoc dst, + ConstBytes bytes) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "load_const destination"); + CU_EXPECT(mock->unit, bytes.size == 8u, + "load_const byte payload should remain intact"); + mock->callback_mask |= SAW_LOAD_CONST; +} + +static void mock_load(NativeTarget *target, NativeLoc dst, NativeAddr addr, + MemAccess mem) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "frame materialization destination"); + CU_EXPECT(mock->unit, mem.size != 0u, + "frame materialization should retain its access width"); + if (addr.base_kind == NATIVE_ADDR_BASE_FRAME) { + if (mock->nframe_load_dsts < + sizeof mock->frame_load_dsts / sizeof mock->frame_load_dsts[0]) + mock->frame_load_dsts[mock->nframe_load_dsts++] = dst.v.reg; + ++mock->frame_loads; + if (dst.v.reg < 32u) + mock->reg_values[dst.v.reg] = addr.base.frame; + } else if (addr.base_kind == NATIVE_ADDR_BASE_REG) { + ++mock->reg_address_loads; + if (dst.v.reg == addr.base.reg) ++mock->in_place_reg_address_loads; + } else { + CU_EXPECT(mock->unit, 0, + "mock load requires a direct frame or register address, got %u", + (unsigned)addr.base_kind); + } + ++mock->loads; +} + +static void mock_load_addr(NativeTarget *target, NativeLoc dst, + NativeAddr addr) { + MockNative *mock = mock_of(target); + u32 value = 0; + expect_reg(mock, dst, "address materialization destination"); + CU_EXPECT(mock->unit, + addr.base_kind == NATIVE_ADDR_BASE_REG && + addr.index_kind == NATIVE_ADDR_INDEX_REG, + "pressure regression must materialize a register base+index"); + CU_EXPECT(mock->unit, dst.v.reg == addr.base.reg, + "scoped address base should be reusable when no third temp exists"); + if (addr.base_kind == NATIVE_ADDR_BASE_REG && addr.base.reg < 32u) + value = mock->reg_values[addr.base.reg]; + if (addr.index_kind == NATIVE_ADDR_INDEX_REG && addr.index.reg < 32u) + value += mock->reg_values[addr.index.reg] << addr.log2_scale; + value += (u32)addr.offset; + if (dst.v.reg < 32u) mock->reg_values[dst.v.reg] = value; + ++mock->load_addrs; +} + +static void mock_store(NativeTarget *target, NativeAddr addr, NativeLoc src, + MemAccess mem) { + MockNative *mock = mock_of(target); + expect_reg(mock, src, "frame writeback source"); + CU_EXPECT(mock->unit, mem.size != 0u, + "frame writeback should retain its access width"); + if (addr.base_kind == NATIVE_ADDR_BASE_FRAME) { + CU_EXPECT(mock->unit, addr.base.frame > 0u && addr.base.frame < 32u, + "frame writeback slot should be mapped and in test range"); + if (addr.base.frame < 32u) + mock->writeback_slots |= 1u << addr.base.frame; + } else if (addr.base_kind == NATIVE_ADDR_BASE_REG) { + CU_EXPECT(mock->unit, src.v.reg != addr.base.reg, + "store value must remain distinct from its address base"); + ++mock->reg_address_stores; + } else { + CU_EXPECT(mock->unit, 0, + "mock store requires a direct frame or register address, got %u", + (unsigned)addr.base_kind); + } +} + +static void mock_tls_addr_of(NativeTarget *target, NativeLoc dst, ObjSymId sym, + i64 addend) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "tls_addr_of destination"); + CU_EXPECT(mock->unit, sym == 17u && addend == 9, + "tls address metadata should remain intact"); + mock->callback_mask |= SAW_TLS_ADDR; +} + +static void mock_bitfield_load(NativeTarget *target, NativeLoc dst, + NativeAddr addr, BitFieldAccess access) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "bitfield_load destination"); + CU_EXPECT(mock->unit, + addr.base_kind == NATIVE_ADDR_BASE_FRAME || + addr.base_kind == NATIVE_ADDR_BASE_REG, + "bitfield record should remain a frame or register address"); + if (addr.base_kind == NATIVE_ADDR_BASE_REG && dst.v.reg == addr.base.reg) + ++mock->in_place_reg_address_bitfield_loads; + CU_EXPECT(mock->unit, access.bit_offset == 3u && access.bit_width == 11u, + "bitfield geometry should remain intact"); + mock->callback_mask |= SAW_BITFIELD_LOAD; +} + +static void mock_bitfield_store(NativeTarget *target, NativeAddr addr, + NativeLoc src, BitFieldAccess access) { + MockNative *mock = mock_of(target); + expect_reg(mock, src, "bitfield_store source"); + CU_EXPECT(mock->unit, addr.base_kind == NATIVE_ADDR_BASE_FRAME, + "bitfield store record should remain a frame address"); + CU_EXPECT(mock->unit, access.bit_offset == 3u && access.bit_width == 11u, + "bitfield store geometry should remain intact"); + mock->callback_mask |= SAW_BITFIELD_STORE; +} + +static int mock_machine_op_clobbers( + NativeTarget *target, const NativeMachineOp *op, + u32 mask[NATIVE_CALL_PLAN_CLASSES]) { + MockNative *mock = mock_of(target); + mask[0] = mask[1] = mask[2] = 0u; + if (!mock->report_bitfield_store_clobber || + op->kind != NATIVE_MOP_BITFIELD_STORE) + return 0; + mask[NATIVE_REG_INT] = 1u << 10u; + return 1; +} + +static void mock_load_label_addr(NativeTarget *target, NativeLoc dst, + MCLabel label) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "load_label_addr destination"); + CU_EXPECT(mock->unit, label != MC_LABEL_NONE, + "label address should reference an allocated native label"); + mock->callback_mask |= SAW_LABEL_ADDR; +} + +static void mock_alloca(NativeTarget *target, NativeLoc dst, NativeLoc size, + u32 align) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "alloca destination"); + expect_reg(mock, size, "alloca size"); + CU_EXPECT(mock->unit, dst.v.reg != size.v.reg, + "alloca destination must not clobber its size input"); + CU_EXPECT(mock->unit, align == 32u, "alloca alignment should remain intact"); + mock->callback_mask |= SAW_ALLOCA; +} + +static void mock_binop(NativeTarget *target, BinOp op, NativeLoc dst, + NativeLoc lhs, NativeLoc rhs) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "binop destination"); + expect_reg(mock, lhs, "binop lhs"); + expect_reg(mock, rhs, "binop rhs"); + CU_EXPECT(mock->unit, op == BO_IADD, "binop opcode should remain intact"); + CU_EXPECT(mock->unit, dst.v.reg != rhs.v.reg && lhs.v.reg != rhs.v.reg, + "all-stack binop must keep its rhs distinct while allowing the " + "dead lhs register to become the destination"); + mock->callback_mask |= SAW_BINOP; +} + +static void mock_atomic_rmw(NativeTarget *target, KitCgAtomicOp op, + NativeLoc dst, NativeAddr addr, NativeLoc val, + MemAccess mem, KitCgMemOrder order) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "atomic_rmw destination"); + expect_reg(mock, val, "atomic_rmw value"); + CU_EXPECT(mock->unit, dst.v.reg != val.v.reg, + "atomic_rmw destination must not alias its value input"); + CU_EXPECT(mock->unit, addr.base_kind == NATIVE_ADDR_BASE_FRAME, + "atomic_rmw address should retain direct frame storage"); + CU_EXPECT(mock->unit, + op == KIT_CG_ATOMIC_ADD && mem.size == 8u && + order == KIT_CG_MO_SEQ_CST, + "atomic_rmw metadata should remain intact"); + mock->callback_mask |= SAW_ATOMIC_RMW; +} + +static void mock_atomic_load(NativeTarget *target, NativeLoc dst, + NativeAddr addr, MemAccess mem, + KitCgMemOrder order) { + MockNative *mock = mock_of(target); + expect_reg(mock, dst, "atomic_load destination"); + CU_EXPECT(mock->unit, + addr.base_kind == NATIVE_ADDR_BASE_REG && + addr.index_kind == NATIVE_ADDR_INDEX_NONE && + dst.v.reg == addr.base.reg, + "atomic load should consume and reuse one collapsed scoped base"); + CU_EXPECT(mock->unit, + mem.size == 8u && order == KIT_CG_MO_SEQ_CST, + "atomic load metadata should remain intact"); + ++mock->atomic_loads; +} + +static void mock_atomic_store(NativeTarget *target, NativeAddr addr, + NativeLoc val, MemAccess mem, + KitCgMemOrder order) { + MockNative *mock = mock_of(target); + expect_reg(mock, val, "atomic_store value"); + CU_EXPECT(mock->unit, addr.base_kind == NATIVE_ADDR_BASE_REG, + "atomic store pressure address should be materialized"); + CU_EXPECT(mock->unit, + addr.base_kind != NATIVE_ADDR_BASE_REG || + val.v.reg != addr.base.reg, + "atomic store reused the overwritten address-base cache fact"); + CU_EXPECT(mock->unit, + mem.size == 8u && order == KIT_CG_MO_SEQ_CST, + "atomic store metadata should remain intact"); + ++mock->atomic_stores; +} + +static void mock_atomic_cas(NativeTarget *target, NativeLoc prior, NativeLoc ok, + NativeAddr addr, NativeLoc expected, + NativeLoc desired, MemAccess mem, + KitCgMemOrder success, KitCgMemOrder failure) { + MockNative *mock = mock_of(target); + NativeLoc locs[4] = {prior, ok, expected, desired}; + for (u32 i = 0; i < 4u; ++i) + expect_reg(mock, locs[i], "atomic_cas scalar operand"); + for (u32 i = 0; i < 4u; ++i) + for (u32 j = i + 1u; j < 4u; ++j) + CU_EXPECT(mock->unit, locs[i].v.reg != locs[j].v.reg, + "all-stack atomic_cas must lease four simultaneous registers"); + CU_EXPECT(mock->unit, addr.base_kind == NATIVE_ADDR_BASE_FRAME, + "atomic_cas address should retain direct frame storage"); + CU_EXPECT(mock->unit, + mem.size == 8u && success == KIT_CG_MO_ACQ_REL && + failure == KIT_CG_MO_ACQUIRE, + "atomic_cas metadata should remain intact"); + mock->callback_mask |= SAW_ATOMIC_CAS; +} + +static void mock_intrinsic(NativeTarget *target, IntrinKind kind, + const NativeLoc *dsts, u32 ndst, + const NativeLoc *args, u32 narg) { + MockNative *mock = mock_of(target); + CU_EXPECT(mock->unit, + kind == INTRIN_UADD_OVERFLOW && ndst == 2u && narg == 2u, + "multi-result intrinsic shape should remain intact"); + for (u32 i = 0; i < ndst; ++i) + expect_reg(mock, dsts[i], "intrinsic destination"); + for (u32 i = 0; i < narg; ++i) + expect_reg(mock, args[i], "intrinsic argument"); + if (ndst == 2u && narg == 2u) { + Reg regs[4] = {dsts[0].v.reg, dsts[1].v.reg, args[0].v.reg, args[1].v.reg}; + for (u32 i = 0; i < 4u; ++i) + for (u32 j = i + 1u; j < 4u; ++j) + CU_EXPECT(mock->unit, regs[i] != regs[j], + "simultaneous intrinsic operands must have distinct temps"); + } + mock->callback_mask |= SAW_INTRINSIC; +} + +static NativeLoc mock_reg_loc(KitCgTypeId type, NativeAllocClass cls, Reg reg) { + NativeLoc loc; + memset(&loc, 0, sizeof loc); + loc.kind = NATIVE_LOC_REG; + loc.cls = (u8)cls; + loc.type = type; + loc.v.reg = reg; + return loc; +} + +/* 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) { + MockNative *mock = mock_of(target); + if (mock->plan_two_arg_indirect_call) { + NativeLoc staged_callee; + CU_EXPECT(mock->unit, + desc->nargs == 2u && desc->nresults == 0u && + desc->callee.kind == NATIVE_LOC_REG, + "indirect callee must be materialized before the argument plan"); + if (desc->nargs != 2u || desc->nresults != 0u) + return; + if (desc->callee.kind == NATIVE_LOC_REG) { + staged_callee = + mock_reg_loc(desc->callee.type, NATIVE_REG_INT, 12u); + mock_move(target, staged_callee, desc->callee); + plan->callee = staged_callee; + mock->phase_callee_value = mock->reg_values[staged_callee.v.reg]; + } else { + /* Keep the mock executable against the pre-fix ordering too: the + * emitter used to materialize this plan location after its argument + * moves. The expectation above remains the focused red assertion. */ + plan->callee = desc->callee; + if (desc->callee.kind == NATIVE_LOC_FRAME) + mock->phase_callee_value = desc->callee.v.frame; + } + for (u32 i = 0; i < 2u; ++i) { + memset(&mock->phase_call_args[i], 0, + sizeof mock->phase_call_args[i]); + mock->phase_call_args[i].src = desc->args[i]; + mock->phase_call_args[i].dst = + mock_reg_loc(desc->args[i].type, NATIVE_REG_INT, (Reg)(8u + i)); + mock->phase_call_args[i].mem.type = desc->args[i].type; + mock->phase_call_args[i].mem.size = 8u; + mock->phase_call_args[i].mem.align = 8u; + CU_EXPECT(mock->unit, desc->args[i].kind == NATIVE_LOC_FRAME, + "planned call argument %u must retain its frame source", i); + if (desc->args[i].kind == NATIVE_LOC_FRAME) + mock->phase_arg_values[i] = desc->args[i].v.frame; + } + plan->args = mock->phase_call_args; + plan->nargs = 2u; + return; + } + CU_EXPECT(mock->unit, desc->nargs == 1u && desc->nresults == 1u, + "call phase test requires one argument and one result"); + if (desc->nargs != 1u || desc->nresults != 1u) + return; + memset(&mock->call_arg, 0, sizeof mock->call_arg); + memset(&mock->call_ret, 0, sizeof mock->call_ret); + plan->callee = desc->callee; + mock->call_arg.src = desc->args[0]; + mock->call_arg.dst = mock_reg_loc(desc->args[0].type, NATIVE_REG_INT, 8u); + mock->call_arg.mem.type = desc->args[0].type; + mock->call_arg.mem.size = 8u; + mock->call_arg.mem.align = 8u; + plan->args = &mock->call_arg; + plan->nargs = 1u; + mock->call_ret.src = desc->args[0]; + mock->call_ret.dst = desc->results[0]; + mock->call_ret.mem.type = desc->results[0].type; + mock->call_ret.mem.size = 8u; + 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) { + MockNative *mock = mock_of(target); + if (mock->plan_two_arg_indirect_call) { + CU_EXPECT(mock->unit, + plan->callee.kind == NATIVE_LOC_REG && plan->nargs == 2u && + plan->nrets == 0u, + "two-argument indirect call plan changed before emission"); + if (plan->callee.kind == NATIVE_LOC_REG) { + CU_EXPECT(mock->unit, + mock->reg_values[plan->callee.v.reg] == + mock->phase_callee_value, + "argument staging overwrote the indirect callee"); + } + CU_EXPECT(mock->unit, + mock->reg_values[8u] == mock->phase_arg_values[0], + "later call setup overwrote completed ABI argument r8"); + CU_EXPECT(mock->unit, + mock->reg_values[9u] == mock->phase_arg_values[1], + "second ABI argument did not reach r9"); + ++mock->calls; + return; + } + CU_EXPECT(mock->unit, plan->nargs == 1u && plan->nrets == 1u, + "call plan shape changed before emission"); + ++mock->calls; +} + +static void mock_plan_ret(NativeTarget *target, const CGFuncDesc *desc, + const NativeLoc *value, + NativeCallPlanRet **out_rets, u32 *out_nrets) { + MockNative *mock = mock_of(target); + (void)desc; + if (!mock->plan_two_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"); + if (!value || value->kind != NATIVE_LOC_FRAME) { + *out_rets = NULL; + *out_nrets = 0u; + return; + } + for (u32 i = 0; i < 2u; ++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)); + mock->ret_parts[i].dst = + mock_reg_loc(value->type, NATIVE_REG_INT, (Reg)(8u + i)); + mock->ret_parts[i].mem.type = value->type; + mock->ret_parts[i].mem.size = 8u; + mock->ret_parts[i].mem.align = 8u; + } + *out_rets = mock->ret_parts; + *out_nrets = 2u; +} + +static void mock_ret(NativeTarget *target) { (void)target; } + +static void mock_init(MockNative *mock, KitUnit *unit, Compiler *c) { + memset(mock, 0, sizeof *mock); + mock->unit = unit; + mock->base.c = c; + mock->int_temps[0] = 8u; + mock->int_temps[1] = 9u; + mock->classes[NATIVE_REG_INT].cls = NATIVE_REG_INT; + mock->classes[NATIVE_REG_INT].scratch = mock->int_temps; + mock->classes[NATIVE_REG_INT].nscratch = 2u; + mock->classes[NATIVE_REG_INT].emit_temps = mock->int_temps; + mock->classes[NATIVE_REG_INT].nemit_temps = 2u; + mock->classes[NATIVE_REG_INT].emit_cache_mask = + (1u << mock->int_temps[0]) | (1u << mock->int_temps[1]); + mock->classes[NATIVE_REG_FP].cls = NATIVE_REG_FP; + mock->regs.classes = mock->classes; + mock->regs.nclasses = 2u; + mock->base.regs = &mock->regs; + mock->base.class_for_type = mock_class_for_type; + mock->base.addr_legal = mock_addr_legal; + mock->base.func_begin_known_frame = mock_func_begin_known_frame; + mock->base.func_end = mock_func_end; + mock->base.label_new = mock_label_new; + mock->base.label_place = mock_label_place; + mock->base.load_imm = mock_load_imm; + mock->base.move = mock_move; + mock->base.load_const = mock_load_const; + mock->base.load_addr = mock_load_addr; + mock->base.load = mock_load; + mock->base.store = mock_store; + mock->base.tls_addr_of = mock_tls_addr_of; + mock->base.bitfield_load = mock_bitfield_load; + mock->base.bitfield_store = mock_bitfield_store; + mock->base.load_label_addr = mock_load_label_addr; + mock->base.alloca_ = mock_alloca; + mock->base.binop = mock_binop; + mock->base.atomic_load = mock_atomic_load; + mock->base.atomic_store = mock_atomic_store; + 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.emit_call = mock_emit_call; + mock->base.machine_op_clobbers = mock_machine_op_clobbers; + mock->base.plan_ret = mock_plan_ret; + mock->base.ret = mock_ret; +} + +static Operand stack_op(FrameSlot slot, KitCgTypeId type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_STACK; + op.cls = RC_INT; + op.type = type; + op.v.frame_slot = slot; + return op; +} + +static Operand frame_addr_op(FrameSlot slot, KitCgTypeId type) { + Operand op = stack_op(slot, type); + op.kind = OPK_FRAME_ADDR; + return op; +} + +static Operand imm_op(i64 value, KitCgTypeId type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_IMM; + op.cls = RC_INT; + op.type = type; + op.v.imm = value; + return op; +} + +static Operand reg_op(Reg reg, KitCgTypeId type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_REG; + op.cls = RC_INT; + op.type = type; + op.v.reg = reg; + return op; +} + +static Operand global_op(ObjSymId sym, KitCgTypeId type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_GLOBAL; + op.cls = RC_INT; + op.type = type; + op.v.global.sym = sym; + return op; +} + +static Operand indirect_frame_op(FrameSlot base, KitCgTypeId ptr_type) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_INDIRECT; + op.cls = RC_INT; + op.type = ptr_type; + op.v.ind.base = base; + op.v.ind.index = REG_NONE; + op.v.ind.base_kind = OPT_INDIRECT_FRAME; + op.v.ind.index_kind = OPT_INDIRECT_REG; + op.v.ind.base_type = ptr_type; + return op; +} + +static Operand indirect_frame_index_op(FrameSlot base, FrameSlot index, + KitCgTypeId ptr_type, + KitCgTypeId index_type) { + Operand op = indirect_frame_op(base, ptr_type); + op.v.ind.index = index; + op.v.ind.index_kind = OPT_INDIRECT_FRAME; + op.v.ind.index_type = index_type; + op.v.ind.log2_scale = 3u; + return op; +} + +static FrameSlot add_slot(Func *f, KitCgTypeId type, u32 size, u32 align) { + FrameSlotDesc desc; + memset(&desc, 0, sizeof desc); + desc.type = type; + desc.size = size; + desc.align = align; + desc.kind = FS_SPILL; + return ir_frame_slot_new(f, &desc); +} + +static Inst *emit_with_ops(Func *f, u32 block, IROp op, u32 nopnds) { + Inst *in = ir_emit(f, block, op); + in->nopnds = nopnds; + in->opnds = nopnds ? arena_zarray(f->arena, Operand, nopnds) : NULL; + return in; +} + +static void frame_destinations_are_staged(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, bool_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + u32 block; + FrameSlot slots[19]; + u64 constant = 0x1122334455667788ull; + Inst *in; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed"); + if (!kit_c) + return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + bool_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_BOOL); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + /* Model x64's deliberately narrow driver-temp bank. The third binop temp + * and the extra multi-result temps must be scavenged from dead caller-saved + * allocation registers, never represented as MIR values. */ + for (u32 i = 0; i < 2u; ++i) { + f->emit_temp_regs[RC_INT][i] = (Reg)(8u + i); + f->opt_reserved_regs[RC_INT] |= 1u << (8u + i); + } + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_hard_regs[RC_INT][0] = 10u; + f->opt_hard_regs[RC_INT][1] = 11u; + f->opt_hard_reg_count[RC_INT] = 2u; + f->opt_caller_saved[RC_INT] = (1u << 10) | (1u << 11); + + slots[0] = add_slot(f, i64_type, 8u, 8u); /* load_const dst */ + slots[1] = add_slot(f, ptr_type, 8u, 8u); /* tls dst */ + slots[2] = add_slot(f, i64_type, 8u, 8u); /* bitfield dst */ + slots[3] = add_slot(f, i64_type, 8u, 8u); /* bitfield record */ + slots[4] = add_slot(f, ptr_type, 8u, 8u); /* label dst */ + slots[5] = add_slot(f, ptr_type, 8u, 8u); /* alloca dst */ + slots[6] = add_slot(f, i64_type, 8u, 8u); /* intrinsic value */ + slots[7] = add_slot(f, bool_type, 1u, 1u); /* intrinsic overflow */ + slots[8] = add_slot(f, i64_type, 8u, 8u); /* intrinsic arg 0 */ + slots[9] = add_slot(f, i64_type, 8u, 8u); /* intrinsic arg 1 */ + slots[10] = add_slot(f, i64_type, 8u, 8u); /* binop dst */ + slots[11] = add_slot(f, i64_type, 8u, 8u); /* binop lhs */ + slots[12] = add_slot(f, i64_type, 8u, 8u); /* binop rhs */ + slots[13] = add_slot(f, i64_type, 8u, 8u); /* atomic_rmw dst */ + slots[14] = add_slot(f, i64_type, 8u, 8u); /* atomic storage */ + slots[15] = add_slot(f, i64_type, 8u, 8u); /* atomic_rmw value */ + slots[16] = add_slot(f, i64_type, 8u, 8u); /* atomic_cas prior */ + slots[17] = add_slot(f, bool_type, 1u, 1u); /* atomic_cas ok */ + slots[18] = add_slot(f, i64_type, 8u, 8u); /* atomic_cas expected */ + /* Reuse the binop lhs slot as the CAS desired input: the instructions have + * disjoint scopes, so this also checks location materialization caching does + * not leak between scopes. */ + + in = emit_with_ops(f, block, IR_LOAD_CONST, 1u); + in->opnds[0] = stack_op(slots[0], i64_type); + in->extra.cbytes.type = i64_type; + in->extra.cbytes.bytes = (const u8 *)&constant; + in->extra.cbytes.size = 8u; + in->extra.cbytes.align = 8u; + + in = emit_with_ops(f, block, IR_TLS_ADDR_OF, 1u); + in->opnds[0] = stack_op(slots[1], ptr_type); + { + IRTlsAux *aux = arena_znew(f->arena, IRTlsAux); + aux->sym = 17u; + aux->addend = 9; + in->extra.aux = aux; + } + + in = emit_with_ops(f, block, IR_BITFIELD_LOAD, 2u); + in->opnds[0] = stack_op(slots[2], i64_type); + in->opnds[1] = frame_addr_op(slots[3], ptr_type); + { + IRBitFieldAux *aux = arena_znew(f->arena, IRBitFieldAux); + aux->access.field_type = i64_type; + aux->access.storage.type = i64_type; + aux->access.storage.size = 8u; + aux->access.storage.align = 8u; + aux->access.bit_offset = 3u; + aux->access.bit_width = 11u; + in->extra.aux = aux; + } + + in = emit_with_ops(f, block, IR_LOAD_LABEL_ADDR, 1u); + in->opnds[0] = stack_op(slots[4], ptr_type); + in->extra.imm = block; + + in = emit_with_ops(f, block, IR_ALLOCA, 2u); + in->opnds[0] = stack_op(slots[5], ptr_type); + in->opnds[1] = imm_op(96, i64_type); + in->extra.imm = 32; + + in = emit_with_ops(f, block, IR_INTRINSIC, 0u); + { + IRIntrinAux *aux = arena_znew(f->arena, IRIntrinAux); + aux->kind = INTRIN_UADD_OVERFLOW; + aux->ndst = 2u; + aux->narg = 2u; + aux->dsts = arena_zarray(f->arena, Operand, aux->ndst); + aux->args = arena_zarray(f->arena, Operand, aux->narg); + aux->dsts[0] = stack_op(slots[6], i64_type); + aux->dsts[1] = stack_op(slots[7], bool_type); + aux->args[0] = stack_op(slots[8], i64_type); + aux->args[1] = stack_op(slots[9], i64_type); + in->extra.aux = aux; + } + + in = emit_with_ops(f, block, IR_BINOP, 3u); + in->opnds[0] = stack_op(slots[10], i64_type); + in->opnds[1] = stack_op(slots[11], i64_type); + in->opnds[2] = stack_op(slots[12], i64_type); + in->extra.imm = BO_IADD; + + in = emit_with_ops(f, block, IR_ATOMIC_RMW, 3u); + in->opnds[0] = stack_op(slots[13], i64_type); + in->opnds[1] = frame_addr_op(slots[14], ptr_type); + in->opnds[2] = stack_op(slots[15], i64_type); + { + IRAtomicAux *aux = arena_znew(f->arena, IRAtomicAux); + aux->op = KIT_CG_ATOMIC_ADD; + aux->mo = KIT_CG_MO_SEQ_CST; + aux->mem.type = i64_type; + aux->mem.size = 8u; + aux->mem.align = 8u; + in->extra.aux = aux; + } + + in = emit_with_ops(f, block, IR_ATOMIC_CAS, 5u); + in->opnds[0] = stack_op(slots[16], i64_type); + in->opnds[1] = stack_op(slots[17], bool_type); + in->opnds[2] = frame_addr_op(slots[14], ptr_type); + in->opnds[3] = stack_op(slots[18], i64_type); + in->opnds[4] = stack_op(slots[11], i64_type); + { + IRCasAux *aux = arena_znew(f->arena, IRCasAux); + aux->mem.type = i64_type; + aux->mem.size = 8u; + aux->mem.align = 8u; + aux->success = KIT_CG_MO_ACQ_REL; + aux->failure = KIT_CG_MO_ACQUIRE; + in->extra.aux = aux; + } + + mock_init(&mock, unit, c); + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, + mock.callback_mask == + (SAW_LOAD_CONST | SAW_TLS_ADDR | SAW_BITFIELD_LOAD | + SAW_LABEL_ADDR | SAW_ALLOCA | SAW_INTRINSIC | SAW_BINOP | + SAW_ATOMIC_RMW | SAW_ATOMIC_CAS), + "all frame-destination emitter hooks should be exercised"); + { + u32 expected = (1u << slots[0]) | (1u << slots[1]) | (1u << slots[2]) | + (1u << slots[4]) | (1u << slots[5]) | (1u << slots[6]) | + (1u << slots[7]) | (1u << slots[10]) | (1u << slots[13]) | + (1u << slots[16]) | (1u << slots[17]); + CU_EXPECT(unit, mock.writeback_slots == expected, + "each staged destination should be written back exactly once " + "(got 0x%x expected 0x%x)", + mock.writeback_slots, expected); + } + CU_EXPECT(unit, mock.loads == 7u, + "stack intrinsic/binop/atomic inputs should each materialize once"); + + kit_compiler_free(kit_c); +} + +static void cached_frame_value_feeds_indirect_address(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot pointer_slot; + Inst *in; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for indirect frame-cache test"); + if (!kit_c) + return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_reg_count[RC_INT] = 1u; + f->opt_reserved_regs[RC_INT] = 1u << 8u; + pointer_slot = add_slot(f, ptr_type, 8u, 8u); + + /* The store makes the frame home authoritative while retaining its clean + * register copy. The next address consumes that copy as an indirect base. */ + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(pointer_slot, ptr_type); + in->extra.imm = 0x1000; + + in = emit_with_ops(f, block, IR_LOAD, 2u); + in->opnds[0] = reg_op(10u, i64_type); + in->opnds[1] = indirect_frame_op(pointer_slot, ptr_type); + in->extra.mem.type = i64_type; + in->extra.mem.size = 8u; + in->extra.mem.align = 8u; + + mock_init(&mock, unit, c); + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, mock.loads == 1u && mock.reg_address_loads == 1u, + "cached spill should feed the indirect base without a frame reload " + "(loads=%u register-address-loads=%u)", + mock.loads, mock.reg_address_loads); + CU_EXPECT(unit, mock.frame_loads == 0u, + "cached indirect base unexpectedly reloaded from its frame home"); + + kit_compiler_free(kit_c); +} + +static void frame_load_reuses_scoped_address_base_under_pressure( + KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot base_slot, index_slot, result_slot; + Inst *in; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for scoped-base pressure test"); + if (!kit_c) return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_regs[RC_INT][1] = 9u; + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_reserved_regs[RC_INT] = (1u << 8u) | (1u << 9u); + base_slot = add_slot(f, ptr_type, 8u, 8u); + index_slot = add_slot(f, i64_type, 8u, 8u); + result_slot = add_slot(f, i64_type, 8u, 8u); + + /* Each frame write leaves a clean value in one emitter temp. The indexed + * load consumes both cached values as its address while its result also + * needs a register before frame writeback: exactly the x64 SQLite pressure + * shape that must reuse the now-transient materialized address base. */ + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(base_slot, ptr_type); + in->extra.imm = 0x1000; + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(index_slot, i64_type); + in->extra.imm = 3; + + in = emit_with_ops(f, block, IR_LOAD, 2u); + in->opnds[0] = stack_op(result_slot, i64_type); + in->opnds[1] = + indirect_frame_index_op(base_slot, index_slot, ptr_type, i64_type); + in->extra.mem.type = i64_type; + in->extra.mem.size = 8u; + in->extra.mem.align = 8u; + + mock_init(&mock, unit, c); + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, + mock.load_addrs == 1u && mock.reg_address_loads == 1u && + mock.in_place_reg_address_loads == 1u, + "frame load should materialize once and consume the scoped base " + "in place (addr=%u reg-load=%u in-place=%u)", + mock.load_addrs, mock.reg_address_loads, + mock.in_place_reg_address_loads); + CU_EXPECT(unit, (mock.writeback_slots & (1u << result_slot)) != 0u, + "in-place frame load result was not written back"); + + kit_compiler_free(kit_c); +} + +static void frame_address_reuses_scoped_base_under_pressure(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot base_slot, index_slot, result_slot; + Inst *in; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for scoped address-result test"); + if (!kit_c) return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_regs[RC_INT][1] = 9u; + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_reserved_regs[RC_INT] = (1u << 8u) | (1u << 9u); + base_slot = add_slot(f, ptr_type, 8u, 8u); + index_slot = add_slot(f, i64_type, 8u, 8u); + result_slot = add_slot(f, ptr_type, 8u, 8u); + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(base_slot, ptr_type); + in->extra.imm = 0x2000; + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(index_slot, i64_type); + in->extra.imm = 5; + + in = emit_with_ops(f, block, IR_ADDR_OF, 2u); + in->opnds[0] = stack_op(result_slot, ptr_type); + in->opnds[1] = + indirect_frame_index_op(base_slot, index_slot, ptr_type, i64_type); + + mock_init(&mock, unit, c); + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, mock.load_addrs == 1u, + "frame address should collapse into its scoped base exactly once " + "(materializations=%u)", + mock.load_addrs); + CU_EXPECT(unit, (mock.writeback_slots & (1u << result_slot)) != 0u, + "in-place frame address result was not written back"); + + kit_compiler_free(kit_c); +} + +static void collapsed_address_releases_index_for_store_value(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot base_slot, index_slot; + Inst *in; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for collapsed-address store test"); + if (!kit_c) return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_regs[RC_INT][1] = 9u; + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_reserved_regs[RC_INT] = (1u << 8u) | (1u << 9u); + base_slot = add_slot(f, ptr_type, 8u, 8u); + index_slot = add_slot(f, i64_type, 8u, 8u); + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(base_slot, ptr_type); + in->extra.imm = 0x3000; + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(index_slot, i64_type); + in->extra.imm = 7; + + in = emit_with_ops(f, block, IR_STORE, 2u); + in->opnds[0] = + indirect_frame_index_op(base_slot, index_slot, ptr_type, i64_type); + in->opnds[1] = imm_op(42, i64_type); + in->extra.mem.type = i64_type; + in->extra.mem.size = 8u; + in->extra.mem.align = 8u; + + mock_init(&mock, unit, c); + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, mock.load_addrs == 1u && mock.reg_address_stores == 1u, + "collapsed store should reuse the dead index lease for its value " + "(addr=%u stores=%u)", + mock.load_addrs, mock.reg_address_stores); + + kit_compiler_free(kit_c); +} + +static void frame_bitfield_load_reuses_scoped_address_base(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot base_slot, index_slot, result_slot; + IRBitFieldAux *aux; + Inst *in; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for bitfield pressure test"); + if (!kit_c) return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_regs[RC_INT][1] = 9u; + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_reserved_regs[RC_INT] = (1u << 8u) | (1u << 9u); + base_slot = add_slot(f, ptr_type, 8u, 8u); + index_slot = add_slot(f, i64_type, 8u, 8u); + result_slot = add_slot(f, i64_type, 8u, 8u); + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(base_slot, ptr_type); + in->extra.imm = 0x4000; + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(index_slot, i64_type); + in->extra.imm = 9; + + in = emit_with_ops(f, block, IR_BITFIELD_LOAD, 2u); + in->opnds[0] = stack_op(result_slot, i64_type); + in->opnds[1] = + indirect_frame_index_op(base_slot, index_slot, ptr_type, i64_type); + aux = arena_znew(f->arena, IRBitFieldAux); + aux->access.field_type = i64_type; + aux->access.storage.type = i64_type; + aux->access.storage.size = 8u; + aux->access.storage.align = 8u; + aux->access.bit_offset = 3u; + aux->access.bit_width = 11u; + in->extra.aux = aux; + + mock_init(&mock, unit, c); + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, + mock.load_addrs == 1u && + mock.in_place_reg_address_bitfield_loads == 1u, + "bitfield load should consume its scoped address base in place " + "(addr=%u in-place=%u)", + mock.load_addrs, mock.in_place_reg_address_bitfield_loads); + CU_EXPECT(unit, (mock.writeback_slots & (1u << result_slot)) != 0u, + "in-place bitfield result was not written back"); + + kit_compiler_free(kit_c); +} + +static void in_place_collapse_drops_overwritten_cache_fact(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot base_slot, index_slot; + IRAtomicAux *aux; + Inst *in; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for collapse cache-fact test"); + if (!kit_c) return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_regs[RC_INT][1] = 9u; + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_reserved_regs[RC_INT] = (1u << 8u) | (1u << 9u); + base_slot = add_slot(f, ptr_type, 8u, 8u); + index_slot = add_slot(f, i64_type, 8u, 8u); + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(base_slot, ptr_type); + in->extra.imm = 0x5000; + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(index_slot, i64_type); + in->extra.imm = 2; + + /* The address consumes both cached spill values and is collapsed in place + * into the cached base register. The atomic value then requests that same + * base spill again without an address-avoidance rider. It must reload the + * authoritative frame home; the register now contains base+index, not the + * cached base value. */ + in = emit_with_ops(f, block, IR_ATOMIC_STORE, 2u); + in->opnds[0] = + indirect_frame_index_op(base_slot, index_slot, ptr_type, i64_type); + in->opnds[1] = stack_op(base_slot, ptr_type); + aux = arena_znew(f->arena, IRAtomicAux); + aux->mem.type = ptr_type; + aux->mem.size = 8u; + aux->mem.align = 8u; + aux->mo = KIT_CG_MO_SEQ_CST; + in->extra.aux = aux; + + mock_init(&mock, unit, c); + mock.reject_indexed_addr = 1u; + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, + mock.load_addrs == 1u && mock.atomic_stores == 1u && + mock.frame_loads == 1u, + "overwritten base source should rematerialize exactly once " + "(addr=%u atomic=%u reloads=%u)", + mock.load_addrs, mock.atomic_stores, mock.frame_loads); + + kit_compiler_free(kit_c); +} + +static void frame_atomic_load_reuses_scoped_address_base(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + u32 block; + FrameSlot base_slot, index_slot, dst_slot; + Inst *in; + IRAtomicAux *aux; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for atomic-load pressure test"); + if (!kit_c) return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_regs[RC_INT][1] = 9u; + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_reserved_regs[RC_INT] = (1u << 8u) | (1u << 9u); + base_slot = add_slot(f, ptr_type, 8u, 8u); + index_slot = add_slot(f, i64_type, 8u, 8u); + dst_slot = add_slot(f, i64_type, 8u, 8u); + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(base_slot, ptr_type); + in->extra.imm = 0x5000; + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(index_slot, i64_type); + in->extra.imm = 2; + + in = emit_with_ops(f, block, IR_ATOMIC_LOAD, 2u); + in->opnds[0] = stack_op(dst_slot, i64_type); + in->opnds[1] = + indirect_frame_index_op(base_slot, index_slot, ptr_type, i64_type); + aux = arena_znew(f->arena, IRAtomicAux); + aux->mem.type = i64_type; + aux->mem.size = 8u; + aux->mem.align = 8u; + aux->mo = KIT_CG_MO_SEQ_CST; + in->extra.aux = aux; + + mock_init(&mock, unit, c); + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, mock.load_addrs == 1u && mock.atomic_loads == 1u, + "atomic load should collapse once and reach its hook once " + "(addr=%u atomic=%u)", + mock.load_addrs, mock.atomic_loads); + kit_compiler_free(kit_c); +} + +static void atomic_store_collapses_address_for_value_temp(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + u32 block; + FrameSlot base_slot, index_slot; + Inst *in; + IRAtomicAux *aux; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for atomic-store pressure test"); + if (!kit_c) return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_regs[RC_INT][1] = 9u; + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_reserved_regs[RC_INT] = (1u << 8u) | (1u << 9u); + base_slot = add_slot(f, ptr_type, 8u, 8u); + index_slot = add_slot(f, i64_type, 8u, 8u); + + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(base_slot, ptr_type); + in->extra.imm = 0x5000; + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = stack_op(index_slot, i64_type); + in->extra.imm = 2; + + in = emit_with_ops(f, block, IR_ATOMIC_STORE, 2u); + in->opnds[0] = + indirect_frame_index_op(base_slot, index_slot, ptr_type, i64_type); + in->opnds[1] = imm_op(42, i64_type); + aux = arena_znew(f->arena, IRAtomicAux); + aux->mem.type = i64_type; + aux->mem.size = 8u; + aux->mem.align = 8u; + aux->mo = KIT_CG_MO_SEQ_CST; + in->extra.aux = aux; + + mock_init(&mock, unit, c); + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, mock.load_addrs == 1u && mock.atomic_stores == 1u, + "atomic store should collapse once and reach its hook once " + "(addr=%u atomic=%u)", + mock.load_addrs, mock.atomic_stores); + kit_compiler_free(kit_c); +} + +static void allocator_cache_obeys_machine_clobbers(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot clobbered_slot, preserved_slot, record_slot; + Inst *in, *clobber; + NativeMachineOp mop; + u32 clobber_mask[NATIVE_CALL_PLAN_CLASSES]; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for allocator-cache clobber test"); + if (!kit_c) + return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_regs[RC_INT][1] = 9u; + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_reserved_regs[RC_INT] = (1u << 8u) | (1u << 9u); + f->opt_hard_regs[RC_INT][0] = 10u; + f->opt_hard_regs[RC_INT][1] = 11u; + f->opt_hard_regs[RC_INT][2] = 12u; + f->opt_hard_reg_count[RC_INT] = 3u; + f->opt_caller_saved[RC_INT] = + (1u << 10u) | (1u << 11u) | (1u << 12u); + clobbered_slot = add_slot(f, i64_type, 8u, 8u); + preserved_slot = add_slot(f, i64_type, 8u, 8u); + record_slot = add_slot(f, i64_type, 8u, 8u); + + /* r10 is dead after this writeback. Its clean copy is therefore eligible + * for allocator-register forwarding until a declared machine effect says + * the backend encoding destroys r10. */ + in = emit_with_ops(f, block, IR_COPY, 2u); + in->opnds[0] = stack_op(clobbered_slot, i64_type); + in->opnds[1] = reg_op(10u, i64_type); + + clobber = emit_with_ops(f, block, IR_BITFIELD_STORE, 2u); + clobber->opnds[0] = frame_addr_op(record_slot, ptr_type); + clobber->opnds[1] = imm_op(7, i64_type); + { + IRBitFieldAux *aux = arena_znew(f->arena, IRBitFieldAux); + aux->access.field_type = i64_type; + aux->access.storage.type = i64_type; + aux->access.storage.size = 8u; + aux->access.storage.align = 8u; + aux->access.bit_offset = 3u; + aux->access.bit_width = 11u; + clobber->extra.aux = aux; + } + + in = emit_with_ops(f, block, IR_COPY, 2u); + in->opnds[0] = reg_op(12u, i64_type); + in->opnds[1] = stack_op(clobbered_slot, i64_type); + + /* Establish a new, independently dead r10 value for the preservation half + * of the contract. The label hook reports no exceptional clobber, so the + * following read should forward without touching the frame. */ + in = emit_with_ops(f, block, IR_LOAD_IMM, 1u); + in->opnds[0] = reg_op(10u, i64_type); + in->extra.imm = 99; + + in = emit_with_ops(f, block, IR_COPY, 2u); + in->opnds[0] = stack_op(preserved_slot, i64_type); + in->opnds[1] = reg_op(10u, i64_type); + + in = emit_with_ops(f, block, IR_LOAD_LABEL_ADDR, 1u); + in->opnds[0] = reg_op(11u, ptr_type); + in->extra.imm = block; + + in = emit_with_ops(f, block, IR_COPY, 2u); + in->opnds[0] = reg_op(12u, i64_type); + in->opnds[1] = stack_op(preserved_slot, i64_type); + + mock_init(&mock, unit, c); + mock.report_bitfield_store_clobber = 1u; + /* The O0 direct-target value bank is deliberately empty. Cross-instruction + * O1 forwarding must derive allocator-register eligibility from the + * machinized `f->opt_hard_regs` table above, never from an unrelated O0 + * cache policy. */ + mock.classes[NATIVE_REG_INT].caller_saved_mask = 1u << 10u; + + memset(&mop, 0, sizeof mop); + mop.kind = NATIVE_MOP_BITFIELD_STORE; + CU_EXPECT(unit, + mock.base.machine_op_clobbers(&mock.base, &mop, clobber_mask) && + clobber_mask[NATIVE_REG_INT] == (1u << 10u), + "mock bitfield-store effect must report its allocator clobber"); + f->inst_clobbers_cap = f->next_inst_id; + f->inst_clobbers = + arena_zarray(f->arena, OptInstClobberMask, f->inst_clobbers_cap); + CU_EXPECT(unit, + clobber->id != INST_ID_NONE && + clobber->id < f->inst_clobbers_cap, + "bitfield store must have a valid clobber-table instruction id"); + if (clobber->id != INST_ID_NONE && clobber->id < f->inst_clobbers_cap) + for (u32 cls = 0; cls < OPT_REG_CLASSES; ++cls) + f->inst_clobbers[clobber->id][cls] = clobber_mask[cls]; + + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, + (mock.callback_mask & + (SAW_BITFIELD_STORE | SAW_LABEL_ADDR)) == + (SAW_BITFIELD_STORE | SAW_LABEL_ADDR), + "clobbering and ordinary preservation hooks must both execute"); + CU_EXPECT(unit, mock.frame_loads == 1u && mock.loads == 1u, + "declared clobber must force one authoritative reload while the " + "ordinary hook preserves forwarding (loads=%u frame-loads=%u)", + mock.loads, mock.frame_loads); + + kit_compiler_free(kit_c); +} + +static void call_ends_pre_call_temp_phase(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot arg_slot, result_slot; + IRCallAux *aux; + Inst *call; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for call phase test"); + if (!kit_c) + return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + f->emit_temp_regs[RC_INT][0] = 8u; + f->emit_temp_regs[RC_INT][1] = 9u; + f->emit_temp_reg_count[RC_INT] = 2u; + f->opt_reserved_regs[RC_INT] = (1u << 8u) | (1u << 9u); + f->opt_caller_saved[RC_INT] = (1u << 8u) | (1u << 9u); + arg_slot = add_slot(f, i64_type, 8u, 8u); + result_slot = add_slot(f, i64_type, 8u, 8u); + + call = emit_with_ops(f, block, IR_CALL, 0u); + aux = arena_znew(f->arena, IRCallAux); + aux->desc.fn_type = i64_type; + aux->desc.callee = global_op(17u, ptr_type); + aux->desc.nargs = 1u; + aux->desc.args = arena_zarray(f->arena, CGABIValue, 1u); + aux->desc.args[0].type = i64_type; + aux->desc.args[0].storage = stack_op(arg_slot, i64_type); + aux->desc.ret.type = i64_type; + aux->desc.ret.storage = stack_op(result_slot, i64_type); + call->extra.aux = aux; + + mock_init(&mock, unit, c); + /* r9 materializes the frame argument into fixed ABI register r8. Both are + * caller-clobbered, so neither pre-call fact may survive into the ret copy. + */ + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, mock.calls == 1u, "mock call was not emitted exactly once"); + CU_EXPECT(unit, mock.loads == 2u && mock.frame_loads == 2u, + "post-call return copy reused a pre-call materialization " + "(loads=%u frame-loads=%u)", + mock.loads, mock.frame_loads); + CU_EXPECT(unit, (mock.writeback_slots & (1u << result_slot)) != 0u, + "post-call result was not written to its frame destination"); + + kit_compiler_free(kit_c); +} + +static void return_parts_keep_prior_abi_destinations_live(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot value_slot; + IRRetAux *aux; + Inst *ret; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for multi-part return phase test"); + if (!kit_c) + return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + + memset(&desc, 0, sizeof desc); + desc.result_type = i64_type; + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + for (u32 i = 0; i < 4u; ++i) { + f->emit_temp_regs[RC_INT][i] = (Reg)(8u + i); + 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); + + ret = emit_with_ops(f, block, IR_RET, 0u); + aux = arena_znew(f->arena, IRRetAux); + aux->present = 1u; + aux->val.type = i64_type; + aux->val.storage = stack_op(value_slot, i64_type); + ret->extra.aux = aux; + + mock_init(&mock, unit, c); + mock.plan_two_part_ret = 1u; + mock.int_temps[2] = 10u; + mock.int_temps[3] = 11u; + mock.classes[NATIVE_REG_INT].nscratch = 4u; + mock.classes[NATIVE_REG_INT].nemit_temps = 4u; + mock.classes[NATIVE_REG_INT].emit_cache_mask = + (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 " + "(loads=%u)", + mock.nframe_load_dsts); + if (mock.nframe_load_dsts == 2u) { + CU_EXPECT(unit, mock.frame_load_dsts[0] != 8u, + "first return-part load must not overwrite its r8 destination"); + CU_EXPECT(unit, mock.frame_load_dsts[1] != 8u, + "second return-part load reused completed ABI destination r8"); + CU_EXPECT(unit, mock.frame_load_dsts[1] != 9u, + "second return-part load must not overwrite its r9 destination"); + } + + kit_compiler_free(kit_c); +} + +static void call_setup_owns_callee_and_completed_arguments(KitUnit *unit) { + KitCompiler *kit_c = NULL; + Compiler *c; + KitCgTypeId i64_type, ptr_type; + OptCGFuncDesc desc; + Func *f; + MockNative mock; + FrameSlot callee_slot, arg_slots[2]; + IRCallAux *aux; + Inst *call; + u32 block; + + CU_EXPECT(unit, + kit_unit_compiler_new( + unit, + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kit_c) == KIT_OK && + kit_c != NULL, + "compiler allocation failed for call setup ownership test"); + if (!kit_c) + return; + c = (Compiler *)kit_c; + i64_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_I64); + ptr_type = kit_cg_type_ptr( + kit_c, kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID), 0); + + memset(&desc, 0, sizeof desc); + desc.result_type = kit_cg_type_builtin(kit_c, KIT_CG_BUILTIN_VOID); + f = ir_func_new(c, &desc); + block = ir_block_new(f); + f->entry = block; + ir_note_emit(f, block); + f->opt_rewritten = 1; + for (u32 i = 0; i < 4u; ++i) { + f->emit_temp_regs[RC_INT][i] = (Reg)(8u + i); + f->opt_reserved_regs[RC_INT] |= 1u << (8u + i); + } + f->emit_temp_reg_count[RC_INT] = 4u; + callee_slot = add_slot(f, ptr_type, 8u, 8u); + arg_slots[0] = add_slot(f, i64_type, 8u, 8u); + arg_slots[1] = add_slot(f, i64_type, 8u, 8u); + + call = emit_with_ops(f, block, IR_CALL, 0u); + aux = arena_znew(f->arena, IRCallAux); + aux->desc.fn_type = i64_type; + aux->desc.callee = stack_op(callee_slot, ptr_type); + aux->desc.nargs = 2u; + aux->desc.args = arena_zarray(f->arena, CGABIValue, 2u); + for (u32 i = 0; i < 2u; ++i) { + aux->desc.args[i].type = i64_type; + aux->desc.args[i].storage = stack_op(arg_slots[i], i64_type); + } + call->extra.aux = aux; + + mock_init(&mock, unit, c); + mock.plan_two_arg_indirect_call = 1u; + mock.int_temps[2] = 10u; + mock.int_temps[3] = 11u; + mock.classes[NATIVE_REG_INT].nscratch = 4u; + mock.classes[NATIVE_REG_INT].nemit_temps = 4u; + mock.classes[NATIVE_REG_INT].emit_cache_mask = + (1u << 8u) | (1u << 9u) | (1u << 10u) | (1u << 11u); + opt_emit_native(c, f, &mock.base); + + CU_EXPECT(unit, mock.calls == 1u, + "two-argument indirect call was not emitted exactly once"); + CU_EXPECT(unit, mock.nframe_load_dsts == 3u && mock.frame_loads == 3u, + "callee and two arguments should each load once " + "(recorded=%u frame-loads=%u)", + mock.nframe_load_dsts, mock.frame_loads); + + kit_compiler_free(kit_c); +} + +int main(void) { + KitUnit unit; + kit_unit_init(&unit); + frame_destinations_are_staged(&unit); + cached_frame_value_feeds_indirect_address(&unit); + frame_load_reuses_scoped_address_base_under_pressure(&unit); + frame_address_reuses_scoped_base_under_pressure(&unit); + collapsed_address_releases_index_for_store_value(&unit); + frame_bitfield_load_reuses_scoped_address_base(&unit); + in_place_collapse_drops_overwritten_cache_fact(&unit); + frame_atomic_load_reuses_scoped_address_base(&unit); + atomic_store_collapses_address_for_value_temp(&unit); + allocator_cache_obeys_machine_clobbers(&unit); + call_ends_pre_call_temp_phase(&unit); + return_parts_keep_prior_abi_destinations_live(&unit); + call_setup_owns_callee_and_completed_arguments(&unit); + if (unit.fails) { + fprintf(stderr, "native-emit-frame-dst: %d/%d failed\n", unit.fails, + unit.checks); + return 1; + } + printf("native-emit-frame-dst: %d checks, 0 failures\n", unit.checks); + return 0; +} diff --git a/test/opt/native_part_chunk_test.c b/test/opt/native_part_chunk_test.c @@ -0,0 +1,61 @@ +#include <stdio.h> + +#include "arch/native_target.h" +#include "lib/kit_unit.h" + +typedef struct ChunkCase { + u32 total; + u32 max_chunk; + u32 count; + u32 offsets[4]; + u32 sizes[4]; +} ChunkCase; + +static void check_case(KitUnit* unit, const ChunkCase* tc) { + NativePartChunkIter it = native_part_chunks(tc->total, tc->max_chunk); + u32 offset = 0; + u32 size = 0; + u32 count = 0; + u32 covered = 0; + while (native_part_chunk_next(&it, &offset, &size)) { + CU_EXPECT(unit, count < tc->count, + "total=%u max=%u produced an extra chunk", tc->total, + tc->max_chunk); + if (count >= tc->count) return; + CU_EXPECT(unit, offset == tc->offsets[count], + "total=%u max=%u chunk %u offset: got %u expected %u", tc->total, + tc->max_chunk, count, offset, tc->offsets[count]); + CU_EXPECT(unit, size == tc->sizes[count], + "total=%u max=%u chunk %u size: got %u expected %u", tc->total, + tc->max_chunk, count, size, tc->sizes[count]); + CU_EXPECT(unit, size != 0u && (size & (size - 1u)) == 0u && + size <= tc->max_chunk, + "total=%u max=%u chunk %u is not a legal native width", + tc->total, tc->max_chunk, count); + covered += size; + count++; + } + CU_EXPECT(unit, count == tc->count, + "total=%u max=%u chunk count: got %u expected %u", tc->total, + tc->max_chunk, count, tc->count); + CU_EXPECT(unit, covered == tc->total, + "total=%u max=%u covered %u bytes", tc->total, tc->max_chunk, + covered); +} + +int main(void) { + static const ChunkCase cases[] = { + {1u, 8u, 1u, {0u}, {1u}}, + {3u, 8u, 2u, {0u, 2u}, {2u, 1u}}, + {6u, 8u, 2u, {0u, 4u}, {4u, 2u}}, + {7u, 8u, 3u, {0u, 4u, 6u}, {4u, 2u, 1u}}, + {8u, 8u, 1u, {0u}, {8u}}, + {12u, 4u, 3u, {0u, 4u, 8u}, {4u, 4u, 4u}}, + }; + KitUnit unit; + kit_unit_init(&unit); + for (u32 i = 0; i < (u32)(sizeof cases / sizeof cases[0]); ++i) + check_case(&unit, &cases[i]); + kit_unit_summary(&unit, "native-part-chunks"); + return kit_unit_status(&unit); +} diff --git a/test/opt/o1_stack_dse.sh b/test/opt/o1_stack_dse.sh @@ -4,10 +4,12 @@ # # W8 deletes a spill store when a LATER store to the same spill slot fully # overwrites it in the same block before any possible read or barrier. It is -# deliberately exact and spill-only; the correctness boundary is aliasing / -# partial overlap, so the pass treats every uncertain case (a read of the slot, -# a different size/addr-space, a call/asm/intrinsic/atomic/volatile, an -# aggregate or non-direct access) as a barrier that preserves the prior store. +# deliberately exact and spill-only. The canonical MIR operand walker clears +# only the pending slot named by any explicit OPK_STACK use, including stack +# locations nested in indirect or auxiliary operands. Non-candidate memory +# writes (call/asm/intrinsic/atomic/volatile/aggregate and uncertain stores) +# remain full barriers; a differently shaped direct spill store replaces the +# pending shape without treating semantic locals as allocator spill traffic. # # Important: in kit's current -O1 pipeline this pass is correct but rarely finds # work -- upstream rematerialization (W2) + dead-definition-elimination + the diff --git a/test/opt/o1p_combine.sh b/test/opt/o1p_combine.sh @@ -337,15 +337,15 @@ printf ' L4: run rc=%s (O0==O1; copy + invert + double-not forms)\n' "$L4_O0" # implemented by the existing same-block copy propagator try_substitute # (unconditional SK_REG forwarding into every whitelisted consumer slot, # pass_combine.c) plus the IR_RET back-propagation try_ret_retarget. The only -# residual is forwarding an EMIT-SCRATCH-sourced copy, which is unsound at MIR -# level (native emit repurposes the scratch regs to materialize locals BETWEEN -# instructions — disabling the scratch-source guard miscompiles sqlite), and the -# broad reg-to-reg `mov` surplus the catalog counted is ABI-mandated arg-shuffle/ -# save moves + the cross-block register-residency problem (NEEDS-SSA-O2, the -# catalog's own §3). So this section is a COVERAGE/REGRESSION guard: it asserts -# the spec's named consumer coalescing holds (no redundant single-use copy -# survives feeding a convert / store-value / return) rather than introducing a -# new, unsound fold. +# former emitter-temp source exception no longer exists: location MIR contains +# one canonical program home, while emitter temps are instruction-scoped leases +# that the MIR verifier rejects as persistent operands. The broad reg-to-reg +# `mov` surplus the catalog counted is ABI-mandated arg-shuffle/save moves plus +# the cross-block register-residency problem (NEEDS-SSA-O2, the catalog's own +# §3). So this section is a COVERAGE/REGRESSION guard: it asserts the spec's +# named consumer coalescing holds (no redundant single-use copy survives +# feeding a convert / store-value / return) rather than introducing a new, +# unsound fold. echo "== L5 single-use copy coalescing (coverage/regression) ==" cat > "$WORK/l5cov.c" <<'EOF' extern void usel(long); diff --git a/test/opt/reg_effects_test.c b/test/opt/reg_effects_test.c @@ -0,0 +1,906 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <kit/frontend.h> + +#include "arch/aa64/aa64.h" +#include "arch/riscv/rv64.h" +#include "arch/x64/x64.h" +#include "core/pool.h" +#include "lib/kit_unit.h" +#include "obj/obj.h" +#include "opt/opt_internal.h" + +static Operand reg_op(u8 cls, Reg reg) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_REG; + op.cls = cls; + op.v.reg = reg; + return op; +} + +static Operand indirect_op(Reg base, Reg index) { + Operand op; + memset(&op, 0, sizeof op); + op.kind = OPK_INDIRECT; + op.cls = RC_INT; + op.v.ind.base = base; + op.v.ind.index = index; + return op; +} + +static u32 reg_bit(Reg reg) { return 1u << reg; } + +typedef struct MachinizeRunCtx { + Func* f; + NativeTarget* target; +} MachinizeRunCtx; + +static int invalid_machine_effect( + NativeTarget* target, const NativeMachineOp* op, + u32 mask[NATIVE_CALL_PLAN_CLASSES]) { + (void)target; + (void)op; + mask[0] = mask[1] = mask[2] = 0; + mask[NATIVE_REG_INT] = reg_bit(31u); + return 1; +} + +static KitStatus run_machinize(KitCompiler* compiler, void* arg) { + MachinizeRunCtx* run = (MachinizeRunCtx*)arg; + (void)compiler; + opt_machinize_native(run->f, run->target); + return KIT_OK; +} + +static Operand typed_reg_op(u8 cls, Reg reg, KitCgTypeId type) { + Operand op = reg_op(cls, reg); + op.type = type; + return op; +} + +static void expect_body_bytes(KitUnit* u, const Section* sec, u32 begin, + const u8* expected, u32 expected_size, + const char* what) { + u32 size = sec ? buf_pos(&sec->bytes) : 0u; + u8* bytes = size ? (u8*)malloc(size) : NULL; + CU_EXPECT(u, sec != NULL && begin <= size && size - begin == expected_size, + "%s size mismatch: begin=%u size=%u expected=%u", what, begin, + size, expected_size); + if (!sec || begin > size || size - begin != expected_size || + (size && !bytes)) { + free(bytes); + return; + } + if (size) buf_flatten(&sec->bytes, bytes); + CU_EXPECT(u, !memcmp(bytes + begin, expected, expected_size), + "%s encoding mismatch", what); + free(bytes); +} + +static void x64_dying_fixed_sources_are_staged(KitUnit* u) { + static const u8 div_expected[] = { + 0x49, 0x89, 0xc3, /* mov r11, rax (save divisor) */ + 0x48, 0x89, 0xd0, /* mov rax, rdx (publish dividend) */ + 0x48, 0x99, /* cqo */ + 0x49, 0xf7, 0xfb, /* idiv r11 */ + 0x49, 0x89, 0xc0, /* mov r8, rax */ + }; + static const u8 shift_expected[] = { + 0x49, 0x89, 0xf2, /* mov r10, rsi (stage value) */ + 0x49, 0xd3, 0xe2, /* shl r10, cl */ + 0x4c, 0x89, 0xd1, /* mov rcx, r10 */ + }; + static const u8 copy_to_rax_addr_expected[] = { + 0x44, 0x0f, 0xb6, 0x12, /* movzx r10d, byte [rdx] */ + 0x66, 0x45, 0x0f, 0x6e, 0xf2, /* movd xmm14, r10d */ + 0x66, 0x45, 0x0f, 0x7e, 0xf2, /* movd r10d, xmm14 */ + 0x44, 0x88, 0x10, /* mov byte [rax], r10b */ + }; + static const u8 copy_from_rax_addr_expected[] = { + 0x44, 0x0f, 0xb6, 0x10, /* movzx r10d, byte [rax] */ + 0x66, 0x45, 0x0f, 0x6e, 0xf2, /* movd xmm14, r10d */ + 0x66, 0x45, 0x0f, 0x7e, 0xf2, /* movd r10d, xmm14 */ + 0x44, 0x88, 0x12, /* mov byte [rdx], r10b */ + }; + static const u8 set_rax_addr_expected[] = { + 0x41, 0x89, 0xca, /* mov r10d, ecx */ + 0x41, 0x81, 0xe2, 0xff, 0x00, 0x00, 0x00, /* and r10d, 255 */ + 0x66, 0x45, 0x0f, 0x6e, 0xf2, /* movd xmm14, r10d */ + 0x66, 0x45, 0x0f, 0x60, 0xf6, /* punpcklbw xmm14 */ + 0x66, 0x45, 0x0f, 0x61, 0xf6, /* punpcklwd xmm14 */ + 0x66, 0x45, 0x0f, 0x62, 0xf6, /* punpckldq xmm14 */ + 0x66, 0x4d, 0x0f, 0x7e, 0xf2, /* movq r10, xmm14 */ + 0x4c, 0x89, 0x10, /* mov [rax], r10 */ + }; + KitCompiler* kc = NULL; + Compiler* c; + ObjBuilder* obj; + MCEmitter* mc; + NativeTarget* target; + const Section* sec; + ObjSecId text; + KitCgTypeId i64_type; + NativeLoc rax, rcx, rdx, rsi, r8; + NativeAddr src_addr, dst_addr; + AggregateAccess byte_copy; + u32 begin; + + 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 fixed-source staging test"); + if (!kc) return; + c = (Compiler*)kc; + obj = obj_new(c); + mc = obj ? mc_new(c, obj) : NULL; + target = mc ? x64_native_target_new(c, obj, mc) : NULL; + CU_EXPECT(u, obj && mc && target, + "x64 fixed-source staging backend allocation failed"); + if (!obj || !mc || !target) { + kit_compiler_free(kc); + return; + } + text = obj_section(obj, pool_intern_slice(c->global, SLICE_LIT(".text")), + SEC_TEXT, SF_EXEC | SF_ALLOC, 16u); + mc_set_section(mc, text); + i64_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + rax = native_loc_reg(i64_type, NATIVE_REG_INT, 0u); + rcx = native_loc_reg(i64_type, NATIVE_REG_INT, 1u); + rdx = native_loc_reg(i64_type, NATIVE_REG_INT, 2u); + rsi = native_loc_reg(i64_type, NATIVE_REG_INT, 6u); + r8 = native_loc_reg(i64_type, NATIVE_REG_INT, 8u); + + begin = mc_pos(mc); + /* The divisor dies in RAX while the dividend arrives in RDX. A naive + * dividend-first move destroys the divisor before IDIV can consume it. */ + target->binop(target, BO_SDIV, r8, rdx, rax); + sec = obj_section_get(obj, text); + expect_body_bytes(u, sec, begin, div_expected, sizeof div_expected, + "x64 crossed division operands"); + + begin = mc_pos(mc); + /* RCX is both the dying count and the requested result. CL must remain the + * count until the shift finishes, so the result is computed in private R10 + * and published afterward. */ + target->binop(target, BO_SHL, rcx, rsi, rcx); + sec = obj_section_get(obj, text); + expect_body_bytes(u, sec, begin, shift_expected, sizeof shift_expected, + "x64 RCX-result variable shift"); + + memset(&src_addr, 0, sizeof src_addr); + memset(&dst_addr, 0, sizeof dst_addr); + memset(&byte_copy, 0, sizeof byte_copy); + src_addr.base_kind = NATIVE_ADDR_BASE_REG; + src_addr.base.reg = 2u; /* RDX */ + src_addr.base_type = i64_type; + dst_addr.base_kind = NATIVE_ADDR_BASE_REG; + dst_addr.base.reg = 0u; /* RAX */ + dst_addr.base_type = i64_type; + byte_copy.size = 1u; + + begin = mc_pos(mc); + target->copy_bytes(target, dst_addr, src_addr, byte_copy); + sec = obj_section_get(obj, text); + expect_body_bytes(u, sec, begin, copy_to_rax_addr_expected, + sizeof copy_to_rax_addr_expected, + "x64 copy to RAX-based address"); + + begin = mc_pos(mc); + target->copy_bytes(target, src_addr, dst_addr, byte_copy); + sec = obj_section_get(obj, text); + expect_body_bytes(u, sec, begin, copy_from_rax_addr_expected, + sizeof copy_from_rax_addr_expected, + "x64 copy from RAX-based address"); + + byte_copy.size = 8u; + begin = mc_pos(mc); + target->set_bytes(target, dst_addr, rcx, byte_copy); + sec = obj_section_get(obj, text); + expect_body_bytes(u, sec, begin, set_rax_addr_expected, + sizeof set_rax_addr_expected, + "x64 set through RAX-based address"); + mc_free(mc); + kit_compiler_free(kc); +} + +static void explicit_and_machine_effects(KitUnit* u) { + Func f; + Inst in; + Operand opnds[3]; + OptInstClobberMask machine_clobbers[4]; + OptRegEffects effects; + + memset(&f, 0, sizeof f); + memset(&in, 0, sizeof in); + memset(machine_clobbers, 0, sizeof machine_clobbers); + f.opt_rewritten = 1; + f.emit_temp_regs[RC_INT][0] = 8; + f.emit_temp_regs[RC_INT][1] = 9; + f.emit_temp_reg_count[RC_INT] = 2; + f.inst_clobbers = machine_clobbers; + f.inst_clobbers_cap = 4; + + in.op = IR_BINOP; + in.id = 2; + in.opnds = opnds; + in.nopnds = 3; + opnds[0] = reg_op(RC_INT, 1); + opnds[1] = reg_op(RC_INT, 2); + opnds[2] = indirect_op(8, 8); + machine_clobbers[in.id][RC_INT] = reg_bit(5); + + opt_inst_reg_effects(&f, &in, &effects); + CU_EXPECT(u, effects.uses.cls[RC_INT] == + (reg_bit(2) | reg_bit(8)), + "explicit/indirect uses mismatch: %#x", effects.uses.cls[RC_INT]); + CU_EXPECT(u, effects.use_count[RC_INT][8] == 2, + "indirect base+index use count should saturate at two, got %u", + (unsigned)effects.use_count[RC_INT][8]); + CU_EXPECT(u, effects.defs.cls[RC_INT] == reg_bit(1), + "explicit defs mismatch: %#x", effects.defs.cls[RC_INT]); + CU_EXPECT(u, effects.clobbers.cls[RC_INT] == reg_bit(5), + "machine clobbers mismatch: %#x", + effects.clobbers.cls[RC_INT]); + + opnds[2].v.ind.base_kind = OPT_INDIRECT_FRAME; + opnds[2].v.ind.index_kind = OPT_INDIRECT_FRAME_ADDR; + opt_inst_reg_effects(&f, &in, &effects); + CU_EXPECT(u, effects.uses.cls[RC_INT] == reg_bit(2), + "frame-backed indirect components are not hard uses: %#x", + effects.uses.cls[RC_INT]); + CU_EXPECT(u, effects.clobbers.cls[RC_INT] == reg_bit(5), + "frame-backed components do not alter machine clobbers: %#x", + effects.clobbers.cls[RC_INT]); +} + +static void emission_temps_are_not_ir_effects(KitUnit* u) { + Func f; + Inst in; + Operand opnds[2]; + OptRegEffects effects; + + memset(&f, 0, sizeof f); + memset(&in, 0, sizeof in); + f.emit_temp_regs[RC_INT][0] = 8; + f.emit_temp_regs[RC_INT][1] = 9; + f.emit_temp_reg_count[RC_INT] = 2; + in.op = IR_COPY; + in.opnds = opnds; + in.nopnds = 2; + opnds[0] = reg_op(RC_INT, 8); + opnds[1] = reg_op(RC_INT, 2); + + opt_inst_reg_effects(&f, &in, &effects); + CU_EXPECT(u, effects.clobbers.cls[RC_INT] == 0, + "emission temps must not clobber virtual HIR: %#x", + effects.clobbers.cls[RC_INT]); + + f.opt_rewritten = 1; + opt_inst_reg_effects(&f, &in, &effects); + CU_EXPECT(u, effects.clobbers.cls[RC_INT] == 0, + "emission temps must remain private in location MIR: %#x", + effects.clobbers.cls[RC_INT]); +} + +static void call_and_asm_effects(KitUnit* u) { + Func f; + Inst call; + IRCallAux call_aux; + CGABIValue arg; + OptRegEffects effects; + + memset(&f, 0, sizeof f); + memset(&call, 0, sizeof call); + memset(&call_aux, 0, sizeof call_aux); + memset(&arg, 0, sizeof arg); + f.opt_caller_saved[RC_INT] = reg_bit(0) | reg_bit(1); + call.op = IR_CALL; + call.extra.aux = &call_aux; + call_aux.desc.callee = reg_op(RC_INT, 10); + call_aux.desc.args = &arg; + call_aux.desc.nargs = 1; + arg.storage = reg_op(RC_INT, 11); + call_aux.desc.ret.storage = reg_op(RC_INT, 0); + + opt_inst_reg_effects(&f, &call, &effects); + CU_EXPECT(u, effects.uses.cls[RC_INT] == + (reg_bit(10) | reg_bit(11)), + "call uses mismatch: %#x", effects.uses.cls[RC_INT]); + CU_EXPECT(u, effects.defs.cls[RC_INT] == reg_bit(0), + "call explicit results mismatch: %#x", effects.defs.cls[RC_INT]); + CU_EXPECT(u, effects.clobbers.cls[RC_INT] == + (reg_bit(0) | reg_bit(1)), + "call ABI clobbers mismatch: %#x", effects.clobbers.cls[RC_INT]); + + call_aux.plan_valid = 1; + call_aux.plan.clobber_mask[RC_INT] = reg_bit(6); + opt_inst_reg_effects(&f, &call, &effects); + CU_EXPECT(u, effects.clobbers.cls[RC_INT] == reg_bit(6), + "planned call clobbers must override ABI fallback: %#x", + effects.clobbers.cls[RC_INT]); + + Inst block; + IRAsmAux asm_aux; + Operand inputs[1]; + Operand outputs[1]; + memset(&block, 0, sizeof block); + memset(&asm_aux, 0, sizeof asm_aux); + block.op = IR_ASM_BLOCK; + block.extra.aux = &asm_aux; + inputs[0] = reg_op(RC_INT, 2); + outputs[0] = reg_op(RC_INT, 3); + asm_aux.in_ops = inputs; + asm_aux.nin = 1; + asm_aux.out_ops = outputs; + asm_aux.nout = 1; + asm_aux.clobber_mask[RC_INT] = reg_bit(4); + + opt_inst_reg_effects(&f, &block, &effects); + CU_EXPECT(u, effects.uses.cls[RC_INT] == reg_bit(2), + "asm uses mismatch: %#x", effects.uses.cls[RC_INT]); + CU_EXPECT(u, effects.defs.cls[RC_INT] == reg_bit(3), + "asm output defs mismatch: %#x", effects.defs.cls[RC_INT]); + CU_EXPECT(u, effects.clobbers.cls[RC_INT] == reg_bit(4), + "asm clobbers mismatch: %#x", effects.clobbers.cls[RC_INT]); +} + +static void machinize_rebuilds_derived_clobbers(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + NativeTarget* target; + CGFuncDesc desc; + Inst* in; + InstId id; + u32 entry; + + 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 machinize rebuild test"); + if (!kc) return; + c = (Compiler*)kc; + memset(&desc, 0, sizeof desc); + desc.fn_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + in = ir_emit(f, entry, IR_BINOP); + in->extra.imm = BO_SDIV; + id = in->id; + target = x64_native_target_new(c, NULL, NULL); + + opt_machinize_native(f, target); + CU_EXPECT(u, + f->inst_clobbers && id < f->inst_clobbers_cap && + f->inst_clobbers[id][RC_INT] != 0, + "first machinization did not derive the division clobber set"); + + /* Change the semantic instruction and rerun the derivation. The old table + * must not survive merely because the new instruction reports no fixed + * machine effect. */ + f->blocks[entry].insts[0].op = IR_COPY; + opt_machinize_native(f, target); + CU_EXPECT(u, f->inst_clobbers == NULL && f->inst_clobbers_cap == 0, + "machinization retained a stale per-instruction clobber table"); + + f->blocks[entry].insts[0].op = IR_BINOP; + f->blocks[entry].insts[0].extra.imm = BO_SDIV; + opt_machinize_native(f, target); + CU_EXPECT(u, + f->inst_clobbers && id < f->inst_clobbers_cap && + f->inst_clobbers[id][RC_INT] != 0, + "machinization did not rebuild a cleared clobber table"); + kit_compiler_free(kc); +} + +static void post_machinize_loop_imm_refreshes_shift_clobber(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + NativeTarget* target; + CGFuncDesc desc; + KitCgTypeId i64_type; + PReg dst; + PReg lhs; + Inst* shift; + Inst* lowered_shift = NULL; + InstId shift_id; + OptRegEffects effects; + u32 entry; + u32 initial_mask; + u32 stale_mask; + + 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 post-machinize effect refresh"); + if (!kc) return; + c = (Compiler*)kc; + i64_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + memset(&desc, 0, sizeof desc); + desc.fn_type = i64_type; + desc.result_type = i64_type; + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + f->blocks[entry].loop_depth = 1; + + dst = ir_alloc_preg(f, i64_type, RC_INT); + lhs = ir_alloc_preg(f, i64_type, RC_INT); + shift = ir_emit(f, entry, IR_BINOP); + shift->type = i64_type; + shift->def = dst; + shift->extra.imm = BO_SHL; + shift->nopnds = 3; + shift->opnds = arena_array(f->arena, Operand, shift->nopnds); + shift->opnds[0] = typed_reg_op(RC_INT, dst, i64_type); + shift->opnds[1] = typed_reg_op(RC_INT, lhs, i64_type); + memset(&shift->opnds[2], 0, sizeof shift->opnds[2]); + shift->opnds[2].kind = OPK_IMM; + shift->opnds[2].cls = RC_INT; + shift->opnds[2].type = i64_type; + shift->opnds[2].v.imm = 64; + shift_id = shift->id; + + target = x64_native_target_new(c, NULL, NULL); + opt_machinize_native(f, target); + initial_mask = f->inst_clobbers && shift_id < f->inst_clobbers_cap + ? f->inst_clobbers[shift_id][RC_INT] + : 0u; + CU_EXPECT(u, initial_mask == 0, + "immediate shift unexpectedly acquired an RCX clobber: %#x", + initial_mask); + + /* This is the production pipeline order: machinize first, then lift a + * non-foldable loop immediate into a register. On x64 that changes the + * encoding to the CL form, so the final HIR boundary must rederive RCX. */ + opt_lower_loop_imm_operands(f, target); + for (u32 i = 0; i < f->blocks[entry].ninsts; ++i) { + Inst* in = &f->blocks[entry].insts[i]; + if (in->id == shift_id) lowered_shift = in; + } + CU_EXPECT(u, + lowered_shift && lowered_shift->nopnds == 3 && + lowered_shift->opnds[2].kind == OPK_REG, + "loop immediate was not lowered to a variable shift"); + + stale_mask = f->inst_clobbers && shift_id < f->inst_clobbers_cap + ? f->inst_clobbers[shift_id][RC_INT] + : 0u; + CU_EXPECT(u, stale_mask == 0, + "modeled pre-refresh table was not the stale immediate-shift form: " + "%#x", + stale_mask); + + opt_refresh_machine_clobbers(f, target); + CU_EXPECT(u, + f->inst_clobbers && shift_id < f->inst_clobbers_cap && + f->inst_clobbers[shift_id][RC_INT] == reg_bit(1u), + "post-lowering x64 shift clobbers %#x, expected RCX", + f->inst_clobbers && shift_id < f->inst_clobbers_cap + ? f->inst_clobbers[shift_id][RC_INT] + : 0u); + if (lowered_shift) { + opt_inst_reg_effects(f, lowered_shift, &effects); + CU_EXPECT(u, effects.clobbers.cls[RC_INT] == reg_bit(1u), + "canonical variable-shift effects omit RCX: %#x", + effects.clobbers.cls[RC_INT]); + } + kit_compiler_free(kc); +} + +static void machinize_rejects_unknown_effect_registers(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + NativeTarget* target; + CGFuncDesc desc; + MachinizeRunCtx run; + KitStatus status; + Inst* in; + u32 entry; + + 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 invalid machine-effect test"); + if (!kc) return; + c = (Compiler*)kc; + memset(&desc, 0, sizeof desc); + desc.fn_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + in = ir_emit(f, entry, IR_BINOP); + in->extra.imm = BO_IADD; + target = x64_native_target_new(c, NULL, NULL); + target->machine_op_clobbers = invalid_machine_effect; + run.f = f; + run.target = target; + + u->last_diag[0] = '\0'; + ++u->suppress_fatal; + status = kit_frontend_run(kc, run_machinize, &run); + --u->suppress_fatal; + CU_EXPECT(u, status == KIT_ERR, + "unknown target machine-effect register was accepted"); + CU_EXPECT(u, + strstr(u->last_diag, + "machine clobber mask names unknown physical register") != + NULL, + "unexpected invalid machine-effect diagnostic: %s", u->last_diag); + kit_compiler_free(kc); +} + +static void regalloc_rebuilds_derived_constraints_and_hints(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + NativeTarget* target; + CGFuncDesc desc; + IRRetAux* ret_aux; + Inst* keep_def; + Inst* lhs_def; + Inst* rhs_def; + Inst* div; + Inst* keep_use; + Inst* ret; + PReg keep; + PReg lhs; + PReg rhs; + PReg quotient; + PReg keep_sink; + KitCgTypeId i64_type; + u32 entry; + u32 first_frequency; + const u32 x64_div_clobbers = reg_bit(0u) | reg_bit(2u); /* rax, rdx */ + + 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 regalloc rebuild test"); + if (!kc) return; + c = (Compiler*)kc; + i64_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + memset(&desc, 0, sizeof desc); + desc.fn_type = i64_type; + desc.result_type = i64_type; + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + + keep = ir_alloc_preg(f, i64_type, RC_INT); + lhs = ir_alloc_preg(f, i64_type, RC_INT); + rhs = ir_alloc_preg(f, i64_type, RC_INT); + quotient = ir_alloc_preg(f, i64_type, RC_INT); + keep_sink = ir_alloc_preg(f, i64_type, RC_INT); + + keep_def = ir_emit(f, entry, IR_LOAD_IMM); + keep_def->type = i64_type; + keep_def->def = keep; + keep_def->nopnds = 1; + keep_def->opnds = arena_array(f->arena, Operand, 1); + keep_def->opnds[0] = typed_reg_op(RC_INT, keep, i64_type); + keep_def->extra.imm = 7; + + lhs_def = ir_emit(f, entry, IR_LOAD_IMM); + lhs_def->type = i64_type; + lhs_def->def = lhs; + lhs_def->nopnds = 1; + lhs_def->opnds = arena_array(f->arena, Operand, 1); + lhs_def->opnds[0] = typed_reg_op(RC_INT, lhs, i64_type); + lhs_def->extra.imm = 84; + + rhs_def = ir_emit(f, entry, IR_LOAD_IMM); + rhs_def->type = i64_type; + rhs_def->def = rhs; + rhs_def->nopnds = 1; + rhs_def->opnds = arena_array(f->arena, Operand, 1); + rhs_def->opnds[0] = typed_reg_op(RC_INT, rhs, i64_type); + rhs_def->extra.imm = 2; + + div = ir_emit(f, entry, IR_BINOP); + div->type = i64_type; + div->def = quotient; + div->extra.imm = BO_SDIV; + div->nopnds = 3; + div->opnds = arena_array(f->arena, Operand, 3); + div->opnds[0] = typed_reg_op(RC_INT, quotient, i64_type); + div->opnds[1] = typed_reg_op(RC_INT, lhs, i64_type); + div->opnds[2] = typed_reg_op(RC_INT, rhs, i64_type); + + keep_use = ir_emit(f, entry, IR_COPY); + keep_use->type = i64_type; + keep_use->def = keep_sink; + keep_use->nopnds = 2; + keep_use->opnds = arena_array(f->arena, Operand, 2); + keep_use->opnds[0] = typed_reg_op(RC_INT, keep_sink, i64_type); + keep_use->opnds[1] = typed_reg_op(RC_INT, keep, i64_type); + + ret = ir_emit(f, entry, IR_RET); + ret_aux = arena_zarray(f->arena, IRRetAux, 1); + ret->extra.aux = ret_aux; + ret_aux->present = 1; + ret_aux->val.storage = typed_reg_op(RC_INT, quotient, i64_type); + + target = x64_native_target_new(c, NULL, NULL); + opt_build_cfg(f); + f->blocks[entry].frequency = 100; + opt_machinize_native(f, target); + opt_regalloc_locations(f, NULL); + first_frequency = f->preg_info[keep].frequency; + CU_EXPECT(u, + (f->preg_info[keep].forbidden_hard_regs & x64_div_clobbers) == + x64_div_clobbers, + "value live across div did not acquire rax/rdx forbids: %#x", + f->preg_info[keep].forbidden_hard_regs); + CU_EXPECT(u, f->preg_info[quotient].preferred_hard_reg == 0, + "returned quotient did not acquire the rax ABI hint: %d", + (int)f->preg_info[quotient].preferred_hard_reg); + + /* Both fields above are derived from this particular instruction stream. + * Remove their sources and rerun the supported allocation entry point. A + * previous allocation must not become an implicit input to the next one. */ + div->extra.imm = BO_IADD; + ret_aux->present = 0; + f->blocks[entry].frequency = 1; + opt_machinize_native(f, target); + opt_regalloc_locations(f, NULL); + CU_EXPECT(u, f->preg_info[keep].forbidden_hard_regs == 0, + "regalloc retained stale fixed-register forbids: %#x", + f->preg_info[keep].forbidden_hard_regs); + CU_EXPECT(u, f->preg_info[quotient].preferred_hard_reg < 0, + "regalloc retained a stale ABI preference: %d", + (int)f->preg_info[quotient].preferred_hard_reg); + CU_EXPECT(u, f->preg_info[keep].frequency < first_frequency, + "regalloc retained stale range frequency %u after recomputing %u", + first_frequency, f->preg_info[keep].frequency); + + kit_compiler_free(kc); +} + +static void rv64_syscall_fixed_effects(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + NativeTarget* target; + CGFuncDesc desc; + IRIntrinAux aux; + Inst* in; + InstId id; + u32 entry; + const u32 syscall_regs = (1u << 10u) | (1u << 11u) | (1u << 12u) | + (1u << 13u) | (1u << 14u) | (1u << 15u) | + (1u << 17u); + + CU_EXPECT(u, + kit_unit_compiler_new( + u, kit_unit_target(KIT_ARCH_RV64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed for rv64 syscall effect test"); + if (!kc) return; + c = (Compiler*)kc; + memset(&desc, 0, sizeof desc); + memset(&aux, 0, sizeof aux); + desc.fn_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + in = ir_emit(f, entry, IR_INTRINSIC); + in->extra.aux = &aux; + aux.kind = INTRIN_SYSCALL; + id = in->id; + target = rv64_native_target_new(c, NULL, NULL); + + opt_machinize_native(f, target); + CU_EXPECT(u, + f->inst_clobbers && id < f->inst_clobbers_cap && + f->inst_clobbers[id][RC_INT] == syscall_regs, + "rv64 syscall fixed effects mismatch: got %#x, expected %#x", + f->inst_clobbers && id < f->inst_clobbers_cap + ? f->inst_clobbers[id][RC_INT] + : 0u, + syscall_regs); + kit_compiler_free(kc); +} + +static void aa64_syscall_fixed_effects(KitUnit* u) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + NativeTarget* target; + CGFuncDesc desc; + IRIntrinAux aux; + Inst* in; + InstId id; + u32 entry; + const u32 syscall_regs = (1u << 0u) | (1u << 1u) | (1u << 2u) | + (1u << 3u) | (1u << 4u) | (1u << 5u) | + (1u << 8u); + + CU_EXPECT(u, + kit_unit_compiler_new( + u, kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed for aa64 syscall effect test"); + if (!kc) return; + c = (Compiler*)kc; + memset(&desc, 0, sizeof desc); + memset(&aux, 0, sizeof aux); + desc.fn_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + in = ir_emit(f, entry, IR_INTRINSIC); + in->extra.aux = &aux; + aux.kind = INTRIN_SYSCALL; + id = in->id; + target = aa64_native_target_new(c, NULL, NULL); + + opt_machinize_native(f, target); + CU_EXPECT(u, + f->inst_clobbers && id < f->inst_clobbers_cap && + f->inst_clobbers[id][RC_INT] == syscall_regs, + "aa64 syscall fixed effects mismatch: got %#x, expected %#x", + f->inst_clobbers && id < f->inst_clobbers_cap + ? f->inst_clobbers[id][RC_INT] + : 0u, + syscall_regs); + kit_compiler_free(kc); +} + +static void x64_tls_model_fixed_effects(KitUnit* u) { + static const struct { + KitOSKind os; + KitObjFmt obj; + u32 expected; + const char* name; + } cases[] = { + {KIT_OS_MACOS, KIT_OBJ_MACHO, (1u << 0u) | (1u << 7u), + "Mach-O descriptor"}, + {KIT_OS_LINUX, KIT_OBJ_ELF, 0u, "ELF local-exec"}, + }; + + for (u32 i = 0; i < sizeof cases / sizeof cases[0]; ++i) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + NativeTarget* target; + CGFuncDesc desc; + Inst* in; + InstId id; + u32 entry; + u32 got; + + CU_EXPECT(u, + kit_unit_compiler_new( + u, + kit_unit_target(KIT_ARCH_X86_64, cases[i].os, cases[i].obj), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed for x64 %s TLS effect test", + cases[i].name); + if (!kc) continue; + c = (Compiler*)kc; + memset(&desc, 0, sizeof desc); + desc.fn_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + in = ir_emit(f, entry, IR_TLS_ADDR_OF); + id = in->id; + target = x64_native_target_new(c, NULL, NULL); + + opt_machinize_native(f, target); + got = f->inst_clobbers && id < f->inst_clobbers_cap + ? f->inst_clobbers[id][RC_INT] + : 0u; + CU_EXPECT(u, got == cases[i].expected, + "x64 %s TLS fixed effects mismatch: got %#x, expected %#x", + cases[i].name, got, cases[i].expected); + if (!cases[i].expected) + CU_EXPECT(u, !f->inst_clobbers && f->inst_clobbers_cap == 0, + "x64 %s TLS unexpectedly allocated a clobber table", + cases[i].name); + kit_compiler_free(kc); + } +} + +static void aa64_tls_model_fixed_effects(KitUnit* u) { + static const struct { + KitOSKind os; + KitObjFmt obj; + u32 expected; + const char* name; + } cases[] = { + {KIT_OS_MACOS, KIT_OBJ_MACHO, 1u << 0u, "Mach-O descriptor"}, + {KIT_OS_LINUX, KIT_OBJ_ELF, 0u, "ELF local-exec"}, + }; + + for (u32 i = 0; i < sizeof cases / sizeof cases[0]; ++i) { + KitCompiler* kc = NULL; + Compiler* c; + Func* f; + NativeTarget* target; + CGFuncDesc desc; + Inst* in; + InstId id; + u32 entry; + u32 got; + + CU_EXPECT(u, + kit_unit_compiler_new( + u, + kit_unit_target(KIT_ARCH_ARM_64, cases[i].os, cases[i].obj), + &kc) == KIT_OK && + kc != NULL, + "compiler allocation failed for aa64 %s TLS effect test", + cases[i].name); + if (!kc) continue; + c = (Compiler*)kc; + memset(&desc, 0, sizeof desc); + desc.fn_type = kit_cg_type_builtin(kc, KIT_CG_BUILTIN_I64); + f = ir_func_new(c, &desc); + entry = ir_block_new(f); + f->entry = entry; + ir_note_emit(f, entry); + in = ir_emit(f, entry, IR_TLS_ADDR_OF); + id = in->id; + target = aa64_native_target_new(c, NULL, NULL); + + opt_machinize_native(f, target); + got = f->inst_clobbers && id < f->inst_clobbers_cap + ? f->inst_clobbers[id][RC_INT] + : 0u; + CU_EXPECT(u, got == cases[i].expected, + "aa64 %s TLS fixed effects mismatch: got %#x, expected %#x", + cases[i].name, got, cases[i].expected); + if (!cases[i].expected) + CU_EXPECT(u, !f->inst_clobbers && f->inst_clobbers_cap == 0, + "aa64 %s TLS unexpectedly allocated a clobber table", + cases[i].name); + kit_compiler_free(kc); + } +} + +int main(void) { + KitUnit u; + kit_unit_init(&u); + explicit_and_machine_effects(&u); + emission_temps_are_not_ir_effects(&u); + call_and_asm_effects(&u); + machinize_rebuilds_derived_clobbers(&u); + post_machinize_loop_imm_refreshes_shift_clobber(&u); + machinize_rejects_unknown_effect_registers(&u); + regalloc_rebuilds_derived_constraints_and_hints(&u); + rv64_syscall_fixed_effects(&u); + aa64_syscall_fixed_effects(&u); + x64_tls_model_fixed_effects(&u); + aa64_tls_model_fixed_effects(&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/CORPUS.md b/test/parse/CORPUS.md @@ -11,18 +11,22 @@ isolation; the parser cases prove the C front-end issues *the right cg sequence* end-to-end. Anything visible at runtime that the parser is expected to lower must have a parse-level row here. -Test paths per case (`run.sh`, sourced from `test/cg/run.sh`): +Test paths per case (`run.sh`, built on `test/lib/kit_corpus.sh`; DREJ run by +default, while C and W are opt-in): -- **D** in-process JIT (aarch64 host only) — `parse-runner --jit`. +- **D** in-process JIT — `parse-runner --jit`; the host architecture must match + the selected target. - **R** ELF roundtrip (host-arch agnostic) — `parse-runner --emit` → `kit-roundtrip` → `readelf` + `normalize.py` diff. -- **E** exec via qemu/podman — `parse-runner --emit` + `start.o` → - `link-exe-runner` → run. -- **J** jit-via-file (aarch64 host only) — `parse-runner --emit` → - `jit-runner`. - -`W` (DWARF directives) is reserved; once the parser drives `Debug` it -will read `<name>.dwarf` sidecars and reuse `cg-check-dwarf`. +- **E** executable run — `parse-runner --emit` + `start.o` → + `link-exe-runner` → qemu/podman, or the freestanding bare-metal runner for + rv32/arm32. +- **J** jit-via-file — `parse-runner --emit` → `jit-runner`; the host + architecture must match the selected target. +- **C** portable C backend — `parse-runner --emit-c` → host C compiler → run; + host and target must match, and this lane currently runs only at O0. +- **W** Wasm roundtrip — compile through the Wasm `CgTarget`, then re-lower and + run `test_main`; this lane currently runs only at O0. Sidecars (each missing file uses the documented default): @@ -30,6 +34,8 @@ Sidecars (each missing file uses the documented default): `test_main`'s return value. - `<name>.skip` — single-line reason; the runner SKIPs (treated as failure unless `KIT_TEST_ALLOW_SKIP=1`). +- `<name>.<arch>.skip` — whole-case opt-out for one target architecture. +- `<name>.cbackend.skip` / `<name>.wasm.skip` — lane-specific opt-outs for C/W. - `cases_err/<name>.errpat` — optional substring assertion against the parser's stderr. Negative cases without an errpat assert only that parse-runner exited nonzero. @@ -43,12 +49,6 @@ Sidecars (each missing file uses the documented default): accept it yet) - (deferred) — explicit non-goal for the current pass -Until the parser lands, every row that exercises a parser feature is -expected to FAIL — the build's `parse_c` stub panics on entry, so paths -D and `--emit` both report nonzero. The corpus is fixed in advance so -each parser-feature landing flips a known set of `·` rows to `★` with no -case authoring required mid-implementation. - ## Spine Initial landing — proves the harness wiring, diagnostic flow, and @@ -586,6 +586,27 @@ cursor, so mixed and class-only sequences both need direct exercise. | `variadic_06_mixed` | ★ | interleaved int/double `...`; per-class cursors run independently | 42 | | `variadic_07_nested_call` | ★ | `va_copy` to a separate cursor that's passed by `va_list*` to a helper, then the original cursor walked again | 42 | +## Codegen and optimizer regressions + +These end-to-end rows preserve frontend types and ownership boundaries through +CG recording, O1 location MIR, register allocation, and native emission. + +| Case | Status | Body | Expected | +|---|---|---|---| +| `asm_06_temp_boundary` | ★ | ARM/RV64 fixed and subset asm constraints are satisfied by instruction-local staging without escaping into MIR | 42 | +| `cg_inline_asm_clobber_return` | ★ | a return-bound value stays live across inline asm that clobbers the ABI return register | 42 | +| `cg_machine_clobber_return` | ★ | a return-bound value stays live across remainder's implicit fixed-register effects | 42 | +| `opt_04_global_incomplete_array_decay` | ★ | a completed global array may decay from an address recipe that retains its declaration's incomplete-array type | 42 | +| `opt_05_frame_addr_call_arg` | ★ | a spilled frame-address recipe is consumed directly as a variadic call argument under register pressure | 38 | +| `opt_06_flexible_array_element_address` | ★ | taking a flexible-array element address does not treat the incomplete array as a by-value storage operand | 42 | +| `opt_07_pointer_difference_index` | ★ | copy propagation preserves the integer interpretation of a pointer-difference index | 42 | +| `opt_08_copy_preserves_compare_type` | ★ | a type-changing physical copy does not replace the compare operand's 64-bit unsigned use-site type | 42 | +| `opt_09_copy_preserves_call_arg_type` | ★ | a type-changing physical copy does not replace the ABI argument's 64-bit use-site type | 42 | +| `opt_10_packed_local_member` | ★ | a packed member remains a typed, deliberately unaligned view into semantic frame storage | 0 | +| `opt_11_large_local_member_loop` | ★ | a lifted large member offset retains pointer arithmetic semantics through MIR address synthesis | 0 | +| `opt_12_small_aggregate_arg_spill` | ★ | 3-, 5-, 6-, and 7-byte aggregate arguments and returns move exactly their semantic bytes under pressure | 0 | +| `opt_13_pointer_first_record_copy` | ★ | the first by-value use completes a pointer-introduced record before aggregate-copy IR captures its size | 0 | + ## Negative cases (cases_err/) | Case | Status | Surface | Notes | diff --git a/test/parse/cases/asm_03_register_operand.c b/test/parse/cases/asm_03_register_operand.c @@ -1,9 +1,10 @@ /* GNU local register variables (`register T x __asm__("reg")`) used as inline- - * asm operands must occupy the named hard register. Each template below - * addresses its registers directly (not via %N operand substitution), so the - * result is correct only when a/b/r really land in the pinned registers — a - * regression guard for register-variable operand pinning through the native - * backends (-O0 direct and the optimizer's tied-hard-reg path). 40 + 2 == 42. + * asm operands must appear in the named hard register at the asm boundary. + * Each template below addresses its registers directly (not via %N operand + * substitution), so the result is correct only when a/b/r are staged into + * those registers for this instruction — a regression guard for the -O0 + * binder and the optimizer's instruction-local requirement/staging path. + * 40 + 2 == 42. * * Arch-guarded; the asm is target-specific. The wasm backend has no native * hard-register file, so this case opts out of W via sidecar. */ diff --git a/test/parse/cases/asm_06_temp_boundary.c b/test/parse/cases/asm_06_temp_boundary.c @@ -0,0 +1,19 @@ +/* Register constraints are properties of this asm boundary, not of a value's + * entire allocated live range. In particular, ARM r0-r3 and RISC-V t4-t6 are + * optimizer-emitter temp banks: explicit pins and restricted constraint sets + * must be satisfied by instruction-local staging without escaping into MIR. */ + +int test_main(void) { +#if defined(__arm__) + register long pinned __asm__("r0") = 40; + long low = 2; + __asm__ volatile("add r0, r0, %1" : "+r"(pinned) : "l"(low)); + return (int)pinned; +#elif defined(__riscv) && __riscv_xlen == 64 + register long pinned __asm__("t6") = 40; + __asm__ volatile("addi t6, t6, 2" : "+r"(pinned)); + return (int)pinned; +#else + return 42; +#endif +} diff --git a/test/parse/cases/asm_06_temp_boundary.expected b/test/parse/cases/asm_06_temp_boundary.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/asm_06_temp_boundary.wasm.skip b/test/parse/cases/asm_06_temp_boundary.wasm.skip @@ -0,0 +1 @@ +native hard-register constraints are intentionally target-specific diff --git a/test/parse/cases/cg_inline_asm_clobber_return.c b/test/parse/cases/cg_inline_asm_clobber_return.c @@ -0,0 +1,20 @@ +/* A return-value preference is only a hint: it must not place a value in the + * ABI return register while an intervening inline-asm block clobbers that + * register. At -O1 the old allocator cleared the asm-derived forbid while + * applying the return hint, so each native case below returned zero instead + * of 42. */ + +long clobber_return_reg(long value) { +#if defined(__aarch64__) + __asm__ volatile("mov x0, #0" : : : "x0"); +#elif defined(__x86_64__) + __asm__ volatile("xorq %%rax, %%rax" : : : "rax"); +#elif defined(__riscv) && __riscv_xlen == 64 + __asm__ volatile("li a0, 0" : : : "a0"); +#elif defined(__arm__) + __asm__ volatile("mov r0, #0" : : : "r0"); +#endif + return value; +} + +int test_main(void) { return (int)clobber_return_reg(42); } diff --git a/test/parse/cases/cg_inline_asm_clobber_return.expected b/test/parse/cases/cg_inline_asm_clobber_return.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/cg_inline_asm_clobber_return.wasm.skip b/test/parse/cases/cg_inline_asm_clobber_return.wasm.skip @@ -0,0 +1 @@ +native inline-asm clobber names and ABI return registers do not exist on wasm diff --git a/test/parse/cases/cg_machine_clobber_return.c b/test/parse/cases/cg_machine_clobber_return.c @@ -0,0 +1,17 @@ +/* Keep a return-bound value live across an operation with implicit machine + * register effects. In particular, x86-64 div writes rax:rdx even though its + * MIR destination need not name either register. Return placement must not + * erase the live value's fixed-clobber exclusion. */ + +volatile unsigned long machine_clobber_sink; + +unsigned long keep_across_remainder(unsigned long keep, + unsigned long dividend, + unsigned long divisor) { + machine_clobber_sink = dividend % divisor; + return keep; +} + +int test_main(void) { + return (int)keep_across_remainder(42, 12345, 97); +} diff --git a/test/parse/cases/cg_machine_clobber_return.expected b/test/parse/cases/cg_machine_clobber_return.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/opt_04_global_incomplete_array_decay.c b/test/parse/cases/opt_04_global_incomplete_array_decay.c @@ -0,0 +1,13 @@ +/* A global address recipe may retain the declaration's incomplete-array type + * even after the initializer completes the object. MIR only needs the symbol + * address here; requiring a sized referent rejects ordinary array decay. */ +static const char* const options[] = {"alpha", "beta", "gamma"}; + +static const char* const* compile_options(void) { + return options; +} + +int test_main(void) { + const char* const* p = compile_options(); + return p[0][0] == 'a' && p[2][0] == 'g' ? 42 : 1; +} diff --git a/test/parse/cases/opt_04_global_incomplete_array_decay.expected b/test/parse/cases/opt_04_global_incomplete_array_decay.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/opt_05_frame_addr_call_arg.c b/test/parse/cases/opt_05_frame_addr_call_arg.c @@ -0,0 +1,12 @@ +/* Keep enough values live across the call to spill the cheap `&x` producer. + * MIR rematerializes it as OPK_FRAME_ADDR; ABI marshalling must consume that + * address recipe directly rather than demanding another optimizer temp. */ +__attribute__((noinline)) static void sink(int* p, ...) { *p += 1; } + +static int pressure(int a, int b, int c, int d, int e, int f, int g, int h) { + int x = a; + sink(&x, a + 1, b + 2, c + 3, d + 4, e + 5, f + 6, g + 7, h + 8); + return x + a + b + c + d + e + f + g + h; +} + +int test_main(void) { return pressure(1, 2, 3, 4, 5, 6, 7, 8); } diff --git a/test/parse/cases/opt_05_frame_addr_call_arg.expected b/test/parse/cases/opt_05_frame_addr_call_arg.expected @@ -0,0 +1 @@ +38 diff --git a/test/parse/cases/opt_06_flexible_array_element_address.c b/test/parse/cases/opt_06_flexible_array_element_address.c @@ -0,0 +1,37 @@ +/* Taking the address of an element in a flexible array carries the unsized + * array lvalue through IR_ADDR_OF. It computes an address and does not access + * the incomplete array object as a by-value storage operand. */ +typedef struct Segment { + int value; +} Segment; + +typedef struct Iterator { + int count; + Segment segments[]; +} Iterator; + +typedef struct Backing { + int count; + Segment segments[2]; +} Backing; + +/* A union containing the flexible-array structure is allowed to provide the + * backing storage. Iterator and Backing have a compatible common initial + * sequence, so this exercises the flexible member without casting an + * unrelated declared object through Iterator (which would violate C11's + * effective-type rules). */ +typedef union Storage { + Iterator iterator; + Backing backing; +} Storage; + +__attribute__((noinline)) static Segment* segment_at(Iterator* p, int i) { + return &p->segments[i]; +} + +int test_main(void) { + Storage storage = {0}; + Segment* segment = segment_at(&storage.iterator, 1); + segment->value = 42; + return storage.backing.segments[1].value; +} diff --git a/test/parse/cases/opt_06_flexible_array_element_address.expected b/test/parse/cases/opt_06_flexible_array_element_address.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/opt_07_pointer_difference_index.c b/test/parse/cases/opt_07_pointer_difference_index.c @@ -0,0 +1,14 @@ +/* A same-width pointer-to-ptrdiff conversion may become a physical copy after + * allocation. Copy propagation may change the index register, but the + * indirect use must retain its integer interpretation. */ +__attribute__((noinline)) static const char* pick(const char* base, + const char* tail, + char* copy) { + return &base[tail - copy]; +} + +int test_main(void) { + char base[] = "0123456789"; + char copy[] = "abcdefghij"; + return *pick(base, copy + 4, copy) == '4' ? 42 : 1; +} diff --git a/test/parse/cases/opt_07_pointer_difference_index.expected b/test/parse/cases/opt_07_pointer_difference_index.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/opt_08_copy_preserves_compare_type.c b/test/parse/cases/opt_08_copy_preserves_compare_type.c @@ -0,0 +1,14 @@ +/* A narrow signed load may be folded with its widening conversion, leaving a + * physical copy whose source and destination have different semantic types. + * Copy propagation may retarget the compare's register, but the compare must + * retain its 64-bit unsigned interpretation. On AArch64, losing that use-site + * type selected `cmp wN, wM` and discarded the sign-extended high bits. */ +__attribute__((noinline)) static int compare_widened(volatile signed char* p) { + long long widened = *p; + return (unsigned long long)widened > 0xffffffffULL ? 42 : 1; +} + +int test_main(void) { + volatile signed char value = -1; + return compare_widened(&value); +} diff --git a/test/parse/cases/opt_08_copy_preserves_compare_type.expected b/test/parse/cases/opt_08_copy_preserves_compare_type.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/opt_09_copy_preserves_call_arg_type.c b/test/parse/cases/opt_09_copy_preserves_call_arg_type.c @@ -0,0 +1,17 @@ +/* ABI call plans consume an already-typed value. If a type-changing physical + * copy is propagated into a planned argument, only its register identity may + * change: replacing the argument operand's 64-bit type with the narrow load's + * type makes AArch64 marshal it with `mov wN, wM`, zeroing the high half. */ +__attribute__((noinline)) static int observe(unsigned long long value) { + return value > 0xffffffffULL ? 42 : 1; +} + +__attribute__((noinline)) static int pass_widened(volatile signed char* p) { + long long widened = *p; + return observe((unsigned long long)widened); +} + +int test_main(void) { + volatile signed char value = -1; + return pass_widened(&value); +} diff --git a/test/parse/cases/opt_09_copy_preserves_call_arg_type.expected b/test/parse/cases/opt_09_copy_preserves_call_arg_type.expected @@ -0,0 +1 @@ +42 diff --git a/test/parse/cases/opt_10_packed_local_member.c b/test/parse/cases/opt_10_packed_local_member.c @@ -0,0 +1,16 @@ +/* A folded local operand is a typed view into semantic frame storage, not a + * scalar spill home. In particular, a member of a packed local can have a + * natural type alignment greater than the aggregate frame slot alignment. + * The access is valid (and deliberately unaligned); O1 must not reject the + * OPK_LOCAL view while lowering physical MIR. */ +typedef struct __attribute__((packed)) Packed { + unsigned value; +} Packed; + +__attribute__((noinline)) static unsigned packed_roundtrip(unsigned value) { + Packed local; + local.value = value; + return local.value; +} + +int test_main(void) { return packed_roundtrip(42u) != 42u; } diff --git a/test/parse/cases/opt_11_large_local_member_loop.c b/test/parse/cases/opt_11_large_local_member_loop.c @@ -0,0 +1,17 @@ +/* A non-encodable member offset used in a loop is lifted into a register. + * Pointer arithmetic keeps that offset register pointer-typed through MIR; + * address synthesis must not reinterpret it as an integer scaled index. */ +typedef struct BigLocal { + unsigned char padding[5000]; + int value; +} BigLocal; + +__attribute__((noinline)) static int update_large_member(unsigned count) { + BigLocal local; + unsigned i; + local.value = 0; + for (i = 0; i < count; ++i) local.value = 42; + return local.value; +} + +int test_main(void) { return update_large_member(3) != 42; } diff --git a/test/parse/cases/opt_12_small_aggregate_arg_spill.c b/test/parse/cases/opt_12_small_aggregate_arg_spill.c @@ -0,0 +1,198 @@ +/* Non-power-of-two ABI aggregate parts must move only their semantic bytes. + * A widened register spill may overlap an adjacent frame slot; a narrowed + * transfer silently drops the tail. Three, five, six, and seven bytes cover + * every non-power-of-two integer-carrier width and the 32-bit lane boundary. */ +typedef struct TinyOptions { + unsigned char packed; + unsigned short max_align; + unsigned short align_override; +} TinyOptions; + +typedef struct TinyThree { + unsigned char first; + unsigned char second; + unsigned char third; +} TinyThree; + +typedef struct TinyFive { + unsigned char first; + unsigned char second; + unsigned char third; + unsigned char fourth; + unsigned char fifth; +} TinyFive; + +typedef struct TinySeven { + unsigned char first; + unsigned char second; + unsigned char third; + unsigned char fourth; + unsigned char fifth; + unsigned char sixth; + unsigned char seventh; +} TinySeven; + +typedef struct Builder { + void* pool; + unsigned kind; + unsigned tag; + unsigned symbol; + TinyOptions opts; + TinyThree three; + TinyFive five; + TinySeven seven; +} Builder; + +__attribute__((noinline)) static void barrier(Builder* b) { + volatile unsigned* p = &b->kind; + *p = *p; +} + +__attribute__((noinline)) static Builder* begin(Builder* b, unsigned kind, + unsigned tag, unsigned symbol, + TinyOptions opts) { + barrier(b); + b->pool = b; + b->kind = kind; + b->tag = tag; + b->symbol = symbol; + b->opts = opts; + return b; +} + +__attribute__((noinline)) static Builder* begin_three(Builder* b, + unsigned kind, + unsigned tag, + unsigned symbol, + TinyThree three) { + barrier(b); + b->pool = b; + b->kind = kind; + b->tag = tag; + b->symbol = symbol; + b->three = three; + return b; +} + +__attribute__((noinline)) static Builder* begin_five(Builder* b, unsigned kind, + unsigned tag, + unsigned symbol, + TinyFive five) { + barrier(b); + b->pool = b; + b->kind = kind; + b->tag = tag; + b->symbol = symbol; + b->five = five; + return b; +} + +__attribute__((noinline)) static Builder* begin_seven(Builder* b, + unsigned kind, + unsigned tag, + unsigned symbol, + TinySeven seven) { + barrier(b); + b->pool = b; + b->kind = kind; + b->tag = tag; + b->symbol = symbol; + b->seven = seven; + return b; +} + +/* Returning a short aggregate exercises the call-plan write phase as well as + * its incoming argument bind. On 64-bit targets the pointer input and integer + * return carrier can alias; on ARM32 a three-byte tail has an I32 carrier. Both + * cases must retain the semantic width through the complete return move. */ +__attribute__((noinline)) static TinyOptions load_options( + const TinyOptions* opts) { + return *opts; +} + +__attribute__((noinline)) static TinyThree load_three(const TinyThree* three) { + return *three; +} + +__attribute__((noinline)) static TinyFive load_five(const TinyFive* five) { + return *five; +} + +__attribute__((noinline)) static TinySeven load_seven(const TinySeven* seven) { + return *seven; +} + +__attribute__((noinline)) static int exercise(Builder* b, unsigned kind, + unsigned tag, unsigned symbol, + int* sentinel) { + TinyOptions opts = {1, 0x2345, 0x6789}; + TinyOptions returned = load_options(&opts); + Builder* out = begin(b, kind, tag, symbol, opts); + return out == b && out->kind == kind && out->tag == tag && + out->symbol == symbol && out->opts.packed == opts.packed && + out->opts.max_align == opts.max_align && + out->opts.align_override == opts.align_override && + returned.packed == opts.packed && + returned.max_align == opts.max_align && + returned.align_override == opts.align_override && *sentinel == 42; +} + +__attribute__((noinline)) static int exercise_three(Builder* b, unsigned kind, + unsigned tag, + unsigned symbol, + int* sentinel) { + TinyThree three = {0x12, 0x34, 0x56}; + TinyThree returned = load_three(&three); + Builder* out = begin_three(b, kind, tag, symbol, three); + return out == b && out->kind == kind && out->tag == tag && + out->symbol == symbol && out->three.first == three.first && + out->three.second == three.second && out->three.third == three.third && + returned.first == three.first && returned.second == three.second && + returned.third == three.third && + *sentinel == 42; +} + +__attribute__((noinline)) static int exercise_five(Builder* b, unsigned kind, + unsigned tag, + unsigned symbol, + int* sentinel) { + TinyFive five = {0x10, 0x20, 0x30, 0x40, 0x50}; + TinyFive returned = load_five(&five); + Builder* out = begin_five(b, kind, tag, symbol, five); + return out == b && out->kind == kind && out->tag == tag && + out->symbol == symbol && out->five.first == five.first && + out->five.second == five.second && out->five.third == five.third && + out->five.fourth == five.fourth && out->five.fifth == five.fifth && + returned.first == five.first && returned.second == five.second && + returned.third == five.third && returned.fourth == five.fourth && + returned.fifth == five.fifth && *sentinel == 42; +} + +__attribute__((noinline)) static int exercise_seven(Builder* b, unsigned kind, + unsigned tag, + unsigned symbol, + int* sentinel) { + TinySeven seven = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77}; + TinySeven returned = load_seven(&seven); + Builder* out = begin_seven(b, kind, tag, symbol, seven); + return out == b && out->kind == kind && out->tag == tag && + out->symbol == symbol && out->seven.first == seven.first && + out->seven.second == seven.second && + out->seven.third == seven.third && + out->seven.fourth == seven.fourth && + out->seven.fifth == seven.fifth && out->seven.sixth == seven.sixth && + out->seven.seventh == seven.seventh && + returned.first == seven.first && returned.second == seven.second && + returned.third == seven.third && returned.fourth == seven.fourth && + returned.fifth == seven.fifth && returned.sixth == seven.sixth && + returned.seventh == seven.seventh && *sentinel == 42; +} + +int test_main(void) { + Builder b = {0}; + int sentinel = 42; + if (!exercise(&b, 11, 22, 33, &sentinel)) return 1; + if (!exercise_three(&b, 44, 55, 66, &sentinel)) return 2; + if (!exercise_five(&b, 77, 88, 99, &sentinel)) return 3; + return exercise_seven(&b, 111, 122, 133, &sentinel) ? 0 : 4; +} diff --git a/test/parse/cases/opt_13_pointer_first_record_copy.c b/test/parse/cases/opt_13_pointer_first_record_copy.c @@ -0,0 +1,23 @@ +/* A record first crossing the CG boundary through a pointer has only a nominal + * declaration there. Its first by-value use must complete the record layout + * before aggregate-copy IR captures the byte count. Keep clone_value before + * test_main so no earlier by-value local happens to complete Value for it. */ +typedef unsigned long long U64; + +typedef struct Value { + U64 word[8]; +} Value; + +__attribute__((noinline)) static void clone_value(Value* dst, + const Value* src) { + *dst = *src; +} + +int test_main(void) { + Value src = {{1, 2, 3, 4, 5, 6, 7, 8}}; + Value dst = {{0, 0, 0, 0, 0, 0, 0, 0}}; + clone_value(&dst, &src); + for (unsigned i = 0; i < 8; ++i) + if (dst.word[i] != src.word[i]) return 1; + return 0; +} diff --git a/test/toy/cases/155_musttail_indirect_packed_stack.toy b/test/toy/cases/155_musttail_indirect_packed_stack.toy @@ -0,0 +1,25 @@ +// Keep this call genuinely indirect and force Tiny3 past all four AAPCS32 core +// argument registers. ARM32 must preserve the target in IP while its exact +// three-byte stack-argument load uses LR as the carrier and IP to pack byte 2. +record @[.packed] Tiny3 { + x: u8, + y: u8, + z: u8, +} + +fn @[.noinline] check(a: i32, b: i32, c: i32, d: i32, tiny: Tiny3): i32 { + return (a - 11) + (b - 13) + (c - 17) + (d - 19) + + (tiny.x as i32 - 23) + (tiny.y as i32 - 29) + + (tiny.z as i32 - 31); +} + +fn @[.noinline] forward(fp: *fn(i32, i32, i32, i32, Tiny3): i32, + a: i32, b: i32, c: i32, d: i32, + tiny: Tiny3): i32 { + return musttail fp(a, b, c, d, tiny); +} + +fn main(): i32 { + let tiny: Tiny3 = Tiny3 { x: 23, y: 29, z: 31 }; + return forward(check, 11, 13, 17, 19, tiny); +} diff --git a/test/toy/cases/155_musttail_indirect_packed_stack.wasm.skip b/test/toy/cases/155_musttail_indirect_packed_stack.wasm.skip @@ -0,0 +1 @@ +ARM32-specific packed stack-argument regression; Wasm musttail roundtrip currently produces an invalid local index diff --git a/test/toy/cases/156_indirect_stack_args.toy b/test/toy/cases/156_indirect_stack_args.toy @@ -0,0 +1,25 @@ +// An indirect target must survive the complete argument-marshalling phase. +// Ten integer arguments force two values through the outgoing stack while the +// target itself arrives as a runtime parameter rather than a direct symbol. + +type Sum10 = fn( + i64, i64, i64, i64, i64, + i64, i64, i64, i64, i64 +): i64; + +fn sum10( + a0: i64, a1: i64, a2: i64, a3: i64, a4: i64, + a5: i64, a6: i64, a7: i64, a8: i64, a9: i64 +): i64 { + return a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9; +} + +fn apply10(fp: *Sum10, seed: i64): i64 { + return fp(seed, 2, 3, 4, 5, 6, 7, 8, 9, 10); +} + +fn __user_main(): i64 { + return apply10(sum10, 1) - 55; +} + +fn main(): i32 { return __user_main() as i32; }