Modules¶
Tero is built as a stack of CMake static libraries (plus one
header-only INTERFACE target and one executable) with strict,
unidirectional dependencies. Each module page below is the
authoritative reference for one CMake target: what it owns, its public
surface (with path:line / Type::method citations), the boundary it
draws and why, the data structures inside it, and how to extend it.
Authority
The authoritative dependency edges are the target_link_libraries
calls in each src/*/CMakeLists.txt. The graph and tables on this
page are taken directly from those files. If the build graph and a
doc page disagree, the CMakeLists.txt wins — fix the page.
The module map¶
| Module | CMake target | Page | Role |
|---|---|---|---|
| Interfaces | tero_interfaces (INTERFACE) |
interfaces | Strong types, Result<T>, every I* contract, PeripheralContext, AddressRange, GatedMutex |
| Core | tero_core |
core | SPARC V8 ISA — CpuState, decoder, ISA handlers, step, trap dispatch, FP via SoftFloat 3e |
| Bus | tero_bus |
bus | Ram, SystemBus (big-endian routing, MMIO dispatch, IBusMaster) |
| Peripherals | tero_peripherals |
peripherals | IrqMP, IrqAMP, GPTimer, ApbUart, MemCtrl, Prom, GrGpio, SignalPort |
| IR | tero_ir |
IR and LLVM JIT | Arch-neutral IR, GuestState, BlockCache, IR interpreter, frontend seam |
| SPARC frontend | tero_arch_sparc |
Adding a frontend | SPARC → IR (translate_block), CpuState↔GuestState bridge |
| JIT | tero_jit |
IR and LLVM JIT | IR → native via LLVM ORCv2, tiered baseline/optimised |
| Runtime | tero_runtime |
runtime | Emulator, EmulatorConfig + recipes, scheduler, ELF loader, CPU-bus bridge, PnP table, GDB stub |
| Defaults | tero_defaults |
defaults | StdoutLogger, StdoutCharDevice, NullFaultInjector, DebugPublisher |
| App | tero_app (executable) |
app | The tero-emu CLI — the only place host I/O is allowed |
The three translation-stack targets (tero_ir, tero_arch_sparc,
tero_jit) get their own deep dives in
IR and LLVM JIT and
Adding a frontend; they are
included in the graph below for completeness.
Dependency graph¶
flowchart TD
tero_app[tero_app<br/>CLI executable] --> tero_runtime
tero_runtime[tero_runtime<br/>Emulator, ElfLoader, Scheduler, GdbStub, PnP] --> tero_defaults
tero_runtime --> tero_peripherals
tero_runtime --> tero_arch_sparc
tero_runtime --> tero_jit
tero_runtime --> tero_core
tero_runtime --> tero_bus
tero_defaults[tero_defaults<br/>Loggers, CharDevices, NullFaultInjector] -.-> tero_interfaces
tero_defaults --> fmt[(fmt)]
tero_peripherals[tero_peripherals<br/>IrqMP, IrqAMP, GPTimer, ApbUart, MemCtrl, Prom, GrGpio] --> tero_bus
tero_peripherals -.-> tero_interfaces
tero_bus[tero_bus<br/>SystemBus, Ram] -.-> tero_interfaces
tero_arch_sparc[tero_arch_sparc<br/>SPARC → IR frontend] --> tero_ir
tero_arch_sparc --> tero_core
tero_jit[tero_jit<br/>IR → native, LLVM ORCv2] --> tero_ir
tero_jit --> LLVM[(LLVM ≥ 18)]
tero_ir[tero_ir<br/>arch-neutral IR, interpreter] -.-> tero_interfaces
tero_core[tero_core<br/>SPARC V8 ISA, CpuState, Decoder] --> tero_ir
tero_core --> softfloat3e[(SoftFloat 3e)]
tero_core -.-> tero_interfaces
tero_interfaces[tero_interfaces<br/>Header-only API] --> tl_expected[(tl::expected)]
Solid arrows are link-time dependencies; dotted arrows are
header-only contracts (tero_interfaces is an INTERFACE target — it
produces no .a, only #include paths and the tl::expected
dependency). Round nodes are external dependencies fetched or located by
CMake.
The graph is acyclic by construction: tero_core does not know
peripherals exist, tero_bus does not know there is a CPU, and
tero_interfaces knows nothing about anybody.
The dependency rules (and their rationale)¶
The flow, stated in CLAUDE.md rule 5:
Dependencies flow:
interfaces ← core, bus ← peripherals ← runtime ← app. The translation stack layers asinterfaces ← ir ← {arch/sparc, jit} ← runtime;tero_irdoes not depend ontero_core(it works on the opaqueGuestStateblob), and the SPARC frontend bridges the two.
| Boundary | Edge | Why it is drawn there |
|---|---|---|
interfaces depends on nothing but tl::expected |
— | One place for vocabulary types and Result<T>; everything links it, nothing else may define a strong type. Carries no global state → two Emulators coexist in one process. |
core ⇏ bus, core ⇏ peripherals |
the CPU never names SystemBus or any device |
The CPU reaches the world only through the injected ICpuBus. Unit-testable against a fake bus with hand-crafted instruction streams. |
bus ⇏ core |
the bus is a pure physical-address router | The same bus instance is the DMA target for peripherals (it implements IBusMaster) with no circular dependency. |
peripherals ⇒ bus, peripherals ⇏ core |
devices DMA through the bus, never see the CPU | A peripheral DMAs through exactly the memory map the CPU sees, and cannot couple to architectural state. |
ir ⇏ core |
the IR works on an opaque GuestState blob |
The IR interpreter and JIT are shared across guest ISAs; a new architecture is a new frontend, not a new core. |
runtime ⇒ everything below |
the only orchestrator | Assembling the SoC is the runtime's job. No other module instantiates Emulator, EventScheduler, or ElfLoader; the graph terminates here before the app. |
defaults ⇒ interfaces only |
swappable host services | Logger, char device, fault injector, publisher — exactly what an external (e.g. SMP2) wrapper replaces — sit behind interfaces, droppable wholesale. |
tero_core PUBLIC-links tero_ir (state unification)
A historically surprising edge: src/core/CMakeLists.txt makes
tero::ir a PUBLIC dependency of tero_core. This is the
state-unification design — CpuState's integer register file is an
ir::GuestState byte blob
(src/core/include/tero/core/cpu_state.hpp:690), so the Switch
interpreter and the IR engine read and write the same bytes with
no per-fallback sync. tero_core still does not depend on
tero_arch_sparc or tero_jit: it knows the opaque blob layout,
not the IR ops or LLVM. The SPARC↔IR bridge lives in
tero_arch_sparc, the only module that links both core and ir.
PUBLIC vs PRIVATE shapes the public header surface
emulator.hpp includes tero/jit/tiered_jit.hpp
(src/runtime/include/tero/runtime/emulator.hpp:36), so tero_jit
is a PUBLIC link of tero_runtime — any consumer (the app,
tests) transitively gets the JIT headers. tero_arch_sparc and
tero_defaults are PRIVATE: their symbols are used only inside
emulator.cpp, so they do not leak into the runtime's include
surface.
How the boundaries are enforced¶
There is no automated layering linter; three mechanisms keep it honest:
- CMake link edges. A module can only name symbols from libraries
it links.
tero_corelinks neithertero_busnortero_peripherals, so a#include "tero/bus/..."in core would not even resolve — the include path is not on core'starget_include_directories. -Werrorwith a strict warning set (tero::warnings):-Wall -Wextra -Wpedantic -Werrorplus-Wshadow -Wold-style-cast -Wconversion -Wsign-conversion, with the whole tree at zero warnings.- Code review against
CLAUDE.mdrule 5. The layering invariant is part of the frozen architectural contract; new edges that violate the flow are rejected.
See also¶
- Architecture: layers and modules — the same graph from the architecture angle, with per-boundary narrative.
- Architecture: design principles — the invariants the layering serves.
- Architecture: execution model — what the runtime drives.
- IR and LLVM JIT — the translation stack.