OR-1 dataflow CPU sketch

test: add tests for ALLOC_SHARED, FREE_LANE, and lane exhaustion

Verifies frame-lanes.AC3.1, AC3.2, AC3.6, AC8.1, AC8.2:
- AC3.1: ALLOC_SHARED assigns next free lane from parent frame
- AC3.2: FREE_LANE clears lane data, keeps frame
- AC3.6: Lane exhaustion emits TokenRejected with 'no free lanes'
- AC8.1: Two act_ids sharing a frame have independent matching
- AC8.2: ALLOC_SHARED with exhausted lanes fails with TokenRejected

New test file: tests/test_pe_lanes.py with 13 test methods covering:
- ALLOC_SHARED basic and multiple lanes
- Invalid parent handling
- Lane exhaustion per-frame and across frames
- FREE_LANE returning lane to pool
- Independent matching at same offset
- Independent matching at different offsets

Orual 7d7be996 ba053ee2

+649
+649
tests/test_pe_lanes.py
··· 1 + """ 2 + Lane-based PE rewrite tests. 3 + 4 + Verifies frame-lanes.AC3 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 + - AC8.1: Two act_ids sharing a frame have independent matching 12 + - AC8.2: ALLOC_SHARED with exhausted lanes emits TokenRejected 13 + - AC8.3: FREE on shared frame preserves other lanes' data 14 + """ 15 + 16 + import pytest 17 + import simpy 18 + 19 + from cm_inst import ( 20 + ArithOp, FrameDest, FrameOp, Instruction, Port, TokenKind, OutputStyle, 21 + ) 22 + from emu.events import ( 23 + FrameAllocated, FrameFreed, TokenReceived, TokenRejected, Matched, Emitted, 24 + ) 25 + from emu.pe import ProcessingElement 26 + from emu.types import PEConfig 27 + from tokens import DyadToken, FrameControlToken 28 + 29 + 30 + def inject_and_run(env, pe, token): 31 + """Helper: inject token and run simulation.""" 32 + def _put(): 33 + yield pe.input_store.put(token) 34 + env.process(_put()) 35 + env.run() 36 + 37 + 38 + class TestAllocShared: 39 + """AC3.1: ALLOC_SHARED assigns next free lane from parent frame.""" 40 + 41 + def test_alloc_shared_basic(self): 42 + """Parent allocates frame, child allocates shared lane.""" 43 + env = simpy.Environment() 44 + events = [] 45 + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 46 + pe = ProcessingElement(env=env, pe_id=0, config=config) 47 + 48 + # Parent ALLOC 49 + fct_parent = FrameControlToken( 50 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 51 + ) 52 + inject_and_run(env, pe, fct_parent) 53 + 54 + parent_frame_id, parent_lane = pe.tag_store[0] 55 + assert parent_lane == 0, "Parent should allocate lane 0" 56 + 57 + # Child ALLOC_SHARED with parent_act_id=0 58 + fct_child = FrameControlToken( 59 + target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 60 + ) 61 + inject_and_run(env, pe, fct_child) 62 + 63 + child_frame_id, child_lane = pe.tag_store[1] 64 + assert child_frame_id == parent_frame_id, "Child should share parent's frame" 65 + assert child_lane == 1, "Child should allocate lane 1" 66 + assert child_lane != parent_lane, "Child lane should differ from parent" 67 + 68 + # Verify FrameAllocated event for child 69 + frame_allocated = [e for e in events if isinstance(e, FrameAllocated)] 70 + assert len(frame_allocated) >= 2, "Should have 2 FrameAllocated events" 71 + assert frame_allocated[0].lane == 0, "Parent allocated lane 0" 72 + assert frame_allocated[1].lane == 1, "Child allocated lane 1" 73 + 74 + def test_alloc_shared_multiple_lanes(self): 75 + """Multiple children allocate different lanes from same parent frame.""" 76 + env = simpy.Environment() 77 + events = [] 78 + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 79 + pe = ProcessingElement(env=env, pe_id=0, config=config) 80 + 81 + # Parent ALLOC 82 + fct_parent = FrameControlToken( 83 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 84 + ) 85 + inject_and_run(env, pe, fct_parent) 86 + parent_frame_id, _parent_lane = pe.tag_store[0] 87 + 88 + # Child 1 ALLOC_SHARED 89 + fct_child1 = FrameControlToken( 90 + target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 91 + ) 92 + inject_and_run(env, pe, fct_child1) 93 + _child1_frame_id, child1_lane = pe.tag_store[1] 94 + 95 + # Child 2 ALLOC_SHARED 96 + fct_child2 = FrameControlToken( 97 + target=0, act_id=2, op=FrameOp.ALLOC_SHARED, payload=0 98 + ) 99 + inject_and_run(env, pe, fct_child2) 100 + _child2_frame_id, child2_lane = pe.tag_store[2] 101 + 102 + # All should share same frame 103 + assert pe.tag_store[0][0] == parent_frame_id 104 + assert pe.tag_store[1][0] == parent_frame_id 105 + assert pe.tag_store[2][0] == parent_frame_id 106 + 107 + # Lanes should differ: 0, 1, 2 108 + assert child1_lane != 0, "Child1 lane should not be 0" 109 + assert child2_lane != 0, "Child2 lane should not be 0" 110 + assert child1_lane != child2_lane, "Child1 and child2 lanes should differ" 111 + 112 + def test_alloc_shared_invalid_parent(self): 113 + """ALLOC_SHARED with non-existent parent emits TokenRejected.""" 114 + env = simpy.Environment() 115 + events = [] 116 + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 117 + pe = ProcessingElement(env=env, pe_id=0, config=config) 118 + 119 + # Try ALLOC_SHARED with non-existent parent_act_id=999 120 + fct = FrameControlToken( 121 + target=0, act_id=0, op=FrameOp.ALLOC_SHARED, payload=999 122 + ) 123 + inject_and_run(env, pe, fct) 124 + 125 + rejected = [e for e in events if isinstance(e, TokenRejected)] 126 + assert len(rejected) > 0, "Should have TokenRejected event" 127 + assert "not in tag store" in rejected[0].reason, "Reason should mention tag_store" 128 + 129 + # Parent should not be in tag_store 130 + assert 999 not in pe.tag_store 131 + 132 + 133 + class TestLaneExhaustion: 134 + """AC3.6, AC8.2: Lane exhaustion and TokenRejected.""" 135 + 136 + def test_alloc_shared_exhausts_all_lanes(self): 137 + """Allocate all lanes, then ALLOC_SHARED fails with TokenRejected.""" 138 + env = simpy.Environment() 139 + events = [] 140 + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 141 + pe = ProcessingElement(env=env, pe_id=0, config=config) 142 + 143 + # Parent ALLOC uses lane 0 144 + fct_parent = FrameControlToken( 145 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 146 + ) 147 + inject_and_run(env, pe, fct_parent) 148 + 149 + # Allocate lanes 1, 2, 3 150 + for i in range(1, 4): 151 + fct = FrameControlToken( 152 + target=0, act_id=i, op=FrameOp.ALLOC_SHARED, payload=0 153 + ) 154 + inject_and_run(env, pe, fct) 155 + assert i in pe.tag_store, f"Child {i} should be allocated" 156 + 157 + # Try to allocate one more (all lanes exhausted) 158 + fct_fail = FrameControlToken( 159 + target=0, act_id=4, op=FrameOp.ALLOC_SHARED, payload=0 160 + ) 161 + inject_and_run(env, pe, fct_fail) 162 + 163 + rejected = [e for e in events if isinstance(e, TokenRejected)] 164 + assert len(rejected) > 0, "Should have TokenRejected event" 165 + assert "no free lanes" in rejected[0].reason, "Reason should be 'no free lanes'" 166 + 167 + # act_id=4 should not be in tag_store 168 + assert 4 not in pe.tag_store, "Failed allocation should not add to tag_store" 169 + 170 + def test_lane_exhaustion_with_multiple_frames(self): 171 + """Lane exhaustion is per-frame; different frames have independent lanes.""" 172 + env = simpy.Environment() 173 + events = [] 174 + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 175 + pe = ProcessingElement(env=env, pe_id=0, config=config) 176 + 177 + # Frame 1: Parent 0 allocates lane 0 178 + fct1 = FrameControlToken( 179 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 180 + ) 181 + inject_and_run(env, pe, fct1) 182 + frame1_id, _lane = pe.tag_store[0] 183 + 184 + # Frame 2: Parent 10 allocates lane 0 185 + fct2 = FrameControlToken( 186 + target=0, act_id=10, op=FrameOp.ALLOC, payload=0 187 + ) 188 + inject_and_run(env, pe, fct2) 189 + frame2_id, _lane = pe.tag_store[10] 190 + 191 + assert frame1_id != frame2_id, "Should allocate different frames" 192 + 193 + # Frame 1: Exhaust all lanes 194 + for i in range(1, 4): 195 + fct = FrameControlToken( 196 + target=0, act_id=i, op=FrameOp.ALLOC_SHARED, payload=0 197 + ) 198 + inject_and_run(env, pe, fct) 199 + 200 + # Frame 2: Can still allocate more lanes (independent) 201 + for i in range(11, 14): 202 + fct = FrameControlToken( 203 + target=0, act_id=i, op=FrameOp.ALLOC_SHARED, payload=10 204 + ) 205 + inject_and_run(env, pe, fct) 206 + assert i in pe.tag_store, f"Frame2 child {i} should be allocated" 207 + 208 + 209 + class TestFreeLane: 210 + """AC3.2: FREE_LANE clears lane data, keeps frame, returns lane to pool.""" 211 + 212 + def test_free_lane_basic(self): 213 + """FREE_LANE removes act_id from tag_store, clears lane data, keeps frame.""" 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 + # Parent ALLOC 220 + fct_parent = FrameControlToken( 221 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 222 + ) 223 + inject_and_run(env, pe, fct_parent) 224 + parent_frame_id, _parent_lane = pe.tag_store[0] 225 + 226 + # Child ALLOC_SHARED 227 + fct_child = FrameControlToken( 228 + target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 229 + ) 230 + inject_and_run(env, pe, fct_child) 231 + _child_frame_id, child_lane = pe.tag_store[1] 232 + 233 + # FREE_LANE for child 234 + fct_free = FrameControlToken( 235 + target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0 236 + ) 237 + inject_and_run(env, pe, fct_free) 238 + 239 + # Child should be removed from tag_store 240 + assert 1 not in pe.tag_store, "Child should be removed from tag_store" 241 + 242 + # Parent should still be present 243 + assert 0 in pe.tag_store, "Parent should still be in tag_store" 244 + 245 + # Frame should NOT be in free_frames (still used by parent) 246 + assert parent_frame_id not in pe.free_frames, "Frame should not be free" 247 + 248 + # FrameFreed event should have frame_freed=False 249 + frame_freed = [e for e in events if isinstance(e, FrameFreed)] 250 + assert len(frame_freed) > 0, "Should have FrameFreed event" 251 + assert frame_freed[-1].frame_freed == False, "frame_freed should be False" 252 + assert frame_freed[-1].lane == child_lane, "Event should report correct lane" 253 + 254 + def test_free_lane_returns_lane_to_pool(self): 255 + """After FREE_LANE, freed lane can be reused by ALLOC_SHARED.""" 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 1 ALLOC_SHARED (lane 1) 269 + fct_child1 = FrameControlToken( 270 + target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 271 + ) 272 + inject_and_run(env, pe, fct_child1) 273 + _child1_frame_id, child1_lane = pe.tag_store[1] 274 + assert child1_lane == 1 275 + 276 + # FREE_LANE child 1 277 + fct_free = FrameControlToken( 278 + target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0 279 + ) 280 + inject_and_run(env, pe, fct_free) 281 + 282 + # Child 2 ALLOC_SHARED (should get lane 1 again) 283 + fct_child2 = FrameControlToken( 284 + target=0, act_id=2, op=FrameOp.ALLOC_SHARED, payload=0 285 + ) 286 + inject_and_run(env, pe, fct_child2) 287 + _child2_frame_id, child2_lane = pe.tag_store[2] 288 + 289 + # Lane 1 should be reused for child 2 290 + assert child2_lane == 1, "Freed lane 1 should be reused" 291 + 292 + 293 + class TestIndependentMatching: 294 + """AC8.1: Two act_ids sharing a frame have independent matching.""" 295 + 296 + def test_independent_matching_same_offset(self): 297 + """L operand for act_id 0 does not interfere with L for act_id 1.""" 298 + env = simpy.Environment() 299 + events = [] 300 + config = PEConfig( 301 + frame_count=4, lane_count=4, matchable_offsets=4, on_event=events.append 302 + ) 303 + pe = ProcessingElement(env=env, pe_id=0, config=config) 304 + 305 + # Parent ALLOC 306 + fct_parent = FrameControlToken( 307 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 308 + ) 309 + inject_and_run(env, pe, fct_parent) 310 + parent_frame_id, _parent_lane = pe.tag_store[0] 311 + 312 + # Child ALLOC_SHARED 313 + fct_child = FrameControlToken( 314 + target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 315 + ) 316 + inject_and_run(env, pe, fct_child) 317 + _child_frame_id, child_lane = pe.tag_store[1] 318 + 319 + # Install dyadic instruction at offset 0 320 + inst = Instruction( 321 + opcode=ArithOp.ADD, 322 + output=OutputStyle.SINK, 323 + has_const=False, 324 + dest_count=0, 325 + wide=False, 326 + fref=0, 327 + ) 328 + pe.iram[0] = inst 329 + 330 + # Send L operand for act_id=0 331 + tok_l_0 = DyadToken( 332 + target=0, offset=0, act_id=0, data=5, port=Port.L 333 + ) 334 + inject_and_run(env, pe, tok_l_0) 335 + 336 + # Should have 1 TokenReceived, 0 Matched (waiting for R) 337 + matched = [e for e in events if isinstance(e, Matched)] 338 + assert len(matched) == 0, "Should not match yet (waiting for R)" 339 + 340 + # Send L operand for act_id=1 at same offset 341 + tok_l_1 = DyadToken( 342 + target=0, offset=0, act_id=1, data=7, port=Port.L 343 + ) 344 + inject_and_run(env, pe, tok_l_1) 345 + 346 + # Should still have 0 Matched (both waiting for R) 347 + matched = [e for e in events if isinstance(e, Matched)] 348 + assert len(matched) == 0, "Both should be waiting for R" 349 + 350 + # Send R for act_id=0 351 + tok_r_0 = DyadToken( 352 + target=0, offset=0, act_id=0, data=3, port=Port.R 353 + ) 354 + inject_and_run(env, pe, tok_r_0) 355 + 356 + # Should now have 1 Matched for act_id=0 357 + matched = [e for e in events if isinstance(e, Matched)] 358 + assert len(matched) == 1, "Should have 1 match for act_id=0" 359 + assert matched[0].act_id == 0, "Match should be for act_id=0" 360 + assert matched[0].left == 5, "Left should be 5" 361 + assert matched[0].right == 3, "Right should be 3" 362 + 363 + # Send R for act_id=1 364 + tok_r_1 = DyadToken( 365 + target=0, offset=0, act_id=1, data=2, port=Port.R 366 + ) 367 + inject_and_run(env, pe, tok_r_1) 368 + 369 + # Should now have 2 Matched 370 + matched = [e for e in events if isinstance(e, Matched)] 371 + assert len(matched) == 2, "Should have 2 matches total" 372 + m1 = [m for m in matched if m.act_id == 1][0] 373 + assert m1.left == 7, "act_id=1 left should be 7" 374 + assert m1.right == 2, "act_id=1 right should be 2" 375 + 376 + def test_independent_matching_different_offsets(self): 377 + """Different offsets per lane maintain independence.""" 378 + env = simpy.Environment() 379 + events = [] 380 + config = PEConfig( 381 + frame_count=4, lane_count=4, matchable_offsets=4, on_event=events.append 382 + ) 383 + pe = ProcessingElement(env=env, pe_id=0, config=config) 384 + 385 + # Parent ALLOC 386 + fct_parent = FrameControlToken( 387 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 388 + ) 389 + inject_and_run(env, pe, fct_parent) 390 + 391 + # Child ALLOC_SHARED 392 + fct_child = FrameControlToken( 393 + target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 394 + ) 395 + inject_and_run(env, pe, fct_child) 396 + 397 + # Install dyadic instructions at offsets 0 and 1 398 + inst0 = Instruction( 399 + opcode=ArithOp.ADD, output=OutputStyle.SINK, 400 + has_const=False, dest_count=0, wide=False, fref=0 401 + ) 402 + inst1 = Instruction( 403 + opcode=ArithOp.SUB, output=OutputStyle.SINK, 404 + has_const=False, dest_count=0, wide=False, fref=0 405 + ) 406 + pe.iram[0] = inst0 407 + pe.iram[1] = inst1 408 + 409 + # Send L for act_id=0 at offset 0 410 + tok_l_0_off0 = DyadToken( 411 + target=0, offset=0, act_id=0, data=10, port=Port.L 412 + ) 413 + inject_and_run(env, pe, tok_l_0_off0) 414 + 415 + # Send L for act_id=1 at offset 1 416 + tok_l_1_off1 = DyadToken( 417 + target=0, offset=1, act_id=1, data=20, port=Port.L 418 + ) 419 + inject_and_run(env, pe, tok_l_1_off1) 420 + 421 + # Neither should match yet 422 + matched = [e for e in events if isinstance(e, Matched)] 423 + assert len(matched) == 0, "No matches yet" 424 + 425 + # Send R for act_id=0 at offset 0 426 + tok_r_0_off0 = DyadToken( 427 + target=0, offset=0, act_id=0, data=5, port=Port.R 428 + ) 429 + inject_and_run(env, pe, tok_r_0_off0) 430 + 431 + # Should match for offset 0 432 + matched = [e for e in events if isinstance(e, Matched)] 433 + assert len(matched) == 1, "Should have 1 match" 434 + assert matched[0].offset == 0, "Match should be at offset 0" 435 + 436 + # Send R for act_id=1 at offset 1 437 + tok_r_1_off1 = DyadToken( 438 + target=0, offset=1, act_id=1, data=15, port=Port.R 439 + ) 440 + inject_and_run(env, pe, tok_r_1_off1) 441 + 442 + # Should match for offset 1 443 + matched = [e for e in events if isinstance(e, Matched)] 444 + assert len(matched) == 2, "Should have 2 matches" 445 + m1 = [m for m in matched if m.offset == 1][0] 446 + assert m1.act_id == 1, "Offset 1 match should be act_id=1" 447 + 448 + 449 + class TestSmartFree: 450 + """AC3.3, AC8.3: Smart FREE on shared frames preserves data and manages lanes.""" 451 + 452 + def test_free_on_shared_frame_preserves_other_lanes(self): 453 + """FREE on act_id=0 when act_id=1 uses frame; lane 1 data preserved.""" 454 + env = simpy.Environment() 455 + events = [] 456 + config = PEConfig( 457 + frame_count=4, lane_count=4, matchable_offsets=4, on_event=events.append 458 + ) 459 + pe = ProcessingElement(env=env, pe_id=0, config=config) 460 + 461 + # Parent ALLOC 462 + fct_parent = FrameControlToken( 463 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 464 + ) 465 + inject_and_run(env, pe, fct_parent) 466 + parent_frame_id, _parent_lane = pe.tag_store[0] 467 + 468 + # Child ALLOC_SHARED 469 + fct_child = FrameControlToken( 470 + target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 471 + ) 472 + inject_and_run(env, pe, fct_child) 473 + _child_frame_id, child_lane = pe.tag_store[1] 474 + 475 + # Install instruction 476 + inst = Instruction( 477 + opcode=ArithOp.ADD, output=OutputStyle.SINK, 478 + has_const=False, dest_count=0, wide=False, fref=0 479 + ) 480 + pe.iram[0] = inst 481 + 482 + # Store L operand on child's lane 483 + tok_l_1 = DyadToken( 484 + target=0, offset=0, act_id=1, data=7, port=Port.L 485 + ) 486 + inject_and_run(env, pe, tok_l_1) 487 + 488 + # Verify child's match slot has data 489 + frame_id, lane = pe.tag_store[1] 490 + assert pe.match_data[frame_id][0][lane] == 7, "Child lane should have L operand" 491 + assert pe.presence[frame_id][0][lane] == True, "Child presence should be set" 492 + 493 + # FREE parent 494 + fct_free_parent = FrameControlToken( 495 + target=0, act_id=0, op=FrameOp.FREE, payload=0 496 + ) 497 + inject_and_run(env, pe, fct_free_parent) 498 + 499 + # Parent should be removed, child should still be present 500 + assert 0 not in pe.tag_store, "Parent should be removed" 501 + assert 1 in pe.tag_store, "Child should still be present" 502 + 503 + # Frame should NOT be in free_frames 504 + assert parent_frame_id not in pe.free_frames, "Frame should not be free" 505 + 506 + # Child's match data should be preserved 507 + assert pe.match_data[frame_id][0][lane] == 7, "Child data should be preserved" 508 + assert pe.presence[frame_id][0][lane] == True, "Child presence should be preserved" 509 + 510 + # FrameFreed event should have frame_freed=False 511 + frame_freed = [e for e in events if isinstance(e, FrameFreed)] 512 + assert any(e.frame_freed == False for e in frame_freed), "Should have frame_freed=False" 513 + 514 + def test_free_last_lane_returns_frame(self): 515 + """FREE on last act_id using frame returns frame to free_frames.""" 516 + env = simpy.Environment() 517 + events = [] 518 + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 519 + pe = ProcessingElement(env=env, pe_id=0, config=config) 520 + 521 + # Parent ALLOC 522 + fct_parent = FrameControlToken( 523 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 524 + ) 525 + inject_and_run(env, pe, fct_parent) 526 + parent_frame_id, _parent_lane = pe.tag_store[0] 527 + 528 + # Child ALLOC_SHARED 529 + fct_child = FrameControlToken( 530 + target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 531 + ) 532 + inject_and_run(env, pe, fct_child) 533 + 534 + # FREE child 535 + fct_free_child = FrameControlToken( 536 + target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0 537 + ) 538 + inject_and_run(env, pe, fct_free_child) 539 + 540 + # Frame should still not be free (parent still using it) 541 + assert parent_frame_id not in pe.free_frames, "Frame should not be free yet" 542 + 543 + # FREE parent 544 + fct_free_parent = FrameControlToken( 545 + target=0, act_id=0, op=FrameOp.FREE, payload=0 546 + ) 547 + inject_and_run(env, pe, fct_free_parent) 548 + 549 + # Now frame should be free 550 + assert parent_frame_id in pe.free_frames, "Frame should be free" 551 + 552 + # tag_store should be empty 553 + assert len(pe.tag_store) == 0, "tag_store should be empty" 554 + 555 + # lane_free entry should be cleaned up 556 + assert parent_frame_id not in pe.lane_free, "lane_free entry should be cleaned" 557 + 558 + # FrameFreed event should have frame_freed=True 559 + frame_freed = [e for e in events if isinstance(e, FrameFreed)] 560 + assert any(e.frame_freed == True for e in frame_freed), "Should have frame_freed=True" 561 + 562 + def test_alloc_unchanged_allocates_fresh_frame(self): 563 + """Regular ALLOC still works: allocates fresh frame, lane 0.""" 564 + env = simpy.Environment() 565 + events = [] 566 + config = PEConfig(frame_count=4, lane_count=4, on_event=events.append) 567 + pe = ProcessingElement(env=env, pe_id=0, config=config) 568 + 569 + # First ALLOC 570 + fct1 = FrameControlToken( 571 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 572 + ) 573 + inject_and_run(env, pe, fct1) 574 + frame_id_0, lane_0 = pe.tag_store[0] 575 + assert lane_0 == 0, "First ALLOC should assign lane 0" 576 + 577 + # Second ALLOC (different frame) 578 + fct2 = FrameControlToken( 579 + target=0, act_id=10, op=FrameOp.ALLOC, payload=0 580 + ) 581 + inject_and_run(env, pe, fct2) 582 + frame_id_10, lane_10 = pe.tag_store[10] 583 + assert lane_10 == 0, "Second ALLOC should assign lane 0" 584 + 585 + # Frames should be different 586 + assert frame_id_0 != frame_id_10, "Different ALLOC should get different frames" 587 + 588 + def test_data_preservation_across_free_lanes(self): 589 + """Match data on one lane not affected by FREE of another lane.""" 590 + env = simpy.Environment() 591 + events = [] 592 + config = PEConfig( 593 + frame_count=4, lane_count=4, matchable_offsets=4, on_event=events.append 594 + ) 595 + pe = ProcessingElement(env=env, pe_id=0, config=config) 596 + 597 + # Parent ALLOC 598 + fct_parent = FrameControlToken( 599 + target=0, act_id=0, op=FrameOp.ALLOC, payload=0 600 + ) 601 + inject_and_run(env, pe, fct_parent) 602 + frame_id, _parent_lane = pe.tag_store[0] 603 + 604 + # Child 1 ALLOC_SHARED 605 + fct_child1 = FrameControlToken( 606 + target=0, act_id=1, op=FrameOp.ALLOC_SHARED, payload=0 607 + ) 608 + inject_and_run(env, pe, fct_child1) 609 + _frame_id_1, lane_1 = pe.tag_store[1] 610 + 611 + # Child 2 ALLOC_SHARED 612 + fct_child2 = FrameControlToken( 613 + target=0, act_id=2, op=FrameOp.ALLOC_SHARED, payload=0 614 + ) 615 + inject_and_run(env, pe, fct_child2) 616 + _frame_id_2, lane_2 = pe.tag_store[2] 617 + 618 + # Install instruction 619 + inst = Instruction( 620 + opcode=ArithOp.ADD, output=OutputStyle.SINK, 621 + has_const=False, dest_count=0, wide=False, fref=0 622 + ) 623 + pe.iram[0] = inst 624 + 625 + # Store L operand on lane 1 626 + tok_l_1 = DyadToken( 627 + target=0, offset=0, act_id=1, data=7, port=Port.L 628 + ) 629 + inject_and_run(env, pe, tok_l_1) 630 + 631 + # Store L operand on lane 2 632 + tok_l_2 = DyadToken( 633 + target=0, offset=0, act_id=2, data=11, port=Port.L 634 + ) 635 + inject_and_run(env, pe, tok_l_2) 636 + 637 + # FREE lane 1 638 + fct_free_1 = FrameControlToken( 639 + target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0 640 + ) 641 + inject_and_run(env, pe, fct_free_1) 642 + 643 + # Lane 2's data should be untouched 644 + assert pe.match_data[frame_id][0][lane_2] == 11, "Lane 2 data should be preserved" 645 + assert pe.presence[frame_id][0][lane_2] == True, "Lane 2 presence should be preserved" 646 + 647 + # Lane 1 should be cleared 648 + assert pe.match_data[frame_id][0][lane_1] is None, "Lane 1 data should be cleared" 649 + assert pe.presence[frame_id][0][lane_1] == False, "Lane 1 presence should be cleared"