Verilog Emission
The Verilog backend (src/backends/verilog/) consumes a fully built
ModelArena and writes one .v file per module. It follows the same
architectural rules as the core model — arena ownership, take/replace_back,
and single-match dispatch — so adding a component or event type never touches
the emitter’s control flow.
Backend entry point and phases
Section titled “Backend entry point and phases”BackendVerilog (src/backends/verilog/backend.rs) owns the arena outright:
pub struct BackendVerilog { model_arena: ModelArena }
impl BackendVerilog { // Takes ownership of the arena — the backend is the final consumer. pub fn new(model_arena: ModelArena) -> Self { ... } pub fn emit(&mut self, output_dir: &str, top_file_name: &str) { self.phase_route(); // IO routing passes self.phase_init(); // sort every UpdatePool self.phase_emit(output_dir, top_file_name); // write .v files }}- Phase 1 — route:
route_and_remap_io_model(LCA-based cross-module routing) thenroute_glob_io_model(IO-marked signals to the top) — see I/O Routing. - Phase 2 — init:
Module::init_module_vb(src/backends/verilog/module/module_vb.rs) sorts every HCP’sUpdatePoolby(priority, sub_priority)and recurses into sub-modules. - Phase 3 — emit: a
DfsModuleIterwalk over the module tree; each module is taken from the arena andgen_module_blk_vbwrites it through aFileWriter(src/util/file/file_writer.rs). The top module’s file is named by the caller; every other module uses its global module name.
flowchart LR
A["BackendVerilog::new<br/>(takes ModelArena)"] --> B["emit(output_dir, top_file_name)"]
B --> P1["phase_route<br/>route_and_remap_io_model<br/>then route_glob_io_model"]
P1 --> P2["phase_init<br/>init_module_vb<br/>(sort every UpdatePool)"]
P2 --> P3["phase_emit<br/>DfsModuleIter walk<br/>gen_module_blk_vb -> FileWriter"]
P3 --> V["one .v file per module"]
The ownership move is real and one-way: the Python wrapper
(src/applications/py/backends/verilog/backend_py.rs) constructs the backend
with std::mem::replace(&mut arena.arena, ModelArena::new()), leaving the
PyModelArena empty. Combined with routing mutating the model and
build_flow not being re-runnable, emission is effectively a destructive,
single-shot operation — build a fresh model if you need to emit again.
The module tree mirror
Section titled “The module tree mirror”The backend’s file layout mirrors src/model/: one *_vb.rs file per HCP
type, common/ for the traits, module/ for the module-level emitter, and
arena_ext_vb.rs for dispatch. Backend code adds methods to core types via
extra impl blocks (e.g. impl Module in module_vb.rs,
impl ModelArena in arena_ext_vb.rs) but only through the public
take_*/replace_back_* surface — never the pub(super) arena fields.
The HcpBaseVb trait
Section titled “The HcpBaseVb trait”Every emittable HCP implements HcpBaseVb
(src/backends/verilog/hw_component/common/hcp_base_vb.rs):
pub trait HcpBaseVb { // ---- atomic queries ---- fn gen_type_vb (&self) -> String; // e.g. "reg [7:0] " or "wire" fn gen_var_name_vb(&self) -> String; // signal name in emitted Verilog
// ---- count queries — default 0; override only when non-zero ---- fn amt_io_line_vb (&self) -> u32 { 0 } // IO port declaration lines fn amt_init_line_vb (&self) -> u32 { 0 } // declaration / initial lines fn amt_precedure_blk_vb(&self) -> u32 { 0 } // always-block count
// ---- write-through generation — default panics; implement when count > 0 ---- fn gen_io_line_vb (&self, idx: u32, arena: &mut ModelArena, fw: &mut FileWriter); fn gen_init_line_vb (&self, idx: u32, arena: &mut ModelArena, fw: &mut FileWriter); fn gen_procedure_blk_vb(&self, idx: u32, arena: &mut ModelArena, fw: &mut FileWriter);
// ---- arena round-trip — each type knows its own slot ---- fn replace_back_into_arena_vb(self: Box<Self>, arena: &mut ModelArena);}Generation methods write directly into the FileWriter rather than returning
Strings, so the emitter never allocates a full-module buffer. The
count/generate split lets the module emitter loop generically
(for idx in 0..vb.amt_init_line_vb() { vb.gen_init_line_vb(idx, ...) })
without knowing what each type contributes.
Representative impls:
- All six register-family types (
Reg,StateReg,SyncReg,CntReg,CondWaitStateReg,CycleWaitStateReg) share one shape via theimpl_reg_vb!macro inreg_vb.rs— 1 declaration line, 1 clocked always block, and a per-typereplace_back_*call injected by the macro. Wire(wire_vb.rs) declares asregand emits one combinational always block (its pool is alwaysClkFree).IoWire(io_wire_vb.rs) is the only type withamt_io_line_vb() == 1; it prefers itsexplicit_nameover the auto name, inputs emit no procedure block, and outputs route theiragent_src_signal.Expressionemits a continuousassign;Valawire ... = literal;.
util_vb.rs helpers
Section titled “util_vb.rs helpers”Shared string helpers live in
src/backends/verilog/hw_component/util_vb.rs:
| Function | Returns |
|---|---|
signal_width(size: i32) | "[N-1:0] " |
slice_to_verilog(&Slice) | "[stop-1:start]", or "" for the {-1,-1} full-width sentinel |
logic_op_to_verilog(op) | Verilog operator token; panics on Assign/ExtendBit/SliceBit/Dummy so unsupported ops fail at the backend boundary |
fmt_init_var(type, name) | "reg [7:0] name;" |
fmt_operand(opr, slice, arena, active_i, active_name) | "var_name[slice]", with a self-reference guard |
sensitivity_list(clk_mode, clk_name) | "posedge <clk>", "negedge <clk>", or "*" — panics on an edge mode with no clock name |
gen_procedure_blk(hcp, active_i, arena, fw) | the whole always @(…) begin … end block from the HCP’s UpdatePool |
The self-reference guard in fmt_operand matters: the HCP being emitted is
already taken out of the arena, so if one of its own events references it
(e.g. cnt <= cnt + 1), re-taking would double-take. The caller passes
active_i/active_name and fmt_operand short-circuits on a global-id match.
gen_procedure_blk builds the destination template and drives the UE layer:
let sens = sensitivity_list(clk_mode, clk_name.as_deref());let tmpl = format!("{active_name}{{DES_SLICE}} <= {{SRC}};");fw.write(&format!("always @({sens}) begin\n"));for &ue_i in pool.get_update_events() { transpile_ue(ue_i, vec![tmpl.clone()], 4, arena, active_i, &active_name, fw);}fw.write("end\n");The op_templates contract: the template carries the destination lvalue
with {DES_SLICE} and {SRC} placeholders. Only the leaf UeBasic performs
the substitution (via slice_to_verilog + fmt_operand); container UEs pass
the templates through unchanged to recursive transpile_ue calls.
Scalable UE dispatch — arena_ext_vb.rs
Section titled “Scalable UE dispatch — arena_ext_vb.rs”src/backends/verilog/arena_ext_vb.rs holds the backend’s only two match
expressions, one per trait:
impl ModelArena { /// ONE match: HwComponentType → Box<dyn HcpBaseVb>. pub fn take_hcp_vb(&mut self, ident: HcpIdent) -> Box<dyn HcpBaseVb> { match ident.get_hw_type() { /* one arm per variant; Nest panics (unported) */ } } /// Zero match — each type's replace_back_into_arena_vb knows its own slot. pub fn replace_back_hcp_vb(&mut self, v: Box<dyn HcpBaseVb>) { v.replace_back_into_arena_vb(self); }
/// ONE match: UeType → Box<dyn VerilogUpdateEvent>. pub fn take_ue_vb(&mut self, ident: UpdateEventIdent) -> Box<dyn VerilogUpdateEvent> { match ident.get_ue_type() { /* Basic / Grp / Cond / Switch; Untype panics */ } }}transpile_ue (common/update_event_vb.rs) is match-free:
pub fn transpile_ue(ue_i, op_templates, front_space, arena, active_i, active_name, fw) { let ue = arena.take_ue_vb(ue_i); ue.transpile(op_templates, front_space, arena, active_i, active_name, fw); ue.replace_back_into_arena_vb(arena); // trait dispatch, no match}VerilogUpdateEvent impls (all four in update_event_vb.rs): UeBasic
substitutes and writes the line; UeGrp writes children in order; UeCond
writes an if / else if / else chain (non-terminal ends omit the newline so
" else" continues the same line); UeSwitch writes a case (state) block.
flowchart TB
T["transpile_ue(ue_i, op_templates, ...)"] --> TK["arena.take_ue_vb<br/>(ONE match: UeType -> Box dyn VerilogUpdateEvent)"]
TK --> D["ue.transpile(...)"]
D --> B["UeBasic<br/>substitute {DES_SLICE}/{SRC}<br/>write leaf line"]
D --> G["UeGrp<br/>write children in order"]
D --> C["UeCond<br/>if / else if / else chain"]
D --> S["UeSwitch<br/>case (state) block"]
G -.->|"recurse, templates unchanged"| T
C -.->|"recurse"| T
S -.->|"recurse"| T
D --> R["replace_back_into_arena_vb<br/>(trait dispatch, no match)"]
Extension recipes. New HCP type: (1) HwComponentType variant, (2) one
arm in take_hcp_vb, (3) impl HcpBaseVb in a new *_vb.rs. New UE type:
(1) UeType variant, (2) one arm in take_ue (arena_impl_ue.rs), (3) one
arm in take_ue_vb, (4) impl VerilogUpdateEvent. In both cases
transpile_ue, gen_procedure_blk, and every existing impl stay untouched.
The five-phase emitted file
Section titled “The five-phase emitted file”Module::gen_module_blk_vb (src/backends/verilog/module/module_vb.rs) glues
the phases; each is bannered in the output:
- Phase 1 —
module name(…);header; ports from each IoWire’sgen_io_line_vb(comma placement handled by aneed_commagate since Verilog forbids a trailing comma). - Phase 2 — signal declarations for every HW type except IoWire (those are already ports). Phase 2.5 declares each sub-module’s output ports as plain wires in the parent scope.
- Phase 3 — always blocks and continuous assigns via
gen_procedure_blk_vb. - Phase 4 — named-port sub-module instantiations
(
gen_inst_sub_module_declaration_vb): input ports connect to their agent drivers, output ports to the wires declared in Phase 2.5. - Phase 5 —
endmodule.
Real output (test/.model_output/tc1_seq_simple/top.v, abridged):
// Phase 1 : module header & IO portsmodule MODULE_tc1_seq_simple0_0( output reg [7:0] my_x, output reg [7:0] my_y, input wire [0:0] clk, input wire [0:0] mrst); // ---- Phase 2 : signal declarations (reg / wire / localparam / mem) ----reg [7:0] REG_x_1;reg [0:0] SR_ST_seq_state_4_0_ST_36;wire [7:0] VAL_simple_val_3 = 8'h30; // ---- Phase 3 : always blocks & continuous assignments ----always @(posedge WIRE_clk_12) begin if (SR_ST_seq_state_4_0_ST_36) begin REG_x_1[7:0] <= VAL_simple_val_3[7:0]; endend // ---- Phase 4 : sub-module instantiations ---- // ---- Phase 5 : endmodule ----endmodule