prolog-notebook 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +233 -0
- package/README.md +131 -28
- package/bin/prolog-notebook.mjs +206 -0
- package/package.json +19 -4
- package/src/browser.js +252 -16
- package/src/build-info.js +94 -0
- package/src/build-info.json +5 -0
- package/src/clauses.js +236 -0
- package/src/engine.js +367 -18
- package/src/export.js +126 -0
- package/src/format.js +649 -0
- package/src/node.js +14 -4
- package/src/notebook.css +322 -1
- package/src/notebook.js +1261 -56
- package/src/page.js +85 -0
- package/src/render.js +367 -0
- package/src/run.js +128 -0
- package/src/session.js +227 -0
- package/src/version.js +51 -0
- package/src/worker.js +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,238 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] — 2026-08-30
|
|
4
|
+
|
|
5
|
+
**A chapter is a file you can read, run, and fill in from the command line.** The renderer,
|
|
6
|
+
the page's own behaviour, and the first half of the CLI.
|
|
7
|
+
|
|
8
|
+
### Why
|
|
9
|
+
|
|
10
|
+
0.2.0 made a `.prolog.md` executable. It did not make one *publishable*: the answers a chapter
|
|
11
|
+
shows had to be typed by its author, which means they were the author's guess at what SWI
|
|
12
|
+
prints, published as though it ran. Everything here follows from closing that gap and from
|
|
13
|
+
what the closing revealed.
|
|
14
|
+
|
|
15
|
+
`prolog-notebook run` now fills a chapter's answers in from a real engine. Run against the
|
|
16
|
+
chapter in this repository it changes nothing — the hand-written answers were already exactly
|
|
17
|
+
what SWI produces, hashes included, which is now a test.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`prolog-notebook run <file>…`** — consults every program cell, runs every query below it,
|
|
22
|
+
and writes the solution sequences back with an `input-hash` for each. `--limit`, `--stdout`,
|
|
23
|
+
`--quiet`. The logic is in `src/run.js` and takes a parsed notebook and a session, so
|
|
24
|
+
`--check` and a VS Code "run all" will get the same behaviour without a shell.
|
|
25
|
+
- **`--version`**, which says which SWI-Prolog will produce your answers. swipl-wasm 8.0.7
|
|
26
|
+
ships SWI-Prolog 10.1.13, and the two numbers are unrelated — so the engine version is the
|
|
27
|
+
one fact there that nobody could have looked up. A published install also reports the commit
|
|
28
|
+
it was built from; a working copy says so, and says when it has uncommitted edits.
|
|
29
|
+
- **A chapter is readable cold.** Saved answers render with no engine anywhere, and are marked
|
|
30
|
+
stale when the program above them has moved.
|
|
31
|
+
- **`hold`** — a query cell can withhold its saved answers until the reader runs it, or until
|
|
32
|
+
they have written the prediction above it. A page that has already printed all six answers is
|
|
33
|
+
arguing with prose that says "press Run".
|
|
34
|
+
- **`rerun="auto"`** — the answers follow the program. On consult, never on edit; never
|
|
35
|
+
starting work nobody asked for; and a held cell stays manual until its wait ends.
|
|
36
|
+
- **The page's own controls**: a lozenge that raises a card — what the engine is holding, the
|
|
37
|
+
chapter's answers shown or hidden, and the notebook itself. It says which version is on
|
|
38
|
+
screen, and once the two differ, lets you download either.
|
|
39
|
+
- **Per-cell reset**, on both runnable kinds. On a program cell that means out of the engine
|
|
40
|
+
as well as back to the chapter's text.
|
|
41
|
+
- **A stateful cell says so** before anything is asserted into it.
|
|
42
|
+
- **Download your own copy** — the reader leaves with a real `.prolog.md`, their edits and
|
|
43
|
+
their answers in it, hashed against the program that produced them.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **An answer containing variables was not what SWI would print.** `app([1,2], Tail, L)` came
|
|
48
|
+
out as `L = [1,2|_20306], Tail = _20428` where a toplevel prints `L = [1, 2|Tail]` — the
|
|
49
|
+
same variable shown as two, with the reader's own name for it discarded. Each binding was
|
|
50
|
+
rendered in a separate round trip into Prolog, and two round trips cannot share a variable.
|
|
51
|
+
Prolog now renders the whole answer, once, with the goal's own `variable_names`. Invisible
|
|
52
|
+
until now because every answer in the shipped chapter is a ground atom.
|
|
53
|
+
- **A half-walked query was destroyed by running any other cell.** SWI keeps open queries on a
|
|
54
|
+
stack; nothing here released one, and the next query nested inside it. There is now one open
|
|
55
|
+
sequence per engine, and the cell that loses its own is told so in words about the notebook
|
|
56
|
+
rather than about SWI's internals.
|
|
57
|
+
- **A `hold` release, a stuck Hide control, and a panel that resized under the cursor** — all
|
|
58
|
+
found by reading the page rather than the tests, which is why `src/notebook.js` now has a
|
|
59
|
+
jsdom harness and the page's behaviour is asserted from Node.
|
|
60
|
+
- **A compile error is reported in the cell's own terms**, not as a path the reader never
|
|
61
|
+
chose.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- **The engine is pinned exactly** (`swipl-wasm 8.0.7`). Two installs of one release, ten
|
|
66
|
+
minutes apart, ran SWI-Prolog 10.1.10 and 10.1.13 — and a chapter's saved answers are only
|
|
67
|
+
ever true of the engine that produced them. Moving the engine is now a commit with the
|
|
68
|
+
chapters re-run in it.
|
|
69
|
+
- Solutions are spelled as SWI's own writer spells them, so compounds gain a space after each
|
|
70
|
+
comma: `foo(1, 2)`, not `foo(1,2)`. Any file with saved answers containing a compound will
|
|
71
|
+
differ on its next `run`.
|
|
72
|
+
- `offerDownload()` takes an options object rather than positional arguments.
|
|
73
|
+
- The engine is imported where it is used rather than at the top of the CLI, so `--help` still
|
|
74
|
+
works on an install whose WebAssembly is missing.
|
|
75
|
+
|
|
76
|
+
### Notes
|
|
77
|
+
|
|
78
|
+
`--check` — run a chapter in CI and fail the build when its answers have drifted — is
|
|
79
|
+
deliberately **not** here. It needs a timeout first: the Node engine runs in-process, so a
|
|
80
|
+
non-terminating goal hangs the command, and a test suite that can hang forever is not a test
|
|
81
|
+
suite. The command says so on every run.
|
|
82
|
+
|
|
83
|
+
## [0.2.0] — 2026-08-16
|
|
84
|
+
|
|
85
|
+
**Breaking: the session API is asynchronous.** `consult`, `next` and `all` now return
|
|
86
|
+
promises, and in the browser the engine runs in a Web Worker.
|
|
87
|
+
|
|
88
|
+
### Why
|
|
89
|
+
|
|
90
|
+
A Prolog query is synchronous WASM, so a goal that never terminates blocks the thread it
|
|
91
|
+
runs on. Measured, not assumed: `consult('loop :- loop.')` followed by `query('loop').next()`
|
|
92
|
+
stopped Node's event loop dead — a timer scheduled for 3 seconds never fired. In a browser
|
|
93
|
+
that is not a slow page, it is a dead one: no repaint, no button, no way back except closing
|
|
94
|
+
the tab.
|
|
95
|
+
|
|
96
|
+
That is unacceptable here specifically, because **non-termination is chapter material**. Left
|
|
97
|
+
recursion and a generator with no base case are things a Prolog book has to demonstrate, and
|
|
98
|
+
the demonstration has to be survivable.
|
|
99
|
+
|
|
100
|
+
### Added
|
|
101
|
+
|
|
102
|
+
- **The page says what the engine is holding.** A program cell's tick carries the time it was
|
|
103
|
+
consulted (`title="consulted at 14:32:05"`, absolute — a relative time baked into an
|
|
104
|
+
attribute is wrong the moment it is written), and flips to **edited since consulted** as soon
|
|
105
|
+
as the textarea diverges from what was actually loaded. That state, not the clock, is the
|
|
106
|
+
reader's real question, and it became easy to get wrong the moment Run started consulting
|
|
107
|
+
cells by itself.
|
|
108
|
+
- **Reset, at two scales.** Per cell: restore the chapter's text *and re-consult it*, because
|
|
109
|
+
putting the page back without putting the engine back leaves them disagreeing. Page-level:
|
|
110
|
+
**restart engine**, which throws the worker away and replays the consult log — the documented
|
|
111
|
+
answer for a `:- dynamic` cell whose state lives in no file. Verified in a browser: a counter
|
|
112
|
+
mutated to 41 is 0 again afterwards. Per-cell *unconsult* is deliberately absent; it raises
|
|
113
|
+
"what happens to the cells that depended on it", and page-level restart has no such question.
|
|
114
|
+
- **The engine's state is visible**: *engine not started* until something needs it, which is
|
|
115
|
+
also the plainest evidence that a cold chapter is readable without it.
|
|
116
|
+
- **The chapter is readable before the engine arrives, and without it.** A query cell renders
|
|
117
|
+
the answers stored in the file — labelled as the chapter's, not the reader's (docs/modes.md
|
|
118
|
+
§3) — and the 5.9 MB WASM bundle is fetched only when someone presses Run. Verified in a
|
|
119
|
+
browser by watching the network: on a cold load there is no request for it at all. This is
|
|
120
|
+
what makes a published chapter degrade to a book rather than to a blank page.
|
|
121
|
+
- **Saved answers that no longer follow from the program above them are marked**, before first
|
|
122
|
+
paint and with no engine: the stored `input-hash` is compared against a 64-bit FNV-1a of the
|
|
123
|
+
goal and the program cells preceding it. Marked rather than hidden — never silently
|
|
124
|
+
discarded and never silently trusted.
|
|
125
|
+
- **A chapter is a file.** `prolog-notebook/page` fetches a `.prolog.md`, parses it, renders
|
|
126
|
+
the whole page and wires up the cells: `await load('chapter-04-cut.prolog.md')`. Program and
|
|
127
|
+
query cells are generated from the model rather than marked up by hand, which is what makes
|
|
128
|
+
writing a chapter *writing markdown*.
|
|
129
|
+
- **The `once/1` chapter is now that file**, and the hand-written page it was ported from is
|
|
130
|
+
deleted. Same prose, same four queries, same answers — six duplicated sons, one from `son_a`,
|
|
131
|
+
three from `son_b`, `true` for the ground goal — driven from
|
|
132
|
+
[`notebooks/ch04-cut.prolog.md`](notebooks/ch04-cut.prolog.md) instead of 200 lines of HTML.
|
|
133
|
+
It carries its saved answers and their `input-hash`es, and a test asserts the chapter agrees
|
|
134
|
+
with them, so an edit to a program cell that invalidates an answer below it fails CI.
|
|
135
|
+
On the repo page it reads as a document: prose as prose, Prolog syntax-highlighted, and the
|
|
136
|
+
prediction still hidden behind a `<details>` you have to click.
|
|
137
|
+
- **Run brings its own context.** Pressing Run on a query consults the program cells above it
|
|
138
|
+
first, so a reader who lands halfway down a chapter gets an answer rather than
|
|
139
|
+
`Unknown procedure`. Cells already loaded at their current text are skipped, so the second
|
|
140
|
+
Run of a chapter consults nothing and an edited cell invalidates only itself.
|
|
141
|
+
There is no dependency graph, and there is no need for one: **Prolog has no load-time name
|
|
142
|
+
binding** — `q(X) :- p(X)` merely mentions `p/1`, which is looked up when it is *called* — so
|
|
143
|
+
consult order cannot affect correctness, and at ~3.5 ms a cell there is nothing to gain by
|
|
144
|
+
computing one.
|
|
145
|
+
- **An error a reader can act on.** `wasm:wasm_call_string/3: Unknown procedure: son_a/1` is
|
|
146
|
+
our own plumbing in the middle of a lesson; it now reads `Unknown procedure: son_a/1`, and
|
|
147
|
+
where the predicate is defined by a cell *below* the query, the notebook says so and names
|
|
148
|
+
it. Context frames belonging to the reader's own code are untouched — `//2: Arithmetic:
|
|
149
|
+
evaluation error` still names the division that failed.
|
|
150
|
+
- `prolog-notebook/render` — the cell model to HTML **strings**, DOM-free. The same emitter
|
|
151
|
+
serves the browser today, the static build in v0.3 and the VS Code renderer later; one
|
|
152
|
+
implementation, four consumers.
|
|
153
|
+
- A generated program cell carries its notebook id as `data-cell`, and the consult is named by
|
|
154
|
+
it. SWI now says `Previously defined at /p-family.pl:20` — a warning that names a cell the
|
|
155
|
+
reader can find in the source, rather than `/cell-3.pl`.
|
|
156
|
+
- **`notebooks/` holds chapters and `viewer/` holds the one page that renders them.** They had
|
|
157
|
+
both been living in `example/`, under a script (`npm run example`, now `npm run dev`) that
|
|
158
|
+
actually served the repo root. A chapter is not an example, there will be many of them, and
|
|
159
|
+
the first one had been doubling as a parser fixture *with a deliberately wrong `input-hash`* —
|
|
160
|
+
which a file anybody is meant to read must never carry. The wrong hash now lives in
|
|
161
|
+
`test/fixtures/stale-output.prolog.md`, where being wrong is the point.
|
|
162
|
+
- **The engine runs in a Web Worker** in the browser, so a runaway goal costs a click on
|
|
163
|
+
Stop rather than the tab. Verified by driving Chrome: with `loop` spinning, timers still
|
|
164
|
+
fire, layout still runs, and the notebook is usable again afterwards.
|
|
165
|
+
- `session.abort()` and `session.restart()` — terminate the worker and replay the consults
|
|
166
|
+
into a new engine. Affordable only because one cell is one virtual file, so a chapter's
|
|
167
|
+
clause store rebuilds in milliseconds; terminating also reclaims the whole WASM heap, so a
|
|
168
|
+
memory blow-up and an infinite loop have the same cure.
|
|
169
|
+
- A `stop` button in the query cell, and a `ConsultLog` holding one entry per cell — the
|
|
170
|
+
latest text, not a history — so a replay restores what the reader actually has.
|
|
171
|
+
- `prolog-notebook/format` (0.1.3, listed here for completeness): the notebook parser,
|
|
172
|
+
canonical serialiser and `input-hash`.
|
|
173
|
+
|
|
174
|
+
### Fixed
|
|
175
|
+
|
|
176
|
+
- **The browser session had no `restart()`**, while the in-process one and the README both did
|
|
177
|
+
— found by wiring a button to it. Both sessions now implement one interface, and a test
|
|
178
|
+
compares them, because nothing else could: Node never runs the worker session and a browser
|
|
179
|
+
never runs the other.
|
|
180
|
+
- `session.formatSolution()` is **removed** rather than added to the worker session. The
|
|
181
|
+
engine-backed one renders through SWI; a worker-backed one could only fall back to the
|
|
182
|
+
engine-free spelling, so the same call would have quietly meant two different things
|
|
183
|
+
depending on where it ran. Use `query.next().text`, or the exported `formatSolution()`.
|
|
184
|
+
|
|
185
|
+
### Changed
|
|
186
|
+
|
|
187
|
+
- `createSession()` returns a session whose methods are all async. Migration is mechanical:
|
|
188
|
+
`session.consult(…)` → `await session.consult(…)`, `q.next()` → `await q.next()`.
|
|
189
|
+
- The browser page no longer loads the WASM bundle with a `<script>` tag; the worker loads
|
|
190
|
+
it. A page may pass `swiplUrl` if it lives somewhere unusual.
|
|
191
|
+
- Node still runs the engine in-process and is **not** protected against a runaway goal. The
|
|
192
|
+
CLI is not an interactive page; the limitation is documented rather than implied.
|
|
193
|
+
|
|
194
|
+
### Notes
|
|
195
|
+
|
|
196
|
+
`assert`/`retract` state does not survive an abort, which is already the documented
|
|
197
|
+
behaviour of "restart engine and run all" (`docs/format.md` §8) rather than a new surprise.
|
|
198
|
+
|
|
199
|
+
## [0.1.2] — 2026-08-04
|
|
200
|
+
|
|
201
|
+
Two bugs, both of which made the library quietly say something untrue. Found by
|
|
202
|
+
running the published entry point, not by reading the code.
|
|
203
|
+
|
|
204
|
+
### Fixed
|
|
205
|
+
|
|
206
|
+
- **Compound terms lost their arguments.** `foo(1,2)` rendered as `foo()`,
|
|
207
|
+
`a-b` as `-()`, `f(g(h))` as `f()`. The arguments of a compound arrive under
|
|
208
|
+
the key *named by the functor* and wrapped in one extra array, not under
|
|
209
|
+
`args` as the formatter assumed. Atoms, numbers and lists were unaffected,
|
|
210
|
+
which is why the `once/1` example never showed it — that chapter only ever
|
|
211
|
+
binds variables to atoms.
|
|
212
|
+
- **A syntax error reported a successful consult.** SWI prints the offending
|
|
213
|
+
clause, skips it and carries on, so `consult/1` still succeeded and the cell
|
|
214
|
+
said `✓ consulted` while the predicate was not there.
|
|
215
|
+
- **A cell could silently destroy another cell's clauses.** Two cells defining
|
|
216
|
+
the same predicate make SWI print "Redefined static procedure" and keep only
|
|
217
|
+
the later one. That warning went to the console and nothing reached the page.
|
|
218
|
+
- `consult` no longer produces paths like `/chapter.pl.pl` when the cell name
|
|
219
|
+
already ends in `.pl`.
|
|
220
|
+
|
|
221
|
+
### Added
|
|
222
|
+
|
|
223
|
+
- `PrologSession#formatTerm` and `#formatSolution` render through SWI itself, so
|
|
224
|
+
operators, quoting and every other rule of the writer come out right —
|
|
225
|
+
`X = a-b`, not `X = -(a, b)`. `query.next()` now carries a `text` field with
|
|
226
|
+
the solution already rendered this way; prefer it over `formatSolution`.
|
|
227
|
+
- `consult` returns `messages: [{kind, text}]` — SWI's warnings and errors for
|
|
228
|
+
that cell, captured through `message_hook/3`.
|
|
229
|
+
- `argumentsOf`, `textOf` and `toEngineTerm` are exported for anything that
|
|
230
|
+
needs to walk a term.
|
|
231
|
+
- Eleven more tests, including the reconsult behaviour the notebook renderer
|
|
232
|
+
will depend on: re-consulting one cell replaces exactly that cell's clauses,
|
|
233
|
+
leaves dependent cells working, and leaves no ghost behind when a predicate is
|
|
234
|
+
renamed.
|
|
235
|
+
|
|
3
236
|
## [0.1.1] — 2026-08-02
|
|
4
237
|
|
|
5
238
|
No functional change. Published from CI via npm trusted publishing (OIDC) to
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ the reader clicks a link.
|
|
|
32
32
|
The reason this is not "Jupyter with a different kernel" is the button marked `; next`.
|
|
33
33
|
|
|
34
34
|
Jupyter's model is request/response: run a cell, get a result. Prolog's model is a stream of
|
|
35
|
-
solutions you walk through. In the included
|
|
35
|
+
solutions you walk through. In the included chapter, `is_son(X)` reports edward *twice* — and
|
|
36
36
|
that duplication **is the lesson**, because it means Prolog found two proofs. A notebook that
|
|
37
37
|
showed only a final list of results would have hidden the very thing worth teaching.
|
|
38
38
|
|
|
@@ -44,15 +44,63 @@ So a query cell gives you the first solution, and then you step.
|
|
|
44
44
|
git clone https://github.com/jarecsni/prolog-notebook
|
|
45
45
|
cd prolog-notebook
|
|
46
46
|
npm install
|
|
47
|
-
npm run
|
|
47
|
+
npm run dev # serves the repo root on :8777
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
Then open **http://localhost:8777/viewer/**. That is a chapter — the `once/1` placement puzzle,
|
|
51
|
+
a real worked section rather than a widget demo — rendered from
|
|
52
|
+
[`notebooks/ch04-cut.prolog.md`](notebooks/ch04-cut.prolog.md). Predict what each version
|
|
53
|
+
returns before you press Run.
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
Edit that file, reload, and the chapter changes: prose, Prolog, margin note and prediction box
|
|
56
|
+
are all in it, and there is no HTML anywhere. Point the viewer at any other notebook with
|
|
57
|
+
`?src=`.
|
|
58
|
+
|
|
59
|
+
| | |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `notebooks/` | chapters. The product. |
|
|
62
|
+
| `viewer/` | one shell page that renders any of them. Replaced by `build` in v0.3. |
|
|
63
|
+
|
|
64
|
+
The chapter also reads on the repo page, [as a file](notebooks/ch04-cut.prolog.md), with no
|
|
65
|
+
build step and no site: prose as prose, Prolog syntax-highlighted, the saved answers in place,
|
|
66
|
+
and the prediction still hidden behind a `<details>` you have to click. That is the whole
|
|
67
|
+
reason the format is markdown.
|
|
68
|
+
|
|
69
|
+
It has to be **served over HTTP**. Opening the page straight from disk leaves the buttons
|
|
70
|
+
inert, because browsers block ES modules over `file://` — the page detects this and says so
|
|
71
|
+
rather than failing silently.
|
|
72
|
+
|
|
73
|
+
## Fill in a chapter's answers
|
|
74
|
+
|
|
75
|
+
A chapter's saved answers have to come from a real run — a hand-written output block is the
|
|
76
|
+
author's *guess* at what SWI prints, published as though it ran. So write the file with no
|
|
77
|
+
output blocks and let the engine fill them in:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
npx prolog-notebook run notebooks/ch04-cut.prolog.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It consults every program cell, runs every query below it, and writes the solution sequences
|
|
84
|
+
back into the file along with an `input-hash` for each — which is what makes the chapter render
|
|
85
|
+
complete, and render as *current*, before the engine arrives. Running it again on an unchanged
|
|
86
|
+
chapter changes nothing.
|
|
87
|
+
|
|
88
|
+
| flag | |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `--limit <n>` | solutions to take from one query before stopping. Default 100. |
|
|
91
|
+
| `--stdout` | print the result instead of writing the file |
|
|
92
|
+
| `--quiet` | report only failures |
|
|
93
|
+
| `--version` | the tool's version, **the SWI-Prolog version it will run your chapters with**, and the copyright |
|
|
94
|
+
|
|
95
|
+
Two things it will not do. A query stopped at the limit is written **without** a terminator,
|
|
96
|
+
which is the format's way of saying the search was never exhausted — `false.` there would be a
|
|
97
|
+
forgery. And if a program cell fails to load, nothing is written at all: every answer below it
|
|
98
|
+
was produced against a chapter that does not exist.
|
|
99
|
+
|
|
100
|
+
It has no defence against a non-terminating goal yet — the engine runs in this process, so
|
|
101
|
+
`loop :- loop.` hangs the command. Say the word `--limit` all you like; a runaway *consult* is
|
|
102
|
+
not a solution count. Fixing it properly means a worker thread, and it is the prerequisite for
|
|
103
|
+
putting this in CI.
|
|
56
104
|
|
|
57
105
|
## Use it
|
|
58
106
|
|
|
@@ -62,36 +110,70 @@ Headless, in Node — this is how you test that every example in a document stil
|
|
|
62
110
|
import { createSession, formatSolution } from 'prolog-notebook';
|
|
63
111
|
|
|
64
112
|
const session = await createSession();
|
|
65
|
-
session.consult(`
|
|
113
|
+
await session.consult(`
|
|
66
114
|
male(edward). male(alfred).
|
|
67
115
|
father(albert, edward). mother(victoria, edward).
|
|
68
116
|
parent(X, Y) :- father(X, Y) ; mother(X, Y).
|
|
69
117
|
is_son(X) :- male(X), parent(_, X).
|
|
70
|
-
|
|
118
|
+
`, 'cell-family');
|
|
71
119
|
|
|
72
120
|
// Step solutions one at a time, as at the prompt
|
|
73
121
|
const q = session.query('is_son(X)');
|
|
74
122
|
let r;
|
|
75
|
-
while (!(r = q.next()).done) console.log(
|
|
123
|
+
while (!(r = await q.next()).done) console.log(r.text);
|
|
76
124
|
|
|
77
125
|
// …or drain it
|
|
78
|
-
const { solutions } = session.query('is_son(X)').all();
|
|
126
|
+
const { solutions } = await session.query('is_son(X)').all();
|
|
79
127
|
```
|
|
80
128
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
129
|
+
**Every call is awaited**, because in the browser the engine runs in a Web Worker. A Prolog
|
|
130
|
+
query is synchronous WASM, so a goal that never terminates blocks whatever thread it is on —
|
|
131
|
+
and non-termination is *chapter material* in a Prolog book. Running the engine somewhere that
|
|
132
|
+
can be terminated is what turns `loop :- loop.` from a dead tab into a Stop button.
|
|
133
|
+
|
|
134
|
+
In a browser, import from `prolog-notebook/browser` and let it start the worker; the page does
|
|
135
|
+
**not** need a `<script>` tag for the WASM bundle any more, because the worker loads it. See
|
|
136
|
+
[`viewer/index.html`](viewer/index.html) for the whole of a host page, and
|
|
137
|
+
[`src/notebook.js`](src/notebook.js) for the wiring.
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
import { createSession } from 'prolog-notebook/browser';
|
|
141
|
+
const session = await createSession(); // boots the worker
|
|
142
|
+
await session.abort(); // stop a runaway goal; cells are re-consulted
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
To render a notebook file instead of marking up cells by hand — the whole page from one call:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { load } from 'prolog-notebook/page';
|
|
149
|
+
await load('chapter-04-cut.prolog.md'); // parse, render into <main>, wire up the cells
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`prolog-notebook/format` (parse, serialise, `inputHash`) and `prolog-notebook/render` (model to
|
|
153
|
+
HTML strings) are exported separately, and neither touches the DOM — the same two modules back
|
|
154
|
+
the browser, the CLI runner and a future VS Code serializer.
|
|
155
|
+
|
|
156
|
+
Node runs the engine in-process and is deliberately **not** protected: a non-terminating goal
|
|
157
|
+
will hang it. The CLI is not an interactive page, and pretending otherwise would hide the
|
|
158
|
+
difference.
|
|
84
159
|
|
|
85
160
|
### API
|
|
86
161
|
|
|
87
162
|
| | |
|
|
88
163
|
|---|---|
|
|
89
|
-
| `createSession(options?)` | boots SWI-Prolog; returns a
|
|
90
|
-
| `session.consult(text, name?)` | loads a clause base into `user`; `{ok, error
|
|
91
|
-
| `session.query(goal)` | opens a query;
|
|
92
|
-
| `query.next()` | one solution: `{done, solution?, error?}` |
|
|
93
|
-
| `query.all(limit?)` | drains it: `{solutions, error?, truncated}` |
|
|
94
|
-
| `
|
|
164
|
+
| `createSession(options?)` | boots SWI-Prolog; returns a session |
|
|
165
|
+
| `await session.consult(text, name?)` | loads a clause base into `user`; `{ok, error?, messages}` |
|
|
166
|
+
| `session.query(goal)` | opens a query; nothing runs until you pull a solution |
|
|
167
|
+
| `await query.next()` | one solution: `{done, solution?, text?, error?}` |
|
|
168
|
+
| `await query.all(limit?)` | drains it: `{solutions, error?, truncated}` |
|
|
169
|
+
| `await session.abort()` | terminates a running goal and replays the consults |
|
|
170
|
+
| `await session.restart()` | same, without anything needing to be running |
|
|
171
|
+
| `formatSolution(s)` | renders bindings the way a top level would, with no engine |
|
|
172
|
+
|
|
173
|
+
Abort is cheap because of a decision made elsewhere: one cell is one virtual file, so the
|
|
174
|
+
clause store rebuilds from the cells in milliseconds. Terminating the worker also reclaims the
|
|
175
|
+
whole WASM heap, so a runaway loop and a memory blow-up have the same cure. Assert/retract
|
|
176
|
+
state does not survive it — see [`docs/format.md`](docs/format.md) §8.
|
|
95
177
|
|
|
96
178
|
## Two things that will bite you if you build this yourself
|
|
97
179
|
|
|
@@ -99,7 +181,19 @@ Both were found by driving a real browser, not by reading documentation.
|
|
|
99
181
|
|
|
100
182
|
**Module context.** `prolog.query(Goal)` runs with `system` as its context module, while
|
|
101
183
|
`consult/1` loads into `user`. Unqualified goals raise `Unknown procedure: system:foo/1`
|
|
102
|
-
*even though the consult reported success*. Goals are
|
|
184
|
+
*even though the consult reported success*. Goals are read and called in `user`.
|
|
185
|
+
|
|
186
|
+
**One round trip per answer, or the variables come apart.** Formatting each binding with its
|
|
187
|
+
own `term_string/2` call loses the fact that two of them are the *same* variable:
|
|
188
|
+
`app([1,2], Tail, L)` came out as `L = [1,2|_20306], Tail = _20428` where a real toplevel
|
|
189
|
+
prints `L = [1, 2|Tail]`. Render the whole answer inside Prolog, in one call, with the goal's
|
|
190
|
+
own `variable_names`.
|
|
191
|
+
|
|
192
|
+
**The engine version is not the package version, and it must not float.**
|
|
193
|
+
`swipl-wasm@8.0.4` ships SWI-Prolog 10.1.10; `8.0.7` ships 10.1.13. Two installs of the same
|
|
194
|
+
release, ten minutes apart, ran different Prologs. Since a notebook's saved answers are only
|
|
195
|
+
true of the engine that produced them — and SWI's answer spelling changes between releases —
|
|
196
|
+
the dependency is pinned exactly, and moving it is a commit with the chapters re-run in it.
|
|
103
197
|
|
|
104
198
|
**The last solution arrives with `done`.** `next()` can return `{done: true, value: {...}}` —
|
|
105
199
|
a final binding and the end of the search in a single step. Treating `done` as "stop, no more
|
|
@@ -110,18 +204,27 @@ lesson about backtracking quietly teaches the wrong thing.
|
|
|
110
204
|
|
|
111
205
|
Working and tested:
|
|
112
206
|
|
|
113
|
-
- execution core, environment-agnostic (`src/engine.js`),
|
|
207
|
+
- execution core, environment-agnostic (`src/engine.js`), run in a Web Worker in the browser
|
|
114
208
|
- Node entry point, browser entry point
|
|
115
|
-
-
|
|
116
|
-
-
|
|
209
|
+
- **a chapter is a file** — parse, render and mount a `.prolog.md`, cells and all
|
|
210
|
+
- **a chapter is readable cold** — saved answers render with no engine, and are marked stale
|
|
211
|
+
when the program above them has moved
|
|
212
|
+
- program cells and query cells with `Run` / `; next` / `all` / `stop`, per-cell reset, and a
|
|
213
|
+
page that says what the engine is holding
|
|
214
|
+
- `hold` and `rerun="auto"` — the author decides what a reader may see and when it refreshes
|
|
215
|
+
- **the CLI runner**: `prolog-notebook run` executes a chapter headlessly and writes its
|
|
216
|
+
answers back
|
|
217
|
+
- download your own copy of a chapter, answers and all
|
|
218
|
+
- 186 passing tests
|
|
117
219
|
|
|
118
220
|
Not built yet:
|
|
119
221
|
|
|
120
|
-
-
|
|
121
|
-
|
|
222
|
+
- `--check` — run a chapter in CI and fail the build when its answers have drifted. Needs a
|
|
223
|
+
timeout first: a test suite that can hang forever is not a test suite.
|
|
224
|
+
- `build` — a static page a reader can open, without a dev server
|
|
122
225
|
- custom elements (`<prolog-program>`, `<prolog-query>`) so notebooks drop into any static site
|
|
123
226
|
- a VS Code notebook controller — VS Code supplies the UI, this supplies the kernel, still no Python
|
|
124
|
-
-
|
|
227
|
+
- persistence, so a reader's edits survive a reload
|
|
125
228
|
- `trace/0` integration, for visible backtracking — where [prolog-trace-viz][ptv] would plug in
|
|
126
229
|
- syntax highlighting
|
|
127
230
|
|
|
@@ -140,4 +243,4 @@ MIT
|
|
|
140
243
|
|
|
141
244
|
[kernel]: https://github.com/hhu-stups/prolog-jupyter-kernel
|
|
142
245
|
[wasm]: https://github.com/SWI-Prolog/swipl-wasm
|
|
143
|
-
[ptv]: https://github.com/
|
|
246
|
+
[ptv]: https://github.com/textologylabs/prolog-trace-viz
|