OR-1 dataflow CPU sketch

feat(asm): add opcode parameter support for macros

Grammar: opcode rule now accepts param_ref alternative, positional_arg
accepts OPCODE token. Lower pass defers ParamRef opcodes and wraps bare
OPCODE tokens in macro call arguments as strings. Expand pass resolves
opcode mnemonic strings to ALUOp/MemOp via MNEMONIC_TO_OP during
macro body cloning.

Enables: #reduce_2 op |> { &r <| ${op} } / #reduce_2 add

Orual dcac0058 281148e3

+482 -11
+24 -1
asm/expand.py
··· 21 21 IRGraph, IRNode, IREdge, IRRegion, RegionKind, ParamRef, ConstExpr, 22 22 MacroDef, IRMacroCall, CallSiteResult, CallSite, IRRepetitionBlock, SourceLoc 23 23 ) 24 + from asm.opcodes import MNEMONIC_TO_OP 24 25 from cm_inst import Port, RoutingOp 25 26 26 27 MAX_EXPANSION_DEPTH = 32 ··· 307 308 message=str(e), 308 309 )) 309 310 311 + # Resolve opcode if it's a ParamRef 312 + new_opcode = node.opcode 313 + if isinstance(new_opcode, ParamRef): 314 + resolved = _substitute_param(new_opcode, subst_map) 315 + if isinstance(resolved, str): 316 + if resolved in MNEMONIC_TO_OP: 317 + new_opcode = MNEMONIC_TO_OP[resolved] 318 + else: 319 + errors.append(AssemblyError( 320 + loc=node.loc, 321 + category=ErrorCategory.MACRO, 322 + message=f"'{resolved}' is not a valid opcode mnemonic", 323 + )) 324 + new_opcode = node.opcode 325 + else: 326 + errors.append(AssemblyError( 327 + loc=node.loc, 328 + category=ErrorCategory.MACRO, 329 + message=f"opcode parameter must resolve to an opcode mnemonic, got {type(resolved).__name__}", 330 + )) 331 + new_opcode = node.opcode 332 + 310 333 # Substitute the node name (may be a ParamRef with token pasting) 311 334 substituted_name = _substitute_param(node.name, subst_map) 312 335 ··· 317 340 # Qualify the node name 318 341 new_name = _qualify_expanded_name(substituted_name, macro_scope, parent_scope, func_scope) 319 342 320 - return replace(node, name=new_name, const=new_const), errors 343 + return replace(node, name=new_name, const=new_const, opcode=new_opcode), errors 321 344 322 345 323 346 def _clone_and_substitute_edge(
+1 -1
asm/ir.py
··· 78 78 sm_id: Optional SM ID for MemOp instructions (populated during lowering) 79 79 """ 80 80 name: Union[str, ParamRef] 81 - opcode: Union[ALUOp, MemOp] 81 + opcode: Union[ALUOp, MemOp, ParamRef] 82 82 dest_l: Optional[Union[NameRef, ResolvedDest]] = None 83 83 dest_r: Optional[Union[NameRef, ResolvedDest]] = None 84 84 const: Optional[Union[int, ParamRef, ConstExpr]] = None
+16 -7
asm/lower.py
··· 977 977 978 978 positional_args = [] 979 979 named_args: dict[str, object] = {} 980 + found_name = False 980 981 for item in args: 981 982 if isinstance(item, LarkToken): 982 - # Skip the macro name token 983 + if not found_name: 984 + # First LarkToken is the macro name 985 + found_name = True 986 + continue 987 + if item.type == "OPCODE": 988 + # Bare opcode token as macro argument — wrap as string 989 + positional_args.append(str(item)) 990 + continue 991 + # Skip other tokens (FLOW_OUT, commas, etc.) 983 992 continue 984 993 elif isinstance(item, tuple) and len(item) == 2 and isinstance(item[0], str): 985 994 # Named argument from named_arg rule (name, value) ··· 987 996 elif isinstance(item, dict) and "name" in item: 988 997 # Positional argument (qualified_ref or value) 989 998 positional_args.append(item) 990 - elif item is not None and not isinstance(item, LarkToken): 991 - # Other argument types 999 + elif item is not None: 1000 + # Other argument types (int literals, etc.) 992 1001 positional_args.append(item) 993 1002 994 1003 macro_call = IRMacroCall( ··· 1242 1251 return (str(param_name), value) 1243 1252 1244 1253 @v_args(inline=True) 1245 - def opcode(self, token: LarkToken) -> Optional[Union[ALUOp, MemOp]]: 1246 - """Map opcode token to ALUOp or MemOp enum, or None if invalid.""" 1254 + def opcode(self, token) -> Optional[Union[ALUOp, MemOp, ParamRef]]: 1255 + """Map opcode token to ALUOp/MemOp enum, ParamRef, or None if invalid.""" 1256 + if isinstance(token, ParamRef): 1257 + return token 1247 1258 mnemonic = str(token) 1248 1259 if mnemonic not in MNEMONIC_TO_OP: 1249 - # Add error but don't crash 1250 1260 self._errors.append(AssemblyError( 1251 1261 loc=SourceLoc(line=token.line, column=token.column), 1252 1262 category=ErrorCategory.PARSE, 1253 1263 message=f"Unknown opcode '{mnemonic}'", 1254 1264 )) 1255 - # Return None for invalid mnemonic 1256 1265 return None 1257 1266 1258 1267 return MNEMONIC_TO_OP[mnemonic]
+2 -2
dfasm.lark
··· 103 103 104 104 ?argument: named_arg | positional_arg 105 105 named_arg: IDENT "=" positional_arg 106 - ?positional_arg: value | qualified_ref 106 + ?positional_arg: value | qualified_ref | OPCODE 107 107 108 108 // === Values (literals) === 109 109 ··· 137 137 // at the lexer level. Semantic validation (monadic/dyadic arity, valid 138 138 // argument combinations) is deferred to the assembler. 139 139 140 - opcode: OPCODE 140 + opcode: OPCODE | param_ref 141 141 142 142 OPCODE.2: "add" | "sub" | "inc" | "dec" 143 143 | "shiftl" | "shiftr" | "ashiftr"
+197
docs/test-requirements.md
··· 1 + # Macro Enhancements — Test Requirements 2 + 3 + Maps each enhancement from `docs/macro-enhancements.md` to specific automated test cases. 4 + 5 + Slug: `macro-enh` 6 + 7 + --- 8 + 9 + ## Enhancement 1: Opcode Parameters 10 + 11 + ### macro-enh.E1.1: Grammar accepts `param_ref` in opcode position 12 + 13 + - **E1.1a Success:** `&r <| ${op}` parses without error inside a macro body 14 + - **E1.1b Success:** `${op} &src |> &dst` (strong_edge with param opcode) parses 15 + - **E1.1c Success:** `&dst ${op} <| &src` (weak_edge with param opcode) parses 16 + - **Test type:** Unit (parse + lower) 17 + - **File:** `tests/test_opcode_params.py` 18 + 19 + ### macro-enh.E1.2: Lower pass stores ParamRef in IRNode.opcode 20 + 21 + - **E1.2a Success:** After lowering a macro body with `${op}`, the IRNode has `opcode` as `ParamRef(param="op")` 22 + - **E1.2b Success:** Anonymous nodes from strong/weak edges with `${op}` also have ParamRef opcode 23 + - **Test type:** Unit (lower) 24 + - **File:** `tests/test_opcode_params.py` 25 + 26 + ### macro-enh.E1.3: OPCODE accepted as positional macro argument 27 + 28 + - **E1.3a Success:** `#reduce_2 add` parses — bare `add` in macro call argument position is accepted 29 + - **E1.3b Success:** Lower pass wraps the OPCODE token as a string `"add"` in `IRMacroCall.positional_args` 30 + - **Test type:** Unit (parse + lower) 31 + - **File:** `tests/test_opcode_params.py` 32 + 33 + ### macro-enh.E1.4: Expand pass resolves opcode ParamRef 34 + 35 + - **E1.4a Success:** Macro `#wrap op |> { &n <| ${op} }` invoked as `#wrap add` produces node with `opcode=ArithOp.ADD` 36 + - **E1.4b Success:** Macro invoked with `sub`, `gate`, `read` (different op types) all resolve correctly 37 + - **E1.4c Failure:** Macro invoked with `#wrap banana` produces MACRO error — invalid opcode mnemonic 38 + - **E1.4d Failure:** Macro invoked with `#wrap 42` (numeric, not opcode) produces MACRO error 39 + - **Test type:** Unit (expand) 40 + - **File:** `tests/test_opcode_params.py` 41 + 42 + ### macro-enh.E1.5: Full pipeline with opcode params 43 + 44 + - **E1.5a Success:** `#reduce_2 op |> { &r <| ${op} }` + `#reduce_2 add` assembles through full pipeline (parse → lower → expand → resolve → place → allocate → codegen) 45 + - **E1.5b Success:** Output PEConfig has correct ALUInst with ArithOp.ADD 46 + - **Test type:** Integration (full pipeline via `assemble()`) 47 + - **File:** `tests/test_opcode_params.py` 48 + 49 + --- 50 + 51 + ## Enhancement 2: Parameterized Placement and Port Qualifiers 52 + 53 + ### macro-enh.E2.1: Grammar accepts `param_ref` in placement position 54 + 55 + - **E2.1a Success:** `&n <| add |${pe}` parses inside macro body 56 + - **E2.1b Success:** Lower pass returns `ParamRef` (wrapped as `PlacementRef`) from placement handler 57 + - **Test type:** Unit (parse + lower) 58 + - **File:** `tests/test_qualified_ref_params.py` 59 + 60 + ### macro-enh.E2.2: Grammar accepts `param_ref` in port position 61 + 62 + - **E2.2a Success:** `&src |> &dst:${port}` parses inside macro body 63 + - **E2.2b Success:** Lower pass returns `ParamRef` (wrapped as `PortRef`) from port handler 64 + - **Test type:** Unit (parse + lower) 65 + - **File:** `tests/test_qualified_ref_params.py` 66 + 67 + ### macro-enh.E2.3: Context slot bracket syntax parses 68 + 69 + - **E2.3a Success:** `&node[2]` parses (literal context slot) 70 + - **E2.3b Success:** `&node|pe0[2]:L` parses (full qualifier chain) 71 + - **E2.3c Success:** `&node[${ctx}]` parses (parameterized context slot) 72 + - **E2.3d Success:** `&node[0..4]` parses (range reservation) 73 + - **Test type:** Unit (parse + lower) 74 + - **File:** `tests/test_qualified_ref_params.py` 75 + 76 + ### macro-enh.E2.4: Expand pass resolves placement ParamRef 77 + 78 + - **E2.4a Success:** Macro with `|${pe}` invoked with `pe0` places node on PE 0 79 + - **E2.4b Success:** Macro with `|${pe}` invoked with `pe1` places node on PE 1 80 + - **E2.4c Failure:** Macro invoked with `|${pe}` where arg is `"banana"` produces MACRO error 81 + - **Test type:** Unit (expand) 82 + - **File:** `tests/test_qualified_ref_params.py` 83 + 84 + ### macro-enh.E2.5: Expand pass resolves port ParamRef 85 + 86 + - **E2.5a Success:** Macro with `:${port}` invoked with `L` resolves to `Port.L` 87 + - **E2.5b Success:** Macro with `:${port}` invoked with `R` resolves to `Port.R` 88 + - **E2.5c Failure:** Macro invoked with invalid port value produces MACRO error 89 + - **Test type:** Unit (expand) 90 + - **File:** `tests/test_qualified_ref_params.py` 91 + 92 + ### macro-enh.E2.6: Expand pass resolves context slot ParamRef 93 + 94 + - **E2.6a Success:** Macro with `[${ctx}]` invoked with `2` resolves to ctx slot 2 95 + - **E2.6b Failure:** Non-numeric ctx slot value produces MACRO error 96 + - **Test type:** Unit (expand) 97 + - **File:** `tests/test_qualified_ref_params.py` 98 + 99 + ### macro-enh.E2.7: Full pipeline with placement/port params 100 + 101 + - **E2.7a Success:** Macro parameterizing PE placement assembles through full pipeline; node placed on correct PE 102 + - **E2.7b Success:** Macro parameterizing port assembles through full pipeline; edge targets correct port 103 + - **Test type:** Integration (full pipeline) 104 + - **File:** `tests/test_qualified_ref_params.py` 105 + 106 + --- 107 + 108 + ## Enhancement 3: @ret Wiring for Macros 109 + 110 + ### macro-enh.E3.1: Grammar accepts output list on macro_call_stmt 111 + 112 + - **E3.1a Success:** `#macro args |> &dest` parses 113 + - **E3.1b Success:** `#macro args |> name=&dest` parses (named output) 114 + - **E3.1c Success:** `#macro args |> &a, &b` parses (multiple outputs) 115 + - **E3.1d Success:** `#macro args |> name1=&a, name2=&b` parses (multiple named outputs) 116 + - **Test type:** Unit (parse + lower) 117 + - **File:** `tests/test_macro_ret.py` 118 + 119 + ### macro-enh.E3.2: Lower pass stores output_dests on IRMacroCall 120 + 121 + - **E3.2a Success:** `IRMacroCall.output_dests` contains positional output refs 122 + - **E3.2b Success:** `IRMacroCall.output_dests` contains named output refs (name, ref) tuples 123 + - **Test type:** Unit (lower) 124 + - **File:** `tests/test_macro_ret.py` 125 + 126 + ### macro-enh.E3.3: Expand pass rewrites @ret edges 127 + 128 + - **E3.3a Success:** Macro body edge `&src |> @ret` becomes `&src |> &actual_dest` after expansion 129 + - **E3.3b Success:** Named `@ret_body` maps to `body=&dest` in call site output 130 + - **E3.3c Success:** Multiple @ret variants (e.g., `@ret_body` + `@ret_exit`) each map to their named outputs 131 + - **E3.3d Failure:** `@ret_body` in macro body but call site has no `body=` output → MACRO error 132 + - **E3.3e Failure:** Macro body has `@ret` but call site provides zero outputs → MACRO error 133 + - **E3.3f Success:** Positional @ret maps to first positional output 134 + - **Test type:** Unit (expand) 135 + - **File:** `tests/test_macro_ret.py` 136 + 137 + ### macro-enh.E3.4: @ret port preservation 138 + 139 + - **E3.4a Success:** `&src |> @ret:R` rewrites to `&src |> &dest:R` — port on @ret is preserved 140 + - **E3.4b Success:** `&src |> @ret_exit:R` also preserves port 141 + - **Test type:** Unit (expand) 142 + - **File:** `tests/test_macro_ret.py` 143 + 144 + ### macro-enh.E3.5: Nested macro @ret scoping 145 + 146 + - **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 147 + - **Test type:** Unit (expand) 148 + - **File:** `tests/test_macro_ret.py` 149 + 150 + ### macro-enh.E3.6: Full pipeline with @ret macros 151 + 152 + - **E3.6a Success:** Macro with @ret + call site output list assembles through full pipeline 153 + - **E3.6b Success:** Generated edges connect expanded macro internals to call-site-specified destinations 154 + - **Test type:** Integration (full pipeline) 155 + - **File:** `tests/test_macro_ret.py` 156 + 157 + --- 158 + 159 + ## Enhancement 4: Built-in Macro Rewrite 160 + 161 + ### macro-enh.E4.1: New builtins use opcode params 162 + 163 + - **E4.1a Success:** `#reduce_2 add` expands correctly (single node with ArithOp.ADD) 164 + - **E4.1b Success:** `#reduce_3 sub` expands correctly (two nodes with ArithOp.SUB, wired) 165 + - **E4.1c Success:** `#reduce_4 add` expands correctly (three nodes, tree structure) 166 + - **Test type:** Unit (expand) 167 + - **File:** `tests/test_builtins_v2.py` 168 + 169 + ### macro-enh.E4.2: New builtins use @ret wiring 170 + 171 + - **E4.2a Success:** `#loop_counted |> body=&proc, exit=&done` wires @ret_body → &proc, @ret_exit → &done 172 + - **E4.2b Success:** `#loop_while |> body=&proc, exit=&done` wires similarly 173 + - **Test type:** Unit (expand) 174 + - **File:** `tests/test_builtins_v2.py` 175 + 176 + ### macro-enh.E4.3: Backwards compatibility 177 + 178 + - **E4.3a:** Old macro names that are removed are documented in CHANGELOG or similar 179 + - **Test type:** Manual verification (pre-1.0, acceptable breakage) 180 + 181 + ### macro-enh.E4.4: Full pipeline with new builtins 182 + 183 + - **E4.4a Success:** Program using `#loop_counted |> body=&body, exit=&done` + `#reduce_2 add` assembles through full pipeline 184 + - **Test type:** Integration (full pipeline) 185 + - **File:** `tests/test_builtins_v2.py` 186 + 187 + --- 188 + 189 + ## Human Verification 190 + 191 + ### macro-enh.HV1: dfgraph renders programs using new macros 192 + - Verify that dfgraph correctly visualises programs using opcode params, @ret wiring 193 + - **Justification:** Graph rendering depends on pipeline output; visual verification needed 194 + 195 + ### macro-enh.HV2: Error messages are useful 196 + - Verify that error messages for invalid opcode params, mismatched @ret, etc. include actionable context (macro name, line, suggestions) 197 + - **Justification:** Error message quality is subjective; automated tests check presence but not clarity
+242
tests/test_opcode_params.py
··· 1 + """Tests for Enhancement 1: Opcode Parameters (macro-enh.E1.*). 2 + 3 + Tests verify: 4 + - macro-enh.E1.1: Grammar accepts param_ref in opcode position 5 + - macro-enh.E1.2: Lower pass stores ParamRef in IRNode.opcode 6 + - macro-enh.E1.3: OPCODE accepted as positional macro argument 7 + - macro-enh.E1.4: Expand pass resolves opcode ParamRef 8 + - macro-enh.E1.5: Full pipeline with opcode params 9 + """ 10 + 11 + from pathlib import Path 12 + 13 + from lark import Lark 14 + 15 + from asm import assemble, run_pipeline 16 + from asm.expand import expand 17 + from asm.lower import lower 18 + from asm.errors import ErrorCategory 19 + from asm.ir import IRNode, ParamRef 20 + from cm_inst import ArithOp, LogicOp, RoutingOp, MemOp, Port 21 + 22 + 23 + def _get_parser(): 24 + grammar_path = Path(__file__).parent.parent / "dfasm.lark" 25 + return Lark( 26 + grammar_path.read_text(), 27 + parser="earley", 28 + propagate_positions=True, 29 + ) 30 + 31 + 32 + def parse_and_lower(source: str): 33 + parser = _get_parser() 34 + tree = parser.parse(source) 35 + return lower(tree) 36 + 37 + 38 + def parse_lower_expand(source: str): 39 + graph = parse_and_lower(source) 40 + return expand(graph) 41 + 42 + 43 + class TestE11_GrammarAcceptsParamRefOpcode: 44 + """E1.1: Grammar accepts param_ref in opcode position.""" 45 + 46 + def test_param_ref_opcode_in_inst_def(self): 47 + """${op} in inst_def opcode position parses and lowers.""" 48 + source = """ 49 + @system pe=1, sm=1 50 + #wrap op |> { 51 + &n <| ${op} 52 + } 53 + """ 54 + graph = parse_and_lower(source) 55 + assert not graph.errors 56 + # Macro body should have a node with ParamRef opcode 57 + assert len(graph.macro_defs) == 1 58 + body_nodes = graph.macro_defs[0].body.nodes 59 + assert len(body_nodes) == 1 60 + node = list(body_nodes.values())[0] 61 + assert isinstance(node.opcode, ParamRef) 62 + assert node.opcode.param == "op" 63 + 64 + def test_param_ref_opcode_in_strong_edge(self): 65 + """${op} in strong_edge opcode position parses and lowers.""" 66 + source = """ 67 + @system pe=1, sm=1 68 + #wrap op |> { 69 + ${op} &src |> &dst 70 + } 71 + """ 72 + graph = parse_and_lower(source) 73 + assert not graph.errors 74 + body_nodes = graph.macro_defs[0].body.nodes 75 + # Strong edge creates anonymous node 76 + anon_nodes = [n for n in body_nodes.values() if isinstance(n.opcode, ParamRef)] 77 + assert len(anon_nodes) == 1 78 + assert anon_nodes[0].opcode.param == "op" 79 + 80 + def test_param_ref_opcode_in_weak_edge(self): 81 + """${op} in weak_edge opcode position parses and lowers.""" 82 + source = """ 83 + @system pe=1, sm=1 84 + #wrap op |> { 85 + &dst ${op} <| &src 86 + } 87 + """ 88 + graph = parse_and_lower(source) 89 + assert not graph.errors 90 + body_nodes = graph.macro_defs[0].body.nodes 91 + anon_nodes = [n for n in body_nodes.values() if isinstance(n.opcode, ParamRef)] 92 + assert len(anon_nodes) == 1 93 + assert anon_nodes[0].opcode.param == "op" 94 + 95 + 96 + class TestE13_OpcodeAsMacroArgument: 97 + """E1.3: OPCODE accepted as positional macro argument.""" 98 + 99 + def test_bare_opcode_in_macro_call(self): 100 + """#reduce_2 add parses — bare opcode as macro argument.""" 101 + source = """ 102 + @system pe=1, sm=1 103 + #wrap op |> { 104 + &n <| ${op} 105 + } 106 + #wrap add 107 + """ 108 + graph = parse_and_lower(source) 109 + assert not graph.errors 110 + assert len(graph.macro_calls) == 1 111 + call = graph.macro_calls[0] 112 + assert call.positional_args == ("add",) 113 + 114 + def test_multiple_opcode_args(self): 115 + """Multiple opcodes can be passed as arguments.""" 116 + source = """ 117 + @system pe=1, sm=1 118 + #pair op1, op2 |> { 119 + &a <| ${op1} 120 + &b <| ${op2} 121 + } 122 + #pair add, sub 123 + """ 124 + graph = parse_and_lower(source) 125 + assert not graph.errors 126 + call = graph.macro_calls[0] 127 + assert call.positional_args == ("add", "sub") 128 + 129 + 130 + class TestE14_ExpandResolvesOpcodeParamRef: 131 + """E1.4: Expand pass resolves opcode ParamRef.""" 132 + 133 + def test_resolve_arith_opcode(self): 134 + """Opcode param 'add' resolves to ArithOp.ADD.""" 135 + source = """ 136 + @system pe=1, sm=1 137 + #wrap op |> { 138 + &n <| ${op} 139 + } 140 + #wrap add 141 + """ 142 + graph = parse_lower_expand(source) 143 + assert not graph.errors 144 + node = list(graph.nodes.values())[0] 145 + assert node.opcode == ArithOp.ADD 146 + 147 + def test_resolve_routing_opcode(self): 148 + """Opcode param 'gate' resolves to RoutingOp.GATE.""" 149 + source = """ 150 + @system pe=1, sm=1 151 + #wrap op |> { 152 + &n <| ${op} 153 + } 154 + #wrap gate 155 + """ 156 + graph = parse_lower_expand(source) 157 + assert not graph.errors 158 + node = list(graph.nodes.values())[0] 159 + assert node.opcode == RoutingOp.GATE 160 + 161 + def test_resolve_mem_opcode(self): 162 + """Opcode param 'read' resolves to MemOp.READ.""" 163 + source = """ 164 + @system pe=1, sm=1 165 + #wrap op |> { 166 + &n <| ${op} 167 + } 168 + #wrap read 169 + """ 170 + graph = parse_lower_expand(source) 171 + assert not graph.errors 172 + node = list(graph.nodes.values())[0] 173 + assert node.opcode == MemOp.READ 174 + 175 + def test_invalid_opcode_mnemonic_error(self): 176 + """Invalid mnemonic produces MACRO error. 177 + 178 + Note: 'banana' lexes as IDENT and parses as a qualified_ref (label_ref &banana), 179 + so we pass it as a qualified_ref dict. The expand pass gets a dict, not a string, 180 + which produces the 'must resolve to an opcode mnemonic' error. 181 + """ 182 + source = """ 183 + @system pe=1, sm=1 184 + #wrap op |> { 185 + &n <| ${op} 186 + } 187 + #wrap &banana 188 + """ 189 + graph = parse_lower_expand(source) 190 + macro_errors = [e for e in graph.errors if e.category == ErrorCategory.MACRO] 191 + assert len(macro_errors) >= 1 192 + assert "opcode mnemonic" in macro_errors[0].message 193 + 194 + def test_numeric_opcode_error(self): 195 + """Numeric value as opcode produces MACRO error.""" 196 + source = """ 197 + @system pe=1, sm=1 198 + #wrap op |> { 199 + &n <| ${op} 200 + } 201 + #wrap 42 202 + """ 203 + graph = parse_lower_expand(source) 204 + macro_errors = [e for e in graph.errors if e.category == ErrorCategory.MACRO] 205 + assert len(macro_errors) >= 1 206 + 207 + 208 + class TestE15_FullPipelineOpcodeParams: 209 + """E1.5: Full pipeline with opcode params.""" 210 + 211 + def test_full_pipeline_opcode_param(self): 212 + """Opcode-parameterized macro assembles through full pipeline.""" 213 + source = """ 214 + @system pe=1, sm=1 215 + #wrap op |> { 216 + &n <| ${op} 217 + } 218 + &seed <| const, 5 219 + #wrap add 220 + &seed |> #wrap_0.&n:L 221 + &seed |> #wrap_0.&n:R 222 + """ 223 + result = assemble(source) 224 + assert result is not None 225 + # Should have at least one PE config 226 + assert len(result.pe_configs) >= 1 227 + 228 + def test_full_pipeline_reduce_pattern(self): 229 + """Reduction tree pattern with opcode param.""" 230 + source = """ 231 + @system pe=1, sm=1 232 + #reduce_2 op |> { 233 + &r <| ${op} 234 + } 235 + &a <| const, 3 236 + &b <| const, 7 237 + #reduce_2 add 238 + &a |> #reduce_2_0.&r:L 239 + &b |> #reduce_2_0.&r:R 240 + """ 241 + result = assemble(source) 242 + assert result is not None