DiscoC is a small compiler toolkit for specialized hardware targets. The current code-generation backend targets the SuperFX/GSU processor; the repository also contains the initial target model for a future SPC-700 backend.
The normal object-file path is:
.dc source
|
v
Lexer -> Parser -> AST -> Analyzer -> Optimizer
|
v
IRLowerer
|
v
IRVerifier
|
v
target backend selection
/ \\
v v
IRCodeGenerator future SPC700 backend
|
v
relocatable .o
|
v
discld linker
|
v
linked target payload
The compiler also exposes two inspection or alternate-emission paths:
discc --emit-astprints the optimized abstract syntax tree.discc --emit-irprints the verified IR and its basic blocks.discc --emit-asmwrites textual GSU assembly. That assembly can be passed todiscasto create a relocatable object file.
The linked .bin is a GSU payload. It is not a complete SNES ROM image: it does not provide a SNES header, host-side startup integration, cartridge metadata, or other ROM-level resources.
The lexer converts source text into tokens. The recursive-descent parser constructs an owning AST using std::unique_ptr for child nodes. Syntax errors are reported before semantic analysis begins.
The analyzer runs before AST transformations. This ordering guarantees that
an optimization cannot erase an invalid expression before it receives a
diagnostic. The optimizer then operates on resolved SymbolId references;
current transformations include recognizing suitable loops for the GSU
hardware LOOP instruction and simplifying selected small arithmetic
operations. Transformations are deliberately conservative when a loop value
is observable outside the loop or control flow can escape it.
The analyzer resolves functions, scopes, variables, structures, ROM data, types, pointer operations, and control-flow-related semantic rules. It also calculates stack offsets and local allocation sizes used by the backend.
Function prototypes may declare a function without defining its body. The prototype participates in semantic checking of calls in that compilation unit, while the definition can be emitted by another source file and linked later. Prototypes do not generate code or duplicate object-file symbols.
IRLowerer converts the analyzed AST into a typed, control-flow-aware IR. IRVerifier checks structural invariants before code generation. This keeps target-independent compiler structure separate from GSU or SPC-700 byte encoding.
The default object path uses IRCodeGenerator. It consumes only verified IR plus analyzed symbol/data information and emits GSU instructions into the project object format.
The SPC700Target model records the SPC-700 address width, memory-mapped
regions, register roles, and initial return-value convention. It is a
foundation-only target at present: --emit-ir can inspect programs selected
for SPC-700, while object and assembly emission reject that target until its
lowering and assembler stages exist.
Before emission, IRCodeGenerator runs a linear-scan allocation over the
verified IRValueId live intervals. Reused values may reside in R5, R7, or
R8; R0 remains the expression accumulator and R1/R3 remain backend
temporaries. Values that do not fit, or are used only once, are rematerialized
from their defining IR instruction. Calls preserve allocated live values while
the existing stack-based argument ABI remains unchanged.
The AssemblyGenerator remains available for human-readable assembly export. It is useful for inspection and for the discas workflow, but it is a separate textual backend and should not be treated as the canonical implementation of every high-level feature.
Each compilation unit can produce a relocatable .o file. The object stores
code, ROM data, exported symbols, and relocation records. All multi-byte object
fields use explicit little-endian encoding. discld verifies that all input
objects use the same target configuration, concatenates code and data sections,
resolves symbols, applies relocations, and writes the final payload. Local
branches that exceed the short displacement range are relaxed by discas into
object-relative absolute jumps.
The linker currently lays out all code before all data. Symbol addresses are calculated from the configured code start address and the accumulated section offsets.
For switches, constant selectors are lowered to a direct branch. Dynamic switches with four or more cases use a balanced comparison tree; smaller switches retain the compact linear form. Case blocks and fall-through edges remain explicit in the IR, so this is a dispatch optimization only.
The compiler targets the GSU instruction set and ABI described in Nintendo's official SNES Development Manual, Book II, Super FX section. The generated code follows the project's documented GSU conventions while keeping external assembly support optional.
The relevant register conventions used by the compiler are:
| Register | Convention |
|---|---|
R0 |
expression result and first return-value register |
R9 |
frame pointer used by generated functions |
R10 / SP |
stack pointer |
R11 |
link/return address register |
R12 |
hardware-loop counter when a LOOP is emitted |
R13 |
hardware-loop target register when required by setup |
R14 |
ROM buffer/address register for ROM reads |
R15 / PC |
program counter and call target register |
Generated functions save the link and frame registers, establish R9 as the frame pointer, allocate aligned local storage, and restore the frame before returning. Parameters use positive frame-pointer offsets beginning at FP + 4; locals use negative offsets. Stack arguments are word-aligned.
The ABI classifies R9 and R11 as callee-preserved. R0, R1, R3, and
the allocated value registers R5, R7, and R8 are caller-preserved; the
caller saves any allocated value that remains live across a call. R10 is
owned by the stack frame, R12/R13 are reserved for hardware loops, R14
is the ROM address register, and R15 is the program counter. Every argument
occupies one aligned two-byte stack slot, including byte arguments.
The object format carries the target, memory mapping, and code start address.
The supported target identifiers are GSU and SPC700; the supported SNES
mappings are LoROM and HiROM. The linker rejects a set of input objects
when their target configurations are incompatible.
Build-level target selection is provided on the command line, while source directives configure target-specific placement details:
set memory_mapping = lorom;
set code_start_address = 0x8000;discc --target gsu program.dc -o program.oThe SPC-700 target model and ABI proposal are documented in
spc700-target.md.
AST nodes own their child nodes. IR graphs do not store pointers into resizable instruction or block vectors; values and blocks are referenced by stable numeric IDs owned by an IRFunction. The backend builds short-lived lookup tables while processing one function and does not make the IR own target byte buffers.
This separation is intentional: the AST and IR are compiler-phase data, while ObjectFile owns the emitted code/data vectors and serialized object contents.
The project is pre-release compiler infrastructure. Register allocation is
still conservative and uses rematerialization rather than explicit spill slots
when register pressure exceeds the current pool. The assembly-export path also
has narrower feature coverage than the IR binary backend for some advanced
constructs. These limitations should be considered when using --emit-asm as
a source of hand-edited assembly.