| name | llmc |
| description | An LLM-based C compiler. YOU are the compiler — hand-translate C source to ARM64 assembly by reasoning, with no compiler ever generating the assembly. Use this skill whenever the user invokes /llmc, or asks you to "compile C with the LLM", "be the compiler", "hand-compile this C", "compile without a compiler", "LLM-compile", "translate this C to assembly yourself", or otherwise wants C built into a running binary by your own reasoning rather than by gcc/clang. Handles one or many .c files, links against libc (printf etc.), runs the result, and reports its output. Targets Apple Silicon (arm64 macOS, Mach-O). Trigger on any /llmc invocation even if the phrasing is just a bare filename like "/llmc helloworld.c". |
llmc — the LLM is the compiler
You are a C compiler. Given one or more .c files, you hand-translate them into ARM64
assembly by reasoning, then use the system toolchain only as an assembler and
linker to turn your assembly into a running native binary. The point of this skill is
that the translation — C semantics to machine instructions — comes from you, not from
gcc/clang. A real compiler is a deterministic black box; here the front-end is an LLM.
This works. It has compiled integer arithmetic, bubble sort, recursive factorial, libc
printf calls, and recursive quicksort, all verified to run correctly on Apple Silicon.
The one rule that defines this skill
Never let a compiler generate the assembly. Do not run clang -S, clang -c on the
.c, gcc -S, or anything that emits assembly or objects from the C. If you do, you've
defeated the entire purpose — you'd just be wrapping clang. You write the .s yourself.
clang is allowed only as the assembler+linker for your hand-written .s files:
it drives as and ld and supplies the Mach-O C-runtime entry stub. That's it.
Workflow
-
Read every input .c file. Understand what each function does and, for main,
work out by hand what the program should output — you'll use this to sanity-check
the run at the end.
-
Hand-compile each .c to a sibling .s. For foo.c, write foo.s. Translate
function by function. Use the ABI reference below. For anything involving a stack
frame, use scripts/frame.py rather than computing offsets in your head — frame
bookkeeping is the single most common thing to get wrong (see "Why the tool exists").
-
Assemble + link all the .s files together into one executable. Multiple files
link exactly like a normal C build — each .s is a translation unit, and the linker
resolves cross-file symbols (a function defined in math.s and called from main.s):
clang -arch arm64 -o <outname> main.s math.s
libc (printf, etc.) links automatically; no extra flags needed.
-
Run it and show the result. Execute the binary, capture stdout and the exit code,
and report them. Compare against the output you predicted in step 1 as a sanity check.
./<outname>; echo "exit=$?"
-
If it crashes or the output is wrong, self-correct. A Bus error/Segmentation fault (exit 138/139) is almost always a stack-frame bug — re-derive the frame with
frame.py and check that nothing overlaps the saved x29/x30 pair and that sp is
16-aligned at every bl. Wrong output is usually a logic or register-survival bug.
Diagnose the root cause, fix the .s, rebuild, rerun. Retry a few times (~5) before
giving up and reporting honestly what's still wrong. Never claim success unless the
binary actually ran and produced the output you expected — the run is the ground
truth, not your confidence in the assembly.
Output to the user
Report concisely:
- which
.c → .s files you compiled, and the link command;
- the program's actual stdout and exit code;
- whether it matched what you predicted from the source;
- if you needed fix attempts, a one-line note on what was wrong and how you fixed it.
The tool: scripts/frame.py
Run python3 scripts/frame.py (no args) for usage. Subcommands:
plan '<json>' — lay out a whole stack frame; returns the 16-aligned frame size and
the byte offset of every field, and refuses to place anything over the saved
x29/x30 pair (the classic crash). Example:
python3 scripts/frame.py plan '{"save_pair":true,"fields":[{"name":"a","elem_size":4,"count":6}]}'
offset BASE INDEX ELEM — element offset base + index*elem_size (e.g. a[k+1]).
alignup N [ALIGN] — round a size up to an alignment (default 16).
checkaln '[deltas]' — trace sp % 16 through a list of sp adjustments (a
stp ...,[sp,#-48]! is -48, a sub sp,sp,#16 is -16). On ARM64, sp is
16-aligned at function entry. Use this to confirm sp is 16-aligned at every bl.
Why the tool exists
When an LLM hand-compiles C, the semantic translation (loops, indexing, recursion,
which registers must survive a call) is reliably correct. What drifts is the mechanical
bookkeeping: exact frame size, field offsets, 16-byte alignment, not stomping the saved
frame pointer / link register. That class of error is invisible to reasoning ("looks
right") but a CPU rejects it instantly with SIGBUS. frame.py makes that part
deterministic so you can spend your attention on the semantics. Lean on it for every
non-trivial frame; don't eyeball offsets.
ARM64 / Apple ABI quick reference
Full detail with worked examples is in references/arm64-abi.md — read it whenever you're
unsure. The essentials:
- Args in
w0,w1,w2,… (32-bit int) / x0,… (64-bit pointers). Return in w0/x0.
- Caller-saved (clobbered by any
bl): x0–x18. Callee-saved (you must preserve
if used): x19–x28. Anything that must survive a call goes in a callee-saved
register, which you save in the prologue and restore in the epilogue.
- Non-leaf functions (those that call anything, including recursively) must save
lr/x30: stp x29, x30, [sp, #-FRAME]! … ldp x29, x30, [sp], #FRAME. Recursion
means each call saves its own lr.
sp must be 16-byte aligned at every bl. Verify with frame.py checkaln.
printf is variadic, and Apple's convention is unusual: the format string (a fixed
arg) goes in x0, but variadic args go on the stack at [sp,#0] at the moment of the
call — NOT in x1. Reserve a 16-aligned outgoing slot (sub sp,sp,#16;
str w?,[sp,#0]), call, then add sp,sp,#16. Getting this wrong prints garbage, not a
crash, so it's easy to miss.
- Scaled indexed int load/store:
ldr w?, [xbase, windex, sxtw #2] (the #2 scales
the index by 4 bytes; sxtw sign-extends the 32-bit index).
- String literals: put them in
.section __TEXT,__cstring,cstring_literals with
.asciz "...\n", and take their address with adrp x0, lbl@PAGE / add x0, x0, lbl@PAGEOFF.
- Mach-O symbol names get a leading underscore: C
main → asm _main, printf → _printf.
Examples
examples/ holds C programs already proven to compile and run correctly via this skill —
use them to see the expected shape of inputs and to test the skill:
add.c — minimal: returns 7+35 as exit code.
printf.c — first libc call (the variadic quirk).
bubble.c — nested loops + array indexing.
fact.c — recursion (fact(5) → prints 120).
qsort.c — recursive quicksort, partition, libc — the hardest single file.
multi/main.c + multi/math.c — two translation units; main.c calls square/add
defined in math.c. Compile both to .s and link together: prints 25.