chipi-cli 0.7.1

CLI tool for generating instruction decoders from .chipi files
chipi-cli-0.7.1 is not a library.

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
cargo install chipi-cli

# Generate decoder/disassembler from a config file
chipi gen --config chipi.toml

# Generate decoder from CLI args
chipi gen --output src/generated/dsp.rs src/dsp.chipi

# Override dispatch for one sub-decoder
chipi gen --config chipi.toml --dispatch-for GcDspExt=jump_table

# Dry run (print to stdout)
chipi gen --dry-run src/dsp.chipi

# Dump parsed IR for debugging
chipi gen --dump-ir src/dsp.chipi

# Generate emulator dispatch LUT from config
chipi lut --config chipi.toml

# Generate handler stubs (one-shot, CLI only)
chipi stubs --output src/handlers.rs src/cpu.chipi

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
[[gen]]
input = "../../specs/dsp.chipi"
lang = "rust"
output = "$OUT_DIR/dsp.rs"

[gen.dispatch_overrides]
GcDspExt = "jump_table"

[gen.type_map]
reg5 = "crate::dsp::DspReg"

# Emulator dispatch LUT target
[[lut]]
input = "../../specs/gekko.chipi"
output = "$OUT_DIR/gekko_lut.rs"
handler_mod = "crate::cpu::interpreter"
ctx_type = "crate::Cpu"
instr_type = "crate::cpu::Instruction"
lut_mod = "crate::cpu::lut"
instr_type_output = "$OUT_DIR/gekko_instr.rs"

[lut.groups]
branch = ["bx", "bcx", "bclrx", "bcctrx"]
alu = ["addi", "addis", "ori", "oris"]

Rust build.rs usage

For Rust projects, use chipi-build in build.rs:

[build-dependencies]
chipi-build = { path = "path/to/chipi/chipi-build" }

Config-driven

// build.rs: processes all [[gen]] and [[lut]] targets from chipi.toml
fn main() {
    chipi_build::run_config("chipi.toml").expect("chipi codegen failed");
}

Programmatic

// build.rs: decoder/disassembler generation
fn main() {
    chipi_build::generate("src/dsp/gcdsp.chipi")
        .type_map("reg5", "crate::dsp::DspReg")
        .dispatch_default(chipi_build::Dispatch::FnPtrLut)
        .dispatch_for("GcDspExt", chipi_build::Dispatch::JumpTable)
        .output("src/dsp/generated/gcdsp.rs")
        .run()
        .expect("chipi codegen failed");
}
// build.rs: emulator dispatch LUT generation
fn main() {
    let out_dir = std::env::var("OUT_DIR").unwrap();
    chipi_build::lut("cpu.chipi")
        .handler_mod("crate::cpu::interpreter")
        .ctx_type("crate::Cpu")
        .instr_type("crate::cpu::Instruction")
        .lut_mod("crate::cpu::lut")
        .group("alu", ["addi", "addis", "ori", "oris"])
        .group("branch", ["bx", "bcx"])
        .output(format!("{out_dir}/cpu_lut.rs"))
        .instr_type_output(format!("{out_dir}/cpu_instr.rs"))
        .run()
        .expect("chipi lut failed");
}

chipi-build automatically emits cargo:rerun-if-changed for all input files and their includes.

Using generated code

mod dsp {
    include!(concat!(env!("OUT_DIR"), "/gcdsp.rs"));
}

match dsp::GcDspInstruction::decode(&data[offset..]) {
    Some((instr, bytes)) => {
        println!("{}", instr);
        offset += bytes;
    }
    None => println!("invalid instruction"),
}

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:

struct MyFormat;
impl ppc::PpcFormat for MyFormat {
    fn fmt_addi(rd: u8, ra: u8, simm: i32, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "ADDI r{}, r{}, {}", rd, ra, simm)
    }
}

println!("{}", instr.display::<MyFormat>());

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:

chipi stubs --output src/cpu/interpreter.rs cpu.chipi

This produces:

pub fn addi(_ctx: &mut Ctx, _opcode: u32) { todo!("addi") }
pub fn lwz(_ctx: &mut Ctx, _opcode: u32) { todo!("lwz") }
// ... 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::cpu::interpreter::alu::<{ OP_ADDI }>

// Expected handler signature
pub fn alu<const OP: u32>(ctx: &mut crate::Cpu, instr: crate::cpu::Instruction) {
    match OP {
        OP_ADDI  => { /* ... */ },
        OP_ADDIS => { /* ... */ },
        _ => unreachable!(),
    }
}

Instruction type generation

When instr_type and instr_type_output are set (in config or builder), chipi generates a newtype with field accessors:

pub struct Instruction(pub u32);
impl Instruction {
    #[inline] pub fn rd(&self) -> u8 { ((self.0 >> 21) & 0x1f) as u8 }
    #[inline] pub fn ra(&self) -> u8 { ((self.0 >> 16) & 0x1f) as u8 }
    #[inline] pub fn simm(&self) -> i32 { (((((self.0 & 0xffff) as i32) << 16) >> 16)) as i32 }
}

Syntax highlighting

VS Code extension