commit 3b67636bfc544336856a69b9a7f419d30b0f0858
parent 2c77a938603088abd0c636df6758674e9a10e723
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 16:24:59 -0700
arm32 Phase 1: 32-bit ARM (ARMv7-M Thumb-2) walking skeleton
`make test-cross TARGET=freestanding-arm32 DEPTH=smoke` is green: kit cc
compiles arm-none-eabi objects, kit ld links them with a clang-assembled
Cortex-M reset stub + linker script, qemu-system-arm (mps2-an385) runs the
image, and the ARM-semihosting SYS_EXIT_EXTENDED exit code matches.
Backend (src/arch/arm32/): arch (ArchImpl + Thumb-2 label-fixup patcher +
predefined macros/features), isa.h (header-only Thumb-2 encoders incl.
ThumbExpandImm), native.c (the -O0 NativeTarget spine: FP-anchored deferred-sub
frame, r0-r3 NDT pool + IP scratch + lr emit-temp, MOVW/MOVT immediates,
ALU/mul/sdiv-udiv-mls/shifts/extends, CMP+IT compare + B<cond>.W branches,
AAPCS calls/returns), reloc.c (THM_CALL/JUMP24 split-immediate patcher),
link.c, regs.c, variant.{h,c}, dbg/disasm/asm stubs.
ABI: src/abi/abi_aapcs32.c (AAPCS32 soft-float: r0-r3 + stack, i64/double GPR
pair, sret in r0, scalar_split_lane_size=4 for the wide8 path).
ELF: EM_ARM + EF_ARM_* + R_ARM_* (obj.h enum tail + elf.h), reloc_arm.c
mapper, obj/registry.c arch-ops row, EM_ARM e_flags synthesis.
Gating/registration: KIT_ARCH_ARM32_ENABLED + arch_impl_arm32 + aapcs32_vtable
across the arch/abi/obj registries, config_assert, mk/lib_srcs.mk, and the
disasm/dbg/link stub files.
Cross-test lane: scripts/hosted.sh (freestanding-arm32), test/lib/exec_bare.sh
(_bare_emit_arm32 + mps2-an385 semihosting oracle, kit-ld-compatible script),
cross_test.sh (arm32 smoke at -O0), mk/test.mk (test-smoke-arm32), plus
test/smoke/arm32.sh + check_arm32_env.sh.
Phase 2 (panic-stubbed/deferred per doc/plan/ARM32.md): -O1 known-frame,
i64/soft-double, atomics/TLS/varargs/inline-asm/structs, disassembler +
assembler, kit-built runtime, SRAM .data LMA.
Diffstat:
37 files changed, 3144 insertions(+), 18 deletions(-)
diff --git a/doc/plan/ARM32.md b/doc/plan/ARM32.md
@@ -1,10 +1,31 @@
# Plan: 32-bit ARM (`arm-none-eabi`, ARMv7-M / ARMv7E-M, Thumb-2)
-## Status — 2026-06-16 — not started; scaffolding inventory + spec
-
-No `arm32` code backend exists yet (`src/arch/` has `aa64`, `riscv`, `x64`,
-`wasm`, `c_target` — no `arm32`). This doc specs the ISA, the backend shape, and
-the cross-test surface so the work is farmable.
+## Status — 2026-06-16 — Phase 1 (walking skeleton) LANDED
+
+**Phase 1 is green:** `make test-cross TARGET=freestanding-arm32 DEPTH=smoke`
+passes — `kit cc -target arm-none-eabi -mcpu=cortex-m3 -ffreestanding` compiles
+the smoke set, `kit ld` links it with a clang-assembled Cortex-M reset stub +
+linker script, `qemu-system-arm -M mps2-an385` runs it, and the ARM-semihosting
+`SYS_EXIT_EXTENDED` exit code matches. The whole `src/arch/arm32/` backend
+(`arch/isa/native/reloc/link/regs/variant/dbg/disasm/asm`), the AAPCS32 ABI
+vtable (`src/abi/abi_aapcs32.c`), the ARM ELF relocations
+(`src/obj/elf/reloc_arm.c` + `EM_ARM`/`EF_ARM_*`/`R_ARM_*`), the
+`KIT_ARCH_ARM32_ENABLED` gate + `arch_impl_arm32` registration, and the
+`freestanding-arm32` cross-test lane are in. Validated at -O0: return-const, ALU
+(add/sub/mul/and/or/eor/shift), sdiv/udiv + mls remainder, signed/unsigned
+compare + if/else, recursion, and ≤4-arg + >4-arg (stack) calls — all correct
+under qemu; disassembly matches `llvm-objdump`.
+
+Scoped to Phase 2 (panic-stubbed or deferred today, per the plan below): the
+`-O1` known-frame path (Phase 1 is -O0 only — the cross smoke lane forces -O0
+for arm32), i64/soft-double wide8 lowering, atomics/TLS/varargs/inline-asm/
+struct-by-value, the descriptor-driven disassembler + standalone assembler, the
+runtime build with kit (the smoke set needs no `libkit_rt.a`), and a kit-ld
+linker script with a real SRAM `.data` LMA (kit ld's script front end lacks
+`AT>`, so the global-free Phase-1 lane keeps everything in flash).
+
+The "not started" inventory + ISA/backend/cross-test spec below remains the
+source of truth for the remaining (Phase 2) work.
The target is a freestanding **Cortex-M** toolchain: `kit cc/as/ld/objdump/disas`
producing and consuming correct `arm-none-eabi` ELFCLASS32 objects and static
diff --git a/include/kit/config.h b/include/kit/config.h
@@ -26,6 +26,7 @@
/* Backend architectures. */
#define KIT_ARCH_AA64_ENABLED 1
#define KIT_ARCH_X64_ENABLED 1
+#define KIT_ARCH_ARM32_ENABLED 1
#define KIT_ARCH_RV32_ENABLED 1
#define KIT_ARCH_RV64_ENABLED 1
#define KIT_ARCH_WASM_ENABLED 1
diff --git a/mk/lib_srcs.mk b/mk/lib_srcs.mk
@@ -23,6 +23,7 @@ flatobjs = $(foreach s,$(1),$(BUILD_DIR)/$(3)/$(dir $(patsubst $(2)/%,%,$(s)))$(
define arch-feature-off
LIB_SRCS_ARCH_AA64 := $$(filter-out %/$(1),$$(LIB_SRCS_ARCH_AA64))
LIB_SRCS_ARCH_X64 := $$(filter-out %/$(1),$$(LIB_SRCS_ARCH_X64))
+LIB_SRCS_ARCH_ARM32 := $$(filter-out %/$(1),$$(LIB_SRCS_ARCH_ARM32))
LIB_SRCS_ARCH_RISCV := $$(filter-out %/$(1),$$(LIB_SRCS_ARCH_RISCV))
LIB_SRCS_NONARCH += $(2)
endef
@@ -57,6 +58,7 @@ LIB_SRCS_NONARCH = $(LIB_SRCS_ABI_CORE) \
# hundreds on every `make` invocation.
LIB_SRCS_ARCH_AA64 := $(shell find src/arch/aa64 -name '*.c' 2>/dev/null)
LIB_SRCS_ARCH_X64 := $(shell find src/arch/x64 -name '*.c' 2>/dev/null)
+LIB_SRCS_ARCH_ARM32 := $(shell find src/arch/arm32 -name '*.c' 2>/dev/null)
LIB_SRCS_ARCH_RISCV := $(shell find src/arch/riscv -name '*.c' 2>/dev/null)
LIB_SRCS_ARCH_WASM := $(shell find src/arch/wasm -name '*.c' 2>/dev/null)
LIB_SRCS_ARCH_C_TARGET := $(shell find src/arch/c_target -name '*.c' 2>/dev/null)
@@ -97,6 +99,9 @@ endif
ifeq ($(filter 1,$(KIT_ARCH_RV32_ENABLED) $(KIT_ARCH_RV64_ENABLED)),)
LIB_SRCS_OBJ_ELF := $(filter-out %/reloc_riscv32.c %/reloc_riscv64.c,$(LIB_SRCS_OBJ_ELF))
endif
+ifneq ($(KIT_ARCH_ARM32_ENABLED),1)
+LIB_SRCS_OBJ_ELF := $(filter-out %/reloc_arm.c,$(LIB_SRCS_OBJ_ELF))
+endif
ifneq ($(KIT_LINK_ENABLED),1)
LIB_SRCS_OBJ_ELF := $(filter-out %/link.c %/link_dyn.c,$(LIB_SRCS_OBJ_ELF))
LIB_SRCS_OBJ_MACHO := $(filter-out %/link.c,$(LIB_SRCS_OBJ_MACHO))
@@ -159,6 +164,7 @@ LIB_SRC_ABI_SYSV_X64 = src/abi/abi_sysv_x64.c
LIB_SRC_ABI_APPLE_X64 = src/abi/abi_apple_x64.c
LIB_SRC_ABI_WIN64_X64 = src/abi/abi_win64_x64.c
LIB_SRC_ABI_RV64 = src/abi/abi_rv64.c
+LIB_SRC_ABI_AAPCS32 = src/abi/abi_aapcs32.c
LIB_SRCS = $(LIB_SRCS_NONARCH)
ifeq ($(KIT_OPT_ENABLED),1)
@@ -211,6 +217,9 @@ endif
ifeq ($(KIT_ARCH_X64_ENABLED),1)
LIB_SRCS += $(LIB_SRCS_ARCH_X64)
endif
+ifeq ($(KIT_ARCH_ARM32_ENABLED),1)
+LIB_SRCS += $(LIB_SRCS_ARCH_ARM32)
+endif
ifneq ($(filter 1,$(KIT_ARCH_RV32_ENABLED) $(KIT_ARCH_RV64_ENABLED)),)
LIB_SRCS += $(LIB_SRCS_ARCH_RISCV)
endif
@@ -275,6 +284,11 @@ ifeq ($(KIT_OBJ_ELF_ENABLED),1)
LIB_SRCS += $(LIB_SRC_ABI_RV64)
endif
endif
+ifneq ($(filter 1,$(KIT_ARCH_ARM32_ENABLED) $(KIT_ARCH_C_TARGET_ENABLED)),)
+ifeq ($(KIT_OBJ_ELF_ENABLED),1)
+LIB_SRCS += $(LIB_SRC_ABI_AAPCS32)
+endif
+endif
# Per-frontend source sets. Each is gated by its KIT_LANG_*_ENABLED flag
# from mk/config.mk so the matching `#if` in src/api/lang_registry.c and
diff --git a/mk/test.mk b/mk/test.mk
@@ -124,6 +124,7 @@ TEST_TARGETS = \
test-rv64-tls-link \
test-smoke-rv64 \
test-smoke-rv32 \
+ test-smoke-arm32 \
test-smoke-x64 \
test-smoke-x64-macos \
test-toy \
@@ -1203,6 +1204,13 @@ test-smoke-rv64:
test-smoke-rv32:
bash test/smoke/rv32.sh
+# test-smoke-arm32: behavioral oracle for arm-none-eabi (Cortex-M3, Thumb-2)
+# codegen under qemu-system-arm -machine mps2-an385 (single soft-float lane,
+# i64 + soft-double + a <=4-int-arg call + a switch). Skips if the arm32
+# toolchain/qemu prerequisites are absent (see test/lib/check_arm32_env.sh).
+test-smoke-arm32:
+ bash test/smoke/arm32.sh
+
# The whole-toolchain bare-metal smoke (compile + assemble reset stub + link +
# boot under qemu-system, for aarch64/x86_64/riscv64/riscv32) is now driven by
# make test-cross TARGET=freestanding
diff --git a/scripts/cross_test.sh b/scripts/cross_test.sh
@@ -184,9 +184,13 @@ smoke_build_case() {
if [ "$os" = freestanding ]; then
# Bare-metal: compile to an object, link with the reset stub now, and leave
# only the qemu boot for the execution phase.
- local obj="$cdir/$name.o" elf="$cdir/$name.elf" barch reason
+ local obj="$cdir/$name.o" elf="$cdir/$name.elf" barch reason smopt
barch="$(tok_arch "$t")"
- if ! "$HOSTED" cc "$t" -O1 -ffreestanding -c "$src" -o "$obj" \
+ # arm32 is a -O0-only Phase-1 walking skeleton (the -O1 known-frame path is
+ # a Phase-2 code-quality track, per doc/plan/ARM32.md); every other
+ # freestanding arch exercises the optimizer at -O1.
+ smopt=O1; [ "$barch" = arm32 ] && smopt=O0
+ if ! "$HOSTED" cc "$t" -$smopt -ffreestanding -c "$src" -o "$obj" \
>"$cdir/$name.cc.out" 2>"$cdir/$name.cc.err"; then
kit_fail "$label:cc" "cc failed"; sed 's/^/ | /' "$cdir/$name.cc.err" | head; return
fi
diff --git a/scripts/hosted.sh b/scripts/hosted.sh
@@ -10,7 +10,7 @@
# Targets: <os>[-<libc>]-<arch> (canonical arch tokens: aa64/x64/rv64[/rv32])
# linux-glibc-{aa64,x64,rv64} linux-musl-{aa64,x64,rv64}
# freebsd-{aa64,x64,rv64} windows-{aa64,x64} macos-aa64
-# freestanding-{aa64,x64,rv64,rv32} (no OS — cross only, bare-metal exec)
+# freestanding-{aa64,x64,rv64,rv32,arm32} (no OS — cross only, bare-metal exec)
# Long arch spellings (aarch64/amd64/x86_64/riscv64) are accepted as input
# aliases; canon_token normalizes them to the short forms above.
#
@@ -44,7 +44,7 @@ linux-musl-aa64 linux-musl-x64 linux-musl-rv64
freebsd-aa64 freebsd-x64 freebsd-rv64
windows-aa64 windows-x64
macos-aa64 macos-x64
-freestanding-aa64 freestanding-x64 freestanding-rv64 freestanding-rv32
+freestanding-aa64 freestanding-x64 freestanding-rv64 freestanding-rv32 freestanding-arm32
"
die() { printf 'hosted: %s\n' "$*" >&2; exit 1; }
@@ -90,6 +90,7 @@ parse_target() {
x64|x86_64|amd64) T_ARCH=x64 ;;
rv64|riscv64) T_ARCH=rv64 ;;
rv32|riscv32) T_ARCH=rv32 ;;
+ arm32|arm|armv7m|armv7em|cortex-m3|cortex-m4|cortex-m7) T_ARCH=arm32 ;;
*) die "bad freestanding arch '$rest' in '$t'" ;;
esac ;;
*) die "unknown os in target '$t'" ;;
@@ -128,6 +129,7 @@ triple_of() {
x64) echo x86_64-none-elf ;;
rv64) echo riscv64-none-elf ;;
rv32) echo riscv32-none-elf ;;
+ arm32) echo arm-none-eabi ;;
esac ;;
esac
}
@@ -153,7 +155,7 @@ canon_token() {
local ea
case "$T_ARCH" in
aarch64) ea=aa64 ;; x64|amd64) ea=x64 ;; rv64|riscv64) ea=rv64 ;;
- rv32) ea=rv32 ;; *) ea="$T_ARCH" ;;
+ rv32) ea=rv32 ;; arm32) ea=arm32 ;; *) ea="$T_ARCH" ;;
esac
case "$T_OS" in
linux) printf 'linux-%s-%s' "$T_LIBC" "$ea" ;;
diff --git a/src/abi/abi_aapcs32.c b/src/abi/abi_aapcs32.c
@@ -0,0 +1,133 @@
+/* AAPCS32 / ARM EABI (AArch32, ELF, soft-float v1).
+ *
+ * Structurally the rv32 ILP32 classifier with the float-ABI pinned to soft:
+ * void -> IGNORE
+ * integer/ptr <= 4 -> DIRECT, one INT part (r0..r3 for args; r0 for return)
+ * i64 / double (8B) -> DIRECT, two 4-byte INT parts (the r0:r1 / r2:r3 pair).
+ * The 8-byte even-register / 8-byte-stack alignment that
+ * AAPCS requires is surfaced to the native backend via
+ * ABIArgPart.align; this layer just declares two lanes.
+ * aggregate <= 16B -> DIRECT, up to four word parts (r0..r3)
+ * aggregate > 16B -> INDIRECT (sret pointer in r0 / byval)
+ *
+ * The sret pointer is passed in r0 (the first integer arg register), consuming
+ * that slot, so sret_consumes_int_arg = 1 (like RISC-V, unlike AArch64's x8).
+ *
+ * Hard-float (FPv4-SP) + HFAs + __int128 are out of scope for v1. */
+#include <string.h>
+
+#include "abi/abi_internal.h"
+#include "cg/type.h"
+#include "core/arena.h"
+#include "core/core.h"
+
+#define ARM32_GPR_BYTES 4u /* r0..r3 are 32-bit */
+#define ARM32_AGG_GPR_BYTES 16u /* aggregates <= 4 words pass in registers */
+
+static void classify_scalar(TargetABI* a, KitCgTypeId t, ABIArgInfo* out) {
+ ABITypeInfo ti = abi_cg_type_info(a, t);
+ /* i64 / soft double -> even/odd GPR pair (r0:r1 or r2:r3). */
+ if (ti.size == 2u * ARM32_GPR_BYTES &&
+ (ti.scalar_kind == ABI_SC_INT || ti.scalar_kind == ABI_SC_FLOAT)) {
+ ABIArgPart* parts = arena_array(a->c->tu, ABIArgPart, 2);
+ memset(parts, 0, sizeof(ABIArgPart) * 2);
+ parts[0].cls = ABI_CLASS_INT;
+ parts[0].loc = ABI_LOC_REG;
+ parts[0].size = ARM32_GPR_BYTES;
+ parts[0].align = ARM32_GPR_BYTES;
+ parts[0].src_offset = 0;
+ parts[1].cls = ABI_CLASS_INT;
+ parts[1].loc = ABI_LOC_REG;
+ parts[1].size = ARM32_GPR_BYTES;
+ parts[1].align = ARM32_GPR_BYTES;
+ parts[1].src_offset = ARM32_GPR_BYTES;
+ out->kind = ABI_ARG_DIRECT;
+ out->flags = ABI_AF_NONE;
+ out->parts = parts;
+ out->nparts = 2;
+ out->indirect_align = 0;
+ return;
+ }
+ /* int / pointer / float<=4 -> one INT part (soft-float: floats in core regs). */
+ abi_classify_scalar_reg_part(a, out, ti, /*is_fp=*/0);
+}
+
+static u32 arm32_scalar_split_lane_size(TargetABI* a, KitCgTypeId t) {
+ ABITypeInfo ti = abi_cg_type_info(a, t);
+ if (ti.size == 2u * ARM32_GPR_BYTES &&
+ (ti.scalar_kind == ABI_SC_INT || ti.scalar_kind == ABI_SC_FLOAT))
+ return ARM32_GPR_BYTES; /* 4 -> drives the arch-neutral wide8 path */
+ return 0;
+}
+
+static void classify_aggregate(TargetABI* a, KitCgTypeId t, ABIArgInfo* out,
+ int is_return) {
+ ABITypeInfo ti = abi_cg_type_info(a, t);
+ if (ti.size == 0) {
+ abi_classify_void(out);
+ return;
+ }
+ if (ti.size <= ARM32_AGG_GPR_BYTES) {
+ u32 nparts = (ti.size + ARM32_GPR_BYTES - 1u) / ARM32_GPR_BYTES;
+ ABIArgPart* parts = arena_array(a->c->tu, ABIArgPart, nparts);
+ u32 off = 0;
+ u32 i;
+ memset(parts, 0, sizeof(ABIArgPart) * nparts);
+ for (i = 0; i < nparts; ++i) {
+ u32 chunk =
+ (ti.size - off > ARM32_GPR_BYTES) ? ARM32_GPR_BYTES : (ti.size - off);
+ parts[i].cls = ABI_CLASS_INT;
+ parts[i].loc = ABI_LOC_REG;
+ parts[i].size = chunk;
+ parts[i].align = ARM32_GPR_BYTES;
+ parts[i].src_offset = off;
+ off += chunk;
+ }
+ out->kind = ABI_ARG_DIRECT;
+ out->flags = ABI_AF_NONE;
+ out->parts = parts;
+ out->nparts = (u16)nparts;
+ out->indirect_align = 0;
+ } else {
+ out->kind = ABI_ARG_INDIRECT;
+ out->flags = is_return ? ABI_AF_SRET : ABI_AF_BYVAL;
+ out->indirect_align = ti.align ? ti.align : ARM32_GPR_BYTES;
+ out->parts = NULL;
+ out->nparts = 0;
+ }
+}
+
+static void classify_one(TargetABI* a, KitCgTypeId t, ABIArgInfo* out,
+ int is_return) {
+ const CgType* ty = cg_type_get(a->c, t);
+ if (!ty || ty->kind == KIT_CG_TYPE_VOID) {
+ abi_classify_void(out);
+ return;
+ }
+ switch (ty->kind) {
+ case KIT_CG_TYPE_RECORD:
+ classify_aggregate(a, t, out, is_return);
+ return;
+ default:
+ classify_scalar(a, t, out);
+ return;
+ }
+}
+
+static ABIFuncInfo* arm32_compute_func_info(TargetABI* a, KitCgTypeId fn) {
+ return abi_compute_func_info_generic(a, fn, classify_one,
+ /*sret_consumes_int_arg=*/1);
+}
+
+const ABIVtable aapcs32_vtable = {
+ .compute_func_info = arm32_compute_func_info,
+ .scalar_split_lane_size = arm32_scalar_split_lane_size,
+ /* AAPCS32 va_list is a plain 4-byte pointer; the variadic register-save
+ * area is r0..r3 spilled contiguously. No FP save area (soft-float). */
+ .va_list_info = {4, 4, ABI_SC_PTR, 0, 0, 0},
+ .va_list_layout = {.kind = ABI_VA_LIST_POINTER,
+ .gp_reg_count = 4,
+ .fp_reg_count = 0,
+ .gp_slot_size = 4,
+ .fp_slot_size = 0},
+};
diff --git a/src/abi/abi_internal.h b/src/abi/abi_internal.h
@@ -35,6 +35,7 @@ extern const ABIVtable aapcs64_vtable;
extern const ABIVtable sysv_x64_vtable;
extern const ABIVtable rv64_vtable;
extern const ABIVtable rv32_vtable;
+extern const ABIVtable aapcs32_vtable;
extern const ABIVtable wasm32_vtable;
/* Apple Darwin variants — selected when (arch, os) matches. See
* abi.c::select_vtable. */
diff --git a/src/abi/registry.c b/src/abi/registry.c
@@ -17,6 +17,8 @@ typedef struct ABIImpl {
(KIT_ARCH_RV64_ENABLED || KIT_ARCH_C_TARGET_ENABLED)
#define KIT_ABI_RV32_ENABLED \
(KIT_ARCH_RV32_ENABLED || KIT_ARCH_C_TARGET_ENABLED)
+#define KIT_ABI_ARM32_ENABLED \
+ (KIT_ARCH_ARM32_ENABLED || KIT_ARCH_C_TARGET_ENABLED)
static const ABIImpl abi_impls[] = {
#if KIT_ABI_AA64_ENABLED && KIT_OBJ_ELF_ENABLED
@@ -43,11 +45,14 @@ static const ABIImpl abi_impls[] = {
#if KIT_ABI_RV32_ENABLED && KIT_OBJ_ELF_ENABLED
{KIT_ARCH_RV32, KIT_OBJ_ELF, &rv32_vtable},
#endif
+#if KIT_ABI_ARM32_ENABLED && KIT_OBJ_ELF_ENABLED
+ {KIT_ARCH_ARM_32, KIT_OBJ_ELF, &aapcs32_vtable},
+#endif
#if KIT_ARCH_WASM_ENABLED && KIT_OBJ_WASM_ENABLED
{KIT_ARCH_WASM, KIT_OBJ_WASM, &wasm32_vtable},
#endif
#if !KIT_ABI_AA64_ENABLED && !KIT_ABI_X64_ENABLED && !KIT_ABI_RV64_ENABLED && \
- !KIT_ABI_RV32_ENABLED && !KIT_ARCH_WASM_ENABLED
+ !KIT_ABI_RV32_ENABLED && !KIT_ABI_ARM32_ENABLED && !KIT_ARCH_WASM_ENABLED
{KIT_ARCH_WASM, KIT_OBJ_WASM, NULL},
#endif
};
@@ -65,3 +70,4 @@ const ABIVtable* abi_vtable_lookup(KitArchKind arch, KitObjFmt obj) {
#undef KIT_ABI_X64_ENABLED
#undef KIT_ABI_RV64_ENABLED
#undef KIT_ABI_RV32_ENABLED
+#undef KIT_ABI_ARM32_ENABLED
diff --git a/src/arch/arm32/arch.c b/src/arch/arm32/arch.c
@@ -0,0 +1,269 @@
+/* arch_impl_arm32: the ARM32 (ARMv7-M Thumb-2, arm-none-eabi) ArchImpl.
+ *
+ * The backend is built on the shared NativeDirectTarget/MCEmitter substrate;
+ * arch.c wires the factories, register metadata, CFI defaults, predefined
+ * macros, target features, and the intra-function Thumb-2 branch label-fixup
+ * patcher. Soft-float v1; ARMv7-M baseline (Cortex-M3). See doc/plan/ARM32.md. */
+#include "arch/arch.h"
+
+#include <string.h>
+
+#include "arch/arm32/arm32.h"
+#include "arch/arm32/regs.h"
+#include "arch/arm32/variant.h"
+#include "cg/native_direct_target.h"
+#include "core/bytes.h"
+#include "core/core.h"
+#include "link/link_arch.h"
+#include "obj/obj.h"
+
+extern const LinkArchDesc link_arch_arm32;
+extern const ArchDbgOps arm32_dbg_ops;
+extern const ArchDwarfOps arm32_dwarf_ops;
+extern ArchAsm* arm32_arch_asm_new(Compiler*);
+extern ArchDisasm* arm32_disasm_new(Compiler*);
+
+static int arm32_register_at_public(uint32_t idx, KitArchReg* out) {
+ return arch_register_at_public(idx, out, arm32_register_iter_get);
+}
+
+static SrcLoc arm32_no_loc(void) {
+ SrcLoc l = {0, 0, 0};
+ return l;
+}
+
+/* Read/write a 32-bit Thumb-2 instruction stored as two little-endian
+ * half-words (high half-word first): the returned value has hw1 in [31:16],
+ * hw2 in [15:0]. */
+static u32 arm_rd_t32(const u8* p) {
+ return ((u32)rd_u16_le(p) << 16) | (u32)rd_u16_le(p + 2);
+}
+static void arm_wr_t32(u8* p, u32 instr) {
+ wr_u16_le(p, (u16)(instr >> 16));
+ wr_u16_le(p + 2, (u16)(instr & 0xffffu));
+}
+
+/* Patch an intra-function Thumb-2 branch recorded by the MCEmitter label-fixup
+ * path: B.W (T4, R_ARM_THM_JUMP24, +-16MiB) and B<cond>.W (T3,
+ * R_ARM_THM_JUMP19, +-1MiB). fx->disp is the site-relative byte displacement
+ * (target - branch_start); the Thumb pipeline reads PC as branch_start + 4, so
+ * the encoded offset is fx->disp - 4. The opcode/cond bits the emitter wrote
+ * are preserved; only the split immediate fields are filled in. */
+static int arm32_apply_label_fixup(Compiler* c, const ArchLabelFixup* fx) {
+ const Section* s;
+ u8 cur[4];
+ u32 instr;
+ i64 d;
+
+ if (!fx) return 1;
+ if (fx->width != 4) return 1;
+ s = obj_section_get(fx->obj, fx->sec_id);
+ if (!s) return 0;
+ buf_read(&s->bytes, fx->offset, cur, 4);
+ instr = arm_rd_t32(cur);
+ d = (i64)fx->disp - 4; /* Thumb PC bias */
+
+ switch (fx->kind) {
+ case R_ARM_THM_JUMP24: {
+ u32 sbit, i1, i2, j1, j2, imm10, imm11;
+ if (d < -(i64)(1 << 24) || d >= (i64)(1 << 24))
+ compiler_panic(c, arm32_no_loc(), "arm32: B.W out of range (+-16MiB)");
+ sbit = (u32)((d >> 24) & 1u);
+ i1 = (u32)((d >> 23) & 1u);
+ i2 = (u32)((d >> 22) & 1u);
+ imm10 = (u32)((d >> 12) & 0x3ffu);
+ imm11 = (u32)((d >> 1) & 0x7ffu);
+ j1 = (~(i1 ^ sbit)) & 1u;
+ j2 = (~(i2 ^ sbit)) & 1u;
+ instr &= 0xf800d000u; /* keep opcode (11110), hw2[15:14]=10, hw2[12]=1 */
+ instr |= (sbit << 26) | (imm10 << 16);
+ instr |= (j1 << 13) | (j2 << 11) | imm11;
+ break;
+ }
+ case R_ARM_THM_JUMP19: {
+ /* B<cond>.W T3: imm32 = SignExtend(S:J2:J1:imm6:imm11:0), no XOR-with-S. */
+ u32 sbit, j1, j2, imm6, imm11;
+ if (d < -(i64)(1 << 20) || d >= (i64)(1 << 20))
+ compiler_panic(c, arm32_no_loc(), "arm32: B.cond.W out of range (+-1MiB)");
+ sbit = (u32)((d >> 20) & 1u);
+ j2 = (u32)((d >> 19) & 1u);
+ j1 = (u32)((d >> 18) & 1u);
+ imm6 = (u32)((d >> 12) & 0x3fu);
+ imm11 = (u32)((d >> 1) & 0x7ffu);
+ /* clear S(26), imm6(21:16), J1(13), J2(11), imm11(10:0); keep opcode,
+ * cond(25:22), hw2[15:14]=10, hw2[12]=0. */
+ instr &= ~((1u << 26) | (0x3fu << 16) | (1u << 13) | (1u << 11) | 0x7ffu);
+ instr |= (sbit << 26) | (imm6 << 16) | (j1 << 13) | (j2 << 11) | imm11;
+ break;
+ }
+ default:
+ return 1;
+ }
+
+ arm_wr_t32(cur, instr);
+ obj_patch(fx->obj, fx->sec_id, fx->offset, cur, 4);
+ return 0;
+}
+
+/* Mirrors `clang --target=arm-none-eabi -march=armv7-m -mfloat-abi=soft -E -dM`
+ * for the in-scope ARMv7-M (Cortex-M3) soft-float profile. Arch-identity +
+ * feature macros only; the ILP32 size/type macros are emitted by the
+ * preprocessor from target.ptr_size=4. Static (ptr,count) table consumed
+ * without a Target, so it reflects this one default profile (soft-float
+ * Cortex-M3); -mcpu/-march cannot retune it (cf. rv32's identical limitation).
+ * Soft-float codegen correctness is driven by c->target.float_abi. */
+static const KitPredefinedMacro arm32_predefined_macros[] = {
+ {KIT_SLICE_LIT("__arm__"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ARMEL__"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__thumb__"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__thumb2__"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__THUMBEL__"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ARM_ARCH"), KIT_SLICE_LIT("7")},
+ {KIT_SLICE_LIT("__ARM_ARCH_7M__"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ARM_ARCH_PROFILE"), KIT_SLICE_LIT("'M'")},
+ {KIT_SLICE_LIT("__ARM_ARCH_ISA_THUMB"), KIT_SLICE_LIT("2")},
+ {KIT_SLICE_LIT("__ARM_ARCH_ISA_ARM"), KIT_SLICE_LIT("0")},
+ {KIT_SLICE_LIT("__ARM_32BIT_STATE"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ARM_EABI__"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ARM_PCS"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__SOFTFP__"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ARM_FP"), KIT_SLICE_LIT("0")},
+ {KIT_SLICE_LIT("__ARM_FEATURE_CLZ"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ARM_FEATURE_IDIV"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ARM_FEATURE_UNALIGNED"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ARM_FEATURE_QBIT"), KIT_SLICE_LIT("1")},
+ {KIT_SLICE_LIT("__ORDER_LITTLE_ENDIAN__"), KIT_SLICE_LIT("1234")},
+ {KIT_SLICE_LIT("__ORDER_BIG_ENDIAN__"), KIT_SLICE_LIT("4321")},
+ {KIT_SLICE_LIT("__BYTE_ORDER__"), KIT_SLICE_LIT("__ORDER_LITTLE_ENDIAN__")},
+ {KIT_SLICE_LIT("__LITTLE_ENDIAN__"), KIT_SLICE_LIT("1")},
+};
+
+static const ArchTargetFeature arm32_target_features[] = {
+ {"dsp"}, /* ARMv7E-M saturating/SIMD (follow-on) */
+ {"vfp"}, /* hard-float FPv4-SP (follow-on) */
+};
+
+static void arm32_target_feature_defaults(const Target* t, u64* words,
+ u32 nwords) {
+ (void)t;
+ (void)words;
+ (void)nwords; /* baseline ARMv7-M soft: no features default-on. */
+}
+
+static KitStatus arm32_target_feature_apply_isa(const Target* t, KitSlice isa,
+ u64* words, u32 nwords) {
+ (void)t;
+ (void)words;
+ (void)nwords;
+ if (kit_slice_eq_cstr(isa, "armv7-m") || kit_slice_eq_cstr(isa, "armv7e-m") ||
+ kit_slice_eq_cstr(isa, "thumbv7m") || kit_slice_eq_cstr(isa, "thumbv7em"))
+ return KIT_OK;
+ return KIT_UNSUPPORTED;
+}
+
+static int arm32_supports_call_conv(const Compiler* c, KitCgCallConv cc) {
+ (void)c;
+ return cc == KIT_CG_CC_TARGET_C;
+}
+
+/* Kept in lockstep with arm32_intrinsic in native.c: returning 1 for an
+ * intrinsic this backend does not lower panics at emit. No default: case so
+ * -Wswitch enforces the twin-sync invariant. Phase 1 lowers only TRAP. */
+static int arm32_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) {
+ (void)c;
+ switch (intrin) {
+ case KIT_CG_INTRIN_TRAP:
+ return 1;
+ case KIT_CG_INTRIN_CLZ:
+ case KIT_CG_INTRIN_CTZ:
+ case KIT_CG_INTRIN_POPCOUNT:
+ case KIT_CG_INTRIN_BSWAP:
+ case KIT_CG_INTRIN_SADD_OVERFLOW:
+ case KIT_CG_INTRIN_UADD_OVERFLOW:
+ case KIT_CG_INTRIN_SSUB_OVERFLOW:
+ case KIT_CG_INTRIN_USUB_OVERFLOW:
+ case KIT_CG_INTRIN_SMUL_OVERFLOW:
+ case KIT_CG_INTRIN_UMUL_OVERFLOW:
+ case KIT_CG_INTRIN_PREFETCH:
+ case KIT_CG_INTRIN_EXPECT:
+ case KIT_CG_INTRIN_ASSUME_ALIGNED:
+ case KIT_CG_INTRIN_CPU_NOP:
+ case KIT_CG_INTRIN_CPU_YIELD:
+ case KIT_CG_INTRIN_ISB:
+ case KIT_CG_INTRIN_DMB:
+ case KIT_CG_INTRIN_DSB:
+ case KIT_CG_INTRIN_WFI:
+ case KIT_CG_INTRIN_WFE:
+ case KIT_CG_INTRIN_SEV:
+ case KIT_CG_INTRIN_FRAME_ADDRESS:
+ case KIT_CG_INTRIN_RETURN_ADDRESS:
+ case KIT_CG_INTRIN_READCYCLECOUNTER:
+ case KIT_CG_INTRIN_SYSCALL:
+ case KIT_CG_INTRIN_SETJMP:
+ case KIT_CG_INTRIN_LONGJMP:
+ case KIT_CG_INTRIN_FMA:
+ case KIT_CG_INTRIN_IRQ_SAVE:
+ case KIT_CG_INTRIN_IRQ_RESTORE:
+ case KIT_CG_INTRIN_IRQ_DISABLE:
+ case KIT_CG_INTRIN_IRQ_ENABLE:
+ case KIT_CG_INTRIN_DCACHE_CLEAN:
+ case KIT_CG_INTRIN_DCACHE_INVALIDATE:
+ case KIT_CG_INTRIN_DCACHE_CLEAN_INVALIDATE:
+ case KIT_CG_INTRIN_ICACHE_INVALIDATE:
+ case KIT_CG_INTRIN_CORO_SWITCH:
+ return 0;
+ }
+ return 0;
+}
+
+static CgTarget* arm32_backend_make(Compiler* c, ObjBuilder* o,
+ const KitCodeOptions* opts) {
+ return native_direct_backend_make(c, o, opts, arm32_native_target_new,
+ arm32_native_direct_ops());
+}
+
+static CgTarget* arm32_semantic_target_new(Compiler* c, ObjBuilder* o,
+ MCEmitter* mc) {
+ return native_direct_semantic_target_new(c, o, mc, arm32_native_target_new,
+ arm32_native_direct_ops());
+}
+
+const ArchImpl arch_impl_arm32 = {
+ .backend = {.name = "arm32", .make = arm32_backend_make},
+ .kind = KIT_ARCH_ARM_32,
+ .name = "arm32",
+ .cgtarget_new = arm32_semantic_target_new,
+ .asm_new = arm32_arch_asm_new,
+ .disasm_new = arm32_disasm_new,
+ .apply_label_fixup = arm32_apply_label_fixup,
+ /* .decode / .emu omitted (NULL) for Phase 1 — like aa64. */
+ .link = &link_arch_arm32,
+ .dwarf = &arm32_dwarf_ops,
+ .dbg = &arm32_dbg_ops,
+ .asm_ops = NULL, /* numeric operand printing until the asm frontend lands */
+ .predefined_macros = arm32_predefined_macros,
+ .npredefined_macros =
+ (u32)(sizeof arm32_predefined_macros / sizeof arm32_predefined_macros[0]),
+ .target_features = arm32_target_features,
+ .ntarget_features =
+ (u32)(sizeof arm32_target_features / sizeof arm32_target_features[0]),
+ .target_feature_defaults = arm32_target_feature_defaults,
+ .target_feature_apply_isa = arm32_target_feature_apply_isa,
+ .register_name = arm32_register_name,
+ .register_index = arm32_register_index,
+ .register_count = arm32_register_iter_size,
+ .register_at = arm32_register_at_public,
+ /* AAPCS32 / Thumb-2: LR = r14, SP = r13 (legacy AArch32 DWARF numbering).
+ * 2-byte minimum insn width -> code_align 2; word stack stride ->
+ * data_align -4. CFA = sp at entry. */
+ .cfi_return_addr_reg = 14u,
+ .cfi_code_align_factor = 2,
+ .cfi_data_align_factor = -4,
+ .cfi_cfa_init_reg = 13u,
+ .cfi_cfa_init_offset = 0,
+ .backend_features = KIT_CG_BACKEND_STRICT_ALIGNMENT,
+ .atomic_lock_free_max = 4u, /* no LDREXD/STREXD on M-profile -> 8B spinlock */
+ .supports_call_conv = arm32_supports_call_conv,
+ .supports_intrinsic = arm32_supports_intrinsic,
+ /* .resolve_float_abi omitted (NULL) for Phase 1 (soft-float only). */
+};
diff --git a/src/arch/arm32/arm32.h b/src/arch/arm32/arm32.h
@@ -0,0 +1,11 @@
+#ifndef KIT_ARCH_ARM32_H
+#define KIT_ARCH_ARM32_H
+
+#include "arch/native_target.h"
+#include "cg/native_direct_target.h"
+
+/* Backend entry points consumed by arch.c (mirrors riscv/rv64.h). */
+NativeTarget* arm32_native_target_new(Compiler* c, ObjBuilder* o, MCEmitter* mc);
+const NativeOps* arm32_native_direct_ops(void);
+
+#endif
diff --git a/src/arch/arm32/asm.c b/src/arch/arm32/asm.c
@@ -0,0 +1,14 @@
+/* ARM32 (Thumb-2) textual-assembler frontend.
+ *
+ * Phase 1 (the walking skeleton) ships no standalone assembler: the qemu
+ * smoke lane's reset stub is clang-assembled, and the smoke C set uses no
+ * inline asm. The `.syntax unified` operand parser + IT-block syntax +
+ * byte-golden lane are a Phase-2 deliverable (ARM32.md "Standalone assembler
+ * frontend" / "Inline asm"). arm32_arch_asm_new yields no assembler until
+ * then; the ArchImpl leaves .asm_ops NULL (numeric operand printing). */
+#include "arch/arch.h"
+
+ArchAsm* arm32_arch_asm_new(Compiler* c) {
+ (void)c;
+ return NULL;
+}
diff --git a/src/arch/arm32/dbg.c b/src/arch/arm32/dbg.c
@@ -0,0 +1,28 @@
+/* ARM32 (Thumb-2) debugger ops. Phase 1 provides the breakpoint primitive
+ * (BKPT #0 = 0xBE00, a 16-bit Thumb instruction) and the instruction-length
+ * bounds; displaced-step and decode are host-gated follow-ons (no AArch32
+ * execution on the arm64 dev host). */
+#include <string.h>
+
+#include "arch/arch.h"
+
+static KitStatus arm32_dbg_breakpoint_patch(u8* out, u32 cap, u32* len_out) {
+ /* BKPT #0, Thumb T1 encoding 0xBE00, little-endian half-word. */
+ static const u8 bkpt[2] = {0x00u, 0xBEu};
+ if (!out || !len_out) return KIT_INVALID;
+ if (cap < sizeof bkpt) return KIT_INVALID;
+ memcpy(out, bkpt, sizeof bkpt);
+ *len_out = (u32)sizeof bkpt;
+ return KIT_OK;
+}
+
+static u64 arm32_dbg_breakpoint_addr_from_fault_pc(u64 fault_pc) {
+ return fault_pc;
+}
+
+const ArchDbgOps arm32_dbg_ops = {
+ .min_insn_len = 2u,
+ .max_insn_len = 4u,
+ .breakpoint_patch = arm32_dbg_breakpoint_patch,
+ .breakpoint_addr_from_fault_pc = arm32_dbg_breakpoint_addr_from_fault_pc,
+};
diff --git a/src/arch/arm32/disasm.c b/src/arch/arm32/disasm.c
@@ -0,0 +1,13 @@
+/* ARM32 (Thumb-2) disassembler.
+ *
+ * Phase 1 (the walking skeleton) ships no descriptor-driven decode — the
+ * codegen gate is run-correctness under qemu, not byte-golden disassembly.
+ * Table-driven decode (the 16/32-bit Thumb-2 families + IT-state tracking)
+ * is a Phase-2 op-group deliverable; until then arm32_disasm_new yields no
+ * disassembler (objdump/disas report unsupported for arm32). */
+#include "arch/arch.h"
+
+ArchDisasm* arm32_disasm_new(Compiler* c) {
+ (void)c;
+ return NULL;
+}
diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h
@@ -0,0 +1,239 @@
+/* ARM32 Thumb-2 instruction encoders (header-only, inline).
+ *
+ * Phase 1 (the walking skeleton) needs only the encoders the -O0 native
+ * backend emits for the smoke set: MOVW/MOVT + modified-immediate constants,
+ * the data-processing reg/imm families, multiply/divide, shifts, extends,
+ * compare, branches, BL, push/pop, and LDR/STR. A descriptor-driven table +
+ * disassembler is a Phase-2 deliverable.
+ *
+ * Every 32-bit Thumb-2 instruction is returned as a single u32 with the FIRST
+ * half-word (hw1) in bits [31:16] and the SECOND (hw2) in [15:0]; native.c's
+ * arm_emit_t32 writes them as two little-endian half-words (hw1 first). 16-bit
+ * instructions are returned as a u16 and emitted via arm_emit_t16. */
+#ifndef KIT_ARCH_ARM32_ISA_H
+#define KIT_ARCH_ARM32_ISA_H
+
+#include "core/core.h"
+
+/* Core register numbers (AAPCS roles). */
+enum {
+ ARM_R0 = 0,
+ ARM_R1 = 1,
+ ARM_R2 = 2,
+ ARM_R3 = 3,
+ ARM_R4 = 4,
+ ARM_R12 = 12,
+ ARM_IP = 12, /* intra-procedure scratch */
+ ARM_SP = 13,
+ ARM_LR = 14,
+ ARM_PC = 15,
+};
+
+/* Thumb condition codes (for B<cond> / IT). */
+enum {
+ ARM_CC_EQ = 0x0,
+ ARM_CC_NE = 0x1,
+ ARM_CC_CS = 0x2, /* HS, unsigned >= */
+ ARM_CC_CC = 0x3, /* LO, unsigned < */
+ ARM_CC_MI = 0x4,
+ ARM_CC_PL = 0x5,
+ ARM_CC_VS = 0x6,
+ ARM_CC_VC = 0x7,
+ ARM_CC_HI = 0x8, /* unsigned > */
+ ARM_CC_LS = 0x9, /* unsigned <= */
+ ARM_CC_GE = 0xa,
+ ARM_CC_LT = 0xb,
+ ARM_CC_GT = 0xc,
+ ARM_CC_LE = 0xd,
+ ARM_CC_AL = 0xe,
+};
+
+static inline u32 arm_t32(u32 hw1, u32 hw2) {
+ return ((hw1 & 0xffffu) << 16) | (hw2 & 0xffffu);
+}
+
+/* --------- ThumbExpandImm (rotated-8-bit modified immediate) --------- */
+/* Encode `v` as a 12-bit i:imm3:imm8 modified immediate. Returns 1 + sets
+ * *out12 on success, 0 if `v` is not representable (caller materializes it). */
+static inline int thumb_expand_imm_encode(u32 v, u32* out12) {
+ u32 b0 = v & 0xffu, b1 = (v >> 8) & 0xffu, b2 = (v >> 16) & 0xffu,
+ b3 = (v >> 24) & 0xffu;
+ u32 rot;
+ if (b1 == 0 && b2 == 0 && b3 == 0) {
+ *out12 = b0;
+ return 1; /* 0x000000XY */
+ }
+ if (b0 == b2 && b0 != 0 && b1 == 0 && b3 == 0) {
+ *out12 = 0x100u | b0;
+ return 1; /* 0x00XY00XY */
+ }
+ if (b1 == b3 && b1 != 0 && b0 == 0 && b2 == 0) {
+ *out12 = 0x200u | b1;
+ return 1; /* 0xXY00XY00 */
+ }
+ if (b0 == b1 && b1 == b2 && b2 == b3 && b0 != 0) {
+ *out12 = 0x300u | b0;
+ return 1; /* 0xXYXYXYXY */
+ }
+ for (rot = 8; rot < 32; ++rot) {
+ u32 base = (v << rot) | (v >> (32u - rot)); /* rol(v, rot) == base8 */
+ if (base <= 0xffu && (base & 0x80u)) {
+ *out12 = (rot << 7) | (base & 0x7fu);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+/* --------- constant / address materialization --------- */
+static inline u32 arm_movw(u32 rd, u32 imm16) {
+ u32 i = (imm16 >> 11) & 1u, imm4 = (imm16 >> 12) & 0xfu, imm3 = (imm16 >> 8) & 7u,
+ imm8 = imm16 & 0xffu;
+ return arm_t32(0xf240u | (i << 10) | imm4, (imm3 << 12) | (rd << 8) | imm8);
+}
+static inline u32 arm_movt(u32 rd, u32 imm16) {
+ u32 i = (imm16 >> 11) & 1u, imm4 = (imm16 >> 12) & 0xfu, imm3 = (imm16 >> 8) & 7u,
+ imm8 = imm16 & 0xffu;
+ return arm_t32(0xf2c0u | (i << 10) | imm4, (imm3 << 12) | (rd << 8) | imm8);
+}
+/* MOV.W rd, #modimm (out12 from thumb_expand_imm_encode). */
+static inline u32 arm_mov_imm(u32 rd, u32 out12) {
+ u32 i = (out12 >> 11) & 1u, imm3 = (out12 >> 8) & 7u, imm8 = out12 & 0xffu;
+ return arm_t32(0xf04fu | (i << 10), (imm3 << 12) | (rd << 8) | imm8);
+}
+/* MVN.W rd, #modimm. */
+static inline u32 arm_mvn_imm(u32 rd, u32 out12) {
+ u32 i = (out12 >> 11) & 1u, imm3 = (out12 >> 8) & 7u, imm8 = out12 & 0xffu;
+ return arm_t32(0xf06fu | (i << 10), (imm3 << 12) | (rd << 8) | imm8);
+}
+
+/* --------- data processing (register, no shift) --------- */
+/* op4: AND=0,BIC=1,ORR=2,ORN=3,EOR=4,ADD=8,ADC=10,SBC=11,SUB=13,RSB=14. */
+static inline u32 arm_dp_reg(u32 op4, u32 setflags, u32 rd, u32 rn, u32 rm) {
+ return arm_t32(0xea00u | (op4 << 5) | (setflags << 4) | rn,
+ (rd << 8) | rm);
+}
+static inline u32 arm_add_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(8u, 0u, rd, rn, rm); }
+static inline u32 arm_adds_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(8u, 1u, rd, rn, rm); }
+static inline u32 arm_sub_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(13u, 0u, rd, rn, rm); }
+static inline u32 arm_and_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(0u, 0u, rd, rn, rm); }
+static inline u32 arm_orr_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(2u, 0u, rd, rn, rm); }
+static inline u32 arm_eor_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(4u, 0u, rd, rn, rm); }
+/* MOV.W rd, rm (ORR rd, 1111, rm). */
+static inline u32 arm_mov_reg(u32 rd, u32 rm) { return arm_dp_reg(2u, 0u, rd, 0xfu, rm); }
+/* MVN.W rd, rm (ORN rd, 1111, rm). */
+static inline u32 arm_mvn_reg(u32 rd, u32 rm) { return arm_dp_reg(3u, 0u, rd, 0xfu, rm); }
+/* CMP.W rn, rm (SUB S=1, Rd=1111). */
+static inline u32 arm_cmp_reg(u32 rn, u32 rm) { return arm_dp_reg(13u, 1u, 0xfu, rn, rm); }
+
+/* --------- data processing (modified immediate) --------- */
+static inline u32 arm_dp_imm(u32 op4, u32 setflags, u32 rd, u32 rn, u32 out12) {
+ u32 i = (out12 >> 11) & 1u, imm3 = (out12 >> 8) & 7u, imm8 = out12 & 0xffu;
+ return arm_t32(0xf000u | (i << 10) | (op4 << 5) | (setflags << 4) | rn,
+ (imm3 << 12) | (rd << 8) | imm8);
+}
+static inline u32 arm_add_imm12(u32 rd, u32 rn, u32 rn_is_imm12) {
+ /* ADDW rd, rn, #imm12 (raw 12-bit, no flags). rn_is_imm12 is the imm12. */
+ u32 imm12 = rn_is_imm12 & 0xfffu;
+ u32 i = (imm12 >> 11) & 1u, imm3 = (imm12 >> 8) & 7u, imm8 = imm12 & 0xffu;
+ return arm_t32(0xf200u | (i << 10) | rn, (imm3 << 12) | (rd << 8) | imm8);
+}
+static inline u32 arm_sub_imm12(u32 rd, u32 rn, u32 imm12) {
+ /* SUBW rd, rn, #imm12 (raw 12-bit, no flags). */
+ u32 v = imm12 & 0xfffu;
+ u32 i = (v >> 11) & 1u, imm3 = (v >> 8) & 7u, imm8 = v & 0xffu;
+ return arm_t32(0xf2a0u | (i << 10) | rn, (imm3 << 12) | (rd << 8) | imm8);
+}
+/* CMP.W rn, #modimm (SUB S=1, Rd=1111). */
+static inline u32 arm_cmp_imm(u32 rn, u32 out12) { return arm_dp_imm(13u, 1u, 0xfu, rn, out12); }
+
+/* --------- shifts --------- */
+/* MOV.W rd, rm, <type> #sh : type LSL=0,LSR=1,ASR=2,ROR=3 (immediate). */
+static inline u32 arm_shift_imm(u32 type, u32 rd, u32 rm, u32 sh) {
+ u32 imm3 = (sh >> 2) & 7u, imm2 = sh & 3u;
+ return arm_t32(0xea4fu, (imm3 << 12) | (rd << 8) | (imm2 << 6) | (type << 4) | rm);
+}
+/* LSL/LSR/ASR (register): Rd = Rn shifted by Rm. */
+static inline u32 arm_shift_reg(u32 type, u32 rd, u32 rn, u32 rm) {
+ u32 hw1 = 0xfa00u | (type << 5) | rn; /* LSL=0xFA00, LSR=0xFA20, ASR=0xFA40 */
+ return arm_t32(hw1, 0xf000u | (rd << 8) | rm);
+}
+
+/* --------- multiply / divide --------- */
+static inline u32 arm_mul(u32 rd, u32 rn, u32 rm) {
+ return arm_t32(0xfb00u | rn, 0xf000u | (rd << 8) | rm);
+}
+static inline u32 arm_mls(u32 rd, u32 rn, u32 rm, u32 ra) {
+ return arm_t32(0xfb00u | rn, (ra << 12) | (rd << 8) | 0x10u | rm);
+}
+static inline u32 arm_sdiv(u32 rd, u32 rn, u32 rm) {
+ return arm_t32(0xfb90u | rn, 0xf0f0u | (rd << 8) | rm);
+}
+static inline u32 arm_udiv(u32 rd, u32 rn, u32 rm) {
+ return arm_t32(0xfbb0u | rn, 0xf0f0u | (rd << 8) | rm);
+}
+
+/* --------- sign/zero extends --------- */
+static inline u32 arm_sxtb(u32 rd, u32 rm) { return arm_t32(0xfa4fu, 0xf080u | (rd << 8) | rm); }
+static inline u32 arm_sxth(u32 rd, u32 rm) { return arm_t32(0xfa0fu, 0xf080u | (rd << 8) | rm); }
+static inline u32 arm_uxtb(u32 rd, u32 rm) { return arm_t32(0xfa5fu, 0xf080u | (rd << 8) | rm); }
+static inline u32 arm_uxth(u32 rd, u32 rm) { return arm_t32(0xfa1fu, 0xf080u | (rd << 8) | rm); }
+
+/* --------- loads / stores (immediate, positive 12-bit offset, T3) --------- */
+static inline u32 arm_ldr_imm(u32 rt, u32 rn, u32 imm12) {
+ return arm_t32(0xf8d0u | rn, (rt << 12) | (imm12 & 0xfffu));
+}
+static inline u32 arm_str_imm(u32 rt, u32 rn, u32 imm12) {
+ return arm_t32(0xf8c0u | rn, (rt << 12) | (imm12 & 0xfffu));
+}
+static inline u32 arm_ldrb_imm(u32 rt, u32 rn, u32 imm12) {
+ return arm_t32(0xf890u | rn, (rt << 12) | (imm12 & 0xfffu));
+}
+static inline u32 arm_strb_imm(u32 rt, u32 rn, u32 imm12) {
+ return arm_t32(0xf880u | rn, (rt << 12) | (imm12 & 0xfffu));
+}
+static inline u32 arm_ldrh_imm(u32 rt, u32 rn, u32 imm12) {
+ return arm_t32(0xf8b0u | rn, (rt << 12) | (imm12 & 0xfffu));
+}
+static inline u32 arm_strh_imm(u32 rt, u32 rn, u32 imm12) {
+ return arm_t32(0xf8a0u | rn, (rt << 12) | (imm12 & 0xfffu));
+}
+static inline u32 arm_ldrsb_imm(u32 rt, u32 rn, u32 imm12) {
+ return arm_t32(0xf990u | rn, (rt << 12) | (imm12 & 0xfffu));
+}
+static inline u32 arm_ldrsh_imm(u32 rt, u32 rn, u32 imm12) {
+ return arm_t32(0xf9b0u | rn, (rt << 12) | (imm12 & 0xfffu));
+}
+
+/* LDR/STR (immediate) T4: [Rn, #+/-imm8], offset addressing (P=1, W=0).
+ * `add` = 1 for +imm8, 0 for -imm8. hw1 base picks the op + width:
+ * LDR=0xF850 STR=0xF840 LDRB=0xF810 STRB=0xF800 LDRH=0xF830 STRH=0xF820
+ * LDRSB=0xF910 LDRSH=0xF930. */
+static inline u32 arm_ldst_t4(u32 hw1_base, u32 rt, u32 rn, u32 imm8, u32 add) {
+ return arm_t32(hw1_base | rn,
+ (rt << 12) | 0xc00u | (add << 9) | (imm8 & 0xffu));
+}
+
+/* 16-bit MOV (register, high-reg form T1): handles all of r0..r15 incl. sp. */
+static inline u16 arm_mov_hi(u32 rd, u32 rm) {
+ return (u16)(0x4600u | ((rd >> 3) << 7) | (rm << 3) | (rd & 7u));
+}
+
+/* --------- branches (placeholders; immediate filled by reloc/label-fixup) --- */
+static inline u32 arm_b_w(void) { return arm_t32(0xf000u, 0x9000u); } /* B.W (T4) */
+static inline u32 arm_b_cond_w(u32 cond) { /* B<cond>.W (T3) */
+ return arm_t32(0xf000u | (cond << 6), 0x8000u);
+}
+static inline u32 arm_bl(void) { return arm_t32(0xf000u, 0xd000u); } /* BL (T1) */
+
+/* --------- 16-bit instructions --------- */
+static inline u16 arm_bx(u32 rm) { return (u16)(0x4700u | (rm << 3)); }
+static inline u16 arm_blx_reg(u32 rm) { return (u16)(0x4780u | (rm << 3)); }
+static inline u16 arm_bkpt(u32 imm8) { return (u16)(0xbe00u | (imm8 & 0xffu)); }
+static inline u16 arm_nop16(void) { return (u16)0xbf00u; }
+
+/* --------- push / pop (32-bit, STMDB sp! / LDMIA sp!) --------- */
+static inline u32 arm_push_w(u32 reglist) { return arm_t32(0xe92du, reglist & 0xdfffu); }
+static inline u32 arm_pop_w(u32 reglist) { return arm_t32(0xe8bdu, reglist & 0xffffu); }
+
+#endif
diff --git a/src/arch/arm32/link.c b/src/arch/arm32/link.c
@@ -0,0 +1,20 @@
+/* ARM32 link-time arch descriptor. See link_arch.h for the contract.
+ *
+ * Static-only for v1: no PLT/GOT/IPLT, no dynamic linking (mirrors rv32's
+ * freestanding decision). ELF TLS is variant I (TCB ahead of .tdata), so
+ * tls_variant_ii = 0 (the struct's zero-init default). The only live fields
+ * are the per-arch reloc descriptor + the Thumb-2 split-immediate patcher;
+ * everything else (PLT geometry, stub emitters, JIT relax) is zero/NULL. */
+#include "core/core.h"
+#include "link/link_arch.h"
+
+/* Defined in src/arch/arm32/reloc.c. */
+const RelocDesc* arm32_reloc_desc(RelocKind k);
+int arm32_reloc_apply_insn(Compiler* c, RelocKind k, u8* P_bytes, u64 S, i64 A,
+ u64 P);
+
+const LinkArchDesc link_arch_arm32 = {
+ .reloc_desc = arm32_reloc_desc,
+ .reloc_apply_insn = arm32_reloc_apply_insn,
+ /* .tls_variant_ii = 0 (variant I) — implicit zero. */
+};
diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c
@@ -0,0 +1,1249 @@
+/* ARM32 (ARMv7-M Thumb-2, soft-float) NativeTarget backend.
+ *
+ * Built on the shared NativeDirectTarget/MCEmitter/NativeFrame substrate. This
+ * is the Phase-1 "walking skeleton" backend: it lowers the smoke set (return,
+ * ALU, compare + if/else, and a <=4-int-arg call) correctly; the op-group
+ * expansion (i64 lanes, bitfields, atomics, varargs, inline asm, the disasm
+ * tables, code-density tiers) is Phase 2.
+ *
+ * Frame model (single-pass, FP-anchored):
+ * prologue: PUSH {r7, lr} ; save the frame pointer + return address
+ * MOV r7, sp ; r7 = frame anchor (just below the saved pair)
+ * SUB sp, sp, #N ; reserve locals + outgoing args (patched at end)
+ * slots: addressed [r7, #-off] (off fixed at allocation, anchor fixed)
+ * epilogue: MOV sp, r7 ; drop the frame (size-independent)
+ * 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). */
+#include <string.h>
+
+#include "abi/abi.h"
+#include "arch/arm32/arm32.h"
+#include "arch/arm32/isa.h"
+#include "arch/arm32/regs.h"
+#include "arch/arm32/variant.h"
+#include "arch/mc.h"
+#include "arch/native_target.h"
+#include "cg/cgir.h"
+#include "cg/native_argmove.h"
+#include "cg/native_asm.h"
+#include "cg/native_direct_target.h"
+#include "cg/native_frame.h"
+#include "cg/type.h"
+#include "core/bytes.h"
+#include "core/core.h"
+#include "obj/obj.h"
+
+extern void debug_emit_row(Debug*, ObjSecId text_section, u32 offset, SrcLoc);
+extern void debug_func_pc_range(Debug*, ObjSecId text_section, u32 begin_ofs,
+ u32 end_ofs);
+
+#define ARM_FP 7u /* frame pointer (Thumb convention) */
+#define ARM_SCRATCH 12u /* IP: NDT operand-materialization / cycle-break scratch */
+/* Backend emit-internal temp for multi-step sequences (div/mod). lr is RESERVED
+ * (never holds an NDT operand) and dead in the body — its live value is saved on
+ * the stack by the prologue and restored via `pop {..,pc}`, so clobbering the lr
+ * register between calls is free and cannot collide with an NDT operand in IP. */
+#define ARM_TMP 14u
+#define ARM_MAX_REG_ARG_MOVES 16u
+
+/* ============================ state ============================ */
+
+typedef struct Arm32NativeTarget {
+ NativeTarget base; /* MUST be first member (arm_of is a reinterpret cast) */
+ const Arm32Variant* variant;
+ SrcLoc loc;
+ const CGFuncDesc* func;
+ NativeFrame frame;
+ u32 next_param_int; /* incoming param cursor: r0..r3 */
+ u32 next_param_stack;
+ u8 has_sret;
+ u8 is_variadic;
+ NativeFrameSlot sret_ptr_slot;
+ u32 func_start;
+ u32 prologue_sub_pos; /* file offset of the deferred SUB sp (patched at end) */
+ MCLabel epilogue_label;
+} Arm32NativeTarget;
+
+static Arm32NativeTarget* arm_of(NativeTarget* t) {
+ return (Arm32NativeTarget*)t;
+}
+
+static _Noreturn void arm_panic(Arm32NativeTarget* a, const char* msg) {
+ compiler_panic(a->base.c, a->loc, "arm32 native target: %s", msg);
+}
+
+static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0xfu; }
+
+/* ============================ emit ============================ */
+
+static void arm_emit_t16(MCEmitter* mc, u16 hw) {
+ u8 b[2];
+ u32 ofs = obj_pos(mc->obj, mc->section_id);
+ b[0] = (u8)(hw & 0xffu);
+ b[1] = (u8)((hw >> 8) & 0xffu);
+ mc_emit_bytes(mc, b, sizeof b);
+ if (mc->debug) debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc);
+}
+
+/* A 32-bit Thumb-2 instruction (passed as (hw1<<16)|hw2) is two LE half-words,
+ * hw1 first in memory. NOT a single LE word — never route through mc_emit32. */
+static void arm_emit_t32(MCEmitter* mc, u32 instr) {
+ u8 b[4];
+ u32 ofs = obj_pos(mc->obj, mc->section_id);
+ u32 hw1 = (instr >> 16) & 0xffffu, hw2 = instr & 0xffffu;
+ b[0] = (u8)(hw1 & 0xffu);
+ b[1] = (u8)((hw1 >> 8) & 0xffu);
+ b[2] = (u8)(hw2 & 0xffu);
+ b[3] = (u8)((hw2 >> 8) & 0xffu);
+ mc_emit_bytes(mc, b, sizeof b);
+ if (mc->debug) debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc);
+}
+
+static void arm_patch_t32(Arm32NativeTarget* a, u32 pos, u32 instr) {
+ u8 b[4];
+ u32 hw1 = (instr >> 16) & 0xffffu, hw2 = instr & 0xffffu;
+ b[0] = (u8)(hw1 & 0xffu);
+ b[1] = (u8)((hw1 >> 8) & 0xffu);
+ b[2] = (u8)(hw2 & 0xffu);
+ b[3] = (u8)((hw2 >> 8) & 0xffu);
+ obj_patch(a->base.obj, a->base.mc->section_id, pos, b, 4);
+}
+
+/* ============================ register tables ============================ */
+
+#define ARM_PHYS_ARG(r, idx) \
+ {.reg = (r), \
+ .cls = NATIVE_REG_INT, \
+ .abi_index = (idx), \
+ .flags = NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | \
+ ((idx) < 2u ? NATIVE_REG_RET : 0), \
+ .spill_cost = 1u, \
+ .copy_cost = 1u}
+#define ARM_PHYS_CALLEE(r) \
+ {.reg = (r), \
+ .cls = NATIVE_REG_INT, \
+ .abi_index = 0xffu, \
+ .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \
+ .spill_cost = 4u, \
+ .copy_cost = 1u}
+#define ARM_PHYS_RESERVED(r) \
+ {.reg = (r), \
+ .cls = NATIVE_REG_INT, \
+ .abi_index = 0xffu, \
+ .flags = NATIVE_REG_RESERVED, \
+ .spill_cost = 0u, \
+ .copy_cost = 0u}
+
+/* NDT (-O0) value-cache pool: the caller-saved arg registers r0..r3, fronted so
+ * a producer materializes a call's args directly into their ABI registers
+ * (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_int_scratch[] = {ARM_SCRATCH};
+
+static const NativePhysRegInfo arm_int_phys[] = {
+ ARM_PHYS_ARG(0u, 0u), ARM_PHYS_ARG(1u, 1u),
+ ARM_PHYS_ARG(2u, 2u), ARM_PHYS_ARG(3u, 3u),
+ ARM_PHYS_CALLEE(4u), ARM_PHYS_CALLEE(5u),
+ ARM_PHYS_CALLEE(6u), ARM_PHYS_RESERVED(7u), /* fp */
+ ARM_PHYS_CALLEE(8u), ARM_PHYS_CALLEE(9u),
+ ARM_PHYS_CALLEE(10u), ARM_PHYS_CALLEE(11u),
+ ARM_PHYS_RESERVED(12u), /* ip = scratch */
+ ARM_PHYS_RESERVED(13u), /* sp */
+ ARM_PHYS_RESERVED(14u), /* lr */
+ ARM_PHYS_RESERVED(15u), /* pc */
+};
+
+static int arm_resolve_name(const NativeRegInfo* ri, Slice name, Reg* out,
+ NativeAllocClass* cls_out) {
+ char buf[16];
+ uint32_t dwarf;
+ (void)ri;
+ if (!name.s || !name.len || name.len >= sizeof buf) return 1;
+ memcpy(buf, name.s, name.len);
+ buf[name.len] = '\0';
+ if (arm32_register_index(buf, &dwarf) != 0) return 1;
+ if (dwarf <= 15u) {
+ *cls_out = NATIVE_REG_INT;
+ *out = (Reg)dwarf;
+ return 0;
+ }
+ return 1;
+}
+
+static int arm_asm_operand_reg_ok(const NativeRegInfo* ri, NativeAllocClass cls,
+ Reg reg) {
+ (void)ri;
+ if (cls != NATIVE_REG_INT) return 0;
+ if (reg <= 3u) return 1; /* r0..r3 */
+ if (reg >= 4u && reg <= 11u && reg != 7u) return 1; /* r4..r11 except fp */
+ return 0;
+}
+
+static int arm_asm_constraint_reg(const NativeRegInfo* ri, const char* body,
+ NativeAllocClass* cls_out, Reg* fixed_out,
+ u32* allowed_mask_out) {
+ (void)ri;
+ if (!body || !body[0]) return 0;
+ if (fixed_out) *fixed_out = REG_NONE;
+ if (allowed_mask_out) *allowed_mask_out = 0;
+ if ((body[0] == 'r' || body[0] == 'l') && body[1] == '\0') {
+ if (cls_out) *cls_out = NATIVE_REG_INT;
+ return 1;
+ }
+ return 0;
+}
+
+static const NativeAllocClassInfo arm_classes[] = {
+ {.cls = NATIVE_REG_INT,
+ .allocable = arm_int_allocable,
+ .nallocable = sizeof arm_int_allocable / sizeof arm_int_allocable[0],
+ .scratch = arm_int_scratch,
+ .nscratch = sizeof arm_int_scratch / sizeof arm_int_scratch[0],
+ .phys = arm_int_phys,
+ .nphys = sizeof arm_int_phys / sizeof arm_int_phys[0],
+ /* r0..r3 + r12 (lr/ip caller-saved; lr reserved) */
+ .caller_saved_mask = 0x0000100fu,
+ /* r4..r11 */
+ .callee_saved_mask = 0x00000ff0u,
+ .arg_mask = 0x0000000fu,
+ .ret_mask = 0x00000003u,
+ /* r7 (fp), r12 (ip), r13 (sp), r14 (lr), r15 (pc) */
+ .reserved_mask = 0x0000f080u},
+};
+
+static const NativeRegInfo arm_reg_info = {
+ .classes = arm_classes,
+ .nclasses = sizeof arm_classes / sizeof arm_classes[0],
+ .ndt_caller_saved_only = 1u,
+ .ndt_result_reg_stable = 1u,
+ .resolve_name = arm_resolve_name,
+ .asm_operand_reg_ok = arm_asm_operand_reg_ok,
+ .asm_constraint_reg = arm_asm_constraint_reg,
+};
+
+/* ============================ legality ============================ */
+
+static int arm_imm_legal(NativeTarget* t, NativeImmUse use, u32 op,
+ KitCgTypeId type, i64 imm) {
+ u32 enc;
+ (void)t;
+ (void)type;
+ switch (use) {
+ case NATIVE_IMM_MOVE:
+ return 1; /* MOVW/MOVT materializes any 32-bit value */
+ case NATIVE_IMM_CMP:
+ return thumb_expand_imm_encode((u32)imm, &enc);
+ case NATIVE_IMM_BINOP:
+ switch ((BinOp)op) {
+ case BO_IADD:
+ case BO_ISUB:
+ if (thumb_expand_imm_encode((u32)(op == BO_ISUB ? -imm : imm), &enc))
+ return 1;
+ return imm >= 0 && imm <= 0xfff; /* ADDW/SUBW raw 12-bit */
+ case BO_AND:
+ case BO_OR:
+ case BO_XOR:
+ return thumb_expand_imm_encode((u32)imm, &enc);
+ case BO_SHL:
+ case BO_SHR_S:
+ case BO_SHR_U:
+ return imm >= 0 && imm < 32;
+ default:
+ return 0;
+ }
+ case NATIVE_IMM_ADDR_OFFSET:
+ return imm >= -255 && imm <= 4095;
+ }
+ return 0;
+}
+
+static int arm_addr_legal(NativeTarget* t, const NativeAddr* addr,
+ MemAccess mem) {
+ (void)t;
+ (void)mem;
+ if (!addr) return 0;
+ /* Base + 12-bit positive or 8-bit signed offset, no scaled index in v1. */
+ if (addr->index_kind != NATIVE_ADDR_INDEX_NONE) return 0;
+ return addr->offset >= -255 && addr->offset <= 4095;
+}
+
+/* ============================ memory ============================ */
+
+/* Resolve a NativeAddr to a (base reg, signed byte offset) pair. */
+static u32 arm_addr_base(Arm32NativeTarget* a, const NativeAddr* addr,
+ i32* off_out) {
+ switch (addr->base_kind) {
+ case NATIVE_ADDR_BASE_FRAME: {
+ NativeFrameSlotEntry* s =
+ native_frame_slot_at(&a->frame, addr->base.frame);
+ *off_out = -(i32)s->off + addr->offset; /* [r7, #-(off) + extra] */
+ return ARM_FP;
+ }
+ case NATIVE_ADDR_BASE_REG:
+ *off_out = addr->offset;
+ return addr->base.reg & 0xfu;
+ default:
+ arm_panic(a, "unsupported addressing mode");
+ }
+}
+
+/* Emit a load (is_load=1) or store of `reg` to [base, #off], dispatching the
+ * width from mem.size. Loads zero-extend (LDR/LDRB/LDRH); signed narrowing is a
+ * separate convert. */
+static void arm_emit_mem(Arm32NativeTarget* a, int is_load, NativeLoc reg,
+ NativeAddr addr, MemAccess mem) {
+ MCEmitter* mc = a->base.mc;
+ i32 off;
+ u32 base = arm_addr_base(a, &addr, &off);
+ u32 rt = loc_reg(reg);
+ u32 size = mem.size ? mem.size : native_type_size(&a->base, reg.type);
+ u32 t3hw1, t4hw1;
+ if (size >= 8u) arm_panic(a, "8-byte memory access not lowered (wide8 path)");
+ /* pick the op (T3 positive, T4 +/-) for this width. */
+ if (size == 1u) {
+ t3hw1 = is_load ? 0xf890u : 0xf880u;
+ t4hw1 = is_load ? 0xf810u : 0xf800u;
+ } else if (size == 2u) {
+ t3hw1 = is_load ? 0xf8b0u : 0xf8a0u;
+ t4hw1 = is_load ? 0xf830u : 0xf820u;
+ } else {
+ t3hw1 = is_load ? 0xf8d0u : 0xf8c0u;
+ t4hw1 = is_load ? 0xf850u : 0xf840u;
+ }
+ if (off >= 0 && off <= 4095) {
+ arm_emit_t32(mc, arm_t32(t3hw1 | base, (rt << 12) | (u32)off));
+ } else if (off < 0 && off >= -255) {
+ arm_emit_t32(mc, arm_ldst_t4(t4hw1, rt, base, (u32)(-off), 0u));
+ } else if (off >= 0 && off <= 255) {
+ arm_emit_t32(mc, arm_ldst_t4(t4hw1, rt, base, (u32)off, 1u));
+ } else {
+ arm_panic(a, "frame/memory offset out of range (needs IP fallback)");
+ }
+}
+
+/* ============================ moves / immediates ============================ */
+
+static void arm_load_imm(NativeTarget* t, NativeLoc dst, i64 imm) {
+ MCEmitter* mc = t->mc;
+ u32 rd = loc_reg(dst);
+ u32 v = (u32)imm;
+ u32 enc;
+ if (thumb_expand_imm_encode(v, &enc)) {
+ arm_emit_t32(mc, arm_mov_imm(rd, enc));
+ return;
+ }
+ if (thumb_expand_imm_encode(~v, &enc)) {
+ arm_emit_t32(mc, arm_mvn_imm(rd, enc));
+ return;
+ }
+ arm_emit_t32(mc, arm_movw(rd, v & 0xffffu));
+ if ((v >> 16) != 0u) arm_emit_t32(mc, arm_movt(rd, (v >> 16) & 0xffffu));
+}
+
+static void arm_move(NativeTarget* t, NativeLoc dst, NativeLoc src) {
+ u32 rd = loc_reg(dst), rs = loc_reg(src);
+ if (rd == rs) return;
+ arm_emit_t16(t->mc, arm_mov_hi(rd, rs));
+}
+
+static void arm_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
+ MemAccess mem) {
+ arm_emit_mem(arm_of(t), 1, dst, addr, mem);
+}
+static void arm_store(NativeTarget* t, NativeAddr addr, NativeLoc src,
+ MemAccess mem) {
+ arm_emit_mem(arm_of(t), 0, src, addr, mem);
+}
+static void arm_spill(NativeTarget* t, NativeLoc src, NativeFrameSlot slot,
+ MemAccess mem) {
+ NativeAddr addr;
+ memset(&addr, 0, sizeof addr);
+ addr.base_kind = NATIVE_ADDR_BASE_FRAME;
+ addr.base.frame = slot;
+ addr.base_type = src.type;
+ arm_emit_mem(arm_of(t), 0, src, addr, mem);
+}
+static void arm_reload(NativeTarget* t, NativeLoc dst, NativeFrameSlot slot,
+ MemAccess mem) {
+ NativeAddr addr;
+ memset(&addr, 0, sizeof addr);
+ addr.base_kind = NATIVE_ADDR_BASE_FRAME;
+ addr.base.frame = slot;
+ addr.base_type = dst.type;
+ arm_emit_mem(arm_of(t), 1, dst, addr, mem);
+}
+
+/* Load the address of a frame slot / reg base into a register (ADD rd, base,
+ * #off via the modified-immediate or ADDW form; small offsets only in v1). */
+static void arm_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) {
+ Arm32NativeTarget* a = arm_of(t);
+ MCEmitter* mc = t->mc;
+ i32 off;
+ u32 base = arm_addr_base(a, &addr, &off);
+ u32 rd = loc_reg(dst);
+ u32 enc;
+ if (off == 0) {
+ arm_emit_t16(mc, arm_mov_hi(rd, base));
+ } else if (off > 0 && off <= 0xfff) {
+ arm_emit_t32(mc, arm_add_imm12(rd, base, (u32)off));
+ } else if (off < 0 && thumb_expand_imm_encode((u32)(-off), &enc)) {
+ arm_emit_t32(mc, arm_dp_imm(13u, 0u, rd, base, enc)); /* SUB rd, base, #-off */
+ } else {
+ arm_panic(a, "load_addr offset out of range");
+ }
+}
+
+/* ============================ ALU ============================ */
+
+static void arm_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc a_loc,
+ NativeLoc b) {
+ Arm32NativeTarget* a = arm_of(t);
+ MCEmitter* mc = t->mc;
+ u32 rd = loc_reg(dst), ra = loc_reg(a_loc);
+ int b_imm = b.kind == NATIVE_LOC_IMM;
+ u32 rb = b_imm ? 0u : loc_reg(b);
+ u32 enc;
+ switch (op) {
+ case BO_IADD:
+ if (b_imm) {
+ if (thumb_expand_imm_encode((u32)b.v.imm, &enc))
+ arm_emit_t32(mc, arm_dp_imm(8u, 0u, rd, ra, enc));
+ else
+ arm_emit_t32(mc, arm_add_imm12(rd, ra, (u32)b.v.imm & 0xfffu));
+ } else {
+ arm_emit_t32(mc, arm_add_reg(rd, ra, rb));
+ }
+ return;
+ case BO_ISUB:
+ if (b_imm) {
+ if (thumb_expand_imm_encode((u32)b.v.imm, &enc))
+ arm_emit_t32(mc, arm_dp_imm(13u, 0u, rd, ra, enc));
+ else
+ arm_emit_t32(mc, arm_sub_imm12(rd, ra, (u32)b.v.imm & 0xfffu));
+ } else {
+ arm_emit_t32(mc, arm_sub_reg(rd, ra, rb));
+ }
+ return;
+ case BO_IMUL:
+ arm_emit_t32(mc, arm_mul(rd, ra, rb));
+ return;
+ case BO_SDIV:
+ arm_emit_t32(mc, arm_sdiv(rd, ra, rb));
+ return;
+ case BO_UDIV:
+ arm_emit_t32(mc, arm_udiv(rd, ra, rb));
+ return;
+ case BO_SREM:
+ arm_emit_t32(mc, arm_sdiv(ARM_TMP, ra, rb));
+ arm_emit_t32(mc, arm_mls(rd, ARM_TMP, rb, ra)); /* rd = ra - (ra/rb)*rb */
+ return;
+ case BO_UREM:
+ arm_emit_t32(mc, arm_udiv(ARM_TMP, ra, rb));
+ arm_emit_t32(mc, arm_mls(rd, ARM_TMP, rb, ra));
+ return;
+ case BO_AND:
+ if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc))
+ arm_emit_t32(mc, arm_dp_imm(0u, 0u, rd, ra, enc));
+ else
+ arm_emit_t32(mc, arm_and_reg(rd, ra, rb));
+ return;
+ case BO_OR:
+ if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc))
+ arm_emit_t32(mc, arm_dp_imm(2u, 0u, rd, ra, enc));
+ else
+ arm_emit_t32(mc, arm_orr_reg(rd, ra, rb));
+ return;
+ case BO_XOR:
+ if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc))
+ arm_emit_t32(mc, arm_dp_imm(4u, 0u, rd, ra, enc));
+ else
+ arm_emit_t32(mc, arm_eor_reg(rd, ra, rb));
+ return;
+ case BO_SHL:
+ if (b_imm)
+ arm_emit_t32(mc, arm_shift_imm(0u, rd, ra, (u32)b.v.imm & 31u));
+ else
+ arm_emit_t32(mc, arm_shift_reg(0u, rd, ra, rb));
+ return;
+ case BO_SHR_U:
+ if (b_imm)
+ arm_emit_t32(mc, arm_shift_imm(1u, rd, ra, (u32)b.v.imm & 31u));
+ else
+ arm_emit_t32(mc, arm_shift_reg(1u, rd, ra, rb));
+ return;
+ case BO_SHR_S:
+ if (b_imm)
+ arm_emit_t32(mc, arm_shift_imm(2u, rd, ra, (u32)b.v.imm & 31u));
+ else
+ arm_emit_t32(mc, arm_shift_reg(2u, rd, ra, rb));
+ return;
+ default:
+ arm_panic(a, "binop not lowered (FP / wide8 is Phase 2)");
+ }
+}
+
+static void arm_unop(NativeTarget* t, UnOp op, NativeLoc dst, NativeLoc src) {
+ Arm32NativeTarget* a = arm_of(t);
+ MCEmitter* mc = t->mc;
+ u32 rd = loc_reg(dst), rs = loc_reg(src);
+ u32 enc;
+ switch (op) {
+ case UO_NEG:
+ thumb_expand_imm_encode(0u, &enc);
+ arm_emit_t32(mc, arm_dp_imm(14u, 0u, rd, rs, enc)); /* RSB rd, rs, #0 */
+ return;
+ case UO_BNOT:
+ arm_emit_t32(mc, arm_mvn_reg(rd, rs));
+ return;
+ default:
+ arm_panic(a, "unop not lowered (FNEG/NOT are Phase 2)");
+ }
+}
+
+/* Map a CmpOp to its ARM condition code (taken-when-true). */
+static u32 arm_cond_for(Arm32NativeTarget* a, CmpOp op) {
+ switch (op) {
+ case CMP_EQ: return ARM_CC_EQ;
+ case CMP_NE: return ARM_CC_NE;
+ case CMP_LT_S: return ARM_CC_LT;
+ case CMP_LE_S: return ARM_CC_LE;
+ case CMP_GT_S: return ARM_CC_GT;
+ case CMP_GE_S: return ARM_CC_GE;
+ case CMP_LT_U: return ARM_CC_CC;
+ case CMP_LE_U: return ARM_CC_LS;
+ case CMP_GT_U: return ARM_CC_HI;
+ case CMP_GE_U: return ARM_CC_CS;
+ default:
+ arm_panic(a, "FP compare not lowered (Phase 2)");
+ }
+}
+
+static void arm_emit_cmp_operands(Arm32NativeTarget* a, NativeLoc x,
+ NativeLoc y) {
+ MCEmitter* mc = a->base.mc;
+ u32 enc;
+ if (y.kind == NATIVE_LOC_IMM && thumb_expand_imm_encode((u32)y.v.imm, &enc))
+ arm_emit_t32(mc, arm_cmp_imm(loc_reg(x), enc));
+ else
+ arm_emit_t32(mc, arm_cmp_reg(loc_reg(x), loc_reg(y)));
+}
+
+/* Materialize the boolean (a OP b) into dst: MOV dst,#0; CMP; IT cc; MOV dst,#1. */
+static void arm_cmp(NativeTarget* t, CmpOp op, NativeLoc dst, NativeLoc a_loc,
+ NativeLoc b) {
+ Arm32NativeTarget* a = arm_of(t);
+ MCEmitter* mc = t->mc;
+ u32 rd = loc_reg(dst);
+ u32 cc = arm_cond_for(a, op);
+ u32 enc0, enc1;
+ thumb_expand_imm_encode(0u, &enc0);
+ thumb_expand_imm_encode(1u, &enc1);
+ arm_emit_t32(mc, arm_mov_imm(rd, enc0)); /* dst = 0 */
+ arm_emit_cmp_operands(a, a_loc, b); /* CMP a, b */
+ arm_emit_t16(mc, (u16)(0xbf08u | (cc << 4))); /* IT cc (1 insn, mask 0b1000) */
+ arm_emit_t32(mc, arm_mov_imm(rd, enc1)); /* MOV<cc> dst, #1 */
+}
+
+static void arm_convert(NativeTarget* t, ConvKind op, NativeLoc dst,
+ NativeLoc src) {
+ Arm32NativeTarget* a = arm_of(t);
+ MCEmitter* mc = t->mc;
+ u32 rd = loc_reg(dst), rs = loc_reg(src);
+ u32 dsz = native_type_size(t, dst.type);
+ u32 ssz = native_type_size(t, src.type);
+ switch (op) {
+ case CV_SEXT:
+ if (ssz == 1u) arm_emit_t32(mc, arm_sxtb(rd, rs));
+ else if (ssz == 2u) arm_emit_t32(mc, arm_sxth(rd, rs));
+ else if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs));
+ return;
+ case CV_ZEXT:
+ if (ssz == 1u) arm_emit_t32(mc, arm_uxtb(rd, rs));
+ else if (ssz == 2u) arm_emit_t32(mc, arm_uxth(rd, rs));
+ else if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs));
+ return;
+ case CV_TRUNC:
+ if (dsz == 1u) arm_emit_t32(mc, arm_uxtb(rd, rs));
+ else if (dsz == 2u) arm_emit_t32(mc, arm_uxth(rd, rs));
+ else if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs));
+ return;
+ case CV_BITCAST:
+ if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs));
+ return;
+ default:
+ arm_panic(a, "convert not lowered (FP conversions are Phase 2)");
+ }
+}
+
+/* ============================ _rr narrow hooks ============================ */
+
+static void arm_binop_rr(NativeTarget* t, BinOp op, NativeRegLoc dst,
+ NativeRegLoc a, NativeRegLoc b) {
+ arm_binop(t, op, native_loc_from_reg(dst), native_loc_from_reg(a),
+ native_loc_from_reg(b));
+}
+static void arm_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) {
+ arm_move(t, native_loc_from_reg(dst), native_loc_from_reg(src));
+}
+static void arm_cmp_rr(NativeTarget* t, CmpOp op, NativeRegLoc dst,
+ NativeRegLoc a, NativeRegLoc b) {
+ arm_cmp(t, op, native_loc_from_reg(dst), native_loc_from_reg(a),
+ native_loc_from_reg(b));
+}
+static void arm_convert_rr(NativeTarget* t, ConvKind op, NativeRegLoc dst,
+ NativeRegLoc src) {
+ arm_convert(t, op, native_loc_from_reg(dst), native_loc_from_reg(src));
+}
+
+/* ============================ control flow ============================ */
+
+static MCLabel arm_label_new(NativeTarget* t) { return mc_label_new(t->mc); }
+static void arm_label_place(NativeTarget* t, MCLabel l) {
+ mc_label_place(t->mc, l);
+}
+static void arm_jump(NativeTarget* t, MCLabel l) {
+ arm_emit_t32(t->mc, arm_b_w());
+ mc_emit_label_ref(t->mc, l, R_ARM_THM_JUMP24, 4, 0);
+}
+static void arm_cmp_branch(NativeTarget* t, CmpOp op, NativeLoc a_loc,
+ NativeLoc b, MCLabel target) {
+ Arm32NativeTarget* a = arm_of(t);
+ u32 cc = arm_cond_for(a, op);
+ arm_emit_cmp_operands(a, a_loc, b);
+ arm_emit_t32(t->mc, arm_b_cond_w(cc));
+ mc_emit_label_ref(t->mc, target, R_ARM_THM_JUMP19, 4, 0);
+}
+
+/* ============================ frame lifecycle ============================ */
+
+static NativeFrameSlot arm_frame_slot(NativeTarget* t,
+ const NativeFrameSlotDesc* d) {
+ return native_frame_slot_alloc(&arm_of(t)->frame, d);
+}
+static void arm_release_frame_slot(NativeTarget* t, NativeFrameSlot slot) {
+ native_frame_release_slot(&arm_of(t)->frame, slot);
+}
+static int arm_frame_slot_debug_loc(NativeTarget* t, NativeFrameSlot slot,
+ CGDebugLoc* out) {
+ Arm32NativeTarget* a = arm_of(t);
+ NativeFrameSlotEntry* s;
+ if (!out) return 0;
+ memset(out, 0, sizeof *out);
+ if (slot == NATIVE_FRAME_SLOT_NONE || slot > a->frame.nslots) return 0;
+ s = native_frame_slot_at(&a->frame, slot);
+ out->kind = CG_DEBUG_LOC_FRAME;
+ out->v.frame_ofs = -(i32)s->off; /* r7-relative */
+ return 1;
+}
+
+static u32 arm_frame_size(const Arm32NativeTarget* a) {
+ return align_up_u32(a->frame.cum_off + a->frame.max_outgoing, 8u);
+}
+
+static void arm_reserve_callee_saves(NativeTarget* t, const u32* used_by_class,
+ u32 nclasses) {
+ native_frame_set_callee_saves(&arm_of(t)->frame, used_by_class, nclasses,
+ NULL, 0, 0);
+}
+
+static void arm_func_begin(NativeTarget* t, const CGFuncDesc* fd) {
+ Arm32NativeTarget* a = arm_of(t);
+ MCEmitter* mc = t->mc;
+ const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type);
+ a->func = fd;
+ a->loc = fd->loc;
+ native_frame_reset(&a->frame);
+ a->next_param_int = 0;
+ a->next_param_stack = 0;
+ a->has_sret = (abi && abi->has_sret) ? 1u : 0u;
+ a->is_variadic = (abi && abi->variadic) ? 1u : 0u;
+ a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE;
+
+ mc_set_section(mc, fd->text_section_id);
+ mc_emit_align(mc, 4, 0);
+ a->func_start = mc_pos(mc);
+ mc_begin_function(mc, fd->sym, fd->text_section_id, a->func_start);
+ mc_cfi_startproc(mc);
+ a->epilogue_label = mc_label_new(mc);
+
+ /* Prologue: save fp+lr, set the frame anchor, reserve the (deferred) frame. */
+ arm_emit_t32(mc, arm_push_w((1u << ARM_FP) | (1u << 14u))); /* PUSH {r7, lr} */
+ arm_emit_t16(mc, arm_mov_hi(ARM_FP, 13u)); /* MOV r7, sp */
+ a->prologue_sub_pos = mc_pos(mc);
+ arm_emit_t32(mc, arm_sub_imm12(13u, 13u, 0u)); /* SUB sp, sp, #0 (patched) */
+
+ /* sret: spill the incoming destination pointer (r0) to a hidden home. */
+ if (a->has_sret) {
+ NativeFrameSlotDesc sd;
+ NativeAddr addr;
+ KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
+ memset(&sd, 0, sizeof sd);
+ sd.type = i32t;
+ sd.size = 4;
+ sd.align = 4;
+ sd.kind = NATIVE_FRAME_SLOT_SAVE;
+ a->sret_ptr_slot = arm_frame_slot(t, &sd);
+ a->next_param_int = 1;
+ memset(&addr, 0, sizeof addr);
+ addr.base_kind = NATIVE_ADDR_BASE_FRAME;
+ addr.base.frame = a->sret_ptr_slot;
+ addr.base_type = i32t;
+ arm_emit_mem(a, 0, native_loc_reg(i32t, NATIVE_REG_INT, ARM_R0), addr,
+ native_mem_for_type(t, i32t, 4));
+ }
+}
+
+static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd,
+ const NativeKnownFrameDesc* kf,
+ NativeFrameSlot* out_slots) {
+ (void)fd;
+ (void)kf;
+ (void)out_slots;
+ arm_panic(arm_of(t), "known-frame (-O1) path not implemented in Phase 1");
+}
+
+static void arm_func_end(NativeTarget* t) {
+ Arm32NativeTarget* a = arm_of(t);
+ MCEmitter* mc = t->mc;
+ u32 frame;
+ /* Place the epilogue and emit the teardown. */
+ mc_label_place(mc, a->epilogue_label);
+ arm_emit_t16(mc, arm_mov_hi(13u, ARM_FP)); /* MOV sp, r7 */
+ arm_emit_t32(mc, arm_pop_w((1u << ARM_FP) | (1u << 15u))); /* POP {r7, pc} */
+
+ native_frame_set_final(&a->frame);
+ frame = arm_frame_size(a);
+ if (frame > 0xfffu)
+ arm_panic(a, "frame too large for Phase 1 (needs IP-staged SUB)");
+ arm_patch_t32(a, a->prologue_sub_pos, arm_sub_imm12(13u, 13u, frame));
+
+ /* Publish the function symbol. ARM marks Thumb STT_FUNC symbols with the
+ * low bit set (the Thumb bit), so &fn and indirect BLX reach Thumb state;
+ * direct-branch relocs mask it off (S & ~1). */
+ {
+ u32 end = mc_pos(mc);
+ ObjSecId sec = a->func->text_section_id;
+ obj_symbol_define(t->obj, a->func->sym, sec, (u64)a->func_start | 1u,
+ (u64)(end - a->func_start));
+ if (a->func->atomize)
+ obj_atom_define(t->obj, sec, a->func_start, end - a->func_start,
+ a->func->sym, 0);
+ if (mc->debug) debug_func_pc_range(mc->debug, sec, a->func_start, end);
+ }
+
+ mc_cfi_endproc(mc);
+ mc_end_function(mc);
+ a->func = NULL;
+}
+
+/* ============================ param binding ============================ */
+
+static void arm_bind_native_param(NativeTarget* t, const CGParamDesc* p,
+ NativeLoc dst) {
+ Arm32NativeTarget* a = arm_of(t);
+ const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, a->func->fn_type);
+ const ABIArgInfo* ai = p->index < abi->nparams ? &abi->params[p->index] : NULL;
+ int to_reg = dst.kind == NATIVE_LOC_REG;
+ u32 i;
+ if (!ai || ai->kind == ABI_ARG_IGNORE) return;
+ if (ai->kind == ABI_ARG_INDIRECT)
+ arm_panic(a, "indirect (by-ref aggregate) params are Phase 2");
+ for (i = 0; i < ai->nparts; ++i) {
+ const ABIArgPart* part = &ai->parts[i];
+ NativeLoc src;
+ if (a->next_param_int < 4u) {
+ src = native_loc_reg(p->type, NATIVE_REG_INT, (Reg)(a->next_param_int++));
+ } else {
+ /* Incoming stack arg: above the saved {r7,lr} pair = [r7 + 8 + k]. */
+ NativeAddr sa;
+ src = native_loc_reg(p->type, NATIVE_REG_INT, ARM_SCRATCH);
+ memset(&sa, 0, sizeof sa);
+ sa.base_kind = NATIVE_ADDR_BASE_REG;
+ sa.base.reg = ARM_FP;
+ sa.base_type = p->type;
+ sa.offset = (i32)(8u + a->next_param_stack);
+ arm_emit_mem(a, 1, src, sa, native_mem_for_type(t, p->type, part->size));
+ a->next_param_stack += 4u;
+ }
+ if (dst.kind == NATIVE_LOC_NONE) {
+ /* unused parameter; cursor already advanced */
+ } else if (to_reg) {
+ NativeLoc d = native_loc_reg(dst.type ? dst.type : p->type,
+ (NativeAllocClass)dst.cls, (Reg)dst.v.reg);
+ if (!(src.kind == NATIVE_LOC_REG && loc_reg(src) == loc_reg(d)))
+ arm_move(t, d, src);
+ } else {
+ NativeAddr home;
+ memset(&home, 0, sizeof home);
+ home.base_kind = NATIVE_ADDR_BASE_FRAME;
+ home.base.frame = dst.v.frame;
+ home.base_type = p->type;
+ home.offset = (i32)part->src_offset;
+ arm_emit_mem(a, 0, src, home, native_mem_for_type(t, p->type, part->size));
+ }
+ }
+}
+
+/* ============================ calls / returns ============================ */
+
+/* Outgoing stack-argument bytes for a call: int parts beyond r0..r3. */
+static u32 arm_call_stack_size(NativeTarget* t, const NativeCallDesc* desc) {
+ const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type);
+ u32 next_int = (abi && abi->has_sret) ? 1u : 0u;
+ u32 stack = 0, i, p;
+ if (!abi) return 0;
+ for (i = 0; i < desc->nargs && i < abi->nparams; ++i) {
+ const ABIArgInfo* ai = &abi->params[i];
+ if (ai->kind == ABI_ARG_IGNORE) continue;
+ if (ai->kind == ABI_ARG_INDIRECT) {
+ if (next_int < 4u) next_int++;
+ else stack += 4u;
+ continue;
+ }
+ for (p = 0; p < ai->nparts; ++p) {
+ if (next_int < 4u) next_int++;
+ else stack += 4u;
+ }
+ }
+ return align_up_u32(stack, 8u);
+}
+
+static u32 arm_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type,
+ int* variadic, u32* nparams) {
+ const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fn_type);
+ u32 next_int = (abi && abi->has_sret) ? 1u : 0u;
+ u32 stack = 0, i, p;
+ if (variadic) *variadic = abi && abi->variadic;
+ if (nparams) *nparams = abi ? abi->nparams : 0u;
+ if (!abi) return 0;
+ for (i = 0; i < abi->nparams; ++i) {
+ const ABIArgInfo* ai = &abi->params[i];
+ if (ai->kind == ABI_ARG_IGNORE) continue;
+ if (ai->kind == ABI_ARG_INDIRECT) {
+ if (next_int < 4u) next_int++;
+ else stack += 4u;
+ continue;
+ }
+ for (p = 0; p < ai->nparts; ++p) {
+ if (next_int < 4u) next_int++;
+ else stack += 4u;
+ }
+ }
+ return align_up_u32(stack, 8u);
+}
+
+static u32 arm_call_stack_bytes(NativeTarget* t, const NativeCallDesc* desc) {
+ return arm_call_stack_size(t, desc);
+}
+
+static void arm_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src,
+ u32 src_offset, u32 size) {
+ /* Load `size` bytes at src_offset of `src` into register `dst`. */
+ if (src.kind == NATIVE_LOC_REG) {
+ arm_move(t, dst, src);
+ 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 {
+ arm_panic(arm_of(t), "unsupported arg source location");
+ }
+ addr.base_type = dst.type;
+ memset(&mem, 0, sizeof mem);
+ mem.type = dst.type;
+ mem.size = size;
+ arm_emit_mem(arm_of(t), 1, dst, addr, mem);
+ }
+}
+
+static void arm_store_outgoing(NativeTarget* t, u32 stack_off, NativeLoc src,
+ u32 size) {
+ NativeAddr addr;
+ MemAccess mem;
+ memset(&addr, 0, sizeof addr);
+ addr.base_kind = NATIVE_ADDR_BASE_REG;
+ addr.base.reg = 13u; /* sp */
+ addr.offset = (i32)stack_off;
+ addr.base_type = src.type;
+ memset(&mem, 0, sizeof mem);
+ mem.type = src.type;
+ mem.size = size;
+ arm_emit_mem(arm_of(t), 0, src, addr, mem);
+}
+
+static void arm_emit_one_arg_move(NativeTarget* t, const NativeArgMove* m) {
+ if (m->is_addr)
+ arm_load_addr(t, m->dst, m->src.v.addr); /* unused in smoke set */
+ else
+ arm_load_part(t, m->dst, m->src, m->src_offset, m->size);
+}
+
+static void arm_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves,
+ u32 n) {
+ NativeArgShuffle s;
+ if (n > ARM_MAX_REG_ARG_MOVES) arm_panic(arm_of(t), "too many register args");
+ memset(&s, 0, sizeof s);
+ s.t = t;
+ s.emit_one = arm_emit_one_arg_move;
+ s.reg_move = arm_move;
+ s.scratch[NATIVE_REG_INT] = ARM_SCRATCH;
+ native_arg_shuffle(&s, moves, n);
+}
+
+static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc,
+ NativeCallPlan* plan) {
+ Arm32NativeTarget* a = arm_of(t);
+ const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type);
+ NativeCallPlanRet* rets;
+ u32 nrets_cap = (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults)
+ ? abi->ret.nparts
+ : ((!abi && desc->nresults) ? 1u : 0u);
+ memset(plan, 0, sizeof *plan);
+ rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPlanRet, nrets_cap) : NULL;
+ plan->callee = desc->callee;
+ plan->rets = rets;
+ plan->flags = desc->flags;
+ plan->has_sret = abi && abi->has_sret;
+ plan->is_variadic = abi && abi->variadic;
+ plan->stack_arg_size = arm_call_stack_size(t, desc);
+ if (plan->stack_arg_size > a->frame.max_outgoing)
+ a->frame.max_outgoing = plan->stack_arg_size;
+
+ /* Stage an indirect callee out of the arg registers (arg loads clobber them). */
+ if (plan->callee.kind == NATIVE_LOC_REG &&
+ (NativeAllocClass)plan->callee.cls == NATIVE_REG_INT &&
+ plan->callee.v.reg <= 3u) {
+ NativeLoc scratch =
+ native_loc_reg(plan->callee.type, NATIVE_REG_INT, ARM_SCRATCH);
+ arm_move(t, scratch, plan->callee);
+ plan->callee = scratch;
+ }
+ {
+ u32 next_int = (abi && abi->has_sret) ? 1u : 0u;
+ u32 stack = 0, nmoves = 0, i, p;
+ NativeArgMove moves[ARM_MAX_REG_ARG_MOVES];
+ KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
+ for (i = 0; i < desc->nargs; ++i) {
+ const ABIArgInfo* ai = i < abi->nparams ? &abi->params[i] : NULL;
+ if (!ai || ai->kind == ABI_ARG_IGNORE) continue;
+ if (ai->kind == ABI_ARG_INDIRECT)
+ arm_panic(a, "indirect (by-ref aggregate) args are Phase 2");
+ for (p = 0; p < ai->nparts; ++p) {
+ const ABIArgPart* part = &ai->parts[p];
+ if (next_int < 4u) {
+ NativeArgMove* m = &moves[nmoves++];
+ m->dst = native_loc_reg(desc->args[i].type, NATIVE_REG_INT,
+ (Reg)(next_int++));
+ m->src = desc->args[i];
+ m->src_offset = part->src_offset;
+ m->size = part->size;
+ m->is_addr = 0;
+ } else {
+ NativeLoc tmp = native_loc_reg(desc->args[i].type, NATIVE_REG_INT,
+ ARM_SCRATCH);
+ arm_load_part(t, tmp, desc->args[i], part->src_offset, part->size);
+ stack = align_up_u32(stack, 4u);
+ arm_store_outgoing(t, stack, tmp, part->size);
+ stack += 4u;
+ }
+ }
+ }
+ arm_emit_reg_arg_moves(t, moves, nmoves);
+ if (abi && abi->has_sret && desc->nresults) {
+ NativeLoc r0 = native_loc_reg(i32t, NATIVE_REG_INT, ARM_R0);
+ arm_load_addr(t, r0, (NativeAddr){.base_kind = NATIVE_ADDR_BASE_FRAME,
+ .base.frame = desc->results[0].v.frame,
+ .base_type = i32t});
+ }
+ }
+ if (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) {
+ u32 nr = 0, ni = 0, p;
+ for (p = 0; p < abi->ret.nparts; ++p) {
+ const ABIArgPart* part = &abi->ret.parts[p];
+ KitCgTypeId pty = desc->results[0].type;
+ rets[nr].src = native_loc_reg(pty, NATIVE_REG_INT, (Reg)(ni++));
+ rets[nr].dst = desc->results[0];
+ if (rets[nr].dst.kind == NATIVE_LOC_FRAME)
+ rets[nr].dst = native_loc_stack(pty, desc->results[0].v.frame,
+ (i32)part->src_offset);
+ rets[nr].mem = native_mem_for_type(t, pty, part->size);
+ nr++;
+ }
+ plan->nrets = nr;
+ } else if (!abi && desc->nresults) {
+ rets[0].src = native_loc_reg(desc->results[0].type, NATIVE_REG_INT, ARM_R0);
+ rets[0].dst = desc->results[0];
+ rets[0].mem = native_mem_for_type(t, desc->results[0].type, 0);
+ plan->nrets = 1;
+ }
+}
+
+static void arm_emit_call(NativeTarget* t, const NativeCallPlan* plan) {
+ Arm32NativeTarget* a = arm_of(t);
+ MCEmitter* mc = t->mc;
+ if (plan->flags & CG_CALL_TAIL)
+ arm_panic(a, "tail calls are Phase 2");
+ if (plan->callee.kind == NATIVE_LOC_GLOBAL) {
+ u32 pos = mc_pos(mc);
+ arm_emit_t32(mc, arm_bl());
+ mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_CALL,
+ plan->callee.v.global.sym, plan->callee.v.global.addend, 0,
+ 0);
+ return;
+ }
+ if (plan->callee.kind == NATIVE_LOC_REG) {
+ arm_emit_t16(mc, arm_blx_reg(loc_reg(plan->callee)));
+ return;
+ }
+ arm_panic(a, "unsupported call target");
+}
+
+static void arm_plan_ret(NativeTarget* t, const CGFuncDesc* fd,
+ const NativeLoc* value, NativeCallPlanRet** out_rets,
+ u32* out_nrets) {
+ Arm32NativeTarget* a = arm_of(t);
+ const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type);
+ NativeCallPlanRet* rets = NULL;
+ u32 nr = 0;
+ if (value) rets = arena_zarray(t->c->tu, NativeCallPlanRet, 4);
+ if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) {
+ arm_panic(a, "indirect (by-ref aggregate) return is Phase 2");
+ }
+ if (value && abi && abi->ret.kind == ABI_ARG_DIRECT) {
+ u32 ni = 0, p;
+ for (p = 0; p < abi->ret.nparts; ++p) {
+ const ABIArgPart* part = &abi->ret.parts[p];
+ KitCgTypeId pty = value->type;
+ 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);
+ rets[nr].dst = native_loc_reg(pty, NATIVE_REG_INT, (Reg)(ni++));
+ rets[nr].mem = native_mem_for_type(t, pty, part->size);
+ nr++;
+ }
+ } else if (value) {
+ rets[0].src = *value;
+ rets[0].dst = native_loc_reg(value->type, NATIVE_REG_INT, ARM_R0);
+ rets[0].mem = native_mem_for_type(t, value->type, 0);
+ nr = 1;
+ }
+ *out_rets = rets;
+ *out_nrets = nr;
+}
+
+static void arm_ret(NativeTarget* t) {
+ Arm32NativeTarget* a = arm_of(t);
+ arm_jump(t, a->epilogue_label);
+}
+
+/* ============================ misc / stubs ============================ */
+
+static void arm_trap(NativeTarget* t) { arm_emit_t16(t->mc, arm_bkpt(0u)); }
+static void arm_set_loc(NativeTarget* t, SrcLoc loc) {
+ arm_of(t)->loc = loc;
+ mc_set_loc(t->mc, loc);
+}
+
+#define ARM_UNIMPL(name) arm_panic(arm_of(t), name " not implemented in Phase 1")
+
+static void arm_load_const(NativeTarget* t, NativeLoc dst, ConstBytes c) {
+ (void)dst;
+ (void)c;
+ ARM_UNIMPL("load_const");
+}
+static void arm_load_label_addr(NativeTarget* t, NativeLoc dst, MCLabel l) {
+ (void)dst;
+ (void)l;
+ ARM_UNIMPL("load_label_addr");
+}
+static void arm_indirect_branch(NativeTarget* t, NativeLoc addr,
+ const MCLabel* targets, u32 n) {
+ (void)addr;
+ (void)targets;
+ (void)n;
+ ARM_UNIMPL("indirect_branch (switch table)");
+}
+static void arm_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym,
+ i64 addend) {
+ (void)dst;
+ (void)sym;
+ (void)addend;
+ ARM_UNIMPL("tls_addr_of");
+}
+static void arm_copy_bytes(NativeTarget* t, NativeAddr dst, NativeAddr src,
+ AggregateAccess acc) {
+ (void)dst;
+ (void)src;
+ (void)acc;
+ ARM_UNIMPL("copy_bytes (aggregate)");
+}
+static void arm_set_bytes(NativeTarget* t, NativeAddr dst, NativeLoc v,
+ AggregateAccess acc) {
+ (void)dst;
+ (void)v;
+ (void)acc;
+ ARM_UNIMPL("set_bytes (memset)");
+}
+static void arm_bitfield_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
+ BitFieldAccess bf) {
+ (void)dst;
+ (void)addr;
+ (void)bf;
+ ARM_UNIMPL("bitfield_load");
+}
+static void arm_bitfield_store(NativeTarget* t, NativeAddr addr, NativeLoc v,
+ BitFieldAccess bf) {
+ (void)addr;
+ (void)v;
+ (void)bf;
+ ARM_UNIMPL("bitfield_store");
+}
+static void arm_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size,
+ u32 align) {
+ (void)dst;
+ (void)size;
+ (void)align;
+ ARM_UNIMPL("alloca");
+}
+static void arm_atomic_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
+ MemAccess mem, KitCgMemOrder order) {
+ (void)dst;
+ (void)addr;
+ (void)mem;
+ (void)order;
+ ARM_UNIMPL("atomic_load");
+}
+static void arm_atomic_store(NativeTarget* t, NativeAddr addr, NativeLoc v,
+ MemAccess mem, KitCgMemOrder order) {
+ (void)addr;
+ (void)v;
+ (void)mem;
+ (void)order;
+ ARM_UNIMPL("atomic_store");
+}
+static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
+ const NativeLoc* dsts, u32 ndst, const NativeLoc* args,
+ u32 narg) {
+ (void)dsts;
+ (void)ndst;
+ (void)args;
+ (void)narg;
+ if (kind == INTRIN_TRAP) {
+ arm_trap(t);
+ return;
+ }
+ ARM_UNIMPL("intrinsic");
+}
+
+/* ============================ construction ============================ */
+
+NativeTarget* arm32_native_target_new(Compiler* c, ObjBuilder* obj,
+ MCEmitter* mc) {
+ Arm32NativeTarget* a = arena_znew(c->tu, Arm32NativeTarget);
+ NativeTarget* t;
+ if (!a) return NULL;
+ t = &a->base;
+ t->c = c;
+ t->obj = obj;
+ t->mc = mc;
+ a->variant = arm32_variant_for_kind(c->target.arch);
+ native_frame_init(&a->frame, c);
+ t->regs = &arm_reg_info;
+ t->class_for_type = native_class_for_type_fp_le8;
+ t->imm_legal = arm_imm_legal;
+ t->addr_legal = arm_addr_legal;
+ t->func_begin = arm_func_begin;
+ t->func_begin_known_frame = arm_func_begin_known_frame;
+ t->reserve_callee_saves = arm_reserve_callee_saves;
+ t->signature_stack_bytes = arm_signature_stack_bytes;
+ t->call_stack_bytes = arm_call_stack_bytes;
+ t->has_store_zero_reg = 0; /* ARM has no zero register */
+ t->func_end = arm_func_end;
+ t->frame_slot = arm_frame_slot;
+ t->release_frame_slot = arm_release_frame_slot;
+ t->frame_slot_debug_loc = arm_frame_slot_debug_loc;
+ t->bind_param = arm_bind_native_param;
+ t->label_new = arm_label_new;
+ t->label_place = arm_label_place;
+ t->jump = arm_jump;
+ t->cmp_branch = arm_cmp_branch;
+ t->indirect_branch = arm_indirect_branch;
+ t->load_label_addr = arm_load_label_addr;
+ t->move = arm_move;
+ t->load_imm = arm_load_imm;
+ t->load_const = arm_load_const;
+ t->load_addr = arm_load_addr;
+ t->load = arm_load;
+ t->store = arm_store;
+ t->tls_addr_of = arm_tls_addr_of;
+ t->copy_bytes = arm_copy_bytes;
+ t->set_bytes = arm_set_bytes;
+ t->bitfield_load = arm_bitfield_load;
+ t->bitfield_store = arm_bitfield_store;
+ t->binop = arm_binop;
+ t->unop = arm_unop;
+ t->cmp = arm_cmp;
+ t->convert = arm_convert;
+ t->binop_rr = arm_binop_rr;
+ t->move_rr = arm_move_rr;
+ t->cmp_rr = arm_cmp_rr;
+ t->convert_rr = arm_convert_rr;
+ t->alloca_ = arm_alloca;
+ t->spill = arm_spill;
+ t->reload = arm_reload;
+ t->plan_call = arm_plan_call;
+ t->emit_call = arm_emit_call;
+ t->plan_ret = arm_plan_ret;
+ t->ret = arm_ret;
+ t->atomic_load = arm_atomic_load;
+ t->atomic_store = arm_atomic_store;
+ t->intrinsic = arm_intrinsic;
+ t->file_scope_asm = native_file_scope_asm;
+ t->trap = arm_trap;
+ t->set_loc = arm_set_loc;
+ t->finalize = native_finalize;
+ return t;
+}
+
+/* ============================ NativeOps (-O0) ============================ */
+
+static void arm_bind_param(NativeDirectTarget* d, const CGParamDesc* p,
+ CGLocal local, NativeDirectLocal* l) {
+ NativeLoc dst;
+ (void)local;
+ memset(&dst, 0, sizeof dst);
+ dst.kind = NATIVE_LOC_FRAME;
+ dst.type = p->type;
+ dst.v.frame = l->home;
+ arm_bind_native_param(d->native, p, dst);
+}
+
+static const char* arm_no_tail(NativeDirectTarget* d, const CGCallDesc* call) {
+ (void)d;
+ (void)call;
+ return "arm32 tail calls not implemented in Phase 1";
+}
+
+static const NativeOps arm_direct_ops = {
+ .bind_param = arm_bind_param,
+ .tail_call_unrealizable_reason = arm_no_tail,
+};
+
+const NativeOps* arm32_native_direct_ops(void) { return &arm_direct_ops; }
diff --git a/src/arch/arm32/regs.c b/src/arch/arm32/regs.c
@@ -0,0 +1,101 @@
+/* ARM32 (AArch32) register name table -- DWARF index <-> AAPCS name.
+ *
+ * "DWARF for the ARM Architecture" numbers the core registers r0..r15 as
+ * 0..15 and the VFP single-precision registers s0..s31 as 64..95. Canonical
+ * names are rN; sp/lr/pc/ip/fp aliases are accepted by lookup. v1 is
+ * soft-float, but the s-registers are listed for disassembly/debug. */
+
+#include "arch/arm32/regs.h"
+
+#include <stdint.h>
+
+#include "core/core.h"
+#include "core/slice.h"
+
+typedef struct Arm32Reg {
+ uint32_t dwarf_idx;
+ const char* name;
+} Arm32Reg;
+
+static const Arm32Reg ARM32_REGS[] = {
+ {0, "r0"}, {1, "r1"}, {2, "r2"}, {3, "r3"}, {4, "r4"},
+ {5, "r5"}, {6, "r6"}, {7, "r7"}, {8, "r8"}, {9, "r9"},
+ {10, "r10"}, {11, "r11"}, {12, "r12"}, {13, "sp"}, {14, "lr"},
+ {15, "pc"},
+
+ {64, "s0"}, {65, "s1"}, {66, "s2"}, {67, "s3"}, {68, "s4"},
+ {69, "s5"}, {70, "s6"}, {71, "s7"}, {72, "s8"}, {73, "s9"},
+ {74, "s10"}, {75, "s11"}, {76, "s12"}, {77, "s13"}, {78, "s14"},
+ {79, "s15"}, {80, "s16"}, {81, "s17"}, {82, "s18"}, {83, "s19"},
+ {84, "s20"}, {85, "s21"}, {86, "s22"}, {87, "s23"}, {88, "s24"},
+ {89, "s25"}, {90, "s26"}, {91, "s27"}, {92, "s28"}, {93, "s29"},
+ {94, "s30"}, {95, "s31"},
+};
+
+static const uint32_t ARM32_REGS_N =
+ (uint32_t)(sizeof ARM32_REGS / sizeof ARM32_REGS[0]);
+
+static int parse_num_suffix(const char* name, char prefix, uint32_t max,
+ uint32_t* out) {
+ uint32_t v = 0;
+ const char* p;
+ if (!name || name[0] != prefix || name[1] == '\0') return 1;
+ p = name + 1;
+ while (*p) {
+ if (*p < '0' || *p > '9') return 1;
+ v = v * 10u + (uint32_t)(*p - '0');
+ if (v > max) return 1;
+ ++p;
+ }
+ if (out) *out = v;
+ return 0;
+}
+
+const char* arm32_register_name(uint32_t dwarf_idx) {
+ uint32_t i;
+ for (i = 0; i < ARM32_REGS_N; ++i) {
+ if (ARM32_REGS[i].dwarf_idx == dwarf_idx) return ARM32_REGS[i].name;
+ }
+ return NULL;
+}
+
+int arm32_register_index(const char* name, uint32_t* idx_out) {
+ uint32_t i;
+ uint32_t n;
+ Slice q;
+ if (!name) return 1;
+ q = slice_from_cstr(name);
+ for (i = 0; i < ARM32_REGS_N; ++i) {
+ if (slice_eq_cstr(q, ARM32_REGS[i].name)) {
+ if (idx_out) *idx_out = ARM32_REGS[i].dwarf_idx;
+ return 0;
+ }
+ }
+ if (!parse_num_suffix(name, 'r', 15, &n)) {
+ if (idx_out) *idx_out = n;
+ return 0;
+ }
+ if (!parse_num_suffix(name, 's', 31, &n)) {
+ if (idx_out) *idx_out = 64u + n;
+ return 0;
+ }
+ if (slice_eq_cstr(q, "ip")) { /* r12 */
+ if (idx_out) *idx_out = 12u;
+ return 0;
+ }
+ if (slice_eq_cstr(q, "fp")) { /* r11 (Thumb FP) */
+ if (idx_out) *idx_out = 11u;
+ return 0;
+ }
+ return 1;
+}
+
+uint32_t arm32_register_iter_size(void) { return ARM32_REGS_N; }
+
+int arm32_register_iter_get(uint32_t i, uint32_t* dwarf_out,
+ const char** name_out) {
+ if (i >= ARM32_REGS_N) return 1;
+ if (dwarf_out) *dwarf_out = ARM32_REGS[i].dwarf_idx;
+ if (name_out) *name_out = ARM32_REGS[i].name;
+ return 0;
+}
diff --git a/src/arch/arm32/regs.h b/src/arch/arm32/regs.h
@@ -0,0 +1,12 @@
+#ifndef KIT_ARCH_ARM32_REGS_H
+#define KIT_ARCH_ARM32_REGS_H
+
+#include <stdint.h>
+
+const char* arm32_register_name(uint32_t dwarf_idx);
+int arm32_register_index(const char* name, uint32_t* idx_out);
+uint32_t arm32_register_iter_size(void);
+int arm32_register_iter_get(uint32_t i, uint32_t* dwarf_out,
+ const char** name_out);
+
+#endif
diff --git a/src/arch/arm32/reloc.c b/src/arch/arm32/reloc.c
@@ -0,0 +1,83 @@
+/* ARM32 (Thumb-2) relocation descriptors (width + classification) + the
+ * split-immediate instruction byte patcher.
+ *
+ * Reached through LinkArchDesc.reloc_desc / .reloc_apply_insn (wired in
+ * link.c) and the arch-aware reloc_desc() / link_reloc_apply() dispatchers.
+ * Wire encoding + diagnostic name live in src/obj/elf/reloc_arm.c.
+ *
+ * Data words (the vector table, data pointers) use the neutral R_ABS32 /
+ * R_REL32 kinds, claimed by reloc_apply_neutral before this hook runs — so
+ * only the instruction-embedded Thumb-2 branch kinds appear here.
+ *
+ * Thumb-2 byte order: a 32-bit instruction is two independently-little-endian
+ * 16-bit half-words, the HIGH half-word FIRST in memory. rd_u32_le/wr_u32_le
+ * would swap them, so arm_rd_t32/arm_wr_t32 read/write half-word-pairs. */
+
+#include "obj/reloc.h"
+
+#include "core/bytes.h"
+#include "core/core.h"
+#include "link/link_arch.h"
+#include "obj/obj.h"
+
+static const RelocDescRow arm32_rows[] = {
+ {R_ARM_THM_CALL, {4, RELOC_IS_BRANCH}}, /* BL (T1), +-16 MiB */
+ {R_ARM_THM_JUMP24, {4, RELOC_IS_BRANCH}}, /* B.W (T4), +-16 MiB */
+};
+
+const RelocDesc* arm32_reloc_desc(RelocKind k) {
+ return reloc_desc_row_find(arm32_rows,
+ (u32)(sizeof arm32_rows / sizeof arm32_rows[0]), k);
+}
+
+/* Read/write a 32-bit Thumb-2 instruction stored as two little-endian
+ * half-words, HIGH half-word first in memory (the Thumb byte order). The
+ * returned/accepted 32-bit value has hw1 in the high 16 bits, hw2 in the low
+ * 16 bits. */
+static u32 arm_rd_t32(const u8* p) {
+ return ((u32)rd_u16_le(p) << 16) | (u32)rd_u16_le(p + 2);
+}
+static void arm_wr_t32(u8* p, u32 instr) {
+ wr_u16_le(p, (u16)(instr >> 16));
+ wr_u16_le(p + 2, (u16)(instr & 0xffffu));
+}
+
+int arm32_reloc_apply_insn(Compiler* c, RelocKind k, u8* P_bytes, u64 S, i64 A,
+ u64 P) {
+ switch (k) {
+ case R_ARM_THM_CALL:
+ case R_ARM_THM_JUMP24: {
+ /* BL/B.W T1/T4 25-bit field {S,I1,I2,imm10,imm11}, in 2-byte units:
+ * instr[26]=Sbit, instr[25:16]=imm10, instr[13]=J1, instr[11]=J2,
+ * instr[10:0]=imm11, with I1=NOT(J1^S), I2=NOT(J2^S).
+ * Thumb-only on Cortex-M: BL never relaxes to BLX, so the callee's
+ * Thumb bit (S|1) is masked out of the displacement. The hardware PC
+ * base is the instruction address + 4 (Thumb pipeline), so the encoded
+ * byte offset is (S&~1) + A - P - 4. (The field is overwritten; a
+ * non-zero in-field addend, i.e. `bl sym+N`, is a Phase-2 refinement.) */
+ i64 disp = (i64)(S & ~(u64)1) + A - (i64)P - 4;
+ u32 instr, sbit, i1, i2, j1, j2, imm10, imm11;
+ if (disp & 1)
+ compiler_panic(c, SRCLOC_NONE,
+ "link: THM_CALL misaligned displacement");
+ if (disp < -(i64)(1 << 24) || disp >= (i64)(1 << 24))
+ compiler_panic(c, SRCLOC_NONE,
+ "link: THM_CALL out of range (need +-16MiB)");
+ sbit = (u32)((disp >> 24) & 1u);
+ i1 = (u32)((disp >> 23) & 1u);
+ i2 = (u32)((disp >> 22) & 1u);
+ imm10 = (u32)((disp >> 12) & 0x3ffu);
+ imm11 = (u32)((disp >> 1) & 0x7ffu);
+ j1 = (~(i1 ^ sbit)) & 1u; /* I1 = NOT(J1^S) -> J1 = NOT(I1^S) */
+ j2 = (~(i2 ^ sbit)) & 1u;
+ instr = arm_rd_t32(P_bytes);
+ instr &= 0xf800d000u; /* keep opcode (11110), hw2[15:14] (BL/B), hw2[12] */
+ instr |= (sbit << 26) | (imm10 << 16);
+ instr |= (j1 << 13) | (j2 << 11) | imm11;
+ arm_wr_t32(P_bytes, instr);
+ return 1;
+ }
+ default:
+ return 0;
+ }
+}
diff --git a/src/arch/arm32/variant.c b/src/arch/arm32/variant.c
@@ -0,0 +1,18 @@
+/* The single immutable ARM32 variant. See variant.h for the contract. */
+#include "arch/arm32/variant.h"
+
+const Arm32Variant arm32_variant_v7m = {
+ .kind = KIT_ARCH_ARM_32,
+ .name = "arm32",
+ .profile = "armv7-m",
+ .ptr_bytes = 4u,
+ .has_thumb2 = 1u,
+ .has_idiv = 1u,
+ .has_dsp = 0u,
+ .frame_save_size = 8u, /* 2 * ptr_bytes */
+};
+
+const Arm32Variant* arm32_variant_for_kind(KitArchKind kind) {
+ (void)kind;
+ return &arm32_variant_v7m;
+}
diff --git a/src/arch/arm32/variant.h b/src/arch/arm32/variant.h
@@ -0,0 +1,29 @@
+/* ARM32 backend variant descriptor — an immutable per-profile table threaded
+ * through the otherwise stateless backend so a single backend can later serve
+ * ARMv7-M and (future) ARMv6-M / A-profile variants. It is always reached
+ * through a context (Arm32NativeTarget.variant) or arm32_variant_for_kind(),
+ * never as ambient global state (CLAUDE.md: hang off a context struct).
+ *
+ * v1 ships exactly one variant: ARMv7-M Thumb-2, soft-float. */
+#ifndef KIT_ARCH_ARM32_VARIANT_H
+#define KIT_ARCH_ARM32_VARIANT_H
+
+#include "core/core.h"
+
+typedef struct Arm32Variant {
+ KitArchKind kind; /* KIT_ARCH_ARM_32 */
+ const char* name; /* "arm32" */
+ const char* profile; /* "armv7-m" / (future) "armv7e-m" / "armv6-m" */
+ u8 ptr_bytes; /* 4 */
+ u8 has_thumb2; /* 1 on v7-M (32-bit Thumb-2, MOVW/MOVT, IT) */
+ u8 has_idiv; /* 1 on v7-M (hardware SDIV/UDIV) */
+ u8 has_dsp; /* 0 on v7-M, 1 on v7E-M (saturating/SIMD; follow-on) */
+ u32 frame_save_size; /* saved lr + fp pair = 2 * ptr_bytes */
+} Arm32Variant;
+
+extern const Arm32Variant arm32_variant_v7m;
+
+/* Returns the variant for KIT_ARCH_ARM_32. v1 has a single variant. */
+const Arm32Variant* arm32_variant_for_kind(KitArchKind kind);
+
+#endif
diff --git a/src/arch/dbg_stubs.c b/src/arch/dbg_stubs.c
@@ -2,5 +2,6 @@
const ArchDbgOps aa64_dbg_ops = {0};
const ArchDbgOps x64_dbg_ops = {0};
+const ArchDbgOps arm32_dbg_ops = {0};
const ArchDbgOps rv64_dbg_ops = {0};
const ArchDbgOps rv32_dbg_ops = {0};
diff --git a/src/arch/disasm_stubs.c b/src/arch/disasm_stubs.c
@@ -15,4 +15,9 @@ ArchDisasm* rv64_disasm_new(Compiler* c) {
return NULL;
}
+ArchDisasm* arm32_disasm_new(Compiler* c) {
+ (void)c;
+ return NULL;
+}
+
const ArchDecodeOps rv64_decode_ops = {0};
diff --git a/src/arch/dwarf.c b/src/arch/dwarf.c
@@ -14,3 +14,9 @@ const ArchDwarfOps rv64_dwarf_ops = {
.min_inst_len = 4u,
.max_ops_per_inst = 1u,
};
+
+/* Thumb-2 mixes 16- and 32-bit instructions; the minimum width is 2. */
+const ArchDwarfOps arm32_dwarf_ops = {
+ .min_inst_len = 2u,
+ .max_ops_per_inst = 1u,
+};
diff --git a/src/arch/link_stubs.c b/src/arch/link_stubs.c
@@ -2,5 +2,6 @@
const LinkArchDesc link_arch_aa64 = {0};
const LinkArchDesc link_arch_x64 = {0};
+const LinkArchDesc link_arch_arm32 = {0};
const LinkArchDesc link_arch_rv64 = {0};
const LinkArchDesc link_arch_rv32 = {0};
diff --git a/src/arch/registry.c b/src/arch/registry.c
@@ -30,6 +30,9 @@ extern const ArchImpl arch_impl_rv64;
#if KIT_ARCH_X64_ENABLED
extern const ArchImpl arch_impl_x64;
#endif
+#if KIT_ARCH_ARM32_ENABLED
+extern const ArchImpl arch_impl_arm32;
+#endif
#if KIT_ARCH_C_TARGET_ENABLED
extern const CGBackend cg_backend_c_target;
#endif
@@ -50,6 +53,9 @@ static const ArchImpl* const arch_impls[] = {
#if KIT_ARCH_X64_ENABLED
&arch_impl_x64,
#endif
+#if KIT_ARCH_ARM32_ENABLED
+ &arch_impl_arm32,
+#endif
#if KIT_ARCH_RV32_ENABLED
&arch_impl_rv32,
#endif
@@ -59,8 +65,9 @@ static const ArchImpl* const arch_impls[] = {
#if KIT_ARCH_WASM_ENABLED
&arch_impl_wasm,
#endif
-#if !KIT_ARCH_AA64_ENABLED && !KIT_ARCH_X64_ENABLED && \
- !KIT_ARCH_RV32_ENABLED && !KIT_ARCH_RV64_ENABLED && !KIT_ARCH_WASM_ENABLED
+#if !KIT_ARCH_AA64_ENABLED && !KIT_ARCH_X64_ENABLED && \
+ !KIT_ARCH_ARM32_ENABLED && !KIT_ARCH_RV32_ENABLED && \
+ !KIT_ARCH_RV64_ENABLED && !KIT_ARCH_WASM_ENABLED
NULL,
#endif
};
diff --git a/src/core/config_assert.c b/src/core/config_assert.c
@@ -4,8 +4,9 @@
#include "kit/config.h"
_Static_assert(KIT_ARCH_AA64_ENABLED + KIT_ARCH_X64_ENABLED +
- KIT_ARCH_RV32_ENABLED + KIT_ARCH_RV64_ENABLED +
- KIT_ARCH_WASM_ENABLED + KIT_ARCH_C_TARGET_ENABLED >=
+ KIT_ARCH_ARM32_ENABLED + KIT_ARCH_RV32_ENABLED +
+ KIT_ARCH_RV64_ENABLED + KIT_ARCH_WASM_ENABLED +
+ KIT_ARCH_C_TARGET_ENABLED >=
1,
"at least one backend architecture must be enabled");
@@ -19,6 +20,7 @@ _Static_assert(KIT_OBJ_ELF_ENABLED + KIT_OBJ_MACHO_ENABLED +
KIT_ASSERT_BOOL(KIT_ARCH_AA64_ENABLED);
KIT_ASSERT_BOOL(KIT_ARCH_X64_ENABLED);
+KIT_ASSERT_BOOL(KIT_ARCH_ARM32_ENABLED);
KIT_ASSERT_BOOL(KIT_ARCH_RV32_ENABLED);
KIT_ASSERT_BOOL(KIT_ARCH_RV64_ENABLED);
KIT_ASSERT_BOOL(KIT_ARCH_WASM_ENABLED);
diff --git a/src/obj/elf/elf.h b/src/obj/elf/elf.h
@@ -61,6 +61,7 @@
#define EM_X86_64 0x3E
#define EM_AARCH64 0xB7
#define EM_RISCV 0xF3
+#define EM_ARM 0x28
/* ---- header sizes (also literal e_*size fields) ----
* On-disk sizes of the ELF64 records the linker emits. Wire-format
@@ -207,6 +208,14 @@ static inline u8 elf_st_other(u8 vis /* SymVis */) {
#define PF_W 0x2u
#define PF_R 0x4u
+/* ---- e_flags (ARM EABI bits, EM_ARM) ----
+ * EABI version is the top byte; the float-ABI is two independent flag bits
+ * (a soft object sets SOFT, a hard object sets HARD, an old object neither). */
+#define EF_ARM_EABIMASK 0xFF000000u
+#define EF_ARM_EABI_VER5 0x05000000u
+#define EF_ARM_ABI_FLOAT_SOFT 0x00000200u
+#define EF_ARM_ABI_FLOAT_HARD 0x00000400u
+
/* ---- e_flags (RISC-V ABI bits, EM_RISCV) ---- */
#define EF_RISCV_RVC 0x0001u
#define EF_RISCV_FLOAT_ABI_SOFT 0x0000u
@@ -470,6 +479,33 @@ const char* elf_riscv_reloc_name(u32 elf_type);
KitFloatAbi elf_riscv_float_abi_from_e_flags(u32 e_flags);
u32 elf_riscv_float_abi_to_e_flags(KitFloatAbi abi);
+/* ---- ARM (32-bit, Thumb-2) relocation types (EM_ARM, ELFCLASS32) ----
+ *
+ * Canonical numbers from "ELF for the ARM Architecture" (ARM IHI 0044).
+ * Static-only subset: data words (ABS32/REL32/ABS16/ABS8) reuse the neutral
+ * RelocKinds; only the instruction-embedded Thumb-2 kinds get arch entries. */
+#define ELF_R_ARM_NONE 0
+#define ELF_R_ARM_ABS32 2
+#define ELF_R_ARM_REL32 3
+#define ELF_R_ARM_ABS16 5
+#define ELF_R_ARM_ABS8 8
+#define ELF_R_ARM_THM_CALL 10
+#define ELF_R_ARM_THM_JUMP24 30
+#define ELF_R_ARM_MOVW_ABS_NC 43
+#define ELF_R_ARM_MOVT_ABS 44
+#define ELF_R_ARM_THM_MOVW_ABS_NC 47
+#define ELF_R_ARM_THM_MOVT_ABS 48
+#define ELF_R_ARM_THM_MOVW_PREL_NC 49
+#define ELF_R_ARM_THM_MOVT_PREL 50
+#define ELF_R_ARM_THM_JUMP19 51
+#define ELF_R_ARM_TLS_LE32 108
+
+u32 elf_arm_reloc_to(u32 kind /* RelocKind */);
+u32 elf_arm_reloc_from(u32 elf_type);
+const char* elf_arm_reloc_name(u32 elf_type);
+KitFloatAbi elf_arm_float_abi_from_e_flags(u32 e_flags);
+u32 elf_arm_float_abi_to_e_flags(KitFloatAbi abi);
+
/* ---- little-endian byte writers (Writer-based) ----
* Writes go through the shared writer_u*_le helpers (core/bytes.h); the
* elf_wr_* aliases keep the ELF spelling at existing call sites. Reads
diff --git a/src/obj/elf/emit.c b/src/obj/elf/emit.c
@@ -653,6 +653,15 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) {
e_flags = (e_flags & ~(u32)EF_RISCV_FLOAT_ABI_MASK) | fa;
}
}
+ /* ARM: keep the EABI version (top byte) from the descriptor and override
+ * the two float-ABI flag bits from -mfloat-abi (float_abi). */
+ if (e_machine == EM_ARM) {
+ Compiler* ec = obj_compiler(ob);
+ u32 fa = ec ? elf_arm_float_abi_to_e_flags(ec->target.float_abi)
+ : EF_ARM_ABI_FLOAT_SOFT;
+ e_flags = (e_flags & ~(u32)(EF_ARM_ABI_FLOAT_SOFT | EF_ARM_ABI_FLOAT_HARD)) |
+ fa;
+ }
}
kit_writer_seek(w, 0);
diff --git a/src/obj/elf/reloc_arm.c b/src/obj/elf/reloc_arm.c
@@ -0,0 +1,130 @@
+/* RelocKind <-> ARM (Thumb-2) ELF reloc-type mapping (ELFCLASS32).
+ *
+ * Self-contained (ARM has no 64-bit ELF sibling). Data words reuse the neutral
+ * R_ABS32 / R_REL32 / R_ABS16 / R_ABS8 kinds; only the instruction-embedded
+ * Thumb-2 kinds get arch entries. The split-immediate byte patcher lives in
+ * src/arch/arm32/reloc.c. */
+#include "obj/elf/elf.h"
+#include "obj/obj.h"
+
+u32 elf_arm_reloc_to(u32 kind /* RelocKind */) {
+ switch (kind) {
+ case R_NONE:
+ return ELF_R_ARM_NONE;
+ case R_ABS32:
+ return ELF_R_ARM_ABS32;
+ case R_REL32:
+ case R_PC32:
+ return ELF_R_ARM_REL32; /* PC-rel data collapses to REL32 */
+ case R_ABS16:
+ return ELF_R_ARM_ABS16;
+ case R_ABS8:
+ return ELF_R_ARM_ABS8;
+ case R_ARM_THM_CALL:
+ return ELF_R_ARM_THM_CALL;
+ case R_ARM_THM_JUMP24:
+ return ELF_R_ARM_THM_JUMP24;
+ case R_ARM_THM_JUMP19:
+ return ELF_R_ARM_THM_JUMP19;
+ case R_ARM_THM_MOVW_ABS_NC:
+ return ELF_R_ARM_THM_MOVW_ABS_NC;
+ case R_ARM_THM_MOVT_ABS:
+ return ELF_R_ARM_THM_MOVT_ABS;
+ case R_ARM_THM_MOVW_PREL_NC:
+ return ELF_R_ARM_THM_MOVW_PREL_NC;
+ case R_ARM_THM_MOVT_PREL:
+ return ELF_R_ARM_THM_MOVT_PREL;
+ case R_ARM_TLS_LE32:
+ return ELF_R_ARM_TLS_LE32;
+ default:
+ return ELF_R_ARM_NONE;
+ }
+}
+
+u32 elf_arm_reloc_from(u32 elf_type) {
+ switch (elf_type) {
+ case ELF_R_ARM_NONE:
+ return R_NONE;
+ case ELF_R_ARM_ABS32:
+ return R_ABS32;
+ case ELF_R_ARM_REL32:
+ return R_REL32;
+ case ELF_R_ARM_ABS16:
+ return R_ABS16;
+ case ELF_R_ARM_ABS8:
+ return R_ABS8;
+ case ELF_R_ARM_THM_CALL:
+ return R_ARM_THM_CALL;
+ case ELF_R_ARM_THM_JUMP24:
+ return R_ARM_THM_JUMP24;
+ case ELF_R_ARM_THM_JUMP19:
+ return R_ARM_THM_JUMP19;
+ case ELF_R_ARM_THM_MOVW_ABS_NC:
+ return R_ARM_THM_MOVW_ABS_NC;
+ case ELF_R_ARM_THM_MOVT_ABS:
+ return R_ARM_THM_MOVT_ABS;
+ case ELF_R_ARM_THM_MOVW_PREL_NC:
+ return R_ARM_THM_MOVW_PREL_NC;
+ case ELF_R_ARM_THM_MOVT_PREL:
+ return R_ARM_THM_MOVT_PREL;
+ case ELF_R_ARM_TLS_LE32:
+ return R_ARM_TLS_LE32;
+ default:
+ return (u32)-1; /* reader diagnoses unsupported wire types */
+ }
+}
+
+const char* elf_arm_reloc_name(u32 elf_type) {
+ switch (elf_type) {
+ case ELF_R_ARM_NONE:
+ return "R_ARM_NONE";
+ case ELF_R_ARM_ABS32:
+ return "R_ARM_ABS32";
+ case ELF_R_ARM_REL32:
+ return "R_ARM_REL32";
+ case ELF_R_ARM_ABS16:
+ return "R_ARM_ABS16";
+ case ELF_R_ARM_ABS8:
+ return "R_ARM_ABS8";
+ case ELF_R_ARM_THM_CALL:
+ return "R_ARM_THM_CALL";
+ case ELF_R_ARM_THM_JUMP24:
+ return "R_ARM_THM_JUMP24";
+ case ELF_R_ARM_THM_JUMP19:
+ return "R_ARM_THM_JUMP19";
+ case ELF_R_ARM_THM_MOVW_ABS_NC:
+ return "R_ARM_THM_MOVW_ABS_NC";
+ case ELF_R_ARM_THM_MOVT_ABS:
+ return "R_ARM_THM_MOVT_ABS";
+ case ELF_R_ARM_THM_MOVW_PREL_NC:
+ return "R_ARM_THM_MOVW_PREL_NC";
+ case ELF_R_ARM_THM_MOVT_PREL:
+ return "R_ARM_THM_MOVT_PREL";
+ case ELF_R_ARM_TLS_LE32:
+ return "R_ARM_TLS_LE32";
+ default:
+ return NULL;
+ }
+}
+
+/* Decode the float ABI from ARM ELF e_flags (EF_ARM_ABI_FLOAT_*). The two
+ * flags are independent bits, not a mask field. */
+KitFloatAbi elf_arm_float_abi_from_e_flags(u32 e_flags) {
+ if (e_flags & EF_ARM_ABI_FLOAT_HARD) return KIT_FLOAT_ABI_SINGLE;
+ if (e_flags & EF_ARM_ABI_FLOAT_SOFT) return KIT_FLOAT_ABI_SOFT;
+ return KIT_FLOAT_ABI_DEFAULT;
+}
+
+u32 elf_arm_float_abi_to_e_flags(KitFloatAbi abi) {
+ /* Match the ARM toolchain convention (clang/gas): only hard-float objects
+ * set a float-ABI bit; soft/agnostic objects leave both unset (soft is the
+ * default). Setting EF_ARM_ABI_FLOAT_SOFT would make every kit object
+ * mismatch the ecosystem's soft objects under the linker's e_flags check. */
+ switch (abi) {
+ case KIT_FLOAT_ABI_SINGLE:
+ case KIT_FLOAT_ABI_DOUBLE:
+ return EF_ARM_ABI_FLOAT_HARD;
+ default:
+ return 0u; /* soft / agnostic: no float-ABI bit */
+ }
+}
diff --git a/src/obj/obj.c b/src/obj/obj.c
@@ -1474,6 +1474,14 @@ const char* reloc_kind_name(RelocKind k) {
_CASE(R_COFF_AARCH64_SECREL_LOW12A);
_CASE(R_COFF_AARCH64_SECREL_HIGH12A);
_CASE(R_COFF_AARCH64_SECREL_LOW12L);
+ _CASE(R_ARM_THM_CALL);
+ _CASE(R_ARM_THM_JUMP24);
+ _CASE(R_ARM_THM_JUMP19);
+ _CASE(R_ARM_THM_MOVW_ABS_NC);
+ _CASE(R_ARM_THM_MOVT_ABS);
+ _CASE(R_ARM_THM_MOVW_PREL_NC);
+ _CASE(R_ARM_THM_MOVT_PREL);
+ _CASE(R_ARM_TLS_LE32);
#undef _CASE
}
return "UNKNOWN";
diff --git a/src/obj/obj.h b/src/obj/obj.h
@@ -308,6 +308,19 @@ typedef enum RelocKind {
/* AArch64 Windows SECREL low-12 load/store form. Appended at the enum tail
* to keep existing public relocation values stable. */
R_COFF_AARCH64_SECREL_LOW12L,
+ /* 32-bit ARM (Thumb-2, M-profile) ELF relocations. Static-only, Thumb-only:
+ * BL never relaxes to BLX (everything is Thumb on Cortex-M). The split-
+ * immediate byte patcher lives in src/arch/arm32/reloc.c. Data words reuse
+ * the neutral R_ABS32/R_REL32/R_ABS16/R_ABS8 kinds. Appended at the enum
+ * tail to keep existing public relocation values stable. */
+ R_ARM_THM_CALL, /* BL/BLX (T1): 25-bit {S,I1,I2,imm10,imm11}, +-16MB */
+ R_ARM_THM_JUMP24, /* B.W (T4): same 25-bit field */
+ R_ARM_THM_JUMP19, /* B<cond>.W (T3): 21-bit {S,J2,J1,imm6,imm11}, +-1MB */
+ R_ARM_THM_MOVW_ABS_NC, /* MOVW (T3): imm16 = (S+A)[15:0], imm4:i:imm3:imm8 */
+ R_ARM_THM_MOVT_ABS, /* MOVT (T3): imm16 = (S+A)[31:16] */
+ R_ARM_THM_MOVW_PREL_NC, /* PC-rel MOVW (PIC; follow-on) */
+ R_ARM_THM_MOVT_PREL, /* PC-rel MOVT (PIC; follow-on) */
+ R_ARM_TLS_LE32, /* TLS local-exec: S + A - tp (variant I; follow-on) */
} RelocKind;
typedef struct Section {
diff --git a/src/obj/registry.c b/src/obj/registry.c
@@ -152,8 +152,32 @@ static const ObjElfArchOps obj_elf_arch_ops[] = {
.float_abi_from_e_flags = elf_riscv_float_abi_from_e_flags,
},
#endif
-#if !KIT_ARCH_AA64_ENABLED && !KIT_ARCH_X64_ENABLED && \
- !KIT_ARCH_RV64_ENABLED && !KIT_ARCH_RV32_ENABLED
+#if KIT_ARCH_ARM32_ENABLED
+ {
+ /* arm-none-eabi: ELFCLASS32, EABI version 5, soft-float default.
+ * Static-only freestanding — no musl interp; the dyn r_* fields are
+ * unused on the static path. EM_ARM is unique (no 64-bit ARM here),
+ * so obj_elf_machine_class matches on e_machine alone. */
+ .arch = KIT_ARCH_ARM_32,
+ .e_machine = EM_ARM,
+ /* EABI v5; soft-float objects carry no float-ABI bit (the ecosystem
+ * convention — clang/gas mark only hard-float). */
+ .e_flags = EF_ARM_EABI_VER5,
+ .default_musl_interp = NULL,
+ .r_relative = ELF_R_ARM_NONE,
+ .r_glob_dat = ELF_R_ARM_NONE,
+ .r_jump_slot = ELF_R_ARM_NONE,
+ .r_irelative = ELF_R_ARM_NONE,
+ /* ARM EABI TLS variant-I: tp points at an 8-byte (2-word) TCB. */
+ .tls_tp_bias = 8u,
+ .reloc_to = elf_arm_reloc_to,
+ .reloc_from = elf_arm_reloc_from,
+ .reloc_name = elf_arm_reloc_name,
+ .float_abi_from_e_flags = elf_arm_float_abi_from_e_flags,
+ },
+#endif
+#if !KIT_ARCH_AA64_ENABLED && !KIT_ARCH_X64_ENABLED && \
+ !KIT_ARCH_RV64_ENABLED && !KIT_ARCH_RV32_ENABLED && !KIT_ARCH_ARM32_ENABLED
{.arch = KIT_ARCH_WASM},
#endif
};
diff --git a/test/lib/check_arm32_env.sh b/test/lib/check_arm32_env.sh
@@ -0,0 +1,114 @@
+#!/usr/bin/env bash
+# test/lib/check_arm32_env.sh — kit arm32 "doctor".
+#
+# Prerequisite check for the arm32 behavioral-oracle lane (test/smoke/arm32.sh).
+# Like rv32, the arm32 target is freestanding `arm-none-eabi`: it runs as a
+# bare-metal Cortex-M3 (Thumb-2) image under qemu-system-arm -machine
+# mps2-an385, with a startup stub at flash 0x0 (M-profile vector table: initial
+# MSP + reset handler) that copies .data flash->SRAM, zeroes .bss, calls the
+# program, and reports its result through ARM semihosting SYS_EXIT_EXTENDED
+# (BKPT #0xAB) — qemu's own process exit then == the program's exit code.
+#
+# arm32 v1 is soft-float only (Cortex-M3 has no FPU): -mfloat-abi=soft routes
+# float + double through the AEABI helpers. So, unlike rv32, there is no
+# FPU-enable step in the stub and only a single ABI lane.
+#
+# Each checked tool is reported as a one-liner (OK / MISSING) with what was
+# looked for and how to install it. Source it and call check_arm32_env to
+# populate the ARM32_* globals; run it directly for a standalone report.
+#
+# After check_arm32_env returns, these globals are set:
+# ARM32_HAVE_CLANG_TARGET 0/1 — clang accepts --target=arm-none-eabi (M3)
+# ARM32_HAVE_LLD 0/1 — ld.lld on PATH (bare-metal link fallback)
+# ARM32_HAVE_QEMU_SYSTEM 0/1 — qemu-system-arm on PATH
+# ARM32_QEMU_SYSTEM_BIN path or empty
+# ARM32_READY 0/1 — clang target + qemu-system both usable
+
+_arm32_os_tag() {
+ case "$(uname -s 2>/dev/null)" in
+ Darwin) echo darwin ;;
+ Linux)
+ if [ -r /etc/os-release ]; then
+ . /etc/os-release
+ case "${ID:-}:${ID_LIKE:-}" in
+ *alpine*) echo alpine ;;
+ *debian*|*ubuntu*) echo debian ;;
+ *fedora*|*rhel*) echo fedora ;;
+ *) echo linux ;;
+ esac
+ else echo linux; fi ;;
+ *) echo other ;;
+ esac
+}
+
+_arm32_hint_clang() {
+ case "$(_arm32_os_tag)" in
+ darwin) echo "brew install llvm (clang ships the arm-none-eabi target)" ;;
+ debian) echo "apt install clang lld" ;;
+ fedora) echo "dnf install clang lld" ;;
+ alpine) echo "apk add clang lld" ;;
+ *) echo "install an LLVM/clang with the arm (Cortex-M) target" ;;
+ esac
+}
+
+_arm32_hint_qemu_system() {
+ case "$(_arm32_os_tag)" in
+ darwin) echo "brew install qemu" ;;
+ debian) echo "apt install qemu-system-arm" ;;
+ fedora) echo "dnf install qemu-system-arm" ;;
+ alpine) echo "apk add qemu-system-arm" ;;
+ *) echo "install qemu (qemu-system-arm)" ;;
+ esac
+}
+
+check_arm32_env() {
+ ARM32_HAVE_CLANG_TARGET=0
+ ARM32_HAVE_LLD=0
+ ARM32_HAVE_QEMU_SYSTEM=0
+ ARM32_QEMU_SYSTEM_BIN=""
+
+ # clang with the arm-none-eabi (Cortex-M3) target: probe by compiling an
+ # empty TU. The smoke lane uses --target=arm-none-eabi; the canonical Thumb-2
+ # M-profile spelling thumbv7m-none-eabi is equivalent for this probe.
+ if command -v clang >/dev/null 2>&1; then
+ if echo 'int _e(void){return 0;}' | \
+ clang --target=arm-none-eabi -mcpu=cortex-m3 -mthumb -mfloat-abi=soft \
+ -ffreestanding -nostdlib -c -x c - -o /dev/null >/dev/null 2>&1; then
+ ARM32_HAVE_CLANG_TARGET=1
+ echo " OK clang --target=arm-none-eabi -mcpu=cortex-m3"
+ else
+ echo " MISSING clang arm-none-eabi target — install: $(_arm32_hint_clang)"
+ fi
+ else
+ echo " MISSING clang — install: $(_arm32_hint_clang)"
+ fi
+
+ if command -v ld.lld >/dev/null 2>&1; then
+ ARM32_HAVE_LLD=1
+ echo " OK ld.lld"
+ else
+ echo " MISSING ld.lld — install: $(_arm32_hint_clang)"
+ fi
+
+ if command -v qemu-system-arm >/dev/null 2>&1; then
+ ARM32_HAVE_QEMU_SYSTEM=1
+ ARM32_QEMU_SYSTEM_BIN="$(command -v qemu-system-arm)"
+ echo " OK qemu-system-arm ($ARM32_QEMU_SYSTEM_BIN)"
+ else
+ echo " MISSING qemu-system-arm — install: $(_arm32_hint_qemu_system)"
+ fi
+
+ ARM32_READY=0
+ if [ "$ARM32_HAVE_CLANG_TARGET" -eq 1 ] && [ "$ARM32_HAVE_QEMU_SYSTEM" -eq 1 ]; then
+ ARM32_READY=1
+ echo " READY arm32 behavioral oracle available"
+ else
+ echo " BLOCKED arm32 behavioral oracle needs clang arm-none-eabi target + qemu-system-arm"
+ fi
+}
+
+# Run standalone: report and exit non-zero if blocked.
+if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
+ check_arm32_env
+ [ "${ARM32_READY:-0}" -eq 1 ]
+fi
diff --git a/test/lib/exec_bare.sh b/test/lib/exec_bare.sh
@@ -1,7 +1,7 @@
# test/lib/exec_bare.sh — bare-metal (freestanding) execution backend for
# test/lib/exec_target.sh, and the single owner of kit's per-arch boot
# scaffolding (reset stub + linker script + exit-code oracle + qemu-system
-# invocation) for aarch64 / x86_64 / riscv64 / riscv32.
+# invocation) for aarch64 / x86_64 / riscv64 / riscv32 / arm32 (Cortex-M).
#
# This consolidates what used to be three scripts: the rv32 corpus runner
# (exec_rv32_bare.sh), the aa64/rv64 ready-image runner (exec_kernel.sh, now a
@@ -28,6 +28,7 @@
# aa64 ARM semihosting SYS_EXIT_EXTENDED → qemu rc = guest code
# rv* SiFive test finisher MMIO @0x100000 → qemu rc = guest code
# x64 isa-debug-exit @0x501 → qemu rc = (code<<1)|1, decoded here
+# arm32 ARM semihosting BKPT #0xAB + SYS_EXIT_EXTENDED → qemu rc = guest code
#
# Corpus depth (exec_bare_setup/run) is fully wired for rv32 only; the aa64/x64/
# rv64 stubs here are smoke-capable (single object, no TLS/rt). Hardening them
@@ -48,6 +49,7 @@ _bare_canon() {
x64|x86_64|amd64) echo x64 ;;
rv64|riscv64) echo rv64 ;;
rv32|riscv32) echo rv32 ;;
+ arm32|arm|armv7m|armv7em|cortex-m3|cortex-m4|cortex-m7) echo arm32 ;;
*) return 1 ;;
esac
}
@@ -57,6 +59,7 @@ _bare_qemu() {
x64) echo qemu-system-x86_64 ;;
rv64) echo qemu-system-riscv64 ;;
rv32) echo qemu-system-riscv32 ;;
+ arm32) echo qemu-system-arm ;;
esac
}
_bare_triple() {
@@ -65,6 +68,7 @@ _bare_triple() {
x64) echo x86_64-none-elf ;;
rv64) echo riscv64-none-elf ;;
rv32) echo riscv32-none-elf ;;
+ arm32) echo arm-none-eabi ;;
esac
}
@@ -94,6 +98,11 @@ _bare_qemu_run() {
$EXEC_BARE_TIMEOUT "$EXEC_BARE_TO" "$q" -machine virt -bios none \
-kernel "$elf" -nographic -no-reboot \
>"$out" 2>"$err"; RUN_RC_RAW=$? ;;
+ arm32)
+ $EXEC_BARE_TIMEOUT "$EXEC_BARE_TO" "$q" -machine mps2-an385 -cpu cortex-m3 \
+ -kernel "$elf" -nographic -monitor none \
+ -semihosting-config enable=on,target=native -no-reboot \
+ >"$out" 2>"$err"; RUN_RC_RAW=$? ;;
*) RUN_RC_RAW=127 ;;
esac
}
@@ -131,6 +140,7 @@ exec_bare_setup_build() { # arch work [entry] -> build per-arch stub
x64) _bare_emit_x64 "$dir" || return 1 ;;
rv64) _bare_emit_rv64 "$dir" || return 1 ;;
rv32) _bare_emit_rv32 "$dir" "$entry" || return 1 ;;
+ arm32) _bare_emit_arm32 "$dir" "$entry" || return 1 ;;
esac
: > "$dir/.ok"
# Cache the stub dir per arch so a caller may pass a different work dir to
@@ -159,6 +169,25 @@ exec_bare_link() { # arch obj work elf -> echoes reason; 0 linked / 2 failed
"$dir/start.o" "$dir/wrap.o" "$obj" "$rt" -o "$elf" 2>"$lderr"; then
echo "kit ld (rv32) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2
fi
+ elif [ "$arch" = arm32 ]; then
+ # The Cortex-M reset stub IS the image entry (Reset_Handler == _start);
+ # i64 mul/div/shift and soft-double pull __aeabi_*/__*di3 helpers from the
+ # arm-eabi-thumb2 runtime variant (built on demand by _bare_emit_arm32).
+ rt="$EXEC_BARE_ROOT/build/rt/arm-eabi-thumb2/libkit_rt.a"
+ if [ -f "$rt" ]; then
+ if ! "$EXEC_BARE_KIT" ld -T "$dir/link.ld" -e _start \
+ "$dir/start.o" "$obj" "$rt" -o "$elf" 2>"$lderr"; then
+ echo "kit ld (arm32) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2
+ fi
+ else
+ # No runtime variant present: link the stub + corpus object alone.
+ # Inline-only i64/double cases still resolve; helper-needing cases
+ # surface as unresolved-symbol link failures (caller decides).
+ if ! "$EXEC_BARE_KIT" ld -T "$dir/link.ld" -e _start \
+ "$dir/start.o" "$obj" -o "$elf" 2>"$lderr"; then
+ echo "kit ld (arm32) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2
+ fi
+ fi
else
if ! "$EXEC_BARE_KIT" ld -T "$dir/link.ld" -e _start \
"$dir/start.o" "$obj" -o "$elf" 2>"$lderr"; then
@@ -408,6 +437,145 @@ EOF
-O1 -ffreestanding -c "$dir/wrap.c" -o "$dir/wrap.o" 2>"$dir/wrap.err" || return 1
}
+# arm32 (Cortex-M3, Thumb-2) bare-metal reset stub for qemu-system-arm
+# -machine mps2-an385. The M-profile boot contract is a vector table at flash
+# address 0x0: word[0] = initial MSP (top of SRAM), word[1] = reset handler.
+# The reset handler copies .data flash->SRAM, zeroes .bss, calls the corpus
+# entry (default `main`), and reports its return through the ARM semihosting
+# SYS_EXIT_EXTENDED oracle (r0=0x20, r1=&{ADP_Stopped_ApplicationExit, code},
+# BKPT #0xAB) — qemu's own process exit then == the guest exit code.
+#
+# Phase-1 bring-up: the startup stub uses M-profile system mnemonics the kit
+# assembler does not yet name, so it is CLANG-assembled (--target=arm-none-eabi
+# -mcpu=cortex-m3 -mthumb). The corpus object itself is produced by kit cc and
+# linked in by exec_bare_link via `kit ld`. There is no TLS image to seed
+# (freestanding arm32 v1 is non-TLS); the soft-float i64/double helpers come
+# from the arm-eabi-thumb2 runtime variant when exec_bare_link finds it.
+#
+# `Reset_Handler` is the true M-profile reset entry (named in the vector table);
+# `_start` is aliased to it so the shared `kit ld -e _start` link path resolves.
+_bare_emit_arm32() {
+ local dir="$1" entry="${2:-main}"
+ local cc="${EXEC_BARE_ARM32_CC:-clang}"
+ command -v "$cc" >/dev/null 2>&1 || return 1
+ # Build the arm-eabi-thumb2 runtime variant on demand for the i64/double
+ # helpers (best-effort; exec_bare_link degrades gracefully if absent).
+ local rt="$EXEC_BARE_ROOT/build/rt/arm-eabi-thumb2/libkit_rt.a"
+ if [ ! -f "$rt" ]; then
+ make -C "$EXEC_BARE_ROOT" rt-arm-eabi-thumb2 >/dev/null 2>&1 || true
+ fi
+ cat > "$dir/start.S" <<EOF
+ .syntax unified
+ .thumb
+ .cpu cortex-m3
+
+/* ---- Vector table: must be FIRST, placed at address 0x0 ---- */
+ .section .vectors, "a"
+ .align 2
+ .global _vectors
+_vectors:
+ .word _estack /* word[0]: initial MSP = top of SRAM */
+ .word Reset_Handler /* word[1]: reset handler (Thumb bit auto-set by
+ .thumb_func; do NOT add +1 or it HardFaults) */
+
+/* ---- Reset handler (== _start for the shared kit-ld entry) ---- */
+ .section .text.start, "ax"
+ .thumb_func
+ .align 2
+ .global Reset_Handler
+ .global _start
+Reset_Handler:
+_start:
+ /* Set SP explicitly (the vector table also provides it). */
+ ldr r0, =_estack
+ mov sp, r0
+
+ /* Copy .data from flash LMA (_sidata) to SRAM VMA (_sdata.._edata). */
+ ldr r0, =_sidata
+ ldr r1, =_sdata
+ ldr r2, =_edata
+1:
+ cmp r1, r2
+ bcs 2f
+ ldr r3, [r0], #4
+ str r3, [r1], #4
+ b 1b
+2:
+
+ /* Zero .bss (_sbss.._ebss). */
+ ldr r0, =_sbss
+ ldr r1, =_ebss
+ movs r2, #0
+3:
+ cmp r0, r1
+ bcs 4f
+ str r2, [r0], #4
+ b 3b
+4:
+
+ /* Call the corpus entry; its return value lands in r0 == exit code. */
+ bl ${entry}
+ bl _exit_semihost
+5: b 5b
+
+/* ---- Semihosting SYS_EXIT_EXTENDED (0x20): r0 = exit code on entry. ---- */
+ .thumb_func
+ .align 2
+ .global _exit_semihost
+_exit_semihost:
+ mov r2, r0 /* save exit code */
+ sub sp, sp, #8 /* 2-word parameter block on the stack */
+ ldr r3, =0x20026 /* ADP_Stopped_ApplicationExit */
+ str r3, [sp, #0] /* block[0] = reason */
+ str r2, [sp, #4] /* block[1] = exit code */
+ mov r1, sp /* r1 -> parameter block */
+ movs r0, #0x20 /* SYS_EXIT_EXTENDED */
+ bkpt 0xAB /* semihosting call */
+ add sp, sp, #8
+6: b 6b
+EOF
+ # mps2-an385: code in FLASH @ 0x0 (vector table first), data in SRAM @
+ # 0x20000000 with the .data init image at a flash LMA (AT> FLASH); the stub
+ # copies it across at reset. kit ld's linker-script front end supports
+ # MEMORY / AT> / LOADADDR / ORIGIN / LENGTH; the one unsupported GNU form is
+ # a *top-level* `sym = expr`, so _estack is assigned inside SECTIONS.
+ cat > "$dir/link.ld" <<'EOF'
+ENTRY(_start)
+MEMORY {
+ FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 4M
+ SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 4M
+}
+SECTIONS {
+ .text : {
+ *(.vectors)
+ *(.text.start)
+ *(.text*)
+ *(.rodata*)
+ . = ALIGN(4);
+ } > FLASH
+ _sidata = LOADADDR(.data);
+ .data : ALIGN(4) {
+ _sdata = .;
+ *(.data*)
+ . = ALIGN(4);
+ _edata = .;
+ } > SRAM AT> FLASH
+ .bss (NOLOAD) : ALIGN(4) {
+ _sbss = .;
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ _ebss = .;
+ } > SRAM
+ _estack = ORIGIN(SRAM) + LENGTH(SRAM);
+ /DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.ARM.attributes)
+ *(.comment) *(.note*) }
+}
+EOF
+ "$cc" --target=arm-none-eabi -mcpu=cortex-m3 -mthumb -ffreestanding -nostdlib \
+ -fno-pic -fno-pie -c "$dir/start.S" -o "$dir/start.o" 2>"$dir/as.err" || return 1
+}
+
# x86_64 long-mode reset: a multiboot header, a 32-bit entry that sets up
# identity-mapped paging + long mode, then calls main and reports its return via
# isa-debug-exit (port 0x501). Ported from freestanding_system.sh.
diff --git a/test/smoke/arm32.sh b/test/smoke/arm32.sh
@@ -0,0 +1,326 @@
+#!/usr/bin/env bash
+# test/smoke/arm32.sh — behavioral oracle for kit's arm-none-eabi (Cortex-M3,
+# Thumb-2) codegen.
+#
+# Like rv32 (freestanding `-none-elf`), arm32 is bare-metal: kit compiles the
+# app, we link a Cortex-M image with FLASH (code) at 0x00000000 and SRAM (data)
+# at 0x20000000, fronted by a startup stub whose M-profile vector table at 0x0
+# carries the initial MSP (top of SRAM) + the reset handler. The reset handler
+# copies .data flash->SRAM, zeroes .bss, calls the app, and reports the result
+# through the ARM semihosting SYS_EXIT_EXTENDED oracle (r0=0x20,
+# r1=&{ADP_Stopped_ApplicationExit=0x20026, code}, BKPT #0xAB) — qemu's own
+# process exit then == the program's exit code (no stdout parsing). Run under
+# qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -semihosting.
+#
+# Scope: the verified codegen surface — 32-bit integer + pointer, control flow,
+# a ≤4-int-arg call to a second function, a switch, AND 64-bit-value
+# legalization: long long carry/borrow/bitwise/compare/convert inline as GPR
+# pairs, i64 mul/div/shift via __*di3 / __aeabi_* runtime calls, and soft
+# `double` arith/compare/convert via __*df3 / __aeabi_* calls. arm32 v1 is a
+# single soft-float lane (Cortex-M3 has no FPU): -mfloat-abi=soft routes both
+# float and double through the AEABI helpers. The i64-mul/div/shift and double
+# cases link kit's freestanding runtime (libkit_rt.a, the arm-eabi-thumb2
+# variant) for the helpers; the inline i64 cases need no runtime.
+#
+# Phase 1 bring-up: kit compiles app.c (the code under test); the startup stub
+# uses M-profile system mnemonics the kit assembler does not yet name, so it is
+# clang-assembled. The final bare-metal link uses ld.lld; a separate kit-ld
+# lane (kitld_lane) re-links with `kit ld` to exercise kit's own static ELF
+# base-addr control + runtime auto-resolution. The .eh_frame/.ARM.exidx kit
+# emits is discarded by the bare-metal link script (no unwinder in a
+# freestanding image).
+#
+# Skipped (per the shared kit_exit convention) if clang lacks the arm-none-eabi
+# target, ld.lld is missing, or qemu-system-arm is unavailable.
+
+set -u
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+BUILD_DIR="$ROOT/build/test/smoke-arm32"
+mkdir -p "$BUILD_DIR"
+
+KIT_KIT_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/kit_sh_kit.sh
+. "$ROOT/test/lib/kit_sh_kit.sh"
+kit_report_init
+KIT_SKIP_IS_FAILURE=1
+
+# ---- prerequisites (the arm32 doctor) -------------------------------------
+# shellcheck source=../lib/check_arm32_env.sh
+. "$ROOT/test/lib/check_arm32_env.sh"
+check_arm32_env
+if [ "${ARM32_HAVE_CLANG_TARGET:-0}" -eq 0 ]; then
+ skip_test "smoke-arm32" "clang arm-none-eabi target unavailable"; kit_summary test-smoke-arm32; kit_exit
+fi
+if [ "${ARM32_HAVE_LLD:-0}" -eq 0 ]; then
+ skip_test "smoke-arm32" "ld.lld unavailable"; kit_summary test-smoke-arm32; kit_exit
+fi
+if [ "${ARM32_HAVE_QEMU_SYSTEM:-0}" -eq 0 ]; then
+ skip_test "smoke-arm32" "qemu-system-arm unavailable"; kit_summary test-smoke-arm32; kit_exit
+fi
+
+KIT="$ROOT/build/kit"
+QEMU="${ARM32_QEMU_SYSTEM_BIN:-qemu-system-arm}"
+CLANG="${ARM32_CLANG:-clang}"
+
+# Cortex-M startup stub: M-profile vector table at 0x0 (word[0]=initial MSP,
+# word[1]=reset handler), reset handler copies .data flash->SRAM, zeroes .bss,
+# calls cmain. The semihosting exit lives in app.c (__semihost_exit, an
+# assembler-op call), so the stub just hands control to cmain. The stub is
+# clang-assembled (uses M-profile mnemonics kit-as does not yet name).
+cat > "$BUILD_DIR/start.S" <<'EOF'
+ .syntax unified
+ .thumb
+ .cpu cortex-m3
+
+ .section .vectors, "a"
+ .align 2
+ .global _vectors
+_vectors:
+ .word _estack /* word[0]: initial MSP = top of SRAM */
+ .word Reset_Handler /* word[1]: reset handler (Thumb bit auto-set) */
+
+ .section .text.start, "ax"
+ .thumb_func
+ .align 2
+ .global Reset_Handler
+ .global _start
+Reset_Handler:
+_start:
+ ldr r0, =_estack
+ mov sp, r0
+
+ /* copy .data flash LMA -> SRAM VMA */
+ ldr r0, =_sidata
+ ldr r1, =_sdata
+ ldr r2, =_edata
+1: cmp r1, r2
+ bcs 2f
+ ldr r3, [r0], #4
+ str r3, [r1], #4
+ b 1b
+2:
+ /* zero .bss */
+ ldr r0, =_sbss
+ ldr r1, =_ebss
+ movs r2, #0
+3: cmp r0, r1
+ bcs 4f
+ str r2, [r0], #4
+ b 3b
+4:
+ bl cmain
+5: b 5b
+EOF
+
+cat > "$BUILD_DIR/link.ld" <<'EOF'
+/* mps2-an385 (Cortex-M3): FLASH (code) @ 0x0, SRAM (data) @ 0x20000000. */
+ENTRY(_start)
+
+MEMORY
+{
+ FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 4M
+ SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 4M
+}
+
+_estack = ORIGIN(SRAM) + LENGTH(SRAM);
+
+SECTIONS
+{
+ .text :
+ {
+ KEEP(*(.vectors))
+ *(.text.start)
+ *(.text*)
+ *(.rodata*)
+ . = ALIGN(4);
+ _etext = .;
+ } > FLASH
+
+ _sidata = LOADADDR(.data);
+
+ .data : ALIGN(4)
+ {
+ _sdata = .;
+ *(.data*)
+ . = ALIGN(4);
+ _edata = .;
+ } > SRAM AT> FLASH
+
+ .bss (NOLOAD) : ALIGN(4)
+ {
+ _sbss = .;
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ _ebss = .;
+ } > SRAM
+
+ /DISCARD/ : { *(.eh_frame) *(.eh_frame_hdr)
+ *(.ARM.exidx*) *(.ARM.extab*) *(.ARM.attributes)
+ *(.comment) *(.note*) }
+}
+EOF
+
+# The app under test. Every check returns a distinct nonzero code on failure so
+# a qemu exit pinpoints the broken case. __semihost_exit reports the result via
+# ARM semihosting SYS_EXIT_EXTENDED (BKPT #0xAB).
+cat > "$BUILD_DIR/app.c" <<'EOF'
+__attribute__((noreturn)) static void __semihost_exit(int code){
+ /* SYS_EXIT_EXTENDED (0x20): r1 -> {ADP_Stopped_ApplicationExit, code}. */
+ volatile unsigned block[2];
+ block[0] = 0x20026u; /* ADP_Stopped_ApplicationExit */
+ block[1] = (unsigned)code;
+ register unsigned r0 __asm__("r0") = 0x20u;
+ register const volatile unsigned *r1 __asm__("r1") = block;
+ __asm__ volatile ("bkpt 0xAB" :: "r"(r0), "r"(r1) : "memory");
+ for(;;){}
+}
+/* A second function with <=4 int args, called from compute(). */
+static int sum4(int a, int b, int c, int d){ return a + b + c + d; }
+static int classify(int n){
+ switch (n){ /* switch -> TBB/TBH or branch table */
+ case 0: return 100;
+ case 1: return 101;
+ case 2: return 102;
+ case 3: return 103;
+ default: return 199;
+ }
+}
+static int compute(void){
+ /* return-const / 32-bit integer / control flow */
+ int acc = 0; for (int i = 0; i < 10; i++) acc += i;
+ if (acc != 45) return 1;
+ /* basic ALU */
+ volatile unsigned u = 0x12345678u; u ^= 0x0F0F0F0Fu;
+ if (u != (0x12345678u ^ 0x0F0F0F0Fu)) return 2;
+ int p = 6, q = 7;
+ if (p * q + (p << 2) - (q >> 1) != 42 + 24 - 3) return 3;
+ /* if/else */
+ int sel = (acc > 40) ? 1 : 0;
+ if (sel) { if (u == 0) return 4; } else return 5;
+ /* call to a second function with <=4 int args */
+ if (sum4(1, 2, 3, 4) != 10) return 6;
+ if (sum4(acc, p, q, sel) != 45 + 6 + 7 + 1) return 7;
+ /* switch */
+ if (classify(2) != 102) return 8;
+ if (classify(9) != 199) return 9;
+ /* i64 inline: carry / borrow / bitwise / compare / convert (no runtime) */
+ volatile unsigned long long a = 0xFFFFFFFFull; a += 1;
+ if (a != 0x100000000ull) return 10;
+ volatile unsigned long long b = 0x100000000ull; b -= 1;
+ if (b != 0x0FFFFFFFFull) return 11;
+ volatile unsigned long long c = 0x1122334455667788ull;
+ if ((c ^ 0xFFFFFFFFFFFFFFFFull) != 0xEEDDCCBBAA998877ull) return 12;
+ volatile long long x = 0x1234567800000000ll, y = 0x1234567700000001ll;
+ if (!(x > y) || !(y < x) || (x == y)) return 13;
+ volatile int s32 = -7; volatile long long s64 = s32;
+ if (s64 != -7) return 14;
+ volatile long long big = 0x00000000FAFAFAFAll;
+ if ((unsigned)(int)big != 0xFAFAFAFAu) return 15;
+ if (a) { } else return 16; /* truthiness (hi word set) */
+ /* i64 runtime: mul / udiv / umod / shifts (__muldi3/__udivdi3/__aeabi_*) */
+ volatile unsigned long long m = 0x0000000100000001ull;
+ if (m * 3ull != 0x0000000300000003ull) return 17;
+ volatile unsigned long long d = 0xFFFFFFFFFFFFFFFFull;
+ if (d / 0xFFFFFFFFull != 0x0000000100000001ull) return 18;
+ if (d % 7ull != (0xFFFFFFFFFFFFFFFFull % 7ull)) return 19;
+ volatile unsigned long long sh = 1ull;
+ if ((sh << 40) != 0x0000010000000000ull) return 20;
+ volatile long long sar = -0x4000000000000000ll;
+ if ((sar >> 36) != (-0x4000000000000000ll >> 36)) return 21;
+ /* soft double: arith / compare / convert (__adddf3/__muldf3/__aeabi_*) */
+ volatile double dp = 1.5, dq = 2.25;
+ if (dp + dq != 3.75) return 22;
+ if (dp * dq != 3.375) return 23;
+ if (!(dp < dq) || (dp >= dq)) return 24;
+ volatile int iv = 7; volatile double dv = iv;
+ if (dv != 7.0) return 25;
+ volatile double dd = 3.75; if ((int)dd != 3) return 26;
+ volatile long long L = 5000000000ll; volatile double dL = L;
+ if (dL != 5000000000.0) return 27;
+ volatile double dbig = 5000000000.0;
+ if ((long long)dbig != 5000000000ll) return 28;
+ return 0;
+}
+void cmain(void){ __semihost_exit(compute()); }
+EOF
+
+# Build kit's freestanding runtime archive for the helpers (i64 mul/div/shift +
+# soft double). Built on demand; skip the runtime-dependent lanes if it cannot
+# be produced (e.g. a partial checkout) rather than failing spuriously.
+RT="$ROOT/build/rt/arm-eabi-thumb2/libkit_rt.a"
+make -C "$ROOT" rt-arm-eabi-thumb2 >/dev/null 2>&1 || true
+
+# Assemble the startup stub once (clang; M-profile mnemonics).
+START_O="$BUILD_DIR/start.o"
+"$CLANG" --target=arm-none-eabi -mcpu=cortex-m3 -mthumb -ffreestanding \
+ -nostdlib -fno-pic -fno-pie -c "$BUILD_DIR/start.S" -o "$START_O" 2>/dev/null
+
+run_lane() { # <name>
+ local name="$1"
+ if [ ! -f "$RT" ]; then
+ skip_test "$name" "runtime archive $RT missing"; return; fi
+ local O
+ for O in -O0 -O1; do
+ local o="$BUILD_DIR/$name$O.o" elf="$BUILD_DIR/$name$O.elf"
+ if ! "$KIT" cc -target arm-none-eabi -mcpu=cortex-m3 -mfloat-abi=soft $O \
+ -ffreestanding -c "$BUILD_DIR/app.c" -o "$o" 2>"$BUILD_DIR/$name$O.cc.err"; then
+ not_ok "$name $O (kit cc)" "$BUILD_DIR/$name$O.cc.err"; continue; fi
+ if ! ld.lld -T "$BUILD_DIR/link.ld" --gc-sections -static -no-pie \
+ "$START_O" "$o" "$RT" -o "$elf" 2>"$BUILD_DIR/$name$O.ld.err"; then
+ not_ok "$name $O (link)" "$BUILD_DIR/$name$O.ld.err"; continue; fi
+ local rc=0
+ timeout 20 "$QEMU" -machine mps2-an385 -cpu cortex-m3 -kernel "$elf" \
+ -nographic -monitor none -semihosting-config enable=on,target=native \
+ -no-reboot >/dev/null 2>&1 || rc=$?
+ if [ "$rc" -eq 0 ]; then ok "$name $O (qemu rc=0)";
+ else not_ok "$name $O" "expected exit 0, got $rc (failed check #$rc)"; fi
+ done
+}
+
+# Single soft-float lane (no FPU on Cortex-M3): float + double + i64 all via the
+# AEABI helpers.
+run_lane "soft"
+
+# Full kit toolchain, runtime auto-linked: re-link the -O1 object with `kit ld`
+# (not ld.lld) and WITHOUT naming a runtime archive. kit ld resolves the
+# float-ABI / arch axis from the object's ELF EABI header, builds the matching
+# libkit_rt.a on demand (build/rt/arm-eabi-thumb2), and links it in. A
+# freestanding arm32 target defaults to non-PIE; -T places .text at the
+# Cortex-M flash base (0x0). The i64-mul/div/shift and soft-double helpers come
+# from the auto-linked runtime, so a clean qemu exit also proves runtime
+# resolution.
+kitld_lane() { # <name>
+ local name="$1"
+ local appo="$BUILD_DIR/$name-O1.o"
+ local elf="$BUILD_DIR/kitld-$name.elf"
+ [ -f "$START_O" ] && [ -f "$appo" ] || return
+ if "$KIT" ld -T "$BUILD_DIR/link.ld" -e _start "$START_O" "$appo" \
+ -o "$elf" 2>"$BUILD_DIR/kitld-$name.ld.err"; then
+ local rc=0
+ timeout 20 "$QEMU" -machine mps2-an385 -cpu cortex-m3 -kernel "$elf" \
+ -nographic -monitor none -semihosting-config enable=on,target=native \
+ -no-reboot >/dev/null 2>&1 || rc=$?
+ if [ "$rc" -eq 0 ]; then ok "kit-ld $name -O1 auto-rt (qemu rc=0)";
+ else not_ok "kit-ld $name -O1 auto-rt" "expected exit 0, got $rc"; fi
+ else
+ not_ok "kit-ld $name -O1 auto-rt (link)" "$BUILD_DIR/kitld-$name.ld.err"
+ fi
+}
+kitld_lane "soft"
+
+# Negative control: a deliberately wrong result must produce a nonzero exit.
+sed 's/if (acc != 45) return 1;/if (acc != 45) return 1; return 99;/' "$BUILD_DIR/app.c" > "$BUILD_DIR/bad.c"
+if "$KIT" cc -target arm-none-eabi -mcpu=cortex-m3 -mfloat-abi=soft -O1 \
+ -ffreestanding -c "$BUILD_DIR/bad.c" -o "$BUILD_DIR/bad.o" 2>/dev/null \
+ && ld.lld -T "$BUILD_DIR/link.ld" --gc-sections -static -no-pie \
+ "$START_O" "$BUILD_DIR/bad.o" "$RT" -o "$BUILD_DIR/bad.elf" 2>/dev/null; then
+ rc=0; timeout 20 "$QEMU" -machine mps2-an385 -cpu cortex-m3 -kernel "$BUILD_DIR/bad.elf" \
+ -nographic -monitor none -semihosting-config enable=on,target=native -no-reboot \
+ >/dev/null 2>&1 || rc=$?
+ if [ "$rc" -eq 99 ]; then ok "negative-control (qemu rc=99)";
+ else not_ok "negative-control" "expected exit 99, got $rc"; fi
+fi
+
+kit_summary test-smoke-arm32
+kit_exit