OR-1 dataflow CPU sketch
at main 1165 lines 47 kB view raw
1""" 2Lane-based PE rewrite tests. 3 4Verifies frame-lanes.AC3, frame-lanes.AC4, frame-lanes.AC5, and frame-lanes.AC8: 5- AC3.1: FrameOp.ALLOC_SHARED assigns next free lane from parent frame 6- AC3.2: FrameOp.FREE_LANE removes tag_store entry, clears lane data, keeps frame 7- AC3.3: FrameOp.FREE on shared frame returns lane if frame still in use 8- AC3.4: FrameOp.ALLOC unchanged — allocates fresh frame, assigns lane 0 9- AC3.5: FrameAllocated event gains lane field 10- AC3.6: ALLOC_SHARED with all lanes occupied emits TokenRejected 11- AC4: ALLOC_REMOTE reads fref+2 for data-driven ALLOC_SHARED vs ALLOC 12- AC5.1: FREE_FRAME opcode uses smart FREE behaviour on shared frames 13- AC8.1: Two act_ids sharing a frame have independent matching 14- AC8.2: ALLOC_SHARED with exhausted lanes emits TokenRejected 15- AC8.3: FREE on shared frame preserves other lanes' data 16- AC8.4: ALLOC_REMOTE emits ALLOC_SHARED when fref+2 is non-zero 17- AC8.5: ALLOC_REMOTE emits ALLOC when fref+2 is zero (backwards compatible) 18- AC8.6: Full loop pipelining scenario — two iterations concurrent on different lanes 19""" 20 21import pytest 22import simpy 23 24from cm_inst import ( 25 ArithOp, FrameDest, FrameOp, Instruction, Port, TokenKind, OutputStyle, 26 RoutingOp, 27) 28from emu.events import ( 29 FrameAllocated, FrameFreed, TokenReceived, TokenRejected, Matched, Emitted, 30) 31from emu.pe import ProcessingElement 32from emu.types import PEConfig 33from tokens import DyadToken, FrameControlToken 34 35 36def inject_and_run(env, pe, token): 37 """Helper: inject token and run simulation.""" 38 def _put(): 39 yield pe.input_store.put(token) 40 env.process(_put()) 41 env.run() 42 43 44class TestAllocShared: 45 """AC3.1: ALLOC_SHARED assigns next free lane from parent frame.""" 46 47 def test_alloc_shared_basic(self): 48 """Parent allocates frame, child allocates shared lane.""" 49 env = simpy.Environment() 50 events = [] 51 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 52 pe = ProcessingElement(env=env, pe_id=0, config=config) 53 54 # Parent ALLOC 55 fct_parent = FrameControlToken( 56 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 57 ) 58 inject_and_run(env, pe, fct_parent) 59 60 parent_frame_id, parent_lane = pe.tag_store[0] 61 assert parent_lane == 0, "Parent should allocate lane 0" 62 63 # Child ALLOC_SHARED with parent_act_id=0 64 fct_child = FrameControlToken( 65 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 66 ) 67 inject_and_run(env, pe, fct_child) 68 69 child_frame_id, child_lane = pe.tag_store[1] 70 assert child_frame_id == parent_frame_id, "Child should share parent's frame" 71 assert child_lane == 1, "Child should allocate lane 1" 72 assert child_lane != parent_lane, "Child lane should differ from parent" 73 74 # Verify FrameAllocated event for child 75 frame_allocated = [e for e in events if isinstance(e, FrameAllocated)] 76 assert len(frame_allocated) >= 2, "Should have 2 FrameAllocated events" 77 assert frame_allocated[0].lane == 0, "Parent allocated lane 0" 78 assert frame_allocated[1].lane == 1, "Child allocated lane 1" 79 80 def test_alloc_shared_multiple_lanes(self): 81 """Multiple children allocate different lanes from same parent frame.""" 82 env = simpy.Environment() 83 events = [] 84 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 85 pe = ProcessingElement(env=env, pe_id=0, config=config) 86 87 # Parent ALLOC 88 fct_parent = FrameControlToken( 89 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 90 ) 91 inject_and_run(env, pe, fct_parent) 92 parent_frame_id, _parent_lane = pe.tag_store[0] 93 94 # Child 1 ALLOC_SHARED 95 fct_child1 = FrameControlToken( 96 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 97 ) 98 inject_and_run(env, pe, fct_child1) 99 _child1_frame_id, child1_lane = pe.tag_store[1] 100 101 # Child 2 ALLOC_SHARED 102 fct_child2 = FrameControlToken( 103 target=0, act_id=2, op=FrameOp.ALLOC_SHARED, payload=0 104 ) 105 inject_and_run(env, pe, fct_child2) 106 _child2_frame_id, child2_lane = pe.tag_store[2] 107 108 # All should share same frame 109 assert pe.tag_store[0][0] == parent_frame_id 110 assert pe.tag_store[1][0] == parent_frame_id 111 assert pe.tag_store[2][0] == parent_frame_id 112 113 # Lanes should differ: 0, 1, 2 114 assert child1_lane != 0, "Child1 lane should not be 0" 115 assert child2_lane != 0, "Child2 lane should not be 0" 116 assert child1_lane != child2_lane, "Child1 and child2 lanes should differ" 117 118 def test_alloc_shared_invalid_parent(self): 119 """ALLOC_SHARED with non-existent parent emits TokenRejected.""" 120 env = simpy.Environment() 121 events = [] 122 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 123 pe = ProcessingElement(env=env, pe_id=0, config=config) 124 125 # Try ALLOC_SHARED with non-existent parent_act_id=999 126 fct = FrameControlToken( 127 target=0, act_id=0, op=FrameOp.ALLOC_SHARED, payload=999 128 ) 129 inject_and_run(env, pe, fct) 130 131 rejected = [e for e in events if isinstance(e, TokenRejected)] 132 assert len(rejected) > 0, "Should have TokenRejected event" 133 assert "not in tag store" in rejected[0].reason, "Reason should mention tag_store" 134 135 # Parent should not be in tag_store 136 assert 999 not in pe.tag_store 137 138 def test_alloc_shared_self_referential_guard(self): 139 """ALLOC_SHARED with act_id already in tag_store emits TokenRejected.""" 140 env = simpy.Environment() 141 events = [] 142 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 143 pe = ProcessingElement(env=env, pe_id=0, config=config) 144 145 # First ALLOC to establish act_id=0 in tag_store 146 fct_alloc = FrameControlToken( 147 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 148 ) 149 inject_and_run(env, pe, fct_alloc) 150 assert 0 in pe.tag_store, "act_id=0 should be in tag_store after ALLOC" 151 frame_id_0, lane_0 = pe.tag_store[0] 152 153 # Now try ALLOC_SHARED with act_id=0 and payload=1 (parent_act_id=1) 154 # This should be rejected because act_id=0 already exists 155 fct_alloc_parent = FrameControlToken( 156 target=0, act_id=1, op=FrameOp.ALLOC, payload=0 157 ) 158 inject_and_run(env, pe, fct_alloc_parent) 159 assert 1 in pe.tag_store, "act_id=1 should be in tag_store after ALLOC" 160 161 events.clear() 162 fct_shared = FrameControlToken( 163 target=0, act_id=0, op=FrameOp.ALLOC_SHARED, payload=1 164 ) 165 inject_and_run(env, pe, fct_shared) 166 167 rejected = [e for e in events if isinstance(e, TokenRejected)] 168 assert len(rejected) > 0, "Should have TokenRejected event" 169 assert "already in tag store" in rejected[0].reason, "Reason should mention already in tag store" 170 171 # Frame and lane should be unchanged 172 assert pe.tag_store[0] == (frame_id_0, lane_0), "act_id=0 state should be unchanged" 173 174 175class TestLaneExhaustion: 176 """AC3.6, AC8.2: Lane exhaustion and TokenRejected.""" 177 178 def test_alloc_shared_exhausts_all_lanes(self): 179 """Allocate all lanes, then ALLOC_SHARED fails with TokenRejected.""" 180 env = simpy.Environment() 181 events = [] 182 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 183 pe = ProcessingElement(env=env, pe_id=0, config=config) 184 185 # Parent ALLOC uses lane 0 186 fct_parent = FrameControlToken( 187 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 188 ) 189 inject_and_run(env, pe, fct_parent) 190 191 # Allocate lanes 1, 2, 3 192 for i in range(1, 4): 193 fct = FrameControlToken( 194 target=0, act_id=i, op=FrameOp.ALLOC_SHARED, payload=0 195 ) 196 inject_and_run(env, pe, fct) 197 assert i in pe.tag_store, f"Child {i} should be allocated" 198 199 # Try to allocate one more (all lanes exhausted) 200 fct_fail = FrameControlToken( 201 target=0, act_id=4, op=FrameOp.ALLOC_SHARED, payload=0 202 ) 203 inject_and_run(env, pe, fct_fail) 204 205 rejected = [e for e in events if isinstance(e, TokenRejected)] 206 assert len(rejected) > 0, "Should have TokenRejected event" 207 assert "no free lanes" in rejected[0].reason, "Reason should be 'no free lanes'" 208 209 # act_id=4 should not be in tag_store 210 assert 4 not in pe.tag_store, "Failed allocation should not add to tag_store" 211 212 def test_lane_exhaustion_with_multiple_frames(self): 213 """Lane exhaustion is per-frame; different frames have independent lanes.""" 214 env = simpy.Environment() 215 events = [] 216 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 217 pe = ProcessingElement(env=env, pe_id=0, config=config) 218 219 # Frame 1: Parent 0 allocates lane 0 220 fct1 = FrameControlToken( 221 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 222 ) 223 inject_and_run(env, pe, fct1) 224 frame1_id, _lane = pe.tag_store[0] 225 226 # Frame 2: Parent 10 allocates lane 0 227 fct2 = FrameControlToken( 228 target=0, act_id=10, op=FrameOp.ALLOC, payload=0 229 ) 230 inject_and_run(env, pe, fct2) 231 frame2_id, _lane = pe.tag_store[10] 232 233 assert frame1_id != frame2_id, "Should allocate different frames" 234 235 # Frame 1: Exhaust all lanes 236 for i in range(1, 4): 237 fct = FrameControlToken( 238 target=0, act_id=i, op=FrameOp.ALLOC_SHARED, payload=0 239 ) 240 inject_and_run(env, pe, fct) 241 242 # Frame 2: Can still allocate more lanes (independent) 243 for i in range(11, 14): 244 fct = FrameControlToken( 245 target=0, act_id=i, op=FrameOp.ALLOC_SHARED, payload=10 246 ) 247 inject_and_run(env, pe, fct) 248 assert i in pe.tag_store, f"Frame2 child {i} should be allocated" 249 250 251class TestFreeLane: 252 """AC3.2: FREE_LANE clears lane data, keeps frame, returns lane to pool.""" 253 254 def test_free_lane_basic(self): 255 """FREE_LANE removes act_id from tag_store, clears lane data, keeps frame.""" 256 env = simpy.Environment() 257 events = [] 258 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 259 pe = ProcessingElement(env=env, pe_id=0, config=config) 260 261 # Parent ALLOC 262 fct_parent = FrameControlToken( 263 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 264 ) 265 inject_and_run(env, pe, fct_parent) 266 parent_frame_id, _parent_lane = pe.tag_store[0] 267 268 # Child ALLOC_SHARED 269 fct_child = FrameControlToken( 270 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 271 ) 272 inject_and_run(env, pe, fct_child) 273 _child_frame_id, child_lane = pe.tag_store[1] 274 275 # FREE_LANE for child 276 fct_free = FrameControlToken( 277 target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0 278 ) 279 inject_and_run(env, pe, fct_free) 280 281 # Child should be removed from tag_store 282 assert 1 not in pe.tag_store, "Child should be removed from tag_store" 283 284 # Parent should still be present 285 assert 0 in pe.tag_store, "Parent should still be in tag_store" 286 287 # Frame should NOT be in free_frames (still used by parent) 288 assert parent_frame_id not in pe.free_frames, "Frame should not be free" 289 290 # FrameFreed event should have frame_freed=False 291 frame_freed = [e for e in events if isinstance(e, FrameFreed)] 292 assert len(frame_freed) > 0, "Should have FrameFreed event" 293 assert frame_freed[-1].frame_freed == False, "frame_freed should be False" 294 assert frame_freed[-1].lane == child_lane, "Event should report correct lane" 295 296 def test_free_lane_returns_lane_to_pool(self): 297 """After FREE_LANE, freed lane can be reused by ALLOC_SHARED.""" 298 env = simpy.Environment() 299 events = [] 300 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 301 pe = ProcessingElement(env=env, pe_id=0, config=config) 302 303 # Parent ALLOC 304 fct_parent = FrameControlToken( 305 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 306 ) 307 inject_and_run(env, pe, fct_parent) 308 parent_frame_id, _parent_lane = pe.tag_store[0] 309 310 # Child 1 ALLOC_SHARED (lane 1) 311 fct_child1 = FrameControlToken( 312 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 313 ) 314 inject_and_run(env, pe, fct_child1) 315 _child1_frame_id, child1_lane = pe.tag_store[1] 316 assert child1_lane == 1 317 318 # FREE_LANE child 1 319 fct_free = FrameControlToken( 320 target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0 321 ) 322 inject_and_run(env, pe, fct_free) 323 324 # Child 2 ALLOC_SHARED (should get lane 1 again) 325 fct_child2 = FrameControlToken( 326 target=0, act_id=2, op=FrameOp.ALLOC_SHARED, payload=0 327 ) 328 inject_and_run(env, pe, fct_child2) 329 _child2_frame_id, child2_lane = pe.tag_store[2] 330 331 # Lane 1 should be reused for child 2 332 assert child2_lane == 1, "Freed lane 1 should be reused" 333 334 335class TestIndependentMatching: 336 """AC8.1: Two act_ids sharing a frame have independent matching.""" 337 338 def test_independent_matching_same_offset(self): 339 """L operand for act_id 0 does not interfere with L for act_id 1.""" 340 env = simpy.Environment() 341 events = [] 342 config = PEConfig( 343 frame_count=4, lane_count=4, matchable_offsets=4, on_event=events.append 344 ) 345 pe = ProcessingElement(env=env, pe_id=0, config=config) 346 347 # Parent ALLOC 348 fct_parent = FrameControlToken( 349 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 350 ) 351 inject_and_run(env, pe, fct_parent) 352 parent_frame_id, _parent_lane = pe.tag_store[0] 353 354 # Child ALLOC_SHARED 355 fct_child = FrameControlToken( 356 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 357 ) 358 inject_and_run(env, pe, fct_child) 359 _child_frame_id, child_lane = pe.tag_store[1] 360 361 # Install dyadic instruction at offset 0 362 inst = Instruction( 363 opcode=ArithOp.ADD, 364 output=OutputStyle.SINK, 365 has_const=False, 366 dest_count=0, 367 wide=False, 368 fref=0, 369 ) 370 pe.iram[0] = inst 371 372 # Send L operand for act_id=0 373 tok_l_0 = DyadToken( 374 target=0, offset=0, act_id=0, data=5, port=Port.L 375 ) 376 inject_and_run(env, pe, tok_l_0) 377 378 # Should have 1 TokenReceived, 0 Matched (waiting for R) 379 matched = [e for e in events if isinstance(e, Matched)] 380 assert len(matched) == 0, "Should not match yet (waiting for R)" 381 382 # Send L operand for act_id=1 at same offset 383 tok_l_1 = DyadToken( 384 target=0, offset=0, act_id=1, data=7, port=Port.L 385 ) 386 inject_and_run(env, pe, tok_l_1) 387 388 # Should still have 0 Matched (both waiting for R) 389 matched = [e for e in events if isinstance(e, Matched)] 390 assert len(matched) == 0, "Both should be waiting for R" 391 392 # Send R for act_id=0 393 tok_r_0 = DyadToken( 394 target=0, offset=0, act_id=0, data=3, port=Port.R 395 ) 396 inject_and_run(env, pe, tok_r_0) 397 398 # Should now have 1 Matched for act_id=0 399 matched = [e for e in events if isinstance(e, Matched)] 400 assert len(matched) == 1, "Should have 1 match for act_id=0" 401 assert matched[0].act_id == 0, "Match should be for act_id=0" 402 assert matched[0].left == 5, "Left should be 5" 403 assert matched[0].right == 3, "Right should be 3" 404 405 # Send R for act_id=1 406 tok_r_1 = DyadToken( 407 target=0, offset=0, act_id=1, data=2, port=Port.R 408 ) 409 inject_and_run(env, pe, tok_r_1) 410 411 # Should now have 2 Matched 412 matched = [e for e in events if isinstance(e, Matched)] 413 assert len(matched) == 2, "Should have 2 matches total" 414 m1 = [m for m in matched if m.act_id == 1][0] 415 assert m1.left == 7, "act_id=1 left should be 7" 416 assert m1.right == 2, "act_id=1 right should be 2" 417 418 def test_independent_matching_different_offsets(self): 419 """Different offsets per lane maintain independence.""" 420 env = simpy.Environment() 421 events = [] 422 config = PEConfig( 423 frame_count=4, lane_count=4, matchable_offsets=4, on_event=events.append 424 ) 425 pe = ProcessingElement(env=env, pe_id=0, config=config) 426 427 # Parent ALLOC 428 fct_parent = FrameControlToken( 429 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 430 ) 431 inject_and_run(env, pe, fct_parent) 432 433 # Child ALLOC_SHARED 434 fct_child = FrameControlToken( 435 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 436 ) 437 inject_and_run(env, pe, fct_child) 438 439 # Install dyadic instructions at offsets 0 and 1 440 inst0 = Instruction( 441 opcode=ArithOp.ADD, output=OutputStyle.SINK, 442 has_const=False, dest_count=0, wide=False, fref=0 443 ) 444 inst1 = Instruction( 445 opcode=ArithOp.SUB, output=OutputStyle.SINK, 446 has_const=False, dest_count=0, wide=False, fref=0 447 ) 448 pe.iram[0] = inst0 449 pe.iram[1] = inst1 450 451 # Send L for act_id=0 at offset 0 452 tok_l_0_off0 = DyadToken( 453 target=0, offset=0, act_id=0, data=10, port=Port.L 454 ) 455 inject_and_run(env, pe, tok_l_0_off0) 456 457 # Send L for act_id=1 at offset 1 458 tok_l_1_off1 = DyadToken( 459 target=0, offset=1, act_id=1, data=20, port=Port.L 460 ) 461 inject_and_run(env, pe, tok_l_1_off1) 462 463 # Neither should match yet 464 matched = [e for e in events if isinstance(e, Matched)] 465 assert len(matched) == 0, "No matches yet" 466 467 # Send R for act_id=0 at offset 0 468 tok_r_0_off0 = DyadToken( 469 target=0, offset=0, act_id=0, data=5, port=Port.R 470 ) 471 inject_and_run(env, pe, tok_r_0_off0) 472 473 # Should match for offset 0 474 matched = [e for e in events if isinstance(e, Matched)] 475 assert len(matched) == 1, "Should have 1 match" 476 assert matched[0].offset == 0, "Match should be at offset 0" 477 478 # Send R for act_id=1 at offset 1 479 tok_r_1_off1 = DyadToken( 480 target=0, offset=1, act_id=1, data=15, port=Port.R 481 ) 482 inject_and_run(env, pe, tok_r_1_off1) 483 484 # Should match for offset 1 485 matched = [e for e in events if isinstance(e, Matched)] 486 assert len(matched) == 2, "Should have 2 matches" 487 m1 = [m for m in matched if m.offset == 1][0] 488 assert m1.act_id == 1, "Offset 1 match should be act_id=1" 489 490 491class TestSmartFree: 492 """AC3.3, AC8.3: Smart FREE on shared frames preserves data and manages lanes.""" 493 494 def test_free_on_shared_frame_preserves_other_lanes(self): 495 """FREE on act_id=0 when act_id=1 uses frame; lane 1 data preserved.""" 496 env = simpy.Environment() 497 events = [] 498 config = PEConfig( 499 frame_count=4, lane_count=4, matchable_offsets=4, on_event=events.append 500 ) 501 pe = ProcessingElement(env=env, pe_id=0, config=config) 502 503 # Parent ALLOC 504 fct_parent = FrameControlToken( 505 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 506 ) 507 inject_and_run(env, pe, fct_parent) 508 parent_frame_id, _parent_lane = pe.tag_store[0] 509 510 # Child ALLOC_SHARED 511 fct_child = FrameControlToken( 512 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 513 ) 514 inject_and_run(env, pe, fct_child) 515 _child_frame_id, child_lane = pe.tag_store[1] 516 517 # Install instruction 518 inst = Instruction( 519 opcode=ArithOp.ADD, output=OutputStyle.SINK, 520 has_const=False, dest_count=0, wide=False, fref=0 521 ) 522 pe.iram[0] = inst 523 524 # Store L operand on child's lane 525 tok_l_1 = DyadToken( 526 target=0, offset=0, act_id=1, data=7, port=Port.L 527 ) 528 inject_and_run(env, pe, tok_l_1) 529 530 # Verify child's match slot has data 531 frame_id, lane = pe.tag_store[1] 532 assert pe.match_data[frame_id][0][lane] == 7, "Child lane should have L operand" 533 assert pe.presence[frame_id][0][lane] == True, "Child presence should be set" 534 535 # FREE parent 536 fct_free_parent = FrameControlToken( 537 target=0, act_id=0, op=FrameOp.FREE, payload=0 538 ) 539 inject_and_run(env, pe, fct_free_parent) 540 541 # Parent should be removed, child should still be present 542 assert 0 not in pe.tag_store, "Parent should be removed" 543 assert 1 in pe.tag_store, "Child should still be present" 544 545 # Frame should NOT be in free_frames 546 assert parent_frame_id not in pe.free_frames, "Frame should not be free" 547 548 # Child's match data should be preserved 549 assert pe.match_data[frame_id][0][lane] == 7, "Child data should be preserved" 550 assert pe.presence[frame_id][0][lane] == True, "Child presence should be preserved" 551 552 # FrameFreed event should have frame_freed=False 553 frame_freed = [e for e in events if isinstance(e, FrameFreed)] 554 assert any(e.frame_freed == False for e in frame_freed), "Should have frame_freed=False" 555 556 def test_free_last_lane_returns_frame(self): 557 """FREE on last act_id using frame returns frame to free_frames.""" 558 env = simpy.Environment() 559 events = [] 560 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 561 pe = ProcessingElement(env=env, pe_id=0, config=config) 562 563 # Parent ALLOC 564 fct_parent = FrameControlToken( 565 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 566 ) 567 inject_and_run(env, pe, fct_parent) 568 parent_frame_id, _parent_lane = pe.tag_store[0] 569 570 # Child ALLOC_SHARED 571 fct_child = FrameControlToken( 572 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 573 ) 574 inject_and_run(env, pe, fct_child) 575 576 # FREE child 577 fct_free_child = FrameControlToken( 578 target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0 579 ) 580 inject_and_run(env, pe, fct_free_child) 581 582 # Frame should still not be free (parent still using it) 583 assert parent_frame_id not in pe.free_frames, "Frame should not be free yet" 584 585 # FREE parent 586 fct_free_parent = FrameControlToken( 587 target=0, act_id=0, op=FrameOp.FREE, payload=0 588 ) 589 inject_and_run(env, pe, fct_free_parent) 590 591 # Now frame should be free 592 assert parent_frame_id in pe.free_frames, "Frame should be free" 593 594 # tag_store should be empty 595 assert len(pe.tag_store) == 0, "tag_store should be empty" 596 597 # lane_free entry should be cleaned up 598 assert parent_frame_id not in pe.lane_free, "lane_free entry should be cleaned" 599 600 # FrameFreed event should have frame_freed=True 601 frame_freed = [e for e in events if isinstance(e, FrameFreed)] 602 assert any(e.frame_freed == True for e in frame_freed), "Should have frame_freed=True" 603 604 def test_alloc_unchanged_allocates_fresh_frame(self): 605 """Regular ALLOC still works: allocates fresh frame, lane 0.""" 606 env = simpy.Environment() 607 events = [] 608 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 609 pe = ProcessingElement(env=env, pe_id=0, config=config) 610 611 # First ALLOC 612 fct1 = FrameControlToken( 613 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 614 ) 615 inject_and_run(env, pe, fct1) 616 frame_id_0, lane_0 = pe.tag_store[0] 617 assert lane_0 == 0, "First ALLOC should assign lane 0" 618 619 # Second ALLOC (different frame) 620 fct2 = FrameControlToken( 621 target=0, act_id=10, op=FrameOp.ALLOC, payload=0 622 ) 623 inject_and_run(env, pe, fct2) 624 frame_id_10, lane_10 = pe.tag_store[10] 625 assert lane_10 == 0, "Second ALLOC should assign lane 0" 626 627 # Frames should be different 628 assert frame_id_0 != frame_id_10, "Different ALLOC should get different frames" 629 630 def test_data_preservation_across_free_lanes(self): 631 """Match data on one lane not affected by FREE of another lane.""" 632 env = simpy.Environment() 633 events = [] 634 config = PEConfig( 635 frame_count=4, lane_count=4, matchable_offsets=4, on_event=events.append 636 ) 637 pe = ProcessingElement(env=env, pe_id=0, config=config) 638 639 # Parent ALLOC 640 fct_parent = FrameControlToken( 641 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 642 ) 643 inject_and_run(env, pe, fct_parent) 644 frame_id, _parent_lane = pe.tag_store[0] 645 646 # Child 1 ALLOC_SHARED 647 fct_child1 = FrameControlToken( 648 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 649 ) 650 inject_and_run(env, pe, fct_child1) 651 _frame_id_1, lane_1 = pe.tag_store[1] 652 653 # Child 2 ALLOC_SHARED 654 fct_child2 = FrameControlToken( 655 target=0, act_id=2, op=FrameOp.ALLOC_SHARED, payload=0 656 ) 657 inject_and_run(env, pe, fct_child2) 658 _frame_id_2, lane_2 = pe.tag_store[2] 659 660 # Install instruction 661 inst = Instruction( 662 opcode=ArithOp.ADD, output=OutputStyle.SINK, 663 has_const=False, dest_count=0, wide=False, fref=0 664 ) 665 pe.iram[0] = inst 666 667 # Store L operand on lane 1 668 tok_l_1 = DyadToken( 669 target=0, offset=0, act_id=1, data=7, port=Port.L 670 ) 671 inject_and_run(env, pe, tok_l_1) 672 673 # Store L operand on lane 2 674 tok_l_2 = DyadToken( 675 target=0, offset=0, act_id=2, data=11, port=Port.L 676 ) 677 inject_and_run(env, pe, tok_l_2) 678 679 # FREE lane 1 680 fct_free_1 = FrameControlToken( 681 target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0 682 ) 683 inject_and_run(env, pe, fct_free_1) 684 685 # Lane 2's data should be untouched 686 assert pe.match_data[frame_id][0][lane_2] == 11, "Lane 2 data should be preserved" 687 assert pe.presence[frame_id][0][lane_2] == True, "Lane 2 presence should be preserved" 688 689 # Lane 1 should be cleared 690 assert pe.match_data[frame_id][0][lane_1] is None, "Lane 1 data should be cleared" 691 assert pe.presence[frame_id][0][lane_1] == False, "Lane 1 presence should be cleared" 692 693 694class TestAllocRemoteDataDriven: 695 """AC8.4, AC8.5: ALLOC_REMOTE reads fref+2 for data-driven ALLOC_SHARED vs ALLOC.""" 696 697 def test_alloc_remote_emits_alloc_shared_when_parent_nonzero(self): 698 """AC8.4: ALLOC_REMOTE emits ALLOC_SHARED when fref+2 is non-zero.""" 699 env = simpy.Environment() 700 events = [] 701 output_store = simpy.Store(env) 702 703 # PE0: source of ALLOC_REMOTE 704 config0 = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 705 pe0 = ProcessingElement(env=env, pe_id=0, config=config0) 706 pe0.route_table[1] = output_store # Capture emitted token 707 708 # Allocate a frame for act_id=0 on PE0 709 fct_parent = FrameControlToken( 710 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 711 ) 712 inject_and_run(env, pe0, fct_parent) 713 frame_id, _lane = pe0.tag_store[0] 714 715 # Set up ALLOC_REMOTE instruction with fref pointing to frame constants 716 # fref+0: target PE=1, fref+1: target act_id=5, fref+2: parent act_id=3 717 inst = Instruction( 718 opcode=RoutingOp.ALLOC_REMOTE, 719 output=OutputStyle.SINK, # Not used for ALLOC_REMOTE 720 has_const=False, 721 dest_count=0, 722 wide=False, 723 fref=10, 724 ) 725 pe0.iram[0] = inst 726 727 # Load frame slots with constants 728 pe0.frames[frame_id][10] = 1 # target PE 729 pe0.frames[frame_id][11] = 5 # target act_id 730 pe0.frames[frame_id][12] = 3 # parent act_id (non-zero = ALLOC_SHARED) 731 732 # Send MonadToken to trigger ALLOC_REMOTE 733 tok = DyadToken( 734 target=0, offset=0, act_id=0, data=0, port=Port.L 735 ) 736 inject_and_run(env, pe0, tok) 737 738 # Verify FrameControlToken was emitted with ALLOC_SHARED 739 assert len(output_store.items) > 0, "Should have emitted a token" 740 emitted = output_store.items[0] 741 assert isinstance(emitted, FrameControlToken), "Should emit FrameControlToken" 742 assert emitted.op == FrameOp.ALLOC_SHARED, "Should emit ALLOC_SHARED" 743 assert emitted.payload == 3, "Payload should be parent act_id=3" 744 assert emitted.target == 1, "Should target PE 1" 745 assert emitted.act_id == 5, "Should target act_id 5" 746 747 def test_alloc_remote_emits_alloc_when_parent_zero(self): 748 """AC8.5: ALLOC_REMOTE emits ALLOC when fref+2 is zero (backwards compatible).""" 749 env = simpy.Environment() 750 events = [] 751 output_store = simpy.Store(env) 752 753 # PE0: source of ALLOC_REMOTE 754 config0 = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 755 pe0 = ProcessingElement(env=env, pe_id=0, config=config0) 756 pe0.route_table[1] = output_store # Capture emitted token 757 758 # Allocate a frame for act_id=0 on PE0 759 fct_parent = FrameControlToken( 760 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 761 ) 762 inject_and_run(env, pe0, fct_parent) 763 frame_id, _lane = pe0.tag_store[0] 764 765 # Set up ALLOC_REMOTE instruction 766 # fref+0: target PE=1, fref+1: target act_id=5, fref+2: parent act_id=0 767 inst = Instruction( 768 opcode=RoutingOp.ALLOC_REMOTE, 769 output=OutputStyle.SINK, 770 has_const=False, 771 dest_count=0, 772 wide=False, 773 fref=10, 774 ) 775 pe0.iram[0] = inst 776 777 # Load frame slots with constants 778 pe0.frames[frame_id][10] = 1 # target PE 779 pe0.frames[frame_id][11] = 5 # target act_id 780 pe0.frames[frame_id][12] = 0 # parent act_id (zero = ALLOC) 781 782 # Send MonadToken to trigger ALLOC_REMOTE 783 tok = DyadToken( 784 target=0, offset=0, act_id=0, data=0, port=Port.L 785 ) 786 inject_and_run(env, pe0, tok) 787 788 # Verify FrameControlToken was emitted with ALLOC (not ALLOC_SHARED) 789 assert len(output_store.items) > 0, "Should have emitted a token" 790 emitted = output_store.items[0] 791 assert isinstance(emitted, FrameControlToken), "Should emit FrameControlToken" 792 assert emitted.op == FrameOp.ALLOC, "Should emit ALLOC" 793 assert emitted.payload == 0, "Payload should be 0 for ALLOC" 794 assert emitted.target == 1, "Should target PE 1" 795 assert emitted.act_id == 5, "Should target act_id 5" 796 797 def test_alloc_remote_fref_plus_2_missing_defaults_to_zero(self): 798 """ALLOC_REMOTE gracefully handles fref+2 outside frame bounds (defaults to 0).""" 799 env = simpy.Environment() 800 events = [] 801 output_store = simpy.Store(env) 802 803 # PE0: source of ALLOC_REMOTE 804 config0 = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 805 pe0 = ProcessingElement(env=env, pe_id=0, config=config0) 806 pe0.route_table[1] = output_store 807 808 # Allocate frame 809 fct_parent = FrameControlToken( 810 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 811 ) 812 inject_and_run(env, pe0, fct_parent) 813 frame_id, _lane = pe0.tag_store[0] 814 815 # Set up ALLOC_REMOTE with fref pointing near end of frame 816 inst = Instruction( 817 opcode=RoutingOp.ALLOC_REMOTE, 818 output=OutputStyle.SINK, 819 has_const=False, 820 dest_count=0, 821 wide=False, 822 fref=62, # frame_slots defaults to 64, so fref+2=64 is outside 823 ) 824 pe0.iram[0] = inst 825 826 # Load only fref+0 and fref+1 (fref+2 is beyond frame bounds) 827 pe0.frames[frame_id][62] = 1 828 pe0.frames[frame_id][63] = 7 829 830 # Send MonadToken 831 tok = DyadToken( 832 target=0, offset=0, act_id=0, data=0, port=Port.L 833 ) 834 inject_and_run(env, pe0, tok) 835 836 # Should emit ALLOC (not ALLOC_SHARED) because fref+2 is missing/falsy 837 assert len(output_store.items) > 0, "Should have emitted a token" 838 emitted = output_store.items[0] 839 assert emitted.op == FrameOp.ALLOC, "Should emit ALLOC when fref+2 is missing" 840 841 842class TestFreeFrameOpcode: 843 """AC5.1: FREE_FRAME opcode uses smart FREE behaviour on shared frames.""" 844 845 def test_free_frame_opcode_shared_frame_partial_free(self): 846 """FREE_FRAME smart free: partial frame free when other lanes remain.""" 847 env = simpy.Environment() 848 events = [] 849 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 850 pe = ProcessingElement(env=env, pe_id=0, config=config) 851 852 # Pre-allocate frame with two act_ids on different lanes 853 # This simulates ALLOC for act_id=0 and ALLOC_SHARED for act_id=1 854 frame_id = 0 855 pe.frames[frame_id] = [None] * pe.frame_slots 856 pe.tag_store[0] = (frame_id, 0) # act_id=0 on lane 0 857 pe.tag_store[1] = (frame_id, 1) # act_id=1 on lane 1 858 pe.lane_free[frame_id] = {2, 3} # Lanes 2 and 3 are free 859 # Remove frame_id from free_frames (it's in use) 860 if frame_id in pe.free_frames: 861 pe.free_frames.remove(frame_id) 862 863 # Install FREE_FRAME instruction 864 inst = Instruction( 865 opcode=RoutingOp.FREE_FRAME, 866 output=OutputStyle.SINK, 867 has_const=False, 868 dest_count=0, 869 wide=False, 870 fref=0, 871 ) 872 pe.iram[0] = inst 873 874 # Send MonadToken for act_id=0 to trigger FREE_FRAME 875 tok = DyadToken( 876 target=0, offset=0, act_id=0, data=0, port=Port.L 877 ) 878 inject_and_run(env, pe, tok) 879 880 # Verify act_id=0 is removed from tag_store 881 assert 0 not in pe.tag_store, "act_id=0 should be removed from tag_store" 882 883 # Verify act_id=1 is still in tag_store 884 assert 1 in pe.tag_store, "act_id=1 should still be in tag_store" 885 886 # Verify frame is NOT returned to free_frames (still in use by act_id=1) 887 assert frame_id not in pe.free_frames, "Frame should not be in free_frames" 888 889 # Verify FrameFreed event has frame_freed=False 890 frame_freed = [e for e in events if isinstance(e, FrameFreed)] 891 assert any(e.frame_freed == False for e in frame_freed), \ 892 "Should have FrameFreed event with frame_freed=False" 893 last_frame_freed = [e for e in frame_freed if e.act_id == 0][-1] 894 assert last_frame_freed.frame_freed == False, "Frame should not be marked as freed" 895 896 def test_free_frame_opcode_shared_frame_full_free(self): 897 """FREE_FRAME smart free: full frame free when last lane is freed.""" 898 env = simpy.Environment() 899 events = [] 900 config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 901 pe = ProcessingElement(env=env, pe_id=0, config=config) 902 903 # Pre-allocate frame with two act_ids 904 frame_id = 0 905 pe.frames[frame_id] = [None] * pe.frame_slots 906 pe.tag_store[0] = (frame_id, 0) # act_id=0 on lane 0 907 pe.tag_store[1] = (frame_id, 1) # act_id=1 on lane 1 908 pe.lane_free[frame_id] = {2, 3} 909 if frame_id in pe.free_frames: 910 pe.free_frames.remove(frame_id) 911 912 # Install FREE_FRAME instruction 913 inst = Instruction( 914 opcode=RoutingOp.FREE_FRAME, 915 output=OutputStyle.SINK, 916 has_const=False, 917 dest_count=0, 918 wide=False, 919 fref=0, 920 ) 921 pe.iram[0] = inst 922 923 # First: free act_id=0 924 tok0 = DyadToken( 925 target=0, offset=0, act_id=0, data=0, port=Port.L 926 ) 927 inject_and_run(env, pe, tok0) 928 929 # Verify frame still not free 930 assert frame_id not in pe.free_frames, "Frame should not be free after first FREE_FRAME" 931 assert 1 in pe.tag_store, "act_id=1 should still be present" 932 933 # Second: free act_id=1 (last lane on frame) 934 tok1 = DyadToken( 935 target=0, offset=0, act_id=1, data=0, port=Port.L 936 ) 937 inject_and_run(env, pe, tok1) 938 939 # Verify frame is now freed 940 assert frame_id in pe.free_frames, "Frame should be in free_frames after last FREE_FRAME" 941 assert 1 not in pe.tag_store, "act_id=1 should be removed from tag_store" 942 943 # Verify tag_store is empty 944 assert len(pe.tag_store) == 0, "tag_store should be empty" 945 946 # Verify lane_free is cleaned up 947 assert frame_id not in pe.lane_free, "lane_free entry should be deleted" 948 949 # Verify FrameFreed event has frame_freed=True 950 frame_freed = [e for e in events if isinstance(e, FrameFreed)] 951 last_frame_freed = [e for e in frame_freed if e.act_id == 1][-1] 952 assert last_frame_freed.frame_freed == True, \ 953 "Last FREE_FRAME should emit FrameFreed with frame_freed=True" 954 955 956class TestLoopPipelining: 957 """AC8.6: Full loop pipelining integration test with multiple lanes.""" 958 959 def test_full_loop_pipelining_scenario(self): 960 """ 961 Complete loop pipelining lifecycle: two iterations of a dyadic instruction 962 running concurrently on different lanes, both producing correct results. 963 964 Simulates: 965 1. ALLOC(act_id=0) → frame, lane 0 966 2. Setup: write destination to frame 967 3. Iteration 1: inject L and R DyadTokens for act_id=0 968 4. ALLOC_SHARED(act_id=1, parent=0) → same frame, lane 1 969 5. Iteration 2: inject L and R DyadTokens for act_id=1 970 6. Both iterations match independently, both produce correct results 971 7. FREE(act_id=0) → lane 0 freed, frame stays 972 8. FREE(act_id=1) → last lane, frame returned to free list 973 """ 974 env = simpy.Environment() 975 events = [] 976 config = PEConfig( 977 frame_count=4, lane_count=4, matchable_offsets=4, 978 on_event=events.append 979 ) 980 pe = ProcessingElement(env=env, pe_id=0, config=config) 981 982 # 1. ALLOC(act_id=0) → frame, lane 0 983 fct_alloc_0 = FrameControlToken( 984 target=0, act_id=0, op=FrameOp.ALLOC, payload=0 985 ) 986 inject_and_run(env, pe, fct_alloc_0) 987 988 # Verify act_id=0 is allocated 989 assert 0 in pe.tag_store, "act_id=0 should be in tag_store" 990 frame_id, lane_0 = pe.tag_store[0] 991 assert lane_0 == 0, "First ALLOC should assign lane 0" 992 993 # Verify FrameAllocated event for iteration 1 994 frame_allocated = [e for e in events if isinstance(e, FrameAllocated)] 995 assert len(frame_allocated) >= 1, "Should have FrameAllocated event" 996 assert frame_allocated[0].frame_id == frame_id, "Event should report correct frame_id" 997 assert frame_allocated[0].lane == 0, "Event should report lane 0" 998 999 # 2. Setup: write destination to frame at slot 8 1000 dest = FrameDest( 1001 target_pe=1, offset=0, act_id=0, port=Port.L, 1002 token_kind=TokenKind.MONADIC 1003 ) 1004 pe.frames[frame_id][8] = dest 1005 1006 # Set up route to capture output 1007 pe.route_table[1] = simpy.Store(env) 1008 1009 # 3. Install ADD instruction at IRAM offset 0 1010 inst = Instruction( 1011 opcode=ArithOp.ADD, 1012 output=OutputStyle.INHERIT, 1013 has_const=False, 1014 dest_count=1, 1015 wide=False, 1016 fref=8, 1017 ) 1018 pe.iram[0] = inst 1019 1020 # 4. ALLOC_SHARED(act_id=1, parent=0) → same frame, lane 1 1021 fct_alloc_shared = FrameControlToken( 1022 target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 1023 ) 1024 inject_and_run(env, pe, fct_alloc_shared) 1025 1026 # Verify act_id=1 is allocated on same frame, different lane 1027 assert 1 in pe.tag_store, "act_id=1 should be in tag_store" 1028 frame_id_1, lane_1 = pe.tag_store[1] 1029 assert frame_id_1 == frame_id, "Both should share same frame" 1030 assert lane_1 == 1, "Second allocation should assign lane 1" 1031 assert lane_1 != lane_0, "Lanes should be different" 1032 1033 # Verify FrameAllocated event for iteration 2 1034 frame_allocated = [e for e in events if isinstance(e, FrameAllocated)] 1035 assert len(frame_allocated) >= 2, "Should have 2 FrameAllocated events" 1036 assert frame_allocated[1].frame_id == frame_id, "Event should report correct frame_id" 1037 assert frame_allocated[1].lane == 1, "Event should report lane 1" 1038 1039 # 5. Inject iteration 1 operands (act_id=0, lane 0) 1040 tok_l_0 = DyadToken( 1041 target=0, offset=0, act_id=0, data=100, port=Port.L 1042 ) 1043 inject_and_run(env, pe, tok_l_0) 1044 1045 tok_r_0 = DyadToken( 1046 target=0, offset=0, act_id=0, data=200, port=Port.R 1047 ) 1048 inject_and_run(env, pe, tok_r_0) 1049 1050 # Verify Matched event for iteration 1 1051 matched = [e for e in events if isinstance(e, Matched)] 1052 assert len(matched) >= 1, "Should have Matched event for iteration 1" 1053 match_0 = [m for m in matched if m.act_id == 0][-1] 1054 assert match_0.left == 100, "Iteration 1 left operand should be 100" 1055 assert match_0.right == 200, "Iteration 1 right operand should be 200" 1056 assert match_0.offset == 0, "Iteration 1 offset should be 0" 1057 1058 # Verify output token with correct data (100+200=300) 1059 emitted = [e for e in events if isinstance(e, Emitted)] 1060 assert len(emitted) >= 1, "Should have Emitted event for iteration 1" 1061 out_tok_0 = emitted[-1].token 1062 assert out_tok_0.data == 300, "Iteration 1 output should be 300 (100+200)" 1063 assert out_tok_0.target == 1, "Output should route to target_pe=1" 1064 1065 # 6. Inject iteration 2 operands (act_id=1, lane 1) 1066 tok_l_1 = DyadToken( 1067 target=0, offset=0, act_id=1, data=1000, port=Port.L 1068 ) 1069 inject_and_run(env, pe, tok_l_1) 1070 1071 tok_r_1 = DyadToken( 1072 target=0, offset=0, act_id=1, data=2000, port=Port.R 1073 ) 1074 inject_and_run(env, pe, tok_r_1) 1075 1076 # Verify Matched event for iteration 2 1077 matched = [e for e in events if isinstance(e, Matched)] 1078 assert len(matched) >= 2, "Should have Matched events for both iterations" 1079 match_1 = [m for m in matched if m.act_id == 1][-1] 1080 assert match_1.left == 1000, "Iteration 2 left operand should be 1000" 1081 assert match_1.right == 2000, "Iteration 2 right operand should be 2000" 1082 assert match_1.offset == 0, "Iteration 2 offset should be 0" 1083 1084 # Verify output token with correct data (1000+2000=3000) 1085 emitted = [e for e in events if isinstance(e, Emitted)] 1086 assert len(emitted) >= 2, "Should have Emitted events for both iterations" 1087 out_tok_1 = emitted[-1].token 1088 assert out_tok_1.data == 3000, "Iteration 2 output should be 3000 (1000+2000)" 1089 assert out_tok_1.target == 1, "Output should route to target_pe=1" 1090 1091 # Interleaved verification: confirm independent lanes 1092 matches_by_id = {} 1093 for m in matched: 1094 if m.act_id not in matches_by_id: 1095 matches_by_id[m.act_id] = [] 1096 matches_by_id[m.act_id].append(m) 1097 1098 assert 0 in matches_by_id, "Should have match for iteration 1 (act_id=0)" 1099 assert 1 in matches_by_id, "Should have match for iteration 2 (act_id=1)" 1100 assert matches_by_id[0][-1].left == 100, "Iteration 1 left should be 100" 1101 assert matches_by_id[1][-1].left == 1000, "Iteration 2 left should be 1000" 1102 1103 # 7. FREE(act_id=0) → lane 0 freed, frame stays 1104 fct_free_0 = FrameControlToken( 1105 target=0, act_id=0, op=FrameOp.FREE, payload=0 1106 ) 1107 inject_and_run(env, pe, fct_free_0) 1108 1109 # Verify act_id=0 removed, act_id=1 still present 1110 assert 0 not in pe.tag_store, "act_id=0 should be removed from tag_store" 1111 assert 1 in pe.tag_store, "act_id=1 should still be in tag_store" 1112 1113 # Verify frame not returned (still used by act_id=1) 1114 assert frame_id not in pe.free_frames, "Frame should not be in free_frames" 1115 1116 # Verify FrameFreed event with frame_freed=False 1117 frame_freed = [e for e in events if isinstance(e, FrameFreed)] 1118 freed_0 = [f for f in frame_freed if f.act_id == 0][-1] 1119 assert freed_0.frame_freed == False, "frame_freed should be False (not last lane)" 1120 assert freed_0.lane == lane_0, "Event should report lane 0" 1121 1122 # 8. FREE(act_id=1) → last lane, frame returned to free list 1123 fct_free_1 = FrameControlToken( 1124 target=0, act_id=1, op=FrameOp.FREE, payload=0 1125 ) 1126 inject_and_run(env, pe, fct_free_1) 1127 1128 # Verify act_id=1 removed from tag_store 1129 assert 1 not in pe.tag_store, "act_id=1 should be removed from tag_store" 1130 1131 # Verify tag_store is now empty 1132 assert len(pe.tag_store) == 0, "tag_store should be empty" 1133 1134 # Verify frame returned to free_frames 1135 assert frame_id in pe.free_frames, "Frame should be in free_frames" 1136 1137 # Verify lane_free entry cleaned up 1138 assert frame_id not in pe.lane_free, "lane_free entry should be deleted" 1139 1140 # Verify FrameFreed event with frame_freed=True 1141 frame_freed = [e for e in events if isinstance(e, FrameFreed)] 1142 freed_1 = [f for f in frame_freed if f.act_id == 1][-1] 1143 assert freed_1.frame_freed == True, "frame_freed should be True (last lane)" 1144 assert freed_1.lane == lane_1, "Event should report lane 1" 1145 1146 # Summary: verify AC8.6 acceptance criteria 1147 # Both iterations produce mathematically correct results 1148 assert matches_by_id[0][-1].left + matches_by_id[0][-1].right == 300, \ 1149 "Iteration 1 arithmetic correct" 1150 assert matches_by_id[1][-1].left + matches_by_id[1][-1].right == 3000, \ 1151 "Iteration 2 arithmetic correct" 1152 1153 # Both iterations ran on SAME frame (verified at allocation, re-confirmed) 1154 assert frame_id_1 == frame_id, "Both iterations ran on same frame" 1155 1156 # Both iterations used DIFFERENT lanes 1157 assert lane_0 != lane_1, "Iterations used different lanes" 1158 assert lane_0 == 0 and lane_1 == 1, "Lanes are 0 and 1 respectively" 1159 1160 # Freeing one iteration preserved the other 1161 frame_freed_events = [e for e in events if isinstance(e, FrameFreed)] 1162 assert len(frame_freed_events) >= 2, "Should have 2 FrameFreed events" 1163 1164 # Freeing the last iteration returned the frame 1165 assert frame_id in pe.free_frames, "Frame returned to pool after last FREE"