OR-1 dataflow CPU sketch
at main 1458 lines 45 kB view raw
1"""Tests for macro expansion pass (Phase 2). 2 3Tests verify: 4- dfasm-macros.AC2.1: Scope-qualified node names (#name_N.&label) 5- dfasm-macros.AC2.2: Literal parameter substitution into const fields 6- dfasm-macros.AC2.3: Ref parameter substitution into edge endpoints 7- dfasm-macros.AC2.4: Nested macro expansion (recursive) 8- dfasm-macros.AC2.5: Macros inside functions (double-scoped) 9- dfasm-macros.AC2.6: Undefined macro → NAME error with suggestions 10- dfasm-macros.AC2.7: Wrong arity → ARITY error 11- dfasm-macros.AC2.8: Recursion depth limit → error 12- dfasm-macros.AC7.3: Unresolvable scope in expanded body → NAME error from resolve 13""" 14 15import re 16 17from lark import Lark 18from pathlib import Path 19 20from asm.expand import expand 21from asm.lower import lower 22from asm.resolve import resolve 23from asm.errors import ErrorCategory 24from asm.ir import ( 25 IRGraph, IRNode, IREdge, MacroDef, MacroParam, ParamRef, ConstExpr, 26 SourceLoc, IRMacroCall 27) 28from cm_inst import ArithOp, Port 29 30 31def _get_parser(): 32 """Get the dfasm parser.""" 33 grammar_path = Path(__file__).parent.parent / "dfasm.lark" 34 return Lark( 35 grammar_path.read_text(), 36 parser="earley", 37 propagate_positions=True, 38 ) 39 40 41def parse_and_lower(source: str) -> IRGraph: 42 """Parse source and lower to IRGraph (before expansion).""" 43 parser = _get_parser() 44 tree = parser.parse(source) 45 return lower(tree) 46 47 48def parse_lower_expand(source: str) -> IRGraph: 49 """Parse, lower, and expand.""" 50 graph = parse_and_lower(source) 51 return expand(graph) 52 53 54class TestAC21_ScopeQualification: 55 """AC2.1: Nodes are scope-qualified with #macroname_N prefix.""" 56 57 def test_simple_macro_invocation_creates_qualified_node(self): 58 """Invoking #wrap creates node with qualified name.""" 59 source = """ 60 @system pe=1, sm=1 61 62 #wrap |> { 63 &inner <| pass 64 } 65 66 #wrap 67 """ 68 graph = parse_lower_expand(source) 69 70 # Look for qualified node name 71 qualified_names = [n for n in graph.nodes.keys() if "#wrap_0.&inner" in n] 72 assert len(qualified_names) == 1, f"Expected #wrap_0.&inner in {list(graph.nodes.keys())}" 73 74 def test_multiple_invocations_get_unique_scopes(self): 75 """Multiple invocations get unique counter values.""" 76 source = """ 77 @system pe=1, sm=1 78 79 #simple |> { 80 &node <| pass 81 } 82 83 #simple 84 #simple 85 """ 86 graph = parse_lower_expand(source) 87 88 # Should have both _0 and _1 scopes 89 nodes = list(graph.nodes.keys()) 90 has_0 = any("#simple_0" in n for n in nodes) 91 has_1 = any("#simple_1" in n for n in nodes) 92 assert has_0 and has_1, f"Expected unique scopes in {nodes}" 93 94 95class TestAC22_LiteralSubstitution: 96 """AC2.2: Literal parameters substitute into const fields. 97 98 Uses ${param} syntax in macro body to reference formal parameters in 99 const positions. The grammar's param_ref rule parses ${IDENT} and the 100 lowerer creates ParamRef objects, which the expand pass substitutes 101 with actual argument values. 102 """ 103 104 def test_const_substitution_inline(self): 105 """${param} in inline_const position substitutes the argument value.""" 106 source = """ 107 @system pe=1, sm=1 108 109 #with_const val |> { 110 &node <| const ${val} 111 } 112 113 #with_const 42 114 """ 115 graph = parse_lower_expand(source) 116 assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" 117 118 expanded = [n for n in graph.nodes.values() if "#with_const_0" in n.name] 119 assert len(expanded) == 1 120 assert expanded[0].const == 42 121 122 def test_const_substitution_comma_separated(self): 123 """${param} in comma-separated argument position substitutes into const.""" 124 source = """ 125 @system pe=1, sm=1 126 127 #with_const val |> { 128 &node <| const, ${val} 129 } 130 131 #with_const 7 132 """ 133 graph = parse_lower_expand(source) 134 assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" 135 136 expanded = [n for n in graph.nodes.values() if "#with_const_0" in n.name] 137 assert len(expanded) == 1 138 assert expanded[0].const == 7 139 140 def test_const_substitution_with_hex(self): 141 """${param} substitutes hex literal arguments correctly.""" 142 source = """ 143 @system pe=1, sm=1 144 145 #set_val v |> { 146 &node <| const ${v} 147 } 148 149 #set_val 0xFF 150 """ 151 graph = parse_lower_expand(source) 152 assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" 153 154 expanded = [n for n in graph.nodes.values() if "#set_val_0" in n.name] 155 assert len(expanded) == 1 156 assert expanded[0].const == 255 157 158 159class TestAC23_RefSubstitution: 160 """AC2.3: Ref parameters substitute into edge endpoints. 161 162 Uses ${param} syntax in macro body edge endpoints to reference formal 163 parameters. The lowerer creates ParamRef objects from ${IDENT} in 164 qualified_ref positions, and the expand pass substitutes them with 165 actual argument values (label/node references). 166 """ 167 168 def test_edge_dest_substitution(self): 169 """${param} in edge dest position wires to the actual argument ref.""" 170 source = """ 171 @system pe=1, sm=1 172 173 &external <| pass 174 175 #connect_to target |> { 176 &src <| const, 1 177 &src |> ${target}:L 178 } 179 180 #connect_to &external 181 """ 182 graph = parse_lower_expand(source) 183 assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" 184 185 src_node = [n for n in graph.nodes.keys() if "#connect_to_0" in n and "&src" in n][0] 186 187 found = any( 188 e.source == src_node and e.dest == "&external" 189 for e in graph.edges 190 ) 191 assert found, f"Expected edge from {src_node} to &external, edges: {[(e.source, e.dest) for e in graph.edges]}" 192 193 def test_edge_source_substitution(self): 194 """${param} in edge source position wires from the actual argument ref.""" 195 source = """ 196 @system pe=1, sm=1 197 198 &provider <| const, 5 199 200 #read_from src |> { 201 &sink <| pass 202 ${src} |> &sink:L 203 } 204 205 #read_from &provider 206 """ 207 graph = parse_lower_expand(source) 208 assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" 209 210 sink_node = [n for n in graph.nodes.keys() if "#read_from_0" in n and "&sink" in n][0] 211 212 found = any( 213 e.source == "&provider" and e.dest == sink_node 214 for e in graph.edges 215 ) 216 assert found, f"Expected edge from &provider to {sink_node}, edges: {[(e.source, e.dest) for e in graph.edges]}" 217 218 def test_both_endpoints_substituted(self): 219 """${param} in both source and dest positions substitutes correctly.""" 220 source = """ 221 @system pe=1, sm=1 222 223 &a <| const, 1 224 &b <| pass 225 226 #wire_between src, dest |> { 227 ${src} |> ${dest}:L 228 } 229 230 #wire_between &a, &b 231 """ 232 graph = parse_lower_expand(source) 233 assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" 234 235 found = any( 236 e.source == "&a" and e.dest == "&b" 237 for e in graph.edges 238 ) 239 assert found, f"Expected edge &a -> &b, edges: {[(e.source, e.dest) for e in graph.edges]}" 240 241 242class TestAC24_NestedExpansion: 243 """AC2.4: Nested macro calls expand recursively.""" 244 245 def test_nested_macro_expansion(self): 246 """Invoking outer which calls inner creates double-scoped nodes.""" 247 source = """ 248 @system pe=1, sm=1 249 250 #inner |> { 251 &x <| pass 252 } 253 254 #outer |> { 255 #inner 256 } 257 258 #outer 259 """ 260 graph = parse_lower_expand(source) 261 262 # Should have both #outer and #inner scopes 263 node_names = list(graph.nodes.keys()) 264 # Look for nodes qualified with #outer_N and #inner_M scopes 265 outer_nodes = [n for n in node_names if "#outer_" in n] 266 inner_nodes = [n for n in node_names if "#inner_" in n] 267 assert len(outer_nodes) > 0, f"Expected #outer_ scoped nodes in {node_names}" 268 assert len(inner_nodes) > 0, f"Expected #inner_ scoped nodes in {node_names}" 269 270 271class TestAC25_FunctionScoping: 272 """AC2.5: Macros inside functions get double-scoped.""" 273 274 def test_macro_in_function_creates_double_scope(self): 275 """Macro inside function gets $func.#macro_N.&label scope.""" 276 source = """ 277 @system pe=1, sm=1 278 279 #inject |> { 280 &gate <| pass 281 } 282 283 $func |> { 284 #inject 285 } 286 """ 287 graph = parse_lower_expand(source) 288 289 # Look for node with pattern $func.#inject_N.&gate 290 # This will be in a function region body, not at top level 291 node_names = list(graph.nodes.keys()) 292 all_node_names = node_names.copy() 293 294 # Check region bodies too 295 for region in graph.regions: 296 all_node_names.extend(region.body.nodes.keys()) 297 298 # Use regex to find pattern 299 pattern = r'\$func\.#inject_\d+\.&gate' 300 found = any(re.search(pattern, name) for name in all_node_names) 301 assert found, f"Expected $func.#inject_N.&gate pattern in {all_node_names}" 302 303 304class TestAC26_UndefinedMacro: 305 """AC2.6: Undefined macro invocation → MACRO error with suggestions.""" 306 307 def test_undefined_macro_produces_macro_error(self): 308 """Invoking undefined macro produces MACRO error.""" 309 source = """ 310 @system pe=1, sm=1 311 312 #undefined_macro &a 313 """ 314 graph = parse_lower_expand(source) 315 316 # Should have an error 317 assert len(graph.errors) > 0, "Expected error for undefined macro" 318 error = graph.errors[0] 319 assert error.category == ErrorCategory.MACRO, f"Expected MACRO error, got {error.category}" 320 assert "undefined" in error.message.lower(), f"Expected 'undefined' in message: {error.message}" 321 322 def test_undefined_macro_has_suggestions(self): 323 """Undefined macro with similar name gets suggestions.""" 324 source = """ 325 @system pe=1, sm=1 326 327 #simple |> { 328 &x <| pass 329 } 330 331 #simpl 332 """ 333 graph = parse_lower_expand(source) 334 335 # Should have error with suggestions 336 assert len(graph.errors) > 0 337 error = graph.errors[0] 338 assert len(error.suggestions) > 0, f"Expected suggestions for typo, got {error.suggestions}" 339 340 341class TestAC27_MacroArityError: 342 """AC2.7: Wrong arity on macro invocation → MACRO error.""" 343 344 def test_too_few_arguments(self): 345 """Providing too few arguments produces MACRO error.""" 346 source = """ 347 @system pe=1, sm=1 348 349 #needs_two a, b |> { 350 &x <| pass 351 } 352 353 #needs_two &a 354 """ 355 graph = parse_lower_expand(source) 356 357 # Should have MACRO error 358 assert len(graph.errors) > 0, "Expected error" 359 error = graph.errors[0] 360 assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" 361 assert "2" in error.message and "1" in error.message, f"Expected counts in message: {error.message}" 362 363 def test_too_many_arguments(self): 364 """Providing too many arguments produces MACRO error.""" 365 source = """ 366 @system pe=1, sm=1 367 368 #needs_one a |> { 369 &x <| pass 370 } 371 372 #needs_one &a, &b, &c 373 """ 374 graph = parse_lower_expand(source) 375 376 assert len(graph.errors) > 0 377 error = graph.errors[0] 378 assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" 379 380 381class TestAC28_RecursionDepth: 382 """AC2.8: Macro recursion exceeding depth limit → error.""" 383 384 def test_infinite_recursion_caught(self): 385 """Infinite recursion is caught at depth limit.""" 386 source = """ 387 @system pe=1, sm=1 388 389 #recursive |> { 390 #recursive 391 } 392 393 #recursive 394 """ 395 graph = parse_lower_expand(source) 396 397 # Should have error about depth 398 assert len(graph.errors) > 0, "Expected error for infinite recursion" 399 error = graph.errors[0] 400 # The error message should mention depth or recursion 401 msg = error.message.lower() 402 assert ("depth" in msg or "recursion" in msg), f"Expected depth/recursion mention in: {error.message}" 403 404 405class TestAC73_UnresolvableScope: 406 """AC7.3: Unresolvable scope in expanded body → NAME error from resolve.""" 407 408 def test_macro_with_unresolvable_scope_ref(self): 409 """Macro body with unresolvable scope reference surfaces error at resolve time.""" 410 source = """ 411 @system pe=1, sm=1 412 413 #bad_ref |> { 414 &node <| pass 415 &node |> $nonexistent.&target:L 416 } 417 418 #bad_ref 419 """ 420 # Expand alone may not error (if scope checking is in resolve) 421 graph = parse_lower_expand(source) 422 # Resolve to check scope validity 423 graph = resolve(graph) 424 425 # Should have NAME error from resolve about nonexistent scope 426 has_error = any( 427 e.category == ErrorCategory.NAME 428 for e in graph.errors 429 ) 430 assert has_error, f"Expected NAME error from resolve, got {[e.category for e in graph.errors]}" 431 432 433class TestMacroDefAndCallCleanup: 434 """Macro definitions and calls are removed from output.""" 435 436 def test_no_macro_defs_in_output(self): 437 """Output graph has no macro_defs.""" 438 source = """ 439 @system pe=1, sm=1 440 441 #simple |> { 442 &x <| pass 443 } 444 445 #simple 446 """ 447 graph = parse_lower_expand(source) 448 449 assert len(graph.macro_defs) == 0, "Expected macro_defs to be empty" 450 451 def test_no_macro_calls_in_output(self): 452 """Output graph has no macro_calls.""" 453 source = """ 454 @system pe=1, sm=1 455 456 #simple |> { 457 &x <| pass 458 } 459 460 #simple 461 """ 462 graph = parse_lower_expand(source) 463 464 assert len(graph.macro_calls) == 0, "Expected macro_calls to be empty" 465 466 467class TestMacroWithNoParams: 468 """Macro with no parameters expands correctly.""" 469 470 def test_parameterless_macro(self): 471 """Macro without params can be invoked without args.""" 472 source = """ 473 @system pe=1, sm=1 474 475 #identity |> { 476 &x <| pass 477 } 478 479 #identity 480 """ 481 graph = parse_lower_expand(source) 482 483 # Should expand without errors 484 assert len(graph.errors) == 0, f"Expected no errors, got {graph.errors}" 485 # Should have qualified node 486 names = list(graph.nodes.keys()) 487 assert any("#identity_0" in n for n in names), f"Expected #identity_0 in {names}" 488 489 490class TestMultipleMacroDefinitions: 491 """Multiple macros can be defined and invoked in same program.""" 492 493 def test_multiple_macros(self): 494 """Define and invoke multiple different macros.""" 495 source = """ 496 @system pe=1, sm=1 497 498 #macro_a |> { 499 &a <| pass 500 } 501 502 #macro_b |> { 503 &b <| pass 504 } 505 506 #macro_a 507 #macro_b 508 """ 509 graph = parse_lower_expand(source) 510 511 # Should have both expanded nodes 512 names = list(graph.nodes.keys()) 513 has_a = any("#macro_a_" in n for n in names) 514 has_b = any("#macro_b_" in n for n in names) 515 assert has_a and has_b, f"Expected both macros in {names}" 516 517 518class TestExpansionCounterIncrement: 519 """Expansion counter increments per invocation.""" 520 521 def test_counter_increments_across_invocations(self): 522 """Same macro invoked twice gets _0 and _1.""" 523 source = """ 524 @system pe=1, sm=1 525 526 #dup |> { 527 &node <| pass 528 } 529 530 #dup 531 #dup 532 """ 533 graph = parse_lower_expand(source) 534 535 names = list(graph.nodes.keys()) 536 has_0 = any("#dup_0" in n for n in names) 537 has_1 = any("#dup_1" in n for n in names) 538 assert has_0 and has_1, f"Expected _0 and _1 in {names}" 539 540 541class TestMacroErrorPropagation: 542 """Errors in macro body template surface at expansion time.""" 543 544 def test_macro_body_with_unknown_opcode_surfaces_error(self): 545 """Errors from macro body lowering surface in expanded graph. 546 547 Note: In Phase 2, grammar validation prevents invalid opcodes from 548 appearing in macro bodies. This test is deferred to Phase 3 when 549 validation of parameter-referenced opcodes becomes possible. 550 """ 551 # Grammar ensures only valid opcodes can appear in macro bodies 552 # This test will be more relevant in Phase 3 with token pasting 553 source = """ 554 @system pe=1, sm=1 555 556 #valid_macro |> { 557 &node <| pass 558 } 559 560 #valid_macro 561 """ 562 graph = parse_lower_expand(source) 563 # Valid macros should have no errors (syntax validation is in lower pass) 564 assert len(graph.errors) == 0 565 566 567class TestAC31_TokenPasting: 568 """AC3.1: ParamRef with prefix/suffix concatenates into label names. 569 570 Tests construct IR directly (not from parsing) to test expand in isolation. 571 """ 572 573 def test_token_paste_with_prefix_and_suffix(self): 574 """Token pasting with both prefix and suffix creates concatenated name.""" 575 # Create macro body with ParamRef containing prefix/suffix 576 param_ref = ParamRef(param="func", prefix="&__", suffix="_fan") 577 body_node = IRNode( 578 name=param_ref, # This will be a ParamRef in the node name 579 opcode=ArithOp.ADD, 580 loc=SourceLoc(0, 0), 581 ) 582 583 macro_body = IRGraph( 584 nodes={"node_placeholder": body_node}, 585 edges=[], 586 macro_defs=[], 587 macro_calls=[], 588 ) 589 590 # Create macro definition 591 macro_def = MacroDef( 592 name="make_fan", 593 params=(MacroParam(name="func"),), 594 body=macro_body, 595 loc=SourceLoc(0, 0), 596 ) 597 598 # Create a graph with the macro and a call to it 599 # The macro call with argument "fib" 600 macro_call = IRMacroCall( 601 name="make_fan", 602 positional_args=("fib",), 603 named_args=(), 604 loc=SourceLoc(0, 0), 605 ) 606 607 graph = IRGraph( 608 nodes={}, 609 edges=[], 610 regions=[], 611 data_defs=[], 612 macro_defs=[macro_def], 613 macro_calls=[macro_call], 614 ) 615 616 # Expand the graph 617 expanded = expand(graph) 618 619 # After expansion, the node should have name: #macro_N.&__fib_fan 620 node_names = list(expanded.nodes.keys()) 621 # Look for the pasted name pattern 622 found = any("&__fib_fan" in name for name in node_names) 623 assert found, f"Expected node with &__fib_fan in name, got {node_names}" 624 625 def test_token_paste_with_prefix_only(self): 626 """Token pasting with prefix only concatenates correctly.""" 627 param_ref = ParamRef(param="x", prefix="&pre_", suffix="") 628 body_node = IRNode( 629 name=param_ref, 630 opcode=ArithOp.ADD, 631 loc=SourceLoc(0, 0), 632 ) 633 634 macro_body = IRGraph( 635 nodes={"node_placeholder": body_node}, 636 edges=[], 637 macro_defs=[], 638 macro_calls=[], 639 ) 640 641 macro_def = MacroDef( 642 name="prefix_test", 643 params=(MacroParam(name="x"),), 644 body=macro_body, 645 loc=SourceLoc(0, 0), 646 ) 647 648 macro_call = IRMacroCall( 649 name="prefix_test", 650 positional_args=("val",), 651 named_args=(), 652 loc=SourceLoc(0, 0), 653 ) 654 655 graph = IRGraph( 656 nodes={}, 657 edges=[], 658 regions=[], 659 data_defs=[], 660 macro_defs=[macro_def], 661 macro_calls=[macro_call], 662 ) 663 664 expanded = expand(graph) 665 666 # Should have node with &pre_val 667 node_names = list(expanded.nodes.keys()) 668 found = any("&pre_val" in name for name in node_names) 669 assert found, f"Expected &pre_val in {node_names}" 670 671 def test_token_paste_with_suffix_only(self): 672 """Token pasting with suffix only concatenates correctly.""" 673 param_ref = ParamRef(param="x", prefix="", suffix="_post") 674 body_node = IRNode( 675 name=param_ref, 676 opcode=ArithOp.ADD, 677 loc=SourceLoc(0, 0), 678 ) 679 680 macro_body = IRGraph( 681 nodes={"node_placeholder": body_node}, 682 edges=[], 683 macro_defs=[], 684 macro_calls=[], 685 ) 686 687 macro_def = MacroDef( 688 name="suffix_test", 689 params=(MacroParam(name="x"),), 690 body=macro_body, 691 loc=SourceLoc(0, 0), 692 ) 693 694 macro_call = IRMacroCall( 695 name="suffix_test", 696 positional_args=("val",), 697 named_args=(), 698 loc=SourceLoc(0, 0), 699 ) 700 701 graph = IRGraph( 702 nodes={}, 703 edges=[], 704 regions=[], 705 data_defs=[], 706 macro_defs=[macro_def], 707 macro_calls=[macro_call], 708 ) 709 710 expanded = expand(graph) 711 712 # Should have node with val_post 713 node_names = list(expanded.nodes.keys()) 714 found = any("val_post" in name for name in node_names) 715 assert found, f"Expected val_post in {node_names}" 716 717 def test_token_paste_in_edge_source(self): 718 """Token pasting in edge source reference resolves correctly.""" 719 # Create two nodes: one with ParamRef source name, one regular dest 720 param_ref_src = ParamRef(param="src", prefix="&", suffix="") 721 regular_dest = "&dest" 722 723 body_nodes = { 724 "src_placeholder": IRNode( 725 name=param_ref_src, 726 opcode=ArithOp.ADD, 727 loc=SourceLoc(0, 0), 728 ), 729 "&dest": IRNode( 730 name=regular_dest, 731 opcode=ArithOp.ADD, 732 loc=SourceLoc(0, 0), 733 ), 734 } 735 736 # Edge connects the pasted source to the destination 737 body_edges = [ 738 IREdge( 739 source=param_ref_src, 740 dest=regular_dest, 741 port=Port.L, 742 loc=SourceLoc(0, 0), 743 ) 744 ] 745 746 macro_body = IRGraph( 747 nodes=body_nodes, 748 edges=body_edges, 749 macro_defs=[], 750 macro_calls=[], 751 ) 752 753 macro_def = MacroDef( 754 name="edge_src_test", 755 params=(MacroParam(name="src"),), 756 body=macro_body, 757 loc=SourceLoc(0, 0), 758 ) 759 760 macro_call = IRMacroCall( 761 name="edge_src_test", 762 positional_args=("input",), 763 named_args=(), 764 loc=SourceLoc(0, 0), 765 ) 766 767 graph = IRGraph( 768 nodes={}, 769 edges=[], 770 regions=[], 771 data_defs=[], 772 macro_defs=[macro_def], 773 macro_calls=[macro_call], 774 ) 775 776 expanded = expand(graph) 777 778 # Check that edges have the resolved pasted names 779 edges = expanded.edges 780 # Find the edge, both source and dest should be qualified with #edge_src_test_0.&name 781 found = False 782 for edge in edges: 783 if "&input" in edge.source and "&dest" in edge.dest: 784 found = True 785 break 786 assert found, f"Expected edge with pasted &input source, got {[(e.source, e.dest) for e in edges]}" 787 788 def test_token_paste_in_edge_dest(self): 789 """Token pasting in edge dest reference resolves correctly.""" 790 param_ref_dest = ParamRef(param="dest", prefix="&", suffix="") 791 regular_src = "&src" 792 793 body_nodes = { 794 "&src": IRNode( 795 name=regular_src, 796 opcode=ArithOp.ADD, 797 loc=SourceLoc(0, 0), 798 ), 799 "dest_placeholder": IRNode( 800 name=param_ref_dest, 801 opcode=ArithOp.ADD, 802 loc=SourceLoc(0, 0), 803 ), 804 } 805 806 # Edge connects source to the pasted destination 807 body_edges = [ 808 IREdge( 809 source=regular_src, 810 dest=param_ref_dest, 811 port=Port.L, 812 loc=SourceLoc(0, 0), 813 ) 814 ] 815 816 macro_body = IRGraph( 817 nodes=body_nodes, 818 edges=body_edges, 819 macro_defs=[], 820 macro_calls=[], 821 ) 822 823 macro_def = MacroDef( 824 name="edge_dest_test", 825 params=(MacroParam(name="dest"),), 826 body=macro_body, 827 loc=SourceLoc(0, 0), 828 ) 829 830 macro_call = IRMacroCall( 831 name="edge_dest_test", 832 positional_args=("output",), 833 named_args=(), 834 loc=SourceLoc(0, 0), 835 ) 836 837 graph = IRGraph( 838 nodes={}, 839 edges=[], 840 regions=[], 841 data_defs=[], 842 macro_defs=[macro_def], 843 macro_calls=[macro_call], 844 ) 845 846 expanded = expand(graph) 847 848 # Check that edges have the resolved pasted names 849 edges = expanded.edges 850 found = False 851 for edge in edges: 852 if "&src" in edge.source and "&output" in edge.dest: 853 found = True 854 break 855 assert found, f"Expected edge with pasted &output dest, got {[(e.source, e.dest) for e in edges]}" 856 857 858class TestAC32_ConstantExpressionEvaluation: 859 """AC3.2: Constant expressions evaluate during macro expansion.""" 860 861 def test_simple_param_substitution_in_const(self): 862 """ParamRef(param="val") + arg=42 -> const=42.""" 863 # Create macro with ParamRef in const field 864 node = IRNode( 865 name="&inner", 866 opcode=ArithOp.ADD, 867 const=ParamRef(param="val"), 868 ) 869 macro_def = MacroDef( 870 name="simple_const", 871 params=(MacroParam(name="val"),), 872 body=IRGraph( 873 nodes={"&inner": node}, 874 edges=[], 875 regions=[], 876 data_defs=[], 877 macro_defs=[], 878 macro_calls=[], 879 ), 880 ) 881 882 macro_call = IRMacroCall( 883 name="simple_const", 884 positional_args=(42,), 885 ) 886 887 graph = IRGraph( 888 nodes={}, 889 edges=[], 890 regions=[], 891 data_defs=[], 892 macro_defs=[macro_def], 893 macro_calls=[macro_call], 894 ) 895 896 expanded = expand(graph) 897 898 # Find the expanded node 899 nodes_with_const = [ 900 (name, node.const) for name, node in expanded.nodes.items() 901 if node.const is not None 902 ] 903 assert any(const == 42 for _, const in nodes_with_const), \ 904 f"Expected const=42, got {nodes_with_const}" 905 906 def test_const_expr_addition_with_one_param(self): 907 """ConstExpr('val + 1') with val=5 evaluates to 6.""" 908 # Create macro with ConstExpr in const field 909 node = IRNode( 910 name="&inner", 911 opcode=ArithOp.ADD, 912 const=ConstExpr( 913 expression="val + 1", 914 params=("val",), 915 ), 916 ) 917 macro_def = MacroDef( 918 name="add_one", 919 params=(MacroParam(name="val"),), 920 body=IRGraph( 921 nodes={"&inner": node}, 922 edges=[], 923 regions=[], 924 data_defs=[], 925 macro_defs=[], 926 macro_calls=[], 927 ), 928 ) 929 930 macro_call = IRMacroCall( 931 name="add_one", 932 positional_args=(5,), 933 ) 934 935 graph = IRGraph( 936 nodes={}, 937 edges=[], 938 regions=[], 939 data_defs=[], 940 macro_defs=[macro_def], 941 macro_calls=[macro_call], 942 ) 943 944 expanded = expand(graph) 945 946 # Verify no errors 947 assert not expanded.errors, f"Expected no errors, got {expanded.errors}" 948 949 # Find the expanded node and verify const 950 nodes_with_const = [ 951 (name, node.const) for name, node in expanded.nodes.items() 952 if node.const is not None 953 ] 954 assert any(const == 6 for _, const in nodes_with_const), \ 955 f"Expected const=6, got {nodes_with_const}" 956 957 def test_const_expr_subtraction(self): 958 """ConstExpr('val - 1') with val=10 evaluates to 9.""" 959 node = IRNode( 960 name="&inner", 961 opcode=ArithOp.ADD, 962 const=ConstExpr( 963 expression="val - 1", 964 params=("val",), 965 ), 966 ) 967 macro_def = MacroDef( 968 name="sub_one", 969 params=(MacroParam(name="val"),), 970 body=IRGraph( 971 nodes={"&inner": node}, 972 edges=[], 973 regions=[], 974 data_defs=[], 975 macro_defs=[], 976 macro_calls=[], 977 ), 978 ) 979 980 macro_call = IRMacroCall( 981 name="sub_one", 982 positional_args=(10,), 983 ) 984 985 graph = IRGraph( 986 nodes={}, 987 edges=[], 988 regions=[], 989 data_defs=[], 990 macro_defs=[macro_def], 991 macro_calls=[macro_call], 992 ) 993 994 expanded = expand(graph) 995 996 assert not expanded.errors, f"Expected no errors, got {expanded.errors}" 997 998 nodes_with_const = [ 999 (name, node.const) for name, node in expanded.nodes.items() 1000 if node.const is not None 1001 ] 1002 assert any(const == 9 for _, const in nodes_with_const), \ 1003 f"Expected const=9, got {nodes_with_const}" 1004 1005 def test_const_expr_multiplication(self): 1006 """ConstExpr('val * 2') with val=4 evaluates to 8.""" 1007 node = IRNode( 1008 name="&inner", 1009 opcode=ArithOp.ADD, 1010 const=ConstExpr( 1011 expression="val * 2", 1012 params=("val",), 1013 ), 1014 ) 1015 macro_def = MacroDef( 1016 name="double", 1017 params=(MacroParam(name="val"),), 1018 body=IRGraph( 1019 nodes={"&inner": node}, 1020 edges=[], 1021 regions=[], 1022 data_defs=[], 1023 macro_defs=[], 1024 macro_calls=[], 1025 ), 1026 ) 1027 1028 macro_call = IRMacroCall( 1029 name="double", 1030 positional_args=(4,), 1031 ) 1032 1033 graph = IRGraph( 1034 nodes={}, 1035 edges=[], 1036 regions=[], 1037 data_defs=[], 1038 macro_defs=[macro_def], 1039 macro_calls=[macro_call], 1040 ) 1041 1042 expanded = expand(graph) 1043 1044 assert not expanded.errors, f"Expected no errors, got {expanded.errors}" 1045 1046 nodes_with_const = [ 1047 (name, node.const) for name, node in expanded.nodes.items() 1048 if node.const is not None 1049 ] 1050 assert any(const == 8 for _, const in nodes_with_const), \ 1051 f"Expected const=8, got {nodes_with_const}" 1052 1053 def test_const_expr_multiple_params(self): 1054 """ConstExpr('a + b') with a=3, b=7 evaluates to 10.""" 1055 node = IRNode( 1056 name="&inner", 1057 opcode=ArithOp.ADD, 1058 const=ConstExpr( 1059 expression="a + b", 1060 params=("a", "b"), 1061 ), 1062 ) 1063 macro_def = MacroDef( 1064 name="add_two", 1065 params=(MacroParam(name="a"), MacroParam(name="b")), 1066 body=IRGraph( 1067 nodes={"&inner": node}, 1068 edges=[], 1069 regions=[], 1070 data_defs=[], 1071 macro_defs=[], 1072 macro_calls=[], 1073 ), 1074 ) 1075 1076 macro_call = IRMacroCall( 1077 name="add_two", 1078 positional_args=(3, 7), 1079 ) 1080 1081 graph = IRGraph( 1082 nodes={}, 1083 edges=[], 1084 regions=[], 1085 data_defs=[], 1086 macro_defs=[macro_def], 1087 macro_calls=[macro_call], 1088 ) 1089 1090 expanded = expand(graph) 1091 1092 assert not expanded.errors, f"Expected no errors, got {expanded.errors}" 1093 1094 nodes_with_const = [ 1095 (name, node.const) for name, node in expanded.nodes.items() 1096 if node.const is not None 1097 ] 1098 assert any(const == 10 for _, const in nodes_with_const), \ 1099 f"Expected const=10, got {nodes_with_const}" 1100 1101 def test_const_expr_with_literal(self): 1102 """ConstExpr('5 + val') with val=2 evaluates to 7.""" 1103 node = IRNode( 1104 name="&inner", 1105 opcode=ArithOp.ADD, 1106 const=ConstExpr( 1107 expression="5 + val", 1108 params=("val",), 1109 ), 1110 ) 1111 macro_def = MacroDef( 1112 name="literal_plus", 1113 params=(MacroParam(name="val"),), 1114 body=IRGraph( 1115 nodes={"&inner": node}, 1116 edges=[], 1117 regions=[], 1118 data_defs=[], 1119 macro_defs=[], 1120 macro_calls=[], 1121 ), 1122 ) 1123 1124 macro_call = IRMacroCall( 1125 name="literal_plus", 1126 positional_args=(2,), 1127 ) 1128 1129 graph = IRGraph( 1130 nodes={}, 1131 edges=[], 1132 regions=[], 1133 data_defs=[], 1134 macro_defs=[macro_def], 1135 macro_calls=[macro_call], 1136 ) 1137 1138 expanded = expand(graph) 1139 1140 assert not expanded.errors, f"Expected no errors, got {expanded.errors}" 1141 1142 nodes_with_const = [ 1143 (name, node.const) for name, node in expanded.nodes.items() 1144 if node.const is not None 1145 ] 1146 assert any(const == 7 for _, const in nodes_with_const), \ 1147 f"Expected const=7, got {nodes_with_const}" 1148 1149 1150class TestAC33_ConstExprNonNumericValues: 1151 """AC3.3: Non-numeric values in arithmetic context produce VALUE error.""" 1152 1153 def test_non_numeric_param_in_arithmetic(self): 1154 """ParamRef in arithmetic context with non-int arg -> VALUE error.""" 1155 node = IRNode( 1156 name="&inner", 1157 opcode=ArithOp.ADD, 1158 const=ConstExpr( 1159 expression="val + 1", 1160 params=("val",), 1161 ), 1162 ) 1163 macro_def = MacroDef( 1164 name="arith_with_ref", 1165 params=(MacroParam(name="val"),), 1166 body=IRGraph( 1167 nodes={"&inner": node}, 1168 edges=[], 1169 regions=[], 1170 data_defs=[], 1171 macro_defs=[], 1172 macro_calls=[], 1173 ), 1174 ) 1175 1176 # Pass a reference name (&label) instead of an integer 1177 macro_call = IRMacroCall( 1178 name="arith_with_ref", 1179 positional_args=("&label",), 1180 ) 1181 1182 graph = IRGraph( 1183 nodes={}, 1184 edges=[], 1185 regions=[], 1186 data_defs=[], 1187 macro_defs=[macro_def], 1188 macro_calls=[macro_call], 1189 ) 1190 1191 expanded = expand(graph) 1192 1193 # Verify we have an error 1194 assert expanded.errors, "Expected VALUE error for non-numeric in arithmetic" 1195 assert any( 1196 e.category == ErrorCategory.VALUE for e in expanded.errors 1197 ), f"Expected VALUE error, got {[e.category for e in expanded.errors]}" 1198 1199 def test_undefined_param_in_arithmetic(self): 1200 """Undefined parameter in arithmetic expression -> VALUE error.""" 1201 node = IRNode( 1202 name="&inner", 1203 opcode=ArithOp.ADD, 1204 const=ConstExpr( 1205 expression="undefined_param + 1", 1206 params=("undefined_param",), 1207 ), 1208 ) 1209 macro_def = MacroDef( 1210 name="undefined", 1211 params=(MacroParam(name="val"),), 1212 body=IRGraph( 1213 nodes={"&inner": node}, 1214 edges=[], 1215 regions=[], 1216 data_defs=[], 1217 macro_defs=[], 1218 macro_calls=[], 1219 ), 1220 ) 1221 1222 macro_call = IRMacroCall( 1223 name="undefined", 1224 positional_args=(5,), 1225 ) 1226 1227 graph = IRGraph( 1228 nodes={}, 1229 edges=[], 1230 regions=[], 1231 data_defs=[], 1232 macro_defs=[macro_def], 1233 macro_calls=[macro_call], 1234 ) 1235 1236 expanded = expand(graph) 1237 1238 # Verify we have an error 1239 assert expanded.errors, "Expected VALUE error for undefined parameter" 1240 assert any( 1241 e.category == ErrorCategory.VALUE for e in expanded.errors 1242 ), f"Expected VALUE error, got {[e.category for e in expanded.errors]}" 1243 1244 1245class TestSourceLocationThreading: 1246 """Verify that errors during macro expansion include source location context.""" 1247 1248 def test_expansion_error_includes_macro_location(self): 1249 """Non-numeric param in arithmetic triggers VALUE error with expansion context.""" 1250 # Create a macro that performs arithmetic on a parameter 1251 node = IRNode( 1252 name="&inner", 1253 opcode=ArithOp.ADD, 1254 const=ConstExpr( 1255 expression="val + 1", 1256 params=("val",), 1257 loc=SourceLoc(10, 5), 1258 ), 1259 ) 1260 macro_def = MacroDef( 1261 name="arith_test", 1262 params=(MacroParam(name="val"),), 1263 body=IRGraph( 1264 nodes={"&inner": node}, 1265 edges=[], 1266 regions=[], 1267 data_defs=[], 1268 macro_defs=[], 1269 macro_calls=[], 1270 ), 1271 loc=SourceLoc(5, 0), 1272 ) 1273 1274 # Create a macro call at a specific location that passes a non-numeric value 1275 macro_call = IRMacroCall( 1276 name="arith_test", 1277 positional_args=("&label",), # Non-numeric value 1278 loc=SourceLoc(20, 10), # Call location 1279 ) 1280 1281 graph = IRGraph( 1282 nodes={}, 1283 edges=[], 1284 regions=[], 1285 data_defs=[], 1286 macro_defs=[macro_def], 1287 macro_calls=[macro_call], 1288 ) 1289 1290 expanded = expand(graph) 1291 1292 # Verify we have a VALUE error 1293 assert expanded.errors, "Expected VALUE error" 1294 value_error = next( 1295 (e for e in expanded.errors if e.category == ErrorCategory.VALUE), None 1296 ) 1297 assert value_error is not None, "Expected VALUE error in errors list" 1298 1299 # Verify the error has context_lines with expansion context 1300 assert value_error.context_lines, "Expected context_lines with expansion context" 1301 assert any( 1302 "expanded from" in ctx and "arith_test" in ctx 1303 for ctx in value_error.context_lines 1304 ), f"Expected 'expanded from #arith_test' in context_lines, got {value_error.context_lines}" 1305 1306 # Verify the context line contains the call location 1307 expansion_ctx = next( 1308 (c for c in value_error.context_lines if "expanded from" in c), None 1309 ) 1310 assert expansion_ctx is not None 1311 assert "20" in expansion_ctx and "10" in expansion_ctx, \ 1312 f"Expected call location (20, 10) in context_line: {expansion_ctx}" 1313 1314 1315class TestErrorMessageQuality: 1316 """Error messages are informative and include appropriate context.""" 1317 1318 def test_undefined_macro_has_macro_category_and_suggestions(self): 1319 """Undefined macro error includes category, name, and suggestions.""" 1320 source = """ 1321 @system pe=1, sm=1 1322 1323 #simple |> { 1324 &x <| pass 1325 } 1326 1327 #simpler 1328 """ 1329 graph = parse_lower_expand(source) 1330 1331 assert len(graph.errors) > 0, "Expected error for undefined macro" 1332 error = graph.errors[0] 1333 assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" 1334 assert "simpler" in error.message, f"Expected macro name in message: {error.message}" 1335 assert len(error.suggestions) > 0, f"Expected suggestions, got {error.suggestions}" 1336 assert any("simple" in s for s in error.suggestions), \ 1337 f"Expected 'simple' in suggestions: {error.suggestions}" 1338 1339 def test_arity_mismatch_includes_counts(self): 1340 """Arity mismatch error message includes expected and actual counts.""" 1341 source = """ 1342 @system pe=1, sm=1 1343 1344 #needs_three a, b, c |> { 1345 &x <| pass 1346 } 1347 1348 #needs_three &x, &y 1349 """ 1350 graph = parse_lower_expand(source) 1351 1352 assert len(graph.errors) > 0 1353 error = graph.errors[0] 1354 assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" 1355 assert "3" in error.message, f"Expected expected count in message: {error.message}" 1356 assert "2" in error.message, f"Expected actual count in message: {error.message}" 1357 1358 def test_depth_exceeded_names_recursive_macro(self): 1359 """Recursion depth limit error names the recursive macro.""" 1360 source = """ 1361 @system pe=1, sm=1 1362 1363 #recursive |> { 1364 #recursive 1365 } 1366 1367 #recursive 1368 """ 1369 graph = parse_lower_expand(source) 1370 1371 assert len(graph.errors) > 0 1372 error = graph.errors[0] 1373 assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" 1374 assert "recursive" in error.message.lower(), \ 1375 f"Expected 'recursive' in message: {error.message}" 1376 assert "depth" in error.message.lower() or "recursion" in error.message.lower(), \ 1377 f"Expected depth/recursion mention: {error.message}" 1378 1379 def test_nested_macro_expansion_error_has_context_lines(self): 1380 """Error in nested macro expansion includes context lines showing call chain.""" 1381 # Create inner macro with arithmetic on parameter 1382 inner_node = IRNode( 1383 name="&inner", 1384 opcode=ArithOp.ADD, 1385 const=ConstExpr( 1386 expression="val + 1", 1387 params=("val",), 1388 loc=SourceLoc(10, 5), 1389 ), 1390 ) 1391 inner_macro = MacroDef( 1392 name="inner_arith", 1393 params=(MacroParam(name="val"),), 1394 body=IRGraph( 1395 nodes={"&inner": inner_node}, 1396 edges=[], 1397 regions=[], 1398 data_defs=[], 1399 macro_defs=[], 1400 macro_calls=[], 1401 ), 1402 loc=SourceLoc(5, 0), 1403 ) 1404 1405 # Create outer macro that calls inner with non-numeric value 1406 inner_call = IRMacroCall( 1407 name="inner_arith", 1408 positional_args=("&label",), # Non-numeric value - will cause VALUE error 1409 loc=SourceLoc(15, 3), 1410 ) 1411 1412 outer_macro = MacroDef( 1413 name="outer_wrapper", 1414 params=(), 1415 body=IRGraph( 1416 nodes={}, 1417 edges=[], 1418 regions=[], 1419 data_defs=[], 1420 macro_defs=[], 1421 macro_calls=[inner_call], 1422 ), 1423 loc=SourceLoc(1, 0), 1424 ) 1425 1426 # Invoke outer macro 1427 outer_call = IRMacroCall( 1428 name="outer_wrapper", 1429 positional_args=(), 1430 loc=SourceLoc(20, 0), 1431 ) 1432 1433 graph = IRGraph( 1434 nodes={}, 1435 edges=[], 1436 regions=[], 1437 data_defs=[], 1438 macro_defs=[inner_macro, outer_macro], 1439 macro_calls=[outer_call], 1440 ) 1441 1442 expanded = expand(graph) 1443 1444 # Verify VALUE error with context lines showing the call chain 1445 assert expanded.errors, f"Expected VALUE error, got {[e.message for e in expanded.errors]}" 1446 value_error = next( 1447 (e for e in expanded.errors if e.category == ErrorCategory.VALUE), None 1448 ) 1449 assert value_error is not None, f"Expected VALUE error, got {[e.category for e in expanded.errors]}" 1450 1451 # Verify context_lines include both inner and outer macro references 1452 assert len(value_error.context_lines) >= 1, \ 1453 f"Expected at least 1 context line for expansion, got {value_error.context_lines}" 1454 context_str = " ".join(value_error.context_lines) 1455 assert "inner_arith" in context_str, \ 1456 f"Expected 'inner_arith' in context: {value_error.context_lines}" 1457 assert "outer_wrapper" in context_str, \ 1458 f"Expected 'outer_wrapper' in context: {value_error.context_lines}"