OR-1 dataflow CPU sketch
1"""Tests for macro definition parsing and lowering.
2
3Tests verify:
4- Macro definition parsing (AC1.1) → MacroDef with name, params, body
5- Macro body with various statement types (AC1.2) → template IRGraph
6- ParamRef in macro body (AC1.3) → const fields and edge endpoints
7- Duplicate parameter names (AC1.4) → error with ErrorCategory.NAME
8- Reserved names (AC1.5) → error with ErrorCategory.NAME
9- Macro call statements → IRMacroCall in graph.macro_calls
10- Dot-notation scope resolution (AC7.1, AC7.2) → qualified names
11- Macro references in edges → scoped_ref support
12"""
13
14from tests.pipeline import parse_and_lower
15
16from asm.ir import RegionKind, SourceLoc, MacroParam, MacroDef, IRMacroCall
17from asm.errors import ErrorCategory
18
19
20class TestMacroDefinition:
21 """Tests for macro definition parsing (AC1.1, AC1.2)."""
22
23 def test_macro_def_basic_parses(self, parser):
24 """Parse simple macro definition."""
25 graph = parse_and_lower(parser, """\
26 #simple |> {
27 &a <| pass
28 }
29 """)
30
31 assert len(graph.macro_defs) == 1
32 macro = graph.macro_defs[0]
33 assert macro.name == "simple"
34 assert macro.params == ()
35 assert "&a" in macro.body.nodes
36
37 def test_macro_def_with_params(self, parser):
38 """Parse macro with parameters."""
39 graph = parse_and_lower(parser, """\
40 #loop_counted init, limit |> {
41 &counter <| add
42 }
43 """)
44
45 assert len(graph.macro_defs) == 1
46 macro = graph.macro_defs[0]
47 assert macro.name == "loop_counted"
48 assert len(macro.params) == 2
49 assert macro.params[0].name == "init"
50 assert macro.params[1].name == "limit"
51 assert "&counter" in macro.body.nodes
52
53 def test_macro_def_body_with_edges(self, parser):
54 """Parse macro body with edges."""
55 graph = parse_and_lower(parser, """\
56 #routing |> {
57 &a <| pass
58 &b <| pass
59 &a |> &b:L
60 }
61 """)
62
63 macro = graph.macro_defs[0]
64 assert len(macro.body.nodes) == 2
65 # The edge is parsed and stored
66 assert len(macro.body.edges) > 0
67
68 def test_macro_def_body_with_strong_edge(self, parser):
69 """Parse macro with inline strong edge."""
70 graph = parse_and_lower(parser, """\
71 #inline_math |> {
72 add 1, 2 |> &result:L
73 }
74 """)
75
76 macro = graph.macro_defs[0]
77 # Anonymous node created by strong_edge
78 assert len(macro.body.nodes) >= 1
79 # &result is the destination, not a node in this context
80 assert len(macro.body.edges) > 0
81
82 def test_macro_def_no_params_with_empty_body(self, parser):
83 """Parse macro with no params and empty body."""
84 graph = parse_and_lower(parser, """\
85 #empty |> {
86 }
87 """)
88
89 macro = graph.macro_defs[0]
90 assert macro.name == "empty"
91 assert macro.params == ()
92 assert len(macro.body.nodes) == 0
93
94
95class TestMacroCallStatement:
96 """Tests for macro invocation statements."""
97
98 def test_macro_call_stmt_no_args(self, parser):
99 """Parse macro call with no arguments."""
100 graph = parse_and_lower(parser, """\
101 #simple
102 """)
103
104 assert len(graph.macro_calls) == 1
105 call = graph.macro_calls[0]
106 assert call.name == "simple"
107 assert call.positional_args == ()
108 assert call.named_args == ()
109
110 def test_macro_call_stmt_with_positional_args(self, parser):
111 """Parse macro call with positional arguments."""
112 graph = parse_and_lower(parser, """\
113 #loop_counted &src, &dest
114 """)
115
116 assert len(graph.macro_calls) == 1
117 call = graph.macro_calls[0]
118 assert call.name == "loop_counted"
119 assert len(call.positional_args) == 2
120 # Args are dicts with 'name' field
121 assert call.positional_args[0]["name"] == "&src"
122 assert call.positional_args[1]["name"] == "&dest"
123
124 def test_macro_call_stmt_with_value_arg(self, parser):
125 """Parse macro call with literal value argument."""
126 graph = parse_and_lower(parser, """\
127 #init 42
128 """)
129
130 call = graph.macro_calls[0]
131 assert call.name == "init"
132 assert len(call.positional_args) == 1
133
134 def test_macro_call_stmt_with_named_arg(self, parser):
135 """Parse macro call with named argument."""
136 graph = parse_and_lower(parser, """\
137 #inject gate=&my_gate
138 """)
139
140 call = graph.macro_calls[0]
141 assert call.name == "inject"
142 assert len(call.named_args) == 1
143 assert call.named_args[0][0] == "gate"
144
145
146class TestMacroParameterValidation:
147 """Tests for macro parameter validation (AC1.4, AC1.5)."""
148
149 def test_duplicate_param_names_error(self, parser):
150 """Detect duplicate parameter names."""
151 graph = parse_and_lower(parser, """\
152 #bad dup, dup |> {
153 &a <| pass
154 }
155 """)
156
157 # Check for error
158 assert len(graph.errors) > 0
159 error = graph.errors[0]
160 assert error.category == ErrorCategory.NAME
161 assert "Duplicate parameter" in error.message
162 assert "dup" in error.message
163
164 def test_reserved_macro_name_error(self, parser):
165 """Detect reserved macro names."""
166 graph = parse_and_lower(parser, """\
167 #ret_value |> {
168 &a <| pass
169 }
170 """)
171
172 assert len(graph.errors) > 0
173 error = graph.errors[0]
174 assert error.category == ErrorCategory.NAME
175 assert "reserved prefix" in error.message.lower()
176 assert "ret" in error.message
177
178
179class TestScopedReferences:
180 """Tests for dot-notation scope resolution (AC7.1, AC7.2)."""
181
182 def test_function_scoped_ref_in_edge_source(self, parser):
183 """Parse function scoped reference as edge source."""
184 graph = parse_and_lower(parser, """\
185 $func |> {
186 &label <| pass
187 }
188 &dest <| pass
189 $func.&label |> &dest:L
190 """)
191
192 # The edge should reference the scoped name
193 edge = graph.edges[0]
194 assert edge.source == "$func.&label"
195
196 def test_macro_scoped_ref_in_edge_source(self, parser):
197 """Parse macro scoped reference as edge source.
198
199 Lower creates the qualified name; expand resolves it to the
200 actual expanded node name (e.g., #macro_0.&label).
201 """
202 graph = parse_and_lower(parser, """\
203 &dest <| pass
204 #macro.&label |> &dest:L
205 """)
206
207 assert len(graph.edges) > 0
208 edge = graph.edges[0]
209 assert edge.source == "#macro.&label"
210
211 def test_macro_ref_in_edge_source(self, parser):
212 """Parse macro reference as edge source."""
213 graph = parse_and_lower(parser, """\
214 &dest <| pass
215 #macro |> &dest:L
216 """)
217
218 # Should parse the macro_ref in the edge source
219 assert len(graph.edges) > 0
220 edge = graph.edges[0]
221 assert edge.source == "#macro"
222
223
224class TestMacroRefGrammar:
225 """Tests for macro_ref and scoped_ref grammar productions."""
226
227 def test_macro_ref_in_data_def_target(self, parser):
228 """Parse macro reference as data definition target."""
229 graph = parse_and_lower(parser, """\
230 #macrodata = 42
231 """)
232
233 # The data_def should reference the macro
234 assert len(graph.data_defs) > 0
235 data_def = graph.data_defs[0]
236 assert data_def.name == "#macrodata"
237
238 def test_scoped_ref_with_label_ref(self, parser):
239 """Parse scoped_ref using label_ref as inner."""
240 graph = parse_and_lower(parser, """\
241 $func |> {
242 &inner <| pass
243 }
244 &dest <| pass
245 $func.&inner |> &dest:L
246 """)
247
248 # Scoped ref to label should work
249 edge = graph.edges[0]
250 assert edge.source == "$func.&inner"
251
252 def test_node_ref_inside_function_stays_global(self, parser):
253 """@name refs inside function bodies are NOT scope-qualified.
254
255 Only &label refs get qualified with function scope. @name refs
256 are global (with @ret/@ret_name as special exceptions handled
257 by the expand pass).
258 """
259 graph = parse_and_lower(parser, """\
260 $func |> {
261 @inner <| pass
262 &local <| pass
263 }
264 """)
265
266 func_region = next(
267 r for r in graph.regions if r.kind == RegionKind.FUNCTION
268 )
269 node_names = set(func_region.body.nodes.keys())
270 assert "@inner" in node_names, f"@inner should stay unqualified, got {node_names}"
271 assert "$func.&local" in node_names, f"&local should be qualified, got {node_names}"
272
273
274class TestMacroInContext:
275 """Tests for macro definitions and calls in full program context."""
276
277 def test_macro_def_followed_by_call(self, parser):
278 """Parse macro definition followed by invocation."""
279 graph = parse_and_lower(parser, """\
280 #loop init, limit |> {
281 &counter <| add
282 }
283 #loop &start, &end
284 """)
285
286 assert len(graph.macro_defs) == 1
287 assert len(graph.macro_calls) == 1
288
289 macro = graph.macro_defs[0]
290 call = graph.macro_calls[0]
291 assert macro.name == "loop"
292 assert call.name == "loop"
293
294 def test_multiple_macros(self, parser):
295 """Parse multiple macro definitions."""
296 graph = parse_and_lower(parser, """\
297 #first x |> {
298 &a <| pass
299 }
300 #second y, z |> {
301 &b <| pass
302 }
303 """)
304
305 assert len(graph.macro_defs) == 2
306 assert graph.macro_defs[0].name == "first"
307 assert graph.macro_defs[1].name == "second"
308 assert len(graph.macro_defs[0].params) == 1
309 assert len(graph.macro_defs[1].params) == 2
310
311 def test_macro_with_regular_nodes(self, parser):
312 """Parse macro alongside regular node definitions."""
313 graph = parse_and_lower(parser, """\
314 &normal <| pass
315 #macro x |> {
316 &inside <| pass
317 }
318 &another <| pass
319 """)
320
321 # Top-level nodes
322 assert "&normal" in graph.nodes
323 assert "&another" in graph.nodes
324
325 # Macro definition
326 assert len(graph.macro_defs) == 1
327 macro = graph.macro_defs[0]
328 assert "&inside" in macro.body.nodes
329
330
331class TestFunctionCallSyntax:
332 """Tests for function call syntax parsing (AC4.1, AC4.9, AC4.10)."""
333
334 def test_call_stmt_basic_named_arg(self, parser):
335 """Parse basic function call with named argument.
336
337 Verifies AC4.1: $func a=&x |> @out generates CallSiteResult
338 with func_name=$func, named arg a=&x, output @out
339 """
340 graph = parse_and_lower(parser, """\
341 $add a=&x |> @out
342 """)
343
344 assert len(graph.raw_call_sites) == 1
345 call_site = graph.raw_call_sites[0]
346 assert call_site.func_name == "$add"
347 assert len(call_site.input_args) == 1
348 assert call_site.input_args[0][0] == "a"
349 assert call_site.input_args[0][1]["name"] == "&x"
350 # output_dests is a flat tuple of output dicts
351 assert len(call_site.output_dests) == 1
352 output_dict = call_site.output_dests[0]
353 assert isinstance(output_dict, dict)
354 assert output_dict["name"] == "@out"
355
356 def test_call_stmt_multiple_named_args(self, parser):
357 """Parse function call with multiple named arguments.
358
359 Verifies AC4.1 with multiple inputs: $func a=&x, b=&y |> @out1, name=@out2
360 """
361 graph = parse_and_lower(parser, """\
362 $add a=&x, b=&y |> @out1, name=@out2
363 """)
364
365 assert len(graph.raw_call_sites) == 1
366 call_site = graph.raw_call_sites[0]
367 assert call_site.func_name == "$add"
368 assert len(call_site.input_args) == 2
369 assert call_site.input_args[0][0] == "a"
370 assert call_site.input_args[1][0] == "b"
371 # Check output dests - flat tuple of dicts
372 assert len(call_site.output_dests) == 2
373 assert call_site.output_dests[0]["name"] == "@out1" # positional output
374 # Named output has {"name": str, "ref": ref_dict}
375 assert call_site.output_dests[1].get("name") == "name"
376 assert call_site.output_dests[1].get("ref")["name"] == "@out2"
377
378 def test_call_stmt_positional_arg(self, parser):
379 """Parse function call with positional argument.
380
381 Verifies AC4.1 with positional syntax: $func &x |> @out
382 """
383 graph = parse_and_lower(parser, """\
384 $add &x |> @out
385 """)
386
387 assert len(graph.raw_call_sites) == 1
388 call_site = graph.raw_call_sites[0]
389 assert call_site.func_name == "$add"
390 assert len(call_site.input_args) == 1
391 # Positional args are stored with None as the parameter name
392 assert call_site.input_args[0][0] is None
393 assert call_site.input_args[0][1]["name"] == "&x"
394
395 def test_call_stmt_no_args_parses_as_plain_edge(self, parser):
396 """Verify that $func |> @out (no args, no parens) parses as plain_edge.
397
398 This is the disambiguation rule from AC4.1: call_stmt requires at least
399 one argument before |>. Bare function references are edges.
400 """
401 graph = parse_and_lower(parser, """\
402 $add |> @out
403 """)
404
405 # Should have parsed as plain_edge, not call_stmt
406 assert len(graph.raw_call_sites) == 0
407 # And should have an edge
408 assert len(graph.edges) > 0
409 assert graph.edges[0].source == "$add"
410 assert graph.edges[0].dest == "@out"
411
412 def test_call_stmt_in_program_context(self, parser):
413 """Parse function call alongside other statements."""
414 graph = parse_and_lower(parser, """\
415 &value <| const, 42
416 $add a=&value |> @result
417 &result <| pass
418 """)
419
420 # Should have nodes, edges, and call site
421 assert "&value" in graph.nodes
422 assert "&result" in graph.nodes
423 assert len(graph.raw_call_sites) == 1
424 call_site = graph.raw_call_sites[0]
425 assert call_site.func_name == "$add"
426
427 def test_call_stmt_multiple_calls(self, parser):
428 """Parse multiple function calls."""
429 graph = parse_and_lower(parser, """\
430 $add a=&x |> @sum
431 $mul a=&y |> @prod
432 """)
433
434 assert len(graph.raw_call_sites) == 2
435 assert graph.raw_call_sites[0].func_name == "$add"
436 assert graph.raw_call_sites[1].func_name == "$mul"
437
438 def test_call_stmt_named_output(self, parser):
439 """Parse function call with named output destination.
440
441 Verifies that name=@dest syntax is captured in output_dests.
442 """
443 graph = parse_and_lower(parser, """\
444 $add a=&x |> sum=@result
445 """)
446
447 call_site = graph.raw_call_sites[0]
448 assert len(call_site.output_dests) == 1
449 output = call_site.output_dests[0]
450 assert output.get("name") == "sum"
451 assert output.get("ref")["name"] == "@result"