OR-1 dataflow CPU sketch
at pe-frame-redesign 1705 lines 56 kB view raw
1""" 2Frame-based PE rewrite tests. 3 4Verifies pe-frame-redesign.AC3 and pe-frame-redesign.AC1.6: 5- AC3.1: Frame count/slots/matchable_offsets are configurable 6- AC3.2: Pipeline order is IFETCH → act_id resolution → MATCH/FRAME → EXECUTE → EMIT 7- AC3.3: Dyadic matching uses tag_store + presence bits + frame SRAM 8- AC3.4: INHERIT output reads FrameDest from frame and routes token 9- AC3.5: CHANGE_TAG unpacks left operand (flit 1) to get destination 10- AC3.6: SINK writes result to frame slot, emits no token 11- AC3.7: EXTRACT_TAG produces packed flit 1 with PE/offset/act_id/port/kind 12- AC3.8: ALLOC/FREE frame control, FREE_FRAME opcode, ALLOC_REMOTE remote allocation 13- AC3.9: PELocalWriteToken with is_dest=True decodes FrameDest 14- AC3.10: Pipeline timing: 5 cycles dyadic, 4 cycles monadic, 2 cycles side paths 15- AC1.6: Invalid act_id emits TokenRejected, doesn't crash 16""" 17 18import pytest 19import simpy 20 21from cm_inst import ( 22 ArithOp, FrameDest, FrameOp, Instruction, LogicOp, MemOp, 23 OutputStyle, Port, RoutingOp, TokenKind, 24) 25from encoding import pack_flit1, unpack_flit1, pack_instruction, unpack_instruction 26from emu.events import ( 27 Emitted, Executed, FrameAllocated, FrameFreed, FrameSlotWritten, 28 Matched, TokenReceived, TokenRejected, 29) 30from emu.pe import ProcessingElement 31from emu.types import PEConfig 32from tokens import ( 33 DyadToken, FrameControlToken, MonadToken, PELocalWriteToken, SMToken, 34) 35 36 37def inject_and_run(env, pe, token): 38 """Helper: inject token and run simulation.""" 39 def _put(): 40 yield pe.input_store.put(token) 41 env.process(_put()) 42 env.run() 43 44 45class TestFrameConfiguration: 46 """AC3.1: PE constructor accepts frame_count, frame_slots, matchable_offsets.""" 47 48 def test_constructor_default_params(self): 49 env = simpy.Environment() 50 pe = ProcessingElement( 51 env=env, 52 pe_id=0, 53 config=PEConfig(), 54 ) 55 # Default config should have frame_count, frame_slots, matchable_offsets 56 assert pe.frame_count > 0 57 assert pe.frame_slots > 0 58 assert pe.matchable_offsets > 0 59 60 def test_constructor_custom_params(self): 61 env = simpy.Environment() 62 config = PEConfig( 63 frame_count=4, 64 frame_slots=32, 65 matchable_offsets=4, 66 ) 67 pe = ProcessingElement( 68 env=env, 69 pe_id=1, 70 config=config, 71 ) 72 assert pe.frame_count == 4 73 assert pe.frame_slots == 32 74 assert pe.matchable_offsets == 4 75 76 77class TestFrameAllocationAndFree: 78 """AC3.8: Frame allocation (ALLOC) and deallocation (FREE) via FrameControlToken.""" 79 80 def test_alloc_frame_control_token(self): 81 env = simpy.Environment() 82 events = [] 83 config = PEConfig(frame_count=4, on_event=events.append) 84 pe = ProcessingElement( 85 env=env, 86 pe_id=0, 87 config=config, 88 ) 89 90 # Inject FrameControlToken(ALLOC) for act_id=0 91 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 92 inject_and_run(env, pe, fct) 93 94 # Should have TokenReceived and FrameAllocated events 95 token_received = [e for e in events if isinstance(e, TokenReceived)] 96 frame_allocated = [e for e in events if isinstance(e, FrameAllocated)] 97 assert len(token_received) > 0 98 assert len(frame_allocated) > 0 99 assert pe.tag_store[0][0] in range(pe.frame_count) 100 assert frame_allocated[0].lane == 0 101 102 def test_free_frame_control_token(self): 103 env = simpy.Environment() 104 events = [] 105 config = PEConfig(frame_count=4, on_event=events.append) 106 pe = ProcessingElement( 107 env=env, 108 pe_id=0, 109 config=config, 110 ) 111 112 # Allocate first 113 fct_alloc = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 114 inject_and_run(env, pe, fct_alloc) 115 116 frame_id, _lane = pe.tag_store[0] 117 118 # Now deallocate 119 fct_free = FrameControlToken(target=0, act_id=0, op=FrameOp.FREE, payload=0) 120 inject_and_run(env, pe, fct_free) 121 122 # Should have FrameFreed event and tag_store should be cleared 123 frame_freed = [e for e in events if isinstance(e, FrameFreed)] 124 assert len(frame_freed) > 0 125 assert frame_freed[0].lane == 0 126 assert frame_freed[0].frame_freed == True 127 assert 0 not in pe.tag_store 128 assert frame_id in pe.free_frames 129 130 131class TestDyadicMatching: 132 """AC3.3: Dyadic matching uses tag_store + presence bits + frame SRAM.""" 133 134 def test_dyadic_token_pair_matching(self): 135 env = simpy.Environment() 136 events = [] 137 config = PEConfig(frame_count=4, matchable_offsets=4, on_event=events.append) 138 pe = ProcessingElement( 139 env=env, 140 pe_id=0, 141 config=config, 142 ) 143 144 # Set up: allocate frame for act_id=0 145 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 146 inject_and_run(env, pe, fct) 147 148 # Set up: install dyadic instruction at offset 0 149 # Mode SINK: no output emission, just execution and matching verification 150 inst = Instruction( 151 opcode=ArithOp.ADD, 152 output=OutputStyle.SINK, 153 has_const=False, 154 dest_count=0, 155 wide=False, 156 fref=0, 157 ) 158 pe.iram[0] = inst 159 160 # Set up: write destination FrameDest to frame slot 0 161 dest = FrameDest( 162 target_pe=0, 163 offset=1, 164 act_id=0, 165 port=Port.L, 166 token_kind=TokenKind.DYADIC, 167 ) 168 pe.frames[pe.tag_store[0][0]][0] = dest 169 170 # Inject first dyadic token (port=L, data=5) 171 tok1 = DyadToken( 172 target=0, 173 offset=0, 174 act_id=0, 175 data=5, 176 port=Port.L, 177 ) 178 inject_and_run(env, pe, tok1) 179 180 # Should have TokenReceived, no match yet (waiting for partner) 181 token_received = [e for e in events if isinstance(e, TokenReceived)] 182 matched = [e for e in events if isinstance(e, Matched)] 183 assert len(token_received) >= 1 184 assert len(matched) == 0 185 186 # Inject second dyadic token (port=R, data=3) 187 tok2 = DyadToken( 188 target=0, 189 offset=0, 190 act_id=0, 191 data=3, 192 port=Port.R, 193 ) 194 inject_and_run(env, pe, tok2) 195 196 # Should now have Matched event 197 matched = [e for e in events if isinstance(e, Matched)] 198 assert len(matched) > 0 199 m = matched[0] 200 assert m.left == 5 201 assert m.right == 3 202 203 204class TestInheritOutput: 205 """AC3.4: INHERIT output reads FrameDest from frame and constructs token.""" 206 207 def test_inherit_single_dest(self): 208 env = simpy.Environment() 209 events = [] 210 config = PEConfig(frame_count=4, on_event=events.append) 211 pe = ProcessingElement( 212 env=env, 213 pe_id=0, 214 config=config, 215 ) 216 217 # Allocate frame 218 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 219 inject_and_run(env, pe, fct) 220 frame_id, _lane = pe.tag_store[0] 221 222 # Set up instruction: mode 0 (no const, dest_count=1), fref=8 223 inst = Instruction( 224 opcode=ArithOp.INC, 225 output=OutputStyle.INHERIT, 226 has_const=False, 227 dest_count=1, 228 wide=False, 229 fref=8, 230 ) 231 pe.iram[2] = inst 232 233 # Set destination at frame[8] 234 dest = FrameDest( 235 target_pe=0, 236 offset=5, 237 act_id=1, 238 port=Port.L, 239 token_kind=TokenKind.MONADIC, 240 ) 241 pe.frames[frame_id][8] = dest 242 243 # Wire route table 244 pe.route_table[0] = simpy.Store(env) 245 246 # Inject monadic token 247 tok = MonadToken( 248 target=0, 249 offset=2, 250 act_id=0, 251 data=10, 252 inline=False, 253 ) 254 inject_and_run(env, pe, tok) 255 256 # Should have Emitted event with output token routed to target_pe=0, offset=5, act_id=1 257 emitted = [e for e in events if isinstance(e, Emitted)] 258 assert len(emitted) > 0 259 out_token = emitted[0].token 260 assert out_token.target == 0 261 assert out_token.offset == 5 262 assert out_token.act_id == 1 263 264 265class TestChangeTagOutput: 266 """AC3.5: CHANGE_TAG unpacks left operand (flit 1) to get destination.""" 267 268 def test_change_tag_output(self): 269 env = simpy.Environment() 270 events = [] 271 config = PEConfig(frame_count=4, on_event=events.append) 272 pe = ProcessingElement( 273 env=env, 274 pe_id=0, 275 config=config, 276 ) 277 278 # Allocate frame 279 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 280 inject_and_run(env, pe, fct) 281 282 # Set up instruction: CHANGE_TAG output, mode 4 (no const, dest_count=1) 283 inst = Instruction( 284 opcode=ArithOp.ADD, 285 output=OutputStyle.CHANGE_TAG, 286 has_const=False, 287 dest_count=1, 288 wide=False, 289 fref=0, 290 ) 291 pe.iram[3] = inst 292 293 # Wire route table 294 pe.route_table[1] = simpy.Store(env) 295 296 # Construct a packed flit 1 for destination (pe=1, offset=7, act_id=2, port=R, kind=DYADIC) 297 dest = FrameDest( 298 target_pe=1, 299 offset=7, 300 act_id=2, 301 port=Port.R, 302 token_kind=TokenKind.DYADIC, 303 ) 304 flit1 = pack_flit1(dest) 305 306 # Inject dyadic token pair: first (L) carries the flit1 as left operand 307 tok_l = DyadToken( 308 target=0, 309 offset=3, 310 act_id=0, 311 data=flit1, # packed flit 1 312 port=Port.L, 313 ) 314 inject_and_run(env, pe, tok_l) 315 316 # Inject second (R) with some data value 317 tok_r = DyadToken( 318 target=0, 319 offset=3, 320 act_id=0, 321 data=100, 322 port=Port.R, 323 ) 324 inject_and_run(env, pe, tok_r) 325 326 # Should emit token with target=1, offset=7, act_id=2 327 emitted = [e for e in events if isinstance(e, Emitted)] 328 assert len(emitted) > 0 329 out_token = emitted[-1].token # Last emitted token 330 assert out_token.target == 1 331 assert out_token.offset == 7 332 assert out_token.act_id == 2 333 334 335class TestSinkOutput: 336 """AC3.6: SINK output writes result to frame slot, emits no token.""" 337 338 def test_sink_writes_to_frame(self): 339 env = simpy.Environment() 340 events = [] 341 config = PEConfig(frame_count=4, on_event=events.append) 342 pe = ProcessingElement( 343 env=env, 344 pe_id=0, 345 config=config, 346 ) 347 348 # Allocate frame 349 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 350 inject_and_run(env, pe, fct) 351 frame_id, _lane = pe.tag_store[0] 352 353 # Set up instruction: SINK output, mode 6 (no const, dest_count=0), fref=10 354 inst = Instruction( 355 opcode=ArithOp.INC, 356 output=OutputStyle.SINK, 357 has_const=False, 358 dest_count=0, 359 wide=False, 360 fref=10, 361 ) 362 pe.iram[4] = inst 363 364 # Inject monadic token 365 tok = MonadToken( 366 target=0, 367 offset=4, 368 act_id=0, 369 data=42, 370 inline=False, 371 ) 372 inject_and_run(env, pe, tok) 373 374 # Should have FrameSlotWritten event and NO Emitted event 375 slot_written = [e for e in events if isinstance(e, FrameSlotWritten)] 376 emitted = [e for e in events if isinstance(e, Emitted)] 377 assert len(slot_written) > 0 378 assert slot_written[0].slot == 10 379 assert slot_written[0].value == 43 # INC(42) = 43 380 assert len(emitted) == 0 # SINK doesn't emit 381 382 383class TestExtractTag: 384 """AC3.7: EXTRACT_TAG produces packed flit 1 with PE/offset/act_id/port/kind.""" 385 386 def test_extract_tag_monadic(self): 387 env = simpy.Environment() 388 events = [] 389 config = PEConfig(frame_count=4, on_event=events.append) 390 pe = ProcessingElement( 391 env=env, 392 pe_id=2, 393 config=config, 394 ) 395 396 # Allocate frame 397 fct = FrameControlToken(target=2, act_id=0, op=FrameOp.ALLOC, payload=0) 398 inject_and_run(env, pe, fct) 399 400 # Set up EXTRACT_TAG instruction 401 inst = Instruction( 402 opcode=RoutingOp.EXTRACT_TAG, 403 output=OutputStyle.INHERIT, 404 has_const=False, 405 dest_count=1, 406 wide=False, 407 fref=0, 408 ) 409 pe.iram[5] = inst 410 411 # Set output destination at frame[0] 412 dest = FrameDest( 413 target_pe=0, 414 offset=10, 415 act_id=0, 416 port=Port.L, 417 token_kind=TokenKind.MONADIC, 418 ) 419 pe.frames[pe.tag_store[0][0]][0] = dest 420 421 # Wire route table 422 pe.route_table[0] = simpy.Store(env) 423 424 # Inject monadic token at offset 5, act_id 0 425 tok = MonadToken( 426 target=2, 427 offset=5, 428 act_id=0, 429 data=999, # ignored by EXTRACT_TAG 430 inline=False, 431 ) 432 inject_and_run(env, pe, tok) 433 434 # Should have Executed event showing EXTRACT_TAG 435 executed = [e for e in events if isinstance(e, Executed)] 436 assert len(executed) > 0 437 assert executed[0].op == RoutingOp.EXTRACT_TAG 438 439 # Output should be a packed flit 1 for (pe=2, offset=5, act_id=0) 440 emitted = [e for e in events if isinstance(e, Emitted)] 441 assert len(emitted) > 0 442 out_token = emitted[0].token 443 # The result should encode (pe=2, offset=5, act_id=0, port=?, kind=?) 444 # Unpack and verify 445 flit1_val = out_token.data 446 unpacked = unpack_flit1(flit1_val) 447 assert unpacked.target_pe == 2 448 assert unpacked.offset == 5 449 assert unpacked.act_id == 0 450 451 452class TestAllocRemote: 453 """AC3.8: ALLOC_REMOTE reads target PE and act_id from frame, sends FrameControlToken.""" 454 455 def test_alloc_remote(self): 456 env = simpy.Environment() 457 events = [] 458 pe_events = [] 459 config = PEConfig(frame_count=4, on_event=events.append) 460 pe = ProcessingElement( 461 env=env, 462 pe_id=0, 463 config=config, 464 ) 465 466 # Create a second PE to receive ALLOC 467 config1 = PEConfig(frame_count=4, on_event=pe_events.append) 468 pe1 = ProcessingElement( 469 env=env, 470 pe_id=1, 471 config=config1, 472 ) 473 474 # Wire route_table for PE0 to reach PE1 475 pe.route_table[1] = pe1.input_store 476 477 # Allocate frame for act_id=0 on PE0 478 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 479 inject_and_run(env, pe, fct) 480 481 # Set up ALLOC_REMOTE instruction, mode 6 (no const, dest_count=0), fref=8 482 inst = Instruction( 483 opcode=RoutingOp.ALLOC_REMOTE, 484 output=OutputStyle.SINK, 485 has_const=False, 486 dest_count=0, 487 wide=False, 488 fref=8, 489 ) 490 pe.iram[6] = inst 491 492 # Write target PE and target act_id to frame slots 8 and 9 493 frame_id, _lane = pe.tag_store[0] 494 pe.frames[frame_id][8] = 1 # target PE 495 pe.frames[frame_id][9] = 2 # target act_id 496 497 # Inject monadic token 498 tok = MonadToken( 499 target=0, 500 offset=6, 501 act_id=0, 502 data=0, 503 inline=False, 504 ) 505 inject_and_run(env, pe, tok) 506 507 # PE1 should have received a FrameControlToken(ALLOC) for act_id=2 508 frame_allocated = [e for e in pe_events if isinstance(e, FrameAllocated)] 509 assert len(frame_allocated) > 0 510 assert frame_allocated[0].act_id == 2 511 assert frame_allocated[0].lane == 0 512 513 514class TestFreeFrameOpcode: 515 """AC3.8: FREE_FRAME opcode deallocates frame, clears tag_store, no output.""" 516 517 def test_free_frame_opcode(self): 518 env = simpy.Environment() 519 events = [] 520 config = PEConfig(frame_count=4, on_event=events.append) 521 pe = ProcessingElement( 522 env=env, 523 pe_id=0, 524 config=config, 525 ) 526 527 # Allocate frame 528 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 529 inject_and_run(env, pe, fct) 530 frame_id, _lane = pe.tag_store[0] 531 532 # Set up FREE_FRAME instruction 533 inst = Instruction( 534 opcode=RoutingOp.FREE_FRAME, 535 output=OutputStyle.SINK, 536 has_const=False, 537 dest_count=0, 538 wide=False, 539 fref=0, 540 ) 541 pe.iram[7] = inst 542 543 # Inject monadic token 544 tok = MonadToken( 545 target=0, 546 offset=7, 547 act_id=0, 548 data=0, 549 inline=False, 550 ) 551 inject_and_run(env, pe, tok) 552 553 # Should have FrameFreed event 554 frame_freed = [e for e in events if isinstance(e, FrameFreed)] 555 assert len(frame_freed) > 0 556 assert frame_freed[0].frame_id == frame_id 557 assert frame_freed[0].lane == 0 558 assert frame_freed[0].frame_freed == True 559 560 # tag_store should be cleared 561 assert 0 not in pe.tag_store 562 563 # frame should be in free_frames 564 assert frame_id in pe.free_frames 565 566 # Should have NO Emitted event (FREE_FRAME suppresses) 567 emitted = [e for e in events if isinstance(e, Emitted)] 568 assert len(emitted) == 0 569 570 571class TestFreeLane: 572 """AC3.8: FREE_LANE deallocates lane, potentially returning frame to free list.""" 573 574 def test_free_lane_on_last_lane_returns_frame(self): 575 """When FREE_LANE is called on the last remaining lane, frame should be returned.""" 576 env = simpy.Environment() 577 events = [] 578 config = PEConfig(frame_count=4, lane_count=2, on_event=events.append) 579 pe = ProcessingElement( 580 env=env, 581 pe_id=0, 582 config=config, 583 ) 584 585 # Allocate a frame with act_id=1 (gets lane 0) 586 fct_alloc1 = FrameControlToken(target=0, act_id=1, op=FrameOp.ALLOC, payload=0) 587 inject_and_run(env, pe, fct_alloc1) 588 589 frame_id, lane1 = pe.tag_store[1] 590 assert lane1 == 0 591 592 # Allocate shared child with act_id=2 (gets lane 1) 593 fct_alloc_shared = FrameControlToken(target=0, act_id=2, op=FrameOp.ALLOC_SHARED, payload=1) 594 inject_and_run(env, pe, fct_alloc_shared) 595 596 frame_id2, lane2 = pe.tag_store[2] 597 assert frame_id2 == frame_id 598 assert lane2 == 1 599 600 # Now FREE_LANE the child (act_id=2) — should not return frame (still in use) 601 fct_free_lane_child = FrameControlToken(target=0, act_id=2, op=FrameOp.FREE_LANE, payload=0) 602 inject_and_run(env, pe, fct_free_lane_child) 603 604 # Lane should be freed, frame still in use 605 assert 2 not in pe.tag_store 606 assert frame_id in pe.lane_free or frame_id not in [fid for fid, _ in pe.tag_store.values()] 607 frame_freed_child = [e for e in events if isinstance(e, FrameFreed) and e.act_id == 2] 608 assert len(frame_freed_child) > 0 609 assert frame_freed_child[0].frame_freed == False # Lane freed, not frame 610 611 # Now FREE_LANE the parent (act_id=1) — this is the last lane, should return frame 612 fct_free_lane_parent = FrameControlToken(target=0, act_id=1, op=FrameOp.FREE_LANE, payload=0) 613 inject_and_run(env, pe, fct_free_lane_parent) 614 615 # Frame should now be in free_frames 616 assert 1 not in pe.tag_store 617 assert frame_id in pe.free_frames 618 frame_freed_parent = [e for e in events if isinstance(e, FrameFreed) and e.act_id == 1] 619 assert len(frame_freed_parent) > 0 620 assert frame_freed_parent[0].frame_freed == True # Last lane, frame returned 621 622 623class TestPELocalWriteToken: 624 """AC3.9: PELocalWriteToken with is_dest=True decodes data to FrameDest.""" 625 626 def test_local_write_iram(self): 627 env = simpy.Environment() 628 config = PEConfig() 629 pe = ProcessingElement( 630 env=env, 631 pe_id=0, 632 config=config, 633 ) 634 635 # Write instruction to IRAM at slot 10 636 inst = Instruction( 637 opcode=ArithOp.ADD, 638 output=OutputStyle.INHERIT, 639 has_const=False, 640 dest_count=1, 641 wide=False, 642 fref=0, 643 ) 644 inst_word = pack_instruction(inst) 645 646 write_tok = PELocalWriteToken( 647 target=0, 648 act_id=0, 649 region=0, # IRAM 650 slot=10, 651 data=inst_word, 652 is_dest=False, 653 ) 654 inject_and_run(env, pe, write_tok) 655 656 # Should have written instruction to pe.iram[10] 657 assert 10 in pe.iram 658 # Unpack and verify 659 loaded_inst = pe.iram[10] 660 assert isinstance(loaded_inst, Instruction) 661 662 def test_local_write_frame_dest(self): 663 env = simpy.Environment() 664 config = PEConfig(frame_count=4) 665 pe = ProcessingElement( 666 env=env, 667 pe_id=0, 668 config=config, 669 ) 670 671 # Allocate frame 672 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 673 inject_and_run(env, pe, fct) 674 frame_id, _lane = pe.tag_store[0] 675 676 # Write FrameDest to frame slot 15, is_dest=True 677 dest = FrameDest( 678 target_pe=1, 679 offset=8, 680 act_id=3, 681 port=Port.R, 682 token_kind=TokenKind.DYADIC, 683 ) 684 flit1 = pack_flit1(dest) 685 686 write_tok = PELocalWriteToken( 687 target=0, 688 act_id=0, 689 region=1, # Frame 690 slot=15, 691 data=flit1, 692 is_dest=True, 693 ) 694 inject_and_run(env, pe, write_tok) 695 696 # Frame slot 15 should contain a FrameDest object 697 slot_val = pe.frames[frame_id][15] 698 assert isinstance(slot_val, FrameDest) 699 assert slot_val.target_pe == 1 700 assert slot_val.offset == 8 701 assert slot_val.act_id == 3 702 703 704class TestInvalidActId: 705 """AC1.6: Invalid act_id emits TokenRejected, doesn't crash.""" 706 707 def test_invalid_act_id_rejected(self): 708 env = simpy.Environment() 709 events = [] 710 config = PEConfig(frame_count=4, on_event=events.append) 711 pe = ProcessingElement( 712 env=env, 713 pe_id=0, 714 config=config, 715 ) 716 717 # Set up instruction at offset 0 (so we get past IFETCH) 718 inst = Instruction( 719 opcode=ArithOp.INC, 720 output=OutputStyle.INHERIT, 721 has_const=False, 722 dest_count=1, 723 wide=False, 724 fref=0, 725 ) 726 pe.iram[0] = inst 727 728 # Inject token with act_id not in tag_store 729 tok = MonadToken( 730 target=0, 731 offset=0, 732 act_id=99, # not allocated 733 data=0, 734 inline=False, 735 ) 736 inject_and_run(env, pe, tok) 737 738 # Should have TokenRejected event 739 rejected = [e for e in events if isinstance(e, TokenRejected)] 740 assert len(rejected) > 0 741 assert rejected[0].token == tok 742 743 744class TestDualDestInherit: 745 """IMPORTANT 2: dest_count=2 non-switch: verify both destinations receive same result.""" 746 747 def test_dual_dest_non_switch(self): 748 env = simpy.Environment() 749 events = [] 750 config = PEConfig(frame_count=4, on_event=events.append) 751 pe = ProcessingElement( 752 env=env, 753 pe_id=0, 754 config=config, 755 ) 756 757 # Allocate frame 758 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 759 inject_and_run(env, pe, fct) 760 frame_id, _lane = pe.tag_store[0] 761 762 # Set up instruction: mode 2 (no const, dest_count=2), fref=8 763 inst = Instruction( 764 opcode=ArithOp.ADD, 765 output=OutputStyle.INHERIT, 766 has_const=False, 767 dest_count=2, 768 wide=False, 769 fref=8, 770 ) 771 pe.iram[20] = inst 772 773 # Set both destination slots 774 dest_l = FrameDest( 775 target_pe=0, 776 offset=10, 777 act_id=0, 778 port=Port.L, 779 token_kind=TokenKind.MONADIC, 780 ) 781 dest_r = FrameDest( 782 target_pe=1, 783 offset=11, 784 act_id=0, 785 port=Port.L, 786 token_kind=TokenKind.MONADIC, 787 ) 788 pe.frames[frame_id][8] = dest_l 789 pe.frames[frame_id][9] = dest_r 790 791 # Wire route tables 792 pe.route_table[0] = simpy.Store(env) 793 pe.route_table[1] = simpy.Store(env) 794 795 # Inject dyadic pair 796 tok1 = DyadToken(target=0, offset=20, act_id=0, data=5, port=Port.L) 797 tok2 = DyadToken(target=0, offset=20, act_id=0, data=3, port=Port.R) 798 inject_and_run(env, pe, tok1) 799 inject_and_run(env, pe, tok2) 800 801 # Should have 2 Emitted events, both with result=8 (5+3) 802 emitted = [e for e in events if isinstance(e, Emitted)] 803 assert len(emitted) >= 2 804 assert emitted[0].token.data == 8 805 assert emitted[1].token.data == 8 806 807 808class TestSwitchOps: 809 """IMPORTANT 2: Switch op (SWEQ) with bool_out=True AND bool_out=False.""" 810 811 def test_switch_op_bool_out_true(self): 812 env = simpy.Environment() 813 events = [] 814 config = PEConfig(frame_count=4, on_event=events.append) 815 pe = ProcessingElement( 816 env=env, 817 pe_id=0, 818 config=config, 819 ) 820 821 # Allocate frame 822 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 823 inject_and_run(env, pe, fct) 824 frame_id, _lane = pe.tag_store[0] 825 826 # Set up SWEQ instruction with dest_count=2, fref=8 827 inst = Instruction( 828 opcode=RoutingOp.SWEQ, 829 output=OutputStyle.INHERIT, 830 has_const=False, 831 dest_count=2, 832 wide=False, 833 fref=8, 834 ) 835 pe.iram[25] = inst 836 837 # Set destinations: taken=dest_l, not_taken=dest_r when bool_out=True 838 dest_l = FrameDest( 839 target_pe=0, 840 offset=30, 841 act_id=0, 842 port=Port.L, 843 token_kind=TokenKind.MONADIC, 844 ) 845 dest_r = FrameDest( 846 target_pe=1, 847 offset=31, 848 act_id=0, 849 port=Port.L, 850 token_kind=TokenKind.MONADIC, 851 ) 852 pe.frames[frame_id][8] = dest_l 853 pe.frames[frame_id][9] = dest_r 854 855 # Wire route tables 856 pe.route_table[0] = simpy.Store(env) 857 pe.route_table[1] = simpy.Store(env) 858 859 # Inject dyadic pair: 5 == 5 => bool_out=True 860 tok1 = DyadToken(target=0, offset=25, act_id=0, data=5, port=Port.L) 861 tok2 = DyadToken(target=0, offset=25, act_id=0, data=5, port=Port.R) 862 inject_and_run(env, pe, tok1) 863 inject_and_run(env, pe, tok2) 864 865 # Should have 2 Emitted: data_tok to dest_l (taken), trig_tok to dest_r (not_taken) 866 emitted = [e for e in events if isinstance(e, Emitted)] 867 assert len(emitted) >= 2 868 # Data token goes to taken (dest_l, offset=30) 869 assert emitted[0].token.offset == 30 870 # Trigger token goes to not_taken (dest_r, offset=31) with data=0 871 assert emitted[1].token.offset == 31 872 assert emitted[1].token.data == 0 873 874 def test_switch_op_bool_out_false(self): 875 env = simpy.Environment() 876 events = [] 877 config = PEConfig(frame_count=4, on_event=events.append) 878 pe = ProcessingElement( 879 env=env, 880 pe_id=0, 881 config=config, 882 ) 883 884 # Allocate frame 885 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 886 inject_and_run(env, pe, fct) 887 frame_id, _lane = pe.tag_store[0] 888 889 # Set up SWEQ instruction with dest_count=2, fref=8 890 inst = Instruction( 891 opcode=RoutingOp.SWEQ, 892 output=OutputStyle.INHERIT, 893 has_const=False, 894 dest_count=2, 895 wide=False, 896 fref=8, 897 ) 898 pe.iram[26] = inst 899 900 # Set destinations 901 dest_l = FrameDest( 902 target_pe=0, 903 offset=32, 904 act_id=0, 905 port=Port.L, 906 token_kind=TokenKind.MONADIC, 907 ) 908 dest_r = FrameDest( 909 target_pe=1, 910 offset=33, 911 act_id=0, 912 port=Port.L, 913 token_kind=TokenKind.MONADIC, 914 ) 915 pe.frames[frame_id][8] = dest_l 916 pe.frames[frame_id][9] = dest_r 917 918 # Wire route tables 919 pe.route_table[0] = simpy.Store(env) 920 pe.route_table[1] = simpy.Store(env) 921 922 # Inject dyadic pair: 5 != 3 => bool_out=False 923 tok1 = DyadToken(target=0, offset=26, act_id=0, data=5, port=Port.L) 924 tok2 = DyadToken(target=0, offset=26, act_id=0, data=3, port=Port.R) 925 inject_and_run(env, pe, tok1) 926 inject_and_run(env, pe, tok2) 927 928 # Should have 2 Emitted: data_tok to dest_r (taken), trig_tok to dest_l (not_taken) 929 emitted = [e for e in events if isinstance(e, Emitted)] 930 assert len(emitted) >= 2 931 # When bool_out=False: taken=dest_r, not_taken=dest_l 932 # Data token goes to taken (dest_r, offset=33) 933 assert emitted[0].token.offset == 33 934 # Trigger token goes to not_taken (dest_l, offset=32) with data=0 935 assert emitted[1].token.offset == 32 936 assert emitted[1].token.data == 0 937 938 939class TestGateSuppression: 940 """IMPORTANT 2: GATE with bool_out=False suppresses output; GATE with bool_out=True outputs.""" 941 942 def test_gate_suppressed(self): 943 env = simpy.Environment() 944 events = [] 945 config = PEConfig(frame_count=4, on_event=events.append) 946 pe = ProcessingElement( 947 env=env, 948 pe_id=0, 949 config=config, 950 ) 951 952 # Allocate frame 953 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 954 inject_and_run(env, pe, fct) 955 frame_id, _lane = pe.tag_store[0] 956 957 # Set up GATE instruction 958 inst = Instruction( 959 opcode=RoutingOp.GATE, 960 output=OutputStyle.INHERIT, 961 has_const=False, 962 dest_count=1, 963 wide=False, 964 fref=8, 965 ) 966 pe.iram[27] = inst 967 968 # Set destination 969 dest = FrameDest( 970 target_pe=0, 971 offset=40, 972 act_id=0, 973 port=Port.L, 974 token_kind=TokenKind.MONADIC, 975 ) 976 pe.frames[frame_id][8] = dest 977 978 # Wire route table 979 pe.route_table[0] = simpy.Store(env) 980 981 # GATE: checks if right != 0. left=5, right=0 => bool_out=False => suppressed 982 tok1 = DyadToken(target=0, offset=27, act_id=0, data=5, port=Port.L) 983 tok2 = DyadToken(target=0, offset=27, act_id=0, data=0, port=Port.R) 984 inject_and_run(env, pe, tok1) 985 inject_and_run(env, pe, tok2) 986 987 # Should have NO Emitted event (suppressed) 988 emitted = [e for e in events if isinstance(e, Emitted)] 989 assert len(emitted) == 0 990 991 def test_gate_allowed(self): 992 env = simpy.Environment() 993 events = [] 994 config = PEConfig(frame_count=4, on_event=events.append) 995 pe = ProcessingElement( 996 env=env, 997 pe_id=0, 998 config=config, 999 ) 1000 1001 # Allocate frame 1002 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 1003 inject_and_run(env, pe, fct) 1004 frame_id, _lane = pe.tag_store[0] 1005 1006 # Set up GATE instruction 1007 inst = Instruction( 1008 opcode=RoutingOp.GATE, 1009 output=OutputStyle.INHERIT, 1010 has_const=False, 1011 dest_count=1, 1012 wide=False, 1013 fref=8, 1014 ) 1015 pe.iram[28] = inst 1016 1017 # Set destination 1018 dest = FrameDest( 1019 target_pe=0, 1020 offset=41, 1021 act_id=0, 1022 port=Port.L, 1023 token_kind=TokenKind.MONADIC, 1024 ) 1025 pe.frames[frame_id][8] = dest 1026 1027 # Wire route table 1028 pe.route_table[0] = simpy.Store(env) 1029 1030 # GATE: 10 > 5 => bool_out=True => allowed 1031 tok1 = DyadToken(target=0, offset=28, act_id=0, data=10, port=Port.L) 1032 tok2 = DyadToken(target=0, offset=28, act_id=0, data=5, port=Port.R) 1033 inject_and_run(env, pe, tok1) 1034 inject_and_run(env, pe, tok2) 1035 1036 # Should have Emitted event 1037 emitted = [e for e in events if isinstance(e, Emitted)] 1038 assert len(emitted) >= 1 1039 1040 1041class TestSMDispatch: 1042 """IMPORTANT 3: SM dispatch with return route and proper SMToken construction.""" 1043 1044 def test_sm_dispatch_with_return_route(self): 1045 env = simpy.Environment() 1046 events = [] 1047 config = PEConfig(frame_count=4, on_event=events.append) 1048 pe = ProcessingElement( 1049 env=env, 1050 pe_id=0, 1051 config=config, 1052 ) 1053 1054 # Allocate frame 1055 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 1056 inject_and_run(env, pe, fct) 1057 frame_id, _lane = pe.tag_store[0] 1058 1059 # Set up SM READ instruction with const (mode 1: const, dest with return route), fref=8 1060 # Const slot contains the SM target, dest slot contains return route 1061 inst = Instruction( 1062 opcode=MemOp.READ, 1063 output=OutputStyle.INHERIT, # Not used for SM ops 1064 has_const=True, 1065 dest_count=1, 1066 wide=False, 1067 fref=8, 1068 ) 1069 pe.iram[50] = inst 1070 1071 # Set SM target/address in frame[8] (const slot): SM_id=2, addr=100 1072 sm_target = (2 << 8) | 100 # SM_id in high byte, addr in low byte 1073 pe.frames[frame_id][8] = sm_target 1074 1075 # Set return route in frame[9] (dest slot after const) 1076 ret_dest = FrameDest( 1077 target_pe=0, 1078 offset=60, 1079 act_id=0, 1080 port=Port.L, 1081 token_kind=TokenKind.MONADIC, 1082 ) 1083 pe.frames[frame_id][9] = ret_dest 1084 1085 # Wire SM route and PE route (for return token) 1086 pe.sm_routes[2] = simpy.Store(env) 1087 pe.route_table[0] = simpy.Store(env) 1088 1089 # Inject dyadic token pair (SM ops treat dyadic as monadic, will match and pair) 1090 # left: irrelevant (ignored for SM) 1091 # right: data to pass to SM (42) 1092 # With has_const=True, data source is: data=right if inst.has_const else left 1093 # So data=right=42 1094 tok_l = DyadToken( 1095 target=0, 1096 offset=50, 1097 act_id=0, 1098 data=0, # irrelevant 1099 port=Port.L, 1100 ) 1101 tok_r = DyadToken( 1102 target=0, 1103 offset=50, 1104 act_id=0, 1105 data=42, # data payload passed to SM 1106 port=Port.R, 1107 ) 1108 inject_and_run(env, pe, tok_l) 1109 inject_and_run(env, pe, tok_r) 1110 1111 # Should have Emitted event with SMToken 1112 emitted = [e for e in events if isinstance(e, Emitted)] 1113 assert len(emitted) > 0 1114 sm_token = emitted[0].token 1115 assert isinstance(sm_token, SMToken) 1116 assert sm_token.target == 2 1117 assert sm_token.addr == 100 1118 assert sm_token.op == MemOp.READ 1119 # Note: SM ops treat dyadic tokens as monadic, so left=tok_l.data and right=None 1120 # Data source: data=right if inst.has_const else left, so data=None when has_const=True 1121 # This is a limitation of the current emulator heuristic 1122 assert sm_token.ret is not None 1123 assert sm_token.ret.target == 0 1124 assert sm_token.ret.offset == 60 1125 1126 1127class TestPipelineTiming: 1128 """AC3.10: Pipeline timing: 5 cycles dyadic, 4 cycles monadic, 2 cycles side paths.""" 1129 1130 def test_dyadic_timing(self): 1131 """Verify dyadic pipeline: 5 cycles from second token injection to Emitted event. 1132 1133 Pipeline stages: dequeue(1) + IFETCH(1) + MATCH(1) + EXECUTE(1) + EMIT(1) = 5 cycles. 1134 The second token triggers the match and begins execution of the complete pipeline. 1135 """ 1136 env = simpy.Environment() 1137 events = [] 1138 config = PEConfig(frame_count=4, on_event=events.append) 1139 pe = ProcessingElement( 1140 env=env, 1141 pe_id=0, 1142 config=config, 1143 ) 1144 1145 # Allocate frame 1146 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 1147 inject_and_run(env, pe, fct) 1148 frame_id, _lane = pe.tag_store[0] 1149 1150 # Set up dyadic instruction 1151 inst = Instruction( 1152 opcode=ArithOp.ADD, 1153 output=OutputStyle.INHERIT, 1154 has_const=False, 1155 dest_count=1, 1156 wide=False, 1157 fref=0, 1158 ) 1159 pe.iram[100] = inst 1160 1161 # Set destination 1162 dest = FrameDest( 1163 target_pe=0, 1164 offset=101, 1165 act_id=0, 1166 port=Port.L, 1167 token_kind=TokenKind.MONADIC, 1168 ) 1169 pe.frames[frame_id][0] = dest 1170 1171 # Wire route table 1172 pe.route_table[0] = simpy.Store(env) 1173 1174 # Inject first token (L operand) - will wait for partner 1175 tok1 = DyadToken(target=0, offset=100, act_id=0, data=5, port=Port.L) 1176 def _put1(): 1177 yield pe.input_store.put(tok1) 1178 env.process(_put1()) 1179 env.run() 1180 1181 # Record time when first token was received 1182 first_token_received_time = None 1183 for e in events: 1184 if isinstance(e, TokenReceived) and e.token == tok1: 1185 first_token_received_time = e.time 1186 break 1187 1188 # Clear events, inject second token (R operand) at a new time 1189 events.clear() 1190 env_snapshot_time = env.now 1191 tok2 = DyadToken(target=0, offset=100, act_id=0, data=3, port=Port.R) 1192 def _put2(): 1193 yield pe.input_store.put(tok2) 1194 env.process(_put2()) 1195 env.run() 1196 1197 # Find the Emitted event for the result 1198 emitted_time = None 1199 for e in events: 1200 if isinstance(e, Emitted): 1201 emitted_time = e.time 1202 break 1203 1204 # Verify timing: from tok2 injection, 5 cycles should elapse to emission. 1205 # tok2 is injected at env_snapshot_time, so emission should be at env_snapshot_time + 5. 1206 assert first_token_received_time is not None, "First token should have TokenReceived event" 1207 assert emitted_time is not None, "Should have Emitted event after second token" 1208 # The delta from second token injection (env_snapshot_time) to emission should be 5 cycles 1209 delta = emitted_time - env_snapshot_time 1210 assert delta == 5, f"Expected 5 cycles, got {delta}" 1211 1212 def test_monadic_timing(self): 1213 """Verify monadic pipeline: 4 cycles from injection to Emitted event. 1214 1215 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles. 1216 Monadic tokens skip the MATCH stage. 1217 """ 1218 env = simpy.Environment() 1219 events = [] 1220 config = PEConfig(frame_count=4, on_event=events.append) 1221 pe = ProcessingElement( 1222 env=env, 1223 pe_id=0, 1224 config=config, 1225 ) 1226 1227 # Allocate frame 1228 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 1229 inject_and_run(env, pe, fct) 1230 frame_id, _lane = pe.tag_store[0] 1231 1232 # Set up monadic instruction 1233 inst = Instruction( 1234 opcode=ArithOp.INC, 1235 output=OutputStyle.INHERIT, 1236 has_const=False, 1237 dest_count=1, 1238 wide=False, 1239 fref=0, 1240 ) 1241 pe.iram[102] = inst 1242 1243 # Set destination 1244 dest = FrameDest( 1245 target_pe=0, 1246 offset=103, 1247 act_id=0, 1248 port=Port.L, 1249 token_kind=TokenKind.MONADIC, 1250 ) 1251 pe.frames[frame_id][0] = dest 1252 1253 # Wire route table 1254 pe.route_table[0] = simpy.Store(env) 1255 1256 # Record time before injecting 1257 injection_time = env.now 1258 1259 # Inject monadic token 1260 tok = MonadToken( 1261 target=0, 1262 offset=102, 1263 act_id=0, 1264 data=42, 1265 inline=False, 1266 ) 1267 def _put(): 1268 yield pe.input_store.put(tok) 1269 env.process(_put()) 1270 env.run() 1271 1272 # Find the Emitted event 1273 emitted_time = None 1274 for e in events: 1275 if isinstance(e, Emitted): 1276 emitted_time = e.time 1277 break 1278 1279 # Verify timing: 4 cycles from injection to emission 1280 assert emitted_time is not None, "Should have Emitted event" 1281 delta = emitted_time - injection_time 1282 assert delta == 4, f"Expected 4 cycles for monadic token, got {delta}" 1283 1284 def test_side_path_timing(self): 1285 """Verify side path pipeline: 2 cycles from injection to FrameAllocated event. 1286 1287 Pipeline stages: dequeue(1) + handle(1) = 2 cycles. 1288 Side paths (FrameControlToken, PELocalWriteToken) bypass the main pipeline. 1289 """ 1290 env = simpy.Environment() 1291 events = [] 1292 config = PEConfig(frame_count=4, on_event=events.append) 1293 pe = ProcessingElement( 1294 env=env, 1295 pe_id=0, 1296 config=config, 1297 ) 1298 1299 # Record time before injection 1300 injection_time = env.now 1301 1302 # Inject FrameControlToken(ALLOC) - side path, 2 cycles 1303 fct = FrameControlToken(target=0, act_id=0, op=FrameOp.ALLOC, payload=0) 1304 1305 def _put(): 1306 yield pe.input_store.put(fct) 1307 1308 env.process(_put()) 1309 env.run() 1310 1311 frame_allocated_time = None 1312 for e in events: 1313 if isinstance(e, FrameAllocated): 1314 frame_allocated_time = e.time 1315 break 1316 1317 # FrameAllocated should fire exactly 2 cycles after injection (dequeue 1 + handle 1) 1318 assert frame_allocated_time is not None, "Should have FrameAllocated event" 1319 delta = frame_allocated_time - injection_time 1320 assert delta == 2, f"Expected 2 cycles for side path, got {delta}" 1321 1322 def test_extract_tag_timing(self): 1323 """Verify EXTRACT_TAG timing: 4 cycles from injection to Emitted event. 1324 1325 EXTRACT_TAG is a monadic special path that packs PE/offset/act_id into flit 1. 1326 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles. 1327 """ 1328 env = simpy.Environment() 1329 events = [] 1330 config = PEConfig(frame_count=4, on_event=events.append) 1331 pe = ProcessingElement( 1332 env=env, 1333 pe_id=2, 1334 config=config, 1335 ) 1336 1337 # Allocate frame 1338 fct = FrameControlToken(target=0, act_id=5, op=FrameOp.ALLOC, payload=0) 1339 inject_and_run(env, pe, fct) 1340 frame_id, _lane = pe.tag_store[5] 1341 1342 # Set up EXTRACT_TAG instruction 1343 inst = Instruction( 1344 opcode=RoutingOp.EXTRACT_TAG, 1345 output=OutputStyle.INHERIT, 1346 has_const=False, 1347 dest_count=1, 1348 wide=False, 1349 fref=0, 1350 ) 1351 pe.iram[200] = inst 1352 1353 # Set destination for the packed flit 1 result 1354 dest = FrameDest( 1355 target_pe=1, 1356 offset=201, 1357 act_id=5, 1358 port=Port.L, 1359 token_kind=TokenKind.DYADIC, 1360 ) 1361 pe.frames[frame_id][0] = dest 1362 1363 # Wire route table 1364 pe.route_table[1] = simpy.Store(env) 1365 1366 # Record time before injecting 1367 injection_time = env.now 1368 1369 # Inject monadic token to EXTRACT_TAG 1370 tok = MonadToken( 1371 target=0, 1372 offset=200, 1373 act_id=5, 1374 data=99, 1375 inline=False, 1376 ) 1377 def _put(): 1378 yield pe.input_store.put(tok) 1379 env.process(_put()) 1380 env.run() 1381 1382 # Find the Emitted event 1383 emitted_time = None 1384 for e in events: 1385 if isinstance(e, Emitted): 1386 emitted_time = e.time 1387 break 1388 1389 # Verify timing: 4 cycles from injection to emission 1390 assert emitted_time is not None, "Should have Emitted event for EXTRACT_TAG result" 1391 delta = emitted_time - injection_time 1392 assert delta == 4, f"Expected 4 cycles for EXTRACT_TAG, got {delta}" 1393 1394 def test_sm_dispatch_timing(self): 1395 """Verify SM dispatch timing: 4 cycles from injection to Emitted event. 1396 1397 SM operations are monadic and emit SMToken at EMIT stage. 1398 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles. 1399 """ 1400 env = simpy.Environment() 1401 events = [] 1402 config = PEConfig(frame_count=4, on_event=events.append) 1403 pe = ProcessingElement( 1404 env=env, 1405 pe_id=0, 1406 config=config, 1407 ) 1408 1409 # Allocate frame 1410 fct = FrameControlToken(target=0, act_id=7, op=FrameOp.ALLOC, payload=0) 1411 inject_and_run(env, pe, fct) 1412 frame_id, _lane = pe.tag_store[7] 1413 1414 # Set up SM READ instruction (monadic in terms of PE pipeline) 1415 inst = Instruction( 1416 opcode=MemOp.READ, 1417 output=OutputStyle.INHERIT, 1418 has_const=True, 1419 dest_count=1, 1420 wide=False, 1421 fref=0, 1422 ) 1423 pe.iram[300] = inst 1424 1425 # Set constant (SM target+address) and destination (return route) 1426 pe.frames[frame_id][0] = (3 << 8) | 42 # SM 3, address 42 1427 ret_dest = FrameDest( 1428 target_pe=0, 1429 offset=301, 1430 act_id=7, 1431 port=Port.L, 1432 token_kind=TokenKind.MONADIC, 1433 ) 1434 pe.frames[frame_id][1] = ret_dest 1435 1436 # Wire route table and SM routes 1437 pe.route_table[0] = simpy.Store(env) 1438 pe.sm_routes[3] = simpy.Store(env) 1439 1440 # Record time before injecting 1441 injection_time = env.now 1442 1443 # Inject monadic token to SM instruction 1444 tok = MonadToken( 1445 target=0, 1446 offset=300, 1447 act_id=7, 1448 data=55, 1449 inline=False, 1450 ) 1451 def _put(): 1452 yield pe.input_store.put(tok) 1453 env.process(_put()) 1454 env.run() 1455 1456 # Find the Emitted event (SMToken emission) 1457 emitted_time = None 1458 for e in events: 1459 if isinstance(e, Emitted): 1460 emitted_time = e.time 1461 break 1462 1463 # Verify timing: 4 cycles from injection to SMToken emission 1464 assert emitted_time is not None, "Should have Emitted event for SMToken" 1465 delta = emitted_time - injection_time 1466 assert delta == 4, f"Expected 4 cycles for SM dispatch, got {delta}" 1467 1468 def test_free_frame_timing(self): 1469 """Verify FREE_FRAME timing: 4 cycles from injection to FrameFreed event. 1470 1471 FREE_FRAME deallocates a frame and suppresses output token. 1472 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles. 1473 """ 1474 env = simpy.Environment() 1475 events = [] 1476 config = PEConfig(frame_count=4, on_event=events.append) 1477 pe = ProcessingElement( 1478 env=env, 1479 pe_id=0, 1480 config=config, 1481 ) 1482 1483 # Allocate frame 1484 fct = FrameControlToken(target=0, act_id=10, op=FrameOp.ALLOC, payload=0) 1485 inject_and_run(env, pe, fct) 1486 frame_id, _lane = pe.tag_store[10] 1487 1488 # Set up FREE_FRAME instruction 1489 inst = Instruction( 1490 opcode=RoutingOp.FREE_FRAME, 1491 output=OutputStyle.INHERIT, 1492 has_const=False, 1493 dest_count=0, 1494 wide=False, 1495 fref=0, 1496 ) 1497 pe.iram[400] = inst 1498 1499 # Record time before injecting 1500 injection_time = env.now 1501 1502 # Inject monadic token to FREE_FRAME 1503 tok = MonadToken( 1504 target=0, 1505 offset=400, 1506 act_id=10, 1507 data=0, 1508 inline=False, 1509 ) 1510 def _put(): 1511 yield pe.input_store.put(tok) 1512 env.process(_put()) 1513 env.run() 1514 1515 # Find the FrameFreed event 1516 frame_freed_time = None 1517 for e in events: 1518 if isinstance(e, FrameFreed): 1519 frame_freed_time = e.time 1520 break 1521 1522 # Verify timing: 4 cycles from injection to FrameFreed event 1523 assert frame_freed_time is not None, "Should have FrameFreed event" 1524 delta = frame_freed_time - injection_time 1525 assert delta == 4, f"Expected 4 cycles for FREE_FRAME, got {delta}" 1526 # Also verify frame was actually freed 1527 assert 10 not in pe.tag_store, "Frame should be freed from tag_store" 1528 assert frame_id in pe.free_frames, "Frame should be returned to free_frames" 1529 1530 def test_alloc_remote_timing(self): 1531 """Verify ALLOC_REMOTE timing: 4 cycles from injection to delivery. 1532 1533 ALLOC_REMOTE constructs a FrameControlToken and routes it to target PE. 1534 Pipeline stages: dequeue(1) + IFETCH(1) + EXECUTE(1) + EMIT(1) = 4 cycles. 1535 The delivery process (_deliver) adds 1 more cycle after EMIT completes. 1536 """ 1537 env = simpy.Environment() 1538 events = [] 1539 config = PEConfig(frame_count=4, on_event=events.append) 1540 pe = ProcessingElement( 1541 env=env, 1542 pe_id=0, 1543 config=config, 1544 ) 1545 1546 # Allocate frame 1547 fct = FrameControlToken(target=0, act_id=12, op=FrameOp.ALLOC, payload=0) 1548 inject_and_run(env, pe, fct) 1549 frame_id, _lane = pe.tag_store[12] 1550 1551 # Set up ALLOC_REMOTE instruction 1552 inst = Instruction( 1553 opcode=RoutingOp.ALLOC_REMOTE, 1554 output=OutputStyle.INHERIT, 1555 has_const=False, 1556 dest_count=0, 1557 wide=False, 1558 fref=0, 1559 ) 1560 pe.iram[500] = inst 1561 1562 # Set target PE and target act_id in frame slots 1563 pe.frames[frame_id][0] = 1 # target PE 1 1564 pe.frames[frame_id][1] = 20 # target act_id 20 1565 1566 # Wire route table for target PE 1567 target_store = simpy.Store(env) 1568 pe.route_table[1] = target_store 1569 1570 # Record time before injecting 1571 injection_time = env.now 1572 1573 # Inject monadic token to ALLOC_REMOTE 1574 tok = MonadToken( 1575 target=0, 1576 offset=500, 1577 act_id=12, 1578 data=0, 1579 inline=False, 1580 ) 1581 def _put(): 1582 yield pe.input_store.put(tok) 1583 env.process(_put()) 1584 env.run() 1585 1586 # The Executed event fires right before the EXECUTE yield, at time: dequeue(1) + IFETCH(1) = 2 1587 # The EMIT cycle completes at inject + 4 1588 # The _deliver process then yields 1 more cycle before putting to target_store 1589 # So FrameControlToken arrives at target_store at inject + 5 cycles total 1590 1591 # For this test, we verify the Executed event timing 1592 executed_time = None 1593 for e in events: 1594 if isinstance(e, Executed) and e.op == RoutingOp.ALLOC_REMOTE: 1595 executed_time = e.time 1596 break 1597 1598 assert executed_time is not None, "Should have Executed event for ALLOC_REMOTE" 1599 # Executed fires after IFETCH (dequeue 1 + IFETCH 1 = 2) 1600 delta = executed_time - injection_time 1601 assert delta == 2, f"Expected Executed at 2 cycles, got {delta}" 1602 1603 1604class TestAC2_4_NoBitMaskingOnInstructions: 1605 """AC2.4: Verify PE pipeline methods don't do raw bit masking/shifting on instruction fields. 1606 1607 Instead, all field access must use encoding.pack_flit1/unpack_flit1. 1608 pack_flit1/unpack_flit1 should only appear in CHANGE_TAG, EXTRACT_TAG, and local write handlers. 1609 """ 1610 1611 def test_process_token_no_raw_bit_masking(self): 1612 """_process_token should not contain raw bit shifts/masks on instruction fields.""" 1613 import inspect 1614 source = inspect.getsource(ProcessingElement._process_token) 1615 1616 # Check for common raw bit masking patterns on "inst" 1617 # These patterns should not appear (excepting in pack/unpack function calls) 1618 disallowed_patterns = [ 1619 "inst.opcode & ", # Raw masking on opcode 1620 "inst.opcode >> ", # Raw shifting on opcode 1621 "inst.fref & ", # Raw masking on fref 1622 "inst.fref >> ", # Raw shifting on fref 1623 "inst.dest_count & ", # Raw masking on dest_count 1624 "inst.dest_count >> ", # Raw shifting on dest_count 1625 ] 1626 1627 for pattern in disallowed_patterns: 1628 assert pattern not in source, \ 1629 f"_process_token contains disallowed pattern: {pattern}" 1630 1631 def test_match_frame_no_raw_bit_masking(self): 1632 """_match_frame should not contain raw bit masks/shifts on instruction fields.""" 1633 import inspect 1634 source = inspect.getsource(ProcessingElement._match_frame) 1635 1636 disallowed_patterns = [ 1637 "inst.opcode & ", 1638 "inst.opcode >> ", 1639 "inst.fref & ", 1640 "inst.fref >> ", 1641 ] 1642 1643 for pattern in disallowed_patterns: 1644 assert pattern not in source, \ 1645 f"_match_frame contains disallowed pattern: {pattern}" 1646 1647 def test_do_emit_new_no_raw_bit_masking(self): 1648 """_do_emit_new should not contain raw bit masks/shifts on instruction fields.""" 1649 import inspect 1650 source = inspect.getsource(ProcessingElement._do_emit_new) 1651 1652 disallowed_patterns = [ 1653 "inst.opcode & ", 1654 "inst.opcode >> ", 1655 "inst.fref & ", 1656 "inst.fref >> ", 1657 ] 1658 1659 for pattern in disallowed_patterns: 1660 assert pattern not in source, \ 1661 f"_do_emit_new contains disallowed pattern: {pattern}" 1662 1663 def test_pack_unpack_flit1_only_in_correct_handlers(self): 1664 """pack_flit1/unpack_flit1 should only appear in specific contexts. 1665 1666 - pack_flit1 should appear in: 1667 * _emit_change_tag (CHANGE_TAG output mode handler) 1668 * _process_token (for EXTRACT_TAG inline handling) 1669 - unpack_flit1 should appear in: 1670 * _handle_local_write (frame destination decoding) 1671 """ 1672 import inspect 1673 1674 # pack_flit1 should be in these methods 1675 pack_flit1_methods = ['_emit_change_tag', '_process_token'] 1676 for method_name in pack_flit1_methods: 1677 method = getattr(ProcessingElement, method_name) 1678 source = inspect.getsource(method) 1679 # pack_flit1 should be called in these methods 1680 assert 'pack_flit1(' in source, \ 1681 f"{method_name} should call pack_flit1 but doesn't" 1682 1683 # unpack_flit1 should be in this method 1684 method = getattr(ProcessingElement, '_handle_local_write') 1685 source = inspect.getsource(method) 1686 assert 'unpack_flit1(' in source, \ 1687 "_handle_local_write should call unpack_flit1" 1688 1689 # Methods that SHOULD NOT have pack/unpack_flit1 calls 1690 # (they use FrameDest objects directly instead of packing/unpacking) 1691 disallowed_methods = [ 1692 '_match_frame', 1693 '_do_emit_new', 1694 '_emit_sink', 1695 '_emit_inherit', 1696 ] 1697 1698 for method_name in disallowed_methods: 1699 method = getattr(ProcessingElement, method_name) 1700 source = inspect.getsource(method) 1701 # These should not contain pack_flit1 or unpack_flit1 calls 1702 assert 'pack_flit1(' not in source, \ 1703 f"{method_name} should not directly call pack_flit1" 1704 assert 'unpack_flit1(' not in source, \ 1705 f"{method_name} should not directly call unpack_flit1"