"""Tests for macro definition parsing and lowering. Tests verify: - Macro definition parsing (AC1.1) → MacroDef with name, params, body - Macro body with various statement types (AC1.2) → template IRGraph - ParamRef in macro body (AC1.3) → const fields and edge endpoints - Duplicate parameter names (AC1.4) → error with ErrorCategory.NAME - Reserved names (AC1.5) → error with ErrorCategory.NAME - Macro call statements → IRMacroCall in graph.macro_calls - Dot-notation scope resolution (AC7.1, AC7.2) → qualified names - Macro references in edges → scoped_ref support """ from tests.pipeline import parse_and_lower from asm.ir import RegionKind, SourceLoc, MacroParam, MacroDef, IRMacroCall from asm.errors import ErrorCategory class TestMacroDefinition: """Tests for macro definition parsing (AC1.1, AC1.2).""" def test_macro_def_basic_parses(self, parser): """Parse simple macro definition.""" graph = parse_and_lower(parser, """\ #simple |> { &a <| pass } """) assert len(graph.macro_defs) == 1 macro = graph.macro_defs[0] assert macro.name == "simple" assert macro.params == () assert "&a" in macro.body.nodes def test_macro_def_with_params(self, parser): """Parse macro with parameters.""" graph = parse_and_lower(parser, """\ #loop_counted init, limit |> { &counter <| add } """) assert len(graph.macro_defs) == 1 macro = graph.macro_defs[0] assert macro.name == "loop_counted" assert len(macro.params) == 2 assert macro.params[0].name == "init" assert macro.params[1].name == "limit" assert "&counter" in macro.body.nodes def test_macro_def_body_with_edges(self, parser): """Parse macro body with edges.""" graph = parse_and_lower(parser, """\ #routing |> { &a <| pass &b <| pass &a |> &b:L } """) macro = graph.macro_defs[0] assert len(macro.body.nodes) == 2 # The edge is parsed and stored assert len(macro.body.edges) > 0 def test_macro_def_body_with_strong_edge(self, parser): """Parse macro with inline strong edge.""" graph = parse_and_lower(parser, """\ #inline_math |> { add 1, 2 |> &result:L } """) macro = graph.macro_defs[0] # Anonymous node created by strong_edge assert len(macro.body.nodes) >= 1 # &result is the destination, not a node in this context assert len(macro.body.edges) > 0 def test_macro_def_no_params_with_empty_body(self, parser): """Parse macro with no params and empty body.""" graph = parse_and_lower(parser, """\ #empty |> { } """) macro = graph.macro_defs[0] assert macro.name == "empty" assert macro.params == () assert len(macro.body.nodes) == 0 class TestMacroCallStatement: """Tests for macro invocation statements.""" def test_macro_call_stmt_no_args(self, parser): """Parse macro call with no arguments.""" graph = parse_and_lower(parser, """\ #simple """) assert len(graph.macro_calls) == 1 call = graph.macro_calls[0] assert call.name == "simple" assert call.positional_args == () assert call.named_args == () def test_macro_call_stmt_with_positional_args(self, parser): """Parse macro call with positional arguments.""" graph = parse_and_lower(parser, """\ #loop_counted &src, &dest """) assert len(graph.macro_calls) == 1 call = graph.macro_calls[0] assert call.name == "loop_counted" assert len(call.positional_args) == 2 # Args are dicts with 'name' field assert call.positional_args[0]["name"] == "&src" assert call.positional_args[1]["name"] == "&dest" def test_macro_call_stmt_with_value_arg(self, parser): """Parse macro call with literal value argument.""" graph = parse_and_lower(parser, """\ #init 42 """) call = graph.macro_calls[0] assert call.name == "init" assert len(call.positional_args) == 1 def test_macro_call_stmt_with_named_arg(self, parser): """Parse macro call with named argument.""" graph = parse_and_lower(parser, """\ #inject gate=&my_gate """) call = graph.macro_calls[0] assert call.name == "inject" assert len(call.named_args) == 1 assert call.named_args[0][0] == "gate" class TestMacroParameterValidation: """Tests for macro parameter validation (AC1.4, AC1.5).""" def test_duplicate_param_names_error(self, parser): """Detect duplicate parameter names.""" graph = parse_and_lower(parser, """\ #bad dup, dup |> { &a <| pass } """) # Check for error assert len(graph.errors) > 0 error = graph.errors[0] assert error.category == ErrorCategory.NAME assert "Duplicate parameter" in error.message assert "dup" in error.message def test_reserved_macro_name_error(self, parser): """Detect reserved macro names.""" graph = parse_and_lower(parser, """\ #ret_value |> { &a <| pass } """) assert len(graph.errors) > 0 error = graph.errors[0] assert error.category == ErrorCategory.NAME assert "reserved prefix" in error.message.lower() assert "ret" in error.message class TestScopedReferences: """Tests for dot-notation scope resolution (AC7.1, AC7.2).""" def test_function_scoped_ref_in_edge_source(self, parser): """Parse function scoped reference as edge source.""" graph = parse_and_lower(parser, """\ $func |> { &label <| pass } &dest <| pass $func.&label |> &dest:L """) # The edge should reference the scoped name edge = graph.edges[0] assert edge.source == "$func.&label" def test_macro_scoped_ref_in_edge_source(self, parser): """Parse macro scoped reference as edge source. Lower creates the qualified name; expand resolves it to the actual expanded node name (e.g., #macro_0.&label). """ graph = parse_and_lower(parser, """\ &dest <| pass #macro.&label |> &dest:L """) assert len(graph.edges) > 0 edge = graph.edges[0] assert edge.source == "#macro.&label" def test_macro_ref_in_edge_source(self, parser): """Parse macro reference as edge source.""" graph = parse_and_lower(parser, """\ &dest <| pass #macro |> &dest:L """) # Should parse the macro_ref in the edge source assert len(graph.edges) > 0 edge = graph.edges[0] assert edge.source == "#macro" class TestMacroRefGrammar: """Tests for macro_ref and scoped_ref grammar productions.""" def test_macro_ref_in_data_def_target(self, parser): """Parse macro reference as data definition target.""" graph = parse_and_lower(parser, """\ #macrodata = 42 """) # The data_def should reference the macro assert len(graph.data_defs) > 0 data_def = graph.data_defs[0] assert data_def.name == "#macrodata" def test_scoped_ref_with_label_ref(self, parser): """Parse scoped_ref using label_ref as inner.""" graph = parse_and_lower(parser, """\ $func |> { &inner <| pass } &dest <| pass $func.&inner |> &dest:L """) # Scoped ref to label should work edge = graph.edges[0] assert edge.source == "$func.&inner" def test_node_ref_inside_function_stays_global(self, parser): """@name refs inside function bodies are NOT scope-qualified. Only &label refs get qualified with function scope. @name refs are global (with @ret/@ret_name as special exceptions handled by the expand pass). """ graph = parse_and_lower(parser, """\ $func |> { @inner <| pass &local <| pass } """) func_region = next( r for r in graph.regions if r.kind == RegionKind.FUNCTION ) node_names = set(func_region.body.nodes.keys()) assert "@inner" in node_names, f"@inner should stay unqualified, got {node_names}" assert "$func.&local" in node_names, f"&local should be qualified, got {node_names}" class TestMacroInContext: """Tests for macro definitions and calls in full program context.""" def test_macro_def_followed_by_call(self, parser): """Parse macro definition followed by invocation.""" graph = parse_and_lower(parser, """\ #loop init, limit |> { &counter <| add } #loop &start, &end """) assert len(graph.macro_defs) == 1 assert len(graph.macro_calls) == 1 macro = graph.macro_defs[0] call = graph.macro_calls[0] assert macro.name == "loop" assert call.name == "loop" def test_multiple_macros(self, parser): """Parse multiple macro definitions.""" graph = parse_and_lower(parser, """\ #first x |> { &a <| pass } #second y, z |> { &b <| pass } """) assert len(graph.macro_defs) == 2 assert graph.macro_defs[0].name == "first" assert graph.macro_defs[1].name == "second" assert len(graph.macro_defs[0].params) == 1 assert len(graph.macro_defs[1].params) == 2 def test_macro_with_regular_nodes(self, parser): """Parse macro alongside regular node definitions.""" graph = parse_and_lower(parser, """\ &normal <| pass #macro x |> { &inside <| pass } &another <| pass """) # Top-level nodes assert "&normal" in graph.nodes assert "&another" in graph.nodes # Macro definition assert len(graph.macro_defs) == 1 macro = graph.macro_defs[0] assert "&inside" in macro.body.nodes class TestFunctionCallSyntax: """Tests for function call syntax parsing (AC4.1, AC4.9, AC4.10).""" def test_call_stmt_basic_named_arg(self, parser): """Parse basic function call with named argument. Verifies AC4.1: $func a=&x |> @out generates CallSiteResult with func_name=$func, named arg a=&x, output @out """ graph = parse_and_lower(parser, """\ $add a=&x |> @out """) assert len(graph.raw_call_sites) == 1 call_site = graph.raw_call_sites[0] assert call_site.func_name == "$add" assert len(call_site.input_args) == 1 assert call_site.input_args[0][0] == "a" assert call_site.input_args[0][1]["name"] == "&x" # output_dests is a flat tuple of output dicts assert len(call_site.output_dests) == 1 output_dict = call_site.output_dests[0] assert isinstance(output_dict, dict) assert output_dict["name"] == "@out" def test_call_stmt_multiple_named_args(self, parser): """Parse function call with multiple named arguments. Verifies AC4.1 with multiple inputs: $func a=&x, b=&y |> @out1, name=@out2 """ graph = parse_and_lower(parser, """\ $add a=&x, b=&y |> @out1, name=@out2 """) assert len(graph.raw_call_sites) == 1 call_site = graph.raw_call_sites[0] assert call_site.func_name == "$add" assert len(call_site.input_args) == 2 assert call_site.input_args[0][0] == "a" assert call_site.input_args[1][0] == "b" # Check output dests - flat tuple of dicts assert len(call_site.output_dests) == 2 assert call_site.output_dests[0]["name"] == "@out1" # positional output # Named output has {"name": str, "ref": ref_dict} assert call_site.output_dests[1].get("name") == "name" assert call_site.output_dests[1].get("ref")["name"] == "@out2" def test_call_stmt_positional_arg(self, parser): """Parse function call with positional argument. Verifies AC4.1 with positional syntax: $func &x |> @out """ graph = parse_and_lower(parser, """\ $add &x |> @out """) assert len(graph.raw_call_sites) == 1 call_site = graph.raw_call_sites[0] assert call_site.func_name == "$add" assert len(call_site.input_args) == 1 # Positional args are stored with None as the parameter name assert call_site.input_args[0][0] is None assert call_site.input_args[0][1]["name"] == "&x" def test_call_stmt_no_args_parses_as_plain_edge(self, parser): """Verify that $func |> @out (no args, no parens) parses as plain_edge. This is the disambiguation rule from AC4.1: call_stmt requires at least one argument before |>. Bare function references are edges. """ graph = parse_and_lower(parser, """\ $add |> @out """) # Should have parsed as plain_edge, not call_stmt assert len(graph.raw_call_sites) == 0 # And should have an edge assert len(graph.edges) > 0 assert graph.edges[0].source == "$add" assert graph.edges[0].dest == "@out" def test_call_stmt_in_program_context(self, parser): """Parse function call alongside other statements.""" graph = parse_and_lower(parser, """\ &value <| const, 42 $add a=&value |> @result &result <| pass """) # Should have nodes, edges, and call site assert "&value" in graph.nodes assert "&result" in graph.nodes assert len(graph.raw_call_sites) == 1 call_site = graph.raw_call_sites[0] assert call_site.func_name == "$add" def test_call_stmt_multiple_calls(self, parser): """Parse multiple function calls.""" graph = parse_and_lower(parser, """\ $add a=&x |> @sum $mul a=&y |> @prod """) assert len(graph.raw_call_sites) == 2 assert graph.raw_call_sites[0].func_name == "$add" assert graph.raw_call_sites[1].func_name == "$mul" def test_call_stmt_named_output(self, parser): """Parse function call with named output destination. Verifies that name=@dest syntax is captured in output_dests. """ graph = parse_and_lower(parser, """\ $add a=&x |> sum=@result """) call_site = graph.raw_call_sites[0] assert len(call_site.output_dests) == 1 output = call_site.output_dests[0] assert output.get("name") == "sum" assert output.get("ref")["name"] == "@result"