fARM64 0.1.0

A pure-Rust, no_std, zero-heap AArch64 (A64) disassembler and semantic encoder.
Documentation
  • Coverage
  • 92.91%
    5258 out of 5659 items documented4 out of 241 items with examples
  • Size
  • Source code size: 3.5 MB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 17.2 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 8s Average build duration of successful builds.
  • all releases: 10s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • binsnake/fARM64
    24 1 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • bombaris34

fARM64

fARM64 is a pure-Rust, #![no_std], zero-heap AArch64 (A64) disassembler and semantic encoder. It decodes 64-bit Arm machine code into a rich, Copy value-type Instruction, renders it through a pluggable Formatter, and can re-encode an Instruction to a 32-bit word. The public API is deliberately iced-x86-shaped (a borrowing Decoder, a value-type Instruction, typed OpKind/Operand accessors, and a token-emitting Formatter). The Arm architectural decode tree is hand-written from the Arm Architecture Reference Manual (the "Arm ARM"); tests also cross-check independent toolchains and corpora. Apple AMX and GXF are implementation-defined exceptions whose encodings come from public reverse-engineering references and are explicitly runtime-gated.


Highlights

  • Freestanding by default. #![no_std] unconditionally, with no alloc and no std in the default build. No-CRT / bare-metal / wasm friendly; builds for wasm32-unknown-unknown and aarch64-unknown-none.
  • Zero heap on the core path. Decoder::decode_into writes into a caller-owned Copy Instruction (no Vec, no Box, no internal pointers); the default formatter writes into a fixed &mut [u8] (BufSink) or any core::fmt::Write.
  • Copy value-type Instruction. Pass it by value; inline [Operand; MAX_OPERANDS] storage, <= 112 bytes, asserted at compile time. Never panics on malformed input — bad words decode to Code::Invalid with a recorded last_error.
  • Ergonomic iteration. Decoder is an Iterator (both for insn in &mut dec and consuming for insn in dec), plus a decode_into fast path for tight loops.
  • Broad ISA coverage. Full base A64 plus Advanced SIMD / FP, SVE / SVE2, SME / SME2, the crypto extensions, and a long tail of recent additions: MOPS, CSSC, RCPC3, D128, THE, LSE128, SVE2p1, CMPBR, CPA, and more.
  • Encoder included. Instruction::encode() reconstructs the 32-bit word from instruction semantics (never from the stored raw word), proving the decode is invertible.
  • Editable instructions. Swap registers, immediates, memory bases, conditions and branch targets on a decoded Instruction, then encode() the result. Register replacement is class- and width-checked, operand decorations are preserved, and every setter is total — an edit with no valid encoding surfaces as an EncodeError, never a panic.
  • Implicit register reads/writes. implicit_registers() reports the state an instruction touches without naming it: the link register of a call/return, the X30/SP/X16/X17 of the pointer-authentication forms, the eight-register LD64B/ST64B group, the SVE FFR, PC for PC-relative address generation, and NZCV. It is merged into instruction_info()'s access set.
  • Pluggable formatting. Default Arm UAL FmtFormatter, an optional GnuFormatter compatibility adapter behind fmt-gnu, a token-classifying FormatterOutput sink, and a SymbolResolver hook. GnuFormatter currently emits the same UAL text as FmtFormatter.
  • Two feature layers. Cargo features compile optional implementation modules; a runtime FeatureSet controls which architectural and implementation-defined encodings the decoder accepts.

Supported targets

Target Notes
x86_64-*, aarch64-* (hosted) development and std testing
wasm32-unknown-unknown default features (no_std, no alloc)
aarch64-unknown-none bare-metal, no-CRT; checked with --no-default-features
any target providing core the default tier is core-only

Feature matrix

Cargo features decide which optional implementation modules are compiled; the runtime FeatureSet decides which encodings are accepted at decode time. They are independent layers, but not every runtime extension has a matching Cargo feature.

Cargo feature Tier Effect
(none / default) A no_std, no alloc, freestanding. Decoder + FmtFormatter + all enums + encoder. Always builds.
alloc B Adds String/Vec conveniences (format_to_string, a reusable cached InstructionInfoFactory, and a token-collecting String sink).
std C Implies alloc; adds std::error::Error for DecodeError, EncodeError, and EnumValueError, plus std-only test helpers.
fmt-gnu A Adds GnuFormatter, currently a UAL-equivalent compatibility adapter. Pure no_std.
sve A Compiles the SVE/SVE2 decoder and encoder modules.
sme A Compiles the SME/SME2 decoder and encoder modules.
crypto A Compiles the Advanced SIMD crypto decoder. Its public enum variants and encoder support remain present without this feature.
full A Enables sve, sme, and crypto.
no-alloc-audit test Enables allocation-counting tests for the zero-heap core path; not intended as a downstream capability.

The default build links neither alloc nor std. std implies alloc. The runtime FeatureSet (FeatureSet::ALL, FeatureSet::BASE, .with(Feature::Sve), .has(..)) is orthogonal to all of the above.


Install

[dependencies]

# Default: no_std, no alloc, zero-heap decoder + formatter + encoder.

fARM64 = "0.1.0"

Opt into more as needed:

# Owned-string conveniences and the cached info factory.

fARM64 = { version = "0.1.0", features = ["alloc"] }



# All optional implementation modules plus std and the GNU adapter.

fARM64 = { version = "0.1.0", features = ["std", "full", "fmt-gnu"] }

The import path uses the stylized crate name: use fARM64::....


Quick start

Zero-allocation decode-and-print into a fixed stack buffer — no heap, no std:

use fARM64::{Decoder, DecoderOptions};
use fARM64::format::{Formatter, FmtFormatter, BufSink};

fn main() {
    // `ADD W0, W1, #1`, little-endian; decode at address 0x1000.
    let code = [0x20, 0x04, 0x00, 0x11];
    let mut dec = Decoder::new(&code, 0x1000, DecoderOptions::default());
    let insn = dec.decode();

    // Format into a fixed [u8; N] — the whole path touches no heap.
    let mut buf = [0u8; 64];
    let mut sink = BufSink::new(&mut buf);
    FmtFormatter::new().format(&insn, &mut sink);

    let text: &str = sink.as_str();
    let _ = text; // e.g. "add     w0, w1, #0x1"
}

Decoder::new(data, ip, options) borrows the byte slice; ip is the address of data[0] and PC-relative operands resolve against it. decode() returns a Copy Instruction and advances the cursor by 4. The FmtFormatter::format call goes through the Formatter trait, which is why that trait is imported.


Decoding

use fARM64::{Decoder, DecoderOptions, Code};

fn main() {
    let code: &[u8] = &[
        0x20, 0x04, 0x00, 0x11, // add w0, w1, #1
        0x1f, 0x20, 0x03, 0xd5, // nop
    ];

    // Construct over the slice; `ip` is the address of code[0].
    let mut dec = Decoder::new(code, 0x1000, DecoderOptions::default());

    // Iterate by &mut: yields instructions until fewer than 4 bytes remain.
    for insn in &mut dec {
        if insn.is_invalid() {
            // Bad/unallocated word: inspect dec.last_error() for why.
            continue;
        }
        let _ = (insn.code(), insn.ip());
    }

    // After the loop, the most recent decode status is available:
    let _ = dec.last_error();
    let _ = Code::Invalid; // the sentinel a failed decode carries
}

Key points:

  • Decoder::new(data, ip, options) never panics. There is also a try_new returning Result<Decoder, DecodeError> for API symmetry. A64 has no bitness parameter — it is always 64-bit, always 4-byte fixed-width.
  • Iteration forms. for insn in &mut dec borrows the decoder (you can inspect dec.last_error() afterward); for insn in dec consumes it. Both yield until fewer than 4 bytes remain.
  • decode() vs decode_into(). decode() returns a fresh Instruction. decode_into(&mut out) writes into a caller-owned Instruction, which is the preferred zero-allocation form in a tight loop because nothing is constructed or moved per iteration:
use fARM64::{Decoder, DecoderOptions, Instruction};

fn main() {
    let code: &[u8] = &[0x20, 0x04, 0x00, 0x11, 0x1f, 0x20, 0x03, 0xd5];
    let mut dec = Decoder::new(code, 0x1000, DecoderOptions::default());

    // Reuse one Instruction across the whole loop — no per-iteration alloc/move.
    let mut insn = Instruction::default();
    while dec.can_decode() {
        dec.decode_into(&mut insn);
        let _ = insn.code();
    }
}
  • Position and address. position()/set_position(pos) move the byte cursor (and keep ip consistent relative to the original base); ip()/set_ip(ip) read/set the current decode address directly.
  • Invalid handling. A malformed or unallocated word never panics — it decodes to an Instruction with Code::Invalid (check insn.is_invalid()), and dec.last_error() returns the reason (DecodeError::Unmatched, DecodeError::EndOfInstruction on a short tail, or DecodeError::None on success).

Restricting accepted extensions

DecoderOptions carries a FeatureSet. The default accepts everything (FeatureSet::ALL); narrow it to reject encodings outside the extensions you target:

use fARM64::{Decoder, DecoderOptions, FeatureSet, Feature};

fn main() {
    // Accept only the base ISA plus FEAT_LSE atomics; reject everything else.
    let features = FeatureSet::BASE.with(Feature::Lse);
    let options = DecoderOptions { features };

    let code = [0x20, 0x04, 0x00, 0x11]; // add w0, w1, #1 (base ISA)
    let mut dec = Decoder::new(&code, 0x1000, options);
    let insn = dec.decode();
    let _ = insn.code();
}

Inspecting an Instruction

use fARM64::{Decoder, DecoderOptions};
use fARM64::{OpKind, Operand, Register};

fn main() {
    let code = [0x20, 0x04, 0x00, 0x11]; // add w0, w1, #1
    let mut dec = Decoder::new(&code, 0x1000, DecoderOptions::default());
    let insn = dec.decode();

    // `code()` is the precise encoding identity (e.g. Code::AddImm32);
    // `mnemonic()` is the preferred/alias display spelling; `.name()` is the text.
    let _enc = insn.code();
    let _mnem = insn.mnemonic();
    let _name: &str = insn.mnemonic().name();

    // Address helpers (A64 is fixed 4-byte wide).
    let _ = (insn.ip(), insn.next_ip(), insn.len(), insn.word());

    // Control-flow class and NZCV write behaviour.
    let _ = (insn.flow_control(), insn.set_flags());

    // Walk operands. `op_kind(n)` is the cheap discriminant; `op(n)` is the rich value.
    for i in 0..insn.op_count() {
        match insn.op_kind(i) {
            // Fast typed accessors that skip the match for the common cases:
            OpKind::Register => {
                let r: Register = insn.op_register(i);
                let _ = (r.name(), r.class(), r.number());
            }
            OpKind::ImmUnsigned | OpKind::ImmSigned | OpKind::ImmLogical => {
                let _v: u64 = insn.op_immediate(i);
            }
            _ => {}
        }

        // Or match the full rich Operand for everything:
        match insn.op(i) {
            Operand::Reg { reg, arr, shift, extend, .. } => {
                let _ = (reg, arr, shift, extend);
            }
            Operand::ImmUnsigned(v) | Operand::ImmLogical(v) => { let _ = v; }
            Operand::ImmSigned(v) => { let _ = v; }
            Operand::MemImm { base, imm, mode } => { let _ = (base, imm, mode); }
            Operand::MemExt { base, index, extend, shift } => { let _ = (base, index, extend, shift); }
            Operand::Label(target) => { let _ = target; }
            Operand::Cond(c) => { let _ = c; }
            _ => {}
        }
    }
}

What each accessor means:

  • code() — the encoding-level identity (Code), one variant per distinct ARM ARM encoding row. Use this when you need the exact encoding (e.g. for re-encoding, or to distinguish B.cond from B).
  • mnemonic() / mnemonic().name() — the width/encoding-independent Mnemonic (alias-resolved for preferred disassembly such as MOV/CMP/LSL), and its &'static str spelling.
  • op_count() / op_kind(n) / op(n) — operand count, the OpKind discriminant of slot n (out-of-range yields OpKind::None), and the full rich Operand (out-of-range yields Operand::None).
  • op_register(n) / op_immediate(n) — fast indexed accessors. op_register returns Register::None if slot n is not a plain register; op_immediate returns the unsigned/logical/signed-as-u64/label value, or 0 otherwise.
  • len() / ip() / next_ip() / word() — fixed length (always 4), decode address, following address (ip + 4), and the raw little-endian word.
  • flow_control()FlowControl classification (branch / call / return / exception / next). set_flags()FlagEffect NZCV behaviour (SetsNormal, SetsFloat, or None).
  • implicit_registers() — the registers touched without an operand naming them (see below).

Implicit register reads and writes

A64 hides real dataflow behind the mnemonic. BL writes X30; PACIASP read-modifies X30 using SP; PACIA1716 read-modifies X17 using X16; LD64B <Xt> writes the eight registers Xt..Xt+7 while spelling only Xt; LDFF1* read-modifies the SVE FFR; ADR reads PC. None of that appears in the operand list.

implicit_registers() reports it as a fixed-capacity, allocation-free list. Architectural state with no numbered register is modelled as a pseudo-register appended to RegisterNzcv, Ffr, Za, Pc — which the decoder never produces as an operand and the formatter never prints (Register::is_pseudo() identifies them).

use fARM64::{implicit_registers, instruction_info, Decoder, DecoderOptions, OpAccess, Register};

fn main() {
    // `ld64b x0, [x1]` — loads x0..x7, but only x0 is spelled.
    let code = 0xF83F_D020u32.to_le_bytes();
    let mut dec = Decoder::new(&code, 0x1000, DecoderOptions::default());
    let insn = dec.decode();

    // The implicit part on its own: x1..x7 are written.
    let imp = implicit_registers(&insn);
    assert!(imp.writes(Register::X7));
    assert_eq!(imp.access_of(Register::X2), OpAccess::Write);

    // `instruction_info()` merges it with the explicit operands, so x1 — both
    // the address base and part of the destination group — reports once.
    let info = instruction_info(&insn);
    let x1 = info.used_registers().iter().find(|u| u.register == Register::X1).unwrap();
    assert_eq!(x1.access, OpAccess::ReadWrite);
}

PC is reported only where it is a genuine data input (ADR/ADRP, the PC-relative literal loads, the FEAT_PAuth_LR PAC*SPPC forms). Sequential fetch and branch-target formation are not reported — otherwise every instruction would read PC — and a branch's resolved absolute target is already available from near_branch_target().

See docs/API.md for the full rule table.


Formatting

The default FmtFormatter renders ARM UAL syntax. It writes through the Formatter trait into any FormatterOutput sink. There is a single operand-dispatch path; nothing allocates inside the formatter itself.

Zero-alloc into a fixed buffer or any core::fmt::Write

use core::fmt::Write;
use fARM64::{Decoder, DecoderOptions};
use fARM64::format::{Formatter, FmtFormatter, BufSink};

fn main() {
    let code = [0x20, 0x04, 0x00, 0x11];
    let mut dec = Decoder::new(&code, 0x1000, DecoderOptions::default());
    let insn = dec.decode();
    let fmt = FmtFormatter::new();

    // (a) Into a fixed [u8; N] via BufSink (no heap). Overflow is observable.
    let mut buf = [0u8; 64];
    let mut sink = BufSink::new(&mut buf);
    fmt.format(&insn, &mut sink);
    assert!(!sink.overflowed());
    let _text: &str = sink.as_str();

    // (b) Into any core::fmt::Write — the blanket impl makes it a sink.
    struct Counter(usize);
    impl Write for Counter {
        fn write_str(&mut self, s: &str) -> core::fmt::Result { self.0 += s.len(); Ok(()) }
    }
    let mut c = Counter(0);
    fmt.format(&insn, &mut c);
    let _ = c.0;
}

Owned String (requires alloc)

use fARM64::{Decoder, DecoderOptions};
use fARM64::format::{FmtFormatter, format_to_string};

fn main() {
    let code = [0x20, 0x04, 0x00, 0x11];
    let mut dec = Decoder::new(&code, 0x1000, DecoderOptions::default());
    let insn = dec.decode();

    let fmt = FmtFormatter::new();
    let s: String = format_to_string(&fmt, &insn);
    let _ = s;
}

FormatterOptions

FmtFormatter::with_options(opts) overrides the defaults. Fields and their defaults:

Field Default Meaning
aliases true Emit preferred aliases (MOV/CMP/MUL/LSL/NOP/...) instead of canonical forms.
uppercase_mnemonics false Upper-case mnemonics.
uppercase_registers false Upper-case register names.
use_sp_not_xzr true Render reg-31 as sp/wsp rather than xzr/wzr where the role is ambiguous.
hex_prefix "0x" Prefix for hex literals.
signed_immediates true Render signed immediates with an explicit - and hex magnitude.
show_lsl_zero false Show LSL #0 explicitly instead of eliding it.
space_after_operand_separator true ", " vs "," between operands.
first_operand_char_index 8 Column at which the first operand starts (mnemonic field width).
use fARM64::format::{FmtFormatter, FormatterOptions};

fn main() {
    let opts = FormatterOptions { uppercase_mnemonics: true, ..FormatterOptions::default() };
    let _fmt = FmtFormatter::with_options(opts);
}

GnuFormatter is available behind feature = "fmt-gnu". It is a compatibility adapter that currently delegates to the UAL renderer, so its output is identical to FmtFormatter; the separate type leaves room for GNU-specific policy later without changing call sites.

A token sink (FormatterOutput + TokenKind)

For syntax coloring or post-processing, implement FormatterOutput and receive every chunk together with its TokenKind:

use fARM64::{Decoder, DecoderOptions};
use fARM64::format::{Formatter, FmtFormatter, FormatterOutput, TokenKind};

struct TokenSink {
    mnemonics: usize,
    registers: usize,
}

impl FormatterOutput for TokenSink {
    fn write(&mut self, _text: &str, kind: TokenKind) {
        match kind {
            TokenKind::Mnemonic => self.mnemonics += 1,
            TokenKind::Register => self.registers += 1,
            _ => {}
        }
    }
}

fn main() {
    let code = [0x20, 0x04, 0x00, 0x11];
    let mut dec = Decoder::new(&code, 0x1000, DecoderOptions::default());
    let insn = dec.decode();

    let mut sink = TokenSink { mnemonics: 0, registers: 0 };
    FmtFormatter::new().format(&insn, &mut sink);
    let _ = (sink.mnemonics, sink.registers);
}

Resolving branch targets (SymbolResolver)

SymbolResolver maps an address to a borrowed name (no allocation required):

use fARM64::Instruction;
use fARM64::format::{SymbolResolver, SymbolResult};

struct MyResolver;

impl SymbolResolver for MyResolver {
    fn symbol(
        &mut self,
        _insn: &Instruction,
        _operand: usize,
        address: u64,
    ) -> Option<SymbolResult<'_>> {
        if address == 0x2000 {
            Some(SymbolResult { name: "my_func", offset: 0 })
        } else {
            None
        }
    }
}

fn main() {
    let mut r = MyResolver;
    // A formatter integration can call `r.symbol(insn, n, target)` for each
    // Label/Address operand to substitute a name for the bare 0x... target.
    let _ = &mut r;
}

Encoding

Instruction::encode() (or the free function fARM64::encode(&insn)) reconstructs the 32-bit little-endian word from the instruction's semantics — its Code, Mnemonic, operands, and ip. It deliberately never reads Instruction::word(), so a successful round-trip proves the decode is invertible. The encoder is no_std, zero-alloc, and total: it returns EncodeError rather than panicking.

use fARM64::{Decoder, DecoderOptions, EncodeError};

fn main() {
    let code = [0x20, 0x04, 0x00, 0x11]; // add w0, w1, #1
    let mut dec = Decoder::new(&code, 0x1000, DecoderOptions::default());
    let insn = dec.decode();

    // Decode -> (optionally inspect/modify) -> re-encode from semantics.
    let word: Result<u32, EncodeError> = insn.encode();
    match word {
        Ok(w) => {
            // Semantic round-trip: re-decoding `w` yields an equivalent instruction.
            let _ = w;
        }
        // Encodings the encoder does not yet cover return EncodeError::Unsupported.
        Err(e) => { let _ = e; }
    }
}

EncodeError variants: Unsupported (this Code/group is not implemented yet), InvalidOperand (operand missing or of the wrong kind), InvalidImmediate (an immediate, shift, or PC-relative target with no valid field encoding), and Invalid (the Code::Invalid sentinel has no encoding).

Because the encoder rebuilds from the canonical Code, the guarantee is a semantic round-trip (the re-encoded word decodes to an equivalent instruction), not necessarily a byte-identical one for encodings that have multiple equivalent spellings.

Re-encoding with different operands

Because the encoder works from semantics alone, editing a decoded Instruction and encoding it gives you the word for the edited instruction. That makes fARM64 usable as a small rewriter, not just a disassembler.

use fARM64::{Code, Condition, Decoder, DecoderOptions, Register};

fn main() {
    // add x0, x1, x2  ->  add x5, x1, x7
    let mut insn = Decoder::new(&0x8B02_0020u32.to_le_bytes(), 0, DecoderOptions::NONE).decode();
    assert!(insn.set_op_register(0, Register::X5));
    assert!(insn.set_op_register(2, Register::X7));
    assert_eq!(insn.encode(), Ok(0x8B07_0025));

    // ...and on to a different encoding entirely: sub x5, x1, x7
    insn.set_code(Code::SubShifted64);
    assert_eq!(insn.encode(), Ok(0xCB07_0025));

    // ldr x0, [x1, #8]  ->  ldr x0, [x3, #16], committed into word().
    let mut ldr = Decoder::new(&0xF940_0420u32.to_le_bytes(), 0, DecoderOptions::NONE).decode();
    assert!(ldr.set_memory_base(Register::X3));
    assert!(ldr.set_memory_displacement64(16));
    assert_eq!(ldr.re_encode(), Ok(0xF940_0860));
    assert_eq!(ldr.word(), 0xF940_0860);

    // b.eq 0x1008  ->  b.ne 0x1000
    let mut b = Decoder::new(&0x5400_0040u32.to_le_bytes(), 0x1000, DecoderOptions::NONE).decode();
    assert!(b.set_condition(Condition::Ne));
    assert!(b.set_near_branch_target(0x1000));
    assert_eq!(b.encode(), Ok(0x5400_0001));
}

The setters:

Setter Edits
set_op_register / set_op_register_unchecked the register of a single-register operand, keeping arrangement / lane / shift / extend / predicate
set_op_immediate an immediate's value, keeping its Operand variant (and so how it is packed)
set_op / push_op / set_op_count the operand list wholesale
set_memory_base / set_memory_index / set_memory_displacement64 a memory operand, keeping its addressing mode
set_condition the condition-code operand
set_near_branch_target / set_label a resolved absolute target
set_code / set_mnemonic the encoding identity / the displayed alias
set_ip / relocate the address — set_ip keeps labels at the same absolute address, relocate keeps them at the same relative displacement

Rules worth knowing:

  • Every setter is total: it returns false and changes nothing when the edit does not apply, rather than panicking.
  • set_op_register is class- and width-checked — it will not put a W register where an X register was, because the operand size lives in Code, not in the operand. It also enforces an operand shape's own range where one is narrower than the register file (the 3-bit predicate-as-counter PNg field takes only p8..p15). Pair set_op_register_unchecked with set_code to change the width deliberately.
  • Edits are not validated against the encoding. A value with no representation in the instruction's fields (a non-bitmask logical immediate, an out-of-range branch target, a displacement the encoding cannot scale, an ADRP target that is not 4 KiB-aligned) surfaces as an EncodeError from encode() — never a silently rounded value.
  • word() keeps returning the word the instruction was decoded from; is_modified() says it may be stale. re_encode() encodes and stores the new word in its place, and changes nothing on failure. Moving an instruction that is not PC-relative leaves both alone: set_ip/relocate only mark an instruction modified when its encoding depends on ip.

Feature gating explained

There are two independent layers:

  1. Cargo features decide which optional implementation modules are compiled into the binary. Omitting sve or sme leaves those large decoder/encoder modules out; crypto gates the Advanced SIMD crypto decoder. Public enums are not compiled out, and features such as FP16, BF16, LSE, PAuth, and MTE have no Cargo gate.
  2. The runtime FeatureSet decides what the decoder will accept at decode time. It is the fine-grained architectural gate and includes many extensions that are always compiled, as well as Apple AMX/GXF. An enabled runtime feature cannot restore an implementation module omitted by Cargo.
use fARM64::{Decoder, DecoderOptions, FeatureSet};

fn main() {
    // Restrict the decoder to the base ISA only: an SVE/SME/extension word will
    // be rejected (decoded as Code::Invalid) even when its implementation is compiled in.
    let options = DecoderOptions { features: FeatureSet::BASE };

    let code = [0x20, 0x04, 0x00, 0x11]; // a base-ISA ADD: still accepted
    let mut dec = Decoder::new(&code, 0x1000, options);
    let insn = dec.decode();
    let _ = insn.is_invalid();
}

FeatureSet::ALL (the default) accepts everything; FeatureSet::BASE/NONE accept only the base ISA; .with(Feature::X) enables one extension; .has(Feature::X) queries one. Feature::Base is always present.


no_std, embedded, and wasm

The default build is #![no_std] with no alloc: it links neither an allocator nor std. The core decode path (Decoder + Instruction) and the default formatter (FmtFormatter + BufSink) never allocate — all names are &'static str from const tables, there are no thread-locals, no I/O, no time, and no panics-as-control-flow. This makes fARM64 usable directly in kernels, bootloaders, hypervisors, and wasm32-unknown-unknown. The alloc and std features are strictly additive conveniences; enabling them never changes the zero-heap behaviour of the core path.


Validation and testing

fARM64 is validated with focused unit/integration tests plus optional differential sweeps. Large corpus-dependent sweeps are #[ignore]d and require a locally supplied corpus that is not included in the published crate:

  • Binary Ninja corpus comparisontests/golden.rs. Decodes a locally supplied corpus and compares rendered text.
    cargo test --features "std full" --test golden -- --ignored --nocapture
    
  • LLVM differentialtests/llvm_diff.rs. A discovery sweep that compares fARM64 with an installed llvm-mc.
    cargo test --features "std full" --test llvm_diff -- --ignored --nocapture
    
  • Encoder round-triptests/roundtrip.rs. Decodes the corpus, re-encodes from semantics, and checks the semantic round-trip.
    cargo test --features "std full" --test roundtrip -- --ignored --nocapture
    
  • Example CLIexamples/disasm.rs. Decodes 8-hex-digit words from args or stdin:
    cargo run --example disasm 11000420 d503201f
    

The fast (non-ignored) unit and integration tests run with cargo test --features "std full". See docs/VALIDATION.md for the reproducible validation procedure and the distinction between required tests and optional local-oracle sweeps.


Project layout and architecture

src/
  lib.rs          crate docs, public re-exports, MAX_OPERANDS / INSN_LEN, static asserts
  decoder.rs      Decoder, DecoderOptions, iterators, position/ip, last_error
  decode/         hand-written recursive A64 decode tree (+ shared ARM pseudocode)
  encode/         hand-written A64 encoder (the inverse of decode)
  instruction.rs  the Copy value-type Instruction and its accessors
  operand.rs      Operand enum + OpKind discriminant
  register.rs     Register, RegClass, RegWidth, gp_register
  enums.rs        Condition / ShiftType / ExtendType / VectorArrangement / FlowControl / FlagEffect
  mnemonic.rs     Code (encoding identity) + Mnemonic (display) enums
  features.rs     Feature + FeatureSet (runtime accept/reject)
  format/         Formatter trait, FmtFormatter, BufSink, options, token sink
  info/ sysop/ sysreg/ tables/   info factory, system ops/registers, name tables
tests/            golden.rs, llvm_diff.rs, roundtrip.rs, the_atomics.rs
examples/         disasm.rs
docs/             DESIGN.md, API.md, ENCODING.md, ROADMAP.md, VALIDATION.md

Design and reference docs: docs/DESIGN.md, docs/API.md, docs/ENCODING.md, docs/ROADMAP.md, docs/VALIDATION.md.


Status

Version 0.1.0 adds implicit register read/write analysis (implicit_registers(), plus the Nzcv/Ffr/Za/Pc pseudo-registers) and in-place instruction editing, so a decoded Instruction can be re-encoded with different operands. It also corrects several access-classification and flow-control results; see the changelog for the behaviour changes, notably that used_registers() now reports NZCV as Register::Nzcv. The checked-in test suite is the release gate; optional corpus and LLVM sweeps provide additional local cross-checking but are not packaged and no fixed coverage percentage is promised. The Code/Mnemonic/Register/Feature enums are #[non_exhaustive] with an append-only discriminant policy.

License

Licensed under the MIT License; see LICENSE. Arm architectural instruction handling is based on the publicly documented Arm ARM. Apple AMX naming and encodings reference the public corsix/amx reverse-engineering project; GXF encodings reference Asahi Linux's Apple Proprietary Instructions documentation and are isolated behind Feature::Gxf. See NOTICE for provenance details.