Backends
One Rail compiler, six targets. All driven from the same rail_native binary.
| Backend | Verb | Output | Toolchain needed | Status (local-verified) |
|---|---|---|---|---|
| macOS ARM64 (Mach-O) | _(default)_ / run | /tmp/rail_out | none (host as+ld) | runs end-to-end |
| Linux ARM64 (ELF) | linux | /tmp/rail_linux | aarch64-elf-as, aarch64-elf-ld | needs cross-binutils |
| Linux x86_64 (ELF, source-only) | x86 | /tmp/rail_x86.s | remote gcc (or Docker linux/amd64) | emits .s, 71/79 conformance via Docker harness |
| WebAssembly | wasm | /tmp/rail_out.wasm | wat2wasm (wabt) | compiles end-to-end |
| Cortex-M4 (Thumb-2 ELF) | cortexm | /tmp/rail_m4.elf | clang --target=thumbv7em-none-eabi | compiles end-to-end |
| RISC-V rv32imc (ELF) | riscv32 | /tmp/rail_rv32.elf | clang --target=riscv32-unknown-none-elf | compiles end-to-end |
macOS ARM64 (Mach-O)
This is the primary target. No flags, no setup.
./rail_native run examples/hello.rail
Compiling examples/hello.rail (244 chars)...
as: OK
ld: OK
hello, rail
3628800
42
rail_native uses the system's as and ld directly. No libc; the runtime (allocator, GC, string ops, list ops) is ARM64 assembly emitted inline.
Linux ARM64 (ELF)
./rail_native linux examples/hello.rail
If you have aarch64-elf-as and aarch64-elf-ld on PATH (look for aarch64-elf-binutils via Homebrew or your distro), this produces /tmp/rail_linux. scp it to a Pi Zero 2 W and run.
Without the cross-tools:
Cross-compiling examples/hello.rail for Linux ARM64...
as: /bin/sh: /opt/homebrew/bin/aarch64-elf-as: No such file or directory
ld: /bin/sh: /opt/homebrew/bin/aarch64-elf-ld: No such file or directory
Binary: /tmp/rail_linux
/tmp/rail_linux: cannot open `/tmp/rail_linux' (No such file or directory)
To install on macOS:
brew install aarch64-elf-binutils
Linux x86_64 (source-only)
./rail_native x86 examples/hello.rail
Compiling examples/hello.rail (244 chars)...
Assembly: /tmp/rail_x86.s (1495 chars)
No local x86_64 assembler. scp to target and build with: gcc -o prog /tmp/rail_x86.s
Try: scp /tmp/rail_x86.s <user>@<host>:~ && ssh <user>@<host> 'gcc -o rail_x86 rail_x86.s && ./rail_x86'
Rail emits the GAS assembly directly; assemble on the target with gcc -o prog /tmp/rail_x86.s. This is intentional — the x86_64 backend was bootstrapped for the Razer WSL test fleet rather than for cross-tools-on-mac. The x86 runtime asm lives at tools/x86_rt.s.
\1 bash tools/test/x86_conformance.sh (requires Docker + linux/amd64 image, e.g., Colima/Rosetta) currently passes \1 representative tests covering ints, strings, lists, ADTs, closures, floats, FFI, TCO, and arena ops. The 8 remaining failures are 3 ELF-prefix ffi-libc tests and 5 missing str-runtime symbols, all classified in ~/.claude/projects/-Users-user/memory/x86_backend_status.md.
WebAssembly
./rail_native wasm examples/wasm/hello.rail
Compiling examples/wasm/hello.rail to WASM...
WAT: 51277 bytes
wat2wasm: OK
Binary: /tmp/rail_out.wasm
Requires wat2wasm from the WebAssembly Binary Toolkit (brew install wabt). The output is /tmp/rail_out.wasm — drop into any WASM runtime that provides the env.print import. The live playground at https://ledatic.org embeds exactly this output.
Known WASM-backend limits (see CLAUDE.html): no filter/map/fold as WASM builtins, 1 MB linear memory. Closures, ADTs, pattern matching, and string ops all work.
Cortex-M4 (Thumb-2)
./rail_native cortexm examples/m4_uart_hello.rail
Compiling examples/m4_uart_hello.rail -> Cortex-M4 (Thumb-2)...
Assembly: /tmp/rail_m4.s (1084 chars)
as: OK -> /tmp/rail_m4.o
startup as: OK
ld: OK -> /tmp/rail_m4.elf
Requires clang with the thumbv7em-none-eabi target. On macOS: brew install llvm (Apple's bundled clang doesn't include the bare-metal targets). The driver also assembles the startup vector (tools/cortexm_rt/startup.s) and links into /tmp/rail_m4.elf.
To verify in qemu (no real hardware needed for CMSDK UART programs):
qemu-system-arm -M mps2-an386 -nographic -kernel /tmp/rail_m4.elf
RISC-V rv32imc
./rail_native riscv32 /tmp/test_riscv.rail
Compiling /tmp/test_riscv.rail -> RISC-V (rv32imc)...
Assembly: /tmp/rail_rv32.s (314 chars)
as: OK
startup: OK
ld: OK -> /tmp/rail_rv32.elf
Requires clang with riscv32-unknown-none-elf target. The minimal main = 42 program boots in qemu-system-riscv32 -M virt -bios none -kernel /tmp/rail_rv32.elf and exits 42 via the SiFive test register.
The rv32 backend is v0 — literals only. See memory:riscv32_backend_v0 for the supported subset.
Adding a backend
Look at tools/compile.rail around line 6605: the cmd dispatch table. Each backend is roughly:
1. A new "emit" pass that walks the AST.
2. A driver function (compile_cortexm, compile_riscv32, etc.) that calls the right assembler/linker via shell.
3. A startup file under tools/cortexm_rt/ or equivalent.
Cortex-M4 took about 600 lines of compiler code; RISC-V was about 300 because Thumb-2 paved the road.