Master the fundamental concepts of instruction set architecture through this focused micro-challenge.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksWriting one function in x86-64, ARM64, and RISC-V assembly exposes how ISA design shapes code size, register pressure, and encoding complexity.
asmLoading…
For example, adding two integers might take three instructions on x86 (move, add, return) but two on ARM and RISC-V because arguments already sit in w0/a0 and w1/a1.
For this exercise, you will implement add_numbers stubs for each architecture and compare instruction counts and byte sizes. This task asks you to connect encoding differences to Apple's Arm transition and LLVM's separate backend code generators.
Keep the relevant datasheet, ISA manual, or architecture textbook chapter open while you implement. When your output disagrees with the reference trace on the same program, the bug is usually a mis-decoded opcode, a stale register read, or a flag bit left unchanged after arithmetic.
For this exercise, you will use those habits while implementing the requirement in the starter code. Microarchitectural product names change across CPU generations, but the control ideas (fetch, bypass, cache lines, vector lanes) stay stable enough to debug from first principles.
Write a tiny code generator with three back ends. It compiles one small integer function to x86-64, ARM64 and RISC-V (RV64IM), then compares instruction counts and code size. You'll see the CISC/RISC split directly. x86 has two-operand instructions and variable-length encodings, so short immediates save bytes. ARM64 and RISC-V have three-operand instructions and every instruction is 4 bytes.
One function per line:
cLoading…
Up to 4 int parameters. Operands are parameter names or decimal constants 0..2047. OP is +, - or *. The expression is evaluated strictly left to right (no precedence), just like an accumulator: a + b * c means (a + b) * c.
Parameters arrive in edi, esi, edx, ecx (x86-64), w0–w3 (ARM64) and a0–a3 (RISC-V); the result is returned in eax / w0 / a0. Every function ends with ret.
x86-64 accumulates in eax:
| Code | Bytes |
|---|---|
first operand: mov eax, REG / mov eax, K | 2 / 5 |
add/sub eax, REG · imul eax, REG | 2 · 3 |
add/sub eax, K | 3 if K ≤ 127, else 5 |
imul eax, eax, K | 3 if K ≤ 127, else 6 |
ret | 1 |
ARM64 / RISC-V (4 bytes each). Operation i computes DST = SRC1 op OPERAND. SRC1 is the first operand for the first operation and the previous DST after that. DST is the return register for the last operation and the temporary (w8 / t0) before it.
| Case | ARM64 | RISC-V |
|---|---|---|
| first operand is a constant | mov w8, #K (then SRC1 = w8) | li t0, K (SRC1 = t0) |
| register operand | add/sub/mul DST, SRC1, wN | add/sub/mul DST, SRC1, aN |
+ K | add DST, SRC1, #K | addi DST, SRC1, K |
- K | sub DST, SRC1, #K | addi DST, SRC1, -K |
* K | mov w9, #K then mul DST, SRC1, w9 | li t1, K then mul DST, SRC1, t1 |
With no operator, the body is only the move into the return register: mov w0, wN, mov w0, #K, mv a0, aN, li a0, K. It is omitted when the operand is already the first parameter.
cLoading…
Print a blank line between functions. On an unknown operand, print only NAME: error: unknown operand X; on a constant above 2047, print only NAME: error: constant K out of range.
Input:
cLoading…
Output:
cLoading…
Hidden tests cover constant operands of both sizes, a constant first operand, multiplication by a constant, a function with no operator, and both errors.