Files
zzg a22c99344e
ci / test (push) Canceled after 0s
initial commit
2026-09-25 22:12:55 -07:00

110 lines
4.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# asm-calc
A calculator built around x86-64 assembly: it parses arithmetic expressions
and compiles them to real x64 instructions, then executes those instructions
to produce the answer.
```
$ asm-calc '2 + 3 * 4'
14
$ asm-calc -S '2 + 3 * 4'
mov rax, 2 ; literal 2
push rax ; save left operand
mov rax, 3 ; literal 3
push rax ; save left operand
mov rax, 4 ; literal 4
mov rcx, rax ; right operand
pop rax ; restore left operand
imul rax, rcx ; multiply
mov rcx, rax ; right operand
pop rax ; restore left operand
add rax, rcx ; add
```
## Install
```sh
go install github.com/zg/asm-calc/cmd/asm-calc@latest
```
## Use
```sh
asm-calc '1 + 2 * 3' # evaluate
asm-calc -S '1 + 2 * 3' # print the assembly
asm-calc -S -c 'x = 42' # complete assemblable source file
asm-calc # REPL (:asm, :vars, :quit)
```
Operators: `+ - * / % ^` and parentheses, unary minus, and assignment with
`=`. Variables persist across REPL lines.
## How it runs the code
The compiler emits genuine x86-64. Executing it takes one of two paths:
- **A built-in simulator** of the emitted instruction subset, which is what
runs by default. It models the registers, the stack, frame-relative
memory, and `idiv`'s use of the `RDX:RAX` pair including its two fault
conditions.
- **A real CPU**, via `-S -c`, which emits a complete NASM source file with
a `_start` entry point. Assembling it needs `nasm` and an x86-64 host:
```sh
asm-calc -S -c '6 * 7' > calc.asm
nasm -f elf64 calc.asm -o calc.o && ld calc.o -o calc
./calc; echo $? # 42
```
The simulator exists so the emitted code is testable *everywhere*, including
on machines that are not x86-64 — which is where this was developed. Without
it, the test suite would only be meaningful on one architecture.
## Design notes
**Operands go via the stack, not a second register.** Each binary operation
evaluates its left side into RAX, pushes it, evaluates its right side, then
pops. That costs a push and pop per operation, but means expressions of any
depth compile without running out of registers — a 200-level nested
expression is a test case.
**`cqo` before every `idiv`.** `idiv` divides the 128-bit `RDX:RAX` pair by
its operand, so RDX must hold the sign extension of RAX. Omitting it gives
wrong answers *only for negative dividends*, which is why `-10 / 2` and
`-17 % 5` have their own test rather than relying on the general arithmetic
cases.
**Operand order is checked explicitly.** The stack strategy leaves the right
operand in RAX and the left on the stack — backwards for subtraction and
division. Commutative tests cannot catch a mistake here, so the
non-commutative cases are tested separately.
**Exponents are constant-folded.** `2^8` unrolls to repeated `imul`, which
requires the exponent be known at compile time since this instruction subset
has no jumps. The first version required a literal and so rejected
`2 ^ 3 ^ 2` — which is right-associative, making the exponent the
*expression* `3 ^ 2`. Exponents are now folded, so any constant expression
works.
**Division faults are modelled, not ignored.** Division by zero and the one
overflow case — the most negative value divided by −1, whose quotient is not
representable — both raise the errors a real CPU would, rather than
panicking the host.
**Frames are 16-byte aligned**, as the ABI requires; an unaligned stack
crashes any callee that touches SSE.
## Tests
The strongest test is differential: **3,000 randomly generated expressions**
are compiled, executed on the simulator, and compared against an independent
tree-walking evaluator that shares none of the compiler's logic. That covers
precedence, associativity and operand ordering together, across far more
cases than a table could enumerate. Push/pop balance is asserted separately —
a program that leaks stack would corrupt real execution.
```sh
go test ./...
```