Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Code Generation

Code generation takes the FIR control-flow graph and produces target machine code, without involving GCC, LLVM, or any external tool.

Machine Lowering

Machine lowering translates FIR to a target-specific machine IR. For RP2040 (flint-backend), the target is Thumb v6-M (ARMv6-M).

Instruction Selection

FIR instructions are mapped to machine instructions:

FIR operationThumb v6-M instruction(s)
Integer addADDS Rd, Rn, Rm
Integer subtractSUBS Rd, Rn, Rm
Load fieldLDR Rd, [Rn, #offset]
Store fieldSTR Rd, [Rn, #offset]
Conditional branchBEQ, BNE, BLT, BGE, …
Function callBL target
ReturnBX LR

The Thumb v6-M encoding is dense: most instructions are 16-bit, producing compact binaries suitable for the RP2040’s 2 MB flash.

Register Allocation

FIR values are assigned to physical ARM registers or stack slots. The RP2040 has 16 registers (R0-R15):

  • R0-R3: argument and return registers (also caller-saved scratch)
  • R4-R7: callee-saved (low registers, accessible to all Thumb v6-M instructions)
  • R8-R11: callee-saved (high registers, limited access in v6-M)
  • R12: scratch (IP)
  • R13: stack pointer (SP)
  • R14: link register (LR)
  • R15: program counter (PC)

The register allocator performs linear scan allocation. Values that cannot be kept in registers are spilled to the stack frame.

ABI

Flint follows the ARM Procedure Call Standard (APCS / AAPCS) for RP2040:

  • First four arguments in R0-R3.
  • Additional arguments on the stack.
  • Return value in R0 (or R0-R1 for 64-bit values).
  • Caller saves R0-R3, R12. Callee saves R4-R11, R14.

Parameter modes:

  • Copy scalars: passed in registers by value.
  • Default (read-only borrow) non-copy: passed as a hidden address register.
  • mut (mutable borrow) non-copy: passed as an exclusive hidden address register.
  • Large aggregates: passed as hidden pointer per ABI rules.

Stack Frames

Every function that uses local variables or calls other functions has a stack frame:

High address
┌─────────────────┐
│  Caller's frame │
├─────────────────┤  ← SP on entry
│  Saved LR       │  (if function calls others)
│  Saved R4-R7    │  (callee-saved registers used)
│  Local variables│
│  Spill slots    │
└─────────────────┘  ← SP during function body
Low address

The compiler emits PUSH on entry and POP on exit for callee-saved registers. Local variable layout is computed from type sizes and alignment requirements.

Instruction Encoding

Thumb v6-M instructions are encoded to 16-bit or 32-bit binary. The encoder converts the machine IR’s instruction objects directly to bytes with no intermediate assembly text format.

All relocations (cross-function calls, PC-relative data references) are resolved after instruction encoding:

  1. Each function is encoded to a byte buffer.
  2. Relocation entries record the offset and target for each unresolved reference.
  3. The linker pass assigns final addresses to all sections and symbols.
  4. Relocations are patched into the byte buffer.

Section Layout

The final image is organized into ELF sections:

SectionContents
.textExecutable code
.rodataRead-only data (string literals, constants, embedded assets)
.dataInitialized mutable data (copied from flash to RAM at startup)
.bssZero-initialized data

For RP2040:

  • .text and .rodata live in flash at 0x10000000.
  • .data and .bss live in RAM at 0x20000000.
  • The boot2 second-stage bootloader is prepended to the flash image.

Image Packaging

ELF: Standard 32-bit ARM ELF executable (ET_EXEC) with section headers and a symbol table containing all emitted functions. Used by probe-rs, OpenOCD, and GDB. The flint-elf crate constructs the ELF from the same raw binary image, adding ELF headers, a PT_LOAD segment mapped to the flash base address, and function symbols from the artifact layout. DWARF debug information is not yet emitted.

BIN: The raw .text + .data binary, stripped of all ELF metadata. Just the bytes that go on flash.

UF2: The flint-uf2 crate wraps the BIN into UF2 format:

  • UF2 family ID for RP2040 (0xe48bff56)
  • 256-byte data blocks with 476-byte UF2 headers
  • Correct flash start address and block count
  • The Pico’s USB bootloader validates the family ID and block structure before accepting the file

No LLVM, No GCC

Everything described above (instruction selection, register allocation, encoding, relocation, image packaging) is implemented in Rust inside the Flint compiler crates. There is no dependency on LLVM IR, GCC’s assembler, GNU ld, or any external compiler infrastructure.

This is a core design requirement: the compiler is self-contained.