Something went wrong. Try again.
OR-1 dataflow CPU sketch
Something went wrong. Try again.
45 kB · 1458 lines
Python
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459"""Tests for macro expansion pass (Phase 2).
Tests verify:- dfasm-macros.AC2.1: Scope-qualified node names (#name_N.&label)- dfasm-macros.AC2.2: Literal parameter substitution into const fields- dfasm-macros.AC2.3: Ref parameter substitution into edge endpoints- dfasm-macros.AC2.4: Nested macro expansion (recursive)- dfasm-macros.AC2.5: Macros inside functions (double-scoped)- dfasm-macros.AC2.6: Undefined macro → NAME error with suggestions- dfasm-macros.AC2.7: Wrong arity → ARITY error- dfasm-macros.AC2.8: Recursion depth limit → error- dfasm-macros.AC7.3: Unresolvable scope in expanded body → NAME error from resolve"""
import re
from lark import Larkfrom pathlib import Path
from asm.expand import expandfrom asm.lower import lowerfrom asm.resolve import resolvefrom asm.errors import ErrorCategoryfrom asm.ir import ( IRGraph, IRNode, IREdge, MacroDef, MacroParam, ParamRef, ConstExpr, SourceLoc, IRMacroCall)from cm_inst import ArithOp, Port
def _get_parser(): """Get the dfasm parser.""" grammar_path = Path(__file__).parent.parent / "dfasm.lark" return Lark( grammar_path.read_text(), parser="earley", propagate_positions=True, )
def parse_and_lower(source: str) -> IRGraph: """Parse source and lower to IRGraph (before expansion).""" parser = _get_parser() tree = parser.parse(source) return lower(tree)
def parse_lower_expand(source: str) -> IRGraph: """Parse, lower, and expand.""" graph = parse_and_lower(source) return expand(graph)
class TestAC21_ScopeQualification: """AC2.1: Nodes are scope-qualified with #macroname_N prefix."""
def test_simple_macro_invocation_creates_qualified_node(self): """Invoking #wrap creates node with qualified name.""" source = """ @system pe=1, sm=1
#wrap |> { &inner <| pass }
#wrap """ graph = parse_lower_expand(source)
# Look for qualified node name qualified_names = [n for n in graph.nodes.keys() if "#wrap_0.&inner" in n] assert len(qualified_names) == 1, f"Expected #wrap_0.&inner in {list(graph.nodes.keys())}"
def test_multiple_invocations_get_unique_scopes(self): """Multiple invocations get unique counter values.""" source = """ @system pe=1, sm=1
#simple |> { &node <| pass }
#simple #simple """ graph = parse_lower_expand(source)
# Should have both _0 and _1 scopes nodes = list(graph.nodes.keys()) has_0 = any("#simple_0" in n for n in nodes) has_1 = any("#simple_1" in n for n in nodes) assert has_0 and has_1, f"Expected unique scopes in {nodes}"
class TestAC22_LiteralSubstitution: """AC2.2: Literal parameters substitute into const fields.
Uses ${param} syntax in macro body to reference formal parameters in const positions. The grammar's param_ref rule parses ${IDENT} and the lowerer creates ParamRef objects, which the expand pass substitutes with actual argument values. """
def test_const_substitution_inline(self): """${param} in inline_const position substitutes the argument value.""" source = """ @system pe=1, sm=1
#with_const val |> { &node <| const ${val} }
#with_const 42 """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}"
expanded = [n for n in graph.nodes.values() if "#with_const_0" in n.name] assert len(expanded) == 1 assert expanded[0].const == 42
def test_const_substitution_comma_separated(self): """${param} in comma-separated argument position substitutes into const.""" source = """ @system pe=1, sm=1
#with_const val |> { &node <| const, ${val} }
#with_const 7 """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}"
expanded = [n for n in graph.nodes.values() if "#with_const_0" in n.name] assert len(expanded) == 1 assert expanded[0].const == 7
def test_const_substitution_with_hex(self): """${param} substitutes hex literal arguments correctly.""" source = """ @system pe=1, sm=1
#set_val v |> { &node <| const ${v} }
#set_val 0xFF """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}"
expanded = [n for n in graph.nodes.values() if "#set_val_0" in n.name] assert len(expanded) == 1 assert expanded[0].const == 255
class TestAC23_RefSubstitution: """AC2.3: Ref parameters substitute into edge endpoints.
Uses ${param} syntax in macro body edge endpoints to reference formal parameters. The lowerer creates ParamRef objects from ${IDENT} in qualified_ref positions, and the expand pass substitutes them with actual argument values (label/node references). """
def test_edge_dest_substitution(self): """${param} in edge dest position wires to the actual argument ref.""" source = """ @system pe=1, sm=1
&external <| pass
#connect_to target |> { &src <| const, 1 &src |> ${target}:L }
#connect_to &external """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}"
src_node = [n for n in graph.nodes.keys() if "#connect_to_0" in n and "&src" in n][0]
found = any( e.source == src_node and e.dest == "&external" for e in graph.edges ) assert found, f"Expected edge from {src_node} to &external, edges: {[(e.source, e.dest) for e in graph.edges]}"
def test_edge_source_substitution(self): """${param} in edge source position wires from the actual argument ref.""" source = """ @system pe=1, sm=1
&provider <| const, 5
#read_from src |> { &sink <| pass ${src} |> &sink:L }
#read_from &provider """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}"
sink_node = [n for n in graph.nodes.keys() if "#read_from_0" in n and "&sink" in n][0]
found = any( e.source == "&provider" and e.dest == sink_node for e in graph.edges ) assert found, f"Expected edge from &provider to {sink_node}, edges: {[(e.source, e.dest) for e in graph.edges]}"
def test_both_endpoints_substituted(self): """${param} in both source and dest positions substitutes correctly.""" source = """ @system pe=1, sm=1
&a <| const, 1 &b <| pass
#wire_between src, dest |> { ${src} |> ${dest}:L }
#wire_between &a, &b """ graph = parse_lower_expand(source) assert len(graph.errors) == 0, f"Unexpected errors: {graph.errors}"
found = any( e.source == "&a" and e.dest == "&b" for e in graph.edges ) assert found, f"Expected edge &a -> &b, edges: {[(e.source, e.dest) for e in graph.edges]}"
class TestAC24_NestedExpansion: """AC2.4: Nested macro calls expand recursively."""
def test_nested_macro_expansion(self): """Invoking outer which calls inner creates double-scoped nodes.""" source = """ @system pe=1, sm=1
#inner |> { &x <| pass }
#outer |> { #inner }
#outer """ graph = parse_lower_expand(source)
# Should have both #outer and #inner scopes node_names = list(graph.nodes.keys()) # Look for nodes qualified with #outer_N and #inner_M scopes outer_nodes = [n for n in node_names if "#outer_" in n] inner_nodes = [n for n in node_names if "#inner_" in n] assert len(outer_nodes) > 0, f"Expected #outer_ scoped nodes in {node_names}" assert len(inner_nodes) > 0, f"Expected #inner_ scoped nodes in {node_names}"
class TestAC25_FunctionScoping: """AC2.5: Macros inside functions get double-scoped."""
def test_macro_in_function_creates_double_scope(self): """Macro inside function gets $func.#macro_N.&label scope.""" source = """ @system pe=1, sm=1
#inject |> { &gate <| pass }
$func |> { #inject } """ graph = parse_lower_expand(source)
# Look for node with pattern $func.#inject_N.&gate # This will be in a function region body, not at top level node_names = list(graph.nodes.keys()) all_node_names = node_names.copy()
# Check region bodies too for region in graph.regions: all_node_names.extend(region.body.nodes.keys())
# Use regex to find pattern pattern = r'\$func\.#inject_\d+\.&gate' found = any(re.search(pattern, name) for name in all_node_names) assert found, f"Expected $func.#inject_N.&gate pattern in {all_node_names}"
class TestAC26_UndefinedMacro: """AC2.6: Undefined macro invocation → MACRO error with suggestions."""
def test_undefined_macro_produces_macro_error(self): """Invoking undefined macro produces MACRO error.""" source = """ @system pe=1, sm=1
#undefined_macro &a """ graph = parse_lower_expand(source)
# Should have an error assert len(graph.errors) > 0, "Expected error for undefined macro" error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO error, got {error.category}" assert "undefined" in error.message.lower(), f"Expected 'undefined' in message: {error.message}"
def test_undefined_macro_has_suggestions(self): """Undefined macro with similar name gets suggestions.""" source = """ @system pe=1, sm=1
#simple |> { &x <| pass }
#simpl """ graph = parse_lower_expand(source)
# Should have error with suggestions assert len(graph.errors) > 0 error = graph.errors[0] assert len(error.suggestions) > 0, f"Expected suggestions for typo, got {error.suggestions}"
class TestAC27_MacroArityError: """AC2.7: Wrong arity on macro invocation → MACRO error."""
def test_too_few_arguments(self): """Providing too few arguments produces MACRO error.""" source = """ @system pe=1, sm=1
#needs_two a, b |> { &x <| pass }
#needs_two &a """ graph = parse_lower_expand(source)
# Should have MACRO error assert len(graph.errors) > 0, "Expected error" error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" assert "2" in error.message and "1" in error.message, f"Expected counts in message: {error.message}"
def test_too_many_arguments(self): """Providing too many arguments produces MACRO error.""" source = """ @system pe=1, sm=1
#needs_one a |> { &x <| pass }
#needs_one &a, &b, &c """ graph = parse_lower_expand(source)
assert len(graph.errors) > 0 error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}"
class TestAC28_RecursionDepth: """AC2.8: Macro recursion exceeding depth limit → error."""
def test_infinite_recursion_caught(self): """Infinite recursion is caught at depth limit.""" source = """ @system pe=1, sm=1
#recursive |> { #recursive }
#recursive """ graph = parse_lower_expand(source)
# Should have error about depth assert len(graph.errors) > 0, "Expected error for infinite recursion" error = graph.errors[0] # The error message should mention depth or recursion msg = error.message.lower() assert ("depth" in msg or "recursion" in msg), f"Expected depth/recursion mention in: {error.message}"
class TestAC73_UnresolvableScope: """AC7.3: Unresolvable scope in expanded body → NAME error from resolve."""
def test_macro_with_unresolvable_scope_ref(self): """Macro body with unresolvable scope reference surfaces error at resolve time.""" source = """ @system pe=1, sm=1
#bad_ref |> { &node <| pass &node |> $nonexistent.&target:L }
#bad_ref """ # Expand alone may not error (if scope checking is in resolve) graph = parse_lower_expand(source) # Resolve to check scope validity graph = resolve(graph)
# Should have NAME error from resolve about nonexistent scope has_error = any( e.category == ErrorCategory.NAME for e in graph.errors ) assert has_error, f"Expected NAME error from resolve, got {[e.category for e in graph.errors]}"
class TestMacroDefAndCallCleanup: """Macro definitions and calls are removed from output."""
def test_no_macro_defs_in_output(self): """Output graph has no macro_defs.""" source = """ @system pe=1, sm=1
#simple |> { &x <| pass }
#simple """ graph = parse_lower_expand(source)
assert len(graph.macro_defs) == 0, "Expected macro_defs to be empty"
def test_no_macro_calls_in_output(self): """Output graph has no macro_calls.""" source = """ @system pe=1, sm=1
#simple |> { &x <| pass }
#simple """ graph = parse_lower_expand(source)
assert len(graph.macro_calls) == 0, "Expected macro_calls to be empty"
class TestMacroWithNoParams: """Macro with no parameters expands correctly."""
def test_parameterless_macro(self): """Macro without params can be invoked without args.""" source = """ @system pe=1, sm=1
#identity |> { &x <| pass }
#identity """ graph = parse_lower_expand(source)
# Should expand without errors assert len(graph.errors) == 0, f"Expected no errors, got {graph.errors}" # Should have qualified node names = list(graph.nodes.keys()) assert any("#identity_0" in n for n in names), f"Expected #identity_0 in {names}"
class TestMultipleMacroDefinitions: """Multiple macros can be defined and invoked in same program."""
def test_multiple_macros(self): """Define and invoke multiple different macros.""" source = """ @system pe=1, sm=1
#macro_a |> { &a <| pass }
#macro_b |> { &b <| pass }
#macro_a #macro_b """ graph = parse_lower_expand(source)
# Should have both expanded nodes names = list(graph.nodes.keys()) has_a = any("#macro_a_" in n for n in names) has_b = any("#macro_b_" in n for n in names) assert has_a and has_b, f"Expected both macros in {names}"
class TestExpansionCounterIncrement: """Expansion counter increments per invocation."""
def test_counter_increments_across_invocations(self): """Same macro invoked twice gets _0 and _1.""" source = """ @system pe=1, sm=1
#dup |> { &node <| pass }
#dup #dup """ graph = parse_lower_expand(source)
names = list(graph.nodes.keys()) has_0 = any("#dup_0" in n for n in names) has_1 = any("#dup_1" in n for n in names) assert has_0 and has_1, f"Expected _0 and _1 in {names}"
class TestMacroErrorPropagation: """Errors in macro body template surface at expansion time."""
def test_macro_body_with_unknown_opcode_surfaces_error(self): """Errors from macro body lowering surface in expanded graph.
Note: In Phase 2, grammar validation prevents invalid opcodes from appearing in macro bodies. This test is deferred to Phase 3 when validation of parameter-referenced opcodes becomes possible. """ # Grammar ensures only valid opcodes can appear in macro bodies # This test will be more relevant in Phase 3 with token pasting source = """ @system pe=1, sm=1
#valid_macro |> { &node <| pass }
#valid_macro """ graph = parse_lower_expand(source) # Valid macros should have no errors (syntax validation is in lower pass) assert len(graph.errors) == 0
class TestAC31_TokenPasting: """AC3.1: ParamRef with prefix/suffix concatenates into label names.
Tests construct IR directly (not from parsing) to test expand in isolation. """
def test_token_paste_with_prefix_and_suffix(self): """Token pasting with both prefix and suffix creates concatenated name.""" # Create macro body with ParamRef containing prefix/suffix param_ref = ParamRef(param="func", prefix="&__", suffix="_fan") body_node = IRNode( name=param_ref, # This will be a ParamRef in the node name opcode=ArithOp.ADD, loc=SourceLoc(0, 0), )
macro_body = IRGraph( nodes={"node_placeholder": body_node}, edges=[], macro_defs=[], macro_calls=[], )
# Create macro definition macro_def = MacroDef( name="make_fan", params=(MacroParam(name="func"),), body=macro_body, loc=SourceLoc(0, 0), )
# Create a graph with the macro and a call to it # The macro call with argument "fib" macro_call = IRMacroCall( name="make_fan", positional_args=("fib",), named_args=(), loc=SourceLoc(0, 0), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
# Expand the graph expanded = expand(graph)
# After expansion, the node should have name: #macro_N.&__fib_fan node_names = list(expanded.nodes.keys()) # Look for the pasted name pattern found = any("&__fib_fan" in name for name in node_names) assert found, f"Expected node with &__fib_fan in name, got {node_names}"
def test_token_paste_with_prefix_only(self): """Token pasting with prefix only concatenates correctly.""" param_ref = ParamRef(param="x", prefix="&pre_", suffix="") body_node = IRNode( name=param_ref, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), )
macro_body = IRGraph( nodes={"node_placeholder": body_node}, edges=[], macro_defs=[], macro_calls=[], )
macro_def = MacroDef( name="prefix_test", params=(MacroParam(name="x"),), body=macro_body, loc=SourceLoc(0, 0), )
macro_call = IRMacroCall( name="prefix_test", positional_args=("val",), named_args=(), loc=SourceLoc(0, 0), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
# Should have node with &pre_val node_names = list(expanded.nodes.keys()) found = any("&pre_val" in name for name in node_names) assert found, f"Expected &pre_val in {node_names}"
def test_token_paste_with_suffix_only(self): """Token pasting with suffix only concatenates correctly.""" param_ref = ParamRef(param="x", prefix="", suffix="_post") body_node = IRNode( name=param_ref, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), )
macro_body = IRGraph( nodes={"node_placeholder": body_node}, edges=[], macro_defs=[], macro_calls=[], )
macro_def = MacroDef( name="suffix_test", params=(MacroParam(name="x"),), body=macro_body, loc=SourceLoc(0, 0), )
macro_call = IRMacroCall( name="suffix_test", positional_args=("val",), named_args=(), loc=SourceLoc(0, 0), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
# Should have node with val_post node_names = list(expanded.nodes.keys()) found = any("val_post" in name for name in node_names) assert found, f"Expected val_post in {node_names}"
def test_token_paste_in_edge_source(self): """Token pasting in edge source reference resolves correctly.""" # Create two nodes: one with ParamRef source name, one regular dest param_ref_src = ParamRef(param="src", prefix="&", suffix="") regular_dest = "&dest"
body_nodes = { "src_placeholder": IRNode( name=param_ref_src, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ), "&dest": IRNode( name=regular_dest, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ), }
# Edge connects the pasted source to the destination body_edges = [ IREdge( source=param_ref_src, dest=regular_dest, port=Port.L, loc=SourceLoc(0, 0), ) ]
macro_body = IRGraph( nodes=body_nodes, edges=body_edges, macro_defs=[], macro_calls=[], )
macro_def = MacroDef( name="edge_src_test", params=(MacroParam(name="src"),), body=macro_body, loc=SourceLoc(0, 0), )
macro_call = IRMacroCall( name="edge_src_test", positional_args=("input",), named_args=(), loc=SourceLoc(0, 0), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
# Check that edges have the resolved pasted names edges = expanded.edges # Find the edge, both source and dest should be qualified with #edge_src_test_0.&name found = False for edge in edges: if "&input" in edge.source and "&dest" in edge.dest: found = True break assert found, f"Expected edge with pasted &input source, got {[(e.source, e.dest) for e in edges]}"
def test_token_paste_in_edge_dest(self): """Token pasting in edge dest reference resolves correctly.""" param_ref_dest = ParamRef(param="dest", prefix="&", suffix="") regular_src = "&src"
body_nodes = { "&src": IRNode( name=regular_src, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ), "dest_placeholder": IRNode( name=param_ref_dest, opcode=ArithOp.ADD, loc=SourceLoc(0, 0), ), }
# Edge connects source to the pasted destination body_edges = [ IREdge( source=regular_src, dest=param_ref_dest, port=Port.L, loc=SourceLoc(0, 0), ) ]
macro_body = IRGraph( nodes=body_nodes, edges=body_edges, macro_defs=[], macro_calls=[], )
macro_def = MacroDef( name="edge_dest_test", params=(MacroParam(name="dest"),), body=macro_body, loc=SourceLoc(0, 0), )
macro_call = IRMacroCall( name="edge_dest_test", positional_args=("output",), named_args=(), loc=SourceLoc(0, 0), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
# Check that edges have the resolved pasted names edges = expanded.edges found = False for edge in edges: if "&src" in edge.source and "&output" in edge.dest: found = True break assert found, f"Expected edge with pasted &output dest, got {[(e.source, e.dest) for e in edges]}"
class TestAC32_ConstantExpressionEvaluation: """AC3.2: Constant expressions evaluate during macro expansion."""
def test_simple_param_substitution_in_const(self): """ParamRef(param="val") + arg=42 -> const=42.""" # Create macro with ParamRef in const field node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ParamRef(param="val"), ) macro_def = MacroDef( name="simple_const", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), )
macro_call = IRMacroCall( name="simple_const", positional_args=(42,), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
# Find the expanded node nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 42 for _, const in nodes_with_const), \ f"Expected const=42, got {nodes_with_const}"
def test_const_expr_addition_with_one_param(self): """ConstExpr('val + 1') with val=5 evaluates to 6.""" # Create macro with ConstExpr in const field node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val + 1", params=("val",), ), ) macro_def = MacroDef( name="add_one", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), )
macro_call = IRMacroCall( name="add_one", positional_args=(5,), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
# Verify no errors assert not expanded.errors, f"Expected no errors, got {expanded.errors}"
# Find the expanded node and verify const nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 6 for _, const in nodes_with_const), \ f"Expected const=6, got {nodes_with_const}"
def test_const_expr_subtraction(self): """ConstExpr('val - 1') with val=10 evaluates to 9.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val - 1", params=("val",), ), ) macro_def = MacroDef( name="sub_one", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), )
macro_call = IRMacroCall( name="sub_one", positional_args=(10,), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
assert not expanded.errors, f"Expected no errors, got {expanded.errors}"
nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 9 for _, const in nodes_with_const), \ f"Expected const=9, got {nodes_with_const}"
def test_const_expr_multiplication(self): """ConstExpr('val * 2') with val=4 evaluates to 8.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val * 2", params=("val",), ), ) macro_def = MacroDef( name="double", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), )
macro_call = IRMacroCall( name="double", positional_args=(4,), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
assert not expanded.errors, f"Expected no errors, got {expanded.errors}"
nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 8 for _, const in nodes_with_const), \ f"Expected const=8, got {nodes_with_const}"
def test_const_expr_multiple_params(self): """ConstExpr('a + b') with a=3, b=7 evaluates to 10.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="a + b", params=("a", "b"), ), ) macro_def = MacroDef( name="add_two", params=(MacroParam(name="a"), MacroParam(name="b")), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), )
macro_call = IRMacroCall( name="add_two", positional_args=(3, 7), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
assert not expanded.errors, f"Expected no errors, got {expanded.errors}"
nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 10 for _, const in nodes_with_const), \ f"Expected const=10, got {nodes_with_const}"
def test_const_expr_with_literal(self): """ConstExpr('5 + val') with val=2 evaluates to 7.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="5 + val", params=("val",), ), ) macro_def = MacroDef( name="literal_plus", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), )
macro_call = IRMacroCall( name="literal_plus", positional_args=(2,), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
assert not expanded.errors, f"Expected no errors, got {expanded.errors}"
nodes_with_const = [ (name, node.const) for name, node in expanded.nodes.items() if node.const is not None ] assert any(const == 7 for _, const in nodes_with_const), \ f"Expected const=7, got {nodes_with_const}"
class TestAC33_ConstExprNonNumericValues: """AC3.3: Non-numeric values in arithmetic context produce VALUE error."""
def test_non_numeric_param_in_arithmetic(self): """ParamRef in arithmetic context with non-int arg -> VALUE error.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val + 1", params=("val",), ), ) macro_def = MacroDef( name="arith_with_ref", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), )
# Pass a reference name (&label) instead of an integer macro_call = IRMacroCall( name="arith_with_ref", positional_args=("&label",), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
# Verify we have an error assert expanded.errors, "Expected VALUE error for non-numeric in arithmetic" assert any( e.category == ErrorCategory.VALUE for e in expanded.errors ), f"Expected VALUE error, got {[e.category for e in expanded.errors]}"
def test_undefined_param_in_arithmetic(self): """Undefined parameter in arithmetic expression -> VALUE error.""" node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="undefined_param + 1", params=("undefined_param",), ), ) macro_def = MacroDef( name="undefined", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), )
macro_call = IRMacroCall( name="undefined", positional_args=(5,), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
# Verify we have an error assert expanded.errors, "Expected VALUE error for undefined parameter" assert any( e.category == ErrorCategory.VALUE for e in expanded.errors ), f"Expected VALUE error, got {[e.category for e in expanded.errors]}"
class TestSourceLocationThreading: """Verify that errors during macro expansion include source location context."""
def test_expansion_error_includes_macro_location(self): """Non-numeric param in arithmetic triggers VALUE error with expansion context.""" # Create a macro that performs arithmetic on a parameter node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val + 1", params=("val",), loc=SourceLoc(10, 5), ), ) macro_def = MacroDef( name="arith_test", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), loc=SourceLoc(5, 0), )
# Create a macro call at a specific location that passes a non-numeric value macro_call = IRMacroCall( name="arith_test", positional_args=("&label",), # Non-numeric value loc=SourceLoc(20, 10), # Call location )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[macro_def], macro_calls=[macro_call], )
expanded = expand(graph)
# Verify we have a VALUE error assert expanded.errors, "Expected VALUE error" value_error = next( (e for e in expanded.errors if e.category == ErrorCategory.VALUE), None ) assert value_error is not None, "Expected VALUE error in errors list"
# Verify the error has context_lines with expansion context assert value_error.context_lines, "Expected context_lines with expansion context" assert any( "expanded from" in ctx and "arith_test" in ctx for ctx in value_error.context_lines ), f"Expected 'expanded from #arith_test' in context_lines, got {value_error.context_lines}"
# Verify the context line contains the call location expansion_ctx = next( (c for c in value_error.context_lines if "expanded from" in c), None ) assert expansion_ctx is not None assert "20" in expansion_ctx and "10" in expansion_ctx, \ f"Expected call location (20, 10) in context_line: {expansion_ctx}"
class TestErrorMessageQuality: """Error messages are informative and include appropriate context."""
def test_undefined_macro_has_macro_category_and_suggestions(self): """Undefined macro error includes category, name, and suggestions.""" source = """ @system pe=1, sm=1
#simple |> { &x <| pass }
#simpler """ graph = parse_lower_expand(source)
assert len(graph.errors) > 0, "Expected error for undefined macro" error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" assert "simpler" in error.message, f"Expected macro name in message: {error.message}" assert len(error.suggestions) > 0, f"Expected suggestions, got {error.suggestions}" assert any("simple" in s for s in error.suggestions), \ f"Expected 'simple' in suggestions: {error.suggestions}"
def test_arity_mismatch_includes_counts(self): """Arity mismatch error message includes expected and actual counts.""" source = """ @system pe=1, sm=1
#needs_three a, b, c |> { &x <| pass }
#needs_three &x, &y """ graph = parse_lower_expand(source)
assert len(graph.errors) > 0 error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" assert "3" in error.message, f"Expected expected count in message: {error.message}" assert "2" in error.message, f"Expected actual count in message: {error.message}"
def test_depth_exceeded_names_recursive_macro(self): """Recursion depth limit error names the recursive macro.""" source = """ @system pe=1, sm=1
#recursive |> { #recursive }
#recursive """ graph = parse_lower_expand(source)
assert len(graph.errors) > 0 error = graph.errors[0] assert error.category == ErrorCategory.MACRO, f"Expected MACRO, got {error.category}" assert "recursive" in error.message.lower(), \ f"Expected 'recursive' in message: {error.message}" assert "depth" in error.message.lower() or "recursion" in error.message.lower(), \ f"Expected depth/recursion mention: {error.message}"
def test_nested_macro_expansion_error_has_context_lines(self): """Error in nested macro expansion includes context lines showing call chain.""" # Create inner macro with arithmetic on parameter inner_node = IRNode( name="&inner", opcode=ArithOp.ADD, const=ConstExpr( expression="val + 1", params=("val",), loc=SourceLoc(10, 5), ), ) inner_macro = MacroDef( name="inner_arith", params=(MacroParam(name="val"),), body=IRGraph( nodes={"&inner": inner_node}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[], ), loc=SourceLoc(5, 0), )
# Create outer macro that calls inner with non-numeric value inner_call = IRMacroCall( name="inner_arith", positional_args=("&label",), # Non-numeric value - will cause VALUE error loc=SourceLoc(15, 3), )
outer_macro = MacroDef( name="outer_wrapper", params=(), body=IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[], macro_calls=[inner_call], ), loc=SourceLoc(1, 0), )
# Invoke outer macro outer_call = IRMacroCall( name="outer_wrapper", positional_args=(), loc=SourceLoc(20, 0), )
graph = IRGraph( nodes={}, edges=[], regions=[], data_defs=[], macro_defs=[inner_macro, outer_macro], macro_calls=[outer_call], )
expanded = expand(graph)
# Verify VALUE error with context lines showing the call chain assert expanded.errors, f"Expected VALUE error, got {[e.message for e in expanded.errors]}" value_error = next( (e for e in expanded.errors if e.category == ErrorCategory.VALUE), None ) assert value_error is not None, f"Expected VALUE error, got {[e.category for e in expanded.errors]}"
# Verify context_lines include both inner and outer macro references assert len(value_error.context_lines) >= 1, \ f"Expected at least 1 context line for expansion, got {value_error.context_lines}" context_str = " ".join(value_error.context_lines) assert "inner_arith" in context_str, \ f"Expected 'inner_arith' in context: {value_error.context_lines}" assert "outer_wrapper" in context_str, \ f"Expected 'outer_wrapper' in context: {value_error.context_lines}"