⏺ ;-paging is in — clippy -D warnings, 16 tests (4 new), fmt, and full workspace #13
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "paging"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
all green.
How it works, given stateless binaries:
The transcript reads like SWI:
plg> ?- p(X, Y).
X = tom, Y = bob ;
X = bob, Y = ann ;
X = ann, Y = sue .
still quits. The input line is replaced by a dim ; next · any other key stop
hint, and the editor cursor/mode indicator are suppressed (you're not editing).
exhausted, it re-fetches with double the limit (the stateless-binary re-fetch
the design called for) and continues seamlessly. Batch is small so we don't
over-compute when you only want the first answer.
The trickier part was the wire format. Text renders terms canonically (point(1,
2), [a, b, c]) but doesn't delimit solutions; JSON delimits + has exhausted
but renders compounds as ugly nested objects. So fetch() takes only
count/exhausted (and error detection) from JSON and the rendered lines from
text, then splits the lines into count equal groups — every solution emits the
same number of Var = Value lines (anonymous vars aren't reported; an
all-anonymous solution is true.). This reuses the engine's term writer instead
of reimplementing it — the alternative (a JSON→Prolog renderer) would've been
shadow logic. Two execs per fetch is the honest cost; I noted it in run.rs.
The pure pieces are unit-tested (multi-var/single-var/all-anonymous splitting,
count/error parsing). The interactive ; loop is inherently TUI, so give it a
hands-on spin: define a few facts, query with multiple solutions, press ; to
walk them and a non-; key to stop early — and try a 30+ solution query to see
the re-fetch kick in past 25.
That clears the last parked item. The REPL now has the full Prolog-native query
loop.
all green. How it works, given stateless binaries: - A query fetches a batch (PAGE_BATCH = 25) and reveals one solution at a time. The transcript reads like SWI: plg> ?- p(X, Y). X = tom, Y = bob ; X = bob, Y = ann ; X = ann, Y = sue . - Trailing ; means more may follow; . marks the last (paging auto-ends). - Keys while paging: ; or space → next solution; any other key stops; Ctrl-C/D still quits. The input line is replaced by a dim ; next · any other key stop hint, and the editor cursor/mode indicator are suppressed (you're not editing). - Running past the batch: if you page to the end and the engine wasn't exhausted, it re-fetches with double the limit (the stateless-binary re-fetch the design called for) and continues seamlessly. Batch is small so we don't over-compute when you only want the first answer. The trickier part was the wire format. Text renders terms canonically (point(1, 2), [a, b, c]) but doesn't delimit solutions; JSON delimits + has exhausted but renders compounds as ugly nested objects. So fetch() takes only count/exhausted (and error detection) from JSON and the rendered lines from text, then splits the lines into count equal groups — every solution emits the same number of Var = Value lines (anonymous vars aren't reported; an all-anonymous solution is true.). This reuses the engine's term writer instead of reimplementing it — the alternative (a JSON→Prolog renderer) would've been shadow logic. Two execs per fetch is the honest cost; I noted it in run.rs. The pure pieces are unit-tested (multi-var/single-var/all-anonymous splitting, count/error parsing). The interactive ; loop is inherently TUI, so give it a hands-on spin: define a few facts, query with multiple solutions, press ; to walk them and a non-; key to stop early — and try a 30+ solution query to see the re-fetch kick in past 25. That clears the last parked item. The REPL now has the full Prolog-native query loop.Review —
;-paging for query solutionsThe overall shape is right. The split between
run::fetch(returns aFetchenum — pure I/O outcome),app::Paging(state machine across;presses), andui(suppress editor cursor + render hint) is clean. Refetch-with-doubled-limit is the documented design for stateless binaries, and reusing the engine's text writer instead of re-rendering JSON terms is the right call — point you made in the PR description and I agree.Issues below, ordered by impact.
Real concerns
1. Refetch is O(n²) total work for long enumerations. Each refetch re-executes the binary with a doubled limit and re-renders every prior solution. A 1000-solution query enumerated to completion runs fetches at 25 → 50 → 100 → 200 → 400 → 800 → 1600. Each fetch is two execs and a full text render — total ~3175 solution-renders for 1000 solutions, plus 14 process spawns. Interactive use is fine (a few
;and done); a user who actually pages through everything pays this cost. Mitigations:pos.run.rs:9calls out "two execs per fetch" but not the cumulative re-render.2.
json_countis called twice on the same payload.run.rs:81-86:Should be one match that binds
count:Cosmetic, but reads as a forgotten cleanup.
3. JSON scraping is fragile to wire-format changes.
json.find("\"count\":")matches anywhere the substring appears — today the engine only emits onecountkey, but if it ever grows aline_count/subgoal_count/etc., this picks the wrong one silently. Same forjson.contains("\"exhausted\":true"). Two options:find(",\"count\":")/find("{\"count\":"). Cheap; pins the position.serde_jsonfor the small JSON header — one more dep but kills the class.The two-exec design also quietly assumes the engine produces identical solutions in both passes (count from JSON, lines from text). Pure Prolog: yes. Anything with side effects (
time/1, random,assertz): could drift. Worth one line in the module doc that this depends on inter-run determinism.4.
split_solutionsnon-uniform fallback can silently misreport count.run.rs:115-117: iflines.len() % count != 0, it falls back tolines.into_iter().map(str::to_string).collect()— returninglines.len()strings as solutions even whencount(the authoritative JSON value) said something else. The user pages through what looks likelines.len()solutions with no signal that something is off. Three lines (aself.log(" warn: malformed engine output")from app, or returningFetch::Failed(...)from fetch) would surface it.Small observations
5. No test for the refetch path. This is the only stateful piece of paging (limit doubling,
poscarries over, new batch overwrites). The pure pieces are tested, but the most likely-to-break thing isn't —page_next's borrow gymnastics aroundneed_more+ refetch are exactly where a refactor would silently regress. A test would needrun::fetchto be injectable (trait or fn pointer); not in scope here, but flagging the gap.6. Doubling is unbounded.
limit.saturating_mul(2)keeps growing as the user pages. With a divergent goal and a longPLG_REPL_TIMEOUT, you can find yourself waiting longer and longer per refetch with no cap. Modest defensive ceiling (e.g. 4096) would bound the worst case without affecting realistic use.7. Paging assumes the engine produces solutions in a limit-independent order. The refetch relies on
solutions[pos]in the new (bigger) batch being the same as the unrevealed tail of the old. Standard left-to-right SLD resolution gives that, but it's worth a one-liner in thePagingdoc — it's the invariant that keepsposcarrying across refetches.8. UI hint text uses literal spaces for spacing.
ui.rs:21—" ; next solution · any other key stop". Hand-tuned centering shifts oddly at narrow widths (and dimming applies to the leading spaces too, which is invisible but ratatui still walks them). Layout-aware (Rect-split and right-justify the hint) would be sturdier; pedantic.9.
Ctrl-Cmid-paging quits the REPL, not just the paging. Reasonable choice — matches "Ctrl-C/D still quits" in the description — but worth confirming intent vs. SWI (which I think takes Ctrl-C as a soft cancel of the current goal). Ifqora(SWI bindings for "stop" / "abort") feel natural, easy to add to the match arm.What's good
--format textinstead of forking it into the REPL. The JSON-only alternative would have been shadow logic with all the precision-loss footguns.Pagingstruct either holds a full batch or paging ends — no half-fetched state.run_query. A pending edit forces a recompile before fetch — paging can't run against a stale binary.Fetchenum keepsappignorant of subprocess mechanics.NoSolutions/Failed/Timeout/Error/Foundcleanly distinguish the cases the UI needs.;and space advance. Matches SWI muscle memory.split_solutionscovers multi-var/single-var/all-anonymous;json_count/json_errorpinned. The interactive seam is honestly acknowledged as untested.Suggested order
#1 (re-render cost), #2 (double
json_count), #3 (anchor JSON scrape) are quick wins worth doing before merge. #4 (silent misreport) is mostly defensive — fine as a follow-up unless you've seen the case fire. #5 onwards are taste/scope.