OR-1 dataflow CPU sketch

feat: add Instruction, OutputStyle, FrameDest, TokenKind, FrameOp; renumber all opcodes to 5-bit hardware encoding; remove ALUInst, SMInst, Addr, FREE_CTX

Orual 8960e6b7 aebec478

+4504 -99
+70 -70
cm_inst.py
··· 1 1 from dataclasses import dataclass 2 - from enum import IntEnum 2 + from enum import Enum, IntEnum 3 3 from typing import Optional 4 4 5 5 ··· 31 31 32 32 33 33 class ArithOp(ALUOp): 34 - ADD = 0b0000 35 - SUB = 0b0001 36 - INC = 0b0010 37 - DEC = 0b0011 38 - SHIFT_L = 0b0100 39 - SHIFT_R = 0b0101 40 - ASHFT_R = 0b0110 34 + ADD = 0b00000 # 0 35 + SUB = 0b00001 # 1 36 + INC = 0b00010 # 2 37 + DEC = 0b00011 # 3 38 + # gap: 4-7 are LogicOp 39 + SHL = 0b01000 # 8 (was SHIFT_L = 4) 40 + SHR = 0b01001 # 9 (was SHIFT_R = 5) 41 + ASR = 0b01010 # 10 (was ASHFT_R = 6) 41 42 42 43 43 44 class LogicOp(ALUOp): 44 - AND = 0b0111 45 - OR = 0b1000 46 - XOR = 0b1001 47 - NOT = 0b1010 48 - EQ = 0b1011 49 - LT = 0b1100 50 - LTE = 0b1101 51 - GT = 0b1110 52 - GTE = 0b1111 45 + AND = 0b00100 # 4 46 + OR = 0b00101 # 5 47 + XOR = 0b00110 # 6 48 + NOT = 0b00111 # 7 49 + # gap: 8-10 are ArithOp shifts 50 + EQ = 0b01011 # 11 51 + LT = 0b01100 # 12 52 + LTE = 0b01101 # 13 53 + GT = 0b01110 # 14 54 + GTE = 0b01111 # 15 53 55 54 56 55 57 class RoutingOp(ALUOp): 56 - BREQ = 0b10000000 57 - BRGT = 0b10000001 58 - BRGE = 0b10000010 59 - BROF = 0b10000011 60 - # + more? 61 - SWEQ = 0b11000000 62 - SWGT = 0b11000001 63 - SWGE = 0b11000010 64 - SWOF = 0b11000011 65 - # + more? 66 - GATE = 0b11100000 67 - PASS = 0b01000000 68 - CONST = 0b0010000 69 - FREE_CTX = 0b1010000 70 - # uncertain 71 - SEL = 0b11110000 72 - MRGE = 0b11111000 58 + BREQ = 0b10000 # 16 59 + BRGT = 0b10001 # 17 60 + BRGE = 0b10010 # 18 61 + BROF = 0b10011 # 19 62 + SWEQ = 0b10100 # 20 63 + SWGT = 0b10101 # 21 64 + SWGE = 0b10110 # 22 65 + SWOF = 0b10111 # 23 66 + GATE = 0b11000 # 24 67 + SEL = 0b11001 # 25 68 + MRGE = 0b11010 # 26 69 + PASS = 0b11011 # 27 70 + CONST = 0b11100 # 28 71 + FREE_FRAME = 0b11101 # 29 (was FREE_CTX) 72 + ALLOC_REMOTE = 0b11110 # 30 73 + EXTRACT_TAG = 0b11111 # 31 73 74 74 75 75 - @dataclass(frozen=True) 76 - class Addr(object): 77 - a: int 78 - port: Port 79 - pe: Optional[int] 76 + class OutputStyle(Enum): 77 + INHERIT = 0 78 + CHANGE_TAG = 1 79 + SINK = 2 80 80 81 81 82 - @dataclass(frozen=True) 83 - class ALUInst(object): 84 - """ 85 - Instruction stored in IRAM 86 - """ 82 + class TokenKind(Enum): 83 + DYADIC = 0 84 + MONADIC = 1 85 + INLINE = 2 86 + 87 87 88 - op: ALUOp 89 - dest_l: Optional[Addr] 90 - dest_r: Optional[Addr] 91 - const: Optional[int] 92 - ctx_mode: int = 0 # 0=inherit, 1=CTX_OVRD (const overrides ctx) 88 + class FrameOp(IntEnum): 89 + ALLOC = 0 90 + FREE = 1 93 91 94 92 95 93 @dataclass(frozen=True) 96 - class SMInst(object): 97 - """ 98 - SM instruction stored in IRAM. Causes PE to emit an SMToken 99 - instead of routing through the ALU. 94 + class FrameDest: 95 + target_pe: int 96 + offset: int 97 + act_id: int 98 + port: Port 99 + token_kind: TokenKind 100 100 101 - Operand mapping (PE constructs SMToken from instruction + token operands): 102 - READ (monadic): cell_addr = const or token.data, result → ret 103 - WRITE (monadic): cell_addr = const, write_data = token.data 104 - WRITE (dyadic): cell_addr = left, write_data = right 105 - CLEAR/ALLOC/FREE: cell_addr = const or token.data 106 - RD_INC/RD_DEC: cell_addr = const or token.data, result → ret 107 - CMP_SW (dyadic): cell_addr = const, expected = left, new = right, result → ret 108 - """ 109 101 110 - op: MemOp 111 - sm_id: int 112 - const: Optional[int] = None 113 - ret: Optional[Addr] = None 114 - ret_dyadic: bool = False 102 + FrameSlotValue = int | FrameDest | None 103 + 104 + 105 + @dataclass(frozen=True) 106 + class Instruction: 107 + opcode: ALUOp | MemOp 108 + output: OutputStyle 109 + has_const: bool 110 + dest_count: int 111 + wide: bool 112 + fref: int 115 113 116 114 117 115 # Monadic ALU operations: take a single operand ··· 119 117 _MONADIC_ARITH_OPS = frozenset({ 120 118 ArithOp.INC, 121 119 ArithOp.DEC, 122 - ArithOp.SHIFT_L, 123 - ArithOp.SHIFT_R, 124 - ArithOp.ASHFT_R, 120 + ArithOp.SHL, 121 + ArithOp.SHR, 122 + ArithOp.ASR, 125 123 }) 126 124 127 125 _MONADIC_LOGIC_OPS = frozenset({ ··· 131 129 _MONADIC_ROUTING_OPS = frozenset({ 132 130 RoutingOp.PASS, 133 131 RoutingOp.CONST, 134 - RoutingOp.FREE_CTX, 132 + RoutingOp.FREE_FRAME, 133 + RoutingOp.ALLOC_REMOTE, 134 + RoutingOp.EXTRACT_TAG, 135 135 }) 136 136 137 137
+12 -12
design-notes/pe-design.md
··· 327 327 328 328 **Hardware cost:** 329 329 330 - | Component | Chips | Notes | 331 - |----------------------------|-------|-----------------------------------------| 332 - | act_id -> frame_id lookup | 2 | 74LS670, indexed by act_id | 333 - | Presence + port metadata | 4 | 74LS670, indexed by frame_id | 334 - | Bit select mux | 1-2 | offset-based selection of presence/port | 335 - | **Total match metadata** | **~8** | | 330 + | Component | Chips | Notes | 331 + | ------------------------- | ------ | --------------------------------------- | 332 + | act_id -> frame_id lookup | 2 | 74LS670, indexed by act_id | 333 + | Presence + port metadata | 4 | 74LS670, indexed by frame_id | 334 + | Bit select mux | 1-2 | offset-based selection of presence/port | 335 + | **Total match metadata** | **~8** | | 336 336 337 337 **Constraint: 8 matchable offsets per frame.** The assembler enforces 338 338 this. 8 dyadic instructions per function chunk per PE is reasonable -- ··· 504 504 15 14-10 9-7 6 5-0 505 505 ``` 506 506 507 - | Field | Bits | Purpose | 508 - |--------|------|---------| 509 - | type | 1 | 0 = CM compute, 1 = SM operation | 510 - | opcode | 5 | Operation code (CM and SM have independent 32-entry spaces) | 511 - | mode | 3 | Combined tag/frame-reference mode (see mode table below) | 507 + | Field | Bits | Purpose | 508 + | ------ | ---- | ------------------------------------------------------------ | 509 + | type | 1 | 0 = CM compute, 1 = SM operation | 510 + | opcode | 5 | Operation code (CM and SM have independent 32-entry spaces) | 511 + | mode | 3 | Combined tag/frame-reference mode (see mode table below) | 512 512 | wide | 1 | 0 = 16-bit frame values, 1 = 32-bit (consecutive slot pairs) | 513 - | fref | 6 | Frame slot base index (64 slots per activation) | 513 + | fref | 6 | Frame slot base index (64 slots per activation) | 514 514 515 515 **type:1** -- operation space select: 516 516 ```
+945
docs/implementation-plans/2026-03-06-pe-frame-redesign/phase_01.md
··· 1 + # PE Frame-Based Redesign — Phase 1: Foundation Types 2 + 3 + **Goal:** Establish all shared types that both the emulator and assembler depend on, plus encoding boundary functions. 4 + 5 + **Architecture:** Add new types (`Instruction`, `OutputStyle`, `FrameOp`, `FrameDest`, `TokenKind`, `FrameSlotValue`) to `cm_inst.py`, restructure the token hierarchy in `tokens.py` with a new `PEToken` intermediate base class, create `encoding.py` with pack/unpack boundary functions, and update `emu/events.py` and `emu/types.py` for frame-based config. Old types (`ALUInst`, `SMInst`, `IRAMWriteToken`, `MatchEntry`) are removed — this will break existing consumers. Fixes come in later phases. 6 + 7 + **Tech Stack:** Python 3.12 (dataclasses, enums, IntEnum) 8 + 9 + **Scope:** Phase 1 of 8 from the PE frame-based redesign design plan. 10 + 11 + **Codebase verified:** 2026-03-06 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### pe-frame-redesign.AC1: Token hierarchy correctly models frame-based architecture 20 + - **pe-frame-redesign.AC1.1 Success:** `PEToken` base class exists; `CMToken`, `PELocalWriteToken`, `FrameControlToken` all inherit from it 21 + - **pe-frame-redesign.AC1.2 Success:** `CMToken.act_id` replaces `ctx` throughout; `DyadToken` has no `gen` field 22 + - **pe-frame-redesign.AC1.3 Success:** `PELocalWriteToken` carries `region`, `slot`, `data`, `is_dest` fields 23 + - **pe-frame-redesign.AC1.4 Success:** `FrameControlToken` carries `act_id`, `op` (FrameOp), `payload` 24 + - **pe-frame-redesign.AC1.6 Failure:** Token with invalid `act_id` (no tag store mapping) is dropped with `TokenRejected` event 25 + 26 + ### pe-frame-redesign.AC2: Semantic Instruction model with pack/unpack 27 + - **pe-frame-redesign.AC2.1 Success:** `Instruction` carries `opcode`, `OutputStyle`, `has_const`, `dest_count`, `wide`, `fref` 28 + - **pe-frame-redesign.AC2.2 Success:** `pack_instruction()` and `unpack_instruction()` round-trip for all valid mode combinations 29 + - **pe-frame-redesign.AC2.3 Success:** CM vs SM type derived from `isinstance(opcode, MemOp)`, not a stored field 30 + - **pe-frame-redesign.AC2.4 Success:** Emulator PE never touches raw mode bits — only semantic fields 31 + 32 + --- 33 + 34 + <!-- START_SUBCOMPONENT_A (tasks 1-3) --> 35 + <!-- START_TASK_1 --> 36 + ### Task 1: Add new types to cm_inst.py 37 + 38 + **Verifies:** pe-frame-redesign.AC2.1, pe-frame-redesign.AC2.3 39 + 40 + **Files:** 41 + - Modify: `cm_inst.py` (add new types, remove ALUInst/SMInst/Addr, replace FREE_CTX with FREE_FRAME) 42 + 43 + **Implementation:** 44 + 45 + Remove the `ALUInst`, `SMInst`, and `Addr` dataclasses entirely from `cm_inst.py`. This will break consumers — that is intentional and will be fixed in later phases. 46 + 47 + Renumber ALL enum values to match the 5-bit hardware opcode encoding from `design-notes/alu-and-output-design.md`. The Python IntEnum values become identical to the hardware opcodes — no mapping table needed. This changes the grouping: shifts move from ArithOp to their own positions (8-10), and logic ops move to positions 4-7. 48 + 49 + ```python 50 + class ArithOp(ALUOp): 51 + ADD = 0b00000 # 0 52 + SUB = 0b00001 # 1 53 + INC = 0b00010 # 2 54 + DEC = 0b00011 # 3 55 + # gap: 4-7 are LogicOp 56 + SHL = 0b01000 # 8 (was SHIFT_L = 4) 57 + SHR = 0b01001 # 9 (was SHIFT_R = 5) 58 + ASR = 0b01010 # 10 (was ASHFT_R = 6) 59 + 60 + 61 + class LogicOp(ALUOp): 62 + AND = 0b00100 # 4 63 + OR = 0b00101 # 5 64 + XOR = 0b00110 # 6 65 + NOT = 0b00111 # 7 66 + # gap: 8-10 are ArithOp shifts 67 + EQ = 0b01011 # 11 68 + LT = 0b01100 # 12 69 + LTE = 0b01101 # 13 70 + GT = 0b01110 # 14 71 + GTE = 0b01111 # 15 72 + 73 + 74 + class RoutingOp(ALUOp): 75 + BREQ = 0b10000 # 16 76 + BRGT = 0b10001 # 17 77 + BRGE = 0b10010 # 18 78 + BROF = 0b10011 # 19 79 + SWEQ = 0b10100 # 20 80 + SWGT = 0b10101 # 21 81 + SWGE = 0b10110 # 22 82 + SWOF = 0b10111 # 23 83 + GATE = 0b11000 # 24 84 + SEL = 0b11001 # 25 85 + MRGE = 0b11010 # 26 86 + PASS = 0b11011 # 27 87 + CONST = 0b11100 # 28 88 + FREE_FRAME = 0b11101 # 29 (was FREE_CTX) 89 + ALLOC_REMOTE = 0b11110 # 30 90 + EXTRACT_TAG = 0b11111 # 31 (reserved slot in design notes) 91 + ``` 92 + 93 + Shifts stay in `ArithOp` but are renamed (`SHIFT_L` → `SHL`, `SHIFT_R` → `SHR`, `ASHFT_R` → `ASR`) and renumbered to match the 5-bit hardware encoding (8-10 instead of 4-6). IntEnum doesn't require contiguous values, so the gap between DEC(3) and SHL(8) is fine. 94 + 95 + `ALLOC_REMOTE` (opcode 30) is new — constructs and emits a frame control token targeting another PE. See `design-notes/alu-and-output-design.md` Frame Management section. 96 + 97 + Update `_MONADIC_ARITH_OPS` and `_MONADIC_ROUTING_OPS`: 98 + 99 + ```python 100 + _MONADIC_ARITH_OPS = frozenset({ 101 + ArithOp.INC, 102 + ArithOp.DEC, 103 + ArithOp.SHL, 104 + ArithOp.SHR, 105 + ArithOp.ASR, 106 + }) 107 + 108 + _MONADIC_ROUTING_OPS = frozenset({ 109 + RoutingOp.PASS, 110 + RoutingOp.CONST, 111 + RoutingOp.FREE_FRAME, 112 + RoutingOp.ALLOC_REMOTE, 113 + RoutingOp.EXTRACT_TAG, 114 + }) 115 + ``` 116 + 117 + `_MONADIC_LOGIC_OPS` remains unchanged — it contains `LogicOp.NOT` by enum identity (frozenset membership uses the enum member object, not the integer value, so the renumbering from 10 to 7 does not affect it). 118 + 119 + Add the following new types to `cm_inst.py`, after the existing `MemOp` enum but before `_MONADIC_ARITH_OPS`: 120 + 121 + ```python 122 + from enum import Enum 123 + 124 + class OutputStyle(Enum): 125 + INHERIT = 0 126 + CHANGE_TAG = 1 127 + SINK = 2 128 + 129 + 130 + class TokenKind(Enum): 131 + DYADIC = 0 132 + MONADIC = 1 133 + INLINE = 2 134 + 135 + 136 + class FrameOp(IntEnum): 137 + ALLOC = 0 138 + FREE = 1 139 + 140 + 141 + @dataclass(frozen=True) 142 + class FrameDest: 143 + target_pe: int 144 + offset: int 145 + act_id: int 146 + port: Port 147 + token_kind: TokenKind 148 + 149 + 150 + FrameSlotValue = int | FrameDest | None 151 + 152 + 153 + @dataclass(frozen=True) 154 + class Instruction: 155 + opcode: ALUOp | MemOp 156 + output: OutputStyle 157 + has_const: bool 158 + dest_count: int 159 + wide: bool 160 + fref: int 161 + ``` 162 + 163 + The `Instruction` dataclass has no stored `type` field — CM vs SM is derived via `isinstance(inst.opcode, MemOp)` per AC2.3. 164 + 165 + **Testing:** 166 + 167 + Tests must verify: 168 + - pe-frame-redesign.AC2.1: `Instruction` dataclass can be constructed with all field types and is frozen 169 + - pe-frame-redesign.AC2.3: `isinstance(Instruction(opcode=MemOp.READ, ...).opcode, MemOp)` returns True; `isinstance(Instruction(opcode=ArithOp.ADD, ...).opcode, MemOp)` returns False 170 + - `FrameDest` is frozen and stores all fields correctly 171 + - `OutputStyle`, `TokenKind`, `FrameOp` enum values are correct 172 + - `RoutingOp.EXTRACT_TAG` exists with value 31 and `is_monadic_alu(RoutingOp.EXTRACT_TAG)` returns True 173 + - `RoutingOp.FREE_FRAME` exists with value 29 174 + - `RoutingOp.FREE_CTX` does NOT exist 175 + - `RoutingOp.ALLOC_REMOTE` exists with value 30 and `is_monadic_alu(RoutingOp.ALLOC_REMOTE)` returns True 176 + - `ArithOp.SHIFT_L`, `ArithOp.SHIFT_R`, `ArithOp.ASHFT_R` do NOT exist (renamed to SHL/SHR/ASR) 177 + - `ArithOp.SHL` exists with value 8, `ArithOp.SHR` with value 9, `ArithOp.ASR` with value 10 178 + - All ALUOp enum values fit in 5 bits (0-31) 179 + 180 + Test file: `tests/test_foundation_types.py` 181 + 182 + **Verification:** 183 + Run: `python -m pytest tests/test_foundation_types.py -v` 184 + Expected: Foundation type tests pass 185 + 186 + **Commit:** `feat: add Instruction, OutputStyle, FrameDest, TokenKind, FrameOp; renumber all opcodes to 5-bit hardware encoding; remove ALUInst, SMInst, Addr, FREE_CTX` 187 + 188 + <!-- END_TASK_1 --> 189 + 190 + <!-- START_TASK_2 --> 191 + ### Task 2: Restructure token hierarchy in tokens.py 192 + 193 + **Verifies:** pe-frame-redesign.AC1.1, pe-frame-redesign.AC1.2, pe-frame-redesign.AC1.3, pe-frame-redesign.AC1.4 194 + 195 + **Files:** 196 + - Modify: `tokens.py` (insert PEToken between Token and CMToken, add new token types, rename ctx→act_id, remove gen from DyadToken, remove IRAMWriteToken) 197 + 198 + **Implementation:** 199 + 200 + Rewrite `tokens.py` to the new hierarchy. The key changes: 201 + 202 + 1. Add `PEToken(Token)` base class — carries only `target` (inherited from Token) 203 + 2. `CMToken` now inherits from `PEToken` instead of `Token` 204 + 3. Rename `CMToken.ctx` → `CMToken.act_id` 205 + 4. Remove `DyadToken.gen` field 206 + 5. Add `PELocalWriteToken(PEToken)` with `act_id`, `region`, `slot`, `data`, `is_dest` fields 207 + 6. Add `FrameControlToken(PEToken)` with `act_id`, `op` (FrameOp), `payload` fields 208 + 7. Remove `IRAMWriteToken` entirely 209 + 210 + The import from `cm_inst` no longer needs `ALUInst` or `SMInst` since `IRAMWriteToken` is gone: 211 + 212 + ```python 213 + from dataclasses import dataclass 214 + from typing import Optional 215 + 216 + from cm_inst import FrameOp, MemOp, Port 217 + 218 + 219 + @dataclass(frozen=True) 220 + class Token: 221 + target: int 222 + 223 + 224 + @dataclass(frozen=True) 225 + class PEToken(Token): 226 + pass 227 + 228 + 229 + @dataclass(frozen=True) 230 + class CMToken(PEToken): 231 + offset: int 232 + act_id: int 233 + data: int 234 + 235 + 236 + @dataclass(frozen=True) 237 + class DyadToken(CMToken): 238 + port: Port 239 + wide: bool 240 + 241 + 242 + @dataclass(frozen=True) 243 + class MonadToken(CMToken): 244 + inline: bool 245 + 246 + 247 + @dataclass(frozen=True) 248 + class PELocalWriteToken(PEToken): 249 + act_id: int 250 + region: int 251 + slot: int 252 + data: int 253 + is_dest: bool 254 + 255 + 256 + @dataclass(frozen=True) 257 + class FrameControlToken(PEToken): 258 + act_id: int 259 + op: FrameOp 260 + payload: int 261 + 262 + 263 + @dataclass(frozen=True) 264 + class SMToken(Token): 265 + addr: int 266 + op: MemOp 267 + flags: Optional[int] 268 + data: Optional[int] 269 + ret: Optional[CMToken] 270 + ``` 271 + 272 + Note: `PELocalWriteToken` and `FrameControlToken` have their own `act_id` field (not inherited from CMToken, since they don't carry `offset`/`data` with the same semantics). 273 + 274 + **Testing:** 275 + 276 + Tests must verify each AC: 277 + - pe-frame-redesign.AC1.1: `issubclass(CMToken, PEToken)`, `issubclass(PELocalWriteToken, PEToken)`, `issubclass(FrameControlToken, PEToken)`, and `issubclass(PEToken, Token)` 278 + - pe-frame-redesign.AC1.2: `CMToken` constructor uses `act_id` keyword (not `ctx`); `DyadToken` constructor has no `gen` parameter 279 + - pe-frame-redesign.AC1.3: `PELocalWriteToken` has `region`, `slot`, `data`, `is_dest` fields 280 + - pe-frame-redesign.AC1.4: `FrameControlToken` has `act_id`, `op`, `payload` fields; `op` accepts `FrameOp` values 281 + - `isinstance(DyadToken(...), PEToken)` returns True (transitive inheritance) 282 + - `isinstance(SMToken(...), PEToken)` returns False 283 + - All token types are frozen dataclasses 284 + - `IRAMWriteToken` does NOT exist in `tokens.py` 285 + 286 + Test file: `tests/test_foundation_types.py` (append to same file from Task 1) 287 + 288 + **Verification:** 289 + Run: `python -m pytest tests/test_foundation_types.py -v` 290 + Expected: Foundation type tests pass 291 + 292 + **Commit:** `feat: restructure token hierarchy with PEToken base, act_id rename, new token types; remove IRAMWriteToken` 293 + 294 + <!-- END_TASK_2 --> 295 + 296 + <!-- START_TASK_3 --> 297 + ### Task 3: Update conftest.py Hypothesis strategies 298 + 299 + **Verifies:** None (infrastructure — supports future test phases) 300 + 301 + **Files:** 302 + - Modify: `tests/conftest.py` (update strategies for ctx→act_id rename, gen removal) 303 + 304 + **Implementation:** 305 + 306 + Update the Hypothesis strategies to match the new token field names: 307 + 308 + 1. `dyad_token` strategy: rename `ctx` parameter → `act_id`, remove `gen` parameter, update `DyadToken` constructor call 309 + 2. `monad_token` strategy: rename `ctx` parameter → `act_id`, update `MonadToken` constructor call 310 + 3. `sm_return_route` strategy: update `CMToken` constructor to use `act_id` instead of `ctx` 311 + 4. `sm_token` strategy: update the embedded `CMToken` return route to use `act_id` 312 + 5. Add new strategies for `PELocalWriteToken` and `FrameControlToken` 313 + 314 + Updated `dyad_token`: 315 + ```python 316 + @st.composite 317 + def dyad_token(draw, target: int = 0, offset: int | None = None, act_id: int | None = None) -> DyadToken: 318 + return DyadToken( 319 + target=target, 320 + offset=draw(st.integers(min_value=0, max_value=63)) if offset is None else offset, 321 + act_id=draw(st.integers(min_value=0, max_value=7)) if act_id is None else act_id, 322 + data=draw(uint16), 323 + port=draw(st.sampled_from(list(Port))), 324 + wide=False, 325 + ) 326 + ``` 327 + 328 + Updated `monad_token`: 329 + ```python 330 + @st.composite 331 + def monad_token(draw, target: int = 0, offset: int | None = None, act_id: int | None = None) -> MonadToken: 332 + return MonadToken( 333 + target=target, 334 + offset=draw(st.integers(min_value=0, max_value=63)) if offset is None else offset, 335 + act_id=draw(st.integers(min_value=0, max_value=7)) if act_id is None else act_id, 336 + data=draw(uint16), 337 + inline=False, 338 + ) 339 + ``` 340 + 341 + Updated `sm_return_route`: 342 + ```python 343 + @st.composite 344 + def sm_return_route(draw, target=0): 345 + return CMToken( 346 + target=target, 347 + offset=draw(st.integers(min_value=0, max_value=63)), 348 + act_id=draw(st.integers(min_value=0, max_value=7)), 349 + data=0, 350 + ) 351 + ``` 352 + 353 + Updated `sm_token` (the embedded CMToken): 354 + ```python 355 + @st.composite 356 + def sm_token(draw, addr=None, op=None, data=None): 357 + _addr = draw(st.integers(min_value=0, max_value=255)) if addr is None else addr 358 + _op = draw(sm_all_ops) if op is None else op 359 + _data = draw(uint16) if data is None else data 360 + ret = CMToken(target=0, offset=0, act_id=0, data=0) 361 + return SMToken( 362 + target=0, 363 + addr=_addr, 364 + op=_op, 365 + flags=None, 366 + data=_data, 367 + ret=ret, 368 + ) 369 + ``` 370 + 371 + New strategies to add: 372 + ```python 373 + from cm_inst import FrameOp 374 + from tokens import FrameControlToken, PELocalWriteToken 375 + 376 + @st.composite 377 + def pe_local_write_token(draw, target: int = 0, act_id: int | None = None) -> PELocalWriteToken: 378 + return PELocalWriteToken( 379 + target=target, 380 + act_id=draw(st.integers(min_value=0, max_value=7)) if act_id is None else act_id, 381 + region=draw(st.integers(min_value=0, max_value=1)), 382 + slot=draw(st.integers(min_value=0, max_value=63)), 383 + data=draw(uint16), 384 + is_dest=draw(st.booleans()), 385 + ) 386 + 387 + @st.composite 388 + def frame_control_token(draw, target: int = 0, act_id: int | None = None) -> FrameControlToken: 389 + return FrameControlToken( 390 + target=target, 391 + act_id=draw(st.integers(min_value=0, max_value=7)) if act_id is None else act_id, 392 + op=draw(st.sampled_from(list(FrameOp))), 393 + payload=draw(uint16), 394 + ) 395 + ``` 396 + 397 + Note: `act_id` range is 0-7 (3-bit), not 0-3 as the old `ctx` was. This matches the design's 3-bit activation ID with `frame_count` up to 8. 398 + 399 + Also update the `conftest.py` imports to include `FrameControlToken`, `PELocalWriteToken`, and `FrameOp`. 400 + 401 + **Verification:** 402 + Run: `python -m pytest tests/test_foundation_types.py -v` 403 + Expected: Foundation type tests pass (strategies are used by foundation type tests) 404 + 405 + **Commit:** `refactor: update Hypothesis strategies for act_id rename and new token types` 406 + 407 + <!-- END_TASK_3 --> 408 + <!-- END_SUBCOMPONENT_A --> 409 + 410 + <!-- START_SUBCOMPONENT_B (tasks 4-6) --> 411 + <!-- START_TASK_4 --> 412 + ### Task 4: Create encoding.py with pack/unpack boundary functions 413 + 414 + **Verifies:** pe-frame-redesign.AC2.2 415 + 416 + **Files:** 417 + - Create: `encoding.py` (project root, alongside cm_inst.py and tokens.py) 418 + 419 + **Implementation:** 420 + 421 + Create `encoding.py` containing the six boundary functions specified in the design. These convert between semantic Python types and 16-bit hardware word representations. 422 + 423 + The instruction word format is: `[type:1][opcode:5][mode:3][wide:1][fref:6]` 424 + 425 + Mode encoding uses the 3-bit table from `design-notes/alu-and-output-design.md`: 426 + 427 + ``` 428 + mode tag behaviour output behaviour 429 + ---- ------------- -------------------------- 430 + 0 INHERIT single output, no constant 431 + 1 INHERIT single output with constant 432 + 2 INHERIT fan-out, no constant 433 + 3 INHERIT fan-out with constant 434 + 4 CHANGE_TAG dynamic routing, no constant 435 + 5 CHANGE_TAG dynamic routing with constant 436 + 6 SINK write result to frame, no output 437 + 7 SINK+CONST read-modify-write frame slot 438 + ``` 439 + 440 + Bit-level decode (directly from mode[2:1:0]): 441 + ``` 442 + output_enable = NOT mode[2] modes 0-3 443 + change_tag = mode[2] AND NOT mode[1] modes 4-5 444 + sink = mode[2] AND mode[1] modes 6-7 445 + has_const = mode[0] modes 1, 3, 5, 7 446 + has_fanout = mode[1] AND NOT mode[2] modes 2-3 447 + ``` 448 + 449 + Since the Python IntEnum values match the 5-bit hardware opcodes (set up in Task 1), opcode encoding is a trivial identity operation — no mapping table needed. 450 + 451 + ```python 452 + from __future__ import annotations 453 + 454 + from cm_inst import ( 455 + ALUOp, ArithOp, FrameDest, Instruction, LogicOp, MemOp, 456 + OutputStyle, Port, RoutingOp, TokenKind, 457 + ) 458 + from tokens import ( 459 + CMToken, DyadToken, FrameControlToken, MonadToken, 460 + PELocalWriteToken, PEToken, SMToken, Token, 461 + ) 462 + 463 + 464 + def _encode_mode(output: OutputStyle, has_const: bool, dest_count: int) -> int: 465 + """Encode OutputStyle + has_const + dest_count into 3-bit mode field. 466 + 467 + Follows the mode table from design-notes/alu-and-output-design.md. 468 + """ 469 + const_bit = int(has_const) 470 + if output == OutputStyle.INHERIT: 471 + if dest_count == 1: 472 + return 0b000 | const_bit # mode 0 or 1 473 + elif dest_count == 2: 474 + return 0b010 | const_bit # mode 2 or 3 475 + raise ValueError(f"INHERIT requires dest_count 1 or 2, got {dest_count}") 476 + elif output == OutputStyle.CHANGE_TAG: 477 + return 0b100 | const_bit # mode 4 or 5 478 + elif output == OutputStyle.SINK: 479 + return 0b110 | const_bit # mode 6 or 7 480 + raise ValueError(f"Unknown OutputStyle: {output}") 481 + 482 + 483 + def _decode_mode(mode: int) -> tuple[OutputStyle, bool, int]: 484 + """Decode 3-bit mode field into (OutputStyle, has_const, dest_count).""" 485 + has_const = bool(mode & 0b001) 486 + if not (mode & 0b100): 487 + # modes 0-3: INHERIT 488 + dest_count = 2 if (mode & 0b010) else 1 489 + return OutputStyle.INHERIT, has_const, dest_count 490 + elif not (mode & 0b010): 491 + # modes 4-5: CHANGE_TAG 492 + # dest_count=1 is nominal — CHANGE_TAG reads destination from the left 493 + # operand (packed flit 1), not from frame slots. The PE ignores dest_count 494 + # for CHANGE_TAG; this value exists only for round-trip consistency. 495 + return OutputStyle.CHANGE_TAG, has_const, 1 496 + else: 497 + # modes 6-7: SINK 498 + return OutputStyle.SINK, has_const, 0 499 + 500 + 501 + def _encode_opcode(opcode: ALUOp | MemOp) -> tuple[int, int]: 502 + """Return (type_bit, 5-bit opcode). 503 + 504 + Python IntEnum values match hardware encoding directly. 505 + MemOp uses type_bit=1 with its own independent 5-bit opcode space. 506 + ALUOp (ArithOp, LogicOp, RoutingOp) uses type_bit=0. 507 + """ 508 + if isinstance(opcode, MemOp): 509 + return 1, int(opcode) & 0x1F 510 + return 0, int(opcode) & 0x1F 511 + 512 + 513 + def _decode_opcode(type_bit: int, raw_opcode: int) -> ALUOp | MemOp: 514 + """Decode type_bit + 5-bit opcode into Python enum.""" 515 + if type_bit: 516 + return MemOp(raw_opcode) 517 + for cls in (ArithOp, LogicOp, RoutingOp): 518 + try: 519 + return cls(raw_opcode) 520 + except ValueError: 521 + continue 522 + raise ValueError(f"Unknown ALU opcode: {raw_opcode}") 523 + 524 + 525 + def pack_instruction(inst: Instruction) -> int: 526 + """Convert semantic Instruction to 16-bit hardware word. 527 + 528 + Format: [type:1][opcode:5][mode:3][wide:1][fref:6] 529 + """ 530 + type_bit, opcode_bits = _encode_opcode(inst.opcode) 531 + mode_bits = _encode_mode(inst.output, inst.has_const, inst.dest_count) 532 + wide_bit = int(inst.wide) 533 + fref_bits = inst.fref & 0x3F 534 + return (type_bit << 15) | (opcode_bits << 10) | (mode_bits << 7) | (wide_bit << 6) | fref_bits 535 + 536 + 537 + def unpack_instruction(word: int) -> Instruction: 538 + """Convert 16-bit hardware word to semantic Instruction.""" 539 + type_bit = (word >> 15) & 1 540 + opcode_raw = (word >> 10) & 0x1F 541 + mode = (word >> 7) & 0x07 542 + wide = bool((word >> 6) & 1) 543 + fref = word & 0x3F 544 + 545 + opcode = _decode_opcode(type_bit, opcode_raw) 546 + output, has_const, dest_count = _decode_mode(mode) 547 + return Instruction( 548 + opcode=opcode, 549 + output=output, 550 + has_const=has_const, 551 + dest_count=dest_count, 552 + wide=wide, 553 + fref=fref, 554 + ) 555 + ``` 556 + 557 + For the flit-level functions, the flit 1 format matches the hardware encoding from `architecture-overview.md`. Destinations are "pre-formed flit 1 values stored in frame slots" — the emulator uses the exact hardware bit layout so frame slots can be put on the bus verbatim. 558 + 559 + The encoding varies by token kind: 560 + 561 + ``` 562 + DYADIC: [00][port:1][PE:2][offset:8][act_id:3] = 16 bits 563 + MONADIC: [010][PE:2][offset:8][act_id:3] = 16 bits 564 + INLINE: [011][PE:2][10][offset:7][spare:2] = 16 bits 565 + ``` 566 + 567 + ```python 568 + def pack_flit1(dest: FrameDest) -> int: 569 + """Pack structured FrameDest to 16-bit flit 1 value. 570 + 571 + Uses the exact hardware bit layout from architecture-overview.md. 572 + """ 573 + if dest.token_kind == TokenKind.DYADIC: 574 + # [00][port:1][PE:2][offset:8][act_id:3] 575 + return ( 576 + ((dest.port & 0x1) << 13) 577 + | ((dest.target_pe & 0x3) << 11) 578 + | ((dest.offset & 0xFF) << 3) 579 + | (dest.act_id & 0x7) 580 + ) 581 + elif dest.token_kind == TokenKind.MONADIC: 582 + # [010][PE:2][offset:8][act_id:3] 583 + return ( 584 + (0b010 << 13) 585 + | ((dest.target_pe & 0x3) << 11) 586 + | ((dest.offset & 0xFF) << 3) 587 + | (dest.act_id & 0x7) 588 + ) 589 + else: 590 + # INLINE: [011][PE:2][10][offset:7][spare:2] 591 + return ( 592 + (0b011 << 13) 593 + | ((dest.target_pe & 0x3) << 11) 594 + | (0b10 << 9) 595 + | ((dest.offset & 0x7F) << 2) 596 + ) 597 + 598 + 599 + def unpack_flit1(flit1: int) -> FrameDest: 600 + """Unpack 16-bit flit 1 value to structured FrameDest.""" 601 + top2 = (flit1 >> 14) & 0x3 602 + if top2 == 0b00: 603 + # DYADIC WIDE 604 + return FrameDest( 605 + target_pe=(flit1 >> 11) & 0x3, 606 + offset=(flit1 >> 3) & 0xFF, 607 + act_id=flit1 & 0x7, 608 + port=Port((flit1 >> 13) & 0x1), 609 + token_kind=TokenKind.DYADIC, 610 + ) 611 + elif (flit1 >> 13) == 0b010: 612 + # MONADIC NORMAL 613 + return FrameDest( 614 + target_pe=(flit1 >> 11) & 0x3, 615 + offset=(flit1 >> 3) & 0xFF, 616 + act_id=flit1 & 0x7, 617 + port=Port.L, 618 + token_kind=TokenKind.MONADIC, 619 + ) 620 + else: 621 + # MONADIC INLINE: [011][PE:2][10][offset:7][spare:2] 622 + return FrameDest( 623 + target_pe=(flit1 >> 11) & 0x3, 624 + offset=(flit1 >> 2) & 0x7F, 625 + act_id=0, 626 + port=Port.L, 627 + token_kind=TokenKind.INLINE, 628 + ) 629 + ``` 630 + 631 + For `pack_token`, `unpack_token`, and `flit_count`, these encode/decode full Token objects as flit sequences using the exact hardware wire format from `architecture-overview.md`. Flit 1 is the first flit on the wire — it contains the prefix, routing, and type fields. Subsequent flits carry data. 632 + 633 + **Note on SMToken.ret:** The `ret` field (return route CMToken) is NOT preserved through pack/unpack. This is by design — in the frame model, SM return routes are stored as `FrameDest` values in frame slots, not embedded in the token. T0 storage only needs to hold the SM request parameters (target, addr, op, data, flags). The PE reads the return route from the frame when constructing the SMToken. 634 + 635 + ```python 636 + def flit_count(flit1: int) -> int: 637 + """Given flit 1 (the first/header flit), return total flit count for this packet.""" 638 + if flit1 & 0x8000: 639 + # SM token: bit[15]=1. Standard = 2 flits, CAS/EXT = 3. 640 + return 2 641 + prefix3 = (flit1 >> 13) & 0x7 642 + if prefix3 <= 0b001: 643 + # Dyadic wide (00x): flit 1 + flit 2 (data) 644 + return 2 645 + elif prefix3 == 0b010: 646 + # Monadic normal: flit 1 + flit 2 (data) 647 + return 2 648 + elif prefix3 == 0b011: 649 + sub = (flit1 >> 9) & 0x3 650 + if sub == 0b10: 651 + # Monadic inline: 1 flit only (no data flit) 652 + return 1 653 + else: 654 + # Frame control, PE-local write: flit 1 + flit 2 655 + return 2 656 + return 2 657 + 658 + 659 + def pack_token(token: Token) -> list[int]: 660 + """Encode a token as a sequence of 16-bit flits. 661 + 662 + Uses the exact hardware wire format. Flit 1 is the routing/header flit. 663 + Used for T0 storage (EXEC reads these back) and future binary output. 664 + """ 665 + if isinstance(token, DyadToken): 666 + dest = FrameDest( 667 + target_pe=token.target, 668 + offset=token.offset, 669 + act_id=token.act_id, 670 + port=token.port, 671 + token_kind=TokenKind.DYADIC, 672 + ) 673 + flit1 = pack_flit1(dest) 674 + flit2 = token.data & 0xFFFF 675 + return [flit1, flit2] 676 + elif isinstance(token, MonadToken): 677 + kind = TokenKind.INLINE if token.inline else TokenKind.MONADIC 678 + dest = FrameDest( 679 + target_pe=token.target, 680 + offset=token.offset, 681 + act_id=token.act_id, 682 + port=Port.L, 683 + token_kind=kind, 684 + ) 685 + flit1 = pack_flit1(dest) 686 + if token.inline: 687 + return [flit1] # monadic inline: 1 flit only 688 + flit2 = token.data & 0xFFFF 689 + return [flit1, flit2] 690 + elif isinstance(token, SMToken): 691 + # SM: [1][SM_id:2][op:3][addr:10] (tier 1 layout) 692 + flit1 = (1 << 15) | ((token.target & 0x3) << 13) | ((int(token.op) & 0x7) << 10) | (token.addr & 0x3FF) 693 + flit2 = (token.data or 0) & 0xFFFF 694 + return [flit1, flit2] 695 + raise ValueError(f"Cannot pack token type: {type(token).__name__}") 696 + 697 + 698 + def unpack_token(flits: list[int]) -> Token: 699 + """Decode a flit sequence into a Token object. 700 + 701 + Flit 1 (flits[0]) is the header/routing flit. Decodes using hardware format. 702 + """ 703 + flit1 = flits[0] 704 + 705 + if flit1 & 0x8000: 706 + # SM token: [1][SM_id:2][op:3][addr:10] 707 + sm_id = (flit1 >> 13) & 0x3 708 + op = MemOp((flit1 >> 10) & 0x7) 709 + addr = flit1 & 0x3FF 710 + return SMToken( 711 + target=sm_id, 712 + addr=addr, 713 + op=op, 714 + flags=None, 715 + data=flits[1] if len(flits) > 1 else 0, 716 + ret=None, 717 + ) 718 + 719 + dest = unpack_flit1(flit1) 720 + 721 + if dest.token_kind == TokenKind.DYADIC: 722 + return DyadToken( 723 + target=dest.target_pe, 724 + offset=dest.offset, 725 + act_id=dest.act_id, 726 + data=flits[1] if len(flits) > 1 else 0, 727 + port=dest.port, 728 + wide=False, 729 + ) 730 + elif dest.token_kind == TokenKind.INLINE: 731 + return MonadToken( 732 + target=dest.target_pe, 733 + offset=dest.offset, 734 + act_id=dest.act_id, 735 + data=0, 736 + inline=True, 737 + ) 738 + else: 739 + # MONADIC normal 740 + return MonadToken( 741 + target=dest.target_pe, 742 + offset=dest.offset, 743 + act_id=dest.act_id, 744 + data=flits[1] if len(flits) > 1 else 0, 745 + inline=False, 746 + ) 747 + ``` 748 + 749 + **Testing:** 750 + 751 + Tests must verify: 752 + - pe-frame-redesign.AC2.2: `pack_instruction` / `unpack_instruction` round-trip for all valid mode combinations (modes 0-7 from design-notes table) × (wide True/False) × various opcodes (ArithOp, LogicOp, RoutingOp, MemOp) × fref values 0-63 753 + - Verify mode encoding matches design-notes bit decode: `_encode_mode(INHERIT, False, 1)` → 0, `_encode_mode(INHERIT, True, 2)` → 3, `_encode_mode(CHANGE_TAG, False, 1)` → 4, `_encode_mode(SINK, True, 0)` → 7 754 + - Verify opcode encoding is identity: `_encode_opcode(ArithOp.ADD)` → `(0, 0)`, `_encode_opcode(RoutingOp.BREQ)` → `(0, 16)`, `_encode_opcode(MemOp.READ)` → `(1, 0)` 755 + - `pack_flit1` / `unpack_flit1` round-trip for all CM token formats (various target_pe, offset, act_id, port, token_kind combinations) 756 + - `pack_token` / `unpack_token` round-trip for DyadToken, MonadToken, SMToken (note: SMToken.ret is NOT preserved — verify it round-trips as None) 757 + - `flit_count` returns correct counts for each prefix type 758 + - Edge cases: fref=0, fref=63, target_pe=0, target_pe=15, act_id=0, act_id=7 759 + 760 + Use Hypothesis property-based testing for round-trip verification — generate random valid Instruction/FrameDest values and verify pack→unpack identity. 761 + 762 + Test file: `tests/test_encoding.py` 763 + 764 + **Verification:** 765 + Run: `python -m pytest tests/test_encoding.py -v` 766 + Expected: Encoding tests pass 767 + 768 + **Commit:** `feat: create encoding.py with pack/unpack boundary functions` 769 + 770 + <!-- END_TASK_4 --> 771 + 772 + <!-- START_TASK_5 --> 773 + ### Task 5: Update emu/events.py for frame-based events 774 + 775 + **Verifies:** pe-frame-redesign.AC1.6 776 + 777 + **Files:** 778 + - Modify: `emu/events.py` (rename Matched.ctx → act_id, add frame_id; add new event types; update SimEvent union) 779 + 780 + **Implementation:** 781 + 782 + Changes to `emu/events.py`: 783 + 784 + 1. Update `Matched` — rename `ctx` to `act_id`, add `frame_id: int` field: 785 + ```python 786 + @dataclass(frozen=True) 787 + class Matched: 788 + time: float 789 + component: str 790 + left: int 791 + right: int 792 + act_id: int 793 + offset: int 794 + frame_id: int 795 + ``` 796 + 797 + 2. Add four new event types after `ResultSent`: 798 + ```python 799 + @dataclass(frozen=True) 800 + class FrameAllocated: 801 + time: float 802 + component: str 803 + act_id: int 804 + frame_id: int 805 + 806 + 807 + @dataclass(frozen=True) 808 + class FrameFreed: 809 + time: float 810 + component: str 811 + act_id: int 812 + frame_id: int 813 + 814 + 815 + @dataclass(frozen=True) 816 + class FrameSlotWritten: 817 + time: float 818 + component: str 819 + frame_id: int 820 + slot: int 821 + value: int | None 822 + 823 + 824 + @dataclass(frozen=True) 825 + class TokenRejected: 826 + time: float 827 + component: str 828 + token: Token 829 + reason: str 830 + ``` 831 + 832 + 3. Update the `SimEvent` union to include all new types: 833 + ```python 834 + SimEvent = ( 835 + TokenReceived | Matched | Executed | Emitted | IRAMWritten 836 + | CellWritten | DeferredRead | DeferredSatisfied | ResultSent 837 + | FrameAllocated | FrameFreed | FrameSlotWritten | TokenRejected 838 + ) 839 + ``` 840 + 841 + **Testing:** 842 + 843 + Tests must verify: 844 + - pe-frame-redesign.AC1.6: `TokenRejected` event can be constructed with token and reason fields; it's a valid `SimEvent` member 845 + - `Matched` now uses `act_id` and has `frame_id` 846 + - All new event types are frozen dataclasses 847 + - All new event types are included in `SimEvent` union (isinstance checks) 848 + 849 + Test file: `tests/test_foundation_types.py` (append to existing) 850 + 851 + **Verification:** 852 + Run: `python -m pytest tests/test_foundation_types.py -v` 853 + Expected: Foundation type tests pass 854 + 855 + **Commit:** `feat: add frame-based events and rename Matched.ctx to act_id` 856 + 857 + <!-- END_TASK_5 --> 858 + 859 + <!-- START_TASK_6 --> 860 + ### Task 6: Update emu/types.py for frame-based PEConfig 861 + 862 + **Verifies:** None directly (infrastructure for Phase 2, but validates pe-frame-redesign.AC3.1 field existence) 863 + 864 + **Files:** 865 + - Modify: `emu/types.py` (replace MatchEntry and legacy PEConfig fields with frame-based config) 866 + 867 + **Implementation:** 868 + 869 + Remove `MatchEntry` entirely from `emu/types.py`. Remove the legacy fields `ctx_slots`, `offsets`, and `gen_counters` from `PEConfig`. Remove the `ALUInst`/`SMInst` import — `iram` now only holds `Instruction`. 870 + 871 + ```python 872 + from cm_inst import FrameSlotValue, Instruction, Port 873 + from emu.events import EventCallback 874 + from sm_mod import Presence 875 + from tokens import CMToken 876 + 877 + 878 + @dataclass(frozen=True) 879 + class DeferredRead: 880 + cell_addr: int 881 + return_route: CMToken 882 + 883 + 884 + @dataclass(frozen=True) 885 + class PEConfig: 886 + pe_id: int 887 + iram: dict[int, Instruction] 888 + frame_count: int = 8 889 + frame_slots: int = 64 890 + matchable_offsets: int = 8 891 + initial_frames: Optional[dict[int, list[FrameSlotValue]]] = None 892 + initial_tag_store: Optional[dict[int, int]] = None 893 + allowed_pe_routes: Optional[set[int]] = None 894 + allowed_sm_routes: Optional[set[int]] = None 895 + on_event: EventCallback | None = None 896 + ``` 897 + 898 + Key changes: 899 + - `MatchEntry` removed entirely 900 + - `iram` type is now `dict[int, Instruction]` only 901 + - Legacy fields `ctx_slots`, `offsets`, `gen_counters` removed 902 + - Added `frame_count` (default 8), `frame_slots` (default 64), `matchable_offsets` (default 8) 903 + - Added `initial_frames` (dict mapping frame_id to slot value lists) and `initial_tag_store` (dict mapping act_id to frame_id) 904 + 905 + `SMConfig` is unchanged in this task. 906 + 907 + **Testing:** 908 + 909 + Tests must verify: 910 + - `PEConfig` can be constructed with new fields (`frame_count`, `frame_slots`, `matchable_offsets`) 911 + - `PEConfig` defaults are correct (frame_count=8, frame_slots=64, matchable_offsets=8) 912 + - `PEConfig` accepts `Instruction` objects in `iram` 913 + - `initial_frames` and `initial_tag_store` can be set 914 + - `MatchEntry` does NOT exist in `emu/types.py` 915 + 916 + Test file: `tests/test_foundation_types.py` (append) 917 + 918 + **Verification:** 919 + Run: `python -m pytest tests/test_foundation_types.py -v` 920 + Expected: Foundation type tests pass 921 + 922 + **Commit:** `feat: replace MatchEntry and legacy PEConfig fields with frame-based config` 923 + 924 + <!-- END_TASK_6 --> 925 + <!-- END_SUBCOMPONENT_B --> 926 + 927 + <!-- START_TASK_7 --> 928 + ### Task 7: Record test breakage 929 + 930 + **Verifies:** None (breakage inventory) 931 + 932 + **Files:** 933 + - No file changes — verification only 934 + 935 + **Step 1: Run full test suite** 936 + 937 + Run: `python -m pytest tests/ -v` 938 + 939 + Expected: Many tests will fail due to removed types (`ALUInst`, `SMInst`, `IRAMWriteToken`, `MatchEntry`, `FREE_CTX`), renamed fields (`ctx` → `act_id`, `gen` removed), and restructured hierarchy. This is expected. 940 + 941 + **Step 2: Record what breaks** 942 + 943 + List all failing tests and the reason for each failure. This inventory will guide the per-phase fix work in Phases 2–8. Do NOT attempt to fix anything here. 944 + 945 + <!-- END_TASK_7 -->
+506
docs/implementation-plans/2026-03-06-pe-frame-redesign/phase_02.md
··· 1 + # PE Frame-Based Redesign — Phase 2: Emulator Core — PE Rewrite 2 + 3 + **Goal:** Rewrite `ProcessingElement` with frame-based matching, reversed pipeline order (IFETCH before MATCH), mode-driven output routing via `OutputStyle`, and PE-level EXTRACT_TAG handling. 4 + 5 + **Architecture:** Replace the `matching_store` 2D array + `gen_counters` with `frames` (list of frame slot arrays), `tag_store` (act_id → frame_id mapping), `presence` (per-frame dense match metadata indexed by match slot), `port_store` (per-frame port metadata), and `free_frames` (available frame IDs). Match operands are stored in the frame's low slots (0 to `matchable_offsets - 1`), indexed by the token's IRAM offset (`token.offset`). Constants and destinations are in higher frame slots, indexed by the instruction's `fref` field per the mode table. Pipeline becomes IFETCH → act_id resolution → MATCH/FRAME → EXECUTE → EMIT. Output routing is driven by `Instruction.output` (OutputStyle enum) rather than `_output_mode()` reading dest_l/dest_r from ALUInst. The PE only handles `Instruction` objects — legacy ALUInst/SMInst are gone. 6 + 7 + **Tech Stack:** Python 3.12, SimPy 4.1 8 + 9 + **Scope:** Phase 2 of 8 from the PE frame-based redesign design plan. 10 + 11 + **Codebase verified:** 2026-03-07 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### pe-frame-redesign.AC3: Frame-based PE matching and output routing 20 + - **pe-frame-redesign.AC3.1 Success:** PE constructor accepts `frame_count`, `frame_slots`, `matchable_offsets` as configurable parameters 21 + - **pe-frame-redesign.AC3.2 Success:** Pipeline order is IFETCH → act_id resolution → MATCH/FRAME → EXECUTE → EMIT 22 + - **pe-frame-redesign.AC3.3 Success:** Dyadic matching uses tag store + presence bits + frame SRAM (not matching_store array) 23 + - **pe-frame-redesign.AC3.4 Success:** INHERIT output reads `FrameDest` from frame slot and constructs token directly 24 + - **pe-frame-redesign.AC3.5 Success:** CHANGE_TAG output calls `unpack_flit1()` on left operand to get `FrameDest` 25 + - **pe-frame-redesign.AC3.6 Success:** SINK output writes result to `frames[frame_id][fref]`, emits no token 26 + - **pe-frame-redesign.AC3.7 Success:** EXTRACT_TAG handled PE-level (not ALU), produces packed flit 1 via `pack_flit1()` 27 + - **pe-frame-redesign.AC3.8 Success:** Frame allocation (ALLOC) and deallocation (FREE) via FrameControlToken work correctly 28 + - **pe-frame-redesign.AC3.9 Success:** PELocalWriteToken with `is_dest=True` decodes data to FrameDest on write 29 + - **pe-frame-redesign.AC3.10 Success:** Pipeline timing preserved: 5 cycles dyadic, 4 cycles monadic, 1 cycle side paths 30 + 31 + ### pe-frame-redesign.AC1: Token hierarchy correctly models frame-based architecture 32 + - **pe-frame-redesign.AC1.5 Success:** Network routing uses `isinstance(token, PEToken)` for all PE-bound tokens 33 + - **pe-frame-redesign.AC1.6 Failure:** Token with invalid `act_id` (no tag store mapping) is dropped with `TokenRejected` event 34 + 35 + --- 36 + 37 + <!-- START_SUBCOMPONENT_A (tasks 1-2) --> 38 + <!-- START_TASK_1 --> 39 + ### Task 1: Update emu/alu.py for opcode renames 40 + 41 + **Verifies:** None (infrastructure — prerequisite for PE rewrite) 42 + 43 + **Files:** 44 + - Modify: `emu/alu.py` (rename shift ops and FREE_CTX, add EXTRACT_TAG and ALLOC_REMOTE) 45 + 46 + **Implementation:** 47 + 48 + Phase 1 renumbers all opcodes and renames shifts (`SHIFT_L` → `SHL`, `SHIFT_R` → `SHR`, `ASHFT_R` → `ASR`). Shifts stay in `ArithOp`, so no structural change to ALU dispatch. Update `emu/alu.py`: 49 + 50 + 1. In `_execute_arith()`: rename match cases: 51 + ```python 52 + case ArithOp.SHL: # was SHIFT_L 53 + result = (left << const) & UINT16_MASK 54 + case ArithOp.SHR: # was SHIFT_R 55 + result = (left >> const) & UINT16_MASK 56 + case ArithOp.ASR: # was ASHFT_R 57 + signed = to_signed(left) 58 + result = (signed >> const) & UINT16_MASK 59 + ``` 60 + 61 + 2. In `_execute_routing()`: rename `FREE_CTX` → `FREE_FRAME`, add `EXTRACT_TAG` and `ALLOC_REMOTE` as no-op fallbacks (both handled at PE level): 62 + ```python 63 + case RoutingOp.FREE_FRAME: 64 + return 0, False 65 + case RoutingOp.EXTRACT_TAG: 66 + return 0, False 67 + case RoutingOp.ALLOC_REMOTE: 68 + return 0, False 69 + ``` 70 + 71 + **Verification:** 72 + Run: `python -m pytest tests/test_alu.py -v` 73 + Expected: All ALU tests pass 74 + 75 + **Commit:** `refactor: rename shift ops (SHL/SHR/ASR), FREE_CTX→FREE_FRAME, add EXTRACT_TAG and ALLOC_REMOTE` 76 + 77 + <!-- END_TASK_1 --> 78 + 79 + <!-- START_TASK_2 --> 80 + ### Task 2: Rewrite ProcessingElement in emu/pe.py 81 + 82 + **Verifies:** pe-frame-redesign.AC3.1, pe-frame-redesign.AC3.2, pe-frame-redesign.AC3.3, pe-frame-redesign.AC3.4, pe-frame-redesign.AC3.5, pe-frame-redesign.AC3.6, pe-frame-redesign.AC3.7, pe-frame-redesign.AC3.8, pe-frame-redesign.AC3.9, pe-frame-redesign.AC3.10, pe-frame-redesign.AC1.6 83 + 84 + **Files:** 85 + - Modify: `emu/pe.py` (full rewrite of ProcessingElement class) 86 + 87 + **Implementation:** 88 + 89 + This is a full rewrite. The new `ProcessingElement` replaces the matching_store/gen_counters model with frames/tag_store/presence. There is no legacy path — the PE only handles `Instruction` objects. 90 + 91 + **Constructor changes:** 92 + 93 + Replace the old constructor parameters and instance variables. The PE now accepts: 94 + - `frame_count` (from PEConfig, default 8) 95 + - `frame_slots` (from PEConfig, default 64) 96 + - `matchable_offsets` (from PEConfig, default 8) 97 + - `initial_frames` and `initial_tag_store` (optional, for pre-loaded state) 98 + 99 + New instance variables: 100 + ```python 101 + self.frames: list[list[FrameSlotValue]] # [frame_count][frame_slots] — all per-activation data 102 + self.tag_store: dict[int, int] # act_id → frame_id 103 + self.presence: list[list[bool]] # [frame_count][matchable_offsets] — match pending 104 + self.port_store: list[list[Port | None]] # [frame_count][matchable_offsets] — port of pending operand 105 + self.free_frames: list[int] # available frame IDs 106 + ``` 107 + 108 + Match metadata (`presence`, `port_store`) is a dense, small structure — one entry per matchable offset per frame, NOT one per frame slot. The match slot index is derived from the low bits of the token's IRAM offset: `match_slot = token.offset % matchable_offsets`. This allows dyadic instructions to be placed at ANY IRAM offset — the assembler just ensures no two simultaneously-live dyadic instructions in the same activation collide in their low-bit hash. Match operands are stored in `frames[frame_id][match_slot]`. `fref` is separate — it indexes into the frame for const/dest reads by the output formatter. 109 + 110 + Remove: `matching_store`, `gen_counters`, `_ctx_slots`, `_offsets` 111 + 112 + The `iram` type is `dict[int, Instruction]`. 113 + 114 + **Updated imports:** 115 + 116 + ```python 117 + import logging 118 + from typing import Optional 119 + 120 + import simpy 121 + 122 + from cm_inst import ( 123 + ALUOp, ArithOp, FrameDest, FrameOp, FrameSlotValue, 124 + Instruction, LogicOp, MemOp, OutputStyle, Port, RoutingOp, 125 + TokenKind, is_monadic_alu, 126 + ) 127 + from encoding import pack_flit1, unpack_flit1, unpack_instruction 128 + from emu.alu import execute 129 + from emu.events import ( 130 + Emitted, EventCallback, Executed, FrameAllocated, FrameFreed, 131 + FrameSlotWritten, IRAMWritten, Matched, TokenReceived, TokenRejected, 132 + ) 133 + from tokens import ( 134 + CMToken, DyadToken, FrameControlToken, 135 + MonadToken, PELocalWriteToken, PEToken, SMToken, 136 + ) 137 + ``` 138 + 139 + **Pipeline rewrite — `_process_token()`:** 140 + 141 + The new pipeline order is: 142 + 1. Side path handling (PELocalWriteToken, FrameControlToken) — 1 cycle each 143 + 2. For CMTokens (DyadToken, MonadToken): 144 + a. IFETCH — look up IRAM by `token.offset` (1 cycle) 145 + b. Act_id resolution — validate `token.act_id` against tag_store 146 + c. MATCH — for dyadic: presence-based matching on `frames[frame_id][token.offset % matchable_offsets]` 147 + d. EXECUTE — ALU execute or SM dispatch (1 cycle) 148 + e. EMIT — mode-driven output routing (1 cycle) 149 + 150 + **Side path: FrameControlToken handling:** 151 + 152 + ```python 153 + if isinstance(token, FrameControlToken): 154 + self._handle_frame_control(token) 155 + yield self.env.timeout(1) 156 + return 157 + ``` 158 + 159 + `_handle_frame_control`: 160 + - ALLOC: pop frame_id from `free_frames`, set `tag_store[token.act_id] = frame_id`, initialise frame slots to None, emit `FrameAllocated` event 161 + - FREE: look up frame_id from tag_store, remove tag_store entry, push frame_id back to `free_frames`, emit `FrameFreed` event. Frame slot data is NOT cleared (stale data overwritten on next ALLOC, matching hardware) 162 + 163 + **Side path: PELocalWriteToken handling:** 164 + 165 + ```python 166 + if isinstance(token, PELocalWriteToken): 167 + self._handle_local_write(token) 168 + yield self.env.timeout(1) 169 + return 170 + ``` 171 + 172 + `_handle_local_write`: 173 + - `region=0`: IRAM write — `self.iram[token.slot] = unpack_instruction(token.data)`, emit `IRAMWritten` event 174 + - `region=1`: Frame write — look up `frame_id = self.tag_store[token.act_id]`, write to `self.frames[frame_id][token.slot]`. If `token.is_dest` is True, decode `token.data` via `unpack_flit1()` and store the resulting `FrameDest`. Otherwise store `token.data` as int. Emit `FrameSlotWritten` event. 175 + 176 + **Main pipeline for CMTokens:** 177 + 178 + ```python 179 + # IFETCH (1 cycle) 180 + inst = self.iram.get(token.offset) 181 + yield self.env.timeout(1) 182 + if inst is None: 183 + logger.warning("PE %d: no instruction at offset %d", self.pe_id, token.offset) 184 + return 185 + 186 + # Act_id resolution 187 + if token.act_id not in self.tag_store: 188 + self._on_event(TokenRejected( 189 + time=self.env.now, component=self._component, 190 + token=token, reason=f"act_id {token.act_id} not in tag store", 191 + )) 192 + return 193 + frame_id = self.tag_store[token.act_id] 194 + ``` 195 + 196 + **Matching:** 197 + 198 + Determine monadic/dyadic from the token type: 199 + 200 + ```python 201 + if isinstance(token, MonadToken): 202 + left, right = token.data, None 203 + elif isinstance(token, DyadToken): 204 + if is_monadic_alu(inst.opcode) if isinstance(inst.opcode, ALUOp) else True: # all SM ops are monadic 205 + left, right = token.data, None 206 + else: 207 + # Dyadic matching via presence bits 208 + operands = self._match_frame(token, inst, frame_id) 209 + yield self.env.timeout(1) # match cycle 210 + if operands is None: 211 + return # waiting for partner 212 + left, right = operands 213 + else: 214 + return 215 + 216 + # EXECUTE 217 + if isinstance(inst.opcode, MemOp): 218 + yield self.env.timeout(1) 219 + self._build_and_emit_sm_new(inst, left, right, token.act_id, frame_id) 220 + yield self.env.timeout(1) 221 + elif inst.opcode == RoutingOp.EXTRACT_TAG: 222 + # PE-level: pack current PE/act_id/offset into flit 1 223 + yield self.env.timeout(1) 224 + result = pack_flit1(FrameDest( 225 + target_pe=self.pe_id, 226 + offset=token.offset, 227 + act_id=token.act_id, 228 + port=Port.L, 229 + token_kind=TokenKind.DYADIC, 230 + )) 231 + self._on_event(Executed( 232 + time=self.env.now, component=self._component, 233 + op=inst.opcode, result=result, bool_out=False, 234 + )) 235 + self._do_emit_new(inst, result, False, token.act_id, frame_id) 236 + yield self.env.timeout(1) 237 + elif inst.opcode == RoutingOp.ALLOC_REMOTE: 238 + # PE-level: read target PE and act_id from frame constants, construct 239 + # and deliver a FrameControlToken(ALLOC) to the target PE. 240 + # fref points to const/dest slots (mode table). ALLOC_REMOTE reads 241 + # target PE and act_id from frame constants at fref, fref+1. 242 + yield self.env.timeout(1) 243 + target_pe = self.frames[frame_id][inst.fref] # target PE id from frame constant 244 + target_act = self.frames[frame_id][inst.fref + 1] # target act_id from frame constant 245 + fct = FrameControlToken( 246 + target=target_pe, 247 + act_id=target_act, 248 + op=FrameOp.ALLOC, 249 + payload=0, 250 + ) 251 + self._on_event(Executed( 252 + time=self.env.now, component=self._component, 253 + op=inst.opcode, result=0, bool_out=False, 254 + )) 255 + self.env.process(self._deliver(self.route_table[target_pe], fct)) 256 + yield self.env.timeout(1) 257 + elif inst.opcode == RoutingOp.FREE_FRAME: 258 + # Deallocate frame: clear tag_store entry, return frame to free pool. 259 + # Frame slot data is NOT cleared — stale data is harmless (overwritten 260 + # on next ALLOC). This matches hardware behavior. 261 + yield self.env.timeout(1) 262 + result, bool_out = execute(inst.opcode, left, right, None) 263 + self._on_event(Executed(...)) 264 + if token.act_id in self.tag_store: 265 + freed_frame = self.tag_store.pop(token.act_id) 266 + self.free_frames.append(freed_frame) 267 + self._on_event(FrameFreed( 268 + time=self.env.now, component=self._component, 269 + act_id=token.act_id, frame_id=freed_frame, 270 + )) 271 + # No emit — FREE_FRAME suppresses output 272 + yield self.env.timeout(1) 273 + else: 274 + # Normal ALU execute 275 + # fref points to const/dest per mode table. For modes 1/3/5/7: const at fref. 276 + const_val = self.frames[frame_id][inst.fref] if inst.has_const else None 277 + result, bool_out = execute(inst.opcode, left, right, const_val) 278 + self._on_event(Executed( 279 + time=self.env.now, component=self._component, 280 + op=inst.opcode, result=result, bool_out=bool_out, 281 + )) 282 + yield self.env.timeout(1) 283 + self._do_emit_new(inst, result, bool_out, token.act_id, frame_id, left=left) 284 + yield self.env.timeout(1) 285 + ``` 286 + 287 + **Frame-based matching — `_match_frame()`:** 288 + 289 + Matching derives the match slot from the low bits of the token's IRAM offset: `match_slot = token.offset % matchable_offsets`. Dyadic instructions can be at any IRAM offset — the assembler ensures no two simultaneously-live dyadic instructions in the same activation collide in their match slot. `fref` is separate — it points to const/dest slots for the output formatter. 290 + 291 + Both L and R tokens for the same instruction write to `frames[frame_id][match_slot]`. The stored port metadata determines left/right ordering when the second token arrives. 292 + 293 + ```python 294 + def _match_frame(self, token: DyadToken, inst: Instruction, frame_id: int) -> tuple[int, int] | None: 295 + match_slot = token.offset % self._matchable_offsets # low bits of IRAM offset 296 + 297 + if self.presence[frame_id][match_slot]: 298 + # Partner already waiting — pair them 299 + partner_data = self.frames[frame_id][match_slot] 300 + partner_port = self.port_store[frame_id][match_slot] 301 + self.presence[frame_id][match_slot] = False 302 + self.frames[frame_id][match_slot] = None 303 + 304 + # Use port metadata to determine left/right ordering 305 + if partner_port == Port.L: 306 + left, right = partner_data, token.data 307 + else: 308 + left, right = token.data, partner_data 309 + 310 + self._on_event(Matched( 311 + time=self.env.now, component=self._component, 312 + left=left, right=right, act_id=token.act_id, 313 + offset=token.offset, frame_id=frame_id, 314 + )) 315 + return left, right 316 + else: 317 + # Store and wait for partner 318 + self.frames[frame_id][match_slot] = token.data 319 + self.port_store[frame_id][match_slot] = token.port 320 + self.presence[frame_id][match_slot] = True 321 + return None 322 + ``` 323 + 324 + **Mode-driven output routing — `_do_emit_new()`:** 325 + 326 + Note: `left` operand must be preserved through the execute stage and passed here for CHANGE_TAG routing. The PE's execute path must thread `left` alongside `result`. 327 + 328 + ```python 329 + def _do_emit_new(self, inst: Instruction, result: int, bool_out: bool, act_id: int, frame_id: int, left: int = 0): 330 + if isinstance(inst.opcode, RoutingOp) and inst.opcode == RoutingOp.GATE and not bool_out: 331 + return # GATE suppressed 332 + 333 + match inst.output: 334 + case OutputStyle.INHERIT: 335 + self._emit_inherit(inst, result, bool_out, frame_id) 336 + case OutputStyle.CHANGE_TAG: 337 + self._emit_change_tag(inst, result, left) 338 + case OutputStyle.SINK: 339 + self._emit_sink(inst, result, frame_id) 340 + ``` 341 + 342 + **INHERIT output — `_emit_inherit()`:** 343 + 344 + Read `FrameDest` from frame slots starting at `inst.fref`. The mode table defines the layout starting from fref: mode 0 = `[dest]`, mode 1 = `[const, dest]`, mode 2 = `[dest1, dest2]`, mode 3 = `[const, dest1, dest2]`. Match operands are in a separate region (indexed by token.offset, slots 0–7); fref points to const/dest slots only. 345 + 346 + ```python 347 + def _emit_inherit(self, inst: Instruction, result: int, bool_out: bool, frame_id: int): 348 + # fref is the base of [const?, dest1, dest2?] — see mode table. 349 + # If has_const, const is at fref and dests start at fref+1. 350 + # If no const, dests start at fref. 351 + dest_base = inst.fref + (1 if inst.has_const else 0) 352 + 353 + if inst.dest_count >= 1: 354 + dest_l: FrameDest = self.frames[frame_id][dest_base] 355 + out_token = self._make_token_from_dest(dest_l, result) 356 + self.output_log.append(out_token) 357 + self._on_event(Emitted(time=self.env.now, component=self._component, token=out_token)) 358 + self.env.process(self._deliver(self.route_table[dest_l.target_pe], out_token)) 359 + 360 + if inst.dest_count >= 2: 361 + dest_r: FrameDest = self.frames[frame_id][dest_base + 1] 362 + # For switch ops, route based on bool_out 363 + if isinstance(inst.opcode, RoutingOp) and inst.opcode in ( 364 + RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF, 365 + ): 366 + # Undo the dest_l append above — switch re-routes both outputs 367 + self.output_log.pop() 368 + if bool_out: 369 + taken, not_taken = dest_l, dest_r 370 + else: 371 + taken, not_taken = dest_r, dest_l 372 + data_tok = self._make_token_from_dest(taken, result) 373 + trig_tok = self._make_token_from_dest(not_taken, 0) 374 + self.output_log.append(data_tok) 375 + self.output_log.append(trig_tok) 376 + # ... events and delivery for both tokens 377 + else: 378 + out_r = self._make_token_from_dest(dest_r, result) 379 + self.output_log.append(out_r) 380 + self._on_event(Emitted(time=self.env.now, component=self._component, token=out_r)) 381 + self.env.process(self._deliver(self.route_table[dest_r.target_pe], out_r)) 382 + ``` 383 + 384 + **CHANGE_TAG output — `_emit_change_tag()`:** 385 + 386 + The left operand carries the packed flit 1 (destination descriptor from the caller's EXTRACT_TAG). The ALU result is the data payload to send. These are distinct values — `left` determines WHERE to send, `result` determines WHAT to send. 387 + 388 + ```python 389 + def _emit_change_tag(self, inst: Instruction, result: int, left: int): 390 + dest = unpack_flit1(left) 391 + out_token = self._make_token_from_dest(dest, result) 392 + self.output_log.append(out_token) 393 + self._on_event(Emitted(time=self.env.now, component=self._component, token=out_token)) 394 + self.env.process(self._deliver(self.route_table[dest.target_pe], out_token)) 395 + ``` 396 + 397 + **SM dispatch — `_build_and_emit_sm_new()`:** 398 + 399 + For SM instructions (where `isinstance(inst.opcode, MemOp)`), the PE constructs an SMToken. The return route is a FrameDest stored in the frame slot following the mode table layout at `inst.fref`. The `FrameDest.token_kind` determines whether the SM return token will be a DyadToken or MonadToken — this replaces the old `SMInst.ret_dyadic` field. 400 + 401 + ```python 402 + def _build_and_emit_sm_new(self, inst: Instruction, left: int, right: int, act_id: int, frame_id: int): 403 + # fref layout per mode table: [const?, dest]. For SM ops with return routes, 404 + # the dest slot holds the return FrameDest. 405 + ret_slot = inst.fref + (1 if inst.has_const else 0) 406 + ret_dest: FrameDest | None = self.frames[frame_id][ret_slot] if inst.dest_count > 0 else None 407 + 408 + # Build return CMToken from FrameDest if return route exists 409 + ret_token: CMToken | None = None 410 + if ret_dest is not None: 411 + ret_token = self._make_token_from_dest(ret_dest, 0) # data will be filled by SM 412 + 413 + # SM target (SM_id + address) can come from frame[fref] OR from 414 + # operand data, depending on the operation. The exact source mapping 415 + # is determined by the instruction's mode and opcode — the assembler's 416 + # codegen sets up frame slots and operand routing accordingly. 417 + # Flit 2 source (ALU out / R operand / frame slot) also varies by 418 + # operation — see pe-design.md flit 2 source mux table. 419 + # 420 + # For the emulator, we dispatch based on has_const: when the instruction 421 + # has a constant (frame[fref] holds SM params), target comes from frame. 422 + # Otherwise, target comes from the left operand. 423 + if inst.has_const: 424 + target_packed = self.frames[frame_id][inst.fref] 425 + else: 426 + target_packed = left 427 + sm_token = SMToken( 428 + target=(target_packed >> 8) & 0xFF, 429 + addr=target_packed & 0xFF, 430 + op=inst.opcode, 431 + flags=right if right is not None else None, 432 + data=right if inst.has_const else left, 433 + ret=ret_token, 434 + ) 435 + self.output_log.append(sm_token) 436 + self._on_event(Emitted(time=self.env.now, component=self._component, token=sm_token)) 437 + self.env.process(self._deliver(self.sm_routes[sm_token.target], sm_token)) 438 + ``` 439 + 440 + Note: SM target source (frame slot vs operand) depends on the operation and mode. The `has_const` dispatch above is a simplified emulator heuristic — the hardware uses the instruction decoder EEPROM to select the source mux. The return route's token kind comes from `FrameDest.token_kind` in the frame slot, not from a separate instruction field. 441 + 442 + **SINK output — `_emit_sink()`:** 443 + 444 + ```python 445 + def _emit_sink(self, inst: Instruction, result: int, frame_id: int): 446 + self.frames[frame_id][inst.fref] = result 447 + self._on_event(FrameSlotWritten( 448 + time=self.env.now, component=self._component, 449 + frame_id=frame_id, slot=inst.fref, value=result, 450 + )) 451 + # No output token emitted 452 + ``` 453 + 454 + **Token construction from FrameDest — `_make_token_from_dest()`:** 455 + 456 + ```python 457 + def _make_token_from_dest(self, dest: FrameDest, data: int) -> CMToken: 458 + match dest.token_kind: 459 + case TokenKind.DYADIC: 460 + return DyadToken( 461 + target=dest.target_pe, offset=dest.offset, 462 + act_id=dest.act_id, data=data, 463 + port=dest.port, wide=False, 464 + ) 465 + case TokenKind.MONADIC: 466 + return MonadToken( 467 + target=dest.target_pe, offset=dest.offset, 468 + act_id=dest.act_id, data=data, inline=False, 469 + ) 470 + case TokenKind.INLINE: 471 + return MonadToken( 472 + target=dest.target_pe, offset=dest.offset, 473 + act_id=dest.act_id, data=data, inline=True, 474 + ) 475 + ``` 476 + 477 + **Timing invariants:** 478 + - Dyadic CMToken: dequeue(1) + IFETCH(1) + MATCH(1) + EXECUTE(1) + EMIT(1) = 5 cycles 479 + - Monadic CMToken: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles 480 + - FrameControlToken / PELocalWriteToken: dequeue(1) + handle(1) = 2 cycles (1 cycle side path after dequeue) 481 + 482 + **Testing:** 483 + 484 + Tests must verify each AC: 485 + - pe-frame-redesign.AC3.1: Construct PE with `frame_count=4`, `frame_slots=32`, `matchable_offsets=4` and verify they're stored 486 + - pe-frame-redesign.AC3.2: Inject a DyadToken pair, capture events, verify event order is TokenReceived → Matched → Executed → Emitted (IFETCH is implicit before match) 487 + - pe-frame-redesign.AC3.3: Set up tag_store and frame with presence bits, inject two DyadTokens at same offset/act_id, verify they match via frame SRAM (not matching_store) 488 + - pe-frame-redesign.AC3.4: Set up frame with FrameDest in destination slot, inject tokens, verify output token has target/offset/act_id from the FrameDest 489 + - pe-frame-redesign.AC3.5: Set up CHANGE_TAG instruction, inject token whose left operand is a packed flit 1 value, verify output token destination matches unpacked FrameDest 490 + - pe-frame-redesign.AC3.6: Set up SINK instruction, inject tokens, verify result written to frame slot and no output token emitted 491 + - pe-frame-redesign.AC3.7: Set up EXTRACT_TAG instruction, inject MonadToken, verify result is packed flit 1 of (pe_id, offset, act_id, port, kind) 492 + - pe-frame-redesign.AC3.8: Inject FrameControlToken(ALLOC), verify tag_store updated and FrameAllocated event. Inject FrameControlToken(FREE), verify tag_store cleared and FrameFreed event. Also test FREE_FRAME opcode path: set up a FREE_FRAME instruction in IRAM, inject a MonadToken, verify tag_store cleared, frame returned to free_frames, FrameFreed event emitted, and no output token produced. Also test ALLOC_REMOTE: set up ALLOC_REMOTE instruction with target PE/act_id in frame slots, inject MonadToken, verify a FrameControlToken(ALLOC) is delivered to the target PE's input store. 493 + - pe-frame-redesign.AC3.9: Inject PELocalWriteToken(region=1, is_dest=True, data=pack_flit1(some_dest)), verify frame slot contains FrameDest object (not raw int) 494 + - pe-frame-redesign.AC3.10: Time dyadic pair processing, verify 5 cycles. Time monadic, verify 4 cycles. Time side paths, verify 1 cycle after dequeue. 495 + - pe-frame-redesign.AC1.6: Inject DyadToken with act_id not in tag_store, verify TokenRejected event and no crash 496 + 497 + Test file: `tests/test_pe_frames.py` (new file) 498 + 499 + **Verification:** 500 + Run: `python -m pytest tests/test_pe_frames.py -v` 501 + Expected: All new frame-based tests pass. Legacy PE tests (test_pe_events.py, test_cycle_timing.py) will break — they will be fixed in the cleanup/E2E phase. 502 + 503 + **Commit:** `feat: rewrite ProcessingElement with frame-based matching and output routing` 504 + 505 + <!-- END_TASK_2 --> 506 + <!-- END_SUBCOMPONENT_A -->
+301
docs/implementation-plans/2026-03-06-pe-frame-redesign/phase_03.md
··· 1 + # PE Frame-Based Redesign — Phase 3: Emulator Supporting — SM, Network, ALU 2 + 3 + **Goal:** Update SM for T0 raw int storage, network for PEToken routing, and wire everything together with new PEConfig fields. 4 + 5 + **Architecture:** SM's `t0_store` changes from `list[Token]` to `list[int]`. EXEC handler uses `flit_count()` + `unpack_token()` to reconstitute tokens from raw int sequences. Network routing changes from `isinstance(token, CMToken)` to `isinstance(token, PEToken)` to route all PE-bound tokens (CMToken, PELocalWriteToken, FrameControlToken). `build_topology()` passes new PEConfig frame parameters to ProcessingElement constructor. 6 + 7 + **Tech Stack:** Python 3.12, SimPy 4.1 8 + 9 + **Scope:** Phase 3 of 8 from the PE frame-based redesign design plan. 10 + 11 + **Codebase verified:** 2026-03-07 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### pe-frame-redesign.AC4: T0 raw storage and EXEC 20 + - **pe-frame-redesign.AC4.1 Success:** SM T0 stores `list[int]` (16-bit words), not Token objects 21 + - **pe-frame-redesign.AC4.2 Success:** EXEC reads consecutive ints, uses `flit_count()` for packet boundaries, reconstitutes tokens via `unpack_token()` 22 + - **pe-frame-redesign.AC4.3 Success:** `pack_token()` / `unpack_token()` round-trip for all token types 23 + - **pe-frame-redesign.AC4.4 Failure:** Malformed flit sequence in T0 (invalid prefix bits) is handled gracefully 24 + 25 + ### pe-frame-redesign.AC1: Token hierarchy correctly models frame-based architecture 26 + - **pe-frame-redesign.AC1.5 Success:** Network routing uses `isinstance(token, PEToken)` for all PE-bound tokens 27 + 28 + --- 29 + 30 + <!-- START_SUBCOMPONENT_A (tasks 1-2) --> 31 + <!-- START_TASK_1 --> 32 + ### Task 1: Update SM for T0 raw int storage and EXEC 33 + 34 + **Verifies:** pe-frame-redesign.AC4.1, pe-frame-redesign.AC4.2, pe-frame-redesign.AC4.4 35 + 36 + **Files:** 37 + - Modify: `emu/sm.py` (change t0_store type, rewrite EXEC handler, update T0 WRITE) 38 + 39 + **Implementation:** 40 + 41 + Changes to `emu/sm.py`: 42 + 43 + 1. Change `t0_store` type from `list[Token]` to `list[int]`: 44 + ```python 45 + self.t0_store: list[int] = [] 46 + ``` 47 + 48 + 2. Add import for encoding functions: 49 + ```python 50 + from encoding import flit_count, unpack_token 51 + ``` 52 + 53 + 3. Rewrite `_handle_exec()` to parse raw ints using flit boundaries: 54 + 55 + ```python 56 + def _handle_exec(self, addr: int): 57 + """EXEC: read raw int sequences from T0, reconstitute tokens, inject into network.""" 58 + if self.system is None: 59 + logger.warning("SM%d: EXEC but no system reference", self.sm_id) 60 + return 61 + t0_idx = addr - self.tier_boundary 62 + if t0_idx >= len(self.t0_store): 63 + return 64 + yield self.env.timeout(1) # process cycle 65 + 66 + pos = t0_idx 67 + while pos < len(self.t0_store): 68 + header = self.t0_store[pos] 69 + if header is None: 70 + break 71 + try: 72 + count = flit_count(header) 73 + except (ValueError, KeyError): 74 + logger.warning("SM%d: malformed flit at T0[%d], stopping EXEC", self.sm_id, pos) 75 + break 76 + if pos + count > len(self.t0_store): 77 + logger.warning("SM%d: truncated packet at T0[%d], stopping EXEC", self.sm_id, pos) 78 + break 79 + flits = self.t0_store[pos:pos + count] 80 + try: 81 + token = unpack_token(flits) 82 + except (ValueError, KeyError): 83 + logger.warning("SM%d: failed to unpack token at T0[%d], stopping EXEC", self.sm_id, pos) 84 + break 85 + yield from self.system.send(token) 86 + yield self.env.timeout(1) # per-token injection cycle 87 + pos += count 88 + ``` 89 + 90 + 4. `_handle_t0_write()` already stores `token.data` (an int) — no change needed for the write path. The t0_store type annotation changes but the actual stored values were already ints. 91 + 92 + 5. `_handle_t0_read()` — verify it returns int values (it does: `self.t0_store[t0_idx]`). No change needed. 93 + 94 + 6. Remove any isinstance(entry, Token) checks in the EXEC path — the new implementation processes raw ints via flit_count/unpack_token instead. 95 + 96 + **Testing:** 97 + 98 + Tests must verify: 99 + - pe-frame-redesign.AC4.1: After construction, `sm.t0_store` is `list[int]` type. After T0 WRITE, stored values are ints. 100 + - pe-frame-redesign.AC4.2: Pre-load t0_store with packed token flits (via `pack_token()`), trigger EXEC, verify reconstituted tokens arrive at target PE with correct field values. 101 + - pe-frame-redesign.AC4.4: Pre-load t0_store with invalid prefix bits (e.g., `[0xFFFF]`), trigger EXEC, verify it stops gracefully without crash and logs warning. 102 + - Test multiple consecutive packets in T0 — EXEC should parse each one by flit_count boundary. 103 + - Test truncated packet (flit_count says 3 but only 2 ints remain) — should stop gracefully. 104 + 105 + Test file: `tests/test_sm_t0_raw.py` (new file) 106 + 107 + **Verification:** 108 + Run: `python -m pytest tests/test_sm_t0_raw.py -v` 109 + Expected: All tests pass 110 + 111 + **Commit:** `feat: change SM T0 to raw int storage with flit-based EXEC parsing` 112 + 113 + <!-- END_TASK_1 --> 114 + 115 + <!-- START_TASK_2 --> 116 + ### Task 2: Update SM T0 tests for raw int storage 117 + 118 + **Verifies:** pe-frame-redesign.AC4.3 119 + 120 + **Files:** 121 + - Modify: `tests/test_sm_tiers.py` (update T0 tests for int storage instead of Token storage) 122 + - Modify: `tests/test_exec_bootstrap.py` (update EXEC tests to pre-load packed flits) 123 + 124 + **Implementation:** 125 + 126 + Existing T0 tests that pre-load `t0_store` with Token objects need updating to pre-load with packed int flits instead. 127 + 128 + In `tests/test_sm_tiers.py`: 129 + - Any test that does `sm.t0_store.append(some_token)` or `t0_store.append(some_token)` must change to `t0_store.extend(pack_token(some_token))` 130 + - Import `pack_token` from `encoding` 131 + - T0 READ tests: stored values are now ints, verify int retrieval 132 + 133 + In `tests/test_exec_bootstrap.py`: 134 + - Tests that pre-load T0 with Token objects for EXEC to read must change to `t0_store.extend(pack_token(token))` for each token 135 + - Verify EXEC reconstitutes the same tokens (round-trip via pack_token -> t0_store -> flit_count -> unpack_token) 136 + - pe-frame-redesign.AC4.3: pack_token/unpack_token round-trip is implicitly tested through EXEC end-to-end 137 + 138 + **Verification:** 139 + Run: `python -m pytest tests/test_sm_tiers.py tests/test_exec_bootstrap.py tests/test_sm_t0_raw.py -v` 140 + Expected: `test_sm_t0_raw.py` and the updated T0 tests in `test_sm_tiers.py` and `test_exec_bootstrap.py` pass. Other tests in these files that depend on types removed in Phase 1 (e.g. `IRAMWriteToken`, `DyadToken.gen`, `MatchEntry`) may fail — that is expected and will be addressed in Phase 8. 141 + 142 + **Commit:** `refactor: update T0 tests for raw int storage with packed flits` 143 + 144 + <!-- END_TASK_2 --> 145 + <!-- END_SUBCOMPONENT_A --> 146 + 147 + <!-- START_SUBCOMPONENT_B (tasks 3-4) --> 148 + <!-- START_TASK_3 --> 149 + ### Task 3: Update network routing for PEToken 150 + 151 + **Verifies:** pe-frame-redesign.AC1.5 152 + 153 + **Files:** 154 + - Modify: `emu/network.py` (update _target_store routing, update build_topology for new PEConfig fields) 155 + 156 + **Implementation:** 157 + 158 + 1. Update `_target_store()` in System class to route on `PEToken` instead of `CMToken`: 159 + 160 + ```python 161 + from tokens import PEToken, SMToken 162 + 163 + def _target_store(self, token: Token) -> simpy.Store: 164 + if isinstance(token, SMToken): 165 + return self.sms[token.target].input_store 166 + if isinstance(token, PEToken): 167 + return self.pes[token.target].input_store 168 + raise TypeError(f"Unknown token type: {type(token).__name__}") 169 + ``` 170 + 171 + This ensures `CMToken`, `PELocalWriteToken`, and `FrameControlToken` all route to the target PE's input store. 172 + 173 + 2. Update `build_topology()` PE construction to pass new PEConfig fields. The PE constructor only accepts frame-based parameters — do not pass `ctx_slots`, `offsets`, or `gen_counters`, which no longer exist: 174 + 175 + ```python 176 + for cfg in pe_configs: 177 + pe = ProcessingElement( 178 + env=env, 179 + pe_id=cfg.pe_id, 180 + iram=cfg.iram, 181 + frame_count=cfg.frame_count, 182 + frame_slots=cfg.frame_slots, 183 + matchable_offsets=cfg.matchable_offsets, 184 + fifo_capacity=fifo_capacity, 185 + on_event=cfg.on_event, 186 + ) 187 + # Load initial frames if provided 188 + if cfg.initial_frames is not None: 189 + for frame_id, slot_values in cfg.initial_frames.items(): 190 + pe.frames[frame_id] = list(slot_values) 191 + if cfg.initial_tag_store is not None: 192 + pe.tag_store.update(cfg.initial_tag_store) 193 + # Mark these frames as allocated (remove from free_frames) 194 + for frame_id in cfg.initial_tag_store.values(): 195 + if frame_id in pe.free_frames: 196 + pe.free_frames.remove(frame_id) 197 + pes[cfg.pe_id] = pe 198 + ``` 199 + 200 + 3. Update `t0_store` type annotation: 201 + ```python 202 + t0_store: list[int] = [] 203 + ``` 204 + 205 + 4. Update `inject()` method — check `PEToken` instead of `CMToken`: 206 + ```python 207 + def inject(self, token: Token): 208 + if isinstance(token, SMToken): 209 + store = self.sms[token.target].input_store 210 + elif isinstance(token, PEToken): 211 + store = self.pes[token.target].input_store 212 + else: 213 + raise TypeError(f"Unknown token type: {type(token).__name__}") 214 + store.items.append(token) 215 + ``` 216 + 217 + **Testing:** 218 + 219 + Tests must verify: 220 + - pe-frame-redesign.AC1.5: Inject `PELocalWriteToken(target=0, ...)`, verify it arrives at PE 0's input_store. Inject `FrameControlToken(target=0, ...)`, verify it arrives at PE 0's input_store. Inject `DyadToken(target=0, ...)`, verify it arrives at PE 0's input_store. Inject `SMToken(target=0, ...)`, verify it arrives at SM 0's input_store. 221 + - `build_topology` constructs PE with frame_count, frame_slots, matchable_offsets from config 222 + - `build_topology` loads initial_frames and initial_tag_store into PE 223 + 224 + Test file: `tests/test_network_routing.py` (new file, or append to test_network_events.py) 225 + 226 + **Verification:** 227 + Run: `python -m pytest tests/test_network_routing.py -v` 228 + Expected: All tests pass 229 + 230 + **Commit:** `feat: update network routing to PEToken and pass frame config to PE` 231 + 232 + <!-- END_TASK_3 --> 233 + 234 + <!-- START_TASK_4 --> 235 + ### Task 4: Update emu/__init__.py exports 236 + 237 + **Verifies:** None (infrastructure) 238 + 239 + **Files:** 240 + - Modify: `emu/__init__.py` (add new event types to exports) 241 + 242 + **Implementation:** 243 + 244 + Add the new event types to the exports: 245 + 246 + ```python 247 + from emu.events import ( 248 + CellWritten, 249 + DeferredRead, 250 + DeferredSatisfied, 251 + Emitted, 252 + EventCallback, 253 + Executed, 254 + FrameAllocated, 255 + FrameFreed, 256 + FrameSlotWritten, 257 + IRAMWritten, 258 + Matched, 259 + ResultSent, 260 + SimEvent, 261 + TokenReceived, 262 + TokenRejected, 263 + ) 264 + from emu.network import System, build_topology 265 + from emu.types import PEConfig, SMConfig 266 + ``` 267 + 268 + **Verification:** 269 + Run: `python -c "from emu import FrameAllocated, FrameFreed, FrameSlotWritten, TokenRejected; print('OK')"` 270 + Expected: Prints "OK" 271 + 272 + **Commit:** `feat: export new frame-based event types from emu package` 273 + 274 + <!-- END_TASK_4 --> 275 + <!-- END_SUBCOMPONENT_B --> 276 + 277 + <!-- START_TASK_5 --> 278 + ### Task 5: Run phase 2-3 emulator tests 279 + 280 + **Verifies:** None (regression check for new work only) 281 + 282 + **Files:** 283 + - None (test-only verification step) 284 + 285 + **Implementation:** 286 + 287 + Run only the test files that were created or updated in phases 2 and 3: 288 + 289 + ``` 290 + python -m pytest tests/test_sm_t0_raw.py tests/test_network_routing.py tests/test_pe_frames.py -v 291 + ``` 292 + 293 + Legacy tests (`test_pe_events.py`, `test_sm_events.py`, `test_sm_tiers.py`, `test_exec_bootstrap.py`, `test_cycle_timing.py`, `test_network_events.py`, etc.) reference removed types — `ALUInst`, `SMInst`, `MatchEntry`, `IRAMWriteToken`, `DyadToken.gen`, and similar — and will fail. That is expected. These tests will be addressed in Phase 8 when the old test suite is updated to match the new architecture. 294 + 295 + **Verification:** 296 + Run: `python -m pytest tests/test_sm_t0_raw.py tests/test_network_routing.py tests/test_pe_frames.py -v` 297 + Expected: All tests in these three files pass. Failures in any other test file are out of scope for this phase. 298 + 299 + **Commit:** None (verification step only) 300 + 301 + <!-- END_TASK_5 -->
+388
docs/implementation-plans/2026-03-06-pe-frame-redesign/phase_04.md
··· 1 + # PE Frame-Based Redesign — Phase 4: Assembler Foundation — IR and Upstream Passes 2 + 3 + **Goal:** Update IR types and upstream assembler passes (lower, expand, place, opcodes) for the frame model. 4 + 5 + **Architecture:** Rename context-slot terminology to activation terminology throughout the assembler IR. Add frame-related fields to IRNode and SystemConfig. Update placement to use frame_count instead of ctx_slots and change IRAM cost to 1 for all node types (frames handle matching separately). Add EXTRACT_TAG and FREE_FRAME to opcode maps. 6 + 7 + **Tech Stack:** Python 3.12 8 + 9 + **Scope:** Phase 4 of 8 from the PE frame-based redesign design plan. 10 + 11 + **Codebase verified:** 2026-03-07 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase does not directly implement any AC test cases — it is infrastructure for Phases 5-6 which cover AC5 and AC6. However, the changes here enable: 18 + 19 + ### pe-frame-redesign.AC5: Assembler allocate produces frame layouts 20 + - **pe-frame-redesign.AC5.3 Success:** Activation IDs assigned sequentially (0-7), at most `frame_count` concurrent per PE 21 + - **pe-frame-redesign.AC5.8 Edge:** Matchable offset exceedance warns but does not error 22 + 23 + --- 24 + 25 + <!-- START_TASK_1 --> 26 + ### Task 1: Update asm/ir.py for frame model 27 + 28 + **Verifies:** None (infrastructure) 29 + 30 + **Files:** 31 + - Modify: `asm/ir.py` (rename ctx fields, add frame fields, update SystemConfig) 32 + 33 + **Implementation:** 34 + 35 + 1. Rename `IRNode.ctx` → `IRNode.act_id`: 36 + - Line 88: `ctx: Optional[int] = None` → `act_id: Optional[int] = None` 37 + 38 + 2. Rename `IRNode.ctx_slot` → `IRNode.act_slot`: 39 + - Line 86: `ctx_slot: Optional[Union[int, CtxSlotRef, CtxSlotRange]] = None` → `act_slot: Optional[Union[int, ActSlotRef, ActSlotRange]] = None` 40 + 41 + 3. Add new fields to IRNode (after `act_id`): 42 + ```python 43 + mode: Optional[tuple] = None # (OutputStyle, has_const, dest_count) — set by allocate 44 + fref: Optional[int] = None # frame slot base index — set by allocate 45 + wide: bool = False # wide operation flag 46 + frame_layout: Optional[FrameLayout] = None # frame slot map — set by allocate 47 + ``` 48 + 49 + 4. Rename type aliases directly — no backward-compat aliases: 50 + - `CtxSlotRef` → `ActSlotRef` 51 + - `CtxSlotRange` → `ActSlotRange` 52 + 53 + 5. Add new types: 54 + ```python 55 + @dataclass(frozen=True) 56 + class FrameSlotMap: 57 + match_slots: tuple[int, ...] # offsets of match operand slots 58 + const_slots: tuple[int, ...] # offsets of constant slots 59 + dest_slots: tuple[int, ...] # offsets of destination slots 60 + sink_slots: tuple[int, ...] # offsets of sink/SM param slots 61 + 62 + @dataclass(frozen=True) 63 + class FrameLayout: 64 + slot_map: FrameSlotMap 65 + total_slots: int 66 + ``` 67 + 68 + 6. Update SystemConfig: 69 + - Remove `ctx_slots` field entirely 70 + - Add `frame_count: int = 8` 71 + - Add `frame_slots: int = 64` 72 + - Add `matchable_offsets: int = 8` 73 + - Bump `iram_capacity` default from 128 to 256 74 + - Remove `DEFAULT_CTX_SLOTS` entirely 75 + 76 + ```python 77 + DEFAULT_IRAM_CAPACITY = 256 78 + DEFAULT_FRAME_COUNT = 8 79 + DEFAULT_FRAME_SLOTS = 64 80 + DEFAULT_MATCHABLE_OFFSETS = 8 81 + 82 + @dataclass(frozen=True) 83 + class SystemConfig: 84 + pe_count: int 85 + sm_count: int 86 + iram_capacity: int = DEFAULT_IRAM_CAPACITY 87 + frame_count: int = DEFAULT_FRAME_COUNT 88 + frame_slots: int = DEFAULT_FRAME_SLOTS 89 + matchable_offsets: int = DEFAULT_MATCHABLE_OFFSETS 90 + loc: SourceLoc = SourceLoc(0, 0) 91 + ``` 92 + 93 + **Testing:** 94 + 95 + Tests must verify: 96 + - `IRNode` has `act_id` field (not `ctx`) 97 + - `IRNode` has `act_slot` field (not `ctx_slot`) 98 + - `IRNode` has `mode`, `fref`, `wide`, `frame_layout` fields 99 + - `SystemConfig` has `frame_count`, `frame_slots`, `matchable_offsets` (not `ctx_slots`) 100 + - `SystemConfig` defaults are correct 101 + - `ActSlotRef` and `ActSlotRange` exist 102 + - `FrameSlotMap` and `FrameLayout` are frozen dataclasses 103 + 104 + Test file: `tests/test_ir_frame_types.py` (new file) 105 + 106 + **Verification:** 107 + Run: `python -m pytest tests/test_ir_frame_types.py -v` 108 + Expected: All tests pass 109 + 110 + **Commit:** `feat: update IR types for frame model (act_id, frame_count, FrameLayout)` 111 + 112 + <!-- END_TASK_1 --> 113 + 114 + <!-- START_TASK_2 --> 115 + ### Task 2: Update asm/opcodes.py 116 + 117 + **Verifies:** None (infrastructure) 118 + 119 + **Files:** 120 + - Modify: `asm/opcodes.py` (add free_frame and extract_tag mnemonics) 121 + 122 + **Implementation:** 123 + 124 + Phase 1 renumbered all ALUOp enum values to match 5-bit hardware encoding and renamed/moved shift operations. Update opcodes.py to match: 125 + 126 + 1. Rename shift mnemonics to match new enum names: 127 + ```python 128 + "shl": ArithOp.SHL, # was "shiftl": ArithOp.SHIFT_L 129 + "shr": ArithOp.SHR, # was "shiftr": ArithOp.SHIFT_R 130 + "asr": ArithOp.ASR, # was "ashiftr": ArithOp.ASHFT_R 131 + ``` 132 + 133 + 2. Replace `free_ctx` with `free_frame` in the mnemonic map: 134 + ```python 135 + "free_frame": RoutingOp.FREE_FRAME, 136 + ``` 137 + 138 + 3. Add new mnemonics: 139 + ```python 140 + "extract_tag": RoutingOp.EXTRACT_TAG, 141 + "alloc_remote": RoutingOp.ALLOC_REMOTE, 142 + ``` 143 + 144 + 4. Add `RoutingOp.EXTRACT_TAG` and `RoutingOp.ALLOC_REMOTE` to the MONADIC_OPS set. 145 + 146 + 5. Update the `_ALL_MONADIC_VALS` set — shift ops renamed: 147 + ```python 148 + (ArithOp, int(ArithOp.SHL)), 149 + (ArithOp, int(ArithOp.SHR)), 150 + (ArithOp, int(ArithOp.ASR)), 151 + ``` 152 + 153 + 6. Update `dfasm.lark` grammar: 154 + - Rename shift mnemonics: `"shiftl"` → `"shl"`, `"shiftr"` → `"shr"`, `"ashiftr"` → `"asr"` 155 + - Replace `"free_ctx"` with `"free_frame"` 156 + - Remove `"change_tag"` (CHANGE_TAG is an OutputStyle, not an opcode) 157 + - Add new opcodes: `| "extract_tag" | "alloc_remote"` 158 + 159 + 7. Import updated enum classes (`FREE_FRAME`/`EXTRACT_TAG`/`ALLOC_REMOTE` in `RoutingOp`). 160 + 161 + **Verification:** 162 + Run: `python -m pytest tests/test_opcodes.py -v` 163 + Expected: All tests pass 164 + 165 + **Commit:** `feat: update opcode map for 5-bit encoding, shift renames, and new routing ops` 166 + 167 + <!-- END_TASK_2 --> 168 + 169 + <!-- START_TASK_3 --> 170 + ### Task 3: Update asm/errors.py 171 + 172 + **Verifies:** None (infrastructure) 173 + 174 + **Files:** 175 + - Modify: `asm/errors.py` (add FRAME error category) 176 + 177 + **Implementation:** 178 + 179 + Add `FRAME` to the `ErrorCategory` enum: 180 + ```python 181 + class ErrorCategory(Enum): 182 + PARSE = "parse" 183 + NAME = "name" 184 + SCOPE = "scope" 185 + PLACEMENT = "placement" 186 + RESOURCE = "resource" 187 + ARITY = "arity" 188 + PORT = "port" 189 + UNREACHABLE = "unreachable" 190 + VALUE = "value" 191 + MACRO = "macro" 192 + CALL = "call" 193 + FRAME = "frame" 194 + ``` 195 + 196 + **Verification:** 197 + Run: `python -c "from asm.errors import ErrorCategory; print(ErrorCategory.FRAME)"` 198 + Expected: `ErrorCategory.FRAME` 199 + 200 + **Commit:** `feat: add FRAME error category` 201 + 202 + <!-- END_TASK_3 --> 203 + 204 + <!-- START_TASK_4 --> 205 + ### Task 4: Update asm/lower.py for act_slot rename 206 + 207 + **Verifies:** None (infrastructure) 208 + 209 + **Files:** 210 + - Modify: `asm/lower.py` (rename ctx_slot → act_slot in IRNode construction) 211 + 212 + **Implementation:** 213 + 214 + Mechanical rename throughout `asm/lower.py`: 215 + 216 + 1. All occurrences of `ctx_slot=` in IRNode constructor calls → `act_slot=` 217 + 2. All references to `qualified_ref_dict.get("ctx_slot")` → `qualified_ref_dict.get("act_slot")` 218 + 3. Variable names: `ctx_slot = ...` → `act_slot = ...` 219 + 4. All `CtxSlotRef` references → `ActSlotRef` and `CtxSlotRange` → `ActSlotRange` in imports 220 + 5. Also update the `@system` pragma parsing: `ctx_slots = config_dict.get("ctx", 4)` changes to `frame_count = config_dict.get("frames", 8)` 221 + 222 + Note on Lark transformer methods: if `dfasm.lark` has a `ctx_slot` grammar rule, the transformer method name must match. In that case, keep the method named `ctx_slot` (because Lark requires it) but update the internal variable names and the IRNode field assignment to use `act_slot`. This is not a backward-compat shim — it is a Lark dispatch requirement. If the grammar rule is renamed to `act_slot`, rename the method freely. 223 + 224 + **Verification:** 225 + Run: `python -m pytest tests/test_lower.py tests/test_parser.py -v` 226 + Expected: Tests that cover renamed fields pass; other tests may fail if they reference removed types. 227 + 228 + **Commit:** `refactor: rename ctx_slot to act_slot in lower pass` 229 + 230 + <!-- END_TASK_4 --> 231 + 232 + <!-- START_TASK_5 --> 233 + ### Task 5: Update asm/expand.py for FREE_FRAME and ActSlotRef 234 + 235 + **Verifies:** None (infrastructure) 236 + 237 + **Files:** 238 + - Modify: `asm/expand.py` (FREE_CTX → FREE_FRAME, free_ctx_nodes → free_frame_nodes, CtxSlotRef → ActSlotRef) 239 + - Modify: `asm/ir.py` (rename CallSite.free_ctx_nodes → free_frame_nodes if present) 240 + 241 + **Implementation:** 242 + 243 + 1. In call wiring (lines ~1153-1169): change `RoutingOp.FREE_CTX` → `RoutingOp.FREE_FRAME` in IRNode construction 244 + 2. Rename variable `free_ctx_name` → `free_frame_name` and `free_ctx_nodes` → `free_frame_nodes` 245 + 3. Update `CallSite` field reference: `free_ctx_nodes` → `free_frame_nodes` 246 + - If `CallSite` in `asm/ir.py` has a `free_ctx_nodes` field, rename it to `free_frame_nodes` directly — no alias. 247 + 4. All `CtxSlotRef` references → `ActSlotRef` (including imports) 248 + 5. All `ctx_slot` references in node field access → `act_slot` 249 + 250 + **Verification:** 251 + Run: `python -m pytest tests/test_expand.py tests/test_call_wiring.py -v` 252 + Expected: Tests that cover renamed fields pass; other tests may fail if they reference removed types. 253 + 254 + **Commit:** `refactor: rename FREE_CTX to FREE_FRAME and CtxSlotRef to ActSlotRef in expand pass` 255 + 256 + <!-- END_TASK_5 --> 257 + 258 + <!-- START_TASK_6 --> 259 + ### Task 6: Update asm/place.py for frame_count and IRAM cost 260 + 261 + **Verifies:** pe-frame-redesign.AC5.8 (matchable offset warning) 262 + 263 + **Files:** 264 + - Modify: `asm/place.py` (update _count_iram_cost, ctx_slots → frame_count, add matchable offset tracking) 265 + 266 + **Implementation:** 267 + 268 + 1. Change `_count_iram_cost()` to return 1 for all node types: 269 + ```python 270 + def _count_iram_cost(node: IRNode) -> int: 271 + """Count IRAM slots used by a node. 272 + 273 + In the frame model, all instructions use 1 IRAM slot. 274 + Matching is handled by frame SRAM, not IRAM entries. 275 + """ 276 + return 1 277 + ``` 278 + 279 + 2. Replace `ctx_slots` tracking with `frames_used` tracking: 280 + - `ctx_used` dict → `frames_used` dict 281 + - `ctx_scopes_per_pe` → `act_scopes_per_pe` 282 + - Validation: `frames_used[pe] <= system.frame_count` (instead of `ctx_used[pe] <= system.ctx_slots`) 283 + 284 + 3. Add matchable offset tracking per activation: 285 + - Track how many dyadic IRAM offsets each activation uses 286 + - If any activation exceeds `system.matchable_offsets`, emit a warning (not an error): 287 + ```python 288 + from asm.errors import AssemblyError, ErrorCategory 289 + # ... 290 + if dyadic_offsets_used > system.matchable_offsets: 291 + graph.errors.append(AssemblyError( 292 + message=f"Activation on PE {pe} uses {dyadic_offsets_used} matchable offsets " 293 + f"(limit: {system.matchable_offsets})", 294 + category=ErrorCategory.FRAME, 295 + severity="warning", 296 + loc=node.loc, 297 + )) 298 + ``` 299 + 300 + 4. Replace all `system.ctx_slots` references with `system.frame_count`. 301 + 302 + 5. Update `ctx_slot` field access on nodes to `act_slot`. 303 + 304 + **Testing:** 305 + 306 + Tests must verify: 307 + - pe-frame-redesign.AC5.8: When a PE has more dyadic offsets than `matchable_offsets`, a warning is emitted but placement succeeds 308 + - `_count_iram_cost()` returns 1 for both dyadic and monadic nodes 309 + - Placement respects `frame_count` limit (not `ctx_slots`) 310 + 311 + Test file: `tests/test_place.py` (update existing tests) and/or `tests/test_ir_frame_types.py` 312 + 313 + **Verification:** 314 + Run: `python -m pytest tests/test_place.py tests/test_autoplacement.py -v` 315 + Expected: Tests that cover frame_count and IRAM cost pass; other tests may fail. 316 + 317 + **Commit:** `feat: update placement for frame_count and uniform IRAM cost` 318 + 319 + <!-- END_TASK_6 --> 320 + 321 + <!-- START_TASK_7 --> 322 + ### Task 7: Update assembler test files for renames 323 + 324 + **Verifies:** None (regression) 325 + 326 + **Files:** 327 + - Modify: various test files in `tests/` that reference ctx, ctx_slot, ctx_slots, free_ctx 328 + 329 + **Implementation:** 330 + 331 + Mechanical renames across all assembler test files: 332 + 333 + 1. `node.ctx` → `node.act_id` 334 + 2. `node.ctx_slot` → `node.act_slot` 335 + 3. `system.ctx_slots` → `system.frame_count` (and update default values in test configs) 336 + 4. `CtxSlotRef` → `ActSlotRef`, `CtxSlotRange` → `ActSlotRange` 337 + 5. `free_ctx` references → `free_frame` 338 + 6. `FREE_CTX` → `FREE_FRAME` 339 + 7. Update any test that validates IRAM cost of 2 for dyadic → expect 1 340 + 341 + Known affected test files: 342 + - `tests/test_allocate.py` 343 + - `tests/test_place.py` 344 + - `tests/test_autoplacement.py` 345 + - `tests/test_expand.py` 346 + - `tests/test_call_wiring.py` 347 + - `tests/test_codegen.py` 348 + - `tests/test_lower.py` 349 + - `tests/test_resolve.py` 350 + - `tests/test_seed_const.py` 351 + - `tests/test_sm_graph_nodes.py` 352 + - `tests/test_macro_ir.py` 353 + - `tests/test_macro_ret_wiring.py` 354 + 355 + **Verification:** 356 + Run: `python -m pytest tests/test_ir_frame_types.py tests/test_opcodes.py -v` 357 + Expected: New IR type tests and opcode tests pass. Legacy test files may still fail — that is expected at this phase. 358 + 359 + **Commit:** `refactor: update assembler tests for ctx→act_id and frame_count renames` 360 + 361 + <!-- END_TASK_7 --> 362 + 363 + <!-- START_TASK_8 --> 364 + ### Task 8: Update asm/serialize.py for field renames 365 + 366 + **Verifies:** None (infrastructure) 367 + 368 + **Files:** 369 + - Modify: `asm/serialize.py` (rename ctx→act_id, ctx_slot→act_slot references) 370 + 371 + **Implementation:** 372 + 373 + The serializer reads IRNode fields and produces dfasm source text. Update it for: 374 + 375 + 1. Any reference to `node.ctx` → `node.act_id` 376 + 2. Any reference to `node.ctx_slot` → `node.act_slot` 377 + 3. Any reference to `CtxSlotRef` → `ActSlotRef`, `CtxSlotRange` → `ActSlotRange` 378 + 4. Handle new fields (`mode`, `fref`, `frame_layout`) — these are allocate-pass outputs and may not need serialization (the serializer operates on pre-allocate IR), but verify 379 + 380 + Also note: `asm/CLAUDE.md` and root `CLAUDE.md` reference removed types extensively (`ALUInst`, `SMInst`, `Addr`, `IRAMWriteToken`, `MatchEntry`, `ctx_slots`, `gen_counters`, `ctx_mode`, `ctx_override`). These documentation files will be updated in Phase 8 as part of final cleanup. 381 + 382 + **Verification:** 383 + Run: `python -c "from asm.serialize import serialize_graph; print('OK')"` 384 + Expected: Prints "OK" (module loads without import errors) 385 + 386 + **Commit:** `refactor: update serialize.py for ctx→act_id renames` 387 + 388 + <!-- END_TASK_8 -->
+483
docs/implementation-plans/2026-03-06-pe-frame-redesign/phase_05.md
··· 1 + # PE Frame-Based Redesign — Phase 5: Assembler Core — Allocate Rewrite 2 + 3 + **Goal:** Rewrite the allocate pass for frame layout allocation, IRAM offset assignment with instruction deduplication, activation ID assignment, and mode computation. 4 + 5 + **Architecture:** The allocate pass gains four new sub-functions: `_assign_iram_offsets()` (rewritten for deduplication), `_assign_act_ids()` (replaces `_assign_context_slots()`), `_compute_frame_layouts()` (new), and `_compute_modes()` (new). Destination resolution produces `FrameDest` objects instead of `Addr`. Frame layouts are canonical per function body — all activations of the same function share the layout, enabling instruction deduplication at the IRAM level. 6 + 7 + **Tech Stack:** Python 3.12 8 + 9 + **Scope:** Phase 5 of 8 from the PE frame-based redesign design plan. 10 + 11 + **Codebase verified:** 2026-03-07 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### pe-frame-redesign.AC5: Assembler allocate produces frame layouts 20 + - **pe-frame-redesign.AC5.1 Success:** IRAM offsets deduplicated — identical Instruction templates on same PE share entries 21 + - **pe-frame-redesign.AC5.2 Success:** Frame layouts canonical per function body — all activations of same function share layout 22 + - **pe-frame-redesign.AC5.3 Success:** Activation IDs assigned sequentially (0-7), at most `frame_count` concurrent per PE 23 + - **pe-frame-redesign.AC5.4 Success:** Frame slot assignment: match operands at 0 to matchable_offsets-1, then constants (deduped), then destinations (deduped), then sinks/SM params 24 + - **pe-frame-redesign.AC5.5 Success:** Mode (OutputStyle + has_const + dest_count) computed from edge topology and opcode 25 + - **pe-frame-redesign.AC5.6 Failure:** Frame slot overflow (> frame_slots) reports error 26 + - **pe-frame-redesign.AC5.7 Failure:** Act_id exhaustion (>8 needed) reports error 27 + 28 + --- 29 + 30 + <!-- START_SUBCOMPONENT_A (tasks 1-3) --> 31 + <!-- START_TASK_1 --> 32 + ### Task 1: Rewrite _assign_iram_offsets with deduplication 33 + 34 + **Verifies:** pe-frame-redesign.AC5.1 35 + 36 + **Files:** 37 + - Modify: `asm/allocate.py` (rewrite `_assign_iram_offsets()`) 38 + 39 + **Implementation:** 40 + 41 + The current `_assign_iram_offsets()` assigns sequential offsets with dyadic first, monadic after. The new version adds instruction template deduplication. 42 + 43 + Two instructions on the same PE share an IRAM entry when they produce identical `Instruction` objects — same opcode, output style, has_const, dest_count, wide, and fref. Since `fref` and mode aren't computed until `_compute_frame_layouts()` and `_compute_modes()`, deduplication at IRAM assignment time is based on the subset of fields available: opcode and arity (dyadic vs monadic). 44 + 45 + Actually, per the design, deduplication happens at the **template** level — after all fields are computed. This means IRAM offset assignment should be a two-phase process: 46 + 47 + **Phase A: Assign provisional offsets** (same algorithm as current — dyadic at low offsets, monadic above). 48 + 49 + **Phase B: Deduplicate** (after frame layouts and modes are computed — called later in the allocate pipeline, or combined into a single function that does both). 50 + 51 + For this task, restructure `_assign_iram_offsets()` to: 52 + 1. Still partition dyadic/monadic and assign sequential offsets 53 + 2. Return the offset assignments AND enough metadata for later deduplication 54 + 3. All instructions return IRAM cost of 1 (per Phase 4 change) 55 + 56 + The deduplication step proper happens after `_compute_modes()` and `_compute_frame_layouts()` produce the `fref` and mode fields. Add `_deduplicate_iram()` as a post-processing step: 57 + 58 + ```python 59 + def _deduplicate_iram( 60 + nodes_on_pe: dict[str, IRNode], 61 + pe_id: int, 62 + ) -> dict[str, IRNode]: 63 + """Deduplicate IRAM entries for nodes that produce identical Instruction templates. 64 + 65 + Two nodes share an IRAM offset when they have identical: 66 + opcode, output (OutputStyle), has_const, dest_count, wide, fref. 67 + """ 68 + template_to_offset: dict[tuple, int] = {} 69 + updated = {} 70 + 71 + for name, node in nodes_on_pe.items(): 72 + if node.seed or node.iram_offset is None: 73 + updated[name] = node 74 + continue 75 + 76 + # Build template key from the fields that make an Instruction 77 + mode = node.mode # (OutputStyle, has_const, dest_count) 78 + if mode is None: 79 + updated[name] = node 80 + continue 81 + 82 + template_key = ( 83 + node.opcode, 84 + mode[0], # OutputStyle 85 + mode[1], # has_const 86 + mode[2], # dest_count 87 + node.wide, 88 + node.fref, 89 + ) 90 + 91 + if template_key in template_to_offset: 92 + # Reuse existing offset 93 + updated[name] = replace(node, iram_offset=template_to_offset[template_key]) 94 + else: 95 + template_to_offset[template_key] = node.iram_offset 96 + updated[name] = node 97 + 98 + return updated 99 + ``` 100 + 101 + This is called near the end of allocate(), after modes and frame layouts are assigned. 102 + 103 + **Testing:** 104 + 105 + Tests must verify: 106 + - pe-frame-redesign.AC5.1: Two nodes on the same PE with identical opcode, mode, and fref share the same IRAM offset after deduplication 107 + - Nodes with different opcodes get different offsets 108 + - Nodes with same opcode but different modes get different offsets 109 + - Seed nodes are excluded from IRAM assignment 110 + 111 + Test file: `tests/test_allocate_frames.py` (new file) 112 + 113 + **Verification:** 114 + Run: `python -m pytest tests/test_allocate_frames.py -v` 115 + Expected: All tests pass 116 + 117 + **Commit:** `feat: rewrite IRAM offset assignment with template deduplication` 118 + 119 + <!-- END_TASK_1 --> 120 + 121 + <!-- START_TASK_2 --> 122 + ### Task 2: Add _assign_act_ids 123 + 124 + **Verifies:** pe-frame-redesign.AC5.3, pe-frame-redesign.AC5.7 125 + 126 + **Files:** 127 + - Modify: `asm/allocate.py` (add `_assign_act_ids()`, remove `_assign_context_slots()`) 128 + 129 + **Implementation:** 130 + 131 + Add `_assign_act_ids()` and remove `_assign_context_slots()`. The logic is similar but: 132 + - Uses 3-bit act_ids (0-7) instead of ctx_slots 133 + - Limit is `system.frame_count` (default 8) 134 + - Field set on IRNode is `act_id` 135 + - Uses the same scope extraction logic (`_extract_function_scope()`) 136 + - CallSite field reference: `free_frame_nodes` 137 + 138 + ```python 139 + def _assign_act_ids( 140 + nodes_on_pe: list[IRNode], 141 + all_nodes: dict[str, IRNode], 142 + frame_count: int, 143 + pe_id: int, 144 + call_sites: list[CallSite] | None = None, 145 + ) -> tuple[dict[str, IRNode], list[AssemblyError]]: 146 + """Assign activation IDs (0 to frame_count-1) per function scope per PE.""" 147 + ``` 148 + 149 + The algorithm is identical to the removed `_assign_context_slots()` except: 150 + - Replace `frame_count` in validation checks (was `ctx_slots`) 151 + - Replace `node.act_id` in field assignments (was `node.ctx`) 152 + - Replace `free_frame_nodes` in CallSite field access (was `free_ctx_nodes`) 153 + - Error message: "activation ID exhaustion" 154 + - Error category: `ErrorCategory.FRAME` 155 + 156 + **Testing:** 157 + 158 + Tests must verify: 159 + - pe-frame-redesign.AC5.3: Act_ids assigned 0, 1, 2, ... sequentially per function scope 160 + - pe-frame-redesign.AC5.7: When >frame_count scopes needed, error with `ErrorCategory.FRAME` 161 + - Single function scope gets act_id 0 162 + - Multiple functions on same PE get different act_ids 163 + - Same function across different PEs can reuse act_id 0 164 + 165 + Test file: `tests/test_allocate_frames.py` (append) 166 + 167 + **Verification:** 168 + Run: `python -m pytest tests/test_allocate_frames.py -v` 169 + Expected: All tests pass 170 + 171 + **Commit:** `feat: add _assign_act_ids, remove _assign_context_slots` 172 + 173 + <!-- END_TASK_2 --> 174 + 175 + <!-- START_TASK_3 --> 176 + ### Task 3: Add _compute_frame_layouts and _compute_modes 177 + 178 + **Verifies:** pe-frame-redesign.AC5.2, pe-frame-redesign.AC5.4, pe-frame-redesign.AC5.5, pe-frame-redesign.AC5.6 179 + 180 + **Files:** 181 + - Modify: `asm/allocate.py` (add two new functions) 182 + 183 + **Implementation:** 184 + 185 + **`_compute_modes()`** — derives OutputStyle, has_const, dest_count from edge topology and opcode: 186 + 187 + ```python 188 + def _compute_modes( 189 + nodes_on_pe: dict[str, IRNode], 190 + edges_by_source: dict[str, list[IREdge]], 191 + ) -> dict[str, IRNode]: 192 + """Compute (OutputStyle, has_const, dest_count) for each node from edge topology.""" 193 + updated = {} 194 + for name, node in nodes_on_pe.items(): 195 + if node.seed: 196 + updated[name] = node 197 + continue 198 + 199 + out_edges = edges_by_source.get(name, []) 200 + dest_count = len(out_edges) 201 + has_const = node.const is not None 202 + 203 + # Determine OutputStyle from opcode and edge topology 204 + if isinstance(node.opcode, MemOp): 205 + # SM instructions: mode depends on MemOp semantics 206 + # WRITE, CLEAR, FREE, SET_PAGE, WRITE_IMM → no return value → SINK 207 + # READ, RD_INC, RD_DEC, CMP_SW, RAW_READ, EXT → return value → INHERIT 208 + _sink_ops = {MemOp.WRITE, MemOp.CLEAR, MemOp.FREE, MemOp.SET_PAGE, MemOp.WRITE_IMM} 209 + if node.opcode in _sink_ops: 210 + output = OutputStyle.SINK 211 + dest_count = 0 212 + else: 213 + output = OutputStyle.INHERIT 214 + elif node.opcode == RoutingOp.FREE_FRAME: 215 + output = OutputStyle.SINK 216 + dest_count = 0 217 + elif node.opcode == RoutingOp.EXTRACT_TAG: 218 + output = OutputStyle.INHERIT 219 + else: 220 + # Check if any outgoing edge has ctx_override=True 221 + # ctx_override=True on an edge means the source node is a 222 + # cross-function return — its left operand carries a packed 223 + # flit 1 (from EXTRACT_TAG) that determines the destination. 224 + # In the frame model, this maps to OutputStyle.CHANGE_TAG. 225 + has_ctx_override = any(e.ctx_override for e in out_edges) 226 + output = OutputStyle.CHANGE_TAG if has_ctx_override else OutputStyle.INHERIT 227 + 228 + mode = (output, has_const, dest_count) 229 + updated[name] = replace(node, mode=mode) 230 + 231 + return updated 232 + ``` 233 + 234 + Note on `ctx_override` → `CHANGE_TAG` mapping: In the current codebase, `expand.py` sets `ctx_override=True` on edges that cross function boundaries (return paths from callee to caller). The old model packed the target context into the instruction's const field via `ctx_mode`. The new model uses `OutputStyle.CHANGE_TAG` instead — the PE reads a packed flit 1 from the left operand (produced by the caller's EXTRACT_TAG instruction) and uses it as the destination. The `IREdge.ctx_override` field should be renamed to `change_tag` in a later cleanup, but functionally it serves the same purpose: marking edges that need dynamic destination routing. 235 + 236 + **`_compute_frame_layouts()`** — per-activation frame slot assignment: 237 + 238 + ```python 239 + def _compute_frame_layouts( 240 + nodes_on_pe: dict[str, IRNode], 241 + edges_by_source: dict[str, list[IREdge]], 242 + edges_by_dest: dict[str, list[IREdge]], 243 + all_nodes: dict[str, IRNode], 244 + frame_slots: int, 245 + pe_id: int, 246 + ) -> tuple[dict[str, IRNode], list[AssemblyError]]: 247 + """Compute frame slot layouts per activation. 248 + 249 + Slot assignment order: 250 + 0 to matchable_offsets-1: match operands (one pair per dyadic instruction) 251 + then: constants (deduplicated by value) 252 + then: destinations (deduplicated by FrameDest identity) 253 + then: sinks and SM parameters 254 + 255 + All activations of the same function share the canonical layout. 256 + """ 257 + ``` 258 + 259 + Frame layout algorithm — match operands and const/dest are in **separate regions** of the frame: 260 + 261 + **Match region** (slots 0 to `matchable_offsets - 1`): 262 + - Dyadic instructions are placed at low IRAM offsets (0 to `matchable_offsets - 1`) by the assembler's IRAM offset assignment (Phase 4 / Task 1). 263 + - Match operands are stored at `frames[frame_id][token.offset]`, where `token.offset` = IRAM offset. 264 + - The match region is indexed by IRAM offset, NOT by `fref`. 265 + - Multiple dyadic instructions can share a match slot if the compiler proves their operands are never pending simultaneously (liveness-based allocation). 266 + - If the activation has more dyadic instructions than `matchable_offsets`, emit a warning (per AC5.8). 267 + 268 + **Const/dest region** (slots `matchable_offsets` and above): 269 + - Each instruction gets a contiguous `fref` group in this region, laid out per the mode table: 270 + - Mode 0: `[dest]` — 1 slot 271 + - Mode 1: `[const, dest]` — 2 slots 272 + - Mode 2: `[dest1, dest2]` — 2 slots 273 + - Mode 3: `[const, dest1, dest2]` — 3 slots 274 + - Mode 4: no frame slots 275 + - Mode 5: `[const]` — 1 slot 276 + - Mode 6: `[sink_target]` — 1 slot (write-back) 277 + - Mode 7: `[RMW_target]` — 1 slot (read-modify-write) 278 + - Constants are deduplicated by value — instructions sharing the same constant value can point to the same slot. 279 + - Destinations are deduplicated by FrameDest identity — instructions with the same target can share dest slots. 280 + 281 + Algorithm: 282 + 1. Group nodes by act_id (same act_id = same activation) 283 + 2. For each activation group: 284 + a. Count dyadic instructions → these occupy match slots 0 to N-1 (their IRAM offsets). Warn if N > `matchable_offsets`. 285 + b. Collect all constants → deduplicate by value → assign const/dest slots starting at `matchable_offsets`. 286 + c. Collect all destinations (from edges) → deduplicate by FrameDest identity → assign dest slots after constants. 287 + d. Collect sinks and SM params → assign remaining slots. 288 + e. Set `fref` on each node to point to the base of its const/dest group. 289 + 3. Build `FrameLayout` with `FrameSlotMap` 290 + 4. If total slots exceed `frame_slots`: report error with `ErrorCategory.FRAME` 291 + 292 + Each node gets: 293 + - `fref` = base of its const/dest group in the frame (NOT the match slot) 294 + - `frame_layout` = the shared `FrameLayout` for its activation 295 + 296 + **Testing:** 297 + 298 + Tests must verify: 299 + - pe-frame-redesign.AC5.2: Two activations of the same function (same set of instructions) produce identical FrameLayout objects 300 + - pe-frame-redesign.AC5.4: Match operands at slots 0 to matchable_offsets-1 (indexed by IRAM offset), const/dest groups at matchable_offsets and above (indexed by fref) 301 + - pe-frame-redesign.AC5.5: OutputStyle derived from edge count (0 edges → SINK, 1-2 edges → INHERIT), has_const from const field, dest_count from edge count 302 + - pe-frame-redesign.AC5.6: When total frame slots > frame_slots limit, error with `ErrorCategory.FRAME` 303 + - Constants with same value share a slot 304 + - Destinations with same FrameDest share a slot 305 + 306 + Test file: `tests/test_allocate_frames.py` (append) 307 + 308 + **Verification:** 309 + Run: `python -m pytest tests/test_allocate_frames.py -v` 310 + Expected: All tests pass 311 + 312 + **Commit:** `feat: add frame layout computation and mode derivation to allocate` 313 + 314 + <!-- END_TASK_3 --> 315 + <!-- END_SUBCOMPONENT_A --> 316 + 317 + <!-- START_TASK_4 --> 318 + ### Task 4: Rework _resolve_destinations for FrameDest 319 + 320 + **Verifies:** None directly (infrastructure for Phase 6 codegen) 321 + 322 + **Files:** 323 + - Modify: `asm/allocate.py` (update `_resolve_destinations()` to produce FrameDest) 324 + 325 + **Implementation:** 326 + 327 + The current `_resolve_destinations()` produces `Addr` objects. The new version produces `FrameDest` objects that include `target_pe`, `offset`, `act_id`, `port`, and `token_kind`. 328 + 329 + Token kind determination: 330 + - If destination node is dyadic: `TokenKind.DYADIC` 331 + - If destination node is monadic and inline: `TokenKind.INLINE` 332 + - Otherwise: `TokenKind.MONADIC` 333 + 334 + For now, determine token kind from the destination node's opcode and edge properties: 335 + - If the destination node's IRAM instruction expects a DyadToken (dyadic opcode): `TokenKind.DYADIC` 336 + - Otherwise: `TokenKind.MONADIC` 337 + 338 + Updated resolution for each edge: 339 + ```python 340 + dest_node = all_nodes[edge.dest] 341 + frame_dest = FrameDest( 342 + target_pe=dest_node.pe, 343 + offset=dest_node.iram_offset, 344 + act_id=dest_node.act_id, 345 + port=edge.port, 346 + token_kind=_determine_token_kind(dest_node), 347 + ) 348 + resolved = ResolvedDest(name=edge.dest, addr=None, frame_dest=frame_dest) 349 + ``` 350 + 351 + Note: `ResolvedDest` may need a `frame_dest` field added, or replaced entirely. Check `asm/ir.py` for the ResolvedDest definition and add `frame_dest: Optional[FrameDest] = None`. 352 + 353 + Also assign FrameDest objects to frame slots during this step — each destination gets stored in the activation's frame layout at the appropriate dest slot offset. 354 + 355 + **Verification:** 356 + Run: `python -m pytest tests/test_allocate_frames.py -v` 357 + Expected: All tests pass 358 + 359 + **Commit:** `feat: resolve destinations as FrameDest objects with token kind` 360 + 361 + <!-- END_TASK_4 --> 362 + 363 + <!-- START_TASK_5 --> 364 + ### Task 5: Wire new functions into allocate() main flow 365 + 366 + **Verifies:** All AC5 criteria (integration) 367 + 368 + **Files:** 369 + - Modify: `asm/allocate.py` (update `allocate()` to call new functions in order) 370 + 371 + **Implementation:** 372 + 373 + Update the `allocate()` main function to use the new sub-functions: 374 + 375 + ```python 376 + def allocate(graph: IRGraph) -> IRGraph: 377 + errors = list(graph.errors) 378 + system = graph.system 379 + 380 + if system is None: 381 + # ... existing error handling ... 382 + 383 + all_nodes, all_edges = collect_all_nodes_and_edges(graph) 384 + edges_by_source = _build_edge_index(all_edges) 385 + edges_by_dest = _build_edge_index_by_dest(all_edges) 386 + 387 + # Validate non-commutative ops (unchanged) 388 + errors.extend(_validate_noncommutative_const(all_nodes, edges_by_dest)) 389 + 390 + # Assign SM IDs (unchanged) 391 + sm_updated, sm_errors = _assign_sm_ids(all_nodes, system.sm_count) 392 + errors.extend(sm_errors) 393 + all_nodes.update(sm_updated) 394 + 395 + # Group nodes by PE 396 + nodes_by_pe = _group_nodes_by_pe(all_nodes) 397 + 398 + intermediate_nodes = {} 399 + for pe_id, nodes_on_pe in sorted(nodes_by_pe.items()): 400 + # 1. Assign IRAM offsets (provisional) 401 + iram_updated, iram_errors = _assign_iram_offsets( 402 + nodes_on_pe, all_nodes, system.iram_capacity, pe_id 403 + ) 404 + errors.extend(iram_errors) 405 + if iram_errors: continue 406 + 407 + # 2. Assign activation IDs 408 + act_updated, act_errors = _assign_act_ids( 409 + list(iram_updated.values()), all_nodes, 410 + system.frame_count, pe_id, 411 + call_sites=graph.call_sites, 412 + ) 413 + errors.extend(act_errors) 414 + if act_errors: continue 415 + 416 + # 3. Compute modes (OutputStyle, has_const, dest_count) 417 + mode_updated = _compute_modes(act_updated, edges_by_source) 418 + 419 + # 4. Compute frame layouts (assigns fref, frame_layout) 420 + frame_updated, frame_errors = _compute_frame_layouts( 421 + mode_updated, edges_by_source, edges_by_dest, 422 + all_nodes, system.frame_slots, pe_id, 423 + ) 424 + errors.extend(frame_errors) 425 + if frame_errors: continue 426 + 427 + # 5. Deduplicate IRAM entries 428 + deduped = _deduplicate_iram(frame_updated, pe_id) 429 + 430 + intermediate_nodes.update(deduped) 431 + 432 + # Resolve destinations (produces FrameDest) 433 + for pe_id in sorted(nodes_by_pe.keys()): 434 + nodes_on_this_pe = { 435 + name: node for name, node in intermediate_nodes.items() 436 + if node.pe == pe_id 437 + } 438 + if not nodes_on_this_pe: continue 439 + 440 + resolved, resolve_errors = _resolve_destinations( 441 + nodes_on_this_pe, intermediate_nodes, edges_by_source 442 + ) 443 + errors.extend(resolve_errors) 444 + intermediate_nodes.update(resolved) 445 + 446 + # Reconstruct graph 447 + result_graph = update_graph_nodes(graph, intermediate_nodes) 448 + return replace(result_graph, errors=errors) 449 + ``` 450 + 451 + **Verification:** 452 + Run: `python -m pytest tests/test_allocate_frames.py -v` 453 + Expected: All tests pass. `tests/test_allocate.py` may fail — that is expected and addressed in Task 6. 454 + 455 + **Commit:** `feat: wire frame-based allocation into allocate() main flow` 456 + 457 + <!-- END_TASK_5 --> 458 + 459 + <!-- START_TASK_6 --> 460 + ### Task 6: Update existing allocate tests 461 + 462 + **Verifies:** None (regression) 463 + 464 + **Files:** 465 + - Modify: `tests/test_allocate.py` (update for act_id, frame_count, new fields) 466 + 467 + **Implementation:** 468 + 469 + Update existing allocate tests: 470 + 1. `ctx` field access → `act_id` 471 + 2. `ctx_slots` in SystemConfig → `frame_count` 472 + 3. Tests that check context overflow → check act_id exhaustion 473 + 4. Tests that check IRAM cost of 2 for dyadic → expect 1 474 + 5. Add assertions for new fields: `mode`, `fref`, `frame_layout` 475 + 6. Update `_assign_context_slots` test calls → `_assign_act_ids` 476 + 477 + **Verification:** 478 + Run: `python -m pytest tests/test_allocate_frames.py -v` 479 + Expected: All tests pass. `tests/test_allocate.py` tests that have been updated should also pass; stale tests in that file may still fail until fully converted. 480 + 481 + **Commit:** `refactor: update allocate tests for frame-based allocation` 482 + 483 + <!-- END_TASK_6 -->
+358
docs/implementation-plans/2026-03-06-pe-frame-redesign/phase_06.md
··· 1 + # PE Frame-Based Redesign — Phase 6: Assembler Core — Codegen Rewrite 2 + 3 + **Goal:** Rewrite codegen to produce `Instruction` objects, frame setup token sequences, and updated `AssemblyResult`. 4 + 5 + **Architecture:** `_build_iram_for_pe()` produces `dict[int, Instruction]` from allocated IR. `generate_tokens()` produces an ordered token stream: SM init → IRAM writes (via `PELocalWriteToken`) → ALLOC (via `FrameControlToken`) → frame slot writes (via `PELocalWriteToken`) → seed tokens. `AssemblyResult` gains a `setup_tokens` field. The output style mechanism uses `OutputStyle` values derived from the allocate pass — no `ctx_mode` or `ctx_override` fields exist in the new model. 6 + 7 + **Tech Stack:** Python 3.12 8 + 9 + **Scope:** Phase 6 of 8 from the PE frame-based redesign design plan. 10 + 11 + **Codebase verified:** 2026-03-07 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### pe-frame-redesign.AC6: Assembler codegen produces frame setup tokens 20 + - **pe-frame-redesign.AC6.1 Success:** `AssemblyResult` includes `setup_tokens` field 21 + - **pe-frame-redesign.AC6.2 Success:** Token stream ordering: SM init → IRAM writes → ALLOC → frame slot writes → seed tokens 22 + - **pe-frame-redesign.AC6.3 Success:** IRAM write data uses `pack_instruction()` 23 + - **pe-frame-redesign.AC6.4 Success:** Destination frame slot writes use `pack_flit1()` with `is_dest=True` 24 + - **pe-frame-redesign.AC6.5 Success:** T0 bootstrap data uses `pack_token()` for packed flits 25 + - **pe-frame-redesign.AC6.6 Success:** Seed tokens use `act_id`, no `gen` field 26 + 27 + --- 28 + 29 + <!-- START_SUBCOMPONENT_A (tasks 1-3) --> 30 + <!-- START_TASK_1 --> 31 + ### Task 1: Rewrite _build_iram_for_pe for Instruction objects 32 + 33 + **Verifies:** pe-frame-redesign.AC6.3 (partial — produces Instruction objects that will be packed) 34 + 35 + **Files:** 36 + - Modify: `asm/codegen.py` (rewrite `_build_iram_for_pe()`) 37 + 38 + **Implementation:** 39 + 40 + Replace the existing implementation with `Instruction` construction: 41 + 42 + ```python 43 + from cm_inst import FrameDest, Instruction, OutputStyle, TokenKind 44 + 45 + def _build_iram_for_pe( 46 + nodes_on_pe: list[IRNode], 47 + all_nodes: dict[str, IRNode], 48 + all_edges: list[IREdge], 49 + ) -> dict[int, Instruction]: 50 + """Build IRAM entries as Instruction objects for a single PE.""" 51 + iram: dict[int, Instruction] = {} 52 + 53 + for node in nodes_on_pe: 54 + if node.seed or node.iram_offset is None: 55 + continue 56 + if node.mode is None: 57 + continue 58 + 59 + output_style, has_const, dest_count = node.mode 60 + 61 + inst = Instruction( 62 + opcode=node.opcode, 63 + output=output_style, 64 + has_const=has_const, 65 + dest_count=dest_count, 66 + wide=node.wide, 67 + fref=node.fref or 0, 68 + ) 69 + 70 + iram[node.iram_offset] = inst 71 + 72 + return iram 73 + ``` 74 + 75 + This is dramatically simpler than the old version because: 76 + - Unified `Instruction` type — no split by instruction kind 77 + - Output style comes from `OutputStyle` enum — no encoding tricks 78 + - No `Addr` construction — replaced by FrameDest in frame slots 79 + - Mode fields (`output`, `has_const`, `dest_count`) already computed by allocate pass 80 + 81 + **Testing:** 82 + 83 + Tests must verify: 84 + - Given IRNodes with mode and fref set, produces correct Instruction objects 85 + - Seed nodes excluded from IRAM 86 + - CM vs SM type derived from isinstance(opcode, MemOp) — no stored type field 87 + 88 + Test file: `tests/test_codegen_frames.py` (new file) 89 + 90 + **Verification:** 91 + Run: `python -m pytest tests/test_codegen_frames.py -v` 92 + Expected: All tests pass 93 + 94 + **Commit:** `feat: rewrite _build_iram_for_pe for Instruction objects` 95 + 96 + <!-- END_TASK_1 --> 97 + 98 + <!-- START_TASK_2 --> 99 + ### Task 2: Add frame setup token generation 100 + 101 + **Verifies:** pe-frame-redesign.AC6.1, pe-frame-redesign.AC6.2, pe-frame-redesign.AC6.3, pe-frame-redesign.AC6.4, pe-frame-redesign.AC6.5 102 + 103 + **Files:** 104 + - Modify: `asm/codegen.py` (add `_generate_setup_tokens()`, update AssemblyResult) 105 + 106 + **Implementation:** 107 + 108 + 1. Update `AssemblyResult` to include `setup_tokens`: 109 + ```python 110 + @dataclass(frozen=True) 111 + class AssemblyResult: 112 + pe_configs: list[PEConfig] 113 + sm_configs: list[SMConfig] 114 + seed_tokens: list[CMToken] 115 + setup_tokens: list[Token] # NEW — ordered setup token sequence 116 + ``` 117 + 118 + 2. Add `_generate_setup_tokens()`: 119 + 120 + ```python 121 + from encoding import pack_flit1, pack_instruction, pack_token 122 + from tokens import FrameControlToken, PELocalWriteToken 123 + 124 + def _generate_setup_tokens( 125 + pe_configs: list[PEConfig], 126 + sm_configs: list[SMConfig], 127 + nodes_by_pe: dict[int, list[IRNode]], 128 + all_nodes: dict[str, IRNode], 129 + all_edges: list[IREdge], 130 + data_defs: list[IRDataDef], 131 + ) -> list[Token]: 132 + """Generate the ordered setup token sequence for frame-based bootstrap. 133 + 134 + Order: SM init → IRAM writes → ALLOC → frame slot writes → (seed tokens added separately) 135 + """ 136 + tokens: list[Token] = [] 137 + 138 + # 1. SM init (WRITE ops to populate I-structure cells) 139 + for data_def in sorted(data_defs, key=lambda d: (d.sm_id, d.cell_addr)): 140 + tokens.append(SMToken( 141 + target=data_def.sm_id, 142 + addr=data_def.cell_addr, 143 + op=MemOp.WRITE, 144 + flags=None, 145 + data=data_def.value, 146 + ret=None, 147 + )) 148 + 149 + # 2. IRAM writes via PELocalWriteToken(region=0) 150 + for pe_cfg in pe_configs: 151 + for offset, inst in sorted(pe_cfg.iram.items()): 152 + tokens.append(PELocalWriteToken( 153 + target=pe_cfg.pe_id, 154 + act_id=0, # IRAM writes are activation-independent 155 + region=0, 156 + slot=offset, 157 + data=pack_instruction(inst), 158 + is_dest=False, 159 + )) 160 + 161 + # 3. ALLOC — one per activation per PE 162 + for pe_id, nodes in sorted(nodes_by_pe.items()): 163 + # Collect unique act_ids on this PE 164 + act_ids = sorted({n.act_id for n in nodes if n.act_id is not None and not n.seed}) 165 + for act_id in act_ids: 166 + tokens.append(FrameControlToken( 167 + target=pe_id, 168 + act_id=act_id, 169 + op=FrameOp.ALLOC, 170 + payload=0, 171 + )) 172 + 173 + # 4. Frame slot writes via PELocalWriteToken(region=1) 174 + for pe_id, nodes in sorted(nodes_by_pe.items()): 175 + act_ids = sorted({n.act_id for n in nodes if n.act_id is not None and not n.seed}) 176 + for act_id in act_ids: 177 + act_nodes = [n for n in nodes if n.act_id == act_id and not n.seed] 178 + if not act_nodes: 179 + continue 180 + # Get frame layout from first node (canonical per activation) 181 + layout = act_nodes[0].frame_layout 182 + if layout is None: 183 + continue 184 + 185 + # Write constants to frame slots 186 + for slot_idx in layout.slot_map.const_slots: 187 + # Find the const value for this slot 188 + const_val = _find_const_for_slot(act_nodes, slot_idx, layout) 189 + if const_val is not None: 190 + tokens.append(PELocalWriteToken( 191 + target=pe_id, 192 + act_id=act_id, 193 + region=1, 194 + slot=slot_idx, 195 + data=const_val & 0xFFFF, 196 + is_dest=False, 197 + )) 198 + 199 + # Write destinations to frame slots 200 + for slot_idx in layout.slot_map.dest_slots: 201 + dest = _find_dest_for_slot(act_nodes, slot_idx, layout, all_nodes, all_edges) 202 + if dest is not None: 203 + tokens.append(PELocalWriteToken( 204 + target=pe_id, 205 + act_id=act_id, 206 + region=1, 207 + slot=slot_idx, 208 + data=pack_flit1(dest), 209 + is_dest=True, # AC6.4: signals PE to decode as FrameDest 210 + )) 211 + 212 + return tokens 213 + ``` 214 + 215 + Helper functions `_find_const_for_slot()` and `_find_dest_for_slot()` need to map frame slot indices back to the source node/edge that populates them. The exact implementation depends on how `_compute_frame_layouts()` (Phase 5) stores the mapping. 216 + 217 + 3. For T0 bootstrap data (AC6.5): when writing tokens to T0 storage, use `pack_token()`: 218 + ```python 219 + # In generate_tokens() for T0 bootstrap: 220 + for token in bootstrap_tokens: 221 + packed_flits = pack_token(token) 222 + for i, flit in enumerate(packed_flits): 223 + tokens.append(SMToken( 224 + target=sm_id, 225 + addr=tier_boundary + t0_offset + i, 226 + op=MemOp.WRITE, 227 + flags=None, 228 + data=flit, 229 + ret=None, 230 + )) 231 + ``` 232 + 233 + **Testing:** 234 + 235 + Tests must verify: 236 + - pe-frame-redesign.AC6.1: `AssemblyResult` has `setup_tokens` field 237 + - pe-frame-redesign.AC6.2: Token ordering is SM init → PELocalWriteToken(region=0) → FrameControlToken(ALLOC) → PELocalWriteToken(region=1) → (seeds last) 238 + - pe-frame-redesign.AC6.3: IRAM write PELocalWriteTokens carry `pack_instruction()` data 239 + - pe-frame-redesign.AC6.4: Destination slot PELocalWriteTokens have `is_dest=True` and carry `pack_flit1()` data 240 + - pe-frame-redesign.AC6.5: T0 data stored as packed flits via `pack_token()` 241 + 242 + Test file: `tests/test_codegen_frames.py` (append) 243 + 244 + **Verification:** 245 + Run: `python -m pytest tests/test_codegen_frames.py -v` 246 + Expected: All tests pass 247 + 248 + **Commit:** `feat: add frame setup token generation and updated AssemblyResult` 249 + 250 + <!-- END_TASK_2 --> 251 + 252 + <!-- START_TASK_3 --> 253 + ### Task 3: Update seed token generation and generate_direct 254 + 255 + **Verifies:** pe-frame-redesign.AC6.6 256 + 257 + **Files:** 258 + - Modify: `asm/codegen.py` (update seed tokens for act_id, update generate_direct and generate_tokens) 259 + 260 + **Implementation:** 261 + 262 + 1. Update seed token construction to use `act_id` and drop `gen`: 263 + 264 + For DyadToken seeds: 265 + ```python 266 + DyadToken( 267 + target=dest_node.pe, 268 + offset=dest_node.iram_offset, 269 + act_id=dest_node.act_id, 270 + data=node.const, 271 + port=edge.port, 272 + wide=False, 273 + ) 274 + ``` 275 + 276 + For MonadToken seeds: 277 + ```python 278 + MonadToken( 279 + target=dest_node.pe, 280 + offset=dest_node.iram_offset, 281 + act_id=dest_node.act_id, 282 + data=node.const, 283 + inline=False, 284 + ) 285 + ``` 286 + 287 + 2. Update `generate_direct()`: 288 + - Use new `_build_iram_for_pe()` (produces `dict[int, Instruction]`) 289 + - Build PEConfig with new fields: 290 + ```python 291 + PEConfig( 292 + pe_id=pe_id, 293 + iram=iram, # dict[int, Instruction] 294 + frame_count=system.frame_count, 295 + frame_slots=system.frame_slots, 296 + matchable_offsets=system.matchable_offsets, 297 + initial_frames=initial_frames_for_pe, # from frame layout data 298 + initial_tag_store=tag_store_for_pe, # from act_id assignments 299 + allowed_pe_routes=pe_routes, 300 + allowed_sm_routes=sm_routes, 301 + ) 302 + ``` 303 + - Compute `initial_frames` from frame layout data: for each act_id on PE, build frame slot value list from constants and FrameDest values 304 + - Compute `initial_tag_store` from act_id → frame_id mapping (sequential: act_id 0 → frame 0, etc.) 305 + 306 + 3. Update `generate_tokens()`: 307 + - Use `PELocalWriteToken(region=0)` for IRAM writes 308 + - Add `FrameControlToken` ALLOC sequence 309 + - Add `PELocalWriteToken(region=1)` frame slot writes 310 + - Ordering: SM init → IRAM writes → ALLOC → frame slot writes → seed tokens 311 + - Call `_generate_setup_tokens()` and append seed_tokens 312 + 313 + **Testing:** 314 + 315 + Tests must verify: 316 + - pe-frame-redesign.AC6.6: Seed tokens use `act_id` field (not `ctx`), and DyadToken has no `gen` field 317 + - `generate_direct()` produces PEConfigs with `Instruction` IRAM and frame data 318 + - `generate_tokens()` ordering: SM init → IRAM → ALLOC → frame slots → seeds 319 + 320 + Test file: `tests/test_codegen_frames.py` (append) 321 + 322 + **Verification:** 323 + Run: `python -m pytest tests/test_codegen_frames.py -v` 324 + Expected: All tests pass 325 + 326 + **Commit:** `feat: update seed tokens for act_id and wire generate_direct/generate_tokens` 327 + 328 + <!-- END_TASK_3 --> 329 + <!-- END_SUBCOMPONENT_A --> 330 + 331 + <!-- START_TASK_4 --> 332 + ### Task 4: Update asm/__init__.py and existing codegen tests 333 + 334 + **Verifies:** None (regression + infrastructure) 335 + 336 + **Files:** 337 + - Modify: `asm/__init__.py` (update exports if needed) 338 + - Modify: `tests/test_codegen.py` (update for new types and field names) 339 + 340 + **Implementation:** 341 + 342 + 1. `asm/__init__.py`: Ensure `AssemblyResult` with `setup_tokens` is properly exported. The `__all__` list already includes `"AssemblyResult"`. 343 + 344 + 2. Update `tests/test_codegen.py`: 345 + - Replace `ctx=` with `act_id=` in all token assertions 346 + - Remove `gen=` from DyadToken assertions 347 + - Update `PEConfig` assertions: check for `frame_count`, `frame_slots`, `matchable_offsets` 348 + - Update IRAM type assertions: `Instruction` instead of `ALUInst`/`SMInst` 349 + - Add assertions for `setup_tokens` field on AssemblyResult 350 + - Update `generate_tokens()` assertions for new token types (PELocalWriteToken, FrameControlToken) 351 + 352 + **Verification:** 353 + Run: `python -m pytest tests/test_codegen_frames.py -v` 354 + Expected: All new frame codegen tests pass. `tests/test_codegen.py` tests that have been updated should pass; stale tests may still fail until fully converted. 355 + 356 + **Commit:** `refactor: update codegen tests for frame-based output` 357 + 358 + <!-- END_TASK_4 -->
+308
docs/implementation-plans/2026-03-06-pe-frame-redesign/phase_07.md
··· 1 + # PE Frame-Based Redesign — Phase 7: Downstream Consumers — Monitor and Dfgraph 2 + 3 + **Goal:** Update monitor and dfgraph packages to consume new frame-based types, display frame state instead of matching store, and serialize new event types. 4 + 5 + **Architecture:** `PESnapshot` gains `frames`, `tag_store`, `presence`, `port_store`, `free_frames` fields. `SMSnapshot.t0_store` becomes `tuple[int, ...]`. Event serialization adds new frame events. Dfgraph updates opcode categories for FREE_FRAME and EXTRACT_TAG. All `ctx` references become `act_id`. 6 + 7 + **Tech Stack:** Python 3.12, FastAPI (monitor server), Cytoscape.js (frontend) 8 + 9 + **Scope:** Phase 7 of 8 from the PE frame-based redesign design plan. 10 + 11 + **Codebase verified:** 2026-03-07 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### pe-frame-redesign.AC7: End-to-end pipeline (partial) 20 + - **pe-frame-redesign.AC7.2 Success:** Monitor loads programs, displays frame state, serialises new events 21 + 22 + --- 23 + 24 + <!-- START_TASK_1 --> 25 + ### Task 1: Update monitor/snapshot.py for frame-based PE state 26 + 27 + **Verifies:** pe-frame-redesign.AC7.2 (partial — snapshot capture) 28 + 29 + **Files:** 30 + - Modify: `monitor/snapshot.py` (rewrite PESnapshot, update capture()) 31 + 32 + **Implementation:** 33 + 34 + 1. Replace PESnapshot fields: 35 + ```python 36 + @dataclass(frozen=True) 37 + class PESnapshot: 38 + pe_id: int 39 + iram: dict[int, Instruction] 40 + frames: tuple[tuple[FrameSlotValue, ...], ...] # [frame_count][frame_slots] 41 + tag_store: dict[int, int] # act_id → frame_id 42 + presence: tuple[tuple[bool, ...], ...] # [frame_count][matchable_offsets] 43 + port_store: tuple[tuple[Port | None, ...], ...] # [frame_count][matchable_offsets] 44 + free_frames: tuple[int, ...] # available frame IDs 45 + input_queue: tuple[Token, ...] 46 + output_log: tuple[Token, ...] 47 + ``` 48 + 49 + 2. Update `capture()` PE section: 50 + ```python 51 + frames = tuple( 52 + tuple(slot for slot in frame) 53 + for frame in pe.frames 54 + ) 55 + tag_store = dict(pe.tag_store) 56 + presence = tuple( 57 + tuple(p for p in frame_presence) 58 + for frame_presence in pe.presence 59 + ) 60 + port_store = tuple( 61 + tuple(p for p in frame_ports) 62 + for frame_ports in pe.port_store 63 + ) 64 + free_frames = tuple(pe.free_frames) 65 + ``` 66 + 67 + 3. Update imports: add `Instruction`, `FrameSlotValue` from `cm_inst`, `Port` from `cm_inst`. 68 + 69 + 4. Update SMSnapshot.t0_store type from `tuple[Token, ...]` to `tuple[int, ...]`. 70 + 71 + **Testing:** 72 + 73 + Tests must verify: 74 + - PESnapshot has frames, tag_store, presence, port_store, free_frames fields 75 + - PESnapshot does NOT have matching_store or gen_counters 76 + - capture() reads frame data correctly from live PE 77 + - SMSnapshot.t0_store contains ints 78 + 79 + Test file: `tests/test_snapshot.py` (update existing) 80 + 81 + **Verification:** 82 + Run: `python -m pytest tests/test_snapshot.py -v` 83 + Expected: Updated snapshot tests pass. Tests referencing removed fields may still fail until fully converted. 84 + 85 + **Commit:** `feat: update PESnapshot for frame-based state capture` 86 + 87 + <!-- END_TASK_1 --> 88 + 89 + <!-- START_TASK_2 --> 90 + ### Task 2: Update monitor/graph_json.py for frame events and act_id 91 + 92 + **Verifies:** pe-frame-redesign.AC7.2 (partial — event serialization) 93 + 94 + **Files:** 95 + - Modify: `monitor/graph_json.py` (ctx → act_id, add new event types, PE state shows frames) 96 + 97 + **Implementation:** 98 + 99 + 1. Node serialization: `"ctx": node.ctx` → `"act_id": node.act_id` 100 + 101 + 2. Matched event: `"ctx": event.ctx` → `"act_id": event.act_id`, add `"frame_id": event.frame_id` 102 + 103 + 3. Add new event type serialization: 104 + ```python 105 + elif isinstance(event, FrameAllocated): 106 + base["details"] = { 107 + "act_id": event.act_id, 108 + "frame_id": event.frame_id, 109 + } 110 + elif isinstance(event, FrameFreed): 111 + base["details"] = { 112 + "act_id": event.act_id, 113 + "frame_id": event.frame_id, 114 + } 115 + elif isinstance(event, FrameSlotWritten): 116 + base["details"] = { 117 + "frame_id": event.frame_id, 118 + "slot": event.slot, 119 + "value": event.value, 120 + } 121 + elif isinstance(event, TokenRejected): 122 + base["details"] = { 123 + "reason": event.reason, 124 + } 125 + ``` 126 + 127 + 4. PE state serialization: replace `matching_store` and `gen_counters` with frame data: 128 + ```python 129 + pe_state = { 130 + "pe_id": pe_snap.pe_id, 131 + "iram": {str(k): _serialise_instruction(v) for k, v in pe_snap.iram.items()}, 132 + "frames": [[_serialise_slot(s) for s in frame] for frame in pe_snap.frames], 133 + "tag_store": pe_snap.tag_store, 134 + "free_frames": list(pe_snap.free_frames), 135 + "input_queue_size": len(pe_snap.input_queue), 136 + } 137 + ``` 138 + 139 + 5. SM state: `t0_store` serialized as list of ints (already correct format). 140 + 141 + 6. Add imports for new event types. 142 + 143 + **Testing:** 144 + 145 + Tests must verify: 146 + - Node JSON has `act_id` key (not `ctx`) 147 + - Matched event JSON has `act_id` and `frame_id` 148 + - New events serialize correctly 149 + - PE state JSON has `frames`, `tag_store`, `free_frames` (not `matching_store`, `gen_counters`) 150 + 151 + Test file: `tests/test_monitor_graph_json.py` (update existing) 152 + 153 + **Verification:** 154 + Run: `python -m pytest tests/test_monitor_graph_json.py -v` 155 + Expected: Updated serialization tests pass. Tests referencing removed fields may still fail until fully converted. 156 + 157 + **Commit:** `feat: update monitor graph JSON for frame events and act_id` 158 + 159 + <!-- END_TASK_2 --> 160 + 161 + <!-- START_TASK_3 --> 162 + ### Task 3: Update monitor/backend.py for setup_tokens 163 + 164 + **Verifies:** pe-frame-redesign.AC7.2 (partial — program loading) 165 + 166 + **Files:** 167 + - Modify: `monitor/backend.py` (inject setup_tokens before seed_tokens) 168 + 169 + **Implementation:** 170 + 171 + In `_handle_load()`, after building the topology and before injecting seed tokens, inject setup_tokens: 172 + 173 + ```python 174 + # After system = build_topology(...) 175 + 176 + # Inject setup tokens (IRAM writes, ALLOC, frame slot writes) 177 + for token in result.setup_tokens: 178 + system.inject(token) 179 + 180 + # Inject seed tokens 181 + for seed in result.seed_tokens: 182 + system.inject(seed) 183 + ``` 184 + 185 + The `result` here is `AssemblyResult` which now has `setup_tokens`. 186 + 187 + Also update the PEConfig construction if backend.py creates configs directly (it may just pass through from codegen — verify). 188 + 189 + **Testing:** 190 + 191 + Tests must verify: 192 + - _handle_load() injects setup_tokens before seed_tokens 193 + - Setup tokens arrive at correct PEs 194 + 195 + Test file: `tests/test_backend.py` (update existing) 196 + 197 + **Verification:** 198 + Run: `python -m pytest tests/test_backend.py -v` 199 + Expected: Updated backend tests pass. Tests referencing removed types may still fail. 200 + 201 + **Commit:** `feat: inject setup_tokens in monitor backend load` 202 + 203 + <!-- END_TASK_3 --> 204 + 205 + <!-- START_TASK_4 --> 206 + ### Task 4: Update monitor/repl.py and monitor/formatting.py 207 + 208 + **Verifies:** pe-frame-redesign.AC7.2 (partial — REPL display) 209 + 210 + **Files:** 211 + - Modify: `monitor/repl.py` (update `pe` command display, update `send` command for act_id) 212 + - Modify: `monitor/formatting.py` (display frame state instead of matching store) 213 + 214 + **Implementation:** 215 + 216 + 1. `monitor/repl.py`: 217 + - `send` command: change `ctx` parameter to `act_id` in token construction 218 + - `pe` command: display calls `format_pe_state()` which needs updating 219 + 220 + 2. `monitor/formatting.py`: 221 + - `format_pe_state()`: replace matching store occupancy count with frame state display: 222 + ``` 223 + PE 0: 224 + Tag store: {0: 0, 1: 1} 225 + Free frames: [2, 3, 4, 5, 6, 7] 226 + Frame 0: [1234, None, FrameDest(...), ...] 227 + Frame 1: [5678, None, None, ...] 228 + ``` 229 + - Remove gen_counters display 230 + - Matched event formatting: `ctx=` → `act_id=`, add `frame_id=` 231 + 232 + **Testing:** 233 + 234 + Tests must verify: 235 + - `pe` command outputs frame state (not matching store) 236 + - `send` command accepts act_id parameter 237 + - Matched event formatting shows act_id and frame_id 238 + 239 + Test file: `tests/test_repl.py` and `tests/test_monitor_graph_json.py` (update existing) 240 + 241 + **Verification:** 242 + Run: `python -m pytest tests/test_repl.py -v` 243 + Expected: Updated REPL tests pass. Tests referencing removed fields may still fail. 244 + 245 + **Commit:** `feat: update REPL and formatting for frame-based PE display` 246 + 247 + <!-- END_TASK_4 --> 248 + 249 + <!-- START_TASK_5 --> 250 + ### Task 5: Update dfgraph/categories.py and dfgraph/graph_json.py 251 + 252 + **Verifies:** None (infrastructure) 253 + 254 + **Files:** 255 + - Modify: `dfgraph/categories.py` (FREE_CTX → FREE_FRAME, add EXTRACT_TAG) 256 + - Modify: `dfgraph/graph_json.py` (ctx → act_id) 257 + 258 + **Implementation:** 259 + 260 + 1. `dfgraph/categories.py`: 261 + - Update `_CONFIG_ROUTING_OPS` frozenset: `RoutingOp.FREE_CTX` → `RoutingOp.FREE_FRAME` 262 + - Add `RoutingOp.EXTRACT_TAG` to `_CONFIG_ROUTING_OPS` (or a new category if appropriate — EXTRACT_TAG produces a value so it might be ROUTING rather than CONFIG) 263 + 264 + 2. `dfgraph/graph_json.py`: 265 + - Node serialization: `"ctx": node.ctx` → `"act_id": node.act_id` 266 + - SM node synthesis: `"ctx": None` → `"act_id": None` 267 + 268 + **Testing:** 269 + 270 + Tests must verify: 271 + - `categorise(RoutingOp.FREE_FRAME)` returns CONFIG 272 + - `categorise(RoutingOp.EXTRACT_TAG)` returns expected category 273 + - Graph JSON has `act_id` key 274 + 275 + Test file: `tests/test_dfgraph_categories.py` and `tests/test_dfgraph_json.py` (update existing) 276 + 277 + **Verification:** 278 + Run: `python -m pytest tests/test_dfgraph_categories.py tests/test_dfgraph_json.py -v` 279 + Expected: All tests pass 280 + 281 + **Commit:** `feat: update dfgraph for FREE_FRAME, EXTRACT_TAG, and act_id` 282 + 283 + <!-- END_TASK_5 --> 284 + 285 + <!-- START_TASK_6 --> 286 + ### Task 6: Run tests for updated monitor and dfgraph modules 287 + 288 + **Verifies:** None (regression) 289 + 290 + **Files:** 291 + - Possibly modify: various test files 292 + 293 + **Implementation:** 294 + 295 + Run the downstream consumer test suite: 296 + ``` 297 + python -m pytest tests/test_backend.py tests/test_snapshot.py tests/test_repl.py tests/test_monitor_graph_json.py tests/test_monitor_server.py tests/test_dfgraph_pipeline.py tests/test_dfgraph_json.py tests/test_dfgraph_server.py tests/test_dfgraph_categories.py -v 298 + ``` 299 + 300 + Focus on getting the new and updated tests working. Some tests may still reference removed types — those will be fixed in Phase 8 as part of the full suite clean pass. 301 + 302 + **Verification:** 303 + Run: `python -m pytest tests/ -v -k "(monitor or dfgraph or backend or snapshot or repl)"` 304 + Expected: New and updated monitor/dfgraph tests pass. Some legacy tests may still fail. 305 + 306 + **Commit:** `fix: resolve monitor and dfgraph test updates` 307 + 308 + <!-- END_TASK_6 -->
+315
docs/implementation-plans/2026-03-06-pe-frame-redesign/phase_08.md
··· 1 + # PE Frame-Based Redesign — Phase 8: Fix Remaining Broken Tests and End-to-End Verification 2 + 3 + **Goal:** Fix all tests that still reference removed or renamed types from earlier phases, verify the full pipeline works end-to-end, and get the entire test suite green. 4 + 5 + **Architecture:** Earlier phases made targeted changes without maintaining backward compatibility. Many legacy test files will be broken by the time this phase begins. Phase 8 is where those are found and fixed. The codebase itself should already be correct — this phase is about aligning tests with what the code now does. 6 + 7 + **Tech Stack:** Python 3.12 8 + 9 + **Scope:** Phase 8 of 8 from the PE frame-based redesign design plan. 10 + 11 + **Codebase verified:** 2026-03-07 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### pe-frame-redesign.AC7: End-to-end pipeline 20 + - **pe-frame-redesign.AC7.1 Success:** Existing test programs produce correct results through full pipeline (parse → ... → emulate) 21 + - **pe-frame-redesign.AC7.3 Success:** No backward compatibility shims remain (ALUInst, SMInst, IRAMWriteToken, MatchEntry removed) 22 + - **pe-frame-redesign.AC7.4 Success:** `python -m pytest tests/ -v` passes clean 23 + 24 + --- 25 + 26 + <!-- START_TASK_1 --> 27 + ### Task 1: Search for and remove remaining stale references 28 + 29 + **Verifies:** pe-frame-redesign.AC7.3 (partial) 30 + 31 + **Files:** 32 + - Modify: any source or test file containing stale references 33 + 34 + **Implementation:** 35 + 36 + Search the codebase for any remaining references to removed types and names. Fix or remove each one. 37 + 38 + Types and names to search for: 39 + - `ALUInst` — removed from cm_inst.py in Phase 1 40 + - `SMInst` — removed from cm_inst.py in Phase 1 41 + - `Addr` — removed from cm_inst.py in Phase 1 42 + - `IRAMWriteToken` — removed from tokens.py in Phase 1 43 + - `MatchEntry` — removed from emu/types.py in Phase 1 44 + - `FREE_CTX` — replaced by `FREE_FRAME` 45 + - `ctx_slots` — replaced by `frame_count` 46 + - `gen_counters` — removed from PEConfig 47 + - `CtxSlotRef` — renamed to `ActSlotRef` 48 + - `CtxSlotRange` — renamed to `ActSlotRange` 49 + - `free_ctx_nodes` — renamed to `free_frame_nodes` 50 + - `.ctx` on IRNode — renamed to `.act_id` 51 + - `.ctx_slot` on IRNode — renamed to `.act_slot` 52 + - `node.ctx` in codegen — renamed to `node.act_id` 53 + - `gen=` in DyadToken construction — field removed 54 + - `ArithOp.SHIFT_L` — renamed to `ArithOp.SHL` 55 + - `ArithOp.SHIFT_R` — renamed to `ArithOp.SHR` 56 + - `ArithOp.ASHFT_R` — renamed to `ArithOp.ASR` 57 + - `shiftl`/`shiftr`/`ashiftr` in dfasm grammar — renamed to `shl`/`shr`/`asr` 58 + - `ctx_override` on IREdge — rename to `change_tag` (maps to OutputStyle.CHANGE_TAG in allocate) 59 + 60 + For each reference found: 61 + - If in a test file: update the test to use the new name/type 62 + - If in a source file: this indicates an incomplete migration from an earlier phase — fix it 63 + 64 + Run grep or similar across the entire repo before and after to confirm nothing remains. 65 + 66 + **Verification:** 67 + Run: `python -c "import cm_inst, tokens, emu.types, asm.ir; print('imports OK')"` 68 + Expected: Prints "imports OK" (no import errors from stale references) 69 + 70 + **Commit:** `chore: remove all remaining stale references to legacy types` 71 + 72 + <!-- END_TASK_1 --> 73 + 74 + <!-- START_TASK_2 --> 75 + ### Task 2: Fix remaining broken test files 76 + 77 + **Verifies:** pe-frame-redesign.AC7.4 (partial) 78 + 79 + **Files:** 80 + - Modify: all test files that fail due to stale type/field references 81 + 82 + **Implementation:** 83 + 84 + Run `python -m pytest tests/ -v` and collect all failures. For each failing test: 85 + 86 + 1. Identify the cause — stale import, stale field access, stale constructor argument, or test helper that constructs old token types 87 + 2. Update the test to use the new types and field names 88 + 3. Re-run to confirm that specific test now passes 89 + 90 + Common patterns to fix: 91 + - `from cm_inst import ALUInst` → remove or replace with `from cm_inst import Instruction` 92 + - `from tokens import IRAMWriteToken` → remove or replace with `from tokens import PELocalWriteToken` 93 + - `DyadToken(..., ctx=x, gen=y, ...)` → `DyadToken(..., act_id=x, ...)` 94 + - `MonadToken(..., ctx=x, ...)` → `MonadToken(..., act_id=x, ...)` 95 + - `PEConfig(ctx_slots=N, offsets=M, ...)` → `PEConfig(frame_count=N, ...)` 96 + - `pe_snap.matching_store` → `pe_snap.frames` 97 + - `pe_snap.gen_counters` → removed (no replacement needed in assertions) 98 + - `event.ctx` on Matched events → `event.act_id` 99 + - `node.ctx` → `node.act_id` 100 + - `system.ctx_slots` → `system.frame_count` 101 + 102 + Work through the full test suite until no failures remain from stale references. 103 + 104 + **Verification:** 105 + Run: `python -m pytest tests/ -v --tb=no -q` 106 + Expected: Failure count decreasing with each fix 107 + 108 + **Commit:** `fix: update test files for frame-based types and field names` 109 + 110 + <!-- END_TASK_2 --> 111 + 112 + <!-- START_TASK_3 --> 113 + ### Task 3: Fix remaining broken test files (continued) 114 + 115 + **Verifies:** pe-frame-redesign.AC7.4 (partial) 116 + 117 + **Files:** 118 + - Modify: test helper files and conftest.py if they construct old types 119 + 120 + **Implementation:** 121 + 122 + Check `tests/conftest.py` for Hypothesis strategies that generate old token types: 123 + - Strategies that produce `DyadToken` with `gen=` or `ctx=` fields need updating 124 + - Strategies that produce `IRAMWriteToken` need replacement with `PELocalWriteToken` 125 + - Strategies that use `ALUInst`/`SMInst` need replacement with `Instruction` 126 + 127 + Also check any shared test utility functions (helper builders used across multiple test files) for stale type usage. 128 + 129 + After updating conftest.py and helpers, re-run the full suite to catch any newly exposed failures. 130 + 131 + **Verification:** 132 + Run: `python -m pytest tests/ -v --tb=short -q` 133 + Expected: Only genuine test logic failures remain (if any), no import or AttributeError failures 134 + 135 + **Commit:** `fix: update conftest and test helpers for frame-based types` 136 + 137 + <!-- END_TASK_3 --> 138 + 139 + <!-- START_TASK_4 --> 140 + ### Task 4: Update test_migration_cleanup.py 141 + 142 + **Verifies:** pe-frame-redesign.AC7.3 143 + 144 + **Files:** 145 + - Modify: `tests/test_migration_cleanup.py` (add checks for removed frame-migration types) 146 + 147 + **Implementation:** 148 + 149 + Add test cases verifying removed types are absent: 150 + 151 + ```python 152 + def test_aluinst_removed(): 153 + """ALUInst should not be importable from cm_inst.""" 154 + with pytest.raises(ImportError): 155 + from cm_inst import ALUInst 156 + 157 + def test_sminst_removed(): 158 + """SMInst should not be importable from cm_inst.""" 159 + with pytest.raises(ImportError): 160 + from cm_inst import SMInst 161 + 162 + def test_addr_removed(): 163 + """Addr should not be importable from cm_inst.""" 164 + with pytest.raises(ImportError): 165 + from cm_inst import Addr 166 + 167 + def test_iram_write_token_removed(): 168 + """IRAMWriteToken should not be importable from tokens.""" 169 + with pytest.raises(ImportError): 170 + from tokens import IRAMWriteToken 171 + 172 + def test_match_entry_removed(): 173 + """MatchEntry should not be importable from emu.types.""" 174 + with pytest.raises(ImportError): 175 + from emu.types import MatchEntry 176 + 177 + def test_free_ctx_removed(): 178 + """FREE_CTX should not exist as a separate value in RoutingOp.""" 179 + from cm_inst import RoutingOp 180 + assert not hasattr(RoutingOp, 'FREE_CTX') or RoutingOp.FREE_CTX == RoutingOp.FREE_FRAME 181 + ``` 182 + 183 + Also add positive tests that new types ARE importable: 184 + ```python 185 + def test_instruction_exists(): 186 + from cm_inst import Instruction, OutputStyle, FrameDest, FrameOp, TokenKind 187 + assert Instruction is not None 188 + 189 + def test_pe_token_exists(): 190 + from tokens import PEToken, PELocalWriteToken, FrameControlToken 191 + assert issubclass(PELocalWriteToken, PEToken) 192 + ``` 193 + 194 + **Verification:** 195 + Run: `python -m pytest tests/test_migration_cleanup.py -v` 196 + Expected: All tests pass 197 + 198 + **Commit:** `test: verify removed legacy types are absent from codebase` 199 + 200 + <!-- END_TASK_4 --> 201 + 202 + <!-- START_TASK_5 --> 203 + ### Task 5: Update E2E tests for full pipeline 204 + 205 + **Verifies:** pe-frame-redesign.AC7.1, pe-frame-redesign.AC7.4 206 + 207 + **Files:** 208 + - Modify: `tests/test_e2e.py` (update run_program_direct and run_program_tokens helpers) 209 + - Modify: `tests/test_integration.py` (update for new types) 210 + 211 + **Implementation:** 212 + 213 + 1. Update `run_program_direct()` helper: 214 + - Use `assemble()` which returns `AssemblyResult` with `setup_tokens` and `seed_tokens` 215 + - `PEConfig` now has `Instruction` IRAM and frame fields 216 + - `build_topology()` takes new PEConfig format 217 + - Inject `setup_tokens` before `seed_tokens` 218 + 219 + 2. Update `run_program_tokens()` helper: 220 + - Use `assemble_to_tokens()` which returns new token types 221 + - Token sequence includes `PELocalWriteToken`, `FrameControlToken` 222 + - Load tokens via `system.load()` 223 + 224 + 3. Update all token constructor calls: `ctx=` → `act_id=`, remove `gen=` 225 + 226 + 4. Update all PEConfig constructor calls: remove `ctx_slots`, `offsets`, `gen_counters` 227 + 228 + 5. Verify existing test programs produce correct results: 229 + - Simple add program: two values → correct sum 230 + - Const program: constant injection → correct output 231 + - Any branch/switch programs: correct routing 232 + - SM programs: correct I-structure semantics 233 + 234 + **Testing:** 235 + 236 + The E2E tests themselves ARE the AC7.1 verification. They run the full pipeline and check results. 237 + 238 + **Verification:** 239 + Run: `python -m pytest tests/test_e2e.py tests/test_integration.py -v` 240 + Expected: All tests pass 241 + 242 + **Commit:** `test: update E2E tests for frame-based pipeline` 243 + 244 + <!-- END_TASK_5 --> 245 + 246 + <!-- START_TASK_6 --> 247 + ### Task 6: Full test suite clean pass 248 + 249 + **Verifies:** pe-frame-redesign.AC7.4 250 + 251 + **Files:** 252 + - Modify: any remaining test files with stale references 253 + 254 + **Implementation:** 255 + 256 + This is the final task. Run the full test suite and fix every remaining failure: 257 + 258 + ``` 259 + python -m pytest tests/ -v 260 + ``` 261 + 262 + Work through failures one by one. At this point all failures should be: 263 + - Stale type references not caught in Tasks 1-3 264 + - Test logic that needs updating to match new behaviour 265 + - Any test helpers that still construct old token types 266 + 267 + After each fix, re-run the suite to confirm the fix did not introduce new failures. 268 + 269 + The suite is done when `python -m pytest tests/ -v` exits with zero failures and zero errors. 270 + 271 + **Verification:** 272 + Run: `python -m pytest tests/ -v` 273 + Expected: All tests pass with zero failures 274 + 275 + **Commit:** `chore: final cleanup — all tests pass clean` 276 + 277 + <!-- END_TASK_6 --> 278 + 279 + <!-- START_TASK_7 --> 280 + ### Task 7: Update CLAUDE.md files 281 + 282 + **Verifies:** None (documentation) 283 + 284 + **Files:** 285 + - Modify: `CLAUDE.md` (root project instructions) 286 + - Modify: `asm/CLAUDE.md` (assembler package docs) 287 + 288 + **Implementation:** 289 + 290 + Both CLAUDE.md files extensively reference removed types and old field names. Update: 291 + 292 + 1. Root `CLAUDE.md`: 293 + - Token hierarchy: remove `IRAMWriteToken`, add `PELocalWriteToken` and `FrameControlToken`, rename `ctx` → `act_id`, remove `gen` from DyadToken, add PEToken base class 294 + - Instruction set: replace `ALUInst`/`SMInst`/`Addr` docs with `Instruction`/`FrameDest`/`OutputStyle`/`TokenKind` 295 + - PE section: replace matching_store/gen_counters with frames/tag_store/presence/port_store/free_frames, update pipeline order to IFETCH→act_id resolution→MATCH→EXECUTE→EMIT 296 + - SM section: t0_store is `list[int]` not `list[Token]` 297 + - Event types: add FrameAllocated, FrameFreed, FrameSlotWritten, TokenRejected; update Matched.ctx→act_id 298 + - PEConfig: remove ctx_slots, offsets, gen_counters; add frame_count, frame_slots, matchable_offsets, initial_frames, initial_tag_store 299 + - Module dependency graph: add encoding.py 300 + 301 + 2. `asm/CLAUDE.md`: 302 + - IR types: ctx→act_id, ctx_slot→act_slot, add mode/fref/wide/frame_layout fields, FrameSlotMap, FrameLayout 303 + - SystemConfig: ctx_slots→frame_count, add frame_slots, matchable_offsets 304 + - Allocate: describe frame layout computation, IRAM dedup, act_id assignment 305 + - Codegen: describe setup_tokens, PELocalWriteToken/FrameControlToken generation 306 + - ErrorCategory: add FRAME 307 + 308 + Update the `<!-- freshness: ... -->` date on both files. 309 + 310 + **Verification:** 311 + Manual review — ensure no references to removed types remain in either file. 312 + 313 + **Commit:** `docs: update CLAUDE.md files for frame-based architecture` 314 + 315 + <!-- END_TASK_7 -->
+172
docs/implementation-plans/2026-03-06-pe-frame-redesign/test-requirements.md
··· 1 + # Test Requirements -- PE Frame-Based Redesign 2 + 3 + This document maps each acceptance criterion from the PE frame-based redesign 4 + design plan to either an automated test or a documented human verification step. 5 + Every AC is accounted for. Test file paths reflect both new files introduced by 6 + the implementation plan and updates to existing test files. 7 + 8 + ## Automated Test Coverage 9 + 10 + ### AC1: Token hierarchy correctly models frame-based architecture 11 + 12 + | AC | Test Type | Test File | Description | 13 + |---|---|---|---| 14 + | AC1.1 | unit | tests/test_foundation_types.py | Verify `issubclass(CMToken, PEToken)`, `issubclass(PELocalWriteToken, PEToken)`, `issubclass(FrameControlToken, PEToken)`, `issubclass(PEToken, Token)`. Verify transitive: `isinstance(DyadToken(...), PEToken)` is True. Verify `isinstance(SMToken(...), PEToken)` is False. Verify `IRAMWriteToken` does not exist in `tokens` module. | 15 + | AC1.2 | unit | tests/test_foundation_types.py | Verify `CMToken` constructor accepts `act_id` keyword (not `ctx`). Verify `DyadToken` constructor has no `gen` parameter (raises `TypeError` if `gen=` is passed). Verify all token types are frozen dataclasses. | 16 + | AC1.3 | unit | tests/test_foundation_types.py | Verify `PELocalWriteToken` has `region`, `slot`, `data`, `is_dest` fields. Construct instances with various field values and confirm frozen dataclass semantics. | 17 + | AC1.4 | unit | tests/test_foundation_types.py | Verify `FrameControlToken` has `act_id`, `op`, `payload` fields. Verify `op` accepts `FrameOp.ALLOC` and `FrameOp.FREE` values. | 18 + | AC1.5 | integration | tests/test_network_routing.py | Inject `PELocalWriteToken(target=0)`, `FrameControlToken(target=0)`, `DyadToken(target=0)`, and `SMToken(target=0)` via `System.inject()`. Verify PE-bound tokens arrive at PE 0's `input_store` and SM-bound tokens arrive at SM 0's `input_store`. Verify `build_topology()` routes all three PE-bound token types via the single `isinstance(token, PEToken)` check. | 19 + | AC1.6 | unit | tests/test_pe_frames.py | Inject a `DyadToken` with an `act_id` not present in the PE's `tag_store`. Verify the token is dropped (no crash, no output token). Verify a `TokenRejected` event is emitted with `reason` containing the invalid `act_id`. | 20 + 21 + ### AC2: Semantic Instruction model with pack/unpack 22 + 23 + | AC | Test Type | Test File | Description | 24 + |---|---|---|---| 25 + | AC2.1 | unit | tests/test_foundation_types.py | Construct `Instruction` with all field types (`opcode: ALUOp`, `output: OutputStyle`, `has_const: bool`, `dest_count: int`, `wide: bool`, `fref: int`). Verify frozen dataclass. Verify `OutputStyle`, `TokenKind`, `FrameOp` enum values. Verify `RoutingOp.EXTRACT_TAG` (value 31), `RoutingOp.FREE_FRAME` (value 29), `RoutingOp.ALLOC_REMOTE` (value 30) exist. Verify `ArithOp.SHL` (8), `ArithOp.SHR` (9), `ArithOp.ASR` (10) exist and old names (`SHIFT_L`, `SHIFT_R`, `ASHFT_R`) do not. Verify all ALUOp values fit in 5 bits. | 26 + | AC2.2 | unit | tests/test_encoding.py | Property-based round-trip: `unpack_instruction(pack_instruction(inst)) == inst` for all valid mode combinations (modes 0-7) x wide x opcode classes (ArithOp, LogicOp, RoutingOp, MemOp) x fref 0-63. Verify mode encoding matches design-notes bit decode table. Verify opcode encoding is identity (Python IntEnum values match hardware). `pack_flit1`/`unpack_flit1` round-trip for all token kinds (DYADIC, MONADIC, INLINE) with various target_pe, offset, act_id, port values. Edge cases: fref=0, fref=63, target_pe=0, target_pe=3, act_id=0, act_id=7. | 27 + | AC2.3 | unit | tests/test_foundation_types.py | Verify `isinstance(Instruction(opcode=MemOp.READ, ...).opcode, MemOp)` returns `True`. Verify `isinstance(Instruction(opcode=ArithOp.ADD, ...).opcode, MemOp)` returns `False`. No stored `type` field on `Instruction`. | 28 + | AC2.4 | integration | tests/test_pe_frames.py | Verify the PE processes `Instruction` objects using only semantic fields (`output`, `has_const`, `dest_count`, `wide`, `fref`). Verify no raw mode bits or pack/unpack calls in the PE's main pipeline (IFETCH/MATCH/EXECUTE/EMIT). Only CHANGE_TAG, EXTRACT_TAG, and PELocalWriteToken(region=0) use encoding boundary functions. | 29 + 30 + ### AC3: Frame-based PE matching and output routing 31 + 32 + | AC | Test Type | Test File | Description | 33 + |---|---|---|---| 34 + | AC3.1 | unit | tests/test_pe_frames.py | Construct PE with `frame_count=4`, `frame_slots=32`, `matchable_offsets=4`. Verify values stored on PE instance. Verify defaults (8, 64, 8) when not specified. | 35 + | AC3.2 | integration | tests/test_pe_frames.py | Inject a DyadToken pair targeting the same offset and act_id. Capture events via `on_event`. Verify event ordering: `TokenReceived` then `Matched` then `Executed` then `Emitted`. Verify IFETCH occurs before MATCH (instruction fetched from IRAM before matching attempted). | 36 + | AC3.3 | integration | tests/test_pe_frames.py | Pre-configure `tag_store` and frame with presence bits. Inject two `DyadToken`s at the same offset and act_id (one Port.L, one Port.R). Verify they match via frame SRAM and presence bits (not via `matching_store` array). Verify `Matched` event contains correct `left`, `right`, `act_id`, `offset`, `frame_id`. Verify port metadata determines left/right ordering. | 37 + | AC3.4 | integration | tests/test_pe_frames.py | Pre-load a `FrameDest` into a frame destination slot. Set up an `Instruction` with `OutputStyle.INHERIT`. Inject tokens to trigger execution. Verify the output token has `target`, `offset`, `act_id`, `port` matching the `FrameDest` values. | 38 + | AC3.5 | integration | tests/test_pe_frames.py | Set up an `Instruction` with `OutputStyle.CHANGE_TAG`. Inject a token whose left operand is a packed flit 1 value (from `pack_flit1(some_dest)`). Verify the output token's destination matches the unpacked `FrameDest` (target_pe, offset, act_id, port from the left operand, not from a frame slot). | 39 + | AC3.6 | integration | tests/test_pe_frames.py | Set up an `Instruction` with `OutputStyle.SINK`. Inject tokens to trigger execution. Verify the ALU result is written to `frames[frame_id][inst.fref]`. Verify no output token is emitted (`output_log` unchanged). Verify `FrameSlotWritten` event is emitted. | 40 + | AC3.7 | integration | tests/test_pe_frames.py | Set up an `Instruction` with `opcode=RoutingOp.EXTRACT_TAG` and `OutputStyle.INHERIT`. Inject a `MonadToken`. Verify the result is `pack_flit1(FrameDest(target_pe=pe_id, offset=token.offset, act_id=token.act_id, port=Port.L, token_kind=TokenKind.DYADIC))`. Verify the result is emitted as the data field of the output token. Verify `Executed` event shows the EXTRACT_TAG opcode. | 41 + | AC3.8 | integration | tests/test_pe_frames.py | Inject `FrameControlToken(op=FrameOp.ALLOC, act_id=X)`. Verify `tag_store[X]` is set to a valid `frame_id`, frame_id removed from `free_frames`, `FrameAllocated` event emitted. Inject `FrameControlToken(op=FrameOp.FREE, act_id=X)`. Verify `tag_store[X]` is removed, frame_id returned to `free_frames`, `FrameFreed` event emitted. Also test `FREE_FRAME` opcode path: set up a `FREE_FRAME` instruction in IRAM, inject a `MonadToken`, verify tag_store cleared, frame returned, `FrameFreed` event, no output token. Also test `ALLOC_REMOTE`: set up `ALLOC_REMOTE` instruction with target PE/act_id in frame constants, inject `MonadToken`, verify a `FrameControlToken(ALLOC)` is delivered to the target PE. | 42 + | AC3.9 | integration | tests/test_pe_frames.py | Inject `PELocalWriteToken(region=1, is_dest=True, data=pack_flit1(some_dest))`. Verify the frame slot at `token.slot` contains a `FrameDest` object (not a raw int). Verify `FrameSlotWritten` event. Also test `is_dest=False`: verify frame slot contains the raw int. | 43 + | AC3.10 | integration | tests/test_pe_frames.py | Inject a dyadic token pair at time T. Verify output token emitted at T+5 (dequeue 1 + IFETCH 1 + MATCH 1 + EXECUTE 1 + EMIT 1). Inject a monadic token at time T. Verify output at T+4 (dequeue 1 + IFETCH 1 + EXECUTE 1 + EMIT 1). Inject `FrameControlToken`/`PELocalWriteToken` at time T. Verify completion at T+2 (dequeue 1 + handle 1). Network delivery adds 1 additional cycle (separate process). | 44 + 45 + ### AC4: T0 raw storage and EXEC 46 + 47 + | AC | Test Type | Test File | Description | 48 + |---|---|---|---| 49 + | AC4.1 | unit | tests/test_sm_t0_raw.py | After SM construction, verify `t0_store` is `list[int]`. Perform a T0 WRITE operation, verify stored values are ints (not Token objects). | 50 + | AC4.2 | integration | tests/test_sm_t0_raw.py | Pre-load `t0_store` with packed token flits via `pack_token()` for multiple tokens (DyadToken, MonadToken, SMToken). Trigger EXEC at the starting address. Verify reconstituted tokens arrive at target PEs/SMs with correct field values. Verify `flit_count()` correctly determines packet boundaries for consecutive multi-flit packets. | 51 + | AC4.3 | unit | tests/test_encoding.py | Property-based round-trip: `unpack_token(pack_token(token))` produces a token with matching fields for `DyadToken`, `MonadToken`, and `SMToken`. Verify `SMToken.ret` is not preserved (round-trips as `None`). Also tested implicitly in `tests/test_sm_t0_raw.py` through EXEC end-to-end. Update `tests/test_sm_tiers.py` and `tests/test_exec_bootstrap.py` to use `pack_token()` for T0 pre-loading. | 52 + | AC4.4 | unit | tests/test_sm_t0_raw.py | Pre-load `t0_store` with invalid prefix bits (e.g., `[0xFFFF]`). Trigger EXEC. Verify SM stops gracefully without crash and logs a warning. Also test truncated packet: set `flit_count` to indicate 3 flits but provide only 2 ints -- verify graceful stop with warning. | 53 + 54 + ### AC5: Assembler allocate produces frame layouts 55 + 56 + | AC | Test Type | Test File | Description | 57 + |---|---|---|---| 58 + | AC5.1 | unit | tests/test_allocate_frames.py | Create two IR nodes on the same PE with identical opcode, mode, wide, and fref. Run `_deduplicate_iram()`. Verify both nodes share the same IRAM offset. Create nodes with different opcodes or modes and verify they get different offsets. Verify seed nodes are excluded. | 59 + | AC5.2 | unit | tests/test_allocate_frames.py | Create two activations of the same function body (same set of instructions). Run `_compute_frame_layouts()`. Verify both activations produce identical `FrameLayout` objects (same `FrameSlotMap`). | 60 + | AC5.3 | unit | tests/test_allocate_frames.py | Run `_assign_act_ids()` with multiple function scopes on a single PE. Verify act_ids assigned 0, 1, 2, ... sequentially. Verify at most `frame_count` concurrent act_ids per PE. Verify same function on different PEs can reuse act_id 0. | 61 + | AC5.4 | unit | tests/test_allocate_frames.py | Run `_compute_frame_layouts()` on an activation with dyadic and monadic nodes, constants, and multiple output edges. Verify match operand slots at 0 to `matchable_offsets-1` (indexed by IRAM offset). Verify constants (deduplicated by value) at `matchable_offsets` and above. Verify destinations (deduplicated by FrameDest identity) after constants. Verify sinks/SM params after destinations. | 62 + | AC5.5 | unit | tests/test_allocate_frames.py | Run `_compute_modes()` on nodes with varying edge topologies. Verify: 0 outgoing edges + FREE_FRAME opcode produces `(OutputStyle.SINK, *, 0)`. 1 outgoing edge produces `(OutputStyle.INHERIT, *, 1)`. 2 outgoing edges produces `(OutputStyle.INHERIT, *, 2)`. Edge with `ctx_override=True` produces `OutputStyle.CHANGE_TAG`. Verify `has_const` derived from `node.const is not None`. Verify SM sink ops (WRITE, CLEAR, etc.) produce `OutputStyle.SINK`. | 63 + | AC5.6 | unit | tests/test_allocate_frames.py | Create an activation requiring more frame slots than `frame_slots` limit. Verify `_compute_frame_layouts()` reports an error with `ErrorCategory.FRAME`. | 64 + | AC5.7 | unit | tests/test_allocate_frames.py | Create a PE requiring more than `frame_count` concurrent activations. Verify `_assign_act_ids()` reports an error with `ErrorCategory.FRAME` and message containing "activation ID exhaustion". | 65 + | AC5.8 | unit | tests/test_allocate_frames.py | Create an activation with more dyadic IRAM offsets than `matchable_offsets`. Verify a warning is emitted with `ErrorCategory.FRAME` and `severity="warning"`. Verify placement still succeeds (not an error). Also tested in `tests/test_place.py` (update existing `_count_iram_cost` tests to expect cost=1 for all node types). | 66 + 67 + ### AC6: Assembler codegen produces frame setup tokens 68 + 69 + | AC | Test Type | Test File | Description | 70 + |---|---|---|---| 71 + | AC6.1 | unit | tests/test_codegen_frames.py | Verify `AssemblyResult` dataclass has a `setup_tokens` field of type `list[Token]`. | 72 + | AC6.2 | integration | tests/test_codegen_frames.py | Run `generate_tokens()` on a simple program. Inspect the output token list. Verify ordering: all `SMToken` writes first, then `PELocalWriteToken(region=0)` IRAM writes, then `FrameControlToken(op=ALLOC)`, then `PELocalWriteToken(region=1)` frame slot writes, then seed `DyadToken`/`MonadToken` last. | 73 + | AC6.3 | unit | tests/test_codegen_frames.py | Run `_build_iram_for_pe()` on allocated IR nodes. Verify IRAM entries are `Instruction` objects. Verify IRAM write `PELocalWriteToken` data field equals `pack_instruction(inst)` for each entry. Verify CM vs SM type derived from `isinstance(opcode, MemOp)`. | 74 + | AC6.4 | unit | tests/test_codegen_frames.py | Inspect destination frame slot write tokens in setup sequence. Verify each has `is_dest=True` and `data == pack_flit1(expected_frame_dest)`. | 75 + | AC6.5 | integration | tests/test_codegen_frames.py | For a program with T0 bootstrap data, verify T0 writes use `pack_token()` to produce packed flit sequences. Verify the written ints can be round-tripped back via `unpack_token()`. | 76 + | AC6.6 | unit | tests/test_codegen_frames.py | Verify seed tokens use `act_id` field. Verify `DyadToken` seeds have no `gen` field. Verify seed `MonadToken`s use `act_id`. Update `tests/test_codegen.py` for `act_id` rename and `gen` removal. | 77 + 78 + ### AC7: End-to-end pipeline 79 + 80 + | AC | Test Type | Test File | Description | 81 + |---|---|---|---| 82 + | AC7.1 | e2e | tests/test_e2e.py | Run existing test programs through the full pipeline (parse, lower, expand, resolve, place, allocate, codegen, build_topology, inject setup_tokens + seed_tokens, run SimPy). Verify correct results for: simple add, constant injection, branch/switch routing, SM I-structure read/write. Update `run_program_direct()` and `run_program_tokens()` helpers for new types. | 83 + | AC7.1 | e2e | tests/test_integration.py | Update integration tests for new token types, field names, and PEConfig format. Verify programs produce correct outputs through the new frame-based pipeline. | 84 + | AC7.2 | integration | tests/test_backend.py | Verify `_handle_load()` injects `setup_tokens` before `seed_tokens`. Verify monitor loads programs and returns `GraphLoaded` with valid snapshot containing frame state. | 85 + | AC7.2 | integration | tests/test_snapshot.py | Verify `PESnapshot` has `frames`, `tag_store`, `presence`, `port_store`, `free_frames`. Verify `PESnapshot` does NOT have `matching_store` or `gen_counters`. Verify `capture()` reads frame data from live PE. Verify `SMSnapshot.t0_store` contains ints. | 86 + | AC7.2 | integration | tests/test_monitor_graph_json.py | Verify node JSON has `act_id` key (not `ctx`). Verify `Matched` event JSON has `act_id` and `frame_id`. Verify new event types (`FrameAllocated`, `FrameFreed`, `FrameSlotWritten`, `TokenRejected`) serialize correctly. Verify PE state JSON has `frames`, `tag_store`, `free_frames`. | 87 + | AC7.2 | integration | tests/test_repl.py | Verify `pe` command displays frame state (not matching store). Verify `send` command accepts `act_id` parameter. | 88 + | AC7.3 | unit | tests/test_migration_cleanup.py | Verify `ALUInst`, `SMInst`, `Addr` not importable from `cm_inst`. Verify `IRAMWriteToken` not importable from `tokens`. Verify `MatchEntry` not importable from `emu.types`. Verify `RoutingOp.FREE_CTX` does not exist (or is the same as `FREE_FRAME`). Verify positive: `Instruction`, `OutputStyle`, `FrameDest`, `PEToken`, `PELocalWriteToken`, `FrameControlToken` all importable. | 89 + | AC7.4 | e2e | tests/ (full suite) | `python -m pytest tests/ -v` passes with zero failures and zero errors. This is the final gate criterion verified in Phase 8 Task 6. | 90 + 91 + ## Additional Test Files Updated (not new) 92 + 93 + These existing test files require updates for renamed fields and removed types. 94 + They do not map to specific ACs but are necessary for AC7.4 (full suite green): 95 + 96 + | Test File | Changes Required | 97 + |---|---| 98 + | tests/conftest.py | Update Hypothesis strategies: `ctx` to `act_id`, remove `gen`, add `PELocalWriteToken` and `FrameControlToken` strategies | 99 + | tests/test_alu.py | Rename shift op references (`SHIFT_L` to `SHL`, etc.), `FREE_CTX` to `FREE_FRAME`, add `EXTRACT_TAG`/`ALLOC_REMOTE` as no-op fallback cases | 100 + | tests/test_pe.py | Rename to use new PE constructor params, `act_id`, frame-based matching. May be superseded by `test_pe_frames.py`. | 101 + | tests/test_pe_events.py | Update for `act_id`, `Instruction` IRAM, frame-based PE constructor, new event fields | 102 + | tests/test_cycle_timing.py | Update PE construction and token fields; timing values unchanged (5/4/1 cycles) | 103 + | tests/test_sm_events.py | Update token constructor calls (`act_id`, no `gen`) | 104 + | tests/test_sm_tiers.py | Update T0 tests to use `pack_token()` for pre-loading, int retrieval | 105 + | tests/test_exec_bootstrap.py | Pre-load T0 with packed flits via `pack_token()` instead of Token objects | 106 + | tests/test_network_events.py | Update token types and field names | 107 + | tests/test_network.py | Update `PEConfig` construction, token fields | 108 + | tests/test_allocate.py | `ctx` to `act_id`, `ctx_slots` to `frame_count`, IRAM cost=1 for all nodes | 109 + | tests/test_place.py | `ctx_slots` to `frame_count`, IRAM cost=1, matchable offset warning | 110 + | tests/test_autoplacement.py | `ctx_slots` to `frame_count` | 111 + | tests/test_codegen.py | `ctx` to `act_id`, `gen` removal, `Instruction` IRAM, `PEConfig` new fields | 112 + | tests/test_lower.py | `ctx_slot` to `act_slot` | 113 + | tests/test_expand.py | `CtxSlotRef` to `ActSlotRef`, `FREE_CTX` to `FREE_FRAME` | 114 + | tests/test_call_wiring.py | `free_ctx_nodes` to `free_frame_nodes`, `FREE_CTX` to `FREE_FRAME` | 115 + | tests/test_serialize.py | `ctx` to `act_id`, `ctx_slot` to `act_slot` | 116 + | tests/test_seed_const.py | `ctx` to `act_id`, `gen` removal | 117 + | tests/test_sm_graph_nodes.py | Token field renames | 118 + | tests/test_macro_ir.py | `ctx_slot` to `act_slot` | 119 + | tests/test_macro_ret_wiring.py | `ctx_slot` to `act_slot`, `FREE_CTX` to `FREE_FRAME` | 120 + | tests/test_opcodes.py | Shift mnemonic renames, `free_ctx` to `free_frame`, add `extract_tag`/`alloc_remote` | 121 + | tests/test_dfgraph_categories.py | `FREE_CTX` to `FREE_FRAME`, add `EXTRACT_TAG` | 122 + | tests/test_dfgraph_json.py | `ctx` to `act_id` | 123 + | tests/test_monitor_server.py | Token field renames, new event types in WebSocket protocol | 124 + 125 + ## Human Verification 126 + 127 + | AC | Justification | Verification Approach | 128 + |---|---|---| 129 + | AC2.4 | "Emulator PE never touches raw mode bits -- only semantic fields" is an architectural invariant about code structure, not runtime behavior. An automated test can verify that the PE does not import pack/unpack for its main pipeline, but confirming the *absence* of bit-level manipulation throughout the PE is a code review concern. | Code review of `emu/pe.py` after Phase 2. Verify: (1) no raw integer bit masking/shifting on instruction fields in `_process_token()`, `_match_frame()`, `_do_emit_new()`, `_emit_inherit()`, or `_emit_sink()`; (2) `pack_flit1`/`unpack_flit1` calls appear only in `_emit_change_tag()` (CHANGE_TAG path), EXTRACT_TAG handler, and `_handle_local_write()` (PELocalWriteToken region=0 decode); (3) `Instruction` fields accessed by name (`inst.output`, `inst.has_const`, `inst.fref`) rather than by bit extraction. A supplementary automated test in `tests/test_pe_frames.py` can use `inspect.getsource()` to grep for bit operations as a heuristic check. | 130 + | AC7.2 (REPL display) | The REPL `pe` command display of frame state is a human-facing text formatting concern. Automated tests verify the data is present, but the visual layout (indentation, column alignment, readability) requires human judgment. | Run `python -m monitor` with a loaded program, execute `pe 0`, visually confirm: tag store mapping displayed, free frames listed, frame slot contents shown per frame, no references to "matching store" or "gen counters". | 131 + | AC7.2 (WebSocket events) | While JSON serialization of new event types is tested automatically, verifying the monitor web UI correctly renders frame state and new events in the browser requires a running frontend. | Start `python -m monitor --web`, load a program, step through execution in the browser. Verify: frame state panel shows frames/tag_store/free_frames, event log shows FrameAllocated/FrameFreed/FrameSlotWritten/TokenRejected events with correct fields, no rendering errors in the console. | 132 + | Phase 8 CLAUDE.md updates | Documentation accuracy cannot be verified by automated tests. The root `CLAUDE.md` and `asm/CLAUDE.md` must be manually reviewed to ensure all references to removed types, old field names, and old pipeline descriptions have been updated. | Review both files against the final codebase state. Check: (1) token hierarchy section matches new PEToken-based hierarchy; (2) instruction set section describes Instruction/FrameDest/OutputStyle not ALUInst/SMInst/Addr; (3) PE section describes frames/tag_store not matching_store/gen_counters; (4) PEConfig section lists frame_count/frame_slots/matchable_offsets not ctx_slots/offsets; (5) event types section includes all four new events; (6) freshness date updated. | 133 + 134 + ## Rationalization Against Implementation Decisions 135 + 136 + The following implementation decisions from the phase plans affect test strategy: 137 + 138 + 1. **Phase 1 removes old types immediately** (no backward-compat shims). This means 139 + many existing tests break at Phase 1 and are not fixed until Phase 8. Test files 140 + created in Phases 1-7 (`test_foundation_types.py`, `test_encoding.py`, 141 + `test_pe_frames.py`, `test_sm_t0_raw.py`, `test_network_routing.py`, 142 + `test_allocate_frames.py`, `test_codegen_frames.py`) are self-contained and pass 143 + within their phase. The "full suite green" criterion (AC7.4) is only achievable 144 + after Phase 8. 145 + 146 + 2. **Enum value renumbering** (Task 1 of Phase 1) to match 5-bit hardware encoding 147 + changes integer values of existing enums. Tests using `frozenset` membership on 148 + enum objects are unaffected (membership is by identity). Tests comparing raw 149 + integer values of enum members need updating. 150 + 151 + 3. **IRAM deduplication is a post-processing step** (Phase 5 Task 1). The 152 + `_deduplicate_iram()` function runs after `_compute_modes()` and 153 + `_compute_frame_layouts()` produce `fref` and mode fields. AC5.1 tests must 154 + set up fully-computed nodes (with mode and fref) before calling dedup. 155 + 156 + 4. **Match slot indexing** uses `token.offset % matchable_offsets` (Phase 2 Task 2), 157 + not a direct frame slot index. Tests for AC3.3 must set up IRAM offsets and 158 + inject tokens at those offsets, not at arbitrary slot indices. 159 + 160 + 5. **CHANGE_TAG uses the left operand** (not the ALU result) as the destination 161 + descriptor (Phase 2 Task 2). Tests for AC3.5 must inject tokens where the left 162 + data field contains a packed flit 1, and verify the destination is derived from 163 + that left operand. 164 + 165 + 6. **SMToken.ret is NOT preserved** through pack/unpack round-trip (Phase 1 Task 4). 166 + Tests for AC4.3 must verify SMToken round-trips with `ret=None`. This is by 167 + design -- SM return routes are stored as FrameDest in frame slots in the new model. 168 + 169 + 7. **ctx_override on IREdge maps to OutputStyle.CHANGE_TAG** (Phase 5 Task 3). The 170 + field is not renamed in Phase 5 but is scheduled for rename to `change_tag` in 171 + Phase 8 cleanup. Tests for AC5.5 must use the `ctx_override` field name when 172 + testing CHANGE_TAG mode derivation.
+31 -10
tests/conftest.py
··· 4 4 from hypothesis import strategies as st 5 5 from lark import Lark 6 6 7 - from cm_inst import ArithOp, LogicOp, MemOp, Port, RoutingOp 8 - from tokens import CMToken, DyadToken, MonadToken, SMToken 7 + from cm_inst import ArithOp, FrameOp, LogicOp, MemOp, Port, RoutingOp 8 + from tokens import CMToken, DyadToken, FrameControlToken, MonadToken, PELocalWriteToken, SMToken 9 9 10 10 GRAMMAR_PATH = Path(__file__).parent.parent / "dfasm.lark" 11 11 ··· 15 15 16 16 arith_dyadic_ops = st.sampled_from([ArithOp.ADD, ArithOp.SUB]) 17 17 arith_monadic_ops = st.sampled_from([ArithOp.INC, ArithOp.DEC]) 18 - shift_ops = st.sampled_from([ArithOp.SHIFT_L, ArithOp.SHIFT_R, ArithOp.ASHFT_R]) 18 + shift_ops = st.sampled_from([ArithOp.SHL, ArithOp.SHR, ArithOp.ASR]) 19 19 logic_dyadic_ops = st.sampled_from([LogicOp.AND, LogicOp.OR, LogicOp.XOR]) 20 20 comparison_ops = st.sampled_from([LogicOp.EQ, LogicOp.LT, LogicOp.LTE, LogicOp.GT, LogicOp.GTE]) 21 21 branch_ops = st.sampled_from([RoutingOp.BREQ, RoutingOp.BRGT, RoutingOp.BRGE]) ··· 25 25 26 26 27 27 @st.composite 28 - def dyad_token(draw, target: int = 0, offset: int | None = None, ctx: int | None = None, gen: int | None = None) -> DyadToken: 28 + def dyad_token(draw, target: int = 0, offset: int | None = None, act_id: int | None = None) -> DyadToken: 29 29 return DyadToken( 30 30 target=target, 31 31 offset=draw(st.integers(min_value=0, max_value=63)) if offset is None else offset, 32 - ctx=draw(st.integers(min_value=0, max_value=3)) if ctx is None else ctx, 32 + act_id=draw(st.integers(min_value=0, max_value=7)) if act_id is None else act_id, 33 33 data=draw(uint16), 34 34 port=draw(st.sampled_from(list(Port))), 35 - gen=draw(st.integers(min_value=0, max_value=3)) if gen is None else gen, 36 35 wide=False, 37 36 ) 38 37 39 38 40 39 @st.composite 41 - def monad_token(draw, target: int = 0, offset: int | None = None, ctx: int | None = None) -> MonadToken: 40 + def monad_token(draw, target: int = 0, offset: int | None = None, act_id: int | None = None) -> MonadToken: 42 41 return MonadToken( 43 42 target=target, 44 43 offset=draw(st.integers(min_value=0, max_value=63)) if offset is None else offset, 45 - ctx=draw(st.integers(min_value=0, max_value=3)) if ctx is None else ctx, 44 + act_id=draw(st.integers(min_value=0, max_value=7)) if act_id is None else act_id, 46 45 data=draw(uint16), 47 46 inline=False, 48 47 ) ··· 64 63 _addr = draw(st.integers(min_value=0, max_value=255)) if addr is None else addr 65 64 _op = draw(sm_all_ops) if op is None else op 66 65 _data = draw(uint16) if data is None else data 67 - ret = CMToken(target=0, offset=0, ctx=0, data=0) 66 + ret = CMToken(target=0, offset=0, act_id=0, data=0) 68 67 return SMToken( 69 68 target=0, 70 69 addr=_addr, ··· 80 79 return CMToken( 81 80 target=target, 82 81 offset=draw(st.integers(min_value=0, max_value=63)), 83 - ctx=draw(st.integers(min_value=0, max_value=3)), 82 + act_id=draw(st.integers(min_value=0, max_value=7)), 84 83 data=0, 84 + ) 85 + 86 + 87 + @st.composite 88 + def pe_local_write_token(draw, target: int = 0, act_id: int | None = None) -> PELocalWriteToken: 89 + return PELocalWriteToken( 90 + target=target, 91 + act_id=draw(st.integers(min_value=0, max_value=7)) if act_id is None else act_id, 92 + region=draw(st.integers(min_value=0, max_value=1)), 93 + slot=draw(st.integers(min_value=0, max_value=63)), 94 + data=draw(uint16), 95 + is_dest=draw(st.booleans()), 96 + ) 97 + 98 + 99 + @st.composite 100 + def frame_control_token(draw, target: int = 0, act_id: int | None = None) -> FrameControlToken: 101 + return FrameControlToken( 102 + target=target, 103 + act_id=draw(st.integers(min_value=0, max_value=7)) if act_id is None else act_id, 104 + op=draw(st.sampled_from(list(FrameOp))), 105 + payload=draw(uint16), 85 106 ) 86 107 87 108
+593
tests/test_foundation_types.py
··· 1 + """Tests for Phase 1 foundation types: cm_inst.py, tokens.py, and conftest.py updates. 2 + 3 + Verifies: 4 + - pe-frame-redesign.AC2.1: Instruction dataclass and field types 5 + - pe-frame-redesign.AC2.3: CM vs SM type derived from isinstance(opcode, MemOp) 6 + - pe-frame-redesign.AC1.1: PEToken inheritance hierarchy 7 + - pe-frame-redesign.AC1.2: act_id rename and gen removal 8 + - pe-frame-redesign.AC1.3: PELocalWriteToken fields 9 + - pe-frame-redesign.AC1.4: FrameControlToken fields 10 + """ 11 + 12 + import pytest 13 + from hypothesis import given 14 + 15 + from cm_inst import ( 16 + ArithOp, FrameDest, FrameOp, FrameSlotValue, Instruction, LogicOp, 17 + MemOp, OutputStyle, Port, RoutingOp, TokenKind, is_monadic_alu, 18 + ) 19 + from tests.conftest import ( 20 + dyad_token, frame_control_token, monad_token, pe_local_write_token, 21 + ) 22 + from tokens import ( 23 + CMToken, DyadToken, FrameControlToken, MonadToken, PELocalWriteToken, 24 + PEToken, SMToken, Token, 25 + ) 26 + 27 + 28 + # ============================================================================ 29 + # Task 1: New types in cm_inst.py 30 + # ============================================================================ 31 + 32 + class TestInstructionType: 33 + """AC2.1: Instruction dataclass with all field types.""" 34 + 35 + def test_instruction_construction(self): 36 + """Instruction can be constructed with all field types.""" 37 + inst = Instruction( 38 + opcode=ArithOp.ADD, 39 + output=OutputStyle.INHERIT, 40 + has_const=False, 41 + dest_count=2, 42 + wide=False, 43 + fref=0, 44 + ) 45 + assert inst.opcode == ArithOp.ADD 46 + assert inst.output == OutputStyle.INHERIT 47 + assert inst.has_const is False 48 + assert inst.dest_count == 2 49 + assert inst.wide is False 50 + assert inst.fref == 0 51 + 52 + def test_instruction_frozen(self): 53 + """Instruction is a frozen dataclass.""" 54 + inst = Instruction( 55 + opcode=ArithOp.ADD, 56 + output=OutputStyle.INHERIT, 57 + has_const=False, 58 + dest_count=2, 59 + wide=False, 60 + fref=0, 61 + ) 62 + with pytest.raises(AttributeError): 63 + inst.fref = 1 # type: ignore 64 + 65 + def test_instruction_with_mem_op(self): 66 + """Instruction can accept MemOp opcodes.""" 67 + inst = Instruction( 68 + opcode=MemOp.READ, 69 + output=OutputStyle.INHERIT, 70 + has_const=False, 71 + dest_count=1, 72 + wide=False, 73 + fref=0, 74 + ) 75 + assert isinstance(inst.opcode, MemOp) 76 + 77 + def test_instruction_with_routing_op(self): 78 + """Instruction can accept RoutingOp opcodes.""" 79 + inst = Instruction( 80 + opcode=RoutingOp.BREQ, 81 + output=OutputStyle.INHERIT, 82 + has_const=True, 83 + dest_count=1, 84 + wide=True, 85 + fref=63, 86 + ) 87 + assert inst.opcode == RoutingOp.BREQ 88 + assert inst.has_const is True 89 + assert inst.wide is True 90 + assert inst.fref == 63 91 + 92 + 93 + class TestInstructionSemantics: 94 + """AC2.3: CM vs SM type derived from isinstance(opcode, MemOp).""" 95 + 96 + def test_cm_type_from_alu_opcode(self): 97 + """CM instruction can be identified by ALUOp opcode.""" 98 + inst = Instruction( 99 + opcode=ArithOp.ADD, 100 + output=OutputStyle.INHERIT, 101 + has_const=False, 102 + dest_count=2, 103 + wide=False, 104 + fref=0, 105 + ) 106 + # CM type: opcode is NOT MemOp 107 + assert not isinstance(inst.opcode, MemOp) 108 + 109 + def test_sm_type_from_memop_opcode(self): 110 + """SM instruction can be identified by MemOp opcode.""" 111 + inst = Instruction( 112 + opcode=MemOp.READ, 113 + output=OutputStyle.INHERIT, 114 + has_const=False, 115 + dest_count=1, 116 + wide=False, 117 + fref=0, 118 + ) 119 + # SM type: opcode IS MemOp 120 + assert isinstance(inst.opcode, MemOp) 121 + 122 + 123 + class TestOutputStyle: 124 + """OutputStyle enum with correct values.""" 125 + 126 + def test_output_style_values(self): 127 + """OutputStyle enum has correct values.""" 128 + assert OutputStyle.INHERIT.value == 0 129 + assert OutputStyle.CHANGE_TAG.value == 1 130 + assert OutputStyle.SINK.value == 2 131 + 132 + def test_output_style_instances(self): 133 + """Can construct Instructions with all OutputStyle values.""" 134 + for style in OutputStyle: 135 + inst = Instruction( 136 + opcode=ArithOp.ADD, 137 + output=style, 138 + has_const=False, 139 + dest_count=1, 140 + wide=False, 141 + fref=0, 142 + ) 143 + assert inst.output == style 144 + 145 + 146 + class TestTokenKind: 147 + """TokenKind enum with correct values.""" 148 + 149 + def test_token_kind_values(self): 150 + """TokenKind enum has correct values.""" 151 + assert TokenKind.DYADIC.value == 0 152 + assert TokenKind.MONADIC.value == 1 153 + assert TokenKind.INLINE.value == 2 154 + 155 + 156 + class TestFrameOp: 157 + """FrameOp enum with correct values.""" 158 + 159 + def test_frame_op_values(self): 160 + """FrameOp enum has correct values.""" 161 + assert FrameOp.ALLOC.value == 0 162 + assert FrameOp.FREE.value == 1 163 + 164 + 165 + class TestFrameDest: 166 + """FrameDest dataclass with correct fields.""" 167 + 168 + def test_frame_dest_construction(self): 169 + """FrameDest can be constructed with all fields.""" 170 + dest = FrameDest( 171 + target_pe=1, 172 + offset=16, 173 + act_id=3, 174 + port=Port.L, 175 + token_kind=TokenKind.DYADIC, 176 + ) 177 + assert dest.target_pe == 1 178 + assert dest.offset == 16 179 + assert dest.act_id == 3 180 + assert dest.port == Port.L 181 + assert dest.token_kind == TokenKind.DYADIC 182 + 183 + def test_frame_dest_frozen(self): 184 + """FrameDest is a frozen dataclass.""" 185 + dest = FrameDest( 186 + target_pe=1, 187 + offset=16, 188 + act_id=3, 189 + port=Port.L, 190 + token_kind=TokenKind.DYADIC, 191 + ) 192 + with pytest.raises(AttributeError): 193 + dest.offset = 20 # type: ignore 194 + 195 + 196 + class TestFrameSlotValue: 197 + """FrameSlotValue type alias covers all valid types.""" 198 + 199 + def test_frame_slot_value_int(self): 200 + """FrameSlotValue can be an int.""" 201 + val: FrameSlotValue = 42 202 + assert isinstance(val, int) 203 + 204 + def test_frame_slot_value_frame_dest(self): 205 + """FrameSlotValue can be a FrameDest.""" 206 + dest = FrameDest( 207 + target_pe=1, 208 + offset=16, 209 + act_id=3, 210 + port=Port.L, 211 + token_kind=TokenKind.DYADIC, 212 + ) 213 + val: FrameSlotValue = dest 214 + assert isinstance(val, FrameDest) 215 + 216 + def test_frame_slot_value_none(self): 217 + """FrameSlotValue can be None.""" 218 + val: FrameSlotValue = None 219 + assert val is None 220 + 221 + 222 + class TestOpcodeRenumbering: 223 + """Opcodes renumbered to match 5-bit hardware encoding.""" 224 + 225 + def test_arith_op_numbering(self): 226 + """ArithOp values match 5-bit hardware encoding.""" 227 + assert ArithOp.ADD == 0b00000 228 + assert ArithOp.SUB == 0b00001 229 + assert ArithOp.INC == 0b00010 230 + assert ArithOp.DEC == 0b00011 231 + assert ArithOp.SHL == 0b01000 # 8 232 + assert ArithOp.SHR == 0b01001 # 9 233 + assert ArithOp.ASR == 0b01010 # 10 234 + 235 + def test_logic_op_numbering(self): 236 + """LogicOp values match 5-bit hardware encoding.""" 237 + assert LogicOp.AND == 0b00100 # 4 238 + assert LogicOp.OR == 0b00101 # 5 239 + assert LogicOp.XOR == 0b00110 # 6 240 + assert LogicOp.NOT == 0b00111 # 7 241 + assert LogicOp.EQ == 0b01011 # 11 242 + assert LogicOp.LT == 0b01100 # 12 243 + assert LogicOp.LTE == 0b01101 # 13 244 + assert LogicOp.GT == 0b01110 # 14 245 + assert LogicOp.GTE == 0b01111 # 15 246 + 247 + def test_routing_op_numbering(self): 248 + """RoutingOp values match 5-bit hardware encoding.""" 249 + assert RoutingOp.BREQ == 0b10000 # 16 250 + assert RoutingOp.BRGT == 0b10001 # 17 251 + assert RoutingOp.BRGE == 0b10010 # 18 252 + assert RoutingOp.BROF == 0b10011 # 19 253 + assert RoutingOp.SWEQ == 0b10100 # 20 254 + assert RoutingOp.SWGT == 0b10101 # 21 255 + assert RoutingOp.SWGE == 0b10110 # 22 256 + assert RoutingOp.SWOF == 0b10111 # 23 257 + assert RoutingOp.GATE == 0b11000 # 24 258 + assert RoutingOp.SEL == 0b11001 # 25 259 + assert RoutingOp.MRGE == 0b11010 # 26 260 + assert RoutingOp.PASS == 0b11011 # 27 261 + assert RoutingOp.CONST == 0b11100 # 28 262 + assert RoutingOp.FREE_FRAME == 0b11101 # 29 263 + assert RoutingOp.ALLOC_REMOTE == 0b11110 # 30 264 + assert RoutingOp.EXTRACT_TAG == 0b11111 # 31 265 + 266 + def test_all_opcodes_fit_5_bits(self): 267 + """All ALU and MemOp opcodes fit in 5 bits (0-31).""" 268 + for op in ArithOp: 269 + assert 0 <= op <= 31 270 + for op in LogicOp: 271 + assert 0 <= op <= 31 272 + for op in RoutingOp: 273 + assert 0 <= op <= 31 274 + for op in MemOp: 275 + assert 0 <= op <= 31 276 + 277 + 278 + class TestOpcodeChanges: 279 + """Removed/renamed opcodes are no longer available.""" 280 + 281 + def test_old_shift_names_gone(self): 282 + """Old shift opcode names (SHIFT_L, SHIFT_R, ASHFT_R) don't exist.""" 283 + with pytest.raises(AttributeError): 284 + ArithOp.SHIFT_L # type: ignore 285 + with pytest.raises(AttributeError): 286 + ArithOp.SHIFT_R # type: ignore 287 + with pytest.raises(AttributeError): 288 + ArithOp.ASHFT_R # type: ignore 289 + 290 + def test_new_shift_names_exist(self): 291 + """New shift opcode names (SHL, SHR, ASR) exist.""" 292 + assert ArithOp.SHL == 8 293 + assert ArithOp.SHR == 9 294 + assert ArithOp.ASR == 10 295 + 296 + def test_free_ctx_gone(self): 297 + """FREE_CTX doesn't exist (renamed to FREE_FRAME).""" 298 + with pytest.raises(AttributeError): 299 + RoutingOp.FREE_CTX # type: ignore 300 + 301 + def test_free_frame_exists(self): 302 + """FREE_FRAME exists (was FREE_CTX).""" 303 + assert RoutingOp.FREE_FRAME == 29 304 + 305 + def test_alloc_remote_exists(self): 306 + """ALLOC_REMOTE opcode exists.""" 307 + assert RoutingOp.ALLOC_REMOTE == 30 308 + 309 + def test_extract_tag_exists(self): 310 + """EXTRACT_TAG opcode exists.""" 311 + assert RoutingOp.EXTRACT_TAG == 31 312 + 313 + 314 + class TestMonadicOps: 315 + """_MONADIC_*_OPS sets updated for new opcode names.""" 316 + 317 + def test_shift_ops_monadic(self): 318 + """SHL, SHR, ASR are monadic.""" 319 + assert is_monadic_alu(ArithOp.SHL) 320 + assert is_monadic_alu(ArithOp.SHR) 321 + assert is_monadic_alu(ArithOp.ASR) 322 + 323 + def test_free_frame_monadic(self): 324 + """FREE_FRAME is monadic.""" 325 + assert is_monadic_alu(RoutingOp.FREE_FRAME) 326 + 327 + def test_alloc_remote_monadic(self): 328 + """ALLOC_REMOTE is monadic.""" 329 + assert is_monadic_alu(RoutingOp.ALLOC_REMOTE) 330 + 331 + def test_extract_tag_monadic(self): 332 + """EXTRACT_TAG is monadic.""" 333 + assert is_monadic_alu(RoutingOp.EXTRACT_TAG) 334 + 335 + 336 + # ============================================================================ 337 + # Task 2: Token hierarchy 338 + # ============================================================================ 339 + 340 + class TestTokenHierarchy: 341 + """AC1.1: PEToken base class and inheritance structure.""" 342 + 343 + def test_petoken_is_token(self): 344 + """PEToken inherits from Token.""" 345 + assert issubclass(PEToken, Token) 346 + 347 + def test_cmtoken_is_petoken(self): 348 + """CMToken inherits from PEToken.""" 349 + assert issubclass(CMToken, PEToken) 350 + 351 + def test_dyadtoken_is_cmtoken(self): 352 + """DyadToken inherits from CMToken.""" 353 + assert issubclass(DyadToken, CMToken) 354 + 355 + def test_monadtoken_is_cmtoken(self): 356 + """MonadToken inherits from CMToken.""" 357 + assert issubclass(MonadToken, CMToken) 358 + 359 + def test_pe_local_write_is_petoken(self): 360 + """PELocalWriteToken inherits from PEToken.""" 361 + assert issubclass(PELocalWriteToken, PEToken) 362 + 363 + def test_frame_control_is_petoken(self): 364 + """FrameControlToken inherits from PEToken.""" 365 + assert issubclass(FrameControlToken, PEToken) 366 + 367 + def test_smtoken_is_token(self): 368 + """SMToken inherits from Token but NOT PEToken.""" 369 + assert issubclass(SMToken, Token) 370 + assert not issubclass(SMToken, PEToken) 371 + 372 + def test_dyadtoken_instanceof_petoken(self): 373 + """DyadToken instances are PEToken (transitive).""" 374 + token = DyadToken( 375 + target=0, 376 + offset=0, 377 + act_id=0, 378 + data=0, 379 + port=Port.L, 380 + wide=False, 381 + ) 382 + assert isinstance(token, PEToken) 383 + assert isinstance(token, CMToken) 384 + assert isinstance(token, DyadToken) 385 + assert isinstance(token, Token) 386 + 387 + def test_smtoken_not_instanceof_petoken(self): 388 + """SMToken instances are NOT PEToken.""" 389 + token = SMToken( 390 + target=0, 391 + addr=0, 392 + op=MemOp.READ, 393 + flags=None, 394 + data=None, 395 + ret=None, 396 + ) 397 + assert isinstance(token, Token) 398 + assert not isinstance(token, PEToken) 399 + 400 + 401 + class TestCMTokenFields: 402 + """AC1.2: CMToken uses act_id (not ctx), DyadToken has no gen.""" 403 + 404 + def test_cmtoken_has_act_id(self): 405 + """CMToken accepts act_id parameter.""" 406 + token = CMToken( 407 + target=0, 408 + offset=16, 409 + act_id=3, 410 + data=100, 411 + ) 412 + assert token.act_id == 3 413 + 414 + def test_cmtoken_no_ctx(self): 415 + """CMToken doesn't accept ctx parameter.""" 416 + with pytest.raises(TypeError): 417 + CMToken( 418 + target=0, 419 + offset=16, 420 + ctx=3, # type: ignore 421 + data=100, 422 + ) 423 + 424 + def test_dyadtoken_no_gen(self): 425 + """DyadToken doesn't have gen parameter.""" 426 + with pytest.raises(TypeError): 427 + DyadToken( 428 + target=0, 429 + offset=16, 430 + act_id=3, 431 + data=100, 432 + port=Port.L, 433 + gen=0, # type: ignore 434 + wide=False, 435 + ) 436 + 437 + def test_dyadtoken_fields(self): 438 + """DyadToken has correct fields.""" 439 + token = DyadToken( 440 + target=0, 441 + offset=16, 442 + act_id=3, 443 + data=100, 444 + port=Port.R, 445 + wide=True, 446 + ) 447 + assert token.target == 0 448 + assert token.offset == 16 449 + assert token.act_id == 3 450 + assert token.data == 100 451 + assert token.port == Port.R 452 + assert token.wide is True 453 + 454 + 455 + class TestPELocalWriteToken: 456 + """AC1.3: PELocalWriteToken has region, slot, data, is_dest fields.""" 457 + 458 + def test_pe_local_write_fields(self): 459 + """PELocalWriteToken has all required fields.""" 460 + token = PELocalWriteToken( 461 + target=1, 462 + act_id=2, 463 + region=0, 464 + slot=32, 465 + data=500, 466 + is_dest=True, 467 + ) 468 + assert token.target == 1 469 + assert token.act_id == 2 470 + assert token.region == 0 471 + assert token.slot == 32 472 + assert token.data == 500 473 + assert token.is_dest is True 474 + 475 + def test_pe_local_write_frozen(self): 476 + """PELocalWriteToken is frozen.""" 477 + token = PELocalWriteToken( 478 + target=1, 479 + act_id=2, 480 + region=0, 481 + slot=32, 482 + data=500, 483 + is_dest=True, 484 + ) 485 + with pytest.raises(AttributeError): 486 + token.slot = 40 # type: ignore 487 + 488 + 489 + class TestFrameControlToken: 490 + """AC1.4: FrameControlToken has act_id, op, payload fields.""" 491 + 492 + def test_frame_control_fields(self): 493 + """FrameControlToken has all required fields.""" 494 + token = FrameControlToken( 495 + target=1, 496 + act_id=3, 497 + op=FrameOp.ALLOC, 498 + payload=1000, 499 + ) 500 + assert token.target == 1 501 + assert token.act_id == 3 502 + assert token.op == FrameOp.ALLOC 503 + assert token.payload == 1000 504 + 505 + def test_frame_control_frozen(self): 506 + """FrameControlToken is frozen.""" 507 + token = FrameControlToken( 508 + target=1, 509 + act_id=3, 510 + op=FrameOp.FREE, 511 + payload=1000, 512 + ) 513 + with pytest.raises(AttributeError): 514 + token.op = FrameOp.ALLOC # type: ignore 515 + 516 + 517 + class TestIRAMWriteTokenRemoved: 518 + """IRAMWriteToken type removed from tokens.py.""" 519 + 520 + def test_iramwrite_token_does_not_exist(self): 521 + """IRAMWriteToken is no longer available in tokens module.""" 522 + with pytest.raises(ImportError): 523 + from tokens import IRAMWriteToken # type: ignore 524 + 525 + 526 + # ============================================================================ 527 + # Task 3: Hypothesis Strategies 528 + # ============================================================================ 529 + 530 + class TestDyadTokenStrategy: 531 + """dyad_token strategy uses act_id (not ctx), no gen.""" 532 + 533 + @given(dyad_token()) 534 + def test_dyad_token_strategy_generates_valid_tokens(self, token): 535 + """dyad_token strategy generates valid DyadToken instances.""" 536 + assert isinstance(token, DyadToken) 537 + assert hasattr(token, "act_id") 538 + assert 0 <= token.act_id <= 7 539 + assert not hasattr(token, "gen") 540 + 541 + @given(dyad_token(act_id=5)) 542 + def test_dyad_token_strategy_respects_act_id(self, token): 543 + """dyad_token strategy respects act_id parameter.""" 544 + assert token.act_id == 5 545 + 546 + 547 + class TestMonadTokenStrategy: 548 + """monad_token strategy uses act_id (not ctx).""" 549 + 550 + @given(monad_token()) 551 + def test_monad_token_strategy_generates_valid_tokens(self, token): 552 + """monad_token strategy generates valid MonadToken instances.""" 553 + assert isinstance(token, MonadToken) 554 + assert hasattr(token, "act_id") 555 + assert 0 <= token.act_id <= 7 556 + 557 + @given(monad_token(act_id=3)) 558 + def test_monad_token_strategy_respects_act_id(self, token): 559 + """monad_token strategy respects act_id parameter.""" 560 + assert token.act_id == 3 561 + 562 + 563 + class TestPELocalWriteTokenStrategy: 564 + """pe_local_write_token strategy generates valid tokens.""" 565 + 566 + @given(pe_local_write_token()) 567 + def test_pe_local_write_token_strategy_valid(self, token): 568 + """pe_local_write_token strategy generates valid tokens.""" 569 + assert isinstance(token, PELocalWriteToken) 570 + assert 0 <= token.act_id <= 7 571 + assert 0 <= token.region <= 1 572 + assert 0 <= token.slot <= 63 573 + 574 + @given(pe_local_write_token(act_id=4)) 575 + def test_pe_local_write_token_strategy_respects_act_id(self, token): 576 + """pe_local_write_token strategy respects act_id.""" 577 + assert token.act_id == 4 578 + 579 + 580 + class TestFrameControlTokenStrategy: 581 + """frame_control_token strategy generates valid tokens.""" 582 + 583 + @given(frame_control_token()) 584 + def test_frame_control_token_strategy_valid(self, token): 585 + """frame_control_token strategy generates valid tokens.""" 586 + assert isinstance(token, FrameControlToken) 587 + assert 0 <= token.act_id <= 7 588 + assert isinstance(token.op, FrameOp) 589 + 590 + @given(frame_control_token(act_id=2)) 591 + def test_frame_control_token_strategy_respects_act_id(self, token): 592 + """frame_control_token strategy respects act_id.""" 593 + assert token.act_id == 2
+22 -7
tokens.py
··· 1 1 from dataclasses import dataclass 2 2 from typing import Optional 3 3 4 - from cm_inst import ALUInst, MemOp, Port, SMInst 4 + from cm_inst import FrameOp, MemOp, Port 5 5 6 6 7 7 @dataclass(frozen=True) 8 - class Token(object): 8 + class Token: 9 9 target: int 10 10 11 11 12 12 @dataclass(frozen=True) 13 - class CMToken(Token): 13 + class PEToken(Token): 14 + pass 15 + 16 + 17 + @dataclass(frozen=True) 18 + class CMToken(PEToken): 14 19 offset: int 15 - ctx: int 20 + act_id: int 16 21 data: int 17 22 18 23 19 24 @dataclass(frozen=True) 20 25 class DyadToken(CMToken): 21 26 port: Port 22 - gen: int 23 27 wide: bool 24 28 25 29 ··· 29 33 30 34 31 35 @dataclass(frozen=True) 32 - class IRAMWriteToken(CMToken): 33 - instructions: tuple[ALUInst | SMInst, ...] 36 + class PELocalWriteToken(PEToken): 37 + act_id: int 38 + region: int 39 + slot: int 40 + data: int 41 + is_dest: bool 42 + 43 + 44 + @dataclass(frozen=True) 45 + class FrameControlToken(PEToken): 46 + act_id: int 47 + op: FrameOp 48 + payload: int 34 49 35 50 36 51 @dataclass(frozen=True)