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:
| function | purpose |
|---|---|
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:
cargo run -p codegen # ts + python + gocargo run -p codegen -- ts # one targetcargo run -p codegen -- --check # exit non-zero if any target driftedThe --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:
- Add the path constants to
paths.rs:pub const FOO_CHI_REL: &str = "thrum-clients/foo/...";plus thefoo_chi()/foo_helpers()functions. - Add
emit_foo(spec, out)andemit_foo_helpers(out)tolib.rs. - Add a match arm in
main.rs’srun_targetfor the new target name. - Add the two
emit_*calls to the loop inthrum-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.rswith macros or move the enum to a different file, codegen breaks until updated. Keepchi.rsshaped the way the existing parser expects. - Not a
thrum-coreconsumer. By design, this crate has zero dependency on the Rust runtime types it reflects, so it can sit insidethrum-core/build.rs.
See also
thrum-core: the canonical home of the chi enum andTHRUM_VERSION.thrum-clients/ts,thrum-clients/python,thrum-clients/go: the generated reflections.- WIRE.md: the language-neutral wire spec the clients implement.