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