diff --git a/asm/expand.py b/asm/expand.py index 5d28dd8..aa7566c 100644 --- a/asm/expand.py +++ b/asm/expand.py @@ -389,7 +389,7 @@ def _clone_and_substitute_edge( subst_map: dict[str, object], func_scope: Optional[str] = None, parent_scope: str = "", -) -> IREdge: +) -> tuple[IREdge, list[AssemblyError]]: """Deep-clone an edge and substitute/qualify names. Args: @@ -400,8 +400,9 @@ def _clone_and_substitute_edge( parent_scope: Optional parent macro scope Returns: - New IREdge with names qualified + Tuple of (new IREdge with names qualified, list of errors) """ + errors: list[AssemblyError] = [] # Substitute source and dest names. # Track whether each was a ParamRef — substituted refs are external # and must NOT be qualified with the macro scope. @@ -432,10 +433,20 @@ def _clone_and_substitute_edge( elif resolved == "R": new_port = Port.R else: - new_port = Port.L # fallback, error reported elsewhere + errors.append(AssemblyError( + loc=edge.loc, + category=ErrorCategory.MACRO, + message=f"port parameter must resolve to 'L' or 'R', got '{resolved}'", + )) + new_port = Port.L elif isinstance(resolved, Port): new_port = resolved else: + errors.append(AssemblyError( + loc=edge.loc, + category=ErrorCategory.MACRO, + message=f"port parameter must resolve to 'L' or 'R', got '{resolved}'", + )) new_port = Port.L # Resolve PortRef on source port @@ -448,13 +459,23 @@ def _clone_and_substitute_edge( elif resolved == "R": new_source_port = Port.R else: + errors.append(AssemblyError( + loc=edge.loc, + category=ErrorCategory.MACRO, + message=f"source port parameter must resolve to 'L' or 'R', got '{resolved}'", + )) new_source_port = None elif isinstance(resolved, Port): new_source_port = resolved else: + errors.append(AssemblyError( + loc=edge.loc, + category=ErrorCategory.MACRO, + message=f"source port parameter must resolve to 'L' or 'R', got '{resolved}'", + )) new_source_port = None - return replace(edge, source=source, dest=dest, port=new_port, source_port=new_source_port) + return replace(edge, source=source, dest=dest, port=new_port, source_port=new_source_port), errors def _add_expansion_context( @@ -539,13 +560,14 @@ def _expand_repetition_block( # Clone and substitute edges from the repetition body for edge in rep_block.body.edges: - qualified_edge = _clone_and_substitute_edge( + qualified_edge, edge_errors = _clone_and_substitute_edge( edge, f"{macro_scope}_rep{idx}", iter_subst_map, func_scope, parent_scope, ) + errors.extend(edge_errors) expanded_edges.append(qualified_edge) return expanded_nodes, expanded_edges, errors @@ -671,7 +693,8 @@ def _expand_call( # Qualify the body's own edges for edge in body.edges: - qualified_edge = _clone_and_substitute_edge(edge, macro_scope, subst_map, func_scope, parent_scope) + qualified_edge, edge_errors = _clone_and_substitute_edge(edge, macro_scope, subst_map, func_scope, parent_scope) + errors.extend(edge_errors) body_edges.append(qualified_edge) # Expand macro calls at this body level @@ -690,7 +713,12 @@ def _expand_call( for error in nested_errors: body_errors.append(_add_expansion_context(error, nested_call, builtin_line_offset)) body_nodes.update(nested_expanded_nodes) - body_edges.extend(nested_expanded_edges) + # Filter out leaked @ret edges from failed inner expansions to prevent + # spurious "defines output(s) @ret" errors at the outer macro level + for nested_edge in nested_expanded_edges: + if isinstance(nested_edge.dest, str) and nested_edge.dest.startswith("@ret"): + continue + body_edges.append(nested_edge) # Expand repetition blocks (Phase 6 variadic macros) if variadic_param: diff --git a/asm/lower.py b/asm/lower.py index 9f4cc51..d18c4f6 100644 --- a/asm/lower.py +++ b/asm/lower.py @@ -219,12 +219,10 @@ class LowerTransformer(Transformer): elif isinstance(stmt, EdgeResult): # Qualify and add edges for edge in stmt.edges: - qualified_edge = IREdge( + qualified_edge = replace( + edge, source=self._qualify_name(edge.source, func_scope), dest=self._qualify_name(edge.dest, func_scope), - port=edge.port, - source_port=edge.source_port, - loc=edge.loc, ) edges.append(qualified_edge) @@ -236,12 +234,10 @@ class LowerTransformer(Transformer): qualified_node = replace(node, name=qualified_name) nodes[qualified_name] = qualified_node for edge in stmt.edges: - qualified_edge = IREdge( + qualified_edge = replace( + edge, source=self._qualify_name(edge.source, func_scope), dest=self._qualify_name(edge.dest, func_scope), - port=edge.port, - source_port=edge.source_port, - loc=edge.loc, ) edges.append(qualified_edge) diff --git a/docs/test-requirements.md b/docs/test-requirements.md index be71049..884cfec 100644 --- a/docs/test-requirements.md +++ b/docs/test-requirements.md @@ -114,14 +114,14 @@ Slug: `macro-enh` - **E3.1c Success:** `#macro args |> &a, &b` parses (multiple outputs) - **E3.1d Success:** `#macro args |> name1=&a, name2=&b` parses (multiple named outputs) - **Test type:** Unit (parse + lower) -- **File:** `tests/test_macro_ret.py` +- **File:** `tests/test_macro_ret_wiring.py` ### macro-enh.E3.2: Lower pass stores output_dests on IRMacroCall - **E3.2a Success:** `IRMacroCall.output_dests` contains positional output refs - **E3.2b Success:** `IRMacroCall.output_dests` contains named output refs (name, ref) tuples - **Test type:** Unit (lower) -- **File:** `tests/test_macro_ret.py` +- **File:** `tests/test_macro_ret_wiring.py` ### macro-enh.E3.3: Expand pass rewrites @ret edges @@ -132,27 +132,27 @@ Slug: `macro-enh` - **E3.3e Failure:** Macro body has `@ret` but call site provides zero outputs → MACRO error - **E3.3f Success:** Positional @ret maps to first positional output - **Test type:** Unit (expand) -- **File:** `tests/test_macro_ret.py` +- **File:** `tests/test_macro_ret_wiring.py` ### macro-enh.E3.4: @ret port preservation - **E3.4a Success:** `&src |> @ret:R` rewrites to `&src |> &dest:R` — port on @ret is preserved - **E3.4b Success:** `&src |> @ret_exit:R` also preserves port - **Test type:** Unit (expand) -- **File:** `tests/test_macro_ret.py` +- **File:** `tests/test_macro_ret_wiring.py` ### macro-enh.E3.5: Nested macro @ret scoping - **E3.5a Success:** Macro A calls macro B which has @ret; B's @ret resolves at B's call site (inside A's body), A's @ret resolves at A's call site - **Test type:** Unit (expand) -- **File:** `tests/test_macro_ret.py` +- **File:** `tests/test_macro_ret_wiring.py` ### macro-enh.E3.6: Full pipeline with @ret macros - **E3.6a Success:** Macro with @ret + call site output list assembles through full pipeline - **E3.6b Success:** Generated edges connect expanded macro internals to call-site-specified destinations - **Test type:** Integration (full pipeline) -- **File:** `tests/test_macro_ret.py` +- **File:** `tests/test_macro_ret_wiring.py` --- @@ -164,14 +164,14 @@ Slug: `macro-enh` - **E4.1b Success:** `#reduce_3 sub` expands correctly (two nodes with ArithOp.SUB, wired) - **E4.1c Success:** `#reduce_4 add` expands correctly (three nodes, tree structure) - **Test type:** Unit (expand) -- **File:** `tests/test_builtins_v2.py` +- **File:** `tests/test_builtins.py` ### macro-enh.E4.2: New builtins use @ret wiring - **E4.2a Success:** `#loop_counted |> body=&proc, exit=&done` wires @ret_body → &proc, @ret_exit → &done - **E4.2b Success:** `#loop_while |> body=&proc, exit=&done` wires similarly - **Test type:** Unit (expand) -- **File:** `tests/test_builtins_v2.py` +- **File:** `tests/test_builtins.py` ### macro-enh.E4.3: Backwards compatibility @@ -182,7 +182,7 @@ Slug: `macro-enh` - **E4.4a Success:** Program using `#loop_counted |> body=&body, exit=&done` + `#reduce_2 add` assembles through full pipeline - **Test type:** Integration (full pipeline) -- **File:** `tests/test_builtins_v2.py` +- **File:** `tests/test_builtins.py` --- diff --git a/tests/test_macro_ret_wiring.py b/tests/test_macro_ret_wiring.py index a717048..bc1b768 100644 --- a/tests/test_macro_ret_wiring.py +++ b/tests/test_macro_ret_wiring.py @@ -284,6 +284,23 @@ class TestE36_NoOutputWiringError: assert "output" in macro_errors[0].message.lower() or "@ret" in macro_errors[0].message + def test_bare_ret_with_only_named_outputs(self): + """Bare @ret with only named outputs at call site -> MACRO error.""" + source = """ + @system pe=1, sm=1 + #test val |> { + &g <| const, ${val} + &g |> @ret + } + &sink <| add + #test 1 |> out=&sink + """ + graph = parse_lower_expand(source) + macro_errors = [e for e in graph.errors if e.category == ErrorCategory.MACRO] + assert len(macro_errors) >= 1 + assert "@ret" in macro_errors[0].message + + class TestE37_MultipleRetMarkers: """E3.7: Multiple @ret markers with mixed positional/named outputs.""" diff --git a/tests/test_qualified_ref_params.py b/tests/test_qualified_ref_params.py index 1e22766..22301c4 100644 --- a/tests/test_qualified_ref_params.py +++ b/tests/test_qualified_ref_params.py @@ -233,6 +233,38 @@ class TestE25_ExpandResolvesPort: assert len(edges) >= 1 assert edges[0].port == Port.R + def test_invalid_port_value_produces_error(self): + """E2.5c: Invalid port value produces MACRO error.""" + source = """ + @system pe=1, sm=1 + #wire port |> { + &src <| pass + &dst <| add + &src |> &dst:${port} + } + #wire X + """ + graph = parse_lower_expand(source) + macro_errors = [e for e in graph.errors if e.category == ErrorCategory.MACRO] + assert len(macro_errors) >= 1 + assert "port" in macro_errors[0].message.lower() + + def test_invalid_source_port_value_produces_error(self): + """Invalid source port value produces MACRO error.""" + source = """ + @system pe=1, sm=1 + #wire port |> { + &src <| pass + &dst <| add + &src:${port} |> &dst + } + #wire Z + """ + graph = parse_lower_expand(source) + macro_errors = [e for e in graph.errors if e.category == ErrorCategory.MACRO] + assert len(macro_errors) >= 1 + assert "port" in macro_errors[0].message.lower() + class TestE26_ExpandResolvesCtxSlot: """E2.6: Expand pass resolves context slot ParamRef."""