Architecture Overview
Kathryn2 is a Rust RTL hardware compiler with a Python frontend. The user describes digital hardware at the register-transfer level through a structured programmatic API — registers, wires, state machines, update events, modules, and control-flow graphs — and Kathryn2 compiles that model into clean, simulatable Verilog.
It is deliberately not a high-level synthesis (HLS) tool: it does not infer micro-architecture from algorithmic code. The user explicitly constructs the hardware model; the compiler’s job is to elaborate, wire, route, and emit it.
flowchart TB
subgraph PY["Python frontend — py/kathryn/"]
U["User Module subclasses<br/>@init declares hardware<br/>@flow describes control"]
end
subgraph CORE["Rust core — one process-wide ModelArena"]
M["Model<br/>Reg / Wire / Val / MemBlk<br/>nodes · flow blocks · modules"]
B["Build pass<br/>elaborate → wire → route"]
end
subgraph BE["Backends — read-only consumers"]
V["Verilog emitter<br/>src/backends/verilog/"]
end
U -- "PyO3 idents" --> M
M --> B
B -- "route IO" --> V
V --> OUT["Synthesizable Verilog"]
Design goals
Section titled “Design goals”The Rust port diverges from its C++ ancestor wherever raw-pointer ownership, virtual inheritance, or unchecked aliasing would be required. Four goals drive every structural decision in the codebase:
- Memory safety without
unsafe. Every model object has exactly one owner — the centralModelArena. All cross-references are lightweightCopyident handles (see the Ident pattern), never raw pointers,Rc, orRefCell. - Scalable dispatch without pervasive
match. Adding a new hardware component or update-event type touches exactly onematcharm; everything above that layer uses trait-object polymorphism (see Dispatch). - Deterministic compilation. The model is built in a deterministic order driven by a module trace stack and a flow-block init stack on the arena.
- Backend isolation. The Verilog emitter lives entirely under
src/backends/verilog/and never mutates the core data model, so alternative backends (VHDL, simulation) are straightforward to add.
Crate layout
Section titled “Crate layout”All code lives in a single library crate (src/lib.rs). The native binary
(src/main.rs, bin name kathryn_cli) is a thin shell over it, and the PyO3
extension (module kathryn._kathryn) is built from the same library via
maturin. PyO3 is an optional dependency behind the python Cargo feature,
so the default cargo build compiles zero PyO3 macros.
src/ common/ ArenaGroup<T>, ArenaHandle, ArenaNode<T> — the generational arena primitive (arena_base.rs) model/ the core data model common/ IdentBase, Identifiable trait, GLOBAL_MODEL_ID controller/ ClockMode policy hw_component/ Reg / Wire / Val / IoWire / MemBlk / Expression / sp_regs, update events, UpdatePool, Slice, AssignMeta nodes/ the nine control-flow node types (AsmNode, StateNode, ...) flow_block/ flow-block types (seq / par / cond / loops / wait / pipeline / zync) and their wiring schematics module/ Module + ModuleIdent hierarchy complex_hardware/ Karray, Arb (composite components) model_arena.rs ModelArena — every ArenaGroup field arena_impl.rs ModelArena::new / reset, module CRUD, trace stacks backends/ model consumers (read + elaborate, never redesign) common/ module DFS iterator, IoWire helpers, cross-module routing verilog/ HcpBaseVb / VerilogUpdateEvent traits, per-type emitters applications/ the PyO3 binding layer (feature-gated, mirrors src/model/) py/ debug/ debug flag/config system params/ compile parameters util/ file and math helperspy/kathryn/ the pure-Python DSL that wraps the bindingThe per-category arena_factory_*.rs / arena_impl_*.rs files that extend
ModelArena sit next to the types they manage — for example
src/model/hw_component/arena_impl_hwc.rs and
src/model/nodes/arena_factory_node.rs. See
Factories & CRUD for the full map.
The three build phases
Section titled “The three build phases”Every module passes through three initialisation stages, tracked per module on
the arena’s module_trace_stack. The enum lives in
src/model/model_arena.rs:
#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum ModuleInitStage { CompInit, FlowBlockInit, FlowBlockBuild,}CompInit— hardware components are created through the factory API and registered directly into the module on top of the trace stack.FlowBlockInit— flow blocks are assembled and nested; finished blocks attach either to their parent block or, at the top level, to the module.FlowBlockBuild— the build pass synthesises the node graph into real hardware (state registers, trigger expressions, update events). HCPs created during this stage are buffered inhcp_pending_bufferand drained into the owning module when the pass ends.
Factory methods route new components by the current stage — see
stamp_hw_to_parent_module in src/model/hw_component/arena_factory_hwc.rs:
during CompInit/FlowBlockInit the component goes straight into the module,
during FlowBlockBuild it is buffered.
The full pipeline: Python declaration → Verilog
Section titled “The full pipeline: Python declaration → Verilog”A user design travels through five layers:
# 1. Declare — pure-Python DSL (py/kathryn/)set_top(Top()) # register the user's top Module (its @init ran eagerly)gen_flow() # 2. Construct every module's deferred @flow blocksbuild_flow() # 3. Host build pass over the whole module tree
# 4-5. Route IO and emit Verilogbackend = BackendVerilog(arena)backend.emit("out_dir", "top")- Declaration (Python DSL,
py/kathryn/).Modulesubclasses declare hardware in@initmethods (run eagerly at construction, inside a module scope on the trace stack) and control flow in@flowmethods (deferred into a process-wide pool). Every DSL wrapper holds only a Rust ident; all state lives in one process-wideModelArena. - Flow construction (
gen_flow()). Each registered flow method re-opens its module’s scope atFlowBlockInitand builds its flow-block tree viainitialize_flow_block/finalize_flow_block(src/model/arena_impl.rs). - Model build (
build_flow()).ModelArena::build_flow(src/model/arena_impl.rs) takes the top module out of the arena and callsModule::build_flow_as_top_module, which creates the primitiveclk/ master-reset input wires and the start node, builds every flow block’s hardware (FlowBlockBase::build_common_hw→build_hw_component), and recurses into sub-modules. This call is not re-runnable — it asserts a fresh start node. - Cross-module IO routing.
route_and_remap_io_model(src/backends/common/routing.rs) walks the module tree, finds cross-module signal references, threadsIoWirechains through the hierarchy, and rewrites every dependency handle to point at the local IO wire. See IO Routing. - Verilog emission.
BackendVerilogtakes ownership of the arena (the Python wrapperPyBackendVeriloginsrc/applications/py/backends/verilog/backend_py.rsmoves it out withstd::mem::replace) and emits the module tree through theHcpBaseVb/VerilogUpdateEventtrait pair. See Verilog Emission.
Where to go next
Section titled “Where to go next”Core infrastructure — the mechanics everything else is built on:
- The Model Arena — generational slots, why dangling handles are impossible.
- The Ident Pattern — copy-by-value handles,
IdentBase, the_inaming convention. - Factories & CRUD —
make_*/mk_*factories and the per-categoryarena_impl_*CRUD surface. - Dispatch — trait-object dispatch, the single-match rule, compile-enforced type lists.
- Memory Model — ownership, take/replace-back, reset, global IDs.
The model — the hardware description itself:
Backend and bindings: