- Odin 98.8%
- Makefile 1.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| examples/fetch | ||
| src | ||
| tests | ||
| .gitignore | ||
| .odin-version | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
Pearmain
Pearmain is an ARM CPU execution library written in Odin, with a focus on the Nintendo Switch 1. The package provides architectural state for A64 and A32/Thumb, pure helpers for CPSR fields, automatic backend selection, and JIT memory callbacks with instruction fetch for A64 and A32/Thumb. Executors will be added separately.
Toolchain and validation
The initial compiler version is dev-2026-09-nightly:a2fb372, recorded in
.odin-version. Install that version of Odin and GNU Make, then run:
make check
make test
Both commands enable -vet, -strict-style, and -warnings-as-errors.
make check verifies the library and the instruction fetch example.
make test runs the internal tests in src and the public API tests in tests.
The test runner removes its executables after use. Set ODIN to use a compiler
at another path, for example make check ODIN=/opt/odin/odin.
The tests cover CPSR.T, fixed ITSTATE encodings, and all 256 ITSTATE values across six initial CPSR values. Updates must preserve every bit outside ITSTATE. Backend tests cover all 12 supported host and guest combinations, unsupported hosts and subtargets, invalid guest modes, and error precedence. Tests on Linux validate the selection policy; they do not validate execution on other hosts or actual Apple hypervisor availability.
Backend selection
Guest_Mode has A64 and A32 variants. Thumb remains part of A32, derived from
CPSR.T. The public query uses the binary's compile target:
query_backend(guest: Guest_Mode) -> (backend: Backend_Kind, error: Backend_Query_Error)
| Host | A64 | A32/Thumb |
|---|---|---|
| Linux, Windows, or macOS x86-64 | JIT_X86_64 |
JIT_X86_64 |
| Linux or Windows ARM64 | JIT_ARM64 |
JIT_ARM64 |
| macOS ARM64 | Apple_Hypervisor |
JIT_ARM64 |
Backend_Kind contains the three mechanisms above and None. Selection reads
ODIN_OS, ODIN_ARCH, and ODIN_PLATFORM_SUBTARGET. Only the default subtarget
is supported; Android and iOS are excluded.
Backend_Query_Error distinguishes these results:
| Condition | Backend | Error |
|---|---|---|
| Invalid guest mode, regardless of host | None |
Invalid_Guest |
| Valid guest mode on an unsupported host or subtarget | None |
Unsupported_Host |
| Supported host and guest pair | Selected mechanism | Backend_Unavailable |
The guest mode is validated before the host. Every supported pair currently
returns Backend_Unavailable with a valid selection, because no executor is
implemented. The None error variant is reserved for success and is never
returned at this stage.
A64 on macOS ARM64 always selects Apple_Hypervisor, with no JIT fallback.
The query performs no runtime framework, permission, or hardware checks. It
allocates no memory, accesses no CPU state, and executes no instructions.
Execution capabilities will be defined with the execution contracts.
Replace the import path with a relative path from example.odin to this
repository's src directory, then run odin run example.odin -file:
package main
import pearmain "path/to/pearmain/src"
import "core:fmt"
main :: proc() {
backend, error := pearmain.query_backend(.A64)
fmt.println(backend, error)
}
On Linux x86-64, the example prints JIT_X86_64 Backend_Unavailable.
JIT memory callbacks
Memory_Callbacks contains an integrator-owned data: rawptr and separate
fetch, read, and write callbacks. All three use the standard Odin calling
convention and the Memory_Access_Proc signature:
proc(data: rawptr, address: u64, buffer: []u8) -> Memory_Result
The address is a guest address, not a host pointer. The buffer length is the exact number of bytes requested. Fetch and read fill the buffer in increasing address order. Write consumes the buffer without changing it. The integrator checks execute, read, or write permissions according to the callback invoked.
Memory_Result is a union with three possible outcomes:
| Result | Meaning |
|---|---|
nil |
The entire access succeeded |
Memory_Fault |
A guest memory fault, with address and kind |
Memory_Error |
A library or integrator error |
Memory_Fault_Kind contains Unmapped, Permission_Denied, Alignment, and
External_Abort. The integrator reports the address that caused the fault,
which can differ from the start of the requested range. Memory_Error contains
Missing_Callback, Address_Overflow, and Callback_Failed. Integrator failures
use Callback_Failed; they must not be reported as guest faults.
Each callback must validate the complete access before transferring bytes. Success transfers the whole buffer. Failure leaves both the destination buffer and guest memory unchanged. This is a callback obligation; Pearmain does not implement rollback or report partial progress. This guarantee does not imply atomicity between CPUs.
The integrator owns guest memory and data, and must keep them valid throughout
each call. Buffers are borrowed only for that call and must not be retained.
Calls are synchronous on the caller's thread. Pearmain does not allocate memory
for these accesses. The integrator must synchronize shared memory and must not
reenter a Pearmain memory operation from a callback.
These callbacks belong to the JIT memory interface. Hypervisor memory mappings will use a separate interface. Data reads and writes will be consumed by future executors.
Instruction fetch
fetch_a64(memory: Memory_Callbacks, pc: u64) and
fetch_a32(memory: Memory_Callbacks, pc: u32, cpsr: u32) return a
Fetched_Instruction and a Memory_Result. The instruction contains
bytes: [4]u8 in increasing address order and size: u8, either 2 or 4 on
success. Unused bytes are zero. The bytes are not a decoded instruction word.
A64 and ARM fetch exactly four bytes through one fetch call. A32 uses only
CPSR.T to select ARM or Thumb. Thumb fetches two bytes first, interprets that
halfword in little-endian order, and fetches two more bytes only when bits
[15:11] are 11101, 11110, or 11111. See the
Arm Thumb encoding rules, section A5.1.
The second call starts at PC + 2 and does not reread the first halfword. The two
calls are not a single transaction; the integrator must keep instruction bytes
stable throughout the fetch.
Both helpers check for a missing fetch callback before checking PC alignment.
A missing callback returns Memory_Error.Missing_Callback. A64 and ARM require
four-byte alignment; Thumb requires two-byte alignment. A misaligned PC returns
an Alignment fault at that PC. Neither case invokes a callback. The helpers
do not mask address bits, advance PC, or change CPSR or ITSTATE.
The highest aligned u64 address is valid for a four-byte A64 request. In A32,
an instruction must fit within the 32-bit address space. A Thumb instruction at
0xFFFFFFFE succeeds if it is 16 bits long. If its prefix indicates 32 bits,
the helper returns Memory_Error.Address_Overflow after the first read, without
reading address zero. This is an explicit library limit, not a guest fault.
Callback results are propagated unchanged, including the fault address. Any
failure returns a completely zeroed instruction, including when the second
Thumb read fails. The helpers do not retry, use the data callbacks, decode
instructions, or modify CPU state. read and write may be absent. Fetch is
available independently of executor availability.
The fetch example fetches A64, ARM, Thumb16, and Thumb32 instructions through a callback over a local byte array. Run it from the repository root:
odin run examples/fetch -vet -strict-style -warnings-as-errors
The tests record callback addresses, lengths, and counts. They cover all 65,536 first Thumb halfwords, region boundaries, both address limits, validation order, and failures at either halfword. A Thumb16 fetch at the end of a readable region does not access the next region.
State conventions
Import the public package with an explicit alias:
import pearmain "path/to/pearmain/src"
A64_State stores X0 through X30 in x, with separate sp and pc fields.
There is no X31 storage slot: instruction semantics determine whether that
encoding refers to SP or the zero register. pstate stores processor status.
The v bank holds 32 vectors as 16 bytes each, with the least significant byte at
index zero. fpcr and fpsr store floating-point control and status.
A32_State stores R0 through R15 in r, processor status in cpsr, and
floating-point status and control in fpscr. Thumb state comes from CPSR.T,
bit 5. ITSTATE[1:0] comes from CPSR[26:25], and ITSTATE[7:2] from CPSR[15:10].
Neither has a separate stored copy. See the
Arm CPSR field definitions.
The A32 floating-point bank stores D0 through D31 as raw 64-bit integers. Its aliases share those bits:
| Register view | Storage, for n from 0 through 15 |
|---|---|
| S(2n) | Low 32 bits of D(n) |
| S(2n+1) | High 32 bits of D(n) |
| Q(n) | D(2n) in the low 64 bits and D(2n+1) in the high 64 bits |
Thus S0 through S31 cover D0 through D15, while Q0 through Q15 cover the entire D bank. There are no separate S or Q arrays that could become inconsistent. See the register mapping in Arm DUI 0801B, section 11.2.
Both states store the address of the current instruction as PC: pc for A64 and
r[15] for A32. Future instruction frontends will calculate architectural R15
read values. SIMD/FP storage uses integer bits and has no dependency on native
vector types or host floating-point operations.
These types are storage records. A zero-initialized record does not represent the reset state of a CPU. There is no serialized layout or stable binary ABI contract for these Odin structs.
CPSR helpers
| Procedure | Input | Result |
|---|---|---|
cpsr_is_thumb |
cpsr: u32 |
CPSR.T as bool |
cpsr_itstate |
cpsr: u32 |
Combined ITSTATE as u8 |
cpsr_with_itstate |
cpsr: u32, itstate: u8 |
Updated CPSR as u32 |
The helpers allocate no memory and take no pointers. cpsr_with_itstate preserves
all fields outside ITSTATE, including CPSR.T and the condition flags. These
helpers manipulate the stored fields; they do not validate whether a bit
pattern represents a valid instruction execution state.
License
Pearmain is licensed under the Mozilla Public License 2.0. See LICENSE.