From edead19d60311b4fbe6889d95a6a5caf6a137235 Mon Sep 17 00:00:00 2001 From: Johnny Wan <2695191695@qq.com> Date: Fri, 30 Jan 2026 18:41:53 +0000 Subject: [PATCH 1/2] refactor: uncomment file-based communication logic in main function for clarity and future implementation --- evaluation_function/main.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/evaluation_function/main.py b/evaluation_function/main.py index 2e170cb..ae972f4 100644 --- a/evaluation_function/main.py +++ b/evaluation_function/main.py @@ -105,16 +105,16 @@ def main(): - If 2+ args provided: File-based communication (last 2 args are input/output paths) - Otherwise: RPC/IPC server mode using lf_toolkit """ - # # Check for file-based communication - # # shimmy passes input and output file paths as the last two arguments - # if len(sys.argv) >= 3: - # input_path = sys.argv[-2] - # output_path = sys.argv[-1] + # Check for file-based communication + # shimmy passes input and output file paths as the last two arguments + if len(sys.argv) >= 3: + input_path = sys.argv[-2] + output_path = sys.argv[-1] - # # Verify they look like file paths (basic check) - # if not input_path.startswith('-') and not output_path.startswith('-'): - # handle_file_based_communication(input_path, output_path) - # return + # Verify they look like file paths (basic check) + if not input_path.startswith('-') and not output_path.startswith('-'): + handle_file_based_communication(input_path, output_path) + return # Fall back to RPC/IPC server mode server = create_server() From 6b0112c088b1c55db8c134f9d74dedabd2a057bd Mon Sep 17 00:00:00 2001 From: Johnny Wan <2695191695@qq.com> Date: Fri, 13 Feb 2026 02:18:52 +0000 Subject: [PATCH 2/2] feat: add comprehensive tests for epsilon transitions in FSA validation; enhance epsilon handling in validation logic --- evaluation_function/test/test_correction.py | 116 ++++++++++++ evaluation_function/test/test_validation.py | 177 +++++++++++++++++++ evaluation_function/validation/validation.py | 51 +++++- 3 files changed, 336 insertions(+), 8 deletions(-) diff --git a/evaluation_function/test/test_correction.py b/evaluation_function/test/test_correction.py index 109ce53..f9359a7 100644 --- a/evaluation_function/test/test_correction.py +++ b/evaluation_function/test/test_correction.py @@ -196,5 +196,121 @@ def test_non_minimal_fsa_fails_when_required(self, equivalent_dfa): assert result.fsa_feedback is not None +# ============================================================================= +# Test Epsilon Transitions (End-to-End) +# ============================================================================= + +class TestEpsilonTransitionCorrection: + """Test the full correction pipeline with ε-NFA inputs.""" + + def test_epsilon_nfa_vs_equivalent_dfa_correct(self): + """ε-NFA student answer equivalent to DFA expected should be correct.""" + # ε-NFA accepts exactly "a": q0 --ε--> q1 --a--> q2 + student_enfa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + # DFA accepts exactly "a": s0 --a--> s1 + expected_dfa = make_fsa( + states=["s0", "s1"], + alphabet=["a"], + transitions=[ + {"from_state": "s0", "to_state": "s1", "symbol": "a"}, + ], + initial="s0", + accept=["s1"], + ) + result = analyze_fsa_correction(student_enfa, expected_dfa) + assert isinstance(result, Result) + assert result.is_correct is True + + def test_epsilon_nfa_vs_different_dfa_incorrect(self): + """ε-NFA accepting 'a' vs DFA accepting 'b' should be incorrect.""" + student_enfa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a", "b"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + expected_dfa = make_fsa( + states=["s0", "s1"], + alphabet=["a", "b"], + transitions=[ + {"from_state": "s0", "to_state": "s1", "symbol": "b"}, + ], + initial="s0", + accept=["s1"], + ) + result = analyze_fsa_correction(student_enfa, expected_dfa) + assert isinstance(result, Result) + assert result.is_correct is False + assert result.fsa_feedback is not None + assert len(result.fsa_feedback.errors) > 0 + + def test_multi_epsilon_nfa_vs_dfa_correct(self): + """ε-NFA for (a|b) with branching epsilons should match equivalent DFA.""" + student_enfa = make_fsa( + states=["q0", "q1", "q2", "q3"], + alphabet=["a", "b"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q0", "to_state": "q2", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q3", "symbol": "a"}, + {"from_state": "q2", "to_state": "q3", "symbol": "b"}, + ], + initial="q0", + accept=["q3"], + ) + expected_dfa = make_fsa( + states=["s0", "s1"], + alphabet=["a", "b"], + transitions=[ + {"from_state": "s0", "to_state": "s1", "symbol": "a"}, + {"from_state": "s0", "to_state": "s1", "symbol": "b"}, + ], + initial="s0", + accept=["s1"], + ) + result = analyze_fsa_correction(student_enfa, expected_dfa) + assert isinstance(result, Result) + assert result.is_correct is True + + def test_epsilon_nfa_structural_info_reports_nondeterministic(self): + """ε-NFA should have structural info reporting non-deterministic.""" + student_enfa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + expected_dfa = make_fsa( + states=["s0", "s1"], + alphabet=["a"], + transitions=[ + {"from_state": "s0", "to_state": "s1", "symbol": "a"}, + ], + initial="s0", + accept=["s1"], + ) + result = analyze_fsa_correction(student_enfa, expected_dfa) + assert result.fsa_feedback is not None + assert result.fsa_feedback.structural is not None + assert result.fsa_feedback.structural.is_deterministic is False + + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/evaluation_function/test/test_validation.py b/evaluation_function/test/test_validation.py index a30b1eb..e1c17bf 100644 --- a/evaluation_function/test/test_validation.py +++ b/evaluation_function/test/test_validation.py @@ -314,5 +314,182 @@ def test_isomorphic_dfas(self): assert are_isomorphic(fsa_user, fsa_sol) == [] +class TestEpsilonTransitions: + """Tests for epsilon transition handling across the validation pipeline.""" + + def test_valid_fsa_with_epsilon_unicode(self): + """ε-NFA with Unicode ε should pass structural validation.""" + fsa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + assert is_valid_fsa(fsa) == [] + + def test_valid_fsa_with_epsilon_string(self): + """ε-NFA with 'epsilon' string should pass structural validation.""" + fsa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "epsilon"}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + assert is_valid_fsa(fsa) == [] + + def test_valid_fsa_with_empty_string_epsilon(self): + """ε-NFA with empty string epsilon should pass structural validation.""" + fsa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": ""}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + assert is_valid_fsa(fsa) == [] + + def test_epsilon_nfa_is_not_deterministic(self): + """ε-NFA should be flagged as non-deterministic.""" + fsa = make_fsa( + states=["q0", "q1"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + ], + initial="q0", + accept=["q1"], + ) + errors = is_deterministic(fsa) + assert len(errors) > 0 + assert ErrorCode.NOT_DETERMINISTIC in [e.code for e in errors] + + def test_accepts_string_via_epsilon_closure(self): + """ε-NFA should accept 'a' by following q0 --ε--> q1 --a--> q2.""" + fsa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + assert accepts_string(fsa, "a") == [] + + def test_rejects_string_with_epsilon_nfa(self): + """ε-NFA that accepts 'a' should reject empty string.""" + fsa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + errors = accepts_string(fsa, "") + assert len(errors) > 0 + + def test_accepts_empty_string_via_epsilon(self): + """ε-NFA should accept empty string when initial reaches accept via ε.""" + fsa = make_fsa( + states=["q0", "q1"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + ], + initial="q0", + accept=["q1"], + ) + assert accepts_string(fsa, "") == [] + + def test_epsilon_nfa_equivalent_to_dfa(self): + """ε-NFA and DFA accepting the same language should be equivalent.""" + enfa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + dfa = make_fsa( + states=["s0", "s1"], + alphabet=["a"], + transitions=[ + {"from_state": "s0", "to_state": "s1", "symbol": "a"}, + ], + initial="s0", + accept=["s1"], + ) + assert fsas_accept_same_language(enfa, dfa) == [] + + def test_epsilon_nfa_not_equivalent_to_different_dfa(self): + """ε-NFA and DFA accepting different languages should not be equivalent.""" + enfa = make_fsa( + states=["q0", "q1", "q2"], + alphabet=["a", "b"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q2", "symbol": "a"}, + ], + initial="q0", + accept=["q2"], + ) + dfa = make_fsa( + states=["s0", "s1"], + alphabet=["a", "b"], + transitions=[ + {"from_state": "s0", "to_state": "s1", "symbol": "b"}, + ], + initial="s0", + accept=["s1"], + ) + errors = fsas_accept_same_language(enfa, dfa) + assert len(errors) > 0 + + def test_multi_epsilon_nfa_equivalent_to_dfa(self): + """ε-NFA for (a|b) with branching epsilons should match equivalent DFA.""" + # q0 --ε--> q1, q0 --ε--> q2, q1 --a--> q3, q2 --b--> q3 + enfa = make_fsa( + states=["q0", "q1", "q2", "q3"], + alphabet=["a", "b"], + transitions=[ + {"from_state": "q0", "to_state": "q1", "symbol": "ε"}, + {"from_state": "q0", "to_state": "q2", "symbol": "ε"}, + {"from_state": "q1", "to_state": "q3", "symbol": "a"}, + {"from_state": "q2", "to_state": "q3", "symbol": "b"}, + ], + initial="q0", + accept=["q3"], + ) + dfa = make_fsa( + states=["s0", "s1"], + alphabet=["a", "b"], + transitions=[ + {"from_state": "s0", "to_state": "s1", "symbol": "a"}, + {"from_state": "s0", "to_state": "s1", "symbol": "b"}, + ], + initial="s0", + accept=["s1"], + ) + assert fsas_accept_same_language(enfa, dfa) == [] + + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/evaluation_function/validation/validation.py b/evaluation_function/validation/validation.py index bd8b6d8..ee37bc5 100644 --- a/evaluation_function/validation/validation.py +++ b/evaluation_function/validation/validation.py @@ -3,6 +3,8 @@ from evaluation_function.schemas.result import StructuralInfo from ..algorithms.minimization import hopcroft_minimization +from ..algorithms.nfa_to_dfa import nfa_to_dfa, is_deterministic as is_dfa_check +from ..algorithms.epsilon_closure import epsilon_closure_set, build_epsilon_transition_map from ..schemas import FSA, ValidationError, ErrorCode, ElementHighlight @@ -102,7 +104,7 @@ def is_valid_fsa(fsa: FSA) -> List[ValidationError]: suggestion=f"Add '{t.to_state}' to your states, or update this transition to go to an existing state" ) ) - if t.symbol not in alphabet: + if t.symbol not in alphabet and t.symbol not in ("ε", "epsilon", ""): errors.append( ValidationError( message=f"The symbol '{t.symbol}' in this transition isn't in your alphabet. Transitions can only use symbols from the alphabet.", @@ -134,6 +136,26 @@ def is_deterministic(fsa: FSA) -> List[ValidationError]: if structural_errors: return structural_errors + # Check for epsilon transitions (makes FSA non-deterministic) + for t in fsa.transitions: + if t.symbol in ("ε", "epsilon", ""): + errors.append( + ValidationError( + message=f"Your FSA has an epsilon (ε) transition from '{t.from_state}' to '{t.to_state}'. A DFA cannot have epsilon transitions.", + code=ErrorCode.NOT_DETERMINISTIC, + severity="error", + highlight=ElementHighlight( + type="transition", + from_state=t.from_state, + to_state=t.to_state, + symbol=t.symbol + ), + suggestion="Remove epsilon transitions to make this a DFA, or note that your FSA is an NFA/ε-NFA, which is also valid!" + ) + ) + if errors: + return errors + for t in fsa.transitions: key = (t.from_state, t.symbol) if key in seen: @@ -152,7 +174,7 @@ def is_deterministic(fsa: FSA) -> List[ValidationError]: ) ) seen.add(key) - + return errors @@ -299,15 +321,19 @@ def find_dead_states(fsa: FSA) -> List[ValidationError]: def accepts_string(fsa: FSA, string: str) -> List[ValidationError]: """ - Simulate the FSA on a string. + Simulate the FSA on a string, with full ε-transition support. Returns [] if accepted, else a ValidationError. """ # First check if FSA is structurally valid structural_errors = is_valid_fsa(fsa) if structural_errors: return structural_errors - - current_states: Set[str] = {fsa.initial_state} + + # Build epsilon transition map for ε-closure computation + epsilon_trans = build_epsilon_transition_map(fsa.transitions) + + # Start with ε-closure of the initial state + current_states: Set[str] = epsilon_closure_set({fsa.initial_state}, epsilon_trans) for symbol in string: # Check if symbol is in alphabet @@ -319,17 +345,20 @@ def accepts_string(fsa: FSA, string: str) -> List[ValidationError]: severity="error" ) ] - + next_states = set() for state in current_states: for t in fsa.transitions: if t.from_state == state and t.symbol == symbol: next_states.add(t.to_state) - current_states = next_states + + # Compute ε-closure of the states reached after reading the symbol + current_states = epsilon_closure_set(next_states, epsilon_trans) + if not current_states: return [ ValidationError( - message=f"String '{string}' rejected: no transition from state(s) {current_states} on symbol '{symbol}'", + message=f"String '{string}' rejected: no transition on symbol '{symbol}'", code=ErrorCode.TEST_CASE_FAILED, severity="error" ) @@ -370,6 +399,12 @@ def fsas_accept_same_string(fsa1: FSA, fsa2: FSA, string: str) -> List[Validatio def fsas_accept_same_language(fsa1: FSA, fsa2: FSA) -> List[ValidationError]: + # Convert NFA/ε-NFA to DFA before minimization (Hopcroft requires DFA input) + if not is_dfa_check(fsa1): + fsa1 = nfa_to_dfa(fsa1) + if not is_dfa_check(fsa2): + fsa2 = nfa_to_dfa(fsa2) + fsa1_min = hopcroft_minimization(fsa1) fsa2_min = hopcroft_minimization(fsa2) return are_isomorphic(fsa1_min, fsa2_min)