OR-1 dataflow CPU sketch
at 00d336d2d4b197bbb9dbbf3641f5f112bf0cf3ec 403 lines 13 kB view raw
1"""Tests for IR-to-JSON conversion (graph_to_json). 2 3Tests verify: 4- dataflow-renderer.AC1.1 (fully allocated graph): Valid dfasm renders as JSON with nodes, edges, metadata 5- dataflow-renderer.AC1.1 (partially resolved graph): Incomplete programs produce partial JSON with errors 6- Error nodes flagged: Nodes with errors marked as has_error: true 7- Function regions: Function regions appear in JSON with correct node_ids 8- Parse error case: Parse failures produce empty JSON with parse_error string 9""" 10 11from dfgraph.graph_json import graph_to_json 12from dfgraph.pipeline import run_progressive, PipelineStage 13 14 15class TestFullyAllocatedGraph: 16 """Test graph_to_json on fully allocated programs (stage=allocate).""" 17 18 def test_simple_const_add_chain_json(self): 19 """AC1.1: Simple CONST→ADD chain renders with full node/edge data.""" 20 source = """\ 21@system pe=2, sm=0 22&c1|pe0 <| const, 3 23&c2|pe0 <| const, 7 24&result|pe0 <| add 25&output|pe1 <| pass 26&c1|pe0 |> &result|pe0:L 27&c2|pe0 |> &result|pe0:R 28&result|pe0 |> &output|pe1:L 29""" 30 result = run_progressive(source) 31 json_out = graph_to_json(result) 32 33 # Verify stage reached 34 assert result.stage == PipelineStage.ALLOCATE 35 assert json_out["stage"] == "allocate" 36 37 # Verify nodes are present with all required fields 38 assert len(json_out["nodes"]) >= 4 39 node_ids = {n["id"] for n in json_out["nodes"]} 40 assert "&c1" in node_ids 41 assert "&c2" in node_ids 42 assert "&result" in node_ids 43 assert "&output" in node_ids 44 45 # Check a node has full allocation data 46 result_node = next(n for n in json_out["nodes"] if n["id"] == "&result") 47 assert result_node["opcode"] == "add" 48 assert result_node["category"] == "arithmetic" 49 assert result_node["pe"] == 0 50 assert result_node["iram_offset"] is not None 51 assert result_node["ctx"] is not None 52 assert result_node["has_error"] is False 53 54 # Verify edges are present 55 assert len(json_out["edges"]) >= 3 56 edges = json_out["edges"] 57 assert any(e["source"] == "&c1" and e["target"] == "&result" for e in edges) 58 assert any(e["source"] == "&result" and e["target"] == "&output" for e in edges) 59 60 # Verify metadata 61 assert json_out["metadata"]["pe_count"] == 2 62 assert json_out["metadata"]["sm_count"] == 0 63 assert json_out["parse_error"] is None 64 assert len(json_out["errors"]) == 0 65 66 def test_sm_program_json(self): 67 """AC1.1: SM operations include sm_id in nodes.""" 68 source = """\ 69@system pe=2, sm=1 70&trigger|pe0 <| const, 1 71&reader|pe0 <| read 72&relay|pe1 <| pass 73&trigger|pe0 |> &reader|pe0:L 74&reader|pe0 |> &relay|pe1:L 75""" 76 result = run_progressive(source) 77 json_out = graph_to_json(result) 78 79 assert result.stage == PipelineStage.ALLOCATE 80 assert json_out["stage"] == "allocate" 81 82 # Check nodes 83 reader_node = next(n for n in json_out["nodes"] if n["id"] == "&reader") 84 assert reader_node["opcode"] == "read" 85 assert reader_node["category"] == "memory" 86 assert reader_node["pe"] == 0 87 assert reader_node["iram_offset"] is not None 88 89 # Check metadata reflects SM 90 assert json_out["metadata"]["sm_count"] == 1 91 92 def test_edge_port_serialization(self): 93 """Edges include source/target ports correctly.""" 94 source = """\ 95@system pe=1, sm=0 96&a|pe0 <| const, 5 97&b|pe0 <| add 98&c|pe0 <| pass 99&a|pe0 |> &b|pe0:L 100&b|pe0 |> &c|pe0:R 101""" 102 result = run_progressive(source) 103 json_out = graph_to_json(result) 104 105 # Find the edge from a to b 106 edge_ab = next(e for e in json_out["edges"] if e["source"] == "&a" and e["target"] == "&b") 107 assert edge_ab["port"] == "L" # Destination input port 108 109 # Find the edge from b to c 110 edge_bc = next(e for e in json_out["edges"] if e["source"] == "&b" and e["target"] == "&c") 111 assert edge_bc["port"] == "R" # Destination input port 112 113 114class TestPartiallyResolvedGraph: 115 """Test graph_to_json on graphs with errors (stage=resolve or lower).""" 116 117 def test_undefined_reference_json(self): 118 """AC1.1: Undefined reference creates error but partial graph still renders.""" 119 source = """\ 120@system pe=1, sm=0 121&a <| const, 5 122&b <| add 123&a |> &b:L 124&b |> &undefined:R 125""" 126 result = run_progressive(source) 127 json_out = graph_to_json(result) 128 129 # Should stop at resolve (error prevents further stages) 130 assert result.stage == PipelineStage.RESOLVE 131 assert json_out["stage"] == "resolve" 132 133 # But we still get nodes that were lowered 134 node_ids = {n["id"] for n in json_out["nodes"]} 135 assert "&a" in node_ids 136 assert "&b" in node_ids 137 138 # Nodes should not have allocation data (stopped at resolve) 139 # Note: pe is set by explicit placement, iram_offset and ctx are set by allocate 140 for node in json_out["nodes"]: 141 assert node["iram_offset"] is None 142 assert node["ctx"] is None 143 144 # Error list should be populated 145 assert len(json_out["errors"]) > 0 146 assert any("undefined" in e["message"].lower() for e in json_out["errors"]) 147 148 def test_error_nodes_flagged(self): 149 """Nodes on error lines are flagged with has_error: true.""" 150 source = """\ 151@system pe=1, sm=0 152&a <| const, 5 153&b <| add 154&a |> &b:L 155&b |> &undefined:R 156""" 157 result = run_progressive(source) 158 json_out = graph_to_json(result) 159 160 # The edge referencing undefined will have an error 161 # which means the destination node (if it exists) or source gets flagged 162 assert len(json_out["errors"]) > 0 163 error_line = json_out["errors"][0]["line"] 164 165 # Check if any node on this error line is flagged 166 # (The edge error is at the line with &b |> &undefined...) 167 error_flagged = any(n["has_error"] for n in json_out["nodes"]) 168 assert error_flagged or any(e["has_error"] for e in json_out["edges"]) 169 170 171class TestFunctionRegions: 172 """Test function region serialization.""" 173 174 def test_function_regions_json(self): 175 """Function regions appear in JSON with correct tag, kind, and node_ids.""" 176 source = """\ 177@system pe=2, sm=0 178 179$func1 |> { 180 &a|pe0 <| const, 1 181 &b|pe0 <| add 182 &a|pe0 |> &b|pe0:L 183} 184 185$func2 |> { 186 &c|pe1 <| const, 2 187} 188""" 189 result = run_progressive(source) 190 json_out = graph_to_json(result) 191 192 # Check regions 193 regions = json_out["regions"] 194 assert len(regions) >= 2 195 196 # Find function regions by tag 197 func1_region = next((r for r in regions if r["tag"] == "$func1"), None) 198 func2_region = next((r for r in regions if r["tag"] == "$func2"), None) 199 200 assert func1_region is not None 201 assert func1_region["kind"] == "function" 202 assert set(func1_region["node_ids"]) == {"$func1.&a", "$func1.&b"} 203 204 assert func2_region is not None 205 assert func2_region["kind"] == "function" 206 assert set(func2_region["node_ids"]) == {"$func2.&c"} 207 208 209class TestParseErrorCase: 210 """Test graph_to_json on parse failures.""" 211 212 def test_parse_error_json(self): 213 """Parse error produces empty JSON with parse_error string.""" 214 source = """\ 215@system pe=1, sm=0 216&invalid syntax [[[ 217""" 218 result = run_progressive(source) 219 json_out = graph_to_json(result) 220 221 # Should be in parse error stage 222 assert result.stage == PipelineStage.PARSE_ERROR 223 assert json_out["stage"] == "parse_error" 224 225 # Should have no nodes/edges 226 assert json_out["nodes"] == [] 227 assert json_out["edges"] == [] 228 assert json_out["regions"] == [] 229 230 # Should have parse_error string 231 assert json_out["parse_error"] is not None 232 assert len(json_out["parse_error"]) > 0 233 234 # metadata should have zeros 235 assert json_out["metadata"]["pe_count"] == 0 236 assert json_out["metadata"]["sm_count"] == 0 237 238 239class TestJsonStructure: 240 """Test JSON output structure compliance.""" 241 242 def test_node_structure(self): 243 """Each node has all required fields.""" 244 source = """\ 245@system pe=1, sm=0 246&a|pe0 <| const, 5 247""" 248 result = run_progressive(source) 249 json_out = graph_to_json(result) 250 251 node = json_out["nodes"][0] 252 required_fields = {"id", "opcode", "category", "colour", "const", "pe", 253 "iram_offset", "ctx", "has_error", "loc"} 254 assert set(node.keys()) >= required_fields 255 256 # Location should have required fields 257 loc = node["loc"] 258 assert "line" in loc 259 assert "column" in loc 260 261 def test_edge_structure(self): 262 """Each edge has all required fields.""" 263 source = """\ 264@system pe=1, sm=0 265&a|pe0 <| const, 5 266&b|pe0 <| add 267&a|pe0 |> &b|pe0:L 268""" 269 result = run_progressive(source) 270 json_out = graph_to_json(result) 271 272 edge = json_out["edges"][0] 273 required_fields = {"source", "target", "port", "source_port", "has_error"} 274 assert set(edge.keys()) >= required_fields 275 276 def test_error_structure(self): 277 """Each error has required fields.""" 278 source = """\ 279@system pe=1, sm=0 280&a|pe0 <| const, 5 281&b|pe0 |> &undefined|pe0:L 282""" 283 result = run_progressive(source) 284 json_out = graph_to_json(result) 285 286 assert len(json_out["errors"]) > 0, "Expected errors for undefined reference" 287 error = json_out["errors"][0] 288 required_fields = {"line", "column", "category", "message", "suggestions"} 289 assert set(error.keys()) >= required_fields 290 291 def test_metadata_structure(self): 292 """Metadata has all required fields.""" 293 source = "@system pe=2, sm=1" 294 result = run_progressive(source) 295 json_out = graph_to_json(result) 296 297 metadata = json_out["metadata"] 298 required_fields = {"stage", "pe_count", "sm_count"} 299 assert set(metadata.keys()) >= required_fields 300 assert metadata["pe_count"] == 2 301 assert metadata["sm_count"] == 1 302 303 def test_top_level_structure(self): 304 """Top-level JSON has all required fields.""" 305 source = "@system pe=1, sm=0" 306 result = run_progressive(source) 307 json_out = graph_to_json(result) 308 309 required_fields = {"type", "stage", "nodes", "edges", "regions", 310 "errors", "parse_error", "metadata"} 311 assert set(json_out.keys()) >= required_fields 312 assert json_out["type"] == "graph_update" 313 314 315class TestEmptyProgram: 316 """Test minimal/empty programs.""" 317 318 def test_system_only_json(self): 319 """Program with only @system pragma produces valid JSON.""" 320 source = "@system pe=2, sm=1" 321 result = run_progressive(source) 322 json_out = graph_to_json(result) 323 324 assert json_out["stage"] == "allocate" 325 assert json_out["nodes"] == [] 326 assert json_out["edges"] == [] 327 assert json_out["metadata"]["pe_count"] == 2 328 assert json_out["metadata"]["sm_count"] == 1 329 330 331class TestColourMapping: 332 """Test opcode-to-colour mapping.""" 333 334 def test_arithmetic_colour(self): 335 """Arithmetic ops get correct colour.""" 336 source = """\ 337@system pe=1, sm=0 338&add_node|pe0 <| add 339""" 340 result = run_progressive(source) 341 json_out = graph_to_json(result) 342 343 node = next(n for n in json_out["nodes"] if n["id"] == "&add_node") 344 assert node["category"] == "arithmetic" 345 assert node["colour"] == "#4a90d9" # arithmetic blue 346 347 def test_memory_colour(self): 348 """Memory ops get correct colour.""" 349 source = """\ 350@system pe=1, sm=0 351&read_node|pe0 <| read 352""" 353 result = run_progressive(source) 354 json_out = graph_to_json(result) 355 356 node = next(n for n in json_out["nodes"] if n["id"] == "&read_node") 357 assert node["category"] == "memory" 358 assert node["colour"] == "#ff5722" # memory red 359 360 def test_routing_colour(self): 361 """Routing ops get correct colour.""" 362 source = """\ 363@system pe=1, sm=0 364&pass_node|pe0 <| pass 365""" 366 result = run_progressive(source) 367 json_out = graph_to_json(result) 368 369 node = next(n for n in json_out["nodes"] if n["id"] == "&pass_node") 370 assert node["category"] == "routing" 371 assert node["colour"] == "#9c27b0" # routing purple 372 373 374class TestMultipleRegions: 375 """Test handling of nested and multiple regions.""" 376 377 def test_multiple_functions_and_locations(self): 378 """Multiple function and location regions handled correctly.""" 379 source = """\ 380@system pe=2, sm=0 381 382$func1 |> { 383 &a|pe0 <| const, 1 384} 385 386@loc1 387 388$func2 |> { 389 &c|pe0 <| add 390} 391""" 392 result = run_progressive(source) 393 json_out = graph_to_json(result) 394 395 # Only FUNCTION regions should appear in output 396 regions = json_out["regions"] 397 function_regions = [r for r in regions if r["kind"] == "function"] 398 399 # Should have 2 function regions 400 assert len(function_regions) == 2 401 tags = {r["tag"] for r in function_regions} 402 assert "$func1" in tags 403 assert "$func2" in tags