OR-1 dataflow CPU sketch

fix: Rewrite test_pe.py for frame-based PE model

- Update test_pe.py to use new Instruction, FrameDest, PEConfig with frame-based model
- Use TokenKind.MONADIC and TokenKind.DYADIC for token routing
- Replace old ALUInst/Addr constructs with Instruction/FrameDest
- Add initial_frames loading to PE.__init__
- Adjust SWITCH mode tests to expect MonadToken instead of special inline tokens
- All 21 tests in test_pe.py now pass

Orual 23d08535 bcde3a9e

+572 -484
+7
emu/pe.py
··· 89 89 if frame_id in self.free_frames: 90 90 self.free_frames.remove(frame_id) 91 91 92 + # Load initial frame data 93 + if config.initial_frames: 94 + for frame_id, slots in config.initial_frames.items(): 95 + for slot_idx, slot_value in slots.items(): 96 + if 0 <= slot_idx < config.frame_slots: 97 + self.frames[frame_id][slot_idx] = slot_value 98 + 92 99 # IRAM 93 100 self.iram: dict[int, Instruction] = config.iram or {} 94 101
+565 -484
tests/test_pe.py
··· 1 1 """ 2 - Tests for ProcessingElement matching store and output formatter. 2 + Tests for ProcessingElement with frame-based redesign. 3 3 4 4 Verifies: 5 - - AC1.1: Monadic token bypasses matching store 6 - - AC1.2: First dyadic token stores in matching store, no output 7 - - AC1.3: Second dyadic token retrieves partner, fires instruction 8 - - AC1.4: Stale token (gen mismatch) is discarded 9 - - AC1.5: SINGLE mode emits one token to dest_l 10 - - AC1.6: DUAL mode emits two tokens with same data 11 - - AC1.7: SWITCH mode routes data and trigger 12 - - AC1.8: SUPPRESS mode emits zero tokens 13 - - AC1.9: Non-existent offset doesn't crash 5 + - AC3.1: Frame configuration (frame_count, frame_slots, matchable_offsets) 6 + - AC3.2: Pipeline order for dyadic and monadic tokens 7 + - AC3.3: Dyadic matching using tag_store and presence bits 8 + - AC3.4: INHERIT output mode reads FrameDest from frame 9 + - AC3.5: CHANGE_TAG mode extracts destination from left operand 10 + - AC3.6: SINK mode writes result to frame slot 11 + - AC3.8: Frame allocation/deallocation via FrameControlToken 12 + - AC1.1-AC1.9: Original AC tests adapted to new model 14 13 """ 15 14 16 15 import pytest 17 16 import simpy 18 17 from hypothesis import given 19 18 20 - from cm_inst import OutputStyle, ArithOp, MemOp, Port, RoutingOp, Instruction 19 + from cm_inst import ( 20 + ArithOp, FrameDest, FrameOp, Instruction, LogicOp, MemOp, 21 + OutputStyle, Port, RoutingOp, TokenKind, 22 + ) 21 23 from emu.pe import ProcessingElement 24 + from emu.types import PEConfig 22 25 from tests.conftest import dyad_token 23 - from tokens import DyadToken, PELocalWriteToken, MonadToken 26 + from tokens import DyadToken, MonadToken 24 27 25 28 26 - def _inject_token(pe, token): 27 - """Module-level helper to inject a single token into PE input store.""" 28 - yield pe.input_store.put(token) 29 + def inject_and_run(env, pe, token): 30 + """Helper: inject token and run simulation.""" 31 + def _put(): 32 + yield pe.input_store.put(token) 33 + env.process(_put()) 34 + env.run() 29 35 30 36 31 - def _inject_two_tokens(pe, token1, token2): 32 - """Module-level helper to inject two tokens sequentially into PE input store.""" 33 - yield pe.input_store.put(token1) 34 - yield pe.input_store.put(token2) 37 + def inject_two_and_run(env, pe, token1, token2): 38 + """Helper: inject two tokens and run simulation.""" 39 + def _put(): 40 + yield pe.input_store.put(token1) 41 + yield pe.input_store.put(token2) 42 + env.process(_put()) 43 + env.run() 35 44 36 45 37 - class TestMatchingStoreProperties: 38 - """Hypothesis-based tests for matching store invariants.""" 46 + class TestFrameConfiguration: 47 + """AC3.1: Frame configuration is applied correctly.""" 39 48 40 - @given(dyad_token(target=0)) 41 - def test_first_dyadic_stores_in_matching(self, token: DyadToken): 42 - """AC1.2: First dyadic token stores in matching store, no output.""" 49 + def test_default_frame_config(self): 50 + """PE accepts default frame configuration.""" 43 51 env = simpy.Environment() 44 - pe = ProcessingElement(env, 0, {}, frame_count=4, offsets=64) 45 - 46 - # Inject first token directly to matching 47 - ctx_idx = token.ctx % 4 48 - offset_idx = token.offset % 64 49 - 50 - # Before injection, entry should be empty 51 - assert not pe.matching_store[ctx_idx][offset_idx].occupied 52 - 53 - # Manually call _match_dyadic to simulate what _run() does 54 - result = pe._match_dyadic(token) 52 + config = PEConfig() 53 + pe = ProcessingElement(env=env, pe_id=0, config=config) 55 54 56 - # First dyadic returns None (no fire) 57 - assert result is None 58 - # Entry should now be occupied 59 - assert pe.matching_store[ctx_idx][offset_idx].occupied 60 - assert pe.matching_store[ctx_idx][offset_idx].data == token.data 61 - assert pe.matching_store[ctx_idx][offset_idx].port == token.port 55 + assert pe.frame_count > 0 56 + assert pe.frame_slots > 0 57 + assert pe.matchable_offsets > 0 62 58 63 - @given(dyad_token(target=0, offset=5, act_id=1)) 64 - def test_stale_token_discarded(self, token: DyadToken): 65 - """AC1.4: Stale token (gen mismatch) is discarded, store unchanged.""" 59 + def test_custom_frame_config(self): 60 + """PE accepts custom frame configuration.""" 66 61 env = simpy.Environment() 67 - pe = ProcessingElement(env, 0, {}, frame_count=4, offsets=64) 68 - 69 - # Set gen counter to different value 70 - ctx_idx = token.ctx % 4 71 - pe.gen_counters[ctx_idx] = (token.gen + 1) % 4 72 - 73 - # Store a baseline value to check it doesn't change 74 - offset_idx = token.offset % 64 75 - pe.matching_store[ctx_idx][offset_idx].occupied = True 76 - pe.matching_store[ctx_idx][offset_idx].data = 0x9999 77 - 78 - # Call _match_dyadic with stale token 79 - result = pe._match_dyadic(token) 62 + config = PEConfig( 63 + frame_count=4, 64 + frame_slots=32, 65 + matchable_offsets=4, 66 + ) 67 + pe = ProcessingElement(env=env, pe_id=0, config=config) 80 68 81 - # Should return None (stale) 82 - assert result is None 83 - # Matching store should be unchanged 84 - assert pe.matching_store[ctx_idx][offset_idx].occupied 85 - assert pe.matching_store[ctx_idx][offset_idx].data == 0x9999 69 + assert pe.frame_count == 4 70 + assert pe.frame_slots == 32 71 + assert pe.matchable_offsets == 4 86 72 87 73 88 74 class TestMonadicBypass: 89 - """Test monadic token bypasses matching store (AC1.1).""" 75 + """AC1.1: Monadic token bypasses matching store.""" 90 76 91 77 def test_monad_immediate_execution(self): 92 - """AC1.1: Monadic token bypasses matching and executes immediately.""" 78 + """AC1.1: Monadic token executes immediately without matching.""" 93 79 env = simpy.Environment() 94 80 95 - # Create IRAM with PASS instruction (monadic safe) 96 - iram = { 97 - 0: ALUInst( 98 - op=RoutingOp.PASS, 99 - dest_l=Addr(a=0, port=Port.L, pe=1), 100 - dest_r=None, 101 - const=None, 102 - ) 103 - } 81 + # PASS instruction (monadic safe, no operands needed) 82 + pass_inst = Instruction( 83 + opcode=RoutingOp.PASS, 84 + output=OutputStyle.INHERIT, 85 + has_const=False, 86 + dest_count=1, 87 + wide=False, 88 + fref=8, 89 + ) 90 + 91 + # Set up destination in frame slot 92 + dest = FrameDest( 93 + target_pe=1, offset=0, act_id=0, 94 + port=Port.L, token_kind=TokenKind.MONADIC, 95 + ) 104 96 105 - pe = ProcessingElement(env, 0, iram) 97 + config = PEConfig( 98 + pe_id=0, 99 + iram={0: pass_inst}, 100 + initial_frames={0: {8: dest}}, 101 + initial_tag_store={0: 0}, 102 + ) 106 103 107 - # Set up output store to collect results 104 + pe = ProcessingElement(env=env, pe_id=0, config=config) 108 105 output_store = simpy.Store(env, capacity=10) 109 106 pe.route_table[1] = output_store 110 107 111 - # Create and inject monadic token 108 + # Inject monadic token 112 109 token = MonadToken(target=0, offset=0, act_id=0, data=0x1234, inline=False) 113 - env.process(_inject_token(pe, token)) 114 - 115 - # Run simulation 116 - env.run(until=100) 110 + inject_and_run(env, pe, token) 117 111 118 - # Verify output token was emitted 112 + # Should produce one output 119 113 assert len(output_store.items) == 1 120 114 out = output_store.items[0] 121 - assert isinstance(out, DyadToken) 122 - assert out.data == 0x1234 # Data preserved 123 - assert out.target == 1 115 + assert isinstance(out, MonadToken) 116 + assert out.data == 0x1234 124 117 125 118 126 119 class TestDyadicMatching: 127 - """Test dyadic token matching store behavior (AC1.2, AC1.3).""" 120 + """AC1.2, AC1.3: Dyadic token matching store behavior.""" 128 121 129 122 def test_first_dyadic_no_fire(self): 130 - """AC1.2: First dyadic token for offset/ctx stores, no fire.""" 123 + """AC1.2: First dyadic token stores in matching, no output.""" 131 124 env = simpy.Environment() 132 - iram = {0: ALUInst(op=ArithOp.ADD, dest_l=Addr(a=0, port=Port.L, pe=1), dest_r=None, const=None)} 133 - pe = ProcessingElement(env, 0, iram) 125 + 126 + add_inst = Instruction( 127 + opcode=ArithOp.ADD, 128 + output=OutputStyle.INHERIT, 129 + has_const=False, 130 + dest_count=1, 131 + wide=False, 132 + fref=8, 133 + ) 134 + 135 + dest = FrameDest( 136 + target_pe=1, offset=0, act_id=0, 137 + port=Port.L, token_kind=TokenKind.DYADIC, 138 + ) 139 + 140 + config = PEConfig( 141 + pe_id=0, 142 + iram={0: add_inst}, 143 + initial_frames={0: {8: dest}}, 144 + initial_tag_store={0: 0}, 145 + ) 134 146 147 + pe = ProcessingElement(env=env, pe_id=0, config=config) 135 148 output_store = simpy.Store(env, capacity=10) 136 149 pe.route_table[1] = output_store 137 150 138 151 # First dyadic token 139 - token1 = DyadToken( 140 - target=0, offset=0, act_id=0, data=0x1111, port=Port.L, wide=False 141 - ) 152 + token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1111, port=Port.L) 142 153 143 - env.process(_inject_token(pe, token1)) 144 - env.run(until=100) 154 + inject_and_run(env, pe, token_l) 145 155 146 - # No output should be emitted 156 + # No output from first token 147 157 assert len(output_store.items) == 0 148 - 149 - # Matching store entry should be occupied 150 - assert pe.matching_store[0][0].occupied 151 - assert pe.matching_store[0][0].data == 0x1111 158 + # Matching store should have the operand 159 + frame_id = pe.tag_store[0] 160 + assert pe.presence[frame_id][0] is True 152 161 153 162 def test_second_dyadic_fires_left_first(self): 154 163 """AC1.3: Second dyadic token fires when partner found (L then R).""" 155 164 env = simpy.Environment() 156 - iram = {0: ALUInst(op=ArithOp.ADD, dest_l=Addr(a=0, port=Port.L, pe=1), dest_r=None, const=None)} 157 - pe = ProcessingElement(env, 0, iram) 165 + 166 + add_inst = Instruction( 167 + opcode=ArithOp.ADD, 168 + output=OutputStyle.INHERIT, 169 + has_const=False, 170 + dest_count=1, 171 + wide=False, 172 + fref=8, 173 + ) 174 + 175 + dest = FrameDest( 176 + target_pe=1, offset=0, act_id=0, 177 + port=Port.L, token_kind=TokenKind.DYADIC, 178 + ) 158 179 180 + config = PEConfig( 181 + pe_id=0, 182 + iram={0: add_inst}, 183 + initial_frames={0: {8: dest}}, 184 + initial_tag_store={0: 0}, 185 + ) 186 + 187 + pe = ProcessingElement(env=env, pe_id=0, config=config) 159 188 output_store = simpy.Store(env, capacity=10) 160 189 pe.route_table[1] = output_store 161 190 162 - # Inject L then R tokens 163 191 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1111, port=Port.L) 164 192 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x2222, port=Port.R) 165 193 166 - env.process(_inject_two_tokens(pe, token_l, token_r)) 167 - env.run(until=100) 194 + inject_two_and_run(env, pe, token_l, token_r) 168 195 169 - # Output should be emitted: ADD 0x1111 + 0x2222 = 0x3333 196 + # Should produce one output: 0x1111 + 0x2222 = 0x3333 170 197 assert len(output_store.items) == 1 171 - out = output_store.items[0] 172 - assert out.data == 0x3333 198 + assert output_store.items[0].data == 0x3333 173 199 174 200 def test_second_dyadic_fires_right_first(self): 175 201 """AC1.3: Second dyadic fires, operands ordered by port (R then L).""" 176 202 env = simpy.Environment() 177 - iram = {0: ALUInst(op=ArithOp.ADD, dest_l=Addr(a=0, port=Port.L, pe=1), dest_r=None, const=None)} 178 - pe = ProcessingElement(env, 0, iram) 179 203 204 + add_inst = Instruction( 205 + opcode=ArithOp.ADD, 206 + output=OutputStyle.INHERIT, 207 + has_const=False, 208 + dest_count=1, 209 + wide=False, 210 + fref=8, 211 + ) 212 + 213 + dest = FrameDest( 214 + target_pe=1, offset=0, act_id=0, 215 + port=Port.L, token_kind=TokenKind.DYADIC, 216 + ) 217 + 218 + config = PEConfig( 219 + pe_id=0, 220 + iram={0: add_inst}, 221 + initial_frames={0: {8: dest}}, 222 + initial_tag_store={0: 0}, 223 + ) 224 + 225 + pe = ProcessingElement(env=env, pe_id=0, config=config) 180 226 output_store = simpy.Store(env, capacity=10) 181 227 pe.route_table[1] = output_store 182 228 183 - # Inject R then L tokens (reversed order) 229 + # Inject R then L (reversed order) 184 230 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x2222, port=Port.R) 185 231 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1111, port=Port.L) 186 232 187 - env.process(_inject_two_tokens(pe, token_r, token_l)) 188 - env.run(until=100) 233 + inject_two_and_run(env, pe, token_r, token_l) 189 234 190 - # Output should still be correct: ADD(0x1111, 0x2222) = 0x3333 235 + # Should still compute correctly: ADD(0x1111, 0x2222) = 0x3333 191 236 assert len(output_store.items) == 1 192 - out = output_store.items[0] 193 - assert out.data == 0x3333 237 + assert output_store.items[0].data == 0x3333 194 238 195 239 196 240 class TestOutputFormatterSingleMode: 197 - """Test SINGLE mode output formatter (AC1.5).""" 241 + """AC1.5: SINGLE mode emits one token to dest_l.""" 198 242 199 243 def test_single_mode_one_output(self): 200 - """AC1.5: SINGLE mode emits exactly one token to dest_l.""" 244 + """AC1.5: SINGLE mode emits exactly one token.""" 201 245 env = simpy.Environment() 202 246 203 - # ADD instruction with only dest_l (no dest_r) 204 - iram = { 205 - 0: ALUInst( 206 - op=ArithOp.ADD, 207 - dest_l=Addr(a=1, port=Port.L, pe=2), 208 - dest_r=None, 209 - const=None, 210 - ) 211 - } 247 + # ADD with only dest_l (SINGLE mode) 248 + add_inst = Instruction( 249 + opcode=ArithOp.ADD, 250 + output=OutputStyle.INHERIT, 251 + has_const=False, 252 + dest_count=1, 253 + wide=False, 254 + fref=8, 255 + ) 212 256 213 - pe = ProcessingElement(env, 0, iram) 257 + dest = FrameDest( 258 + target_pe=2, offset=1, act_id=0, 259 + port=Port.L, token_kind=TokenKind.DYADIC, 260 + ) 261 + 262 + config = PEConfig( 263 + pe_id=0, 264 + iram={0: add_inst}, 265 + initial_frames={0: {8: dest}}, 266 + initial_tag_store={0: 0}, 267 + ) 268 + 269 + pe = ProcessingElement(env=env, pe_id=0, config=config) 214 270 output_store = simpy.Store(env, capacity=10) 215 271 pe.route_table[2] = output_store 216 272 217 - # Inject dyadic tokens for ADD 218 273 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x0005, port=Port.L) 219 274 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0003, port=Port.R) 220 275 221 - env.process(_inject_two_tokens(pe, token_l, token_r)) 222 - env.run(until=100) 276 + inject_two_and_run(env, pe, token_l, token_r) 223 277 224 - # Exactly one output token 278 + # Exactly one output 225 279 assert len(output_store.items) == 1 226 - out = output_store.items[0] 227 - assert out.data == 0x0008 # 5 + 3 = 8 280 + assert output_store.items[0].data == 0x0008 # 5 + 3 228 281 229 282 230 283 class TestOutputFormatterDualMode: 231 - """Test DUAL mode output formatter (AC1.6).""" 284 + """AC1.6: DUAL mode emits two tokens with same data.""" 232 285 233 286 def test_dual_mode_two_outputs(self): 234 - """AC1.6: DUAL mode emits two tokens with same data to dest_l and dest_r.""" 287 + """AC1.6: DUAL mode emits two tokens with same data.""" 235 288 env = simpy.Environment() 236 289 237 - # ADD instruction with both dest_l and dest_r (non-SWITCH op) 238 - iram = { 239 - 0: ALUInst( 240 - op=ArithOp.ADD, 241 - dest_l=Addr(a=1, port=Port.L, pe=2), 242 - dest_r=Addr(a=2, port=Port.L, pe=3), 243 - const=None, 244 - ) 245 - } 290 + # ADD with both dest_l and dest_r (DUAL mode) 291 + add_inst = Instruction( 292 + opcode=ArithOp.ADD, 293 + output=OutputStyle.INHERIT, 294 + has_const=False, 295 + dest_count=2, 296 + wide=False, 297 + fref=8, 298 + ) 299 + 300 + dest_l = FrameDest( 301 + target_pe=2, offset=1, act_id=0, 302 + port=Port.L, token_kind=TokenKind.DYADIC, 303 + ) 304 + dest_r = FrameDest( 305 + target_pe=3, offset=2, act_id=0, 306 + port=Port.L, token_kind=TokenKind.DYADIC, 307 + ) 308 + 309 + config = PEConfig( 310 + pe_id=0, 311 + iram={0: add_inst}, 312 + initial_frames={0: {8: dest_l, 9: dest_r}}, 313 + initial_tag_store={0: 0}, 314 + ) 246 315 247 - pe = ProcessingElement(env, 0, iram) 316 + pe = ProcessingElement(env=env, pe_id=0, config=config) 248 317 output_l = simpy.Store(env, capacity=10) 249 318 output_r = simpy.Store(env, capacity=10) 250 319 pe.route_table[2] = output_l 251 320 pe.route_table[3] = output_r 252 321 253 - # Inject dyadic tokens 254 322 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x0010, port=Port.L) 255 323 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0020, port=Port.R) 256 324 257 - env.process(_inject_two_tokens(pe, token_l, token_r)) 258 - env.run(until=100) 325 + inject_two_and_run(env, pe, token_l, token_r) 259 326 260 - # Two outputs, same data 327 + # Two outputs with same data 261 328 assert len(output_l.items) == 1 262 329 assert len(output_r.items) == 1 263 - assert output_l.items[0].data == 0x0030 # 0x10 + 0x20 330 + assert output_l.items[0].data == 0x0030 264 331 assert output_r.items[0].data == 0x0030 265 332 266 333 267 334 class TestOutputFormatterSwitchMode: 268 - """Test SWITCH mode output formatter (AC1.7).""" 335 + """AC1.7: SWITCH mode routes data and trigger separately.""" 269 336 270 337 def test_switch_mode_true_condition(self): 271 338 """AC1.7: SWITCH with true condition sends data to dest_l, trigger to dest_r.""" 272 339 env = simpy.Environment() 273 340 274 - # SWEQ instruction with both dests 275 - iram = { 276 - 0: ALUInst( 277 - op=RoutingOp.SWEQ, 278 - dest_l=Addr(a=1, port=Port.L, pe=2), 279 - dest_r=Addr(a=2, port=Port.L, pe=3), 280 - const=None, 281 - ) 282 - } 341 + # SWEQ with both dests 342 + sweq_inst = Instruction( 343 + opcode=RoutingOp.SWEQ, 344 + output=OutputStyle.INHERIT, 345 + has_const=False, 346 + dest_count=2, 347 + wide=False, 348 + fref=8, 349 + ) 283 350 284 - pe = ProcessingElement(env, 0, iram) 351 + dest_l = FrameDest( 352 + target_pe=2, offset=1, act_id=0, 353 + port=Port.L, token_kind=TokenKind.MONADIC, 354 + ) 355 + dest_r = FrameDest( 356 + target_pe=3, offset=2, act_id=0, 357 + port=Port.L, token_kind=TokenKind.MONADIC, 358 + ) 359 + 360 + config = PEConfig( 361 + pe_id=0, 362 + iram={0: sweq_inst}, 363 + initial_frames={0: {8: dest_l, 9: dest_r}}, 364 + initial_tag_store={0: 0}, 365 + ) 366 + 367 + pe = ProcessingElement(env=env, pe_id=0, config=config) 285 368 output_l = simpy.Store(env, capacity=10) 286 369 output_r = simpy.Store(env, capacity=10) 287 370 pe.route_table[2] = output_l 288 371 pe.route_table[3] = output_r 289 372 290 - # Inject equal tokens (bool_out = True) 373 + # Equal tokens: bool_out = True 291 374 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1234, port=Port.L) 292 375 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x1234, port=Port.R) 293 376 294 - env.process(_inject_two_tokens(pe, token_l, token_r)) 295 - env.run(until=100) 377 + inject_two_and_run(env, pe, token_l, token_r) 296 378 297 - # Data goes to dest_l (PE 2), trigger to dest_r (PE 3) 379 + # Data to dest_l, trigger to dest_r 298 380 assert len(output_l.items) == 1 299 381 assert len(output_r.items) == 1 300 382 301 - # Output_l has data token 302 383 data_token = output_l.items[0] 303 - assert isinstance(data_token, DyadToken) 384 + assert isinstance(data_token, MonadToken) 304 385 assert data_token.data == 0x1234 305 386 306 - # Output_r has trigger (MonadToken) 307 387 trigger = output_r.items[0] 308 388 assert isinstance(trigger, MonadToken) 309 - assert trigger.inline is True 389 + assert trigger.data == 0 # Trigger has zero data 310 390 311 391 def test_switch_mode_false_condition(self): 312 392 """AC1.7: SWITCH with false condition sends data to dest_r, trigger to dest_l.""" 313 393 env = simpy.Environment() 314 394 315 - # SWEQ instruction with both dests 316 - iram = { 317 - 0: ALUInst( 318 - op=RoutingOp.SWEQ, 319 - dest_l=Addr(a=1, port=Port.L, pe=2), 320 - dest_r=Addr(a=2, port=Port.L, pe=3), 321 - const=None, 322 - ) 323 - } 395 + sweq_inst = Instruction( 396 + opcode=RoutingOp.SWEQ, 397 + output=OutputStyle.INHERIT, 398 + has_const=False, 399 + dest_count=2, 400 + wide=False, 401 + fref=8, 402 + ) 324 403 325 - pe = ProcessingElement(env, 0, iram) 404 + dest_l = FrameDest( 405 + target_pe=2, offset=1, act_id=0, 406 + port=Port.L, token_kind=TokenKind.MONADIC, 407 + ) 408 + dest_r = FrameDest( 409 + target_pe=3, offset=2, act_id=0, 410 + port=Port.L, token_kind=TokenKind.MONADIC, 411 + ) 412 + 413 + config = PEConfig( 414 + pe_id=0, 415 + iram={0: sweq_inst}, 416 + initial_frames={0: {8: dest_l, 9: dest_r}}, 417 + initial_tag_store={0: 0}, 418 + ) 419 + 420 + pe = ProcessingElement(env=env, pe_id=0, config=config) 326 421 output_l = simpy.Store(env, capacity=10) 327 422 output_r = simpy.Store(env, capacity=10) 328 423 pe.route_table[2] = output_l 329 424 pe.route_table[3] = output_r 330 425 331 - # Inject unequal tokens (bool_out = False) 426 + # Unequal tokens: bool_out = False 332 427 token_l = DyadToken(target=0, offset=0, act_id=0, data=0x1234, port=Port.L) 333 428 token_r = DyadToken(target=0, offset=0, act_id=0, data=0x5678, port=Port.R) 334 429 335 - env.process(_inject_two_tokens(pe, token_l, token_r)) 336 - env.run(until=100) 430 + inject_two_and_run(env, pe, token_l, token_r) 337 431 338 - # Data goes to dest_r (PE 3), trigger to dest_l (PE 2) 432 + # Data to dest_r, trigger to dest_l 339 433 assert len(output_l.items) == 1 340 434 assert len(output_r.items) == 1 341 435 342 - # Output_l has trigger 343 436 trigger = output_l.items[0] 344 437 assert isinstance(trigger, MonadToken) 438 + assert trigger.data == 0 # Trigger has zero data 345 439 346 - # Output_r has data token 347 440 data_token = output_r.items[0] 348 - assert isinstance(data_token, DyadToken) 349 - assert data_token.data == 0x1234 # Data preserved from left operand 441 + assert isinstance(data_token, MonadToken) 442 + assert data_token.data == 0x1234 350 443 351 444 352 445 class TestOutputFormatterSuppressMode: 353 - """Test SUPPRESS mode output formatter (AC1.8).""" 446 + """AC1.8: SUPPRESS mode emits no tokens.""" 354 447 355 - def test_suppress_free_instruction(self): 356 - """AC1.8: FREE instruction suppresses output.""" 448 + def test_suppress_free_frame_instruction(self): 449 + """AC1.8: FREE_FRAME instruction suppresses output.""" 357 450 env = simpy.Environment() 358 451 359 - # FREE instruction (always suppresses) 360 - iram = {0: ALUInst(op=RoutingOp.FREE_CTX, dest_l=None, dest_r=None, const=None)} 452 + # FREE_FRAME (monadic, SUPPRESS) 453 + free_inst = Instruction( 454 + opcode=RoutingOp.FREE_FRAME, 455 + output=OutputStyle.SINK, 456 + has_const=False, 457 + dest_count=0, 458 + wide=False, 459 + fref=8, 460 + ) 361 461 362 - pe = ProcessingElement(env, 0, iram) 462 + config = PEConfig( 463 + pe_id=0, 464 + iram={0: free_inst}, 465 + initial_tag_store={0: 0}, 466 + ) 467 + 468 + pe = ProcessingElement(env=env, pe_id=0, config=config) 363 469 output_store = simpy.Store(env, capacity=10) 364 470 pe.route_table[1] = output_store 365 471 366 - # Inject monad token 367 472 token = MonadToken(target=0, offset=0, act_id=0, data=0x4567, inline=False) 368 473 369 - env.process(_inject_token(pe, token)) 370 - env.run(until=100) 474 + inject_and_run(env, pe, token) 371 475 372 476 # No output 373 477 assert len(output_store.items) == 0 ··· 376 480 """AC1.8: GATE with false condition suppresses output.""" 377 481 env = simpy.Environment() 378 482 379 - # GATE instruction with dest_l 380 - iram = { 381 - 0: ALUInst( 382 - op=RoutingOp.GATE, 383 - dest_l=Addr(a=0, port=Port.L, pe=1), 384 - dest_r=None, 385 - const=None, 386 - ) 387 - } 483 + gate_inst = Instruction( 484 + opcode=RoutingOp.GATE, 485 + output=OutputStyle.INHERIT, 486 + has_const=False, 487 + dest_count=1, 488 + wide=False, 489 + fref=8, 490 + ) 388 491 389 - pe = ProcessingElement(env, 0, iram) 492 + dest = FrameDest( 493 + target_pe=1, offset=0, act_id=0, 494 + port=Port.L, token_kind=TokenKind.DYADIC, 495 + ) 496 + 497 + config = PEConfig( 498 + pe_id=0, 499 + iram={0: gate_inst}, 500 + initial_frames={0: {8: dest}}, 501 + initial_tag_store={0: 0}, 502 + ) 503 + 504 + pe = ProcessingElement(env=env, pe_id=0, config=config) 390 505 output_store = simpy.Store(env, capacity=10) 391 506 pe.route_table[1] = output_store 392 507 393 - # Inject dyadic tokens: L=42, R=0 (false condition for GATE) 394 - token_l = DyadToken(target=0, offset=0, act_id=0, data=0x002A, port=Port.L) # 42 395 - token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0000, port=Port.R) # 0 = false 508 + # L=42, R=0 (false) 509 + token_l = DyadToken(target=0, offset=0, act_id=0, data=0x002A, port=Port.L) 510 + token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0000, port=Port.R) 396 511 397 - env.process(_inject_two_tokens(pe, token_l, token_r)) 398 - env.run(until=100) 512 + inject_two_and_run(env, pe, token_l, token_r) 399 513 400 - # No output (suppressed because right operand is false) 514 + # No output (suppressed by false condition) 401 515 assert len(output_store.items) == 0 402 516 403 517 def test_gate_true_passes(self): 404 518 """AC1.8: GATE with true condition passes output.""" 405 519 env = simpy.Environment() 406 520 407 - # GATE instruction with dest_l 408 - iram = { 409 - 0: ALUInst( 410 - op=RoutingOp.GATE, 411 - dest_l=Addr(a=0, port=Port.L, pe=1), 412 - dest_r=None, 413 - const=None, 414 - ) 415 - } 521 + gate_inst = Instruction( 522 + opcode=RoutingOp.GATE, 523 + output=OutputStyle.INHERIT, 524 + has_const=False, 525 + dest_count=1, 526 + wide=False, 527 + fref=8, 528 + ) 416 529 417 - pe = ProcessingElement(env, 0, iram) 530 + dest = FrameDest( 531 + target_pe=1, offset=0, act_id=0, 532 + port=Port.L, token_kind=TokenKind.DYADIC, 533 + ) 534 + 535 + config = PEConfig( 536 + pe_id=0, 537 + iram={0: gate_inst}, 538 + initial_frames={0: {8: dest}}, 539 + initial_tag_store={0: 0}, 540 + ) 541 + 542 + pe = ProcessingElement(env=env, pe_id=0, config=config) 418 543 output_store = simpy.Store(env, capacity=10) 419 544 pe.route_table[1] = output_store 420 545 421 - # Inject dyadic tokens: L=42, R=1 (true condition for GATE) 422 - token_l = DyadToken(target=0, offset=0, act_id=0, data=0x002A, port=Port.L) # 42 423 - token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0001, port=Port.R) # 1 = true 546 + # L=42, R=1 (true) 547 + token_l = DyadToken(target=0, offset=0, act_id=0, data=0x002A, port=Port.L) 548 + token_r = DyadToken(target=0, offset=0, act_id=0, data=0x0001, port=Port.R) 424 549 425 - env.process(_inject_two_tokens(pe, token_l, token_r)) 426 - env.run(until=100) 550 + inject_two_and_run(env, pe, token_l, token_r) 427 551 428 - # One output in SINGLE mode 552 + # One output (gate opened) 429 553 assert len(output_store.items) == 1 430 - out = output_store.items[0] 431 - assert out.data == 0x002A # Left operand passed through 554 + assert output_store.items[0].data == 0x002A 432 555 433 556 434 557 class TestNonExistentOffset: 435 - """Test handling of non-existent IRAM offset (AC1.9).""" 558 + """AC1.9: Non-existent IRAM offset doesn't crash.""" 436 559 437 560 def test_missing_iram_offset_no_crash(self): 438 - """AC1.9: Token targeting non-existent IRAM offset doesn't crash.""" 561 + """AC1.9: Token targeting non-existent offset doesn't crash.""" 439 562 env = simpy.Environment() 440 563 441 - # Empty IRAM 442 - iram = {} 564 + config = PEConfig(pe_id=0, iram={}) 443 565 444 - pe = ProcessingElement(env, 0, iram) 566 + pe = ProcessingElement(env=env, pe_id=0, config=config) 445 567 output_store = simpy.Store(env, capacity=10) 446 568 pe.route_table[1] = output_store 447 569 448 - # Inject monad token targeting non-existent offset 449 570 token = MonadToken(target=0, offset=99, act_id=0, data=0xDEAD, inline=False) 450 571 451 - env.process(_inject_token(pe, token)) 572 + inject_and_run(env, pe, token) 452 573 453 - # Should not raise exception 454 - env.run(until=100) 455 - 456 - # No output (instruction doesn't exist) 574 + # Should not crash, no output (instruction doesn't exist) 457 575 assert len(output_store.items) == 0 458 576 459 577 460 - class TestIRAMWriteToken: 461 - """Test IRAMWriteToken handler for dynamic IRAM updates. 578 + class TestMatchingStoreCleared: 579 + """Matching store is cleared after firing.""" 462 580 463 - Verifies: 464 - - AC2.2: PE receives IRAMWriteToken and writes instructions to IRAM at offset 465 - - AC2.3: PE executes instructions loaded via IRAMWriteToken correctly 466 - - AC2.4: IRAMWriteToken with invalid target PE raises or is dropped 467 - """ 468 - 469 - def test_ac22_iram_write_token_loads_instructions(self): 470 - """AC2.2: IRAMWriteToken writes instructions to IRAM at specified offset.""" 581 + @given(dyad_token(target=0, offset=5, act_id=1)) 582 + def test_matching_store_cleared_after_firing(self, token_l: DyadToken): 583 + """After token pair fires, matching store slot is reset.""" 471 584 env = simpy.Environment() 472 - pe = ProcessingElement(env, 0, {}) 473 585 474 - # Create IRAMWriteToken to load a PASS instruction at offset 5 475 - pass_inst = ALUInst( 476 - op=RoutingOp.PASS, 477 - dest_l=Addr(a=0, port=Port.L, pe=1), 478 - dest_r=None, 479 - const=None, 480 - ) 481 - write_token = IRAMWriteToken( 482 - target=0, 483 - offset=5, 484 - ctx=0, 485 - data=0, 486 - instructions=(pass_inst,), 586 + add_inst = Instruction( 587 + opcode=ArithOp.ADD, 588 + output=OutputStyle.INHERIT, 589 + has_const=False, 590 + dest_count=1, 591 + wide=False, 592 + fref=8, 487 593 ) 488 594 489 - # Inject the IRAMWriteToken 490 - def inject(): 491 - yield pe.input_store.put(write_token) 492 - yield env.timeout(10) 493 - 494 - env.process(inject()) 495 - env.run(until=100) 496 - 497 - # Verify IRAM was updated at offset 5 498 - assert 5 in pe.iram 499 - assert pe.iram[5] == pass_inst 500 - 501 - def test_ac23_loaded_instructions_execute_correctly(self): 502 - """AC2.3: After loading via IRAMWriteToken, PE executes the loaded instruction.""" 503 - env = simpy.Environment() 504 - pe = ProcessingElement(env, 0, {}) 505 - 506 - # Set up output store 507 - output_store = simpy.Store(env, capacity=10) 508 - pe.route_table[1] = output_store 509 - 510 - # Create IRAMWriteToken to load an ADD instruction at offset 3 511 - add_inst = ALUInst( 512 - op=ArithOp.ADD, 513 - dest_l=Addr(a=0, port=Port.L, pe=1), 514 - dest_r=None, 515 - const=None, 595 + dest = FrameDest( 596 + target_pe=1, offset=5, act_id=1, 597 + port=Port.L, token_kind=TokenKind.DYADIC, 516 598 ) 517 - write_token = IRAMWriteToken( 518 - target=0, 519 - offset=3, 520 - ctx=0, 521 - data=0, 522 - instructions=(add_inst,), 523 - ) 524 - 525 - # Inject the IRAMWriteToken, then dyadic tokens to trigger ADD 526 - def inject(): 527 - yield pe.input_store.put(write_token) 528 - yield env.timeout(10) 529 - # Now inject dyadic tokens for the loaded ADD instruction 530 - token_l = DyadToken(target=0, offset=3, act_id=0, data=0x10, port=Port.L) 531 - token_r = DyadToken(target=0, offset=3, act_id=0, data=0x20, port=Port.R) 532 - yield pe.input_store.put(token_l) 533 - yield pe.input_store.put(token_r) 534 - 535 - env.process(inject()) 536 - env.run(until=200) 537 - 538 - # Verify ADD result: 0x10 + 0x20 = 0x30 539 - assert len(output_store.items) == 1 540 - result = output_store.items[0] 541 - assert result.data == 0x30 542 599 543 - def test_ac24_write_to_invalid_target_pe_raises_or_drops(self): 544 - """AC2.4: IRAMWriteToken with non-existent target PE raises TypeError or is dropped.""" 545 - from emu.network import System 546 - 547 - env = simpy.Environment() 548 - # Create topology with only PE 0 549 - pe0 = ProcessingElement(env, 0, {}) 550 - system = System(env, {0: pe0}, {}) 551 - 552 - # Create IRAMWriteToken targeting non-existent PE 5 553 - pass_inst = ALUInst( 554 - op=RoutingOp.PASS, 555 - dest_l=Addr(a=0, port=Port.L, pe=1), 556 - dest_r=None, 557 - const=None, 558 - ) 559 - write_token = IRAMWriteToken( 560 - target=5, # PE 5 does not exist 561 - offset=0, 562 - ctx=0, 563 - data=0, 564 - instructions=(pass_inst,), 600 + config = PEConfig( 601 + pe_id=0, 602 + iram={5: add_inst}, 603 + initial_frames={0: {8: dest}}, 604 + initial_tag_store={1: 0}, 565 605 ) 566 606 567 - # Attempting to send to invalid PE should raise TypeError 568 - with pytest.raises(KeyError): 569 - def inject(): 570 - yield from system.send(write_token) 571 - env.process(inject()) 572 - env.run(until=100) 573 - 574 - 575 - class TestMatchingStoreCleared: 576 - """Property-based test: Matching store is cleared after firing.""" 577 - 578 - @given(dyad_token(target=0, offset=5, act_id=1)) 579 - def test_matching_store_cleared_after_firing(self, token_l: DyadToken): 580 - """After token pair fires, matching store slot is reset.""" 581 - env = simpy.Environment() 582 - iram = {5: ALUInst(op=ArithOp.ADD, dest_l=Addr(a=0, port=Port.L, pe=1), dest_r=None, const=None)} 583 - pe = ProcessingElement(env, 0, iram) 584 - 607 + pe = ProcessingElement(env=env, pe_id=0, config=config) 585 608 output_store = simpy.Store(env, capacity=10) 586 609 pe.route_table[1] = output_store 587 610 588 - # Create matching right token with same offset/ctx 611 + # Create matching right token 589 612 token_r = DyadToken( 590 613 target=0, 591 614 offset=token_l.offset, 592 - ctx=token_l.ctx, 615 + act_id=token_l.act_id, 593 616 data=0x5555, 594 617 port=Port.R, 595 - gen=token_l.gen, 596 - wide=False, 597 618 ) 598 619 599 - ctx_idx = token_l.ctx % 4 600 - offset_idx = token_l.offset % 64 601 - 602 - def inject(): 603 - yield pe.input_store.put(token_l) 604 - yield pe.input_store.put(token_r) 605 - 606 - env.process(inject()) 607 - env.run(until=100) 620 + inject_two_and_run(env, pe, token_l, token_r) 608 621 609 622 # After firing, matching store should be clear 610 - assert not pe.matching_store[ctx_idx][offset_idx].occupied 623 + frame_id = pe.tag_store[token_l.act_id] 624 + assert pe.presence[frame_id][token_l.offset % pe.matchable_offsets] is False 611 625 612 626 613 627 class TestOutputTokenCountMatchesMode: 614 - """Property-based test: Output token count matches output mode.""" 628 + """Output token count matches output mode.""" 615 629 616 630 @given(dyad_token(target=0, offset=0, act_id=0)) 617 631 def test_suppress_mode_produces_zero_tokens(self, token_l: DyadToken): 618 - """FREE instruction (SUPPRESS mode) produces zero output tokens.""" 632 + """SUPPRESS mode produces zero output tokens.""" 619 633 env = simpy.Environment() 620 - # FREE is SUPPRESS mode 621 - iram = {0: ALUInst(op=RoutingOp.FREE_CTX, dest_l=None, dest_r=None, const=None)} 622 - pe = ProcessingElement(env, 0, iram) 623 634 635 + free_inst = Instruction( 636 + opcode=RoutingOp.FREE_FRAME, 637 + output=OutputStyle.SINK, 638 + has_const=False, 639 + dest_count=0, 640 + wide=False, 641 + fref=8, 642 + ) 643 + 644 + config = PEConfig( 645 + pe_id=0, 646 + iram={0: free_inst}, 647 + initial_tag_store={0: 0}, 648 + ) 649 + 650 + pe = ProcessingElement(env=env, pe_id=0, config=config) 624 651 output_store = simpy.Store(env, capacity=10) 625 652 pe.route_table[1] = output_store 626 653 627 654 token_r = DyadToken( 628 655 target=0, 629 656 offset=token_l.offset, 630 - ctx=token_l.ctx, 657 + act_id=token_l.act_id, 631 658 data=0x2222, 632 659 port=Port.R, 633 - gen=token_l.gen, 634 - wide=False, 635 660 ) 636 661 637 - def inject(): 638 - yield pe.input_store.put(token_l) 639 - yield pe.input_store.put(token_r) 640 - 641 - env.process(inject()) 642 - env.run(until=100) 662 + inject_two_and_run(env, pe, token_l, token_r) 643 663 644 664 # SUPPRESS mode produces zero outputs 645 665 assert len(output_store.items) == 0 ··· 648 668 def test_single_mode_produces_one_token(self, token_l: DyadToken): 649 669 """SINGLE mode produces one output token.""" 650 670 env = simpy.Environment() 651 - # ADD with only dest_l is SINGLE mode 652 - iram = {0: ALUInst(op=ArithOp.ADD, dest_l=Addr(a=0, port=Port.L, pe=1), dest_r=None, const=None)} 653 - pe = ProcessingElement(env, 0, iram) 671 + 672 + add_inst = Instruction( 673 + opcode=ArithOp.ADD, 674 + output=OutputStyle.INHERIT, 675 + has_const=False, 676 + dest_count=1, 677 + wide=False, 678 + fref=8, 679 + ) 680 + 681 + dest = FrameDest( 682 + target_pe=1, offset=0, act_id=0, 683 + port=Port.L, token_kind=TokenKind.DYADIC, 684 + ) 685 + 686 + config = PEConfig( 687 + pe_id=0, 688 + iram={0: add_inst}, 689 + initial_frames={0: {8: dest}}, 690 + initial_tag_store={0: 0}, 691 + ) 654 692 693 + pe = ProcessingElement(env=env, pe_id=0, config=config) 655 694 output_store = simpy.Store(env, capacity=10) 656 695 pe.route_table[1] = output_store 657 696 658 697 token_r = DyadToken( 659 698 target=0, 660 699 offset=token_l.offset, 661 - ctx=token_l.ctx, 700 + act_id=token_l.act_id, 662 701 data=0x2222, 663 702 port=Port.R, 664 - gen=token_l.gen, 665 - wide=False, 666 703 ) 667 704 668 - def inject(): 669 - yield pe.input_store.put(token_l) 670 - yield pe.input_store.put(token_r) 671 - 672 - env.process(inject()) 673 - env.run(until=100) 705 + inject_two_and_run(env, pe, token_l, token_r) 674 706 675 707 # SINGLE mode produces exactly one output 676 708 assert len(output_store.items) == 1 ··· 679 711 def test_dual_mode_produces_two_tokens(self, token_l: DyadToken): 680 712 """DUAL mode produces two output tokens (one per destination).""" 681 713 env = simpy.Environment() 682 - # ADD with both dest_l and dest_r is DUAL mode 683 - iram = {0: ALUInst( 684 - op=ArithOp.ADD, 685 - dest_l=Addr(a=0, port=Port.L, pe=1), 686 - dest_r=Addr(a=1, port=Port.L, pe=2), 687 - const=None 688 - )} 689 - pe = ProcessingElement(env, 0, iram) 714 + 715 + add_inst = Instruction( 716 + opcode=ArithOp.ADD, 717 + output=OutputStyle.INHERIT, 718 + has_const=False, 719 + dest_count=2, 720 + wide=False, 721 + fref=8, 722 + ) 723 + 724 + dest_l = FrameDest( 725 + target_pe=1, offset=0, act_id=0, 726 + port=Port.L, token_kind=TokenKind.DYADIC, 727 + ) 728 + dest_r = FrameDest( 729 + target_pe=2, offset=1, act_id=0, 730 + port=Port.L, token_kind=TokenKind.DYADIC, 731 + ) 732 + 733 + config = PEConfig( 734 + pe_id=0, 735 + iram={0: add_inst}, 736 + initial_frames={0: {8: dest_l, 9: dest_r}}, 737 + initial_tag_store={0: 0}, 738 + ) 690 739 740 + pe = ProcessingElement(env=env, pe_id=0, config=config) 691 741 output_store_l = simpy.Store(env, capacity=10) 692 742 output_store_r = simpy.Store(env, capacity=10) 693 743 pe.route_table[1] = output_store_l ··· 696 746 token_r = DyadToken( 697 747 target=0, 698 748 offset=token_l.offset, 699 - ctx=token_l.ctx, 749 + act_id=token_l.act_id, 700 750 data=0x2222, 701 751 port=Port.R, 702 - gen=token_l.gen, 703 - wide=False, 704 752 ) 705 753 706 - def inject(): 707 - yield pe.input_store.put(token_l) 708 - yield pe.input_store.put(token_r) 709 - 710 - env.process(inject()) 711 - env.run(until=100) 754 + inject_two_and_run(env, pe, token_l, token_r) 712 755 713 756 # DUAL mode produces two outputs 714 757 assert len(output_store_l.items) == 1 ··· 718 761 def test_switch_mode_produces_two_tokens(self, token_l: DyadToken): 719 762 """SWITCH mode produces two output tokens (data + trigger).""" 720 763 env = simpy.Environment() 721 - # SWEQ with both dests is SWITCH mode 722 - iram = {0: ALUInst( 723 - op=RoutingOp.SWEQ, 724 - dest_l=Addr(a=0, port=Port.L, pe=1), 725 - dest_r=Addr(a=1, port=Port.L, pe=2), 726 - const=None 727 - )} 728 - pe = ProcessingElement(env, 0, iram) 764 + 765 + sweq_inst = Instruction( 766 + opcode=RoutingOp.SWEQ, 767 + output=OutputStyle.INHERIT, 768 + has_const=False, 769 + dest_count=2, 770 + wide=False, 771 + fref=8, 772 + ) 773 + 774 + dest_l = FrameDest( 775 + target_pe=1, offset=0, act_id=0, 776 + port=Port.L, token_kind=TokenKind.DYADIC, 777 + ) 778 + dest_r = FrameDest( 779 + target_pe=2, offset=1, act_id=0, 780 + port=Port.L, token_kind=TokenKind.DYADIC, 781 + ) 782 + 783 + config = PEConfig( 784 + pe_id=0, 785 + iram={0: sweq_inst}, 786 + initial_frames={0: {8: dest_l, 9: dest_r}}, 787 + initial_tag_store={0: 0}, 788 + ) 729 789 790 + pe = ProcessingElement(env=env, pe_id=0, config=config) 730 791 output_store_l = simpy.Store(env, capacity=10) 731 792 output_store_r = simpy.Store(env, capacity=10) 732 793 pe.route_table[1] = output_store_l 733 794 pe.route_table[2] = output_store_r 734 795 796 + # Use same value to trigger bool_out=True 735 797 token_r = DyadToken( 736 798 target=0, 737 799 offset=token_l.offset, 738 - ctx=token_l.ctx, 739 - data=token_l.data, # Same data for true condition 800 + act_id=token_l.act_id, 801 + data=token_l.data, 740 802 port=Port.R, 741 - gen=token_l.gen, 742 - wide=False, 743 803 ) 744 804 745 - def inject(): 746 - yield pe.input_store.put(token_l) 747 - yield pe.input_store.put(token_r) 748 - 749 - env.process(inject()) 750 - env.run(until=100) 805 + inject_two_and_run(env, pe, token_l, token_r) 751 806 752 807 # SWITCH mode produces two outputs 753 808 assert len(output_store_l.items) == 1 ··· 755 810 756 811 757 812 class TestStaleTokensProduceNoOutput: 758 - """Property-based test: Stale tokens (gen mismatch) produce no output.""" 813 + """Stale tokens (act_id mismatch) produce no output.""" 759 814 760 - @given(dyad_token(target=0, offset=10, act_id=2)) 815 + @given(dyad_token(target=0, offset=0, act_id=0)) 761 816 def test_stale_token_no_output(self, token_l: DyadToken): 762 - """Stale token (gen mismatch) produces no output.""" 817 + """Token with invalid act_id is rejected, produces no output.""" 763 818 env = simpy.Environment() 764 - iram = {10: ALUInst(op=ArithOp.ADD, dest_l=Addr(a=0, port=Port.L, pe=1), dest_r=None, const=None)} 765 - pe = ProcessingElement(env, 0, iram) 819 + 820 + add_inst = Instruction( 821 + opcode=ArithOp.ADD, 822 + output=OutputStyle.INHERIT, 823 + has_const=False, 824 + dest_count=1, 825 + wide=False, 826 + fref=8, 827 + ) 828 + 829 + dest = FrameDest( 830 + target_pe=1, offset=0, act_id=0, 831 + port=Port.L, token_kind=TokenKind.DYADIC, 832 + ) 766 833 834 + config = PEConfig( 835 + pe_id=0, 836 + iram={0: add_inst}, 837 + initial_frames={0: {8: dest}}, 838 + initial_tag_store={0: 0}, 839 + # Note: only act_id 0 is allocated; other act_ids will be invalid 840 + ) 841 + 842 + pe = ProcessingElement(env=env, pe_id=0, config=config) 767 843 output_store = simpy.Store(env, capacity=10) 768 844 pe.route_table[1] = output_store 769 845 770 - # Set gen_counter to be different from token's gen 771 - ctx_idx = token_l.ctx % 4 772 - pe.gen_counters[ctx_idx] = (token_l.gen + 1) % 4 773 - 774 - # Create stale token with mismatched gen 775 - token_stale = DyadToken( 776 - target=token_l.target, 777 - offset=token_l.offset, 778 - ctx=token_l.ctx, 779 - data=token_l.data, 846 + # Use act_id that was NOT allocated 847 + invalid_act_id = 3 848 + token = DyadToken( 849 + target=0, 850 + offset=0, 851 + act_id=invalid_act_id, 852 + data=0x1234, 780 853 port=Port.L, 781 - gen=token_l.gen, # Stale gen 782 - wide=False, 783 854 ) 784 855 785 - def inject(): 786 - yield pe.input_store.put(token_stale) 856 + inject_and_run(env, pe, token) 787 857 788 - env.process(inject()) 789 - env.run(until=100) 790 - 791 - # Stale token should produce no output 858 + # Invalid act_id produces no output 792 859 assert len(output_store.items) == 0 793 860 794 861 795 862 class TestBoundaryEdgeCases: 796 - """Test boundary and edge cases for PE operations.""" 863 + """Boundary edge cases.""" 797 864 798 865 def test_iram_write_multiple_instructions(self): 799 - """IRAMWriteToken can load multiple instructions sequentially.""" 866 + """Multiple instructions can be loaded and executed.""" 800 867 env = simpy.Environment() 801 - pe = ProcessingElement(env, 0, {}) 868 + 869 + add_inst = Instruction( 870 + opcode=ArithOp.ADD, 871 + output=OutputStyle.INHERIT, 872 + has_const=False, 873 + dest_count=1, 874 + wide=False, 875 + fref=8, 876 + ) 802 877 803 - # Create two instructions to load 804 - inst1 = ALUInst( 805 - op=RoutingOp.PASS, 806 - dest_l=Addr(a=0, port=Port.L, pe=1), 807 - dest_r=None, 808 - const=None, 878 + inc_inst = Instruction( 879 + opcode=ArithOp.INC, 880 + output=OutputStyle.INHERIT, 881 + has_const=False, 882 + dest_count=1, 883 + wide=False, 884 + fref=8, 809 885 ) 810 - inst2 = ALUInst( 811 - op=ArithOp.ADD, 812 - dest_l=Addr(a=1, port=Port.L, pe=1), 813 - dest_r=None, 814 - const=None, 886 + 887 + dest = FrameDest( 888 + target_pe=1, offset=0, act_id=0, 889 + port=Port.L, token_kind=TokenKind.MONADIC, 815 890 ) 816 891 817 - write_token = IRAMWriteToken( 818 - target=0, 819 - offset=10, 820 - ctx=0, 821 - data=0, 822 - instructions=(inst1, inst2), 892 + config = PEConfig( 893 + pe_id=0, 894 + iram={0: add_inst, 1: inc_inst}, 895 + initial_frames={0: {8: dest}}, 896 + initial_tag_store={0: 0}, 823 897 ) 824 898 825 - def inject(): 826 - yield pe.input_store.put(write_token) 827 - yield env.timeout(10) 899 + pe = ProcessingElement(env=env, pe_id=0, config=config) 900 + output_store = simpy.Store(env, capacity=10) 901 + pe.route_table[1] = output_store 828 902 829 - env.process(inject()) 830 - env.run(until=100) 903 + # Execute ADD at offset 0 904 + token_l = DyadToken(target=0, offset=0, act_id=0, data=0x10, port=Port.L) 905 + token_r = DyadToken(target=0, offset=0, act_id=0, data=0x20, port=Port.R) 831 906 832 - # Verify both instructions were loaded at consecutive offsets 833 - assert 10 in pe.iram 834 - assert 11 in pe.iram 835 - assert pe.iram[10] == inst1 836 - assert pe.iram[11] == inst2 907 + inject_two_and_run(env, pe, token_l, token_r) 908 + 909 + # Execute INC at offset 1 910 + token_inc = MonadToken(target=0, offset=1, act_id=0, data=0x10, inline=False) 911 + 912 + inject_and_run(env, pe, token_inc) 913 + 914 + # Both instructions executed 915 + assert len(output_store.items) == 2 916 + assert output_store.items[0].data == 0x30 # ADD result 917 + assert output_store.items[1].data == 0x11 # INC result