eyeling 1.23.1 → 1.23.3
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/HANDBOOK.md +19 -7
- package/dist/browser/eyeling.browser.js +137 -565
- package/examples/builtin/queens.js +141 -0
- package/{lib/builtin-sudoku.js → examples/builtin/sudoku.js} +15 -0
- package/examples/output/queens.txt +21 -0
- package/examples/queens.n3 +20 -0
- package/examples/sudoku.n3 +7 -1
- package/eyeling.js +147 -562
- package/lib/cli.js +3 -75
- package/lib/engine.js +0 -4
- package/lib/multisource.js +133 -14
- package/package.json +1 -1
- package/test/examples.test.js +33 -10
package/eyeling.js
CHANGED
|
@@ -8,474 +8,6 @@
|
|
|
8
8
|
const __cache = Object.create(null);
|
|
9
9
|
|
|
10
10
|
// ---- bundled modules ----
|
|
11
|
-
__modules["lib/builtin-sudoku.js"] = function(require, module, exports){
|
|
12
|
-
'use strict';
|
|
13
|
-
|
|
14
|
-
module.exports = function registerSudokuBuiltins(api) {
|
|
15
|
-
const { registerBuiltin, internLiteral, termToJsString, unifyTerm, terms } = api;
|
|
16
|
-
const { Var } = terms;
|
|
17
|
-
|
|
18
|
-
const SUDOKU_NS = 'http://example.org/sudoku-builtin#';
|
|
19
|
-
const __sudokuReportCache = new Map();
|
|
20
|
-
const __SUDOKU_ALL = 0x1ff;
|
|
21
|
-
|
|
22
|
-
function makeStringLiteral(str) {
|
|
23
|
-
return internLiteral(JSON.stringify(str));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function digitMask(v) {
|
|
27
|
-
return 1 << (v - 1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function boxIndex(r, c) {
|
|
31
|
-
return Math.floor(r / 3) * 3 + Math.floor(c / 3);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function popcount(mask) {
|
|
35
|
-
let n = 0;
|
|
36
|
-
while (mask) {
|
|
37
|
-
mask &= mask - 1;
|
|
38
|
-
n += 1;
|
|
39
|
-
}
|
|
40
|
-
return n;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function maskToDigits(mask) {
|
|
44
|
-
const out = [];
|
|
45
|
-
for (let d = 1; d <= 9; d += 1) if (mask & digitMask(d)) out.push(d);
|
|
46
|
-
return out;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function formatBoard(cells) {
|
|
50
|
-
let out = '';
|
|
51
|
-
for (let r = 0; r < 9; r += 1) {
|
|
52
|
-
if (r > 0 && r % 3 === 0) out += '\n';
|
|
53
|
-
for (let c = 0; c < 9; c += 1) {
|
|
54
|
-
if (c > 0 && c % 3 === 0) out += '| ';
|
|
55
|
-
const v = cells[r * 9 + c];
|
|
56
|
-
out += v === 0 ? '. ' : `${String(v)} `;
|
|
57
|
-
}
|
|
58
|
-
out += '\n';
|
|
59
|
-
}
|
|
60
|
-
return out;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function parsePuzzle(input) {
|
|
64
|
-
const filtered = [];
|
|
65
|
-
for (const ch of input) {
|
|
66
|
-
if (/\s/.test(ch) || ch === '|' || ch === '+') continue;
|
|
67
|
-
filtered.push(ch);
|
|
68
|
-
}
|
|
69
|
-
if (filtered.length !== 81) {
|
|
70
|
-
return { error: `Expected exactly 81 cells after removing whitespace, but found ${filtered.length}.` };
|
|
71
|
-
}
|
|
72
|
-
const cells = new Array(81).fill(0);
|
|
73
|
-
for (let i = 0; i < 81; i += 1) {
|
|
74
|
-
const ch = filtered[i];
|
|
75
|
-
if (ch >= '1' && ch <= '9') cells[i] = ch.charCodeAt(0) - 48;
|
|
76
|
-
else if (ch === '0' || ch === '.' || ch === '_') cells[i] = 0;
|
|
77
|
-
else return { error: `Unexpected character '${ch}' at position ${i + 1}.` };
|
|
78
|
-
}
|
|
79
|
-
return { cells };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function attachMethods(state) {
|
|
83
|
-
state.place = function place(idx, value) {
|
|
84
|
-
if (this.cells[idx] !== 0) return this.cells[idx] === value;
|
|
85
|
-
const row = Math.floor(idx / 9);
|
|
86
|
-
const col = idx % 9;
|
|
87
|
-
const bx = boxIndex(row, col);
|
|
88
|
-
const bit = digitMask(value);
|
|
89
|
-
if (((this.rowUsed[row] | this.colUsed[col] | this.boxUsed[bx]) & bit) !== 0) return false;
|
|
90
|
-
this.cells[idx] = value;
|
|
91
|
-
this.rowUsed[row] |= bit;
|
|
92
|
-
this.colUsed[col] |= bit;
|
|
93
|
-
this.boxUsed[bx] |= bit;
|
|
94
|
-
return true;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
state.candidates = function candidates(idx) {
|
|
98
|
-
const row = Math.floor(idx / 9);
|
|
99
|
-
const col = idx % 9;
|
|
100
|
-
const bx = boxIndex(row, col);
|
|
101
|
-
return __SUDOKU_ALL & ~(this.rowUsed[row] | this.colUsed[col] | this.boxUsed[bx]);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
state.clone = function clone() {
|
|
105
|
-
return attachMethods({
|
|
106
|
-
cells: this.cells.slice(),
|
|
107
|
-
rowUsed: this.rowUsed.slice(),
|
|
108
|
-
colUsed: this.colUsed.slice(),
|
|
109
|
-
boxUsed: this.boxUsed.slice(),
|
|
110
|
-
moves: this.moves.slice(),
|
|
111
|
-
});
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
return state;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function stateFromPuzzle(cells) {
|
|
118
|
-
const state = attachMethods({
|
|
119
|
-
cells: new Array(81).fill(0),
|
|
120
|
-
rowUsed: new Array(9).fill(0),
|
|
121
|
-
colUsed: new Array(9).fill(0),
|
|
122
|
-
boxUsed: new Array(9).fill(0),
|
|
123
|
-
moves: [],
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
for (let idx = 0; idx < 81; idx += 1) {
|
|
127
|
-
const value = cells[idx];
|
|
128
|
-
if (value === 0) continue;
|
|
129
|
-
if (value < 1 || value > 9) {
|
|
130
|
-
return { error: `Cell ${idx + 1} contains ${value}, but only digits 1-9 or 0/. are allowed.` };
|
|
131
|
-
}
|
|
132
|
-
if (!state.place(idx, value)) {
|
|
133
|
-
const row = Math.floor(idx / 9) + 1;
|
|
134
|
-
const col = (idx % 9) + 1;
|
|
135
|
-
return { error: `The given clues already conflict at row ${row}, column ${col}.` };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return { state };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function summarizeMoves(moves, limit) {
|
|
143
|
-
if (!moves.length) return 'no placements were needed';
|
|
144
|
-
const parts = [];
|
|
145
|
-
for (const mv of moves.slice(0, limit)) {
|
|
146
|
-
const row = Math.floor(mv.index / 9) + 1;
|
|
147
|
-
const col = (mv.index % 9) + 1;
|
|
148
|
-
const mode = mv.forced ? 'forced' : 'guess';
|
|
149
|
-
parts.push(`r${row}c${col}=${mv.value}: ${mode}`);
|
|
150
|
-
}
|
|
151
|
-
if (moves.length > limit) parts.push(`… and ${moves.length - limit} more placements`);
|
|
152
|
-
return parts.join(', ');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function unitIsComplete(values) {
|
|
156
|
-
let seen = 0;
|
|
157
|
-
for (const v of values) {
|
|
158
|
-
if (v < 1 || v > 9) return false;
|
|
159
|
-
const bit = digitMask(v);
|
|
160
|
-
if (seen & bit) return false;
|
|
161
|
-
seen |= bit;
|
|
162
|
-
}
|
|
163
|
-
return seen === __SUDOKU_ALL;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function replayMovesAreLegal(puzzleCells, moves) {
|
|
167
|
-
const init = stateFromPuzzle(puzzleCells);
|
|
168
|
-
if (init.error) return false;
|
|
169
|
-
const state = init.state;
|
|
170
|
-
for (const mv of moves) {
|
|
171
|
-
if (state.cells[mv.index] !== 0) return false;
|
|
172
|
-
const maskNow = state.candidates(mv.index);
|
|
173
|
-
if (maskNow !== mv.candidatesMask) return false;
|
|
174
|
-
if ((maskNow & digitMask(mv.value)) === 0) return false;
|
|
175
|
-
if (mv.forced && popcount(maskNow) !== 1) return false;
|
|
176
|
-
if (!state.place(mv.index, mv.value)) return false;
|
|
177
|
-
}
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function propagateSingles(state, stats) {
|
|
182
|
-
for (;;) {
|
|
183
|
-
let progress = false;
|
|
184
|
-
for (let idx = 0; idx < 81; idx += 1) {
|
|
185
|
-
if (state.cells[idx] !== 0) continue;
|
|
186
|
-
const mask = state.candidates(idx);
|
|
187
|
-
const count = popcount(mask);
|
|
188
|
-
if (count === 0) return false;
|
|
189
|
-
if (count === 1) {
|
|
190
|
-
const digit = maskToDigits(mask)[0];
|
|
191
|
-
state.moves.push({ index: idx, value: digit, candidatesMask: mask, forced: true });
|
|
192
|
-
if (!state.place(idx, digit)) return false;
|
|
193
|
-
stats.forcedMoves += 1;
|
|
194
|
-
progress = true;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (!progress) return true;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function selectUnfilledCell(state) {
|
|
202
|
-
let best = null;
|
|
203
|
-
for (let idx = 0; idx < 81; idx += 1) {
|
|
204
|
-
if (state.cells[idx] !== 0) continue;
|
|
205
|
-
const mask = state.candidates(idx);
|
|
206
|
-
const count = popcount(mask);
|
|
207
|
-
if (best === null || count < best.count) best = { idx, mask, count };
|
|
208
|
-
if (count === 2) break;
|
|
209
|
-
}
|
|
210
|
-
return best;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function solve(state, stats, depth) {
|
|
214
|
-
stats.recursiveNodes += 1;
|
|
215
|
-
if (depth > stats.maxDepth) stats.maxDepth = depth;
|
|
216
|
-
const current = state.clone();
|
|
217
|
-
if (!propagateSingles(current, stats)) {
|
|
218
|
-
stats.backtracks += 1;
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
const best = selectUnfilledCell(current);
|
|
222
|
-
if (!best) return current;
|
|
223
|
-
for (const digit of maskToDigits(best.mask)) {
|
|
224
|
-
const next = current.clone();
|
|
225
|
-
const candidatesMask = next.candidates(best.idx);
|
|
226
|
-
next.moves.push({ index: best.idx, value: digit, candidatesMask, forced: false });
|
|
227
|
-
stats.guessedMoves += 1;
|
|
228
|
-
if (!next.place(best.idx, digit)) continue;
|
|
229
|
-
const solved = solve(next, stats, depth + 1);
|
|
230
|
-
if (solved) return solved;
|
|
231
|
-
}
|
|
232
|
-
stats.backtracks += 1;
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function countSolutions(state, limit, countRef) {
|
|
237
|
-
if (countRef.count >= limit) return;
|
|
238
|
-
const current = state.clone();
|
|
239
|
-
const dummy = {
|
|
240
|
-
givens: 0,
|
|
241
|
-
blanks: 0,
|
|
242
|
-
forcedMoves: 0,
|
|
243
|
-
guessedMoves: 0,
|
|
244
|
-
recursiveNodes: 0,
|
|
245
|
-
backtracks: 0,
|
|
246
|
-
maxDepth: 0,
|
|
247
|
-
};
|
|
248
|
-
if (!propagateSingles(current, dummy)) return;
|
|
249
|
-
const best = selectUnfilledCell(current);
|
|
250
|
-
if (!best) {
|
|
251
|
-
countRef.count += 1;
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
for (const digit of maskToDigits(best.mask)) {
|
|
255
|
-
if (countRef.count >= limit) return;
|
|
256
|
-
const next = current.clone();
|
|
257
|
-
if (next.place(best.idx, digit)) countSolutions(next, limit, countRef);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function computeReport(term) {
|
|
262
|
-
const raw = termToJsString(term);
|
|
263
|
-
if (raw === null) return null;
|
|
264
|
-
if (__sudokuReportCache.has(raw)) return __sudokuReportCache.get(raw);
|
|
265
|
-
|
|
266
|
-
const parsed = parsePuzzle(raw);
|
|
267
|
-
if (parsed.error) {
|
|
268
|
-
const rep = { status: 'invalid-input', error: parsed.error, raw, normalized: null };
|
|
269
|
-
__sudokuReportCache.set(raw, rep);
|
|
270
|
-
return rep;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const normalized = parsed.cells.join('');
|
|
274
|
-
const init = stateFromPuzzle(parsed.cells);
|
|
275
|
-
if (init.error) {
|
|
276
|
-
const rep = {
|
|
277
|
-
status: 'illegal-clues',
|
|
278
|
-
error: init.error,
|
|
279
|
-
raw,
|
|
280
|
-
normalized,
|
|
281
|
-
givens: parsed.cells.filter((v) => v !== 0).length,
|
|
282
|
-
blanks: parsed.cells.filter((v) => v === 0).length,
|
|
283
|
-
puzzleText: formatBoard(parsed.cells),
|
|
284
|
-
};
|
|
285
|
-
__sudokuReportCache.set(raw, rep);
|
|
286
|
-
return rep;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const initial = init.state;
|
|
290
|
-
const stats = {
|
|
291
|
-
givens: parsed.cells.filter((v) => v !== 0).length,
|
|
292
|
-
blanks: parsed.cells.filter((v) => v === 0).length,
|
|
293
|
-
forcedMoves: 0,
|
|
294
|
-
guessedMoves: 0,
|
|
295
|
-
recursiveNodes: 0,
|
|
296
|
-
backtracks: 0,
|
|
297
|
-
maxDepth: 0,
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const solved = solve(initial, stats, 0);
|
|
301
|
-
if (!solved) {
|
|
302
|
-
const rep = {
|
|
303
|
-
status: 'unsatisfiable',
|
|
304
|
-
raw,
|
|
305
|
-
normalized,
|
|
306
|
-
givens: stats.givens,
|
|
307
|
-
blanks: stats.blanks,
|
|
308
|
-
recursiveNodes: stats.recursiveNodes,
|
|
309
|
-
backtracks: stats.backtracks,
|
|
310
|
-
puzzleText: formatBoard(parsed.cells),
|
|
311
|
-
};
|
|
312
|
-
__sudokuReportCache.set(raw, rep);
|
|
313
|
-
return rep;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const countRef = { count: 0 };
|
|
317
|
-
countSolutions(initial, 2, countRef);
|
|
318
|
-
|
|
319
|
-
const givensPreserved = parsed.cells.every((v, i) => v === 0 || v === solved.cells[i]);
|
|
320
|
-
const noBlanks = solved.cells.every((v) => v >= 1 && v <= 9);
|
|
321
|
-
const rowsComplete = Array.from({ length: 9 }, (_, r) =>
|
|
322
|
-
unitIsComplete(solved.cells.slice(r * 9, r * 9 + 9)),
|
|
323
|
-
).every(Boolean);
|
|
324
|
-
const colsComplete = Array.from({ length: 9 }, (_, c) =>
|
|
325
|
-
unitIsComplete(Array.from({ length: 9 }, (_, r) => solved.cells[r * 9 + c])),
|
|
326
|
-
).every(Boolean);
|
|
327
|
-
const boxesComplete = Array.from({ length: 9 }, (_, b) => {
|
|
328
|
-
const br = Math.floor(b / 3) * 3;
|
|
329
|
-
const bc = (b % 3) * 3;
|
|
330
|
-
const vals = [];
|
|
331
|
-
for (let dr = 0; dr < 3; dr += 1) {
|
|
332
|
-
for (let dc = 0; dc < 3; dc += 1) vals.push(solved.cells[(br + dr) * 9 + (bc + dc)]);
|
|
333
|
-
}
|
|
334
|
-
return unitIsComplete(vals);
|
|
335
|
-
}).every(Boolean);
|
|
336
|
-
const replayLegal = replayMovesAreLegal(parsed.cells, solved.moves);
|
|
337
|
-
const proofPathGuessCount = solved.moves.filter((m) => !m.forced).length;
|
|
338
|
-
const storyConsistent =
|
|
339
|
-
stats.recursiveNodes >= 1 &&
|
|
340
|
-
stats.maxDepth <= stats.blanks &&
|
|
341
|
-
solved.moves.length === stats.blanks &&
|
|
342
|
-
proofPathGuessCount <= stats.guessedMoves;
|
|
343
|
-
|
|
344
|
-
const rep = {
|
|
345
|
-
status: 'ok',
|
|
346
|
-
raw,
|
|
347
|
-
normalized,
|
|
348
|
-
givens: stats.givens,
|
|
349
|
-
blanks: stats.blanks,
|
|
350
|
-
forcedMoves: stats.forcedMoves,
|
|
351
|
-
guessedMoves: stats.guessedMoves,
|
|
352
|
-
recursiveNodes: stats.recursiveNodes,
|
|
353
|
-
backtracks: stats.backtracks,
|
|
354
|
-
maxDepth: stats.maxDepth,
|
|
355
|
-
unique: countRef.count === 1,
|
|
356
|
-
solution: solved.cells.join(''),
|
|
357
|
-
puzzleText: formatBoard(parsed.cells),
|
|
358
|
-
solutionText: formatBoard(solved.cells),
|
|
359
|
-
moveSummary: summarizeMoves(solved.moves, 8),
|
|
360
|
-
moveCount: solved.moves.length,
|
|
361
|
-
givensPreserved,
|
|
362
|
-
noBlanks,
|
|
363
|
-
rowsComplete,
|
|
364
|
-
colsComplete,
|
|
365
|
-
boxesComplete,
|
|
366
|
-
replayLegal,
|
|
367
|
-
storyConsistent,
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
__sudokuReportCache.set(raw, rep);
|
|
371
|
-
return rep;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function reportFieldAsTerm(report, field) {
|
|
375
|
-
if (!report) return null;
|
|
376
|
-
if (field === 'status') return makeStringLiteral(report.status);
|
|
377
|
-
if (field === 'error') return report.error ? makeStringLiteral(report.error) : null;
|
|
378
|
-
if (field === 'normalizedPuzzle') return report.normalized ? makeStringLiteral(report.normalized) : null;
|
|
379
|
-
if (field === 'solution') return report.solution ? makeStringLiteral(report.solution) : null;
|
|
380
|
-
if (field === 'puzzleText') return report.puzzleText ? makeStringLiteral(report.puzzleText) : null;
|
|
381
|
-
if (field === 'solutionText') return report.solutionText ? makeStringLiteral(report.solutionText) : null;
|
|
382
|
-
if (field === 'moveSummary') return report.moveSummary ? makeStringLiteral(report.moveSummary) : null;
|
|
383
|
-
if (field === 'givensPreservedText')
|
|
384
|
-
return report.givensPreserved === undefined ? null : makeStringLiteral(report.givensPreserved ? 'OK' : 'failed');
|
|
385
|
-
if (field === 'noBlanksText')
|
|
386
|
-
return report.noBlanks === undefined ? null : makeStringLiteral(report.noBlanks ? 'OK' : 'failed');
|
|
387
|
-
if (field === 'rowsCompleteText')
|
|
388
|
-
return report.rowsComplete === undefined ? null : makeStringLiteral(report.rowsComplete ? 'OK' : 'failed');
|
|
389
|
-
if (field === 'colsCompleteText')
|
|
390
|
-
return report.colsComplete === undefined ? null : makeStringLiteral(report.colsComplete ? 'OK' : 'failed');
|
|
391
|
-
if (field === 'boxesCompleteText')
|
|
392
|
-
return report.boxesComplete === undefined ? null : makeStringLiteral(report.boxesComplete ? 'OK' : 'failed');
|
|
393
|
-
if (field === 'replayLegalText')
|
|
394
|
-
return report.replayLegal === undefined ? null : makeStringLiteral(report.replayLegal ? 'OK' : 'failed');
|
|
395
|
-
if (field === 'storyConsistentText')
|
|
396
|
-
return report.storyConsistent === undefined ? null : makeStringLiteral(report.storyConsistent ? 'OK' : 'failed');
|
|
397
|
-
|
|
398
|
-
const boolFields = [
|
|
399
|
-
'unique',
|
|
400
|
-
'givensPreserved',
|
|
401
|
-
'noBlanks',
|
|
402
|
-
'rowsComplete',
|
|
403
|
-
'colsComplete',
|
|
404
|
-
'boxesComplete',
|
|
405
|
-
'replayLegal',
|
|
406
|
-
'storyConsistent',
|
|
407
|
-
];
|
|
408
|
-
if (boolFields.includes(field))
|
|
409
|
-
return report[field] === undefined ? null : internLiteral(report[field] ? 'true' : 'false');
|
|
410
|
-
|
|
411
|
-
const numberFields = [
|
|
412
|
-
'givens',
|
|
413
|
-
'blanks',
|
|
414
|
-
'forcedMoves',
|
|
415
|
-
'guessedMoves',
|
|
416
|
-
'recursiveNodes',
|
|
417
|
-
'backtracks',
|
|
418
|
-
'maxDepth',
|
|
419
|
-
'moveCount',
|
|
420
|
-
];
|
|
421
|
-
if (numberFields.includes(field)) return report[field] === undefined ? null : internLiteral(String(report[field]));
|
|
422
|
-
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function evalSudokuField(goal, subst, field) {
|
|
427
|
-
const report = computeReport(goal.s);
|
|
428
|
-
if (!report) return [];
|
|
429
|
-
const term = reportFieldAsTerm(report, field);
|
|
430
|
-
if (!term) return [];
|
|
431
|
-
if (goal.o instanceof Var) {
|
|
432
|
-
const s2 = { ...subst };
|
|
433
|
-
s2[goal.o.name] = term;
|
|
434
|
-
return [s2];
|
|
435
|
-
}
|
|
436
|
-
const s2 = unifyTerm(goal.o, term, subst);
|
|
437
|
-
return s2 !== null ? [s2] : [];
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const fields = [
|
|
441
|
-
'status',
|
|
442
|
-
'error',
|
|
443
|
-
'normalizedPuzzle',
|
|
444
|
-
'solution',
|
|
445
|
-
'givens',
|
|
446
|
-
'blanks',
|
|
447
|
-
'forcedMoves',
|
|
448
|
-
'guessedMoves',
|
|
449
|
-
'recursiveNodes',
|
|
450
|
-
'backtracks',
|
|
451
|
-
'maxDepth',
|
|
452
|
-
'unique',
|
|
453
|
-
'givensPreserved',
|
|
454
|
-
'noBlanks',
|
|
455
|
-
'rowsComplete',
|
|
456
|
-
'colsComplete',
|
|
457
|
-
'boxesComplete',
|
|
458
|
-
'replayLegal',
|
|
459
|
-
'storyConsistent',
|
|
460
|
-
'givensPreservedText',
|
|
461
|
-
'noBlanksText',
|
|
462
|
-
'rowsCompleteText',
|
|
463
|
-
'colsCompleteText',
|
|
464
|
-
'boxesCompleteText',
|
|
465
|
-
'replayLegalText',
|
|
466
|
-
'storyConsistentText',
|
|
467
|
-
'moveSummary',
|
|
468
|
-
'puzzleText',
|
|
469
|
-
'solutionText',
|
|
470
|
-
'moveCount',
|
|
471
|
-
];
|
|
472
|
-
|
|
473
|
-
for (const field of fields) {
|
|
474
|
-
registerBuiltin(SUDOKU_NS + field, ({ goal, subst }) => evalSudokuField(goal, subst, field));
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
};
|
|
479
11
|
__modules["lib/builtins.js"] = function(require, module, exports){
|
|
480
12
|
/**
|
|
481
13
|
* Eyeling Reasoner — builtins
|
|
@@ -5116,6 +4648,8 @@ function main() {
|
|
|
5116
4648
|
parseN3Text(text, {
|
|
5117
4649
|
baseIri: __sourceLabelToBaseIri(sourceLabel),
|
|
5118
4650
|
label: sourceLabel,
|
|
4651
|
+
collectUsedPrefixes: true,
|
|
4652
|
+
keepSourceArtifacts: false,
|
|
5119
4653
|
}),
|
|
5120
4654
|
);
|
|
5121
4655
|
} catch (e) {
|
|
@@ -5133,8 +4667,6 @@ function main() {
|
|
|
5133
4667
|
const frules = mergedDocument.frules;
|
|
5134
4668
|
const brules = mergedDocument.brules;
|
|
5135
4669
|
const qrules = mergedDocument.logQueryRules;
|
|
5136
|
-
const tokenSets = parsedSources.map((source) => ({ tokens: source.tokens, prefixes: source.prefixes }));
|
|
5137
|
-
|
|
5138
4670
|
if (showAst) {
|
|
5139
4671
|
function astReplacer(unusedJsonKey, value) {
|
|
5140
4672
|
if (value instanceof Set) return Array.from(value);
|
|
@@ -5190,75 +4722,6 @@ function main() {
|
|
|
5190
4722
|
// In --stream mode we print prefixes *before* any derivations happen.
|
|
5191
4723
|
// To keep the header small and stable, emit only prefixes that are actually
|
|
5192
4724
|
// used (as QNames) in the *input* N3 program.
|
|
5193
|
-
function prefixesUsedInInputTokens(toks2, prefEnv) {
|
|
5194
|
-
const used = new Set();
|
|
5195
|
-
|
|
5196
|
-
function maybeAddFromQName(name) {
|
|
5197
|
-
if (typeof name !== 'string') return;
|
|
5198
|
-
if (!name.includes(':')) return;
|
|
5199
|
-
if (name.startsWith('_:')) return; // blank node
|
|
5200
|
-
|
|
5201
|
-
// Split only on the first ':'
|
|
5202
|
-
const idx = name.indexOf(':');
|
|
5203
|
-
const p = name.slice(0, idx); // may be '' for ":foo"
|
|
5204
|
-
|
|
5205
|
-
// Ignore things like "http://..." unless that prefix is actually defined.
|
|
5206
|
-
if (!Object.prototype.hasOwnProperty.call(prefEnv.map, p)) return;
|
|
5207
|
-
|
|
5208
|
-
used.add(p);
|
|
5209
|
-
}
|
|
5210
|
-
|
|
5211
|
-
for (let i = 0; i < toks2.length; i++) {
|
|
5212
|
-
const t = toks2[i];
|
|
5213
|
-
|
|
5214
|
-
// Skip @prefix ... .
|
|
5215
|
-
if (t.typ === 'AtPrefix') {
|
|
5216
|
-
while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
|
|
5217
|
-
continue;
|
|
5218
|
-
}
|
|
5219
|
-
// Skip @base ... .
|
|
5220
|
-
if (t.typ === 'AtBase') {
|
|
5221
|
-
while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
|
|
5222
|
-
continue;
|
|
5223
|
-
}
|
|
5224
|
-
|
|
5225
|
-
// Skip SPARQL/Turtle PREFIX pfx: <iri>
|
|
5226
|
-
if (
|
|
5227
|
-
t.typ === 'Ident' &&
|
|
5228
|
-
typeof t.value === 'string' &&
|
|
5229
|
-
t.value.toLowerCase() === 'prefix' &&
|
|
5230
|
-
toks2[i + 1] &&
|
|
5231
|
-
toks2[i + 1].typ === 'Ident' &&
|
|
5232
|
-
typeof toks2[i + 1].value === 'string' &&
|
|
5233
|
-
toks2[i + 1].value.endsWith(':') &&
|
|
5234
|
-
toks2[i + 2] &&
|
|
5235
|
-
(toks2[i + 2].typ === 'IriRef' || toks2[i + 2].typ === 'Ident')
|
|
5236
|
-
) {
|
|
5237
|
-
i += 2;
|
|
5238
|
-
continue;
|
|
5239
|
-
}
|
|
5240
|
-
|
|
5241
|
-
// Skip SPARQL BASE <iri>
|
|
5242
|
-
if (
|
|
5243
|
-
t.typ === 'Ident' &&
|
|
5244
|
-
typeof t.value === 'string' &&
|
|
5245
|
-
t.value.toLowerCase() === 'base' &&
|
|
5246
|
-
toks2[i + 1] &&
|
|
5247
|
-
toks2[i + 1].typ === 'IriRef'
|
|
5248
|
-
) {
|
|
5249
|
-
i += 1;
|
|
5250
|
-
continue;
|
|
5251
|
-
}
|
|
5252
|
-
|
|
5253
|
-
// Count QNames in identifiers (including datatypes like xsd:integer).
|
|
5254
|
-
if (t.typ === 'Ident') {
|
|
5255
|
-
maybeAddFromQName(t.value);
|
|
5256
|
-
}
|
|
5257
|
-
}
|
|
5258
|
-
|
|
5259
|
-
return used;
|
|
5260
|
-
}
|
|
5261
|
-
|
|
5262
4725
|
function restrictPrefixEnv(prefEnv, usedSet) {
|
|
5263
4726
|
const m = {};
|
|
5264
4727
|
for (const p of usedSet) {
|
|
@@ -5276,10 +4739,7 @@ function main() {
|
|
|
5276
4739
|
const mayAutoRenderOutputStrings = programMayProduceOutputStrings(triples, frules, qrules);
|
|
5277
4740
|
|
|
5278
4741
|
if (streamMode && !hasQueries && !mayAutoRenderOutputStrings) {
|
|
5279
|
-
const usedInInput = new Set();
|
|
5280
|
-
for (const source of tokenSets) {
|
|
5281
|
-
for (const pfx of prefixesUsedInInputTokens(source.tokens, source.prefixes)) usedInInput.add(pfx);
|
|
5282
|
-
}
|
|
4742
|
+
const usedInInput = mergedDocument.usedPrefixes instanceof Set ? new Set(mergedDocument.usedPrefixes) : new Set();
|
|
5283
4743
|
const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
|
|
5284
4744
|
|
|
5285
4745
|
// Ensure log:trace uses the same compact prefix set as the output.
|
|
@@ -6445,10 +5905,6 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
|
|
|
6445
5905
|
termsEqualNoIntDecimal,
|
|
6446
5906
|
});
|
|
6447
5907
|
|
|
6448
|
-
try {
|
|
6449
|
-
registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
|
|
6450
|
-
} catch (_) {}
|
|
6451
|
-
|
|
6452
5908
|
// Initialize proof/output helpers (implemented in lib/explain.js).
|
|
6453
5909
|
const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
|
|
6454
5910
|
applySubstTerm,
|
|
@@ -10581,13 +10037,101 @@ function emptyParsedDocument() {
|
|
|
10581
10037
|
};
|
|
10582
10038
|
}
|
|
10583
10039
|
|
|
10040
|
+
function prefixesUsedInTokens(tokens, prefEnv) {
|
|
10041
|
+
const used = new Set();
|
|
10042
|
+
const toks = Array.isArray(tokens) ? tokens : [];
|
|
10043
|
+
const prefixes = prefEnv && prefEnv.map ? prefEnv.map : {};
|
|
10044
|
+
|
|
10045
|
+
function maybeAddFromQName(name) {
|
|
10046
|
+
if (typeof name !== 'string') return;
|
|
10047
|
+
if (!name.includes(':')) return;
|
|
10048
|
+
if (name.startsWith('_:')) return; // blank node
|
|
10049
|
+
|
|
10050
|
+
// Split only on the first ':'; the empty prefix is valid for ":foo".
|
|
10051
|
+
const idx = name.indexOf(':');
|
|
10052
|
+
const p = name.slice(0, idx);
|
|
10053
|
+
|
|
10054
|
+
// Ignore strings like "http://..." unless that prefix is actually defined.
|
|
10055
|
+
if (!Object.prototype.hasOwnProperty.call(prefixes, p)) return;
|
|
10056
|
+
|
|
10057
|
+
used.add(p);
|
|
10058
|
+
}
|
|
10059
|
+
|
|
10060
|
+
for (let i = 0; i < toks.length; i++) {
|
|
10061
|
+
const t = toks[i];
|
|
10062
|
+
if (!t) continue;
|
|
10063
|
+
|
|
10064
|
+
// Skip @prefix ... .
|
|
10065
|
+
if (t.typ === 'AtPrefix') {
|
|
10066
|
+
while (i < toks.length && toks[i].typ !== 'Dot' && toks[i].typ !== 'EOF') i++;
|
|
10067
|
+
continue;
|
|
10068
|
+
}
|
|
10069
|
+
|
|
10070
|
+
// Skip @base ... .
|
|
10071
|
+
if (t.typ === 'AtBase') {
|
|
10072
|
+
while (i < toks.length && toks[i].typ !== 'Dot' && toks[i].typ !== 'EOF') i++;
|
|
10073
|
+
continue;
|
|
10074
|
+
}
|
|
10075
|
+
|
|
10076
|
+
// Skip SPARQL/Turtle PREFIX pfx: <iri>
|
|
10077
|
+
if (
|
|
10078
|
+
t.typ === 'Ident' &&
|
|
10079
|
+
typeof t.value === 'string' &&
|
|
10080
|
+
t.value.toLowerCase() === 'prefix' &&
|
|
10081
|
+
toks[i + 1] &&
|
|
10082
|
+
toks[i + 1].typ === 'Ident' &&
|
|
10083
|
+
typeof toks[i + 1].value === 'string' &&
|
|
10084
|
+
toks[i + 1].value.endsWith(':') &&
|
|
10085
|
+
toks[i + 2] &&
|
|
10086
|
+
(toks[i + 2].typ === 'IriRef' || toks[i + 2].typ === 'Ident')
|
|
10087
|
+
) {
|
|
10088
|
+
i += 2;
|
|
10089
|
+
continue;
|
|
10090
|
+
}
|
|
10091
|
+
|
|
10092
|
+
// Skip SPARQL BASE <iri>
|
|
10093
|
+
if (
|
|
10094
|
+
t.typ === 'Ident' &&
|
|
10095
|
+
typeof t.value === 'string' &&
|
|
10096
|
+
t.value.toLowerCase() === 'base' &&
|
|
10097
|
+
toks[i + 1] &&
|
|
10098
|
+
toks[i + 1].typ === 'IriRef'
|
|
10099
|
+
) {
|
|
10100
|
+
i += 1;
|
|
10101
|
+
continue;
|
|
10102
|
+
}
|
|
10103
|
+
|
|
10104
|
+
// Count QNames in identifiers, including datatypes like xsd:integer.
|
|
10105
|
+
if (t.typ === 'Ident') maybeAddFromQName(t.value);
|
|
10106
|
+
}
|
|
10107
|
+
|
|
10108
|
+
return used;
|
|
10109
|
+
}
|
|
10110
|
+
|
|
10584
10111
|
function parseN3Text(text, opts = {}) {
|
|
10585
|
-
const { baseIri = '', label = '<input>' } = opts || {};
|
|
10112
|
+
const { baseIri = '', label = '<input>', keepSourceArtifacts = true, collectUsedPrefixes = false } = opts || {};
|
|
10586
10113
|
const tokens = lex(text);
|
|
10587
10114
|
const parser = new Parser(tokens);
|
|
10588
10115
|
if (baseIri) parser.prefixes.setBase(baseIri);
|
|
10589
10116
|
const [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
|
|
10590
|
-
|
|
10117
|
+
|
|
10118
|
+
const doc = { prefixes, triples, frules, brules, logQueryRules, label };
|
|
10119
|
+
|
|
10120
|
+
if (collectUsedPrefixes) {
|
|
10121
|
+
Object.defineProperty(doc, 'usedPrefixes', {
|
|
10122
|
+
value: prefixesUsedInTokens(tokens, prefixes),
|
|
10123
|
+
enumerable: false,
|
|
10124
|
+
writable: false,
|
|
10125
|
+
configurable: true,
|
|
10126
|
+
});
|
|
10127
|
+
}
|
|
10128
|
+
|
|
10129
|
+
if (keepSourceArtifacts) {
|
|
10130
|
+
doc.tokens = tokens;
|
|
10131
|
+
doc.text = text;
|
|
10132
|
+
}
|
|
10133
|
+
|
|
10134
|
+
return doc;
|
|
10591
10135
|
}
|
|
10592
10136
|
|
|
10593
10137
|
function sourceBlankPrefix(sourceIndex) {
|
|
@@ -10646,16 +10190,27 @@ function scopeBlankNodesInDocument(doc, sourceIndex) {
|
|
|
10646
10190
|
return out;
|
|
10647
10191
|
}
|
|
10648
10192
|
|
|
10649
|
-
|
|
10193
|
+
const out = {
|
|
10650
10194
|
prefixes: doc.prefixes,
|
|
10651
10195
|
triples: (doc.triples || []).map(cloneTriple),
|
|
10652
10196
|
frules: (doc.frules || []).map(cloneRule),
|
|
10653
10197
|
brules: (doc.brules || []).map(cloneRule),
|
|
10654
10198
|
logQueryRules: (doc.logQueryRules || []).map(cloneRule),
|
|
10655
|
-
tokens: doc.tokens,
|
|
10656
|
-
text: doc.text,
|
|
10657
10199
|
label: doc.label,
|
|
10658
10200
|
};
|
|
10201
|
+
|
|
10202
|
+
if (doc.usedPrefixes instanceof Set) {
|
|
10203
|
+
Object.defineProperty(out, 'usedPrefixes', {
|
|
10204
|
+
value: new Set(doc.usedPrefixes),
|
|
10205
|
+
enumerable: false,
|
|
10206
|
+
writable: false,
|
|
10207
|
+
configurable: true,
|
|
10208
|
+
});
|
|
10209
|
+
}
|
|
10210
|
+
if (Object.prototype.hasOwnProperty.call(doc, 'tokens')) out.tokens = doc.tokens;
|
|
10211
|
+
if (Object.prototype.hasOwnProperty.call(doc, 'text')) out.text = doc.text;
|
|
10212
|
+
|
|
10213
|
+
return out;
|
|
10659
10214
|
}
|
|
10660
10215
|
|
|
10661
10216
|
function mergePrefixEnvs(target, source) {
|
|
@@ -10674,9 +10229,10 @@ function mergePrefixEnvs(target, source) {
|
|
|
10674
10229
|
function mergeParsedDocuments(docs, opts = {}) {
|
|
10675
10230
|
const documents = Array.isArray(docs) ? docs : [];
|
|
10676
10231
|
const scopeBlankNodes = typeof opts.scopeBlankNodes === 'boolean' ? opts.scopeBlankNodes : documents.length > 1;
|
|
10232
|
+
const keepSources = !!opts.keepSources || !!opts.keepSourceArtifacts;
|
|
10677
10233
|
|
|
10678
10234
|
const merged = emptyParsedDocument();
|
|
10679
|
-
const mergedSources = [];
|
|
10235
|
+
const mergedSources = keepSources ? [] : null;
|
|
10680
10236
|
|
|
10681
10237
|
for (let i = 0; i < documents.length; i++) {
|
|
10682
10238
|
const originalDoc = documents[i] || emptyParsedDocument();
|
|
@@ -10687,15 +10243,30 @@ function mergeParsedDocuments(docs, opts = {}) {
|
|
|
10687
10243
|
merged.frules.push(...(doc.frules || []));
|
|
10688
10244
|
merged.brules.push(...(doc.brules || []));
|
|
10689
10245
|
merged.logQueryRules.push(...(doc.logQueryRules || []));
|
|
10690
|
-
|
|
10246
|
+
|
|
10247
|
+
if (doc.usedPrefixes instanceof Set) {
|
|
10248
|
+
if (!(merged.usedPrefixes instanceof Set)) {
|
|
10249
|
+
Object.defineProperty(merged, 'usedPrefixes', {
|
|
10250
|
+
value: new Set(),
|
|
10251
|
+
enumerable: false,
|
|
10252
|
+
writable: false,
|
|
10253
|
+
configurable: true,
|
|
10254
|
+
});
|
|
10255
|
+
}
|
|
10256
|
+
for (const pfx of doc.usedPrefixes) merged.usedPrefixes.add(pfx);
|
|
10257
|
+
}
|
|
10258
|
+
|
|
10259
|
+
if (keepSources) mergedSources.push(doc);
|
|
10691
10260
|
}
|
|
10692
10261
|
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10262
|
+
if (keepSources) {
|
|
10263
|
+
Object.defineProperty(merged, 'sources', {
|
|
10264
|
+
value: mergedSources,
|
|
10265
|
+
enumerable: false,
|
|
10266
|
+
writable: false,
|
|
10267
|
+
configurable: true,
|
|
10268
|
+
});
|
|
10269
|
+
}
|
|
10699
10270
|
|
|
10700
10271
|
return merged;
|
|
10701
10272
|
}
|
|
@@ -10727,14 +10298,17 @@ function parseN3SourceList(input, opts = {}) {
|
|
|
10727
10298
|
if (!isN3SourceListInput(input)) return null;
|
|
10728
10299
|
const sources = input.sources.map(normalizeN3SourceItem);
|
|
10729
10300
|
const defaultBaseIri = typeof opts.baseIri === 'string' ? opts.baseIri : '';
|
|
10730
|
-
const parsed = sources.map((source
|
|
10301
|
+
const parsed = sources.map((source) =>
|
|
10731
10302
|
parseN3Text(source.text, {
|
|
10732
10303
|
label: source.label,
|
|
10733
10304
|
baseIri: source.baseIri || (sources.length === 1 ? defaultBaseIri : ''),
|
|
10305
|
+
collectUsedPrefixes: true,
|
|
10306
|
+
keepSourceArtifacts: !!opts.keepSourceArtifacts,
|
|
10734
10307
|
}),
|
|
10735
10308
|
);
|
|
10736
10309
|
return mergeParsedDocuments(parsed, {
|
|
10737
10310
|
scopeBlankNodes: typeof input.scopeBlankNodes === 'boolean' ? input.scopeBlankNodes : parsed.length > 1,
|
|
10311
|
+
keepSources: !!opts.keepSourceArtifacts,
|
|
10738
10312
|
});
|
|
10739
10313
|
}
|
|
10740
10314
|
|
|
@@ -10743,6 +10317,7 @@ module.exports = {
|
|
|
10743
10317
|
parseN3Text,
|
|
10744
10318
|
mergeParsedDocuments,
|
|
10745
10319
|
scopeBlankNodesInDocument,
|
|
10320
|
+
prefixesUsedInTokens,
|
|
10746
10321
|
isN3SourceListInput,
|
|
10747
10322
|
parseN3SourceList,
|
|
10748
10323
|
};
|
|
@@ -13231,7 +12806,17 @@ module.exports = {
|
|
|
13231
12806
|
|
|
13232
12807
|
'use strict';
|
|
13233
12808
|
|
|
13234
|
-
const {
|
|
12809
|
+
const {
|
|
12810
|
+
LOG_NS,
|
|
12811
|
+
Iri,
|
|
12812
|
+
Var,
|
|
12813
|
+
Blank,
|
|
12814
|
+
ListTerm,
|
|
12815
|
+
OpenListTerm,
|
|
12816
|
+
GraphTerm,
|
|
12817
|
+
Triple,
|
|
12818
|
+
copyQuotedGraphMetadata,
|
|
12819
|
+
} = require('./prelude');
|
|
13235
12820
|
|
|
13236
12821
|
function liftBlankRuleVars(premise, conclusion) {
|
|
13237
12822
|
function isLogIncludesLikePredicate(p) {
|