"""Tests for IRGraph serialization to dfasm source. Tests verify the serialize() function converts IRGraphs back to valid dfasm source and preserves structural information through round-trip parsing and lowering. """ from tests.pipeline import parse_and_lower, parse_lower_resolve from asm.ir import ( IRGraph, IRNode, IREdge, IRRegion, RegionKind, IRDataDef ) from asm.serialize import serialize from asm.opcodes import OP_TO_MNEMONIC from cm_inst import ArithOp, LogicOp, MemOp, Port, RoutingOp class TestRoundTrip: """AC11.1, AC11.2: Round-trip parse → lower → serialize → parse → lower""" def test_simple_two_node_graph(self, parser): """AC11.1: Simple graph with two nodes and one edge round-trips.""" source = """ &add|pe0 <| add &const|pe0 <| const, 42 &add |> &const:L """ # Parse and lower to get baseline IRGraph graph1 = parse_and_lower(parser, source) # Serialize and re-parse, lower serialized = serialize(graph1) graph2 = parse_and_lower(parser, serialized) # Check structural equivalence assert set(graph1.nodes.keys()) == set(graph2.nodes.keys()) assert len(graph1.edges) == len(graph2.edges) def test_single_node_roundtrip(self, parser): """AC11.1: Single node without edges round-trips.""" source = "&foo|pe0 <| add" graph1 = parse_and_lower(parser, source) serialized = serialize(graph1) graph2 = parse_and_lower(parser, serialized) assert set(graph1.nodes.keys()) == set(graph2.nodes.keys()) assert len(graph1.edges) == len(graph2.edges) def test_graph_with_multiple_edges(self, parser): """AC11.2: Graph with multiple edges preserves edge count.""" source = """ &a|pe0 <| add &b|pe0 <| sub &c|pe1 <| add &a |> &b:L &a |> &c:R &b |> &c:L """ graph1 = parse_and_lower(parser, source) serialized = serialize(graph1) graph2 = parse_and_lower(parser, serialized) assert len(graph1.edges) == len(graph2.edges) assert set(graph1.nodes.keys()) == set(graph2.nodes.keys()) class TestPlacementQualifiers: """AC11.3: All inst_def lines include |pe{N} qualifiers.""" def test_all_nodes_have_pe_qualifier(self): """AC11.3: Serialized output contains |pe{N} on every inst_def.""" # Create a graph with nodes on different PEs nodes = { "&a": IRNode(name="&a", opcode=ArithOp.ADD, pe=0), "&b": IRNode(name="&b", opcode=ArithOp.SUB, pe=1), "&c": IRNode(name="&c", opcode=RoutingOp.CONST, const=42, pe=0), } graph = IRGraph(nodes=nodes) serialized = serialize(graph) lines = serialized.strip().split('\n') # Filter to inst_def lines (contain '<|') inst_lines = [l for l in lines if '<|' in l] # Each should contain |pe{N} for line in inst_lines: assert '|pe' in line, f"Missing PE qualifier in: {line}" class TestFunctionScoping: """AC11.4, AC11.7: FUNCTION regions serialize with scoping blocks.""" def test_function_region_structure(self): """AC11.4, AC11.7: FUNCTION regions emit $name |> { ... } blocks.""" # Create a function region with unqualified node names inside func_nodes = { "&add": IRNode(name="&add", opcode=ArithOp.ADD, pe=0), "&const": IRNode(name="&const", opcode=RoutingOp.CONST, const=10, pe=0), } func_body = IRGraph(nodes=func_nodes) func_region = IRRegion(tag="$main", kind=RegionKind.FUNCTION, body=func_body) graph = IRGraph(regions=[func_region]) serialized = serialize(graph) # Check for function block syntax assert "$main |>" in serialized assert "{" in serialized assert "}" in serialized # Check that node names inside are unqualified (no $main. prefix) assert "&add|pe0 <| add" in serialized or "&add|pe0 <| add" in serialized.replace('\n', ' ') def test_unqualified_names_in_function(self): """AC11.7: Names inside FUNCTION regions are unqualified.""" func_nodes = { "&label": IRNode(name="&label", opcode=ArithOp.ADD, pe=0), } func_body = IRGraph(nodes=func_nodes) func_region = IRRegion(tag="$main", kind=RegionKind.FUNCTION, body=func_body) graph = IRGraph(regions=[func_region]) serialized = serialize(graph) # Should not contain the qualified name $main.&label assert "$main.&label" not in serialized # Should contain unqualified &label assert "&label" in serialized class TestDataDefs: """AC11.5: data_def entries serialize with SM placement and cell addresses.""" def test_data_def_serialization(self): """AC11.5: Data definitions serialize as @name|sm{id}:{cell} = {value}.""" data_defs = [ IRDataDef(name="@hello", sm_id=0, cell_addr=5, value=0x2a), IRDataDef(name="@world", sm_id=1, cell_addr=10, value=0x3f), ] graph = IRGraph(data_defs=data_defs) serialized = serialize(graph) # Check for data def entries assert "@hello|sm0:5 =" in serialized assert "@world|sm1:10 =" in serialized # Check hex values are present assert "0x2a" in serialized or "42" in serialized assert "0x3f" in serialized or "63" in serialized class TestAnonymousNodes: """AC11.6: Anonymous nodes serialize as inst_def + plain_edge, not inline.""" def test_anonymous_node_not_inline(self): """AC11.6: Nodes with __anon_ in name use inst_def + plain_edge form.""" nodes = { "&source": IRNode(name="&source", opcode=ArithOp.ADD, pe=0), "__anon_1": IRNode(name="__anon_1", opcode=ArithOp.SUB, pe=0), "&dest": IRNode(name="&dest", opcode=ArithOp.ADD, pe=1), } edges = [ IREdge(source="&source", dest="__anon_1", port=Port.L), IREdge(source="__anon_1", dest="&dest", port=Port.R), ] graph = IRGraph(nodes=nodes, edges=edges) serialized = serialize(graph) # Anonymous node should be defined as a separate inst_def, not inline assert "__anon_1|pe0 <|" in serialized # Should have explicit edges, not inline syntax assert "&source |> __anon_1:L" in serialized assert "__anon_1 |> &dest:R" in serialized class TestLocationRegions: """AC11.8: LOCATION regions serialize as bare directive + body.""" def test_location_region_structure(self): """AC11.8: LOCATION regions emit bare directive tag followed by body.""" # Create a location region with data definitions inside loc_data = [ IRDataDef(name="@data1", sm_id=0, cell_addr=5, value=100), ] loc_body = IRGraph(data_defs=loc_data) loc_region = IRRegion(tag="@data_section", kind=RegionKind.LOCATION, body=loc_body) graph = IRGraph(regions=[loc_region]) serialized = serialize(graph) # Location directive should appear as bare tag with trailing colon (not in $func |> form) assert "@data_section:" in serialized # Body content should follow assert "@data1|sm0:5" in serialized class TestEdgeSerialization: """Edges serialize as plain_edge form.""" def test_edge_port_notation(self): """Edges serialize with :L and :R port notation.""" nodes = { "&a": IRNode(name="&a", opcode=ArithOp.ADD, pe=0), "&b": IRNode(name="&b", opcode=ArithOp.ADD, pe=1), } edges = [ IREdge(source="&a", dest="&b", port=Port.L), ] graph = IRGraph(nodes=nodes, edges=edges) serialized = serialize(graph) # Should contain edge with port notation assert "|> &b:L" in serialized class TestMnemonicUsage: """Opcodes serialize using OP_TO_MNEMONIC.""" def test_various_opcodes(self): """Different opcodes serialize with correct mnemonics.""" nodes = { "&add": IRNode(name="&add", opcode=ArithOp.ADD, pe=0), "&sub": IRNode(name="&sub", opcode=ArithOp.SUB, pe=0), "&and": IRNode(name="&and", opcode=LogicOp.AND, pe=0), "&read": IRNode(name="&read", opcode=MemOp.READ, pe=0), } graph = IRGraph(nodes=nodes) serialized = serialize(graph) # Check that mnemonics appear assert "add" in serialized assert "sub" in serialized assert "and" in serialized assert "read" in serialized class TestConstValues: """Const values serialize in inst_def format.""" def test_const_node_with_value(self): """Nodes with const values serialize with const argument.""" nodes = { "&c": IRNode(name="&c", opcode=RoutingOp.CONST, const=255, pe=0), } graph = IRGraph(nodes=nodes) serialized = serialize(graph) # Should include const value assert "const, 255" in serialized or "const, 0xff" in serialized class TestEmptyGraph: """Edge case: empty graph.""" def test_empty_graph_serializes(self): """AC11.1: Empty graph serializes to empty or whitespace string.""" graph = IRGraph() serialized = serialize(graph) # Should not raise, should be empty or whitespace assert serialized.strip() == ""