"""Tests for macro expansion pass (Phase 2). Tests verify: - dfasm-macros.AC2.1: Scope-qualified node names (#name_N.&label) - dfasm-macros.AC2.2: Literal parameter substitution into const fields - dfasm-macros.AC2.3: Ref parameter substitution into edge endpoints - dfasm-macros.AC2.4: Nested macro expansion (recursive) - dfasm-macros.AC2.5: Macros inside functions (double-scoped) - dfasm-macros.AC2.6: Undefined macro → NAME error with suggestions - dfasm-macros.AC2.7: Wrong arity → ARITY error - dfasm-macros.AC2.8: Recursion depth limit → error - dfasm-macros.AC7.3: Unresolvable scope in expanded body → NAME error from resolve """ import re from lark import Lark from pathlib import Path from asm.expand import expand from asm.lower import lower from asm.resolve import resolve from asm.errors import ErrorCategory from asm.ir import ( IRGraph, IRNode, IREdge, MacroDef, MacroParam, ParamRef, ConstExpr, SourceLoc, IRMacroCall ) from cm_inst import ArithOp, Port def _get_parser(): """Get the dfasm parser.""" grammar_path = Path(__file__).parent.parent / "dfasm.lark" return Lark( grammar_path.read_text(), parser="earley", propagate_positions=True, ) def parse_and_lower(source: str) -> IRGraph: """Parse source and lower to IRGraph (before expansion).""" parser = _get_parser() tree = parser.parse(source) return lower(tree) def parse_lower_expand(source: str) -> IRGraph: """Parse, lower, and expand.""" graph = parse_and_lower(source) return expand(graph) class TestAC21_ScopeQualification: """AC2.1: Nodes are scope-qualified with #macroname_N prefix.""" def test_simple_macro_invocation_creates_qualified_node(self): """Invoking #wrap creates node with qualified name.""" source = """ @system pe=1, sm=1 #wrap |> { &inner <| pass } #wrap """ graph = parse_lower_expand(source) # Look for qualified node name qualified_names = [n for n in graph.nodes.keys() if "#wrap_0.&inner" in n] assert len(qualified_names) == 1, f"Expected #wrap_0.&inner in {list(graph.nodes.keys())}" def test_multiple_invocations_get_unique_scopes(self): """Multiple invocations get unique counter values.""" source = """ @system pe=1, sm=1 #simple |> { &node <| pass } #simple #simple """ graph = parse_lower_expand(source) # Should have both _0 and _1 scopes nodes = list(graph.nodes.keys()) has_0 = any("#simple_0" in n for n in nodes) has_1 = any("#simple_1" in n for n in nodes) assert has_0 and has_1, f"Expected unique scopes in {nodes}" class TestAC22_LiteralSubstitution: """AC2.2: Literal parameters substitute into const fields. Uses ${param} syntax in macro body to reference formal parameters in const positions. The grammar's param_ref rule parses ${IDENT} and the lowerer creates ParamRef objects, which the expand pass substitutes with actual argument values. """ def test_const_substitution_inline(self): """${param} in inline_const position substitutes the argument value.""" source = """ @system pe=1, sm=1 #with_const val |> { &node <| const ${val} } #with_const 42 """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" expanded = [n for n in graph.nodes.values() if "#with_const_0" in n.name] assert len(expanded) == 1 assert expanded[0].const == 42 def test_const_substitution_comma_separated(self): """${param} in comma-separated argument position substitutes into const.""" source = """ @system pe=1, sm=1 #with_const val |> { &node <| const, ${val} } #with_const 7 """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" expanded = [n for n in graph.nodes.values() if "#with_const_0" in n.name] assert len(expanded) == 1 assert expanded[0].const == 7 def test_const_substitution_with_hex(self): """${param} substitutes hex literal arguments correctly.""" source = """ @system pe=1, sm=1 #set_val v |> { &node <| const ${v} } #set_val 0xFF """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" expanded = [n for n in graph.nodes.values() if "#set_val_0" in n.name] assert len(expanded) == 1 assert expanded[0].const == 255 class TestAC23_RefSubstitution: """AC2.3: Ref parameters substitute into edge endpoints. Uses ${param} syntax in macro body edge endpoints to reference formal parameters. The lowerer creates ParamRef objects from ${IDENT} in qualified_ref positions, and the expand pass substitutes them with actual argument values (label/node references). """ def test_edge_dest_substitution(self): """${param} in edge dest position wires to the actual argument ref.""" source = """ @system pe=1, sm=1 &external <| pass #connect_to target |> { &src <| const, 1 &src |> ${target}:L } #connect_to &external """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" src_node = [n for n in graph.nodes.keys() if "#connect_to_0" in n and "&src" in n][0] found = any( e.source == src_node and e.dest == "&external" for e in graph.edges ) assert found, f"Expected edge from {src_node} to &external, edges: {[(e.source, e.dest) for e in graph.edges]}" def test_edge_source_substitution(self): """${param} in edge source position wires from the actual argument ref.""" source = """ @system pe=1, sm=1 &provider <| const, 5 #read_from src |> { &sink <| pass ${src} |> &sink:L } #read_from &provider """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" sink_node = [n for n in graph.nodes.keys() if "#read_from_0" in n and "&sink" in n][0] found = any( e.source == "&provider" and e.dest == sink_node for e in graph.edges ) assert found, f"Expected edge from &provider to {sink_node}, edges: {[(e.source, e.dest) for e in graph.edges]}" def test_both_endpoints_substituted(self): """${param} in both source and dest positions substitutes correctly.""" source = """ @system pe=1, sm=1 &a <| const, 1 &b <| pass #wire_between src, dest |> { ${src} |> ${dest}:L } #wire_between &a, &b """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}" found = any( e.source == "&a" and e.dest == "&b" for e in graph.edges ) assert found, f"Expected edge &a -> &b, edges: {[(e.source, e.dest) for e in graph.edges]}" class TestAC24_NestedExpansion: """AC2.4: Nested macro calls expand recursively.""" def test_nested_macro_expansion(self): """Invoking outer which calls inner creates double-scoped nodes.""" source = """ @system pe=1, sm=1 #inner |> { &x <| pass } #outer |> { #inner } #outer """ graph = parse_lower_expand(source) # Should have both #outer and #inner scopes node_names = list(graph.nodes.keys()) # Look for nodes qualified with #outer_N and #inner_M scopes outer_nodes = [n for n in node_names if "#outer_" in n] inner_nodes = [n for n in node_names if "#inner_" in n] assert len(outer_nodes) > 0, f"Expected #outer_ scoped nodes in {node_names}" assert len(inner_nodes) > 0, f"Expected #inner_ scoped nodes in {node_names}" class TestAC25_FunctionScoping: """AC2.5: Macros inside functions get double-scoped.""" def test_macro_in_function_creates_double_scope(self): """Macro inside function gets $func.#macro_N.&label scope.""" source = """ @system pe=1, sm=1 #inject |> { &gate <| pass } $func |> { #inject } """ graph = parse_lower_expand(source) # Look for node with pattern $func.#inject_N.&gate # This will be in a function region body, not at top level node_names = list(graph.nodes.keys()) all_node_names = node_names.copy() # Check region bodies too for region in graph.regions: all_node_names.extend(region.body.nodes.keys()) # Use regex to find pattern pattern = r'\$func\.#inject_\d+\.&gate' found = any(re.search(pattern, name) for name in all_node_names) assert found, f"Expected $func.#inject_N.&gate pattern in {all_node_names}" class TestAC26_UndefinedMacro: """AC2.6: Undefined macro invocation → MACRO error with suggestions.""" def test_undefined_macro_produces_macro_error(self): """Invoking undefined macro produces MACRO error.""" source = """ @system pe=1, sm=1 #undefined_macro &a """ graph = parse_lower_expand(source) # Should have an error assert len(graph.errors) > 0, "Expected error for undefined macro" error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO error, got {error.category}" assert "undefined" in error.message.lower(), f"Expected 'undefined' in message: {error.message}" def test_undefined_macro_has_suggestions(self): """Undefined macro with similar name gets suggestions.""" source = """ @system pe=1, sm=1 #simple |> { &x <| pass } #simpl """ graph = parse_lower_expand(source) # Should have error with suggestions assert len(graph.errors) > 0 error = graph.errors[0] assert len(error.suggestions) > 0, f"Expected suggestions for typo, got {error.suggestions}" class TestAC27_MacroArityError: """AC2.7: Wrong arity on macro invocation → MACRO error.""" def test_too_few_arguments(self): """Providing too few arguments produces MACRO error.""" source = """ @system pe=1, sm=1 #needs_two a, b |> { &x <| pass } #needs_two &a """ graph = parse_lower_expand(source) # Should have MACRO error assert len(graph.errors) > 0, "Expected error" error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" assert "2" in error.message and "1" in error.message, f"Expected counts in message: {error.message}" def test_too_many_arguments(self): """Providing too many arguments produces MACRO error.""" source = """ @system pe=1, sm=1 #needs_one a |> { &x <| pass } #needs_one &a, &b, &c """ graph = parse_lower_expand(source) assert len(graph.errors) > 0 error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" class TestAC28_RecursionDepth: """AC2.8: Macro recursion exceeding depth limit → error.""" def test_infinite_recursion_caught(self): """Infinite recursion is caught at depth limit.""" source = """ @system pe=1, sm=1 #recursive |> { #recursive } #recursive """ graph = parse_lower_expand(source) # Should have error about depth assert len(graph.errors) > 0, "Expected error for infinite recursion" error = graph.errors[0] # The error message should mention depth or recursion msg = error.message.lower() assert ("depth" in msg or "recursion" in msg), f"Expected depth/recursion mention in: {error.message}" class TestAC73_UnresolvableScope: """AC7.3: Unresolvable scope in expanded body → NAME error from resolve.""" def test_macro_with_unresolvable_scope_ref(self): """Macro body with unresolvable scope reference surfaces error at resolve time.""" source = """ @system pe=1, sm=1 #bad_ref |> { &node <| pass &node |> $nonexistent.&target:L } #bad_ref """ # Expand alone may not error (if scope checking is in resolve) graph = parse_lower_expand(source) # Resolve to check scope validity graph = resolve(graph) # Should have NAME error from resolve about nonexistent scope has_error = any( e.category == ErrorCategory.NAME for e in graph.errors ) assert has_error, f"Expected NAME error from resolve, got {[e.category for e in graph.errors]}" class TestMacroDefAndCallCleanup: """Macro definitions and calls are removed from output.""" def test_no_macro_defs_in_output(self): """Output graph has no macro_defs.""" source = """ @system pe=1, sm=1 #simple |> { &x <| pass } #simple """ graph = parse_lower_expand(source) assert len(graph.macro_defs) == 0, "Expected macro_defs to be empty" def test_no_macro_calls_in_output(self): """Output graph has no macro_calls.""" source = """ @system pe=1, sm=1 #simple |> { &x <| pass } #simple """ graph = parse_lower_expand(source) assert len(graph.macro_calls) == 0, "Expected macro_calls to be empty" class TestMacroWithNoParams: """Macro with no parameters expands correctly.""" def test_parameterless_macro(self): """Macro without params can be invoked without args.""" source = """ @system pe=1, sm=1 #identity |> { &x <| pass } #identity """ graph = parse_lower_expand(source) # Should expand without errors assert len(graph.errors) == 0, f"Expected no errors, got {graph.errors}" # Should have qualified node names = list(graph.nodes.keys()) assert any("#identity_0" in n for n in names), f"Expected #identity_0 in {names}" class TestMultipleMacroDefinitions: """Multiple macros can be defined and invoked in same program.""" def test_multiple_macros(self): """Define and invoke multiple different macros.""" source = """ @system pe=1, sm=1 #macro_a |> { &a <| pass } #macro_b |> { &b <| pass } #macro_a #macro_b """ graph = parse_lower_expand(source) # Should have both expanded nodes names = list(graph.nodes.keys()) has_a = any("#macro_a_" in n for n in names) has_b = any("#macro_b_" in n for n in names) assert has_a and has_b, f"Expected both macros in {names}" class TestExpansionCounterIncrement: """Expansion counter increments per invocation.""" def test_counter_increments_across_invocations(self): """Same macro invoked twice gets _0 and _1.""" source = """ @system pe=1, sm=1 #dup |> { &node <| pass } #dup #dup """ graph = parse_lower_expand(source) names = list(graph.nodes.keys()) has_0 = any("#dup_0" in n for n in names) has_1 = any("#dup_1" in n for n in names) assert has_0 and has_1, f"Expected _0 and _1 in {names}" class TestMacroErrorPropagation: """Errors in macro body template surface at expansion time.""" def test_macro_body_with_unknown_opcode_surfaces_error(self): """Errors from macro body lowering surface in expanded graph. Note: In Phase 2, grammar validation prevents invalid opcodes from appearing in macro bodies. This test is deferred to Phase 3 when validation of parameter-referenced opcodes becomes possible. """ # Grammar ensures only valid opcodes can appear in macro bodies # This test will be more relevant in Phase 3 with token pasting source = """ @system pe=1, sm=1 #valid_macro |> { &node <| pass } #valid_macro """ graph = parse_lower_expand(source) # Valid macros should have no errors (syntax validation is in lower pass) assert len(graph.errors) == 0 class TestAC31_TokenPasting: """AC3.1: ParamRef with prefix/suffix concatenates into label names. Tests construct IR directly (not from parsing) to test expand in isolation. """ def test_token_paste_with_prefix_and_suffix(self): """Token pasting with both prefix and suffix creates concatenated name.""" # Create macro body with ParamRef containing prefix/suffix param_ref = ParamRef(param="func", prefix="&__", suffix="_fan") body_node = IRNode( name=param_ref, # This will be a ParamRef in the node name opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ) macro_body = IRGraph( nodes={"node_placeholder": body_node}, edges=[], macro_defs=[], macro_calls=[], ) # Create macro definition macro_def = MacroDef( name="make_fan", params=(MacroParam(name="func"),), body=macro_body, loc=SourceLoc(0, 0), ) # Create a graph with the macro and a call to it # The macro call with argument "fib" macro_call = IRMacroCall( name="make_fan", positional_args=("fib",), named_args=(), loc=SourceLoc(0, 0), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) # Expand the graph expanded = expand(graph) # After expansion, the node should have name: #macro_N.&__fib_fan node_names = list(expanded.nodes.keys()) # Look for the pasted name pattern found = any("&__fib_fan" in name for name in node_names) assert found, f"Expected node with &__fib_fan in name, got {node_names}" def test_token_paste_with_prefix_only(self): """Token pasting with prefix only concatenates correctly.""" param_ref = ParamRef(param="x", prefix="&pre_", suffix="") body_node = IRNode( name=param_ref, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ) macro_body = IRGraph( nodes={"node_placeholder": body_node}, edges=[], macro_defs=[], macro_calls=[], ) macro_def = MacroDef( name="prefix_test", params=(MacroParam(name="x"),), body=macro_body, loc=SourceLoc(0, 0), ) macro_call = IRMacroCall( name="prefix_test", positional_args=("val",), named_args=(), loc=SourceLoc(0, 0), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) # Should have node with &pre_val node_names = list(expanded.nodes.keys()) found = any("&pre_val" in name for name in node_names) assert found, f"Expected &pre_val in {node_names}" def test_token_paste_with_suffix_only(self): """Token pasting with suffix only concatenates correctly.""" param_ref = ParamRef(param="x", prefix="", suffix="_post") body_node = IRNode( name=param_ref, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ) macro_body = IRGraph( nodes={"node_placeholder": body_node}, edges=[], macro_defs=[], macro_calls=[], ) macro_def = MacroDef( name="suffix_test", params=(MacroParam(name="x"),), body=macro_body, loc=SourceLoc(0, 0), ) macro_call = IRMacroCall( name="suffix_test", positional_args=("val",), named_args=(), loc=SourceLoc(0, 0), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) # Should have node with val_post node_names = list(expanded.nodes.keys()) found = any("val_post" in name for name in node_names) assert found, f"Expected val_post in {node_names}" def test_token_paste_in_edge_source(self): """Token pasting in edge source reference resolves correctly.""" # Create two nodes: one with ParamRef source name, one regular dest param_ref_src = ParamRef(param="src", prefix="&", suffix="") regular_dest = "&dest" body_nodes = { "src_placeholder": IRNode( name=param_ref_src, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ), "&dest": IRNode( name=regular_dest, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ), } # Edge connects the pasted source to the destination body_edges = [ IREdge( source=param_ref_src, dest=regular_dest, port=Port.L, loc=SourceLoc(0, 0), ) ] macro_body = IRGraph( nodes=body_nodes, edges=body_edges, macro_defs=[], macro_calls=[], ) macro_def = MacroDef( name="edge_src_test", params=(MacroParam(name="src"),), body=macro_body, loc=SourceLoc(0, 0), ) macro_call = IRMacroCall( name="edge_src_test", positional_args=("input",), named_args=(), loc=SourceLoc(0, 0), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) # Check that edges have the resolved pasted names edges = expanded.edges # Find the edge, both source and dest should be qualified with #edge_src_test_0.&name found = False for edge in edges: if "&input" in edge.source and "&dest" in edge.dest: found = True break assert found, f"Expected edge with pasted &input source, got {[(e.source, e.dest) for e in edges]}" def test_token_paste_in_edge_dest(self): """Token pasting in edge dest reference resolves correctly.""" param_ref_dest = ParamRef(param="dest", prefix="&", suffix="") regular_src = "&src" body_nodes = { "&src": IRNode( name=regular_src, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ), "dest_placeholder": IRNode( name=param_ref_dest, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ), } # Edge connects source to the pasted destination body_edges = [ IREdge( source=regular_src, dest=param_ref_dest, port=Port.L, loc=SourceLoc(0, 0), ) ] macro_body = IRGraph( nodes=body_nodes, edges=body_edges, macro_defs=[], macro_calls=[], ) macro_def = MacroDef( name="edge_dest_test", params=(MacroParam(name="dest"),), body=macro_body, loc=SourceLoc(0, 0), ) macro_call = IRMacroCall( name="edge_dest_test", positional_args=("output",), named_args=(), loc=SourceLoc(0, 0), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) # Check that edges have the resolved pasted names edges = expanded.edges found = False for edge in edges: if "&src" in edge.source and "&output" in edge.dest: found = True break assert found, f"Expected edge with pasted &output dest, got {[(e.source, e.dest) for e in edges]}" class TestAC32_ConstantExpressionEvaluation: """AC3.2: Constant expressions evaluate during macro expansion.""" def test_simple_param_substitution_in_const(self): """ParamRef(param="val") + arg=42 -> const=42.""" # Create macro with ParamRef in const field node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ParamRef(param="val"), ) macro_def = MacroDef( name="simple_const", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), ) macro_call = IRMacroCall( name="simple_const", positional_args=(42,), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) # Find the expanded node nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 42 for _, const in nodes_with_const), \ f"Expected const=42, got {nodes_with_const}" def test_const_expr_addition_with_one_param(self): """ConstExpr('val + 1') with val=5 evaluates to 6.""" # Create macro with ConstExpr in const field node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val + 1", params=("val",), ), ) macro_def = MacroDef( name="add_one", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), ) macro_call = IRMacroCall( name="add_one", positional_args=(5,), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) # Verify no errors assert not expanded.errors, f"Expected no errors, got {expanded.errors}" # Find the expanded node and verify const nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 6 for _, const in nodes_with_const), \ f"Expected const=6, got {nodes_with_const}" def test_const_expr_subtraction(self): """ConstExpr('val - 1') with val=10 evaluates to 9.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val - 1", params=("val",), ), ) macro_def = MacroDef( name="sub_one", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), ) macro_call = IRMacroCall( name="sub_one", positional_args=(10,), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) assert not expanded.errors, f"Expected no errors, got {expanded.errors}" nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 9 for _, const in nodes_with_const), \ f"Expected const=9, got {nodes_with_const}" def test_const_expr_multiplication(self): """ConstExpr('val * 2') with val=4 evaluates to 8.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val * 2", params=("val",), ), ) macro_def = MacroDef( name="double", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), ) macro_call = IRMacroCall( name="double", positional_args=(4,), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) assert not expanded.errors, f"Expected no errors, got {expanded.errors}" nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 8 for _, const in nodes_with_const), \ f"Expected const=8, got {nodes_with_const}" def test_const_expr_multiple_params(self): """ConstExpr('a + b') with a=3, b=7 evaluates to 10.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="a + b", params=("a", "b"), ), ) macro_def = MacroDef( name="add_two", params=(MacroParam(name="a"), MacroParam(name="b")), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), ) macro_call = IRMacroCall( name="add_two", positional_args=(3, 7), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) assert not expanded.errors, f"Expected no errors, got {expanded.errors}" nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 10 for _, const in nodes_with_const), \ f"Expected const=10, got {nodes_with_const}" def test_const_expr_with_literal(self): """ConstExpr('5 + val') with val=2 evaluates to 7.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="5 + val", params=("val",), ), ) macro_def = MacroDef( name="literal_plus", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), ) macro_call = IRMacroCall( name="literal_plus", positional_args=(2,), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) assert not expanded.errors, f"Expected no errors, got {expanded.errors}" nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 7 for _, const in nodes_with_const), \ f"Expected const=7, got {nodes_with_const}" class TestAC33_ConstExprNonNumericValues: """AC3.3: Non-numeric values in arithmetic context produce VALUE error.""" def test_non_numeric_param_in_arithmetic(self): """ParamRef in arithmetic context with non-int arg -> VALUE error.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val + 1", params=("val",), ), ) macro_def = MacroDef( name="arith_with_ref", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), ) # Pass a reference name (&label) instead of an integer macro_call = IRMacroCall( name="arith_with_ref", positional_args=("&label",), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) # Verify we have an error assert expanded.errors, "Expected VALUE error for non-numeric in arithmetic" assert any( e.category == ErrorCategory.VALUE for e in expanded.errors ), f"Expected VALUE error, got {[e.category for e in expanded.errors]}" def test_undefined_param_in_arithmetic(self): """Undefined parameter in arithmetic expression -> VALUE error.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="undefined_param + 1", params=("undefined_param",), ), ) macro_def = MacroDef( name="undefined", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), ) macro_call = IRMacroCall( name="undefined", positional_args=(5,), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) # Verify we have an error assert expanded.errors, "Expected VALUE error for undefined parameter" assert any( e.category == ErrorCategory.VALUE for e in expanded.errors ), f"Expected VALUE error, got {[e.category for e in expanded.errors]}" class TestSourceLocationThreading: """Verify that errors during macro expansion include source location context.""" def test_expansion_error_includes_macro_location(self): """Non-numeric param in arithmetic triggers VALUE error with expansion context.""" # Create a macro that performs arithmetic on a parameter node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val + 1", params=("val",), loc=SourceLoc(10, 5), ), ) macro_def = MacroDef( name="arith_test", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), loc=SourceLoc(5, 0), ) # Create a macro call at a specific location that passes a non-numeric value macro_call = IRMacroCall( name="arith_test", positional_args=("&label",), # Non-numeric value loc=SourceLoc(20, 10), # Call location ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], ) expanded = expand(graph) # Verify we have a VALUE error assert expanded.errors, "Expected VALUE error" value_error = next( (e for e in expanded.errors if e.category == ErrorCategory.VALUE), None ) assert value_error is not None, "Expected VALUE error in errors list" # Verify the error has context_lines with expansion context assert value_error.context_lines, "Expected context_lines with expansion context" assert any( "expanded from" in ctx and "arith_test" in ctx for ctx in value_error.context_lines ), f"Expected 'expanded from #arith_test' in context_lines, got {value_error.context_lines}" # Verify the context line contains the call location expansion_ctx = next( (c for c in value_error.context_lines if "expanded from" in c), None ) assert expansion_ctx is not None assert "20" in expansion_ctx and "10" in expansion_ctx, \ f"Expected call location (20, 10) in context_line: {expansion_ctx}" class TestErrorMessageQuality: """Error messages are informative and include appropriate context.""" def test_undefined_macro_has_macro_category_and_suggestions(self): """Undefined macro error includes category, name, and suggestions.""" source = """ @system pe=1, sm=1 #simple |> { &x <| pass } #simpler """ graph = parse_lower_expand(source) assert len(graph.errors) > 0, "Expected error for undefined macro" error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" assert "simpler" in error.message, f"Expected macro name in message: {error.message}" assert len(error.suggestions) > 0, f"Expected suggestions, got {error.suggestions}" assert any("simple" in s for s in error.suggestions), \ f"Expected 'simple' in suggestions: {error.suggestions}" def test_arity_mismatch_includes_counts(self): """Arity mismatch error message includes expected and actual counts.""" source = """ @system pe=1, sm=1 #needs_three a, b, c |> { &x <| pass } #needs_three &x, &y """ graph = parse_lower_expand(source) assert len(graph.errors) > 0 error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" assert "3" in error.message, f"Expected expected count in message: {error.message}" assert "2" in error.message, f"Expected actual count in message: {error.message}" def test_depth_exceeded_names_recursive_macro(self): """Recursion depth limit error names the recursive macro.""" source = """ @system pe=1, sm=1 #recursive |> { #recursive } #recursive """ graph = parse_lower_expand(source) assert len(graph.errors) > 0 error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" assert "recursive" in error.message.lower(), \ f"Expected 'recursive' in message: {error.message}" assert "depth" in error.message.lower() or "recursion" in error.message.lower(), \ f"Expected depth/recursion mention: {error.message}" def test_nested_macro_expansion_error_has_context_lines(self): """Error in nested macro expansion includes context lines showing call chain.""" # Create inner macro with arithmetic on parameter inner_node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val + 1", params=("val",), loc=SourceLoc(10, 5), ), ) inner_macro = MacroDef( name="inner_arith", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": inner_node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), loc=SourceLoc(5, 0), ) # Create outer macro that calls inner with non-numeric value inner_call = IRMacroCall( name="inner_arith", positional_args=("&label",), # Non-numeric value - will cause VALUE error loc=SourceLoc(15, 3), ) outer_macro = MacroDef( name="outer_wrapper", params=(), body=IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[inner_call], ), loc=SourceLoc(1, 0), ) # Invoke outer macro outer_call = IRMacroCall( name="outer_wrapper", positional_args=(), loc=SourceLoc(20, 0), ) graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[inner_macro, outer_macro], macro_calls=[outer_call], ) expanded = expand(graph) # Verify VALUE error with context lines showing the call chain assert expanded.errors, f"Expected VALUE error, got {[e.message for e in expanded.errors]}" value_error = next( (e for e in expanded.errors if e.category == ErrorCategory.VALUE), None ) assert value_error is not None, f"Expected VALUE error, got {[e.category for e in expanded.errors]}" # Verify context_lines include both inner and outer macro references assert len(value_error.context_lines) >= 1, \ f"Expected at least 1 context line for expansion, got {value_error.context_lines}" context_str = " ".join(value_error.context_lines) assert "inner_arith" in context_str, \ f"Expected 'inner_arith' in context: {value_error.context_lines}" assert "outer_wrapper" in context_str, \ f"Expected 'outer_wrapper' in context: {value_error.context_lines}"