Standalone ARM64 assembler

afs-as

How afs-as is structured, what its CLI guarantees, which directives it supports today, and how the compiler uses the library API underneath.

Open repository

Overview

afs-as is the assembler in the toolchain stack. It parses ARM64 assembly text, encodes instructions, and writes Mach-O MH_OBJECT files for macOS. The public surface is intentionally small: one input, one output, explicit failure modes, and no hidden driver behavior.

The repo layout mirrors the pipeline. src/lex.rs tokenizes assembly, src/parse.rs builds the directive and instruction model, src/encode.rs turns that model into ARM64 instruction words, src/assemble.rs coordinates the full source path, and src/macho.rs writes the final object file.

It is usable both as a standalone binary and as a library. That split matters because the compiler-facing fast path can skip textual parsing entirely by constructing valid instruction values directly.

Build and standalone use

From the armfortas workspace root or the standalone repo root:

cargo build -p afs-as
cargo test -p afs-as
cargo clippy -p afs-as --all-targets -- -D warnings

The core standalone flows from the README are still the right starting point:

afs-as hello.s -o hello.o
afs-as hello.s
cat hello.s | afs-as - -o - > hello.o
ld hello.o -o hello -lSystem -syslibroot $(xcrun --show-sdk-path) -e _main
./hello

The binary is deliberately strict. Usage errors exit 2; parse or assembly failures exit 1; help and version exit 0.

Supported surface

The README documents the public standalone support matrix and the parser tests in src/parse.rs back it up. The supported section set is intentionally narrow and Mach-O specific:

  • __TEXT,__text, __TEXT,__cstring, __TEXT,__literal16, __TEXT,__const
  • __DATA,__data, __DATA,__bss, __DATA,__thread_data, __DATA,__thread_vars, __DATA,__thread_bss

Supported directive families include:

  • symbol directives like .globl, .extern, .private_extern, .weak_reference, and .weak_definition
  • data and layout directives like .byte, .short, .word, .quad, .ascii, .asciz, .space, .zero, .fill, .align, .p2align, .comm, .zerofill, and .tbss
  • metadata directives like .subsections_via_symbols, .build_version, linker optimization hints under .loh, and the supported CFI subset

Unsupported forms are expected to fail loudly. That is a feature, not a gap: the project prefers explicit incompatibility over silently emitting a wrong object file.

CLI reference

The full standalone parser lives in src/main.rs and does not depend on clap. The accepted flags are intentionally tiny:

FlagBehaviorNotes
<input.s>Assemble one input file into a Mach-O object.Only a single input file is supported.
-Read assembly from stdin.When stdin is used, `-o <path>` or `-o -` is required.
-o <path>Write the object file to an explicit path.If omitted for file input, the output path defaults to `<stem>.o`.
-o -Write the object bytes to stdout.Useful for piping or embedding in other tooling.
--Stop option parsing.Allows an input path that begins with a dash.
--help, -hPrint the usage text and exit 0.The usage block in `src/main.rs` is the full standalone CLI contract.
--version, -VPrint `afs-as <version>` and exit 0.Version comes from Cargo package metadata.

Two details are easy to miss:

  • stdin input does not infer an output path; you must specify -o explicitly.
  • default object naming always rewrites the input extension to .o.

Library API and integration

The library surface is defined in src/lib.rs. There are two main ways to use it:

use afs_as::assemble;
use afs_as::encode::Inst;
use afs_as::reg::*;

let obj = assemble::assemble_source(".global _main\n_main:\nret\n").unwrap();

let obj = assemble::assemble_instructions(
    &[Inst::Ret { rn: X30 }],
    &["_main"],
);

assemble_source is the source-text entry point. It gives you lexical, parsing, and source-context diagnostics. assemble_instructions is the compiler-facing fast path. It assumes the caller has already built valid instruction values and will panic if those values violate encoder preconditions.

That contract is important if you wire afs-as into armfortas. The assembler library is fast and direct, but correctness of the instruction stream becomes the caller’s responsibility.

Diagnostics and tests

User-facing errors are designed to be legible: file, line, column, source line, and a caret pointing at the failure site. That behavior lives in the assembly pipeline and is validated by snapshot and smoke tests in the repo.

cargo test -p afs-as
cargo test -p afs-as --test cli_smoke
cargo test -p afs-as --test verify_against_system_as

The test surface is broad. It includes unit tests for parsing and encoding, differential tests against Apple as, raw-object parity suites, linker and runtime end-to-end tests, malformed-input mutation tests, and generated stress coverage.

Practical reading: if you need to know whether a directive family is intended to be supported, check the README and then confirm in the parser tests. The project uses those tests as the real compatibility boundary.