chipi
A declarative instruction decoder generator. Define your CPU's instruction encoding in a portable .chipi DSL file, and chipi generates decoders, disassemblers, and emulator dispatch code.
.chipi files are language-agnostic: they describe bit patterns, field extractions, and display formats with zero language-specific information. All language-specific configuration (type mappings, output paths, dispatch strategies) lives in a project-level chipi.toml or is passed as CLI arguments.
An example disassembler for the GameCube CPU and (partially) DSP can be found here.
Architecture
.chipi files --> Parser --> Decoder IR --> Backend --> generated source
^
chipi.toml + CLI args --------+
Three crates:
| Crate | Purpose |
|---|---|
chipi-core |
Parser, IR, validation, code generation backends |
chipi-cli |
Standalone CLI (chipi gen, chipi lut, chipi stubs) |
chipi-build |
build.rs helper for Rust projects |
CLI
# Install the chipi CLI tool
# Generate decoder/disassembler from a config file
# Generate decoder from CLI args
# Override dispatch for one sub-decoder
# Dry run (print to stdout)
# Dump parsed IR for debugging
# Generate emulator dispatch LUT from config
# Generate handler stubs (one-shot, CLI only)
chipi.toml
A single config file can define both decoder ([[gen]]) and emulator LUT ([[lut]]) targets.
Paths support $VAR environment variable expansion (e.g. $OUT_DIR/lut.rs), useful in build.rs contexts.
Relative paths are resolved from the TOML file's directory.
# Decoder/disassembler target
[[]]
= "../../specs/dsp.chipi"
= "rust"
= "$OUT_DIR/dsp.rs"
[]
= "jump_table"
[]
= "crate::dsp::DspReg"
# Emulator dispatch LUT target
[[]]
= "../../specs/gekko.chipi"
= "$OUT_DIR/gekko_lut.rs"
= "crate::cpu::interpreter"
= "crate::Cpu"
= "crate::cpu::Instruction"
= "crate::cpu::lut"
= "$OUT_DIR/gekko_instr.rs"
[]
= ["bx", "bcx", "bclrx", "bcctrx"]
= ["addi", "addis", "ori", "oris"]
Rust build.rs usage
For Rust projects, use chipi-build in build.rs:
[]
= { = "path/to/chipi/chipi-build" }
Config-driven
// build.rs: processes all [[gen]] and [[lut]] targets from chipi.toml
Programmatic
// build.rs: decoder/disassembler generation
// build.rs: emulator dispatch LUT generation
chipi-build automatically emits cargo:rerun-if-changed for all input files and their includes.
Using generated code
match decode
DSL
Decoder block
decoder Ppc {
width = 32 # bits per instruction unit
bit_order = msb0 # msb0 or lsb0
endian = big # big or little
max_units = 1 # optional safety guard
}
Instructions
addi [0:5]=001110 rd:u8[6:10] ra:u8[11:15] simm:simm16[16:31]
| ra == 0: "li {rd}, {simm}"
| "addi {rd}, {ra}, {simm}"
- Fixed bits:
[range]=pattern(use?for wildcards) - Fields:
name:type[range] - Format lines:
| [guard:] "template"
Custom types
type simm16 = i32 { sign_extend(16) }
type addr = u32 { shift_left(2), zero_extend(32) }
type simm16 = i32 { sign_extend(16), display(signed_hex) }
Builtin types: bool, u1-u7, u8, u16, u32, i8, i16, i32
Transforms: sign_extend(n), zero_extend(n), shift_left(n)
Display hints: display(hex), display(signed_hex)
Format strings
- Field references:
{field},{field:#x} - Ternary:
{flag ? yes : no} - Arithmetic:
{a + b * 4},{-field} - Map calls:
{spr_name(spr)} - Builtins:
{rotate_right(val, amt)},{rotate_left(val, amt)} - Sub-decoder fragments:
{ext.mnemonic} - Guards:
| ra == 0: "li {rd}, {simm}"(supports==,!=,<,<=,>,>=) - Escapes:
\{,\},\?,\:
Maps
map spr_name(spr) {
1 => "xer"
8 => "lr"
9 => "ctr"
_ => "???"
}
Includes
include "dsp_ext.chipi"
Resolved relative to the including file. Circular includes are detected and rejected.
Variable-length instructions
Bit positions beyond width - 1 reference subsequent units:
decoder GcDsp {
width = 16
bit_order = msb0
endian = big
max_units = 2
}
# 2-unit instruction: bits [16:31] are in the second unit
lri [0:10]=00000010000 rd:u5[11:15] imm:u16[16:31]
| "lri r{rd}, #0x{imm:04x}"
Sub-decoders
Sub-decoders decode a bit-field within a parent instruction using a separate dispatch table:
subdecoder GcDspExt {
width = 8
bit_order = msb0
}
ext_dr [0:5]=000001 r:u8[6:7]
| .mnemonic = "'DR"
| .operands = " : $ar{r}"
ext_nop [0:5]=000000 [6:7]=??
| .mnemonic = ""
| .operands = ""
Reference from a parent instruction:
addr [0:3]=0100 [4]=0 ss:u8[5:6] d:u8[7] ext:GcDspExt[8:15]
| "ADDR{ext.mnemonic} $ac{d}, ${ax2_name(ss)}{ext.operands}"
Formatting trait
The generated trait allows per-instruction display overrides:
;
println!;
Dispatch strategies
| Strategy | Rust |
|---|---|
fn_ptr_lut |
static [Option<fn>; N] (default) |
jump_table |
#[inline(always)] fn + match |
Handler stubs
Stubs are generated via the CLI only. They are one-shot scaffolding meant to be edited by hand:
This produces:
// ... one stub per instruction
Grouped handlers with const generics
.group() (in config or builder) folds multiple instructions into one handler via const OP: u32:
// Generated LUT entry
crate OP_ADDI }>
// Expected handler signature
Instruction type generation
When instr_type and instr_type_output are set (in config or builder), chipi generates a newtype with field accessors:
;