OR-1 dataflow CPU sketch
at ba08ffded3d3b2badb2a7e22816feafaacea5ded 1010 lines 33 kB view raw
1"""Tests for code generation. 2 3Tests verify: 4- token-migration.AC7.1: Token stream mode emits IRAMWriteToken (not LoadInstToken) 5- token-migration.AC7.2: Token stream mode does not emit RouteSetToken 6- token-migration.AC7.3: Direct mode (PEConfig/SMConfig) still works 7 8Also tests original codegen AC8 criteria: 9- or1-asm.AC8.1: Direct mode produces valid PEConfig with correct IRAM contents 10- or1-asm.AC8.2: Direct mode produces valid SMConfig with initial cell values 11- or1-asm.AC8.3: Direct mode produces seed MonadTokens for const nodes with no incoming edges 12- or1-asm.AC8.4: Direct mode PEConfig includes route restrictions matching edge analysis 13- or1-asm.AC8.9: Program with no data_defs produces empty SM init section 14""" 15 16import pytest 17 18from asm.codegen import generate_direct, generate_tokens, AssemblyResult 19from asm.ir import ( 20 IRGraph, 21 IRNode, 22 IREdge, 23 IRDataDef, 24 SystemConfig, 25 SourceLoc, 26 ResolvedDest, 27) 28from cm_inst import ALUInst, Addr, ArithOp, MemOp, Port, RoutingOp, SMInst 29from tokens import IRAMWriteToken, MonadToken, SMToken 30from emu.types import PEConfig, SMConfig 31from sm_mod import Presence 32 33 34class TestTokenMigration: 35 """Token migration acceptance criteria (AC7.1, AC7.2, AC7.3).""" 36 37 def test_ac71_iram_write_token_in_stream(self): 38 """AC7.1: Token stream mode emits IRAMWriteToken (not LoadInstToken). 39 40 Tests that: 41 - generate_tokens() produces IRAMWriteToken instances 42 - At least one IRAMWriteToken is present for each PE with instructions 43 """ 44 node = IRNode( 45 name="&add", 46 opcode=ArithOp.ADD, 47 pe=0, 48 iram_offset=0, 49 ctx=0, 50 loc=SourceLoc(1, 1), 51 ) 52 system = SystemConfig(pe_count=1, sm_count=1) 53 graph = IRGraph({"&add": node}, system=system) 54 55 tokens = generate_tokens(graph) 56 57 # Find IRAMWriteToken instances 58 iram_write_tokens = [ 59 t for t in tokens if isinstance(t, IRAMWriteToken) 60 ] 61 62 assert len(iram_write_tokens) > 0, "Should emit at least one IRAMWriteToken (AC7.1)" 63 for token in iram_write_tokens: 64 assert isinstance(token.instructions, tuple), "IRAMWriteToken should have instructions tuple" 65 assert len(token.instructions) > 0, "IRAMWriteToken instructions should not be empty" 66 67 def test_ac72_no_route_set_token(self): 68 """AC7.2: Token stream mode does not emit RouteSetToken. 69 70 Tests that: 71 - generate_tokens() produces no RouteSetToken instances 72 """ 73 # Multi-PE graph to test routing 74 node_pe0 = IRNode( 75 name="&a", 76 opcode=ArithOp.ADD, 77 pe=0, 78 iram_offset=0, 79 ctx=0, 80 dest_l=ResolvedDest( 81 name="&b", 82 addr=Addr(a=0, port=Port.L, pe=1), 83 ), 84 loc=SourceLoc(1, 1), 85 ) 86 node_pe1 = IRNode( 87 name="&b", 88 opcode=ArithOp.SUB, 89 pe=1, 90 iram_offset=0, 91 ctx=0, 92 loc=SourceLoc(2, 1), 93 ) 94 edge = IREdge(source="&a", dest="&b", port=Port.L, loc=SourceLoc(1, 1)) 95 system = SystemConfig(pe_count=2, sm_count=1) 96 graph = IRGraph( 97 {"&a": node_pe0, "&b": node_pe1}, 98 edges=[edge], 99 system=system, 100 ) 101 102 tokens = generate_tokens(graph) 103 104 # Verify no RouteSetToken (check by class name to be robust) 105 route_set_tokens = [ 106 t for t in tokens 107 if type(t).__name__ == 'RouteSetToken' 108 ] 109 110 assert len(route_set_tokens) == 0, "Should not emit RouteSetToken (AC7.2)" 111 112 def test_ac73_direct_mode_still_works(self): 113 """AC7.3: Direct mode (PEConfig/SMConfig) still works. 114 115 Tests that: 116 - generate_direct() produces valid PEConfig with correct IRAM, route restrictions 117 - generate_direct() produces valid SMConfig with initial cell values 118 - Seed tokens are generated correctly 119 """ 120 data_def = IRDataDef( 121 name="@val", 122 sm_id=0, 123 cell_addr=5, 124 value=42, 125 loc=SourceLoc(1, 1), 126 ) 127 node1 = IRNode( 128 name="&a", 129 opcode=ArithOp.ADD, 130 pe=0, 131 iram_offset=0, 132 ctx=0, 133 loc=SourceLoc(1, 1), 134 ) 135 node2 = IRNode( 136 name="&b", 137 opcode=RoutingOp.CONST, 138 pe=0, 139 iram_offset=1, 140 ctx=0, 141 const=99, 142 loc=SourceLoc(2, 1), 143 ) 144 system = SystemConfig(pe_count=1, sm_count=1) 145 graph = IRGraph( 146 {"&a": node1, "&b": node2}, 147 data_defs=[data_def], 148 system=system, 149 ) 150 151 result = generate_direct(graph) 152 153 # Verify PEConfig 154 assert len(result.pe_configs) == 1 155 pe_config = result.pe_configs[0] 156 assert pe_config.pe_id == 0 157 assert len(pe_config.iram) == 2 158 assert pe_config.allowed_pe_routes == {0} 159 assert pe_config.allowed_sm_routes == set() 160 161 # Verify SMConfig 162 assert len(result.sm_configs) == 1 163 sm_config = result.sm_configs[0] 164 assert sm_config.sm_id == 0 165 assert 5 in sm_config.initial_cells 166 pres, val = sm_config.initial_cells[5] 167 assert pres == Presence.FULL 168 assert val == 42 169 170 # Verify seed tokens 171 assert len(result.seed_tokens) == 1 172 seed = result.seed_tokens[0] 173 assert isinstance(seed, MonadToken) 174 assert seed.data == 99 175 176 177class TestDirectMode: 178 """AC8.1, AC8.2, AC8.3, AC8.4: Direct mode code generation.""" 179 180 def test_ac81_simple_alu_instructions(self): 181 """AC8.1: Two ALU nodes on PE0 produce PEConfig with correct IRAM. 182 183 Tests that: 184 - ALU instructions are correctly converted to ALUInst 185 - They are placed in IRAM at assigned offsets 186 """ 187 # Create two simple dyadic ALU nodes 188 add_node = IRNode( 189 name="&add", 190 opcode=ArithOp.ADD, 191 pe=0, 192 iram_offset=0, 193 ctx=0, 194 loc=SourceLoc(1, 1), 195 ) 196 sub_node = IRNode( 197 name="&sub", 198 opcode=ArithOp.SUB, 199 pe=0, 200 iram_offset=1, 201 ctx=0, 202 loc=SourceLoc(2, 1), 203 ) 204 system = SystemConfig(pe_count=1, sm_count=1) 205 graph = IRGraph( 206 {"&add": add_node, "&sub": sub_node}, 207 system=system, 208 ) 209 210 result = generate_direct(graph) 211 212 assert len(result.pe_configs) == 1 213 pe_config = result.pe_configs[0] 214 assert pe_config.pe_id == 0 215 assert len(pe_config.iram) == 2 216 assert 0 in pe_config.iram 217 assert 1 in pe_config.iram 218 219 # Check the instruction types 220 inst_0 = pe_config.iram[0] 221 inst_1 = pe_config.iram[1] 222 assert isinstance(inst_0, ALUInst) # Is an ALUInst 223 assert isinstance(inst_1, ALUInst) # Is an ALUInst 224 assert inst_0.op == ArithOp.ADD 225 assert inst_1.op == ArithOp.SUB 226 227 def test_ac82_data_defs_to_smconfig(self): 228 """AC8.2: Data definitions produce SMConfig with initial cell values. 229 230 Tests that: 231 - Data defs with SM placement are converted to SMConfig 232 - initial_cells dict contains correct (Presence.FULL, value) tuples 233 """ 234 data_def = IRDataDef( 235 name="@val", 236 sm_id=0, 237 cell_addr=5, 238 value=42, 239 loc=SourceLoc(1, 1), 240 ) 241 graph = IRGraph({}, data_defs=[data_def], system=SystemConfig(1, 1)) 242 243 result = generate_direct(graph) 244 245 assert len(result.sm_configs) == 1 246 sm_config = result.sm_configs[0] 247 assert sm_config.sm_id == 0 248 assert sm_config.initial_cells is not None 249 assert 5 in sm_config.initial_cells 250 pres, val = sm_config.initial_cells[5] 251 assert pres == Presence.FULL 252 assert val == 42 253 254 def test_ac83_const_node_seed_token(self): 255 """AC8.3: CONST node with no incoming edges produces seed MonadToken. 256 257 Tests that: 258 - CONST nodes are detected 259 - Nodes with no incoming edges are marked as seeds 260 - MonadToken has correct target PE, offset, ctx, data 261 """ 262 const_node = IRNode( 263 name="&seed", 264 opcode=RoutingOp.CONST, 265 pe=0, 266 iram_offset=2, 267 ctx=0, 268 const=99, 269 loc=SourceLoc(1, 1), 270 ) 271 graph = IRGraph({"&seed": const_node}, system=SystemConfig(1, 1)) 272 273 result = generate_direct(graph) 274 275 assert len(result.seed_tokens) == 1 276 token = result.seed_tokens[0] 277 assert isinstance(token, MonadToken) 278 assert token.target == 0 279 assert token.offset == 2 280 assert token.ctx == 0 281 assert token.data == 99 282 assert token.inline == False 283 284 def test_ac84_route_restrictions(self): 285 """AC8.4: Cross-PE edges produce correct allowed_pe_routes. 286 287 Tests that: 288 - Edges from PE0 to PE1 add PE1 to PE0's allowed_pe_routes 289 - Self-routes are always included 290 """ 291 # PE0 node connecting to PE1 node 292 node_pe0 = IRNode( 293 name="&a", 294 opcode=ArithOp.ADD, 295 pe=0, 296 iram_offset=0, 297 ctx=0, 298 dest_l=ResolvedDest( 299 name="&b", 300 addr=Addr(a=0, port=Port.L, pe=1), 301 ), 302 loc=SourceLoc(1, 1), 303 ) 304 node_pe1 = IRNode( 305 name="&b", 306 opcode=ArithOp.ADD, 307 pe=1, 308 iram_offset=0, 309 ctx=0, 310 loc=SourceLoc(2, 1), 311 ) 312 edge = IREdge(source="&a", dest="&b", port=Port.L, loc=SourceLoc(1, 1)) 313 system = SystemConfig(pe_count=2, sm_count=1) 314 graph = IRGraph( 315 {"&a": node_pe0, "&b": node_pe1}, 316 edges=[edge], 317 system=system, 318 ) 319 320 result = generate_direct(graph) 321 322 assert len(result.pe_configs) == 2 323 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0) 324 pe1_config = next(c for c in result.pe_configs if c.pe_id == 1) 325 326 # PE0 should have routes to {0, 1} 327 assert 0 in pe0_config.allowed_pe_routes 328 assert 1 in pe0_config.allowed_pe_routes 329 330 # PE1 should have route to {1} (self only, no incoming cross-PE edges) 331 assert 1 in pe1_config.allowed_pe_routes 332 333 def test_sm_instructions_in_iram(self): 334 """Verify SMInst objects are correctly created and placed in IRAM. 335 336 Tests that MemOp instructions produce SMInst in IRAM. 337 """ 338 sm_node = IRNode( 339 name="&read", 340 opcode=MemOp.READ, 341 pe=0, 342 iram_offset=0, 343 ctx=0, 344 sm_id=0, 345 const=42, 346 dest_l=ResolvedDest( 347 name="&out", 348 addr=Addr(a=1, port=Port.L, pe=0), 349 ), 350 loc=SourceLoc(1, 1), 351 ) 352 graph = IRGraph({"&read": sm_node}, system=SystemConfig(1, 1)) 353 354 result = generate_direct(graph) 355 356 assert len(result.pe_configs) == 1 357 pe_config = result.pe_configs[0] 358 assert 0 in pe_config.iram 359 inst = pe_config.iram[0] 360 assert isinstance(inst, SMInst) # Is an SMInst 361 assert inst.op == MemOp.READ 362 assert inst.sm_id == 0 363 assert inst.const == 42 364 365 366class TestTokenStream: 367 """AC7.1, AC7.2, AC7.3: Token stream generation and ordering.""" 368 369 def test_ac85_ac86_ac87_token_ordering(self): 370 """AC7.1-7.2: Token stream emits SM init, IRAM writes, then seeds (no ROUTE_SET or LOAD_INST). 371 372 Tests that: 373 - SM init tokens come first 374 - IRAM write tokens come next (IRAMWriteToken, not LoadInstToken) 375 - Seed tokens come last 376 - No RouteSetToken is present 377 """ 378 # Create a multi-PE graph with data_defs 379 data_def = IRDataDef( 380 name="@val", 381 sm_id=0, 382 cell_addr=5, 383 value=42, 384 loc=SourceLoc(1, 1), 385 ) 386 node1 = IRNode( 387 name="&a", 388 opcode=ArithOp.ADD, 389 pe=0, 390 iram_offset=0, 391 ctx=0, 392 loc=SourceLoc(1, 1), 393 ) 394 node2 = IRNode( 395 name="&b", 396 opcode=RoutingOp.CONST, 397 pe=0, 398 iram_offset=1, 399 ctx=0, 400 const=10, 401 loc=SourceLoc(2, 1), 402 ) 403 system = SystemConfig(pe_count=1, sm_count=1) 404 graph = IRGraph( 405 {"&a": node1, "&b": node2}, 406 data_defs=[data_def], 407 system=system, 408 ) 409 410 tokens = generate_tokens(graph) 411 412 # Find positions of token types 413 smtoken_indices = [ 414 i for i, t in enumerate(tokens) 415 if isinstance(t, SMToken) 416 ] 417 iram_write_indices = [ 418 i for i, t in enumerate(tokens) 419 if isinstance(t, IRAMWriteToken) 420 ] 421 seed_indices = [ 422 i for i, t in enumerate(tokens) if isinstance(t, MonadToken) 423 ] 424 425 # Verify order: SM < IRAM write < seed 426 assert smtoken_indices, "Should have at least one SM token" 427 assert iram_write_indices, "Should have at least one IRAM write token" 428 assert seed_indices, "Should have at least one seed token" 429 assert max(smtoken_indices) < min(iram_write_indices), "SM tokens should come before IRAM write tokens" 430 assert max(iram_write_indices) < min(seed_indices), "IRAM write tokens should come before seed tokens" 431 432 def test_ac88_tokens_are_valid(self): 433 """AC7.3: Generated tokens in direct mode are valid and direct mode PEConfig/SMConfig still works. 434 435 Tests that: 436 - All tokens have required fields set 437 - Token structure matches emulator expectations 438 - Direct mode produces valid PEConfig/SMConfig 439 - Tokens can be injected into an emulator System and execution completes 440 """ 441 from emu.network import build_topology 442 import simpy 443 444 data_def = IRDataDef( 445 name="@val", 446 sm_id=0, 447 cell_addr=5, 448 value=42, 449 loc=SourceLoc(1, 1), 450 ) 451 node = IRNode( 452 name="&add", 453 opcode=ArithOp.ADD, 454 pe=0, 455 iram_offset=0, 456 ctx=0, 457 loc=SourceLoc(1, 1), 458 ) 459 system = SystemConfig(pe_count=1, sm_count=1) 460 graph = IRGraph( 461 {"&add": node}, 462 data_defs=[data_def], 463 system=system, 464 ) 465 466 result = generate_direct(graph) 467 tokens = generate_tokens(graph) 468 469 # Verify direct mode result structure 470 assert isinstance(result, AssemblyResult) 471 assert len(result.pe_configs) == 1 472 assert result.pe_configs[0].pe_id == 0 473 assert len(result.sm_configs) == 1 474 assert result.sm_configs[0].sm_id == 0 475 476 # Build emulator system from AssemblyResult configs 477 env = simpy.Environment() 478 emu_system = build_topology( 479 env, 480 result.pe_configs, 481 result.sm_configs, 482 fifo_capacity=16, 483 ) 484 485 # Inject tokens into the system following the new sequence: 486 # 1. SM init tokens 487 # 2. IRAM write tokens 488 # 3. Seed MonadTokens 489 for token in tokens: 490 if isinstance(token, SMToken): 491 emu_system.inject(token) 492 elif isinstance(token, IRAMWriteToken): 493 emu_system.inject(token) 494 elif isinstance(token, MonadToken): 495 emu_system.inject(token) 496 497 # Run the simulation for enough steps to complete initialization 498 env.run(until=1000) 499 500 # Verify token structure 501 for token in tokens: 502 if isinstance(token, SMToken): 503 assert isinstance(token.target, int) 504 assert isinstance(token.addr, int) 505 assert isinstance(token.op, MemOp) 506 assert token.op == MemOp.WRITE 507 elif isinstance(token, IRAMWriteToken): 508 assert isinstance(token.target, int) 509 assert isinstance(token.offset, int) 510 assert isinstance(token.ctx, int) 511 assert isinstance(token.data, int) 512 assert isinstance(token.instructions, tuple) 513 elif isinstance(token, MonadToken): 514 assert isinstance(token.target, int) 515 assert isinstance(token.offset, int) 516 assert isinstance(token.ctx, int) 517 assert isinstance(token.data, int) 518 519 520class TestEdgeCases: 521 """AC8.9, AC8.10: Edge cases for code generation.""" 522 523 def test_ac89_no_data_defs(self): 524 """AC8.9: Program with no data_defs produces no SMConfig or SM tokens. 525 526 Tests that: 527 - sm_configs contains 1 SM (from @system sm_count) with no initial cells 528 - Token stream has no SMTokens 529 """ 530 node = IRNode( 531 name="&add", 532 opcode=ArithOp.ADD, 533 pe=0, 534 iram_offset=0, 535 ctx=0, 536 loc=SourceLoc(1, 1), 537 ) 538 system = SystemConfig(pe_count=1, sm_count=1) 539 graph = IRGraph({"&add": node}, system=system) 540 541 result = generate_direct(graph) 542 assert len(result.sm_configs) == 1 543 assert result.sm_configs[0].sm_id == 0 544 assert result.sm_configs[0].initial_cells is None 545 546 tokens = generate_tokens(graph) 547 sm_tokens = [t for t in tokens if isinstance(t, SMToken)] 548 assert len(sm_tokens) == 0 549 550 def test_ac810_single_pe_self_route(self): 551 """AC7.2: Single-PE program produces IRAM writes with no RouteSetToken. 552 553 Tests that: 554 - allowed_pe_routes contains only the PE's own ID (in direct mode) 555 - Token stream has no RouteSetToken (route restrictions are not emitted) 556 - Token stream has IRAMWriteToken instead 557 """ 558 node = IRNode( 559 name="&add", 560 opcode=ArithOp.ADD, 561 pe=0, 562 iram_offset=0, 563 ctx=0, 564 loc=SourceLoc(1, 1), 565 ) 566 system = SystemConfig(pe_count=1, sm_count=1) 567 graph = IRGraph({"&add": node}, system=system) 568 569 result = generate_direct(graph) 570 assert len(result.pe_configs) == 1 571 pe_config = result.pe_configs[0] 572 assert pe_config.allowed_pe_routes == {0} 573 574 tokens = generate_tokens(graph) 575 # Verify no RouteSetToken (AC7.2) 576 route_set_tokens = [ 577 t for t in tokens 578 if type(t).__name__ == 'RouteSetToken' # Check by class name to avoid import 579 ] 580 assert len(route_set_tokens) == 0, "RouteSetToken should not be in token stream (AC7.2)" 581 582 # Verify IRAMWriteToken is present (AC7.1) 583 iram_write_tokens = [ 584 t for t in tokens 585 if isinstance(t, IRAMWriteToken) 586 ] 587 assert len(iram_write_tokens) == 1, "Should have exactly one IRAMWriteToken per PE" 588 token = iram_write_tokens[0] 589 assert token.target == 0 590 591 def test_multiple_data_defs_same_sm(self): 592 """Multiple data_defs targeting same SM produce single SMConfig. 593 594 Tests that: 595 - Multiple data_defs for SM0 are merged into single SMConfig 596 - initial_cells contains all entries 597 """ 598 data_def1 = IRDataDef( 599 name="@val1", 600 sm_id=0, 601 cell_addr=5, 602 value=42, 603 loc=SourceLoc(1, 1), 604 ) 605 data_def2 = IRDataDef( 606 name="@val2", 607 sm_id=0, 608 cell_addr=10, 609 value=99, 610 loc=SourceLoc(2, 1), 611 ) 612 system = SystemConfig(pe_count=1, sm_count=1) 613 graph = IRGraph({}, data_defs=[data_def1, data_def2], system=system) 614 615 result = generate_direct(graph) 616 617 assert len(result.sm_configs) == 1 618 sm_config = result.sm_configs[0] 619 assert sm_config.sm_id == 0 620 assert len(sm_config.initial_cells) == 2 621 assert sm_config.initial_cells[5] == (Presence.FULL, 42) 622 assert sm_config.initial_cells[10] == (Presence.FULL, 99) 623 624 def test_const_node_with_incoming_edge_not_seed(self): 625 """CONST node with incoming edge is not a seed token. 626 627 Tests that: 628 - Only CONST nodes with NO incoming edges produce seed_tokens 629 """ 630 source_node = IRNode( 631 name="&src", 632 opcode=ArithOp.ADD, 633 pe=0, 634 iram_offset=0, 635 ctx=0, 636 loc=SourceLoc(1, 1), 637 ) 638 const_node = IRNode( 639 name="&const", 640 opcode=RoutingOp.CONST, 641 pe=0, 642 iram_offset=1, 643 ctx=0, 644 const=5, 645 loc=SourceLoc(2, 1), 646 ) 647 edge = IREdge(source="&src", dest="&const", port=Port.L, loc=SourceLoc(1, 1)) 648 system = SystemConfig(pe_count=1, sm_count=1) 649 graph = IRGraph( 650 {"&src": source_node, "&const": const_node}, 651 edges=[edge], 652 system=system, 653 ) 654 655 result = generate_direct(graph) 656 657 # The CONST node has an incoming edge, so it should NOT be a seed 658 assert len(result.seed_tokens) == 0 659 660 661class TestMultiPERouting: 662 """Extended tests for multi-PE routing scenarios.""" 663 664 def test_multi_pe_route_computation(self): 665 """Multi-PE graph with multiple cross-PE edges. 666 667 Tests route computation across multiple PEs with various edge patterns. 668 """ 669 # Create a 3-PE system with cross-PE edges 670 node_pe0 = IRNode( 671 name="&a", 672 opcode=ArithOp.ADD, 673 pe=0, 674 iram_offset=0, 675 ctx=0, 676 dest_l=ResolvedDest( 677 name="&b", 678 addr=Addr(a=0, port=Port.L, pe=1), 679 ), 680 loc=SourceLoc(1, 1), 681 ) 682 node_pe1 = IRNode( 683 name="&b", 684 opcode=ArithOp.SUB, 685 pe=1, 686 iram_offset=0, 687 ctx=0, 688 dest_l=ResolvedDest( 689 name="&c", 690 addr=Addr(a=0, port=Port.L, pe=2), 691 ), 692 loc=SourceLoc(2, 1), 693 ) 694 node_pe2 = IRNode( 695 name="&c", 696 opcode=ArithOp.INC, 697 pe=2, 698 iram_offset=0, 699 ctx=0, 700 loc=SourceLoc(3, 1), 701 ) 702 edge1 = IREdge(source="&a", dest="&b", port=Port.L, loc=SourceLoc(1, 1)) 703 edge2 = IREdge(source="&b", dest="&c", port=Port.L, loc=SourceLoc(2, 1)) 704 system = SystemConfig(pe_count=3, sm_count=1) 705 graph = IRGraph( 706 {"&a": node_pe0, "&b": node_pe1, "&c": node_pe2}, 707 edges=[edge1, edge2], 708 system=system, 709 ) 710 711 result = generate_direct(graph) 712 713 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0) 714 pe1_config = next(c for c in result.pe_configs if c.pe_id == 1) 715 pe2_config = next(c for c in result.pe_configs if c.pe_id == 2) 716 717 # PE0 -> PE1 718 assert 1 in pe0_config.allowed_pe_routes 719 # PE1 -> PE2 720 assert 2 in pe1_config.allowed_pe_routes 721 # PE2 has no outgoing edges 722 assert pe2_config.allowed_pe_routes == {2} 723 724 725class TestCTXOvrd: 726 """Tests for CTX_OVRD codegen (AC5.2, AC5.3).""" 727 728 def test_ac52_ctx_override_edge_sets_ctx_mode_1(self): 729 """AC5.2: Node with ctx_override edge gets ctx_mode=1. 730 731 Tests that: 732 - Edge with ctx_override=True triggers ctx_mode=1 in ALUInst 733 - Const field is packed with target ctx and gen 734 """ 735 # Create a source node and a destination node 736 node_src = IRNode( 737 name="&source", 738 opcode=ArithOp.ADD, 739 pe=0, 740 iram_offset=0, 741 ctx=0, 742 dest_l=ResolvedDest( 743 name="&dest", 744 addr=Addr(a=0, port=Port.L, pe=0), 745 ), 746 loc=SourceLoc(1, 1), 747 ) 748 node_dest = IRNode( 749 name="&dest", 750 opcode=ArithOp.SUB, 751 pe=0, 752 iram_offset=1, 753 ctx=3, # Different context (call site ctx) 754 loc=SourceLoc(2, 1), 755 ) 756 # Edge with ctx_override=True (crosses context boundary) 757 edge = IREdge( 758 source="&source", 759 dest="&dest", 760 port=Port.L, 761 ctx_override=True, 762 loc=SourceLoc(1, 1), 763 ) 764 system = SystemConfig(pe_count=1, sm_count=1) 765 graph = IRGraph( 766 {"&source": node_src, "&dest": node_dest}, 767 edges=[edge], 768 system=system, 769 ) 770 771 result = generate_direct(graph) 772 773 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0) 774 src_inst = pe0_config.iram[0] 775 776 # Verify ctx_mode is set to 1 777 assert isinstance(src_inst, ALUInst) 778 assert src_inst.ctx_mode == 1, "ctx_mode should be 1 for ctx_override edge" 779 780 # Verify const is packed: ((target_ctx & 0xF) << 4) | ((target_gen & 0x3) << 2) 781 # target_ctx=3, target_gen=0 -> (3 << 4) | (0 << 2) = 48 782 expected_const = ((3 & 0xF) << 4) | ((0 & 0x3) << 2) 783 assert src_inst.const == expected_const, f"const should be {expected_const}, got {src_inst.const}" 784 785 def test_normal_nodes_have_ctx_mode_0(self): 786 """Normal nodes (no ctx_override) should have ctx_mode=0 (default). 787 788 Tests that: 789 - Nodes without ctx_override edges get ctx_mode=0 790 - Const field remains unchanged 791 """ 792 node_a = IRNode( 793 name="&a", 794 opcode=ArithOp.ADD, 795 pe=0, 796 iram_offset=0, 797 ctx=0, 798 const=42, # Regular ALU const operand 799 dest_l=ResolvedDest( 800 name="&b", 801 addr=Addr(a=0, port=Port.L, pe=0), 802 ), 803 loc=SourceLoc(1, 1), 804 ) 805 node_b = IRNode( 806 name="&b", 807 opcode=ArithOp.SUB, 808 pe=0, 809 iram_offset=1, 810 ctx=0, 811 loc=SourceLoc(2, 1), 812 ) 813 # Normal edge, no ctx_override 814 edge = IREdge(source="&a", dest="&b", port=Port.L, loc=SourceLoc(1, 1)) 815 system = SystemConfig(pe_count=1, sm_count=1) 816 graph = IRGraph( 817 {"&a": node_a, "&b": node_b}, 818 edges=[edge], 819 system=system, 820 ) 821 822 result = generate_direct(graph) 823 824 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0) 825 inst_a = pe0_config.iram[0] 826 827 # Verify ctx_mode is 0 (default) 828 assert isinstance(inst_a, ALUInst) 829 assert inst_a.ctx_mode == 0, "ctx_mode should be 0 for normal edges" 830 # Const should be unchanged 831 assert inst_a.const == 42, "const should remain 42 for normal nodes" 832 833 def test_ac53_conflict_detection_const_and_ctx_override(self): 834 """AC5.3: Node with both const and ctx_override raises error. 835 836 Tests that: 837 - Codegen detects conflict when node has both const and ctx_override edges 838 - Error message is clear 839 """ 840 # Create a node with both const operand AND ctx_override edge 841 node_src = IRNode( 842 name="&source", 843 opcode=ArithOp.ADD, 844 pe=0, 845 iram_offset=0, 846 ctx=0, 847 const=42, # ALU const operand 848 dest_l=ResolvedDest( 849 name="&dest", 850 addr=Addr(a=0, port=Port.L, pe=0), 851 ), 852 loc=SourceLoc(1, 1), 853 ) 854 node_dest = IRNode( 855 name="&dest", 856 opcode=ArithOp.SUB, 857 pe=0, 858 iram_offset=1, 859 ctx=3, 860 loc=SourceLoc(2, 1), 861 ) 862 # Edge with ctx_override=True 863 edge = IREdge( 864 source="&source", 865 dest="&dest", 866 port=Port.L, 867 ctx_override=True, 868 loc=SourceLoc(1, 1), 869 ) 870 system = SystemConfig(pe_count=1, sm_count=1) 871 graph = IRGraph( 872 {"&source": node_src, "&dest": node_dest}, 873 edges=[edge], 874 system=system, 875 ) 876 877 # Should raise ValueError for conflict 878 with pytest.raises(ValueError, match=r"const operand and CTX_OVRD"): 879 generate_direct(graph) 880 881 def test_trampoline_pass_node_normal_codegen(self): 882 """Trampoline PASS nodes generate normal ALUInst (no special handling). 883 884 Tests that: 885 - PASS nodes are codegen'd like any other monadic operation 886 - No special trampoline handling needed in codegen 887 """ 888 # PASS is a monadic routing op used for trampolines 889 node_pass = IRNode( 890 name="&trampoline", 891 opcode=RoutingOp.PASS, 892 pe=0, 893 iram_offset=0, 894 ctx=1, # Call site ctx 895 dest_l=ResolvedDest( 896 name="&next", 897 addr=Addr(a=0, port=Port.L, pe=0), 898 ), 899 loc=SourceLoc(1, 1), 900 ) 901 node_next = IRNode( 902 name="&next", 903 opcode=ArithOp.ADD, 904 pe=0, 905 iram_offset=1, 906 ctx=1, 907 loc=SourceLoc(2, 1), 908 ) 909 edge = IREdge(source="&trampoline", dest="&next", port=Port.L, loc=SourceLoc(1, 1)) 910 system = SystemConfig(pe_count=1, sm_count=1) 911 graph = IRGraph( 912 {"&trampoline": node_pass, "&next": node_next}, 913 edges=[edge], 914 system=system, 915 ) 916 917 result = generate_direct(graph) 918 919 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0) 920 pass_inst = pe0_config.iram[0] 921 922 # Verify PASS node produces normal ALUInst with PASS opcode 923 assert isinstance(pass_inst, ALUInst) 924 assert pass_inst.op == RoutingOp.PASS 925 assert pass_inst.ctx_mode == 0 # Normal operation 926 927 def test_free_ctx_node_normal_codegen(self): 928 """FREE_CTX nodes generate normal ALUInst. 929 930 Tests that: 931 - FREE_CTX (context deallocation) nodes are codegen'd normally 932 """ 933 node_free_ctx = IRNode( 934 name="&free_ctx", 935 opcode=RoutingOp.FREE_CTX, 936 pe=0, 937 iram_offset=0, 938 ctx=2, # Call site ctx to free 939 loc=SourceLoc(1, 1), 940 ) 941 system = SystemConfig(pe_count=1, sm_count=1) 942 graph = IRGraph( 943 {"&free_ctx": node_free_ctx}, 944 system=system, 945 ) 946 947 result = generate_direct(graph) 948 949 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0) 950 free_inst = pe0_config.iram[0] 951 952 # Verify FREE_CTX node produces normal ALUInst 953 assert isinstance(free_inst, ALUInst) 954 assert free_inst.op == RoutingOp.FREE_CTX 955 assert free_inst.ctx_mode == 0 # Normal operation 956 957 def test_packed_const_bit_layout(self): 958 """Verify packed const field bit layout: [reserved:8][ctx:4][gen:2][spare:2]. 959 960 Tests correct packing: 961 - ctx occupies bits [7:4] 962 - gen occupies bits [3:2] 963 - spare bits [1:0] 964 - upper 8 bits reserved (zero) 965 """ 966 node_src = IRNode( 967 name="&source", 968 opcode=ArithOp.ADD, 969 pe=0, 970 iram_offset=0, 971 ctx=0, 972 dest_l=ResolvedDest( 973 name="&dest", 974 addr=Addr(a=0, port=Port.L, pe=0), 975 ), 976 loc=SourceLoc(1, 1), 977 ) 978 # Target ctx=15 (max 4-bit), gen=3 (max 2-bit) 979 node_dest = IRNode( 980 name="&dest", 981 opcode=ArithOp.SUB, 982 pe=0, 983 iram_offset=1, 984 ctx=15, 985 loc=SourceLoc(2, 1), 986 ) 987 edge = IREdge( 988 source="&source", 989 dest="&dest", 990 port=Port.L, 991 ctx_override=True, 992 loc=SourceLoc(1, 1), 993 ) 994 system = SystemConfig(pe_count=1, sm_count=1) 995 graph = IRGraph( 996 {"&source": node_src, "&dest": node_dest}, 997 edges=[edge], 998 system=system, 999 ) 1000 1001 result = generate_direct(graph) 1002 1003 pe0_config = next(c for c in result.pe_configs if c.pe_id == 0) 1004 inst = pe0_config.iram[0] 1005 1006 # Expected: ((15 & 0xF) << 4) | ((0 & 0x3) << 2) = (15 << 4) | 0 = 240 1007 expected = ((15 & 0xF) << 4) | ((0 & 0x3) << 2) 1008 assert inst.const == expected == 240 1009 # Verify upper 8 bits are zero (reserved) 1010 assert inst.const <= 0xFF, "Packed const must fit in lower 8 bits"