"""Tests for Enhancement 1: Opcode Parameters (macro-enh.E1.*). Tests verify: - macro-enh.E1.1: Grammar accepts param_ref in opcode position - macro-enh.E1.2: Lower pass stores ParamRef in IRNode.opcode - macro-enh.E1.3: OPCODE accepted as positional macro argument - macro-enh.E1.4: Expand pass resolves opcode ParamRef - macro-enh.E1.5: Full pipeline with opcode params """ from pathlib import Path from lark import Lark from asm import assemble, run_pipeline from asm.expand import expand from asm.lower import lower from asm.errors import ErrorCategory from asm.ir import IRNode, ParamRef from cm_inst import ArithOp, LogicOp, RoutingOp, MemOp, Port def _get_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): parser = _get_parser() tree = parser.parse(source) return lower(tree) def parse_lower_expand(source: str): graph = parse_and_lower(source) return expand(graph) class TestE11_GrammarAcceptsParamRefOpcode: """E1.1: Grammar accepts param_ref in opcode position.""" def test_param_ref_opcode_in_inst_def(self): """${op} in inst_def opcode position parses and lowers.""" source = """ @system pe=1, sm=1 #wrap op |> { &n <| ${op} } """ graph = parse_and_lower(source) assert not graph.errors # Macro body should have a node with ParamRef opcode assert len(graph.macro_defs) == 1 body_nodes = graph.macro_defs[0].body.nodes assert len(body_nodes) == 1 node = list(body_nodes.values())[0] assert isinstance(node.opcode, ParamRef) assert node.opcode.param == "op" def test_param_ref_opcode_in_strong_edge(self): """${op} in strong_edge opcode position parses and lowers.""" source = """ @system pe=1, sm=1 #wrap op |> { ${op} &src |> &dst } """ graph = parse_and_lower(source) assert not graph.errors body_nodes = graph.macro_defs[0].body.nodes # Strong edge creates anonymous node anon_nodes = [n for n in body_nodes.values() if isinstance(n.opcode, ParamRef)] assert len(anon_nodes) == 1 assert anon_nodes[0].opcode.param == "op" def test_param_ref_opcode_in_weak_edge(self): """${op} in weak_edge opcode position parses and lowers.""" source = """ @system pe=1, sm=1 #wrap op |> { &dst ${op} <| &src } """ graph = parse_and_lower(source) assert not graph.errors body_nodes = graph.macro_defs[0].body.nodes anon_nodes = [n for n in body_nodes.values() if isinstance(n.opcode, ParamRef)] assert len(anon_nodes) == 1 assert anon_nodes[0].opcode.param == "op" class TestE13_OpcodeAsMacroArgument: """E1.3: OPCODE accepted as positional macro argument.""" def test_bare_opcode_in_macro_call(self): """#reduce_2 add parses — bare opcode as macro argument.""" source = """ @system pe=1, sm=1 #wrap op |> { &n <| ${op} } #wrap add """ graph = parse_and_lower(source) assert not graph.errors assert len(graph.macro_calls) == 1 call = graph.macro_calls[0] assert call.positional_args == ("add",) def test_multiple_opcode_args(self): """Multiple opcodes can be passed as arguments.""" source = """ @system pe=1, sm=1 #pair op1, op2 |> { &a <| ${op1} &b <| ${op2} } #pair add, sub """ graph = parse_and_lower(source) assert not graph.errors call = graph.macro_calls[0] assert call.positional_args == ("add", "sub") class TestE14_ExpandResolvesOpcodeParamRef: """E1.4: Expand pass resolves opcode ParamRef.""" def test_resolve_arith_opcode(self): """Opcode param 'add' resolves to ArithOp.ADD.""" source = """ @system pe=1, sm=1 #wrap op |> { &n <| ${op} } #wrap add """ graph = parse_lower_expand(source) assert not graph.errors node = list(graph.nodes.values())[0] assert node.opcode == ArithOp.ADD def test_resolve_routing_opcode(self): """Opcode param 'gate' resolves to RoutingOp.GATE.""" source = """ @system pe=1, sm=1 #wrap op |> { &n <| ${op} } #wrap gate """ graph = parse_lower_expand(source) assert not graph.errors node = list(graph.nodes.values())[0] assert node.opcode == RoutingOp.GATE def test_resolve_mem_opcode(self): """Opcode param 'read' resolves to MemOp.READ.""" source = """ @system pe=1, sm=1 #wrap op |> { &n <| ${op} } #wrap read """ graph = parse_lower_expand(source) assert not graph.errors node = list(graph.nodes.values())[0] assert node.opcode == MemOp.READ def test_invalid_opcode_mnemonic_error(self): """Invalid mnemonic produces MACRO error. Note: 'banana' lexes as IDENT and parses as a qualified_ref (label_ref &banana), so we pass it as a qualified_ref dict. The expand pass gets a dict, not a string, which produces the 'must resolve to an opcode mnemonic' error. """ source = """ @system pe=1, sm=1 #wrap op |> { &n <| ${op} } #wrap &banana """ graph = parse_lower_expand(source) macro_errors = [e for e in graph.errors if e.category == ErrorCategory.MACRO] assert len(macro_errors) >= 1 assert "opcode mnemonic" in macro_errors[0].message def test_numeric_opcode_error(self): """Numeric value as opcode produces MACRO error.""" source = """ @system pe=1, sm=1 #wrap op |> { &n <| ${op} } #wrap 42 """ graph = parse_lower_expand(source) macro_errors = [e for e in graph.errors if e.category == ErrorCategory.MACRO] assert len(macro_errors) >= 1 class TestE15_FullPipelineOpcodeParams: """E1.5: Full pipeline with opcode params.""" def test_full_pipeline_opcode_param(self): """Opcode-parameterized macro assembles through full pipeline.""" source = """ @system pe=1, sm=1 #wrap op |> { &n <| ${op} } &seed <| const, 5 #wrap add &seed |> #wrap_0.&n:L &seed |> #wrap_0.&n:R """ result = assemble(source) assert result is not None # Should have at least one PE config assert len(result.pe_configs) >= 1 def test_full_pipeline_reduce_pattern(self): """Reduction tree pattern with opcode param.""" source = """ @system pe=1, sm=1 #reduce_2 op |> { &r <| ${op} } &a <| const, 3 &b <| const, 7 #reduce_2 add &a |> #reduce_2_0.&r:L &b |> #reduce_2_0.&r:R """ result = assemble(source) assert result is not None