No description
  • Odin 98.8%
  • Makefile 1.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-30 11:13:53 -03:00
examples/fetch Rename Odin library to Pearmain 2026-09-30 11:13:53 -03:00
src Rename Odin library to Pearmain 2026-09-30 11:13:53 -03:00
tests Rename Odin library to Pearmain 2026-09-30 11:13:53 -03:00
.gitignore Define ARM architectural state 2026-09-30 09:38:28 -03:00
.odin-version Define ARM architectural state 2026-09-30 09:38:28 -03:00
LICENSE Initial commit 2026-09-29 12:41:43 -04:00
Makefile Rename Odin library to Pearmain 2026-09-30 11:13:53 -03:00
README.md Rename Odin library to Pearmain 2026-09-30 11:13:53 -03:00

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.