Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
417 changes: 417 additions & 0 deletions .claude-docs/bscRuntime-modules.md

Large diffs are not rendered by default.

117 changes: 117 additions & 0 deletions .claude-docs/bscTests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# BSV Runtime Module Tests

40 hardware simulation tests for the modules in `bscRuntime/`. Located in `bscTests/`.

## Running

```bash
cd bscTests
export BLUESPECDIR=/opt/homebrew/opt/bsc/libexec
make test # Run all 40 tests
make clean # Remove build artifacts
make run_mkTestQL_BasicLifecycle # Run a single test
```

Requires: `bsc`, `iverilog`, `vvp`, `timeout` or `gtimeout`. Build artifacts are cleaned up automatically after `make test`.

## Test Design

Tests model realistic pipeline behavior based on analysis of generated BSV from the RISC-V pipeline tests. Each test uses a step-counter FSM where each step corresponds to a pipeline stage's operation on the module.

### Harness (TestHelper.bsv)

Two standalone functions (no module state, no scheduling conflicts):
- `testAssert(Bool cond, String msg, UInt#(32) cycle)` -- prints `ok:` or `FAIL:`
- `testDone(String name, UInt#(32) fails)` -- prints `PASS` or `FAIL`, calls `$finish`

Each test module tracks its own `fails` counter and `cyc` register.

### BSV Scheduling Rules

These constraints shaped the test structure:
- **One write per register per rule** -- multiple `if (cond) fails <= fails + 1` in the same rule causes a parallel write conflict. Each rule has at most one conditional fail increment.
- **Method isolation** -- methods that read and write the same internal wires (e.g., `canAtom_r1` reads bypass wires, `write` sets them) cannot be called in the same rule. These are split into separate steps.
- **One `spec.free()` per rule** -- freeing multiple entries conflicts on `inUse[]`.
- **No non-ASCII in string literals** -- BSC 2025.07 crashes with "Internal Bluespec Compiler Error: quoting a character value" on em dashes or other non-ASCII. Use `--` not `--`.

## Test Files and Cases

### TestQueueLock.bsv (5 tests for mkQueueLock)

| Test | Scenario |
|------|----------|
| `mkTestQL_BasicLifecycle` | Reserve 1 ID, verify owns, release, verify empty |
| `mkTestQL_PipelineStall` | 3-deep pipeline: reserve 3 IDs, only head owns, release in order |
| `mkTestQL_FullQueue` | Fill depth-4 queue, verify `canRes1` backpressure, drain one by one |
| `mkTestQL_RapidReserveRelease` | Alternate reserve/release each cycle for 6 iterations (steady-state throughput) |
| `mkTestQL_WrongRelease` | Release non-owner is a no-op -- queue state preserved |

### TestCountingLock.bsv (5 tests for mkCountingLock)

| Test | Scenario |
|------|----------|
| `mkTestCL_BasicLifecycle` | Same basic reserve/owns/release lifecycle |
| `mkTestCL_SameCycleResRel` | EHR enables reserve and release in the same cycle via separate rules |
| `mkTestCL_ManyReservations` | 6 outstanding reservations on depth-8 lock, drain all |
| `mkTestCL_OwnerAdvancement` | Release head, verify next becomes owner, three-stage progression |
| `mkTestCL_Wraparound` | 10 reserve/release iterations wrapping the 3-bit counter |

### TestCheckpointLock.bsv (5 tests for mkCheckpointQueueLock)

| Test | Scenario |
|------|----------|
| `mkTestCKL_BasicCheckpointRollback` | Checkpoint after 2 reserves, speculative 3rd, rollback undoes it |
| `mkTestCKL_CheckpointNoRollback` | Checkpoint doesn't interfere with normal release flow |
| `mkTestCKL_MultipleCheckpoints` | Nested checkpoints (c1, c2), rollback to c1 undoes everything after c1 |
| `mkTestCKL_RollbackToEmpty` | Rollback speculative work, release original to reach empty |
| `mkTestCKL_RollbackAndContinue` | Rollback, then resume with new correct-path reservations |

### TestAddrLock.bsv (5 tests for mkFAAddrLock, mkDMAddrLock)

| Test | Scenario |
|------|----------|
| `mkTestAL_IndependentAddrs` | 3 addresses are independent, unrelated address reports empty |
| `mkTestAL_SameAddrConflict` | Two reservations on same address (WAW hazard), ownership advances on release |
| `mkTestAL_PoolExhaustion` | 4-slot FA lock full, `canRes1` false for new addr, release frees slot |
| `mkTestAL_AutoFree` | `freelock` rule auto-clears entry after release, freeing slot for reuse |
| `mkTestAL_DMBasic` | Direct-mapped lock: per-address independence, always has capacity |

### TestSpeculation.bsv (5 tests for mkSpecTable)

| Test | Scenario |
|------|----------|
| `mkTestSpec_AllocAndValidate` | Alloc 3 entries, validate first, check statuses, free all (correct prediction path) |
| `mkTestSpec_InvalidateCascade` | Invalidate s1 cascades to kill s2 (newer), s0 (older) unaffected |
| `mkTestSpec_FullTable` | Fill 4-entry table, verify alloc blocks, free one to resume |
| `mkTestSpec_ValidateThenInvalidate` | Invalidate overrides prior validate on same entry |
| `mkTestSpec_RapidAllocFree` | Alloc-validate-free loop for 6 rounds without running out of space |

### TestBypassLock.bsv (5 tests for mkBypassLockCombMem)

| Test | Scenario |
|------|----------|
| `mkTestBP_ReserveWriteReadRelease` | Full lifecycle with bypass forwarding, then RF commit |
| `mkTestBP_ReadBeforeWrite` | `canAtom_r1` false before write, true after (stall behavior) |
| `mkTestBP_TwoWritesSameAddr` | WAW: newest write (200) wins over older (100) in bypass |
| `mkTestBP_WriteReadDifferentAddrs` | Independent addresses return correct bypass data |
| `mkTestBP_CommitOrder` | Three writes released in order, each commit persists in RF |

### TestNewMemories.bsv (5 tests for mkQueueLockCombMem, mkFAAddrLockCombMem)

| Test | Scenario |
|------|----------|
| `mkTestMem_QLBasicReadWrite` | Write/read, lock blocks `canAtom`, release restores it |
| `mkTestMem_ALReadAfterWrite` | Lock on addr1 blocks only addr1, addr2 remains readable (RAW stall) |
| `mkTestMem_ALMultipleReaders` | Three locks, unrelated addr still readable, all restored after release |
| `mkTestMem_QLAtomicOps` | `atom_r`/`atom_w` work when unlocked, both blocked when locked |
| `mkTestMem_ALWriteAndRelease` | Reserve, write, release lifecycle modeling writeback stage |

### TestBHT.bsv (5 tests for mkBHT)

| Test | Scenario |
|------|----------|
| `mkTestBHT_StateMachine` | Full walk through all 4 counter states and back |
| `mkTestBHT_SaturationStrong` | 5 consecutive taken/not-taken verify saturation (no overflow) |
| `mkTestBHT_DifferentPCs` | 3 PCs trained independently, predictions don't interfere |
| `mkTestBHT_SameCycleReqUpd` | `req` and `upd` in same cycle -- `req` reads pre-update value (CF schedule) |
| `mkTestBHT_AliasingBehavior` | Two PCs aliasing same entry share counter, non-aliased is independent |
202 changes: 202 additions & 0 deletions .claude-docs/codegen-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# PDL Code Generation Notes

How the PDL compiler generates Bluespec System Verilog from the stage graph.

## Overall Flow

```
PDL Source
-> Parse (Parser.scala -> Prog AST)
-> Type Check + Passes (Main.runPasses -> annotated Prog)
-> Stage Splitting (SplitStagesPass -> Map[Id, List[PStage]])
-> Stage Optimization (ConvertAsync, AddEdgeValue, LockElimination, Collapse)
-> BSV Generation (BluespecGeneration -> BProgram AST)
-> BSV Pretty Print (BSVPrettyPrinter -> .bsv files)
-> BSC Compiler (bsc -> Verilog)
```

## Key Classes

- **`BluespecProgramGenerator`** (BluespecGeneration.scala) -- top-level: takes a `Prog`, stage info, and config; produces `List[BProgram]`
- **`BluespecModuleGenerator`** (inner class) -- per-pipeline module: generates rules, declarations, interfaces
- **`Translations`** (Translations.scala) -- translates PDL expressions/types to BSV expressions/types
- **`BluespecInterfaces`** (BluespecInterfaces.scala) -- generates BSV module instantiation expressions, method calls
- **`BSVPrettyPrinter`** (BSVPrettyPrinter.scala) -- serializes the BSV AST to text

## BSV AST (BSVSyntax.scala)

The compiler builds a BSV AST before printing. Key nodes:

| Node | Represents |
|------|-----------|
| `BProgram(name, body)` | A BSV package |
| `BModuleDef(name, typ, params, body)` | A BSV module |
| `BRuleDef(name, conds, body)` | A BSV rule (conds = guard expressions) |
| `BMethodDef(sig, cond, body)` | A BSV method |
| `BModInst(name, module)` | Module/register instantiation |
| `BExprStmt(expr)` | Expression statement |
| `BAssign(lhs, rhs)` | Combinational assignment (`=`) |
| `BInvokeAssign(lhs, invoke)` | `let x <- invoke` |
| `BMethodInvoke(mod, method, args)` | Method call (`mod.method(args)`) |
| `BIf(cond, thenStmts, elseStmts)` | Conditional |
| `BStmtSeq(stmts)` | Statement sequence |
| `BBOp(op, l, r)` | Binary operation |
| `BUOp(op, e)` | Unary operation |

**Important**: `BAssign` produces `=` (combinational wire). For register writes, use `BExprStmt(BMethodInvoke(reg, "_write", List(value)))` which produces `reg <= value` in BSV.

## Per-Module Generation (BluespecModuleGenerator)

### Module Structure

Each PDL pipeline module becomes a BSV module containing:

1. **Instantiations**: FIFOs for pipeline edges, lock regions, registers
2. **Rules**: One execute rule + optional kill rule per pipeline stage
3. **Methods**: `req` (start pipeline), `peek`/`checkHandle`/`resp` (output), `busy` (backpressure)

### Key Data Structures

| Variable | Type | Purpose |
|----------|------|---------|
| `specTable` | `BVar` | Speculation table module instance |
| `busyReg` | `BVar` | Busy register (backpressure) |
| `globalExnFlag` | `BVar` | Global exception flag register |
| `threadIdVar` | `BVar` | Thread ID counter register |
| `outputQueue` | `BVar` | Output queue for pipeline results |
| `edgeParams` | `Map[PipelineEdge, BVar]` | FIFO variables for each pipeline edge |
| `modParams` | `Map[Id, BVar]` | Module parameter variables (memories, locks) |
| `lockRegions` | `Map[Id, BVar]` | Lock region registers |

### Stage Rule Generation (`getStageRule`)

Each `PStage` becomes a BSV rule:

```
rule <stage>_execute (<guards>);
<declarations> // request handle declarations
<effects> // memory ops, lock ops, sends/receives
<queue ops> // FIFO enqueues/dequeues
<debug> // optional $display
endrule
```

**Guards** come from two sources:
- `getBlockingConds(cmds)` -- lock ownership checks, output queue space, spec checks, exception flag checks
- `getRecvConds(cmds)` -- FIFO not-empty checks, memory response ready checks

The guards are AND'd together. The rule only fires when ALL guards are true.

### Kill Rule Generation (`getStageKillRule`)

Optional per-stage rule that fires when a speculated instruction is killed:

```
rule <stage>_kill (<kill_conds> && <recv_conds>);
<dequeue input FIFOs> // consume the dead instruction's data
<free spec table entry> // release speculation resources
endrule
```

Kill conditions check `isValid(specId) && !fromMaybe(True, specTable.check(specId))` -- the instruction was speculative AND is confirmed mispredicted.

### Guard Extraction

**`getBlockingConds(cmds)`** extracts guards from:
- `CLockStart(mod)` -> lock region start check
- `IReserveLock` -> lock reservation availability
- `ICheckLockOwned` -> lock ownership verification
- `IMemSend/IMemWrite` with `isAtomic` -> atomic access availability
- `COutput` -> output queue can write
- `CCheckSpec(blocking=true)` -> spec status must be True (non-speculative)
- `CCheckSpec(blocking=false)` -> spec status must not be False (not definitely killed)
- `ICheckExn` -> `!globalExnFlag` (not in exception handling mode)
- `ICondCommand` -> recursively extracts from conditional blocks

**`getKillConds(cmds)`** extracts kill triggers from:
- `CCheckSpec` -> spec status is definitely False (mispredicted)
- `ICondCommand` -> recursive extraction

### Effect Command Translation (`getEffectCmd`)

Translates PDL commands to BSV statements:

| PDL Command | BSV Output |
|-------------|-----------|
| `IMemSend(handle, ...)` | `let handle <- mem.req(addr, data, wmask)` |
| `IMemRecv(mem, handle, _)` | `mem.resp(handle)` |
| `IMemWrite(mem, addr, data, ...)` | `mem.write(addr, data)` or lock write |
| `ISend(handle, receiver, args)` | `fifo.enq(args)` or `let handle <- mod.req(args)` |
| `IRecv(_, sender, _)` | `sender.resp()` |
| `COutput(exp)` | `outputQueue.enq(value); threadId++` |
| `CSpecCall(handle, ...)` | `let specId <- specTable.alloc(); fifo.enq(args, specId)` |
| `CVerify(handle, args, preds)` | spec validate/invalidate + rollback |
| `IAbort(mem)` | `mem.lock.abort()` or `mem.clear()` |
| `ISetGlobalExnFlag(state)` | `globalExnFlag <= state` |
| `IFifoClear()` | `.clear()` on all edge FIFOs |
| `ISpecClear()` | `specTable.clear()` |
| `ICheckExn()` | *(guard condition, not a statement)* |

### FIFO / Edge Management

Pipeline edges are implemented as FIFOs. Each edge carries a struct with:
- All live variables needed by downstream stages
- Thread ID (`_threadID`)
- Speculation ID (`_specId`, if speculative)

**Edge struct names**: `E_<from>_TO_<to>` (generated by `getEdgeStructInfo`)

**FIFO variable names**: `fifo_<from>_TO_<to>` (generated by `genParamName`)

**Edge queue operations** (`getEdgeQueueStmts`):
- Input edges: `fifo.deq()` at start of rule
- Output edges: `fifo.enq(struct)` at end of rule
- Out-of-order coordination edges: tag-based routing

### Module Instantiation (`getTopModule`)

Assembles all pieces into a BSV module:

```bsv
module mkPipeline(PipelineInterface);
// Instantiations
FIFOF#(E_input_TO_Start) fifo_input_TO_Start <- mkNBFIFOF();
FIFOF#(E_Start_TO_Stage0) fifo_Start_TO_Stage0 <- mkFIFOF();
// ... more FIFOs, lock regions, module locks
Reg#(Bool) busyReg <- mkReg(False);
SpecTable#(...) specTable <- mkSpecTable(); // if speculative
Reg#(Bool) globalExnFlag <- mkReg(False); // if exception pipeline
OutputQ#(...) outputQueue <- mkOutputFIFOF(0);
Reg#(UInt#(N)) threadId <- mkReg(0);

// Rules (one pair per stage)
rule s_Start_execute (...); ... endrule
rule s_Start_kill (...); ... endrule // optional
rule s_Stage0_execute (...); ... endrule
// ... etc

// Interface methods
method req(args) if (!busyReg); ... endmethod
method peek(); ... endmethod
method checkHandle(h); ... endmethod
method resp(); ... endmethod
endmodule
```

## Things to Watch Out For

1. **`BAssign` vs register write**: `BAssign(v, e)` produces `v = e` (combinational). For registers, use `BExprStmt(BMethodInvoke(reg, "_write", List(value)))` which produces `reg <= value`.

2. **Guard vs effect**: Some commands are guards (prevent rule firing) not effects (state changes). `ICheckExn` and `CCheckSpec` are guards extracted by `getBlockingConds`, not effects. If added to `getEffectCmd` they should return `None`.

3. **FIFO naming**: Edge FIFOs use generated names from `genEdgeName`. The `edgeParams` map stores the BVar for each edge. When generating `.clear()` calls, iterate `edgeParams.values`.

4. **Speculation table ports**: The `Integer i` parameter in `check(s, i)` and `validate/invalidate(s, i)` selects the EHR port. Lower = earlier in cycle. Stages with `spec_call` use port 0, stages with `verify` use higher ports. This is tracked by `specAnnotations` and `stgSpecOrder`.

5. **Module parameters**: Memories and locks passed to the pipeline are stored in `modParams: Map[Id, BVar]`. Lock methods are accessed as `modParams(mem).lock.method()` or directly via `LockImplementation.getXxxInfo()`.

6. **Non-blocking input FIFO**: The first stage's input FIFO uses `mkNBFIFOF` (non-blocking, last-writer-wins) because the recursive call, verify redirect, and external request can all enqueue in the same cycle.

7. **3-port EHR on AsyncMem**: We upgraded `AsyncMem`'s valid bits from 2-port to 3-port EHR to add `clear()` without breaking `fire_when_enabled` on existing rules. Port 0 = moveToOutFifo, port 1 = freeResp/checkResp/peekResp, port 2 = clear.

8. **Exception flag as guard**: `ICheckExn` becomes `!globalExnFlag._read()` in `getBlockingConds`. This prevents body stages from executing while the except block runs. The except block's own stages don't have `ICheckExn` so they execute normally.
Loading
Loading