Skip to content

codegen

the Rust chi registry is canonical; this crate emits its TypeScript, Python, and Go reflections so consumers in those languages cannot drift.

thrum-core/src/chi.rs is the only place a chi value is defined. The wire spec (WIRE.md) is the contract everybody else conforms to, and the chi registry lives in Rust because the daemon and the in-process ensemble routing already live in Rust. Anyone writing a bee in another language needs the same list of chi values, the same THRUM_VERSION, and the same handful of runtime helpers (sigil, rid, dusk, WaneTracker). This crate is what gives them that, by parsing chi.rs as plain text and emitting per-language reflections under thrum-clients/{ts,python,go}/.

Why a separate crate

Two reasons.

First, no cycle on thrum-core. If codegen depended on thrum-core, then thrum-core/build.rs could not depend on codegen (cargo refuses circular [build-dependencies]). So codegen parses chi.rs and lib.rs as text using regex. It never uses thrum-core. That keeps the build graph clean.

Second, the binary is for humans, the library is for build.rs. cargo run -p codegen is the manual lever for regenerating clients (and for --check in CI). thrum-core/build.rs calls the same library functions on every cargo build of thrum-core, so the clients refresh automatically as the Rust enum changes. Same code path, two entry points.

Layout

codegen/
├─ Cargo.toml
└─ src/
├─ lib.rs parse + 6 emit_* functions (ts/py/go × chi/helpers)
├─ main.rs CLI: per-target dispatch, --check drift gate
└─ paths.rs every literal repo path lives here (const + helper fn)

src/paths.rs

Single source of truth for the repo layout codegen knows about. Two kinds of items, paired:

pub const CHI_RS_REL: &str = "thrum-core/src/chi.rs";
pub fn chi_rs() -> PathBuf { repo_root().join(CHI_RS_REL) }

The *_REL constants are the repo-relative strings, used inside emitted @generated headers (so the headers stay truthful through renames). The *() functions resolve to absolute paths for disk I/O. build.rs and main.rs both go through these. Nothing in the crate hand-writes "thrum-core/..." or "thrum-clients/..." literals.

src/lib.rs

Public surface:

functionpurpose
parse(chi_rs, lib_rs)Read chi.rs + lib.rs, return a ChiSpec (version, chi variants, pulse variants, doc comments).
emit_ts(spec, out)Write chi.ts to out.
emit_py(spec, out)Write chi.py to out.
emit_go(spec, out)Write chi.go to out.
emit_helpers(out)Write helpers.ts (sigil, rid, dusk, WaneTracker).
emit_py_helpers(out)Same for Python.
emit_go_helpers(out)Same for Go.

All emit_* functions take a destination Path. Path resolution is the caller’s job: the canonical sites come from paths::*.

src/main.rs

CLI dispatcher. No-arg invocation regenerates all three targets:

Terminal window
cargo run -p codegen # ts + python + go
cargo run -p codegen -- ts # one target
cargo run -p codegen -- --check # exit non-zero if any target drifted

The --check path emits to a tempfile and byte-compares against the checked-in artifact, so CI can gate on it without needing a separate manifest of expected outputs.

How a generated client gets refreshed

chi.rs (edit)
cargo build -p thrum-core
├─ build.rs sees chi.rs changed
├─ calls codegen::parse(&paths::chi_rs(), &paths::lib_rs())
└─ calls codegen::emit_{ts,py,go} + emit_{ts,py,go}_helpers
writing into thrum-clients/{ts,python,go}/

The TypeScript, Python, and Go files are checked in (not gitignored). That keeps consumers of those clients buildable without a Rust toolchain. CI drift-checks them with cargo run -p codegen -- --check so a missed regen fails the build instead of silently rotting.

Output anatomy

Each generated chi file carries a header like:

// @generated by `cargo run -p codegen` from thrum-core/src/chi.rs — DO NOT EDIT.
//
// Rust is the canonical home of the wire registry. Hand-edit chi.rs;
// the file is regenerated on every cargo build of thrum-core (build.rs).
// Manual regen: `cargo run -p codegen`.

The source path in that header comes from paths::CHI_RS_REL, so a directory rename updates every header on the next build. The runtime-helpers files name thrum-core/src/{prims,wane}.rs for the same reason (via paths::HELPERS_SOURCE_REF).

Adding a target

To add a new client language:

  1. Add the path constants to paths.rs: pub const FOO_CHI_REL: &str = "thrum-clients/foo/..."; plus the foo_chi() / foo_helpers() functions.
  2. Add emit_foo(spec, out) and emit_foo_helpers(out) to lib.rs.
  3. Add a match arm in main.rs’s run_target for the new target name.
  4. Add the two emit_* calls to the loop in thrum-core/build.rs.

After that, cargo run -p codegen writes the new artifacts and --check gates drift on them.

What this crate is not

  • Not a parser for arbitrary Rust. It pattern-matches enum definitions and a small set of doc comment shapes. If you rewrite chi.rs with macros or move the enum to a different file, codegen breaks until updated. Keep chi.rs shaped the way the existing parser expects.
  • Not a thrum-core consumer. By design, this crate has zero dependency on the Rust runtime types it reflects, so it can sit inside thrum-core/build.rs.

See also