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 repositoryOverview
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 warningsThe 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
./helloThe 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:
| Flag | Behavior | Notes |
|---|---|---|
<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, -h | Print the usage text and exit 0. | The usage block in `src/main.rs` is the full standalone CLI contract. |
--version, -V | Print `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
-oexplicitly. - 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_asThe 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.