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

4.1 KiB
Raw Permalink Blame History

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

go install github.com/zg/asm-calc/cmd/asm-calc@latest

Use

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:

    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.

go test ./...