"""Tests for the Resolve pass (name resolution in IRGraph). Tests verify: - Valid programs with all names resolved (AC4.1, AC4.2) - Undefined name references with "did you mean" suggestions (AC4.3) - Scope violations when cross-referencing function-local labels (AC4.4) - Levenshtein distance computation for suggestions (AC4.5) """ from tests.pipeline import parse_lower_resolve from asm.resolve import _levenshtein from asm.errors import ErrorCategory class TestValidResolution: """Tests for successful name resolution (AC4.1, AC4.2).""" def test_simple_two_node_edge_resolves(self, parser): """Simple program with two nodes and an edge between them resolves with no errors.""" graph = parse_lower_resolve( parser, """\ &a <| pass &b <| add &a |> &b:L """, ) # Should have no resolution errors name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] scope_errors = [e for e in graph.errors if e.category == ErrorCategory.SCOPE] assert len(name_errors) == 0, f"Unexpected NAME errors: {name_errors}" assert len(scope_errors) == 0, f"Unexpected SCOPE errors: {scope_errors}" def test_cross_function_wiring_via_global_nodes(self, parser): """Cross-function wiring via @nodes resolves correctly. Test that a global node can be wired to and from function-scoped labels. After lowering, these are stored as region.body edges with simple names, but when resolved, they reference the flattened qualified names. """ graph = parse_lower_resolve( parser, """\ @bridge <| pass $foo |> { &a <| pass &a |> @bridge:L } $bar |> { &b <| add @bridge |> &b:L } """, ) # Should have no resolution errors name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 0, f"Unexpected NAME errors: {name_errors}" def test_function_scoped_labels_within_same_function_resolve(self, parser): """Program with function-scoped labels and edges within same function resolve correctly.""" graph = parse_lower_resolve( parser, """\ $main |> { &input <| pass &process <| add &output <| pass &input |> &process:L &process |> &output:L } """, ) # Should have no resolution errors name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 0, f"Unexpected NAME errors: {name_errors}" def test_global_and_function_nodes_coexist(self, parser): """Program with both global @nodes and function-scoped &labels resolves correctly.""" graph = parse_lower_resolve( parser, """\ @global <| pass $worker |> { &local <| add &local |> @global:L } """, ) # Should have no resolution errors name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 0, f"Unexpected NAME errors: {name_errors}" class TestUndefinedReference: """Tests for undefined name errors with suggestions (AC4.3).""" def test_undefined_label_produces_name_error(self, parser): """Edge referencing undefined &nonexistent produces error with NAME category.""" graph = parse_lower_resolve( parser, """\ &a <| pass &b <| add &a |> &nonexistent:L """, ) # Should have a NAME error name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 1 error = name_errors[0] assert "undefined" in error.message.lower() def test_error_includes_source_location(self, parser): """NAME error includes source location (line/column).""" graph = parse_lower_resolve( parser, """\ &a <| pass &b <| add &a |> &nonexistent:L """, ) name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 1 error = name_errors[0] # Error should have a valid location assert error.loc.line > 0 assert error.loc.column >= 0 def test_suggestion_for_similar_name(self, parser): """Reference to &nonexistant suggests &nonexistent if it exists.""" graph = parse_lower_resolve( parser, """\ &nonexistent <| pass &a <| add &a |> &nonexistant:L """, ) name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 1 error = name_errors[0] # Should have suggestions assert len(error.suggestions) > 0 class TestScopeViolation: """Tests for scope violation errors (AC4.4).""" def test_cross_scope_reference_produces_scope_error(self, parser): """Reference to function-local label from top level produces SCOPE error.""" graph = parse_lower_resolve( parser, """\ $foo |> { &private <| pass } &top <| add &top |> &private:L """, ) # Should have a SCOPE error, not NAME scope_errors = [e for e in graph.errors if e.category == ErrorCategory.SCOPE] assert len(scope_errors) == 1 error = scope_errors[0] # Error should identify the function containing the label assert "$foo" in error.message or "function" in error.message.lower() def test_scope_error_mentions_actual_scope(self, parser): """Scope error message mentions the function where label is actually defined.""" graph = parse_lower_resolve( parser, """\ $foo |> { &private <| pass } $bar |> { &x <| add } &top <| pass &top |> &private:L """, ) scope_errors = [e for e in graph.errors if e.category == ErrorCategory.SCOPE] assert len(scope_errors) == 1 error = scope_errors[0] assert "$foo" in error.message def test_reference_from_different_function_is_scope_error(self, parser): """Reference to label in $foo from within $bar produces SCOPE error.""" graph = parse_lower_resolve( parser, """\ $foo |> { &data <| const, 42 } $bar |> { &use <| add &use |> &data:L } """, ) # Should have a SCOPE error scope_errors = [e for e in graph.errors if e.category == ErrorCategory.SCOPE] assert len(scope_errors) == 1 class TestLevenshteinSuggestions: """Tests for Levenshtein distance suggestions (AC4.5).""" def test_levenshtein_kitten_sitting(self): """Direct test: _levenshtein("kitten", "sitting") == 3.""" distance = _levenshtein("kitten", "sitting") assert distance == 3 def test_levenshtein_identical_strings(self): """_levenshtein identical strings returns 0.""" assert _levenshtein("test", "test") == 0 def test_levenshtein_empty_string(self): """_levenshtein empty string.""" assert _levenshtein("", "") == 0 assert _levenshtein("abc", "") == 3 assert _levenshtein("", "abc") == 3 def test_levenshtein_single_insertion(self): """_levenshtein single character insertion.""" assert _levenshtein("add", "addd") == 1 def test_levenshtein_single_deletion(self): """_levenshtein single character deletion.""" assert _levenshtein("addd", "add") == 1 def test_levenshtein_single_substitution(self): """_levenshtein single character substitution.""" assert _levenshtein("cat", "bat") == 1 def test_suggestion_for_typo_one_char(self, parser): """Reference to &ad suggests &add (distance 1).""" graph = parse_lower_resolve( parser, """\ &add <| pass &x <| pass &x |> &ad:L """, ) # Should have NAME error with suggestion name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 1 error = name_errors[0] assert len(error.suggestions) > 0 # Suggestion should include &add suggestion_text = " ".join(error.suggestions) assert "add" in suggestion_text def test_suggestion_for_typo_two_chars(self, parser): """Reference to &addd suggests &add (distance 1).""" graph = parse_lower_resolve( parser, """\ &add <| pass &x <| pass &x |> &addd:L """, ) name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 1 error = name_errors[0] assert len(error.suggestions) > 0 suggestion_text = " ".join(error.suggestions) assert "add" in suggestion_text def test_no_suggestion_for_completely_wrong_name(self, parser): """Reference to &completely_wrong with no similar names may have no suggestion.""" graph = parse_lower_resolve( parser, """\ &add <| pass &x <| pass &x |> &completely_wrong:L """, ) name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 1 # May or may not have suggestions depending on implementation # (best-effort or distance threshold) def test_multiple_errors_all_collected(self, parser): """Multiple undefined references all produce errors (error accumulation).""" graph = parse_lower_resolve( parser, """\ &a <| pass &b <| add &a |> &undef1:L &b |> &undef2:R """, ) name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] # Should have 2 NAME errors (both undefined refs) assert len(name_errors) == 2 class TestEdgeCases: """Edge case tests for resolution.""" def test_empty_program_resolves(self, parser): """Empty program resolves with no errors.""" graph = parse_lower_resolve(parser, "") assert len(graph.errors) == 0 def test_program_only_defs_no_edges_resolves(self, parser): """Program with only definitions and no edges resolves with no errors.""" graph = parse_lower_resolve( parser, """\ &a <| pass &b <| add &c <| sub """, ) name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 0 def test_circular_wiring_resolves(self, parser): """Circular wiring (feedback loops) resolves correctly.""" graph = parse_lower_resolve( parser, """\ &a <| pass &b <| add &a |> &b:L &b |> &a:R """, ) name_errors = [e for e in graph.errors if e.category == ErrorCategory.NAME] assert len(name_errors) == 0