OR-1 dataflow CPU sketch
at pe-frame-redesign 451 lines 15 kB view raw
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"