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/src/clauses.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// What does this program cell define?
|
|
2
|
+
//
|
|
3
|
+
// A deliberately shallow reading of Prolog source — clause heads only, no terms,
|
|
4
|
+
// no operators, no module qualification. That is not laziness, it is the design:
|
|
5
|
+
// the answer is only ever used to IMPROVE AN ERROR MESSAGE that the engine has
|
|
6
|
+
// already produced. A head this misses costs a hint; a head it invents costs a
|
|
7
|
+
// hint that is wrong about a predicate nobody asked about. Neither can change
|
|
8
|
+
// what a query does, which is why a regex is an honest tool here and would not
|
|
9
|
+
// be anywhere near the execution path.
|
|
10
|
+
//
|
|
11
|
+
// DOM-free and engine-free, like format.js: the same reading serves the browser,
|
|
12
|
+
// the CLI runner and eventually the `:- dynamic` detection that 869eddzfp needs.
|
|
13
|
+
|
|
14
|
+
/** `name` and `'quoted name'`, the two spellings of a functor. */
|
|
15
|
+
const FUNCTOR = /^(?:'((?:[^'\\]|\\.)*)'|([a-z][a-zA-Z0-9_]*))/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The predicate indicators a cell defines, as `name/arity`.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} source Prolog text
|
|
21
|
+
* @returns {Set<string>}
|
|
22
|
+
*/
|
|
23
|
+
export function definedPredicates(source) {
|
|
24
|
+
const found = new Set();
|
|
25
|
+
for (const line of clauseStarts(source)) {
|
|
26
|
+
const indicator = headOf(line);
|
|
27
|
+
if (indicator) found.add(indicator);
|
|
28
|
+
}
|
|
29
|
+
return found;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Lines that can begin a clause.
|
|
34
|
+
*
|
|
35
|
+
* A clause head starts at column 0 — the same rule the notebook format relies on
|
|
36
|
+
* for cells (format §1), and the reason both can be line scanners. Continuation
|
|
37
|
+
* lines of a clause body are indented by every convention in use, including
|
|
38
|
+
* SWI's own portray_clause.
|
|
39
|
+
*/
|
|
40
|
+
function clauseStarts(source) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
let inBlockComment = false;
|
|
43
|
+
for (const raw of source.split('\n')) {
|
|
44
|
+
let line = raw;
|
|
45
|
+
if (inBlockComment) {
|
|
46
|
+
const end = line.indexOf('*/');
|
|
47
|
+
if (end === -1) continue;
|
|
48
|
+
line = line.slice(end + 2);
|
|
49
|
+
inBlockComment = false;
|
|
50
|
+
}
|
|
51
|
+
// A block comment opening on this line takes the rest of it with it.
|
|
52
|
+
const open = line.indexOf('/*');
|
|
53
|
+
if (open !== -1 && line.indexOf('*/', open) === -1) {
|
|
54
|
+
inBlockComment = true;
|
|
55
|
+
line = line.slice(0, open);
|
|
56
|
+
}
|
|
57
|
+
if (/^\s/.test(line) || line.trim() === '') continue;
|
|
58
|
+
if (line.startsWith('%')) continue;
|
|
59
|
+
// A directive is an instruction to the loader, not a definition. `:- dynamic
|
|
60
|
+
// counter/1.` declares one, but the clauses are still what define it.
|
|
61
|
+
if (line.startsWith(':-') || line.startsWith('?-')) continue;
|
|
62
|
+
lines.push(line);
|
|
63
|
+
}
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `foo(a, b) :- …` → `foo/2`. Null if the line does not start with a functor.
|
|
69
|
+
*/
|
|
70
|
+
function headOf(line) {
|
|
71
|
+
const m = FUNCTOR.exec(line);
|
|
72
|
+
if (!m) return null;
|
|
73
|
+
const name = m[1] !== undefined ? m[1].replace(/\\(.)/g, '$1') : m[2];
|
|
74
|
+
const rest = line.slice(m[0].length);
|
|
75
|
+
|
|
76
|
+
let arity = 0;
|
|
77
|
+
let after = rest;
|
|
78
|
+
if (rest.startsWith('(')) {
|
|
79
|
+
const args = countArguments(rest);
|
|
80
|
+
if (args === null) return null;
|
|
81
|
+
arity = args.count;
|
|
82
|
+
after = rest.slice(args.end + 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A DCG rule defines a predicate with two extra arguments — the difference list
|
|
86
|
+
// SWI threads through it. `greeting --> [hello]` is greeting/2, and a reader
|
|
87
|
+
// told otherwise would go looking for greeting/0.
|
|
88
|
+
if (/^\s*-->/.test(after)) arity += 2;
|
|
89
|
+
|
|
90
|
+
return `${name}/${arity}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Count top-level arguments in `(…)`, respecting nesting and quotes.
|
|
95
|
+
*
|
|
96
|
+
* @returns {{count: number, end: number}|null} null if the parenthesis never closes
|
|
97
|
+
*/
|
|
98
|
+
function countArguments(text) {
|
|
99
|
+
let depth = 0;
|
|
100
|
+
let count = 1;
|
|
101
|
+
let quote = null;
|
|
102
|
+
for (let i = 0; i < text.length; i++) {
|
|
103
|
+
const c = text[i];
|
|
104
|
+
if (quote) {
|
|
105
|
+
if (c === '\\') i++;
|
|
106
|
+
else if (c === quote) quote = null;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (c === "'" || c === '"' || c === '`') { quote = c; continue; }
|
|
110
|
+
if (c === '(' || c === '[' || c === '{') { depth++; continue; }
|
|
111
|
+
if (c === ')' || c === ']' || c === '}') {
|
|
112
|
+
depth--;
|
|
113
|
+
if (depth === 0) return { count, end: i };
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// `foo(a, b)` has two arguments; `foo()` is not valid Prolog, so a comma at
|
|
117
|
+
// depth 1 is always an argument separator.
|
|
118
|
+
if (c === ',' && depth === 1) count++;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The predicates a cell declares `:- dynamic`, as `name/arity`.
|
|
125
|
+
*
|
|
126
|
+
* WHY THIS IS WORTH KNOWING WITHOUT RUNNING ANYTHING (format §8): a cell that
|
|
127
|
+
* declares one is **stateful**. Its assert/retract state lives in no file, so
|
|
128
|
+
* re-consulting the cell does not undo it and neither does resetting the cell —
|
|
129
|
+
* only throwing the engine away does. That is the one place where the otherwise
|
|
130
|
+
* reliable promise "the clause store self-heals" stops being true, and a reader
|
|
131
|
+
* who does not know it will conclude something false about Prolog rather than
|
|
132
|
+
* about us.
|
|
133
|
+
*
|
|
134
|
+
* Read statically so the page can say so BEFORE the reader has asserted anything,
|
|
135
|
+
* rather than after they are already confused. Shallow like the rest of this file
|
|
136
|
+
* and for the same reason: at worst it fails to warn, and it can never change
|
|
137
|
+
* what a goal does.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} source Prolog text
|
|
140
|
+
* @returns {Set<string>} predicate indicators
|
|
141
|
+
*/
|
|
142
|
+
export function declaredDynamic(source) {
|
|
143
|
+
const found = new Set();
|
|
144
|
+
for (const body of directives(source)) {
|
|
145
|
+
// `:- dynamic foo/1.` and `:- dynamic(foo/1).` are the same declaration.
|
|
146
|
+
const m = /^dynamic\b\s*(.*)$/s.exec(body);
|
|
147
|
+
if (!m) continue;
|
|
148
|
+
let list = m[1].trim();
|
|
149
|
+
if (list.startsWith('(') && list.endsWith(')')) list = list.slice(1, -1);
|
|
150
|
+
for (const item of splitTopLevel(list)) {
|
|
151
|
+
const indicator = /^\s*(?:'((?:[^'\\]|\\.)*)'|([a-z][a-zA-Z0-9_]*))\s*\/\s*(\d+)\s*$/.exec(item);
|
|
152
|
+
if (indicator) {
|
|
153
|
+
const name = indicator[1] !== undefined ? indicator[1].replace(/\\(.)/g, '$1') : indicator[2];
|
|
154
|
+
found.add(`${name}/${indicator[3]}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return found;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The body of every `:- …` directive, with comments stripped.
|
|
163
|
+
*
|
|
164
|
+
* Directives wrap across lines far more often than clauses do — a chapter that
|
|
165
|
+
* declares six dynamic predicates will list them one per line — so this cannot be
|
|
166
|
+
* the line scanner the rest of the file uses. It reads to the terminating full
|
|
167
|
+
* stop instead.
|
|
168
|
+
*/
|
|
169
|
+
function directives(source) {
|
|
170
|
+
const bodies = [];
|
|
171
|
+
const text = stripComments(source);
|
|
172
|
+
const pattern = /(^|\n)\s*:-\s*/g;
|
|
173
|
+
let m;
|
|
174
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
175
|
+
const start = m.index + m[0].length;
|
|
176
|
+
const end = endOfTerm(text, start);
|
|
177
|
+
if (end === -1) break;
|
|
178
|
+
bodies.push(text.slice(start, end).trim());
|
|
179
|
+
pattern.lastIndex = end;
|
|
180
|
+
}
|
|
181
|
+
return bodies;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Index of the `.` that ends a term, skipping quotes. -1 if it never ends. */
|
|
185
|
+
function endOfTerm(text, from) {
|
|
186
|
+
let quote = null;
|
|
187
|
+
for (let i = from; i < text.length; i++) {
|
|
188
|
+
const c = text[i];
|
|
189
|
+
if (quote) {
|
|
190
|
+
if (c === '\\') i++;
|
|
191
|
+
else if (c === quote) quote = null;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (c === "'" || c === '"' || c === '`') { quote = c; continue; }
|
|
195
|
+
// A full stop ends a term only when whitespace or the end of input follows,
|
|
196
|
+
// which is exactly SWI's own rule — otherwise `1.5` would end one.
|
|
197
|
+
if (c === '.' && (i + 1 === text.length || /\s/.test(text[i + 1]))) return i;
|
|
198
|
+
}
|
|
199
|
+
return -1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stripComments(source) {
|
|
203
|
+
return source
|
|
204
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
205
|
+
.split('\n')
|
|
206
|
+
.map((line) => line.replace(/(^|\s)%.*$/, '$1'))
|
|
207
|
+
.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Split on commas that are not inside brackets or quotes. */
|
|
211
|
+
function splitTopLevel(text) {
|
|
212
|
+
const parts = [];
|
|
213
|
+
let depth = 0;
|
|
214
|
+
let quote = null;
|
|
215
|
+
let start = 0;
|
|
216
|
+
for (let i = 0; i < text.length; i++) {
|
|
217
|
+
const c = text[i];
|
|
218
|
+
if (quote) {
|
|
219
|
+
if (c === '\\') i++;
|
|
220
|
+
else if (c === quote) quote = null;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (c === "'" || c === '"' || c === '`') { quote = c; continue; }
|
|
224
|
+
if (c === '(' || c === '[' || c === '{') depth++;
|
|
225
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
226
|
+
else if (c === ',' && depth === 0) { parts.push(text.slice(start, i)); start = i + 1; }
|
|
227
|
+
}
|
|
228
|
+
parts.push(text.slice(start));
|
|
229
|
+
return parts;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The predicate indicator an "Unknown procedure" error is complaining about. */
|
|
233
|
+
export function unknownProcedure(message) {
|
|
234
|
+
const m = /Unknown procedure:\s*(?:[a-z][a-zA-Z0-9_]*:)?((?:'[^']*'|[^\s/]+)\/\d+)/.exec(message ?? '');
|
|
235
|
+
return m ? m[1] : null;
|
|
236
|
+
}
|