Assembler+ is a two-pass ARM64 assembler that supports three input formats:
- Pre-tokenized (
--tokenized, default) — the CS241 scanner output format (TOKEN_TYPE lexeme) - Raw assembly (
--raw) — standard ARM64 assembly text - High-level pseudocode (
--high) — a readable, C-like syntax
All three modes produce identical machine code for equivalent programs.
make # produces ./asm
make clean # removes the binaryRequires a C++20-compatible compiler (e.g. g++ or clang++).
asm [OPTIONS] [FILE]
| Flag | Description |
|---|---|
--tokenized |
Input is pre-tokenized format (default) |
--raw |
Input is raw ARM64 assembly |
--high |
Input is high-level pseudocode |
--dump-ir |
(--high only) Print IR to stderr instead of assembling |
--help, -h |
Show usage |
If FILE is omitted or is -, reads from stdin. Binary output goes to stdout; labels are printed to stderr.
./asm --raw program.s > program.bin
./asm --high program.hl > program.bin
./asm --high --dump-ir program.hl # inspect the IR without assembling
cat tokens.txt | ./asm > program.bin| Instruction | Syntax | Description |
|---|---|---|
add |
add xd, xn, xm |
xd = xn + xm |
sub |
sub xd, xn, xm |
xd = xn − xm |
mul |
mul xd, xn, xm |
xd = xn × xm (low 64 bits) |
smulh |
smulh xd, xn, xm |
xd = (xn × xm) >> 64 (signed) |
umulh |
umulh xd, xn, xm |
xd = (xn × xm) >> 64 (unsigned) |
sdiv |
sdiv xd, xn, xm |
xd = xn ÷ xm (signed) |
udiv |
udiv xd, xn, xm |
xd = xn ÷ xm (unsigned) |
cmp |
cmp xn, xm |
Set flags for xn − xm |
b |
b label |
Unconditional branch |
b.cond |
b.eq label |
Conditional branch (eq, ne, lt, le, gt, ge, hs, lo, hi, ls) |
br |
br xn |
Branch to register |
blr |
blr xn |
Branch-with-link to register |
ldr |
ldr xd, offset |
PC-relative load |
ldur |
ldur xd, [xn, imm] |
Load from base + offset |
stur |
stur xd, [xn, imm] |
Store to base + offset |
.8byte |
.8byte value |
Emit a 64-bit constant |
The --high mode accepts a pseudocode language that is lowered to ARM64 instructions before assembly.
| Syntax | Lowers to |
|---|---|
label <name> |
<name>: |
x1 = x2 + x3 |
add x1, x2, x3 |
x1 = x2 - x3 |
sub x1, x2, x3 |
x1 = x2 * x3 |
mul x1, x2, x3 |
x1 = x2 / x3 |
sdiv x1, x2, x3 |
x1 = x2 % x3 |
sdiv → mul → sub sequence |
x1 = x2 |
add x1, x2, xzr (move) |
x1 = *x2 |
ldur x1, [x2, 0] |
x1 = *(x2 + 8) |
ldur x1, [x2, 8] |
*x1 = x2 |
stur x2, [x1, 0] |
*(x1 + 8) = x2 |
stur x2, [x1, 8] |
if x1 == x2 goto lbl |
cmp x1, x2 + b.eq lbl |
if x1 != x2 goto lbl |
cmp x1, x2 + b.ne lbl |
if x1 < x2 goto lbl |
cmp x1, x2 + b.lt lbl |
if x1 <= x2 goto lbl |
cmp x1, x2 + b.le lbl |
if x1 > x2 goto lbl |
cmp x1, x2 + b.gt lbl |
if x1 >= x2 goto lbl |
cmp x1, x2 + b.ge lbl |
goto lbl |
b lbl |
call x1 |
blr x1 |
ret |
br x30 |
.8byte val |
.8byte val |
# comment |
ignored |
# sum x1 += x3 while x1 != x2
label loop
if x1 == x2 goto done
x1 = x1 + x3
goto loop
label done
ret
With --dump-ir, this produces the following target-independent IR:
loop:
CMP_BRANCH x1 == x2, done
ADD x1, x1, x3
BRANCH loop
done:
RET
Which is then lowered to the equivalent raw assembly:
loop:
cmp x1, x2
b.eq done
add x1, x1, x3
b loop
done:
br x30The --high pipeline follows a classic compiler architecture with an explicit IR lowering pass:
Source code IR Tokens Binary
┌──────────┐ ┌───────────┐ ┌──────────────┐ ┌──────────────┐
│HighLevel │───▶│ IR │───▶│ IRCodeGen │───▶│ Assembler │
│Parser │ │ (target- │ │ (instruction │ │ (two-pass │
│ │ │ indep.) │ │ selection) │ │ encode) │
└──────────┘ └───────────┘ └──────────────┘ └──────────────┘
--dump-ir
The --raw and --tokenized pipelines skip the IR and feed tokens directly into the assembler.
├── main.cpp # Entry point — mode selection & I/O
├── token.h # Token struct, TokenType enum, I/O operators
├── lexer.h # TokenizedLexer (CS241 format), RawAsmLexer (raw text)
├── ir.h # IRInstruction — target-independent intermediate representation
├── highlevel.h # HighLevelParser — pseudocode → IR
├── ir_codegen.h # IRCodeGen — IR → ARM64 Token lowering (instruction selection)
├── symbol_table.h # SymbolTable — label definition & lookup
├── encoder.h # Encoder — instruction validation & machine code encoding
├── assembler.h # Assembler — two-pass orchestration
├── Makefile
└── README.md
| Module | Responsibility |
|---|---|
| Token | Data types shared across all stages |
| Lexer | Convert input text → Token stream (two strategies) |
| IR | Target-independent intermediate representation (IRInstruction) |
| HighLevelParser | Parse pseudocode → vector<IRInstruction> (frontend) |
| IRCodeGen | Lower IR → ARM64 Token stream (instruction selection) |
| SymbolTable | Track label → address mappings |
| Encoder | Validate operands and emit 32-bit machine code per instruction |
| Assembler | Group tokens into lines, run pass 1 (symbols) and pass 2 (encode + emit) |