Computer Architecture
RISC-V Pipeline Simulator
A browser simulator that runs RV32I assembly through an explicit 5-stage pipeline and shows, cycle by cycle, where forwarding hides a dependence and where a stall or a flush is unavoidable, with the timing pinned by hand-computed stage traces inside a 63-case suite.

Employer signal
What This Project Shows
Most simulators are judged by whether the registers end up right. This one is judged by when things happen: six hand-computed traces assert full stage occupancy cycle by cycle, four of them on the cases that actually bite (the load-use interlock, the chained double stall, lw feeding a dependent sw, and the taken-branch squash are each pinned across every cycle they occupy) so a machine that produced the correct answer on the wrong schedule would fail. That is the standard I want to be held to in architecture work.
Problem
What Needed To Be Solved
A five-stage pipeline is usually taught as a picture: five boxes, arrows for forwarding, and a note that loads cost a bubble. Understanding tends to stop at the picture, because the part that actually bites is timing, which cycle a bubble enters EX, which two instructions die when a branch resolves, whether a value reaches its consumer from the ALU or from memory, and which of those cases the hardware cannot fix by bypassing. A dump of register values at the end of a program tells you none of that, and neither does the diagram.
Approach
How I Built The Solution
I modelled the datapath rather than interpreting instructions and adding penalties afterwards: pipeline registers between every stage, a hazard unit, a forwarding unit, and a step() that runs WB through IF in that order, so a write-first register file falls out of the ordering instead of being special-cased. Then I made timing the thing under test. The engine renders each cycle as an "IF ID EX MEM WB" string with tokens for a squashed instruction, a stall bubble and a flush bubble, so a test can assert eight consecutive cycles of stage occupancy in a form a reviewer can read in a diff. Whole programs are checked against a closed-form cycle count computed by hand, not against a recorded snapshot. The engine itself is UI-free (its barrel exports the simulator, the assembler, the trace helpers and the ISA predicates, and no React import appears anywhere beneath it) so the visualiser consumes the model and has no way to fudge the timing it draws.
Outcome
What It Demonstrates
The repo is public and holds the engine (1,261 lines across six files in src/engine), the stepping UI with preset programs, and 63 Vitest cases spread across the assembler, per-instruction semantics, hazards, control flow and whole programs. The whole repo is one commit dated 2026-07-12, authored by me with a Co-Authored-By trailer: a one-day AI-assisted build, so what it evidences is the machine and the suite that pins it rather than a long build history. There is no live URL yet: GitHub Pages is not serving it and there is no CI workflow, though Vite's base is already set to './' for a static subpath. The scope is narrow on purpose: word-only memory, no M extension, no CSRs, no branch prediction, and an assembler that stops at decoded instruction records rather than emitting 32-bit machine words. What it demonstrates is the discipline: choosing a microarchitectural convention, writing down why, and then testing the awkward consequence of it at cycle granularity instead of checking that the program printed the right number.
Evidence From Source
The case most hand-written 5-stage simulators miss
A load feeding a dependent store. Because store data is latched in EX, sw's rs2 read is a genuine use and lw to sw costs a bubble exactly like lw to add. That only comes out right if the hazard unit reasons about what each instruction format reads rather than about ALU operands, and if rs2 forwarding is wired to the store-data port as well as the ALU port. The test does not check the stored word and stop: it pins all seven cycles of stage occupancy, including which cycle the bubble sits in.
Timing checked against arithmetic, not a snapshot
Three whole programs are asserted against a closed-form model (retired instructions, plus 4 to fill the pipe, plus 1 per load-use stall, plus 2 per taken transfer) so the simulator and a hand analysis have to agree. fib(10) = 55 in 78 cycles from 56 retired and 9 redirects; a four-word memcpy in 42 cycles at CPI 1.5; sum 1..10 in 55. A snapshot test records whatever the machine did; this one fails if the machine is systematically wrong. The same file tests reset() as a replay property rather than a state check (restore the initial memory image, drop every write, rerun the program, and assert an identical cycle trace and register file) which is what makes the UI's reset button trustworthy.
The interaction that would wedge the pipeline
ebreak stops fetch the moment it is fetched, not when it retires, so an ebreak fetched down a wrong path and then squashed by a taken branch would leave fetch halted forever. The redirect path clears that flag, and the test proves both halves: the program still reaches its real ebreak and halts there, and exactly one instruction is flushed rather than two, because fetch had already stopped and the IF slot was empty when the branch resolved.