Skip to content

Commit d9a2e73

Browse files
Merge pull request #48 from lambda-feedback/file-fix
File fix
2 parents 92332ce + b62805a commit d9a2e73

4 files changed

Lines changed: 409 additions & 68 deletions

File tree

evaluation_function/main.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,16 @@ def main():
105105
- If 2+ args provided: File-based communication (last 2 args are input/output paths)
106106
- Otherwise: RPC/IPC server mode using lf_toolkit
107107
"""
108-
# # Check for file-based communication
109-
# # shimmy passes input and output file paths as the last two arguments
110-
# if len(sys.argv) >= 3:
111-
# input_path = sys.argv[-2]
112-
# output_path = sys.argv[-1]
108+
# Check for file-based communication
109+
# shimmy passes input and output file paths as the last two arguments
110+
if len(sys.argv) >= 3:
111+
input_path = sys.argv[-2]
112+
output_path = sys.argv[-1]
113113

114-
# # Verify they look like file paths (basic check)
115-
# if not input_path.startswith('-') and not output_path.startswith('-'):
116-
# handle_file_based_communication(input_path, output_path)
117-
# return
114+
# Verify they look like file paths (basic check)
115+
if not input_path.startswith('-') and not output_path.startswith('-'):
116+
handle_file_based_communication(input_path, output_path)
117+
return
118118

119119
# Fall back to RPC/IPC server mode
120120
server = create_server()

evaluation_function/test/test_correction.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,5 +207,121 @@ def test_non_minimal_fsa_fails_when_required(self, equivalent_dfa):
207207
assert any(e.code == ErrorCode.NOT_MINIMAL for e in result.fsa_feedback.errors)
208208

209209

210+
# =============================================================================
211+
# Test Epsilon Transitions (End-to-End)
212+
# =============================================================================
213+
214+
class TestEpsilonTransitionCorrection:
215+
"""Test the full correction pipeline with ε-NFA inputs."""
216+
217+
def test_epsilon_nfa_vs_equivalent_dfa_correct(self):
218+
"""ε-NFA student answer equivalent to DFA expected should be correct."""
219+
# ε-NFA accepts exactly "a": q0 --ε--> q1 --a--> q2
220+
student_enfa = make_fsa(
221+
states=["q0", "q1", "q2"],
222+
alphabet=["a"],
223+
transitions=[
224+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
225+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
226+
],
227+
initial="q0",
228+
accept=["q2"],
229+
)
230+
# DFA accepts exactly "a": s0 --a--> s1
231+
expected_dfa = make_fsa(
232+
states=["s0", "s1"],
233+
alphabet=["a"],
234+
transitions=[
235+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
236+
],
237+
initial="s0",
238+
accept=["s1"],
239+
)
240+
result = analyze_fsa_correction(student_enfa, expected_dfa)
241+
assert isinstance(result, Result)
242+
assert result.is_correct is True
243+
244+
def test_epsilon_nfa_vs_different_dfa_incorrect(self):
245+
"""ε-NFA accepting 'a' vs DFA accepting 'b' should be incorrect."""
246+
student_enfa = make_fsa(
247+
states=["q0", "q1", "q2"],
248+
alphabet=["a", "b"],
249+
transitions=[
250+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
251+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
252+
],
253+
initial="q0",
254+
accept=["q2"],
255+
)
256+
expected_dfa = make_fsa(
257+
states=["s0", "s1"],
258+
alphabet=["a", "b"],
259+
transitions=[
260+
{"from_state": "s0", "to_state": "s1", "symbol": "b"},
261+
],
262+
initial="s0",
263+
accept=["s1"],
264+
)
265+
result = analyze_fsa_correction(student_enfa, expected_dfa)
266+
assert isinstance(result, Result)
267+
assert result.is_correct is False
268+
assert result.fsa_feedback is not None
269+
assert len(result.fsa_feedback.errors) > 0
270+
271+
def test_multi_epsilon_nfa_vs_dfa_correct(self):
272+
"""ε-NFA for (a|b) with branching epsilons should match equivalent DFA."""
273+
student_enfa = make_fsa(
274+
states=["q0", "q1", "q2", "q3"],
275+
alphabet=["a", "b"],
276+
transitions=[
277+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
278+
{"from_state": "q0", "to_state": "q2", "symbol": "ε"},
279+
{"from_state": "q1", "to_state": "q3", "symbol": "a"},
280+
{"from_state": "q2", "to_state": "q3", "symbol": "b"},
281+
],
282+
initial="q0",
283+
accept=["q3"],
284+
)
285+
expected_dfa = make_fsa(
286+
states=["s0", "s1"],
287+
alphabet=["a", "b"],
288+
transitions=[
289+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
290+
{"from_state": "s0", "to_state": "s1", "symbol": "b"},
291+
],
292+
initial="s0",
293+
accept=["s1"],
294+
)
295+
result = analyze_fsa_correction(student_enfa, expected_dfa)
296+
assert isinstance(result, Result)
297+
assert result.is_correct is True
298+
299+
def test_epsilon_nfa_structural_info_reports_nondeterministic(self):
300+
"""ε-NFA should have structural info reporting non-deterministic."""
301+
student_enfa = make_fsa(
302+
states=["q0", "q1", "q2"],
303+
alphabet=["a"],
304+
transitions=[
305+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
306+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
307+
],
308+
initial="q0",
309+
accept=["q2"],
310+
)
311+
expected_dfa = make_fsa(
312+
states=["s0", "s1"],
313+
alphabet=["a"],
314+
transitions=[
315+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
316+
],
317+
initial="s0",
318+
accept=["s1"],
319+
)
320+
result = analyze_fsa_correction(student_enfa, expected_dfa)
321+
assert result.fsa_feedback is not None
322+
assert result.fsa_feedback.structural is not None
323+
assert result.fsa_feedback.structural.is_deterministic is False
324+
325+
210326
if __name__ == "__main__":
211327
pytest.main([__file__, "-v"])

evaluation_function/test/test_validation.py

Lines changed: 176 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -314,55 +314,185 @@ def test_isomorphic_dfas(self):
314314
initial="s0",
315315
accept=["s1"],
316316
)
317-
assert are_isomorphic(fsa_user, fsa_sol).ok
318-
319-
320-
# =============================================================================
321-
# Test Minimality
322-
# =============================================================================
323-
324-
@pytest.fixture
325-
def dfa_accepts_a():
326-
"""DFA that accepts exactly 'a'."""
327-
return make_fsa(
328-
states=["q0", "q1", "q2"],
329-
alphabet=["a", "b"],
330-
transitions=[
331-
{"from_state": "q0", "to_state": "q1", "symbol": "a"},
332-
{"from_state": "q0", "to_state": "q2", "symbol": "b"},
333-
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
334-
{"from_state": "q1", "to_state": "q2", "symbol": "b"},
335-
{"from_state": "q2", "to_state": "q2", "symbol": "a"},
336-
{"from_state": "q2", "to_state": "q2", "symbol": "b"},
337-
],
338-
initial="q0",
339-
accept=["q1"]
340-
)
341-
342-
343-
class TestCheckMinimality:
344-
"""Test check_minimality function."""
345-
346-
def test_minimal_dfa(self, dfa_accepts_a):
347-
result = is_minimal(dfa_accepts_a)
348-
assert result.ok
349-
assert result.value is True
350-
351-
def test_non_minimal_dfa_with_unreachable(self):
352-
non_minimal = make_fsa(
353-
states=["q0", "q1", "q2", "unreachable"],
317+
assert are_isomorphic(fsa_user, fsa_sol) == []
318+
319+
320+
class TestEpsilonTransitions:
321+
"""Tests for epsilon transition handling across the validation pipeline."""
322+
323+
def test_valid_fsa_with_epsilon_unicode(self):
324+
"""ε-NFA with Unicode ε should pass structural validation."""
325+
fsa = make_fsa(
326+
states=["q0", "q1", "q2"],
327+
alphabet=["a"],
328+
transitions=[
329+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
330+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
331+
],
332+
initial="q0",
333+
accept=["q2"],
334+
)
335+
assert is_valid_fsa(fsa) == []
336+
337+
def test_valid_fsa_with_epsilon_string(self):
338+
"""ε-NFA with 'epsilon' string should pass structural validation."""
339+
fsa = make_fsa(
340+
states=["q0", "q1", "q2"],
341+
alphabet=["a"],
342+
transitions=[
343+
{"from_state": "q0", "to_state": "q1", "symbol": "epsilon"},
344+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
345+
],
346+
initial="q0",
347+
accept=["q2"],
348+
)
349+
assert is_valid_fsa(fsa) == []
350+
351+
def test_valid_fsa_with_empty_string_epsilon(self):
352+
"""ε-NFA with empty string epsilon should pass structural validation."""
353+
fsa = make_fsa(
354+
states=["q0", "q1", "q2"],
355+
alphabet=["a"],
356+
transitions=[
357+
{"from_state": "q0", "to_state": "q1", "symbol": ""},
358+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
359+
],
360+
initial="q0",
361+
accept=["q2"],
362+
)
363+
assert is_valid_fsa(fsa) == []
364+
365+
def test_epsilon_nfa_is_not_deterministic(self):
366+
"""ε-NFA should be flagged as non-deterministic."""
367+
fsa = make_fsa(
368+
states=["q0", "q1"],
369+
alphabet=["a"],
370+
transitions=[
371+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
372+
],
373+
initial="q0",
374+
accept=["q1"],
375+
)
376+
errors = is_deterministic(fsa)
377+
assert len(errors) > 0
378+
assert ErrorCode.NOT_DETERMINISTIC in [e.code for e in errors]
379+
380+
def test_accepts_string_via_epsilon_closure(self):
381+
"""ε-NFA should accept 'a' by following q0 --ε--> q1 --a--> q2."""
382+
fsa = make_fsa(
383+
states=["q0", "q1", "q2"],
384+
alphabet=["a"],
385+
transitions=[
386+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
387+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
388+
],
389+
initial="q0",
390+
accept=["q2"],
391+
)
392+
assert accepts_string(fsa, "a") == []
393+
394+
def test_rejects_string_with_epsilon_nfa(self):
395+
"""ε-NFA that accepts 'a' should reject empty string."""
396+
fsa = make_fsa(
397+
states=["q0", "q1", "q2"],
398+
alphabet=["a"],
399+
transitions=[
400+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
401+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
402+
],
403+
initial="q0",
404+
accept=["q2"],
405+
)
406+
errors = accepts_string(fsa, "")
407+
assert len(errors) > 0
408+
409+
def test_accepts_empty_string_via_epsilon(self):
410+
"""ε-NFA should accept empty string when initial reaches accept via ε."""
411+
fsa = make_fsa(
412+
states=["q0", "q1"],
413+
alphabet=["a"],
414+
transitions=[
415+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
416+
],
417+
initial="q0",
418+
accept=["q1"],
419+
)
420+
assert accepts_string(fsa, "") == []
421+
422+
def test_epsilon_nfa_equivalent_to_dfa(self):
423+
"""ε-NFA and DFA accepting the same language should be equivalent."""
424+
enfa = make_fsa(
425+
states=["q0", "q1", "q2"],
426+
alphabet=["a"],
427+
transitions=[
428+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
429+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
430+
],
431+
initial="q0",
432+
accept=["q2"],
433+
)
434+
dfa = make_fsa(
435+
states=["s0", "s1"],
436+
alphabet=["a"],
437+
transitions=[
438+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
439+
],
440+
initial="s0",
441+
accept=["s1"],
442+
)
443+
assert fsas_accept_same_language(enfa, dfa) == []
444+
445+
def test_epsilon_nfa_not_equivalent_to_different_dfa(self):
446+
"""ε-NFA and DFA accepting different languages should not be equivalent."""
447+
enfa = make_fsa(
448+
states=["q0", "q1", "q2"],
354449
alphabet=["a", "b"],
355450
transitions=[
356-
{"from_state": "q0", "to_state": "q1", "symbol": "a"},
357-
{"from_state": "q0", "to_state": "q2", "symbol": "b"},
451+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
358452
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
359-
{"from_state": "q1", "to_state": "q2", "symbol": "b"},
360-
{"from_state": "q2", "to_state": "q2", "symbol": "a"},
361-
{"from_state": "q2", "to_state": "q2", "symbol": "b"},
362-
{"from_state": "unreachable", "to_state": "unreachable", "symbol": "a"},
363453
],
364454
initial="q0",
365-
accept=["q1"]
455+
accept=["q2"],
366456
)
367-
result = is_minimal(non_minimal)
368-
assert not result.ok
457+
dfa = make_fsa(
458+
states=["s0", "s1"],
459+
alphabet=["a", "b"],
460+
transitions=[
461+
{"from_state": "s0", "to_state": "s1", "symbol": "b"},
462+
],
463+
initial="s0",
464+
accept=["s1"],
465+
)
466+
errors = fsas_accept_same_language(enfa, dfa)
467+
assert len(errors) > 0
468+
469+
def test_multi_epsilon_nfa_equivalent_to_dfa(self):
470+
"""ε-NFA for (a|b) with branching epsilons should match equivalent DFA."""
471+
# q0 --ε--> q1, q0 --ε--> q2, q1 --a--> q3, q2 --b--> q3
472+
enfa = make_fsa(
473+
states=["q0", "q1", "q2", "q3"],
474+
alphabet=["a", "b"],
475+
transitions=[
476+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
477+
{"from_state": "q0", "to_state": "q2", "symbol": "ε"},
478+
{"from_state": "q1", "to_state": "q3", "symbol": "a"},
479+
{"from_state": "q2", "to_state": "q3", "symbol": "b"},
480+
],
481+
initial="q0",
482+
accept=["q3"],
483+
)
484+
dfa = make_fsa(
485+
states=["s0", "s1"],
486+
alphabet=["a", "b"],
487+
transitions=[
488+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
489+
{"from_state": "s0", "to_state": "s1", "symbol": "b"},
490+
],
491+
initial="s0",
492+
accept=["s1"],
493+
)
494+
assert fsas_accept_same_language(enfa, dfa) == []
495+
496+
497+
if __name__ == "__main__":
498+
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)