OR-1 dataflow CPU sketch
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}"