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.
Backends
| Backend | Lang | Output | Usage |
|---|---|---|---|
rust |
Rust | Decoder enum + decode() + Display |
build.rs or CLI |
cpp |
C++ | Single-header decoder with std::format |
CLI or CMake |
ida |
Python | IDA Pro 9.x processor module | CLI |
binja |
Python | Binary Ninja Architecture plugin | CLI |
Examples
| Project | Description |
|---|---|
| chipi-gekko | GameCube CPU & DSP disassembler (Rust) |
| chipi-gekko-cpp | GameCube CPU disassembler (C++) |
| gc-dsp-ida | GameCube DSP processor plugin for IDA Pro 9.x |
| gc-dsp-binja | GameCube DSP processor plugin for Binary Ninja |
| chipi-spec | Reusable .chipi specs |
| chipi-vscode | VS Code syntax highlighting for .chipi files |
Note: The IDA and Binary Ninja generators backends are highly experimental and do not replace fully fledged processor modules.
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
# Generate decoder/disassembler (Rust, C++, IDA, or Binary Ninja)
# Override dispatch for a sub-decoder
# Dry run (print to stdout)
# Dump parsed IR for debugging
# Generate emulator dispatch LUT from config
# Generate handler stubs
chipi.toml
A single config file can define multiple targets across backends.
Paths support $VAR environment variable expansion (e.g. $OUT_DIR/lut.rs).
Relative paths are resolved from the TOML file's directory.
# Rust decoder
[[]]
= "specs/dsp.chipi"
= "rust"
= "$OUT_DIR/dsp.rs"
[]
= "jump_table"
[]
= "crate::dsp::DspReg"
# C++ decoder (single-header)
[[]]
= "specs/gekko.chipi"
= "cpp"
= "generated/gekko.hpp"
[]
= ["types.hpp"]
[]
= "Gpr"
= "Fpr"
# IDA Pro processor module
[[]]
= "specs/dsp.chipi"
= "ida"
= "dsp_proc.py"
[]
= "gcdsp"
= "GameCube DSP"
= 0x8002
= 16
= 2
= ["ar0", "ar1", "ar2", "ar3", "CS", "DS"]
= ["CS", "DS"]
[]
= ["callcc"]
= ["retcc"]
= ["halt"]
# Binary Ninja architecture plugin
[[]]
= "specs/dsp.chipi"
= "binja"
= "dsp_arch.py"
[]
= "gcdsp"
= 2
= 2
= "BigEndian"
= ["ar0", "ar1", "ar2", "ar3"]
# Emulator dispatch LUT (Rust only)
[[]]
= "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:
[]
= "0.8.0" # or whatever is latest
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
C++ backend
The C++ backend generates a single header with enum class Opcode, a tagged Instruction struct, decode(), and format() (using std::format).
Type aliases with display(hex) or display(signed_hex) automatically generate wrapper types (Hex, SignedHex) with std::formatter specializations. Custom types like registers are provided by the user via type_map and a user-written header with std::formatter specializations.
[]
= "Gpr" # user provides Gpr type with std::formatter<Gpr>
= "Fpr"
[]
= ["types.hpp"] # user's type definitions
// types.hpp (provided by user)
;
;
IDA backend
Generates a self-contained IDA Pro 9.x processor module (Python). Requires lang_options with processor metadata, register names, and optional flow analysis hints. See the chipi.toml example above.
Binary Ninja backend
Generates a Binary Ninja Architecture plugin (Python). Implements get_instruction_info, get_instruction_text, and a stub get_instruction_low_level_il.
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), display(signed_hex) }
type addr = u32 { shift_left(2), zero_extend(32) }
type gpr = u8
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 (Rust)
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:
;