OR-1 dataflow CPU sketch
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