Skip to content

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), CpuStateGuestState 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 as interfaces ← ir ← {arch/sparc, jit} ← runtime; tero_ir does not depend on tero_core (it works on the opaque GuestState blob), 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.
corebus, coreperipherals 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.
buscore 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.
peripheralsbus, peripheralscore 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.
ircore 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.
defaultsinterfaces 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:

  1. CMake link edges. A module can only name symbols from libraries it links. tero_core links neither tero_bus nor tero_peripherals, so a #include "tero/bus/..." in core would not even resolve — the include path is not on core's target_include_directories.
  2. -Werror with a strict warning set (tero::warnings): -Wall -Wextra -Wpedantic -Werror plus -Wshadow -Wold-style-cast -Wconversion -Wsign-conversion, with the whole tree at zero warnings.
  3. Code review against CLAUDE.md rule 5. The layering invariant is part of the frozen architectural contract; new edges that violate the flow are rejected.

See also