Skip to content

Commit 06c7dad

Browse files
feat(step-through): implement step-through functionality in parser and UI, including button integration and API updates
1 parent afe2b24 commit 06c7dad

8 files changed

Lines changed: 93 additions & 4 deletions

File tree

src/dylan/dag/word_level_context_dag.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,25 @@ def this_is_first_tuple_after_last_word(self) -> None:
277277
def reset_to_first_tuple_after_last_word(self) -> None:
278278
"""Reset current tuple to the last post-word anchor and clear children."""
279279
anchor = self.first_tuple_after_last_word or self.root
280-
self.set_current_tuple(anchor)
280+
path: list[tuple[DAGTuple, GroundableEdge]] = []
281+
node = anchor
282+
while node != self.root:
283+
inc = self._in.get(node)
284+
if inc is None:
285+
break
286+
edge, parent = inc
287+
path.append((parent, edge))
288+
node = parent
289+
path.reverse()
290+
291+
for parent, _edge in path:
292+
for out_edge in self.get_out_edges(parent):
293+
out_edge.set_seen(False)
294+
out_edge.set_in_context(False)
295+
296+
self.set_current_tuple(path[0][0] if path else anchor)
297+
for _parent, edge in path:
298+
edge.traverse(self)
281299
self.word_stack.clear()
282300
self.remove_children(self.cur)
283301
self.exhausted = False
@@ -319,7 +337,7 @@ def get_all_tuples(self) -> list[DAGTuple]:
319337
return sorted(self._nodes, key=lambda t: t.tuple_id)
320338

321339
def get_n_best_final_tuples(self, n: int) -> list[DAGTuple]:
322-
"""Return up to *n* complete leaf tuples."""
340+
"""Return up to *n* complete leaf tuples in creation order, not parser step-through order."""
323341
leaves = [
324342
node for node in self.get_all_tuples()
325343
if self.out_degree(node) == 0 and node.is_complete()

src/dylan/gui/app.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,15 @@ def do_parse(_: ft.ControlEvent | None = None) -> None:
514514
)
515515
_refresh_views(msg)
516516

517+
def do_step_through(_: ft.ControlEvent | None = None) -> None:
518+
"""Advance to the next interpretation from the current parser state."""
519+
err, ok = session.run_step_through()
520+
if err is not None:
521+
append_log(err)
522+
return
523+
msg = "Stepped to next interpretation." if ok else "No further interpretation available."
524+
_refresh_views(msg)
525+
517526
def on_tree_canvas_resize(e: cv.CanvasResizeEvent) -> None:
518527
"""Re-layout the parse tree when the canvas control receives its real size."""
519528
aw = float(e.width)
@@ -536,6 +545,11 @@ def on_tree_canvas_resize(e: cv.CanvasResizeEvent) -> None:
536545
shape=_primary_btn_shape,
537546
),
538547
)
548+
step_through_btn = ft.Button(
549+
content="Step Through",
550+
on_click=do_step_through,
551+
tooltip="Advance to the next parse interpretation (Java Step Through)",
552+
)
539553
set_grammar_btn.on_click = pick_grammar_dir
540554

541555
# --- app bar ----------------------------------------------------------
@@ -611,6 +625,7 @@ def on_tree_canvas_resize(e: cv.CanvasResizeEvent) -> None:
611625
[
612626
sentence_box,
613627
parse_btn,
628+
step_through_btn,
614629
],
615630
spacing=8,
616631
),

src/dylan/gui/parse_session.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,10 @@ def run_parse(
215215
utt = utterance_from_text(speaker, text)
216216
ok = self.parser.parse_utterance(utt)
217217
return (None, ok)
218+
219+
def run_step_through(self) -> tuple[str | None, bool | None]:
220+
"""Advance to the next parser interpretation, matching Java ``ParserPanel`` step-through."""
221+
if self.parser is None:
222+
return ("=== Step through ===\nLoad grammar first.", None)
223+
ok = self.parser.parse_goal(None)
224+
return (None, ok)

src/dylan/parser/dag_parser.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,26 @@ def complete(self, word: UtteredWord | None = None) -> DAGTuple:
180180
dag.add_child(tup, edge)
181181
return tup
182182

183+
def get_state_with_n_best_tuples(self, n: int) -> list[DAGTuple]:
184+
"""Return current tuple plus up to *n* further interpretations by stepping the parser."""
185+
if n < 0:
186+
raise ValueError("n must be non-negative")
187+
dag = self.get_state()
188+
dag.reset_to_first_tuple_after_last_word()
189+
result: list[DAGTuple] = [dag.get_current_tuple()]
190+
try:
191+
for _ in range(n):
192+
if not self.parse_goal(None):
193+
break
194+
result.append(dag.get_current_tuple())
195+
return result
196+
finally:
197+
dag.reset_to_first_tuple_after_last_word()
198+
183199
def get_n_best_final_semantics(self, n: int) -> list[TTRRecordType]:
184-
"""Return up to *n* final semantics from complete leaf tuples."""
200+
"""Return final semantics from Java-style N-best tuple stepping."""
185201
out: list[TTRRecordType] = []
186-
for tup in self.get_state().get_n_best_final_tuples(n):
202+
for tup in self.get_state_with_n_best_tuples(n):
187203
sem = tup.get_semantics(self.context)
188204
ev = sem.evaluate()
189205
if isinstance(ev, TTRRecordType):
@@ -212,4 +228,5 @@ def parse_word(self, word: UtteredWord) -> WordLevelContextDAG | None:
212228
DAGParser.completeTree = DAGParser.complete_tree # type: ignore[attr-defined]
213229
DAGParser.getFinalSemantics = DAGParser.get_final_semantics # type: ignore[attr-defined]
214230
DAGParser.newSentence = DAGParser.new_sentence # type: ignore[attr-defined]
231+
DAGParser.getStateWithNBestTuples = DAGParser.get_state_with_n_best_tuples # type: ignore[attr-defined]
215232
DAGParser.getNBestFinalSemantics = DAGParser.get_n_best_final_semantics # type: ignore[attr-defined]

src/dylan/pyodide_api.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ def dispatch(action: str, payload: dict[str, Any]) -> dict[str, Any]:
7777
"log_message": format_parse_state_log(msg),
7878
"views": _view_dict(),
7979
}
80+
if action == "step_through":
81+
err, ok = _session.run_step_through()
82+
if err is not None:
83+
return {"error": err, "step_ok": None, "views": None, "log_message": None}
84+
msg = "Stepped to next interpretation." if ok else "No further interpretation available."
85+
return {
86+
"error": None,
87+
"step_ok": ok,
88+
"log_message": format_parse_state_log(msg),
89+
"views": _view_dict(),
90+
}
8091
return {"error": f"unknown action: {action}"}
8192

8293

tests/test_pyodide_api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ def test_dispatch_load_init_parse_bundled_grammar() -> None:
3737
assert isinstance(pv, dict)
3838
assert "man(" in pv.get("semantics", "") and "arrive" in pv.get("semantics", "")
3939

40+
step_out = pyodide_api.dispatch("step_through", {})
41+
assert step_out.get("error") is None
42+
assert "step_ok" in step_out
43+
assert isinstance(step_out.get("views"), dict)
44+
4045

4146
def test_api_json_round_trip() -> None:
4247
"""``api_json`` returns valid JSON and echoes dispatch results."""

web/app.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ await micropip.install(${JSON.stringify(wheelUrl)})
159159
document.getElementById("btn-init").disabled = false;
160160
document.getElementById("btn-new-sentence").disabled = false;
161161
document.getElementById("btn-parse").disabled = false;
162+
document.getElementById("btn-step-through").disabled = false;
162163
}
163164

164165
async function loadGrammarFromVirtualPath() {
@@ -293,6 +294,18 @@ await micropip.install(${JSON.stringify(wheelUrl)})
293294
appendLog(String(err));
294295
}
295296
});
297+
document.getElementById("btn-step-through").addEventListener("click", () => {
298+
try {
299+
const r = callApi("step_through", {});
300+
if (r.error) appendLog(r.error);
301+
else {
302+
applyViews(r.views);
303+
if (r.log_message) appendLog(r.log_message);
304+
}
305+
} catch (err) {
306+
appendLog(String(err));
307+
}
308+
});
296309
}
297310

298311
wireTabs();

web/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454
<button type="button" class="btn btn-primary" id="btn-parse" disabled style="align-self: center">
5555
Parse
5656
</button>
57+
<button type="button" class="btn btn-secondary" id="btn-step-through" disabled style="align-self: center">
58+
Step Through
59+
</button>
5760
</div>
5861
<div class="tabs">
5962
<div class="tab-bar">

0 commit comments

Comments
 (0)