ModelController and Elaboration
Building a Kathryn model executes no simulation and emits no Verilog — it is
pure elaboration, and all of it is coordinated by one object:
ModelController (src/model/controller/controller.h), which implements the
MainControlable interface (src/abstract/mainControlable.h: start /
reset / clean — the same interface the sim and gen controllers implement).
A file-local centralControllerPtr in src/model/controller/controller.cpp
holds the singleton; getControllerPtr() lazily constructs it on first use, so
the very first mMod in main() is enough to bring the whole machinery up.
Every maker macro, component constructor, assignment operator, and flow-block
macro ultimately calls back into this one object — the controller is how a
plain C++ constructor run turns into a hardware model.
Three kinds of stacks
Section titled “Three kinds of stacks”Because elaboration is C++ execution, “where am I?” is always a question
about what is currently on a stack. ModelController keeps three:
- Module stack —
std::stack<Module_Stack_Element>, where each element pairs aModule*with aMODULE_BUILDING_STATE(MODULE_INIT,MODULE_END_GLOB_DECLARE,MODULE_INIT_DESIGN_FLOW,MODULE_END, plus theMODULE_INIT_AUX/MODULE_FINAL_AUXpair used by the simulator).getTopModulePtr()answers “which module owns the component being declared right now”. - Box stack —
std::stack<Box*>. While a box is under construction,on_box_tryAddToBoxroutes every user-declared component into the top box instead of only the module. - Flow-block stacks —
std::stack<FlowBlockBase*> flowBlockStacks[FLOW_ST_CNT], four parallel stacks indexed byFLOW_STACK_TYPE(src/model/flowBlock/abstract/flowBlock_Base.h):FLOW_ST_BASE_STACKfor every block,FLOW_ST_PATTERN_STACKforseq/par,FLOW_ST_HEAD_COND_STACKforifheads, andFLOW_ST_PIP_BLKfor pipelines. Each block declares which stacks it lives on viagetSelFbStack(), andpushFlowBlock/popFlowBlock(src/model/controller/flowController.cpp) keep them in lockstep.
(getCurModelStack() pretty-prints the module and base flow stacks — the
controller’s own debugging aid.)
The _make<> allocation lock
Section titled “The _make<> allocation lock”The controller field hwCompAllocLock starts true. The _make<> template
behind every maker macro (src/model/hwComponent/abstract/makeComponent.h)
calls unlockAlloc() — which forwards to unlockAllocation() — immediately
before new T(...). Every hardware component derives from
HwCompControllerItf (src/model/controller/conInterf/controllerItf.h),
whose constructor does the other half:
HwCompControllerItf::HwCompControllerItf(bool requiredAllocCheck): ctrl(getControllerPtr()) { if (requiredAllocCheck) { assert(!ctrl->isAllocationLock()); ctrl->lockAllocation(); }}Unlock, construct one component, re-lock. The effect: you cannot new Reg(...)
by hand — only mReg, mMod, mBox, and friends can mint components, which
guarantees each one passes through _make<>’s bookkeeping (name capture via
setRetrieveVarMeta, then com_final()) before anything else touches it.
Registration callbacks: on_*_init and on_*_update
Section titled “Registration callbacks: on_*_init and on_*_update”Components introduce themselves through com_init() — Reg::com_init() is
literally ctrl->on_reg_init(this) — and the controller’s handlers live in
src/model/controller/hwCompController.cpp in two families.
The init family runs during construction: on_reg_init, on_wire_init,
on_expression_init, on_value_init, on_pmValue_init, on_memBlk_init,
on_nest_init, on_itf_init, on_box_init/on_box_end_init (which push and
pop the box stack), and on_sp_reg_init for the framework’s own state,
sync, and wait registers. They all follow one shape: fetch the top module,
add the component to the right per-module list (addUserReg, addUserWires,
…), setParent, and buildInheritName so the component’s hierarchical name
is fixed at declaration.
The update family runs when an assignment executes inside flow():
on_reg_update, on_wire_update, on_memBlkEleHolder_update,
on_nest_update, on_box_update. Each receives the freshly built AsmNode
and asks isTopFbBelongToTopModule(): if an enclosing flow block of the
current module is on the stack, the node is attached to it with
addElementInFlowBlock; otherwise it is wrapped in a FlowBlockPseudo
(src/model/flowBlock/pseudo/pseudo.cpp) so bare assignments outside any
block still enter the model. How these nodes become hardware is the
flow-block internals story.
Flow-block attach and detach
Section titled “Flow-block attach and detach”Flow-block macros expand to for statements — seq in
src/model/flowBlock/seq/seq.h constructs a FlowBlockSeq whose attach hook
calls ctrl->on_attach_flowBlock(this) and whose detach hook calls
ctrl->on_detach_flowBlock(this) when the braces close. Attach assigns the
block’s module and flow-block parents (assignFlowBlockParent) and pushes it
onto its selected stacks. Detach pops it and joins it upward according to its
FLOW_BLOCK_JOIN_POLICY: FLOW_JO_SUB_FLOW nests it as a sub-block,
FLOW_JO_CON_FLOW chains it as a consecutive block (elif/else), and
FLOW_JO_EXT_FLOW extracts its AsmNodes into the enclosing block. A block
with no enclosing block lands directly in the module via addFlowBlock.
Blocks flagged lazy-delete stay on the stack until tryPurifyFlowStack()
evicts them — which is why every update handler calls it first.
A module’s elaboration lifecycle
Section titled “A module’s elaboration lifecycle”Walking the blink sample through the
controller (src/model/controller/hwCompController.cpp):
- Constructing
ModelControlleritself runson_globalModule_init_component(): it builds a hiddenModulenamedglobeModand pushes it with stateMODULE_INIT, so user code always has a top module to report to. mMod(ex, BlinkAB, 0)→Module::com_init→on_module_init_components: the new module is added to its parent’s_userSubModulesand pushed withMODULE_INIT. Every field macro inside it now registers against this stack top. When_make<>finishes,com_final→on_module_end_init_componentsmarks the stageMODEL_GLOB_INITED, sets the state toMODULE_END_GLOB_DECLARE, and pops.startModelKathryn()(src/kathryn.cpp) callsModelController::start(), which finalizes the global module’s declarations and runson_globalModule_init_designFlow()— pushingglobeModback withMODULE_INIT_DESIGN_FLOWand callingbuildAll().Module::buildAll()(src/model/hwComponent/module/module.cpp) runsflow(), purifies the flow stacks, then for each child callson_module_init_designFlow(sub)(push, recurse into the child’sbuildAll()) followed byon_module_final(sub)(assert the flow stacks are clear of this module, setMODULE_END, pop) — and finally lowers its own recorded blocks withbuildFlow().
stateDiagram-v2
[*] --> MODULE_INIT: on_module_init_components pushes module
MODULE_INIT --> MODULE_END_GLOB_DECLARE: on_module_end_init_components pops
MODULE_END_GLOB_DECLARE --> MODULE_INIT_DESIGN_FLOW: on_module_init_designFlow pushes again and runs buildAll
MODULE_INIT_DESIGN_FLOW --> MODULE_END: on_module_final pops
MODULE_END --> [*]
note right of MODULE_INIT: constructor phase<br/>field macros register components
note right of MODULE_INIT_DESIGN_FLOW: flow phase<br/>the flow body records blocks and CCOs
The simulator’s SimInterface (src/sim/interface/simInterface.cpp) later
reopens globeMod with the on_globalModule_init_auxilaryComponent /
on_globalModule_final_auxilaryComponent pair to add read-only trigger logic
— the same stack discipline, in the MODULE_INIT_AUX state.
clockMode and asmMode: ambient elaboration contexts
Section titled “clockMode and asmMode: ambient elaboration contexts”Two small files beside the controller scope how assignments are recorded, without touching the stacks:
src/model/controller/clockMode.hholds a globalCLOCK_MODE(CM_POSEDGEdefault,CM_NEGEDGE,CM_CLK_FREE,CM_CLK_UNUSED) set bySET_CLK_MODE2NEG_EDGE()/SET_CLK_MODE2DEF().RegandMemBlockAgentread it back throughgetCurAssignClkMode(), so every assignment elaborated while a mode is active captures that clock edge.src/model/controller/asmMode.hholds the assignment-priority context:SET_ASM_PRI_TO_MANUAL(p)/SET_ASM_PRI_TO_AUTO()(defaultDEFAULT_UE_PRI_USER, 10).createUEHelper(src/model/hwComponent/abstract/updateEvent.cpp) stampsGET_ASM_PRI_VAL()onto each auto-priority update event — this is the mechanism beneath Decentralized Update priority scoping.
Both are set/restore contexts: bracket a region of flow() with the setter
and the default restorer, and only the CCOs elaborated in between are
affected.
Where next
Section titled “Where next”- Architecture — where the model controller sits relative to the sim and gen controllers.
- Hardware components — what the registered components themselves look like.
- Flow-block internals — what happens to a
block after
on_detach_flowBlockhands it to its parent.