OR-1 dataflow CPU sketch
1"""Tests for the progressive pipeline runner (dfgraph/pipeline.py).
2
3Tests verify:
4- dataflow-renderer.AC2.1: Clean source produces allocate-stage graph with zero errors
5- dataflow-renderer.AC2.2: Source with errors at any stage still returns a graph with valid nodes
6- dataflow-renderer.AC5.2: Error results contain AssemblyError objects with loc.line, category, and message
7- dataflow-renderer.AC5.3: Name resolution errors include suggestions (Levenshtein)
8- Parse errors are handled gracefully and return PARSE_ERROR stage with graph=None
9"""
10
11from textwrap import dedent
12
13from dfgraph.pipeline import run_progressive, PipelineStage, PipelineResult
14from asm.errors import ErrorCategory
15
16
17class TestProgressivePipelineCleanSource:
18 """Tests for AC2.1: Clean source produces allocate-stage graph with zero errors."""
19
20 def test_simple_const_add_program(self):
21 """Clean program with const and add nodes produces ALLOCATE stage with no errors."""
22 source = dedent("""\
23 @system pe=2, sm=0
24 &c1|pe0 <| const, 3
25 &c2|pe0 <| const, 7
26 &result|pe0 <| add
27 &output|pe1 <| pass
28 &c1|pe0 |> &result|pe0:L
29 &c2|pe0 |> &result|pe0:R
30 &result|pe0 |> &output|pe1:L
31 """)
32
33 result = run_progressive(source)
34
35 assert result.stage == PipelineStage.ALLOCATE
36 assert len(result.errors) == 0
37 assert result.graph is not None
38 assert len(result.graph.nodes) >= 4
39
40 def test_program_with_functions(self):
41 """Clean program with function-scoped labels produces ALLOCATE stage with no errors."""
42 source = dedent("""\
43 @system pe=2, sm=0
44 $main |> {
45 &input <| const, 42
46 &process <| pass
47 &input |> &process:L
48 }
49 """)
50
51 result = run_progressive(source)
52
53 assert result.stage == PipelineStage.ALLOCATE
54 assert len(result.errors) == 0
55 assert result.graph is not None
56
57 def test_program_with_sm_operations(self):
58 """Clean program with SM operations produces ALLOCATE stage with no errors."""
59 source = dedent("""\
60 @system pe=2, sm=1
61 @val|sm0:5 = 0x42
62 &trigger|pe0 <| const, 1
63 &reader|pe0 <| read, 5
64 &relay|pe1 <| pass
65 &trigger|pe0 |> &reader|pe0:L
66 &reader|pe0 |> &relay|pe1:L
67 """)
68
69 result = run_progressive(source)
70
71 assert result.stage == PipelineStage.ALLOCATE
72 assert len(result.errors) == 0
73 assert result.graph is not None
74
75 def test_program_with_cross_pe_routing(self):
76 """Clean program with cross-PE routing produces ALLOCATE stage with no errors."""
77 source = dedent("""\
78 @system pe=3, sm=0
79 &source|pe0 <| const, 99
80 &dest|pe1 <| pass
81 &output|pe2 <| pass
82 &source|pe0 |> &dest|pe1:L
83 &dest|pe1 |> &output|pe2:L
84 """)
85
86 result = run_progressive(source)
87
88 assert result.stage == PipelineStage.ALLOCATE
89 assert len(result.errors) == 0
90 assert result.graph is not None
91
92
93class TestProgressivePipelineResolveErrors:
94 """Tests for AC2.2: Source with resolve errors still returns graph at RESOLVE stage."""
95
96 def test_undefined_name_reference_stops_at_resolve(self):
97 """Program with undefined node reference produces graph at RESOLVE stage."""
98 source = dedent("""\
99 @system pe=2, sm=0
100 &a <| const, 42
101 &b <| add
102 &a |> &undefined:L
103 """)
104
105 result = run_progressive(source)
106
107 assert result.stage == PipelineStage.RESOLVE
108 assert len(result.errors) > 0
109 assert result.graph is not None
110 assert len(result.graph.nodes) >= 2
111
112 def test_name_error_has_category(self):
113 """NAME error contains ErrorCategory.NAME."""
114 source = dedent("""\
115 @system pe=2, sm=0
116 &a <| pass
117 &b <| add
118 &a |> &undefined:L
119 """)
120
121 result = run_progressive(source)
122
123 assert result.stage == PipelineStage.RESOLVE
124 name_errors = [e for e in result.errors if e.category == ErrorCategory.NAME]
125 assert len(name_errors) > 0
126
127 def test_name_error_has_suggestions(self):
128 """NAME error includes suggestions when available (AC5.3)."""
129 source = dedent("""\
130 @system pe=2, sm=0
131 &nonexistent <| pass
132 &a <| add
133 &a |> &nonexistant:L
134 """)
135
136 result = run_progressive(source)
137
138 assert result.stage == PipelineStage.RESOLVE
139 name_errors = [e for e in result.errors if e.category == ErrorCategory.NAME]
140 assert len(name_errors) > 0
141 error = name_errors[0]
142 # Levenshtein suggestions should be present
143 assert len(error.suggestions) > 0
144
145 def test_scope_violation_stops_at_resolve(self):
146 """Program with scope violation produces graph at RESOLVE stage."""
147 source = dedent("""\
148 @system pe=2, sm=0
149 $foo |> {
150 &private <| pass
151 }
152 &top <| add
153 &top |> &private:L
154 """)
155
156 result = run_progressive(source)
157
158 assert result.stage == PipelineStage.RESOLVE
159 assert len(result.errors) > 0
160 scope_errors = [e for e in result.errors if e.category == ErrorCategory.SCOPE]
161 assert len(scope_errors) > 0
162
163
164class TestProgressivePipelinePlacementErrors:
165 """Tests for AC2.2: Source with placement errors returns graph at PLACE stage."""
166
167 def test_explicit_placement_violation_stops_at_place(self):
168 """Program with invalid explicit PE placement produces graph at PLACE stage."""
169 source = dedent("""\
170 @system pe=2, sm=0
171 &a|pe5 <| const, 1
172 &b <| add
173 &a |> &b:L
174 """)
175
176 result = run_progressive(source)
177
178 assert result.stage == PipelineStage.PLACE
179 assert len(result.errors) > 0
180 assert result.graph is not None
181 # Should have placement errors
182 placement_errors = [e for e in result.errors if e.category == ErrorCategory.PLACEMENT]
183 assert len(placement_errors) > 0
184
185
186class TestProgressivePipelineErrorMetadata:
187 """Tests for AC5.2: Errors contain loc.line, category, and message fields."""
188
189 def test_error_contains_source_location(self):
190 """Error contains SourceLoc with line and column."""
191 source = dedent("""\
192 @system pe=2, sm=0
193 &a <| pass
194 &b <| add
195 &a |> &undefined:L
196 """)
197
198 result = run_progressive(source)
199
200 assert len(result.errors) > 0
201 error = result.errors[0]
202 assert error.loc.line > 0
203 assert error.loc.column >= 0
204
205 def test_error_contains_category(self):
206 """Error contains an ErrorCategory enum value."""
207 source = dedent("""\
208 @system pe=2, sm=0
209 &a <| pass
210 &b <| add
211 &a |> &undefined:L
212 """)
213
214 result = run_progressive(source)
215
216 assert len(result.errors) > 0
217 error = result.errors[0]
218 assert isinstance(error.category, ErrorCategory)
219
220 def test_error_contains_message(self):
221 """Error contains a non-empty message string."""
222 source = dedent("""\
223 @system pe=2, sm=0
224 &a <| pass
225 &b <| add
226 &a |> &undefined:L
227 """)
228
229 result = run_progressive(source)
230
231 assert len(result.errors) > 0
232 error = result.errors[0]
233 assert len(error.message) > 0
234 assert isinstance(error.message, str)
235
236
237class TestProgressivePipelineParseErrors:
238 """Tests for parse errors: invalid syntax returns PARSE_ERROR stage."""
239
240 def test_invalid_syntax_returns_parse_error(self):
241 """Syntactically invalid source returns PARSE_ERROR stage."""
242 source = "this is not valid dfasm @ @@ {{{ syntax"
243
244 result = run_progressive(source)
245
246 assert result.stage == PipelineStage.PARSE_ERROR
247 assert result.graph is None
248 assert result.parse_error is not None
249 assert isinstance(result.parse_error, str)
250 assert len(result.parse_error) > 0
251
252 def test_parse_error_no_errors_list(self):
253 """PARSE_ERROR result has empty errors list (error is in parse_error field)."""
254 source = "@@@"
255
256 result = run_progressive(source)
257
258 assert result.stage == PipelineStage.PARSE_ERROR
259 assert len(result.errors) == 0
260
261
262class TestProgressivePipelineResultDataclass:
263 """Tests for PipelineResult dataclass structure."""
264
265 def test_result_is_frozen_dataclass(self):
266 """PipelineResult is a frozen dataclass."""
267 source = "@system pe=1, sm=0\n&a <| pass"
268
269 result = run_progressive(source)
270
271 assert isinstance(result, PipelineResult)
272 # Should be frozen (immutable)
273 try:
274 result.stage = PipelineStage.LOWER
275 assert False, "PipelineResult should be frozen"
276 except (AttributeError, ValueError):
277 pass # Expected
278
279 def test_result_stage_is_enum(self):
280 """result.stage is a PipelineStage enum value."""
281 source = "@system pe=1, sm=0\n&a <| pass"
282
283 result = run_progressive(source)
284
285 assert isinstance(result.stage, PipelineStage)
286
287 def test_result_errors_is_list(self):
288 """result.errors is a list of AssemblyError objects."""
289 source = "@system pe=2, sm=0\n&a <| pass\n&b <| add\n&a |> &undefined:L"
290
291 result = run_progressive(source)
292
293 assert isinstance(result.errors, list)
294 if len(result.errors) > 0:
295 from asm.errors import AssemblyError
296 assert all(isinstance(e, AssemblyError) for e in result.errors)
297
298
299class TestProgressivePipelineGraphStructure:
300 """Tests for IRGraph structure returned at each stage."""
301
302 def test_graph_at_lower_stage_has_nodes(self):
303 """IRGraph at LOWER stage contains IRNodes."""
304 source = dedent("""\
305 @system pe=1, sm=0
306 &a <| const, 42
307 &b <| pass
308 """)
309
310 result = run_progressive(source)
311
312 assert result.graph is not None
313 assert len(result.graph.nodes) > 0
314 assert all(name for name in result.graph.nodes.keys())
315
316 def test_graph_at_resolve_stage_has_nodes(self):
317 """IRGraph at RESOLVE stage contains IRNodes."""
318 source = dedent("""\
319 @system pe=2, sm=0
320 &a <| pass
321 &b <| add
322 &a |> &undefined:L
323 """)
324
325 result = run_progressive(source)
326
327 assert result.stage == PipelineStage.RESOLVE
328 assert result.graph is not None
329 assert len(result.graph.nodes) > 0
330
331 def test_graph_at_allocate_stage_has_offsets(self):
332 """IRGraph at ALLOCATE stage has IRAM offsets on nodes."""
333 source = dedent("""\
334 @system pe=2, sm=0
335 &a <| const, 42
336 &b <| pass
337 &a |> &b:L
338 """)
339
340 result = run_progressive(source)
341
342 assert result.stage == PipelineStage.ALLOCATE
343 # After allocation, nodes should have iram_offset set
344 for node in result.graph.nodes.values():
345 assert node.iram_offset is not None
346
347
348class TestProgressivePipelineEmptySource:
349 """Tests for edge cases."""
350
351 def test_empty_source_parses_successfully(self):
352 """Empty source parses and returns ALLOCATE stage with default @system."""
353 source = ""
354
355 result = run_progressive(source)
356
357 # Empty source parses successfully and creates a default @system pragma
358 assert result.stage == PipelineStage.ALLOCATE
359 assert result.graph is not None
360 # Should have default system config (pe=1, sm=1)
361 assert result.graph.system is not None
362
363 def test_system_pragma_only(self):
364 """Source with only @system pragma parses successfully."""
365 source = "@system pe=2, sm=0"
366
367 result = run_progressive(source)
368
369 assert result.stage == PipelineStage.ALLOCATE
370 assert len(result.errors) == 0
371 assert result.graph is not None