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
|
@@ -8,478 +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
|
|
385
|
-
? null
|
|
386
|
-
: makeStringLiteral(report.givensPreserved ? 'OK' : 'failed');
|
|
387
|
-
if (field === 'noBlanksText')
|
|
388
|
-
return report.noBlanks === undefined ? null : makeStringLiteral(report.noBlanks ? 'OK' : 'failed');
|
|
389
|
-
if (field === 'rowsCompleteText')
|
|
390
|
-
return report.rowsComplete === undefined ? null : makeStringLiteral(report.rowsComplete ? 'OK' : 'failed');
|
|
391
|
-
if (field === 'colsCompleteText')
|
|
392
|
-
return report.colsComplete === undefined ? null : makeStringLiteral(report.colsComplete ? 'OK' : 'failed');
|
|
393
|
-
if (field === 'boxesCompleteText')
|
|
394
|
-
return report.boxesComplete === undefined ? null : makeStringLiteral(report.boxesComplete ? 'OK' : 'failed');
|
|
395
|
-
if (field === 'replayLegalText')
|
|
396
|
-
return report.replayLegal === undefined ? null : makeStringLiteral(report.replayLegal ? 'OK' : 'failed');
|
|
397
|
-
if (field === 'storyConsistentText')
|
|
398
|
-
return report.storyConsistent === undefined
|
|
399
|
-
? null
|
|
400
|
-
: makeStringLiteral(report.storyConsistent ? 'OK' : 'failed');
|
|
401
|
-
|
|
402
|
-
const boolFields = [
|
|
403
|
-
'unique',
|
|
404
|
-
'givensPreserved',
|
|
405
|
-
'noBlanks',
|
|
406
|
-
'rowsComplete',
|
|
407
|
-
'colsComplete',
|
|
408
|
-
'boxesComplete',
|
|
409
|
-
'replayLegal',
|
|
410
|
-
'storyConsistent',
|
|
411
|
-
];
|
|
412
|
-
if (boolFields.includes(field))
|
|
413
|
-
return report[field] === undefined ? null : internLiteral(report[field] ? 'true' : 'false');
|
|
414
|
-
|
|
415
|
-
const numberFields = [
|
|
416
|
-
'givens',
|
|
417
|
-
'blanks',
|
|
418
|
-
'forcedMoves',
|
|
419
|
-
'guessedMoves',
|
|
420
|
-
'recursiveNodes',
|
|
421
|
-
'backtracks',
|
|
422
|
-
'maxDepth',
|
|
423
|
-
'moveCount',
|
|
424
|
-
];
|
|
425
|
-
if (numberFields.includes(field))
|
|
426
|
-
return report[field] === undefined ? null : internLiteral(String(report[field]));
|
|
427
|
-
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function evalSudokuField(goal, subst, field) {
|
|
432
|
-
const report = computeReport(goal.s);
|
|
433
|
-
if (!report) return [];
|
|
434
|
-
const term = reportFieldAsTerm(report, field);
|
|
435
|
-
if (!term) return [];
|
|
436
|
-
if (goal.o instanceof Var) {
|
|
437
|
-
const s2 = { ...subst };
|
|
438
|
-
s2[goal.o.name] = term;
|
|
439
|
-
return [s2];
|
|
440
|
-
}
|
|
441
|
-
const s2 = unifyTerm(goal.o, term, subst);
|
|
442
|
-
return s2 !== null ? [s2] : [];
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const fields = [
|
|
446
|
-
'status',
|
|
447
|
-
'error',
|
|
448
|
-
'normalizedPuzzle',
|
|
449
|
-
'solution',
|
|
450
|
-
'givens',
|
|
451
|
-
'blanks',
|
|
452
|
-
'forcedMoves',
|
|
453
|
-
'guessedMoves',
|
|
454
|
-
'recursiveNodes',
|
|
455
|
-
'backtracks',
|
|
456
|
-
'maxDepth',
|
|
457
|
-
'unique',
|
|
458
|
-
'givensPreserved',
|
|
459
|
-
'noBlanks',
|
|
460
|
-
'rowsComplete',
|
|
461
|
-
'colsComplete',
|
|
462
|
-
'boxesComplete',
|
|
463
|
-
'replayLegal',
|
|
464
|
-
'storyConsistent',
|
|
465
|
-
'givensPreservedText',
|
|
466
|
-
'noBlanksText',
|
|
467
|
-
'rowsCompleteText',
|
|
468
|
-
'colsCompleteText',
|
|
469
|
-
'boxesCompleteText',
|
|
470
|
-
'replayLegalText',
|
|
471
|
-
'storyConsistentText',
|
|
472
|
-
'moveSummary',
|
|
473
|
-
'puzzleText',
|
|
474
|
-
'solutionText',
|
|
475
|
-
'moveCount',
|
|
476
|
-
];
|
|
477
|
-
|
|
478
|
-
for (const field of fields) {
|
|
479
|
-
registerBuiltin(SUDOKU_NS + field, ({ goal, subst }) => evalSudokuField(goal, subst, field));
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
};
|
|
483
11
|
__modules['lib/builtins.js'] = function (require, module, exports) {
|
|
484
12
|
/**
|
|
485
13
|
* Eyeling Reasoner — builtins
|
|
@@ -5132,6 +4660,8 @@
|
|
|
5132
4660
|
parseN3Text(text, {
|
|
5133
4661
|
baseIri: __sourceLabelToBaseIri(sourceLabel),
|
|
5134
4662
|
label: sourceLabel,
|
|
4663
|
+
collectUsedPrefixes: true,
|
|
4664
|
+
keepSourceArtifacts: false,
|
|
5135
4665
|
}),
|
|
5136
4666
|
);
|
|
5137
4667
|
} catch (e) {
|
|
@@ -5149,8 +4679,6 @@
|
|
|
5149
4679
|
const frules = mergedDocument.frules;
|
|
5150
4680
|
const brules = mergedDocument.brules;
|
|
5151
4681
|
const qrules = mergedDocument.logQueryRules;
|
|
5152
|
-
const tokenSets = parsedSources.map((source) => ({ tokens: source.tokens, prefixes: source.prefixes }));
|
|
5153
|
-
|
|
5154
4682
|
if (showAst) {
|
|
5155
4683
|
function astReplacer(unusedJsonKey, value) {
|
|
5156
4684
|
if (value instanceof Set) return Array.from(value);
|
|
@@ -5208,75 +4736,6 @@
|
|
|
5208
4736
|
// In --stream mode we print prefixes *before* any derivations happen.
|
|
5209
4737
|
// To keep the header small and stable, emit only prefixes that are actually
|
|
5210
4738
|
// used (as QNames) in the *input* N3 program.
|
|
5211
|
-
function prefixesUsedInInputTokens(toks2, prefEnv) {
|
|
5212
|
-
const used = new Set();
|
|
5213
|
-
|
|
5214
|
-
function maybeAddFromQName(name) {
|
|
5215
|
-
if (typeof name !== 'string') return;
|
|
5216
|
-
if (!name.includes(':')) return;
|
|
5217
|
-
if (name.startsWith('_:')) return; // blank node
|
|
5218
|
-
|
|
5219
|
-
// Split only on the first ':'
|
|
5220
|
-
const idx = name.indexOf(':');
|
|
5221
|
-
const p = name.slice(0, idx); // may be '' for ":foo"
|
|
5222
|
-
|
|
5223
|
-
// Ignore things like "http://..." unless that prefix is actually defined.
|
|
5224
|
-
if (!Object.prototype.hasOwnProperty.call(prefEnv.map, p)) return;
|
|
5225
|
-
|
|
5226
|
-
used.add(p);
|
|
5227
|
-
}
|
|
5228
|
-
|
|
5229
|
-
for (let i = 0; i < toks2.length; i++) {
|
|
5230
|
-
const t = toks2[i];
|
|
5231
|
-
|
|
5232
|
-
// Skip @prefix ... .
|
|
5233
|
-
if (t.typ === 'AtPrefix') {
|
|
5234
|
-
while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
|
|
5235
|
-
continue;
|
|
5236
|
-
}
|
|
5237
|
-
// Skip @base ... .
|
|
5238
|
-
if (t.typ === 'AtBase') {
|
|
5239
|
-
while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
|
|
5240
|
-
continue;
|
|
5241
|
-
}
|
|
5242
|
-
|
|
5243
|
-
// Skip SPARQL/Turtle PREFIX pfx: <iri>
|
|
5244
|
-
if (
|
|
5245
|
-
t.typ === 'Ident' &&
|
|
5246
|
-
typeof t.value === 'string' &&
|
|
5247
|
-
t.value.toLowerCase() === 'prefix' &&
|
|
5248
|
-
toks2[i + 1] &&
|
|
5249
|
-
toks2[i + 1].typ === 'Ident' &&
|
|
5250
|
-
typeof toks2[i + 1].value === 'string' &&
|
|
5251
|
-
toks2[i + 1].value.endsWith(':') &&
|
|
5252
|
-
toks2[i + 2] &&
|
|
5253
|
-
(toks2[i + 2].typ === 'IriRef' || toks2[i + 2].typ === 'Ident')
|
|
5254
|
-
) {
|
|
5255
|
-
i += 2;
|
|
5256
|
-
continue;
|
|
5257
|
-
}
|
|
5258
|
-
|
|
5259
|
-
// Skip SPARQL BASE <iri>
|
|
5260
|
-
if (
|
|
5261
|
-
t.typ === 'Ident' &&
|
|
5262
|
-
typeof t.value === 'string' &&
|
|
5263
|
-
t.value.toLowerCase() === 'base' &&
|
|
5264
|
-
toks2[i + 1] &&
|
|
5265
|
-
toks2[i + 1].typ === 'IriRef'
|
|
5266
|
-
) {
|
|
5267
|
-
i += 1;
|
|
5268
|
-
continue;
|
|
5269
|
-
}
|
|
5270
|
-
|
|
5271
|
-
// Count QNames in identifiers (including datatypes like xsd:integer).
|
|
5272
|
-
if (t.typ === 'Ident') {
|
|
5273
|
-
maybeAddFromQName(t.value);
|
|
5274
|
-
}
|
|
5275
|
-
}
|
|
5276
|
-
|
|
5277
|
-
return used;
|
|
5278
|
-
}
|
|
5279
|
-
|
|
5280
4739
|
function restrictPrefixEnv(prefEnv, usedSet) {
|
|
5281
4740
|
const m = {};
|
|
5282
4741
|
for (const p of usedSet) {
|
|
@@ -5294,10 +4753,8 @@
|
|
|
5294
4753
|
const mayAutoRenderOutputStrings = programMayProduceOutputStrings(triples, frules, qrules);
|
|
5295
4754
|
|
|
5296
4755
|
if (streamMode && !hasQueries && !mayAutoRenderOutputStrings) {
|
|
5297
|
-
const usedInInput =
|
|
5298
|
-
|
|
5299
|
-
for (const pfx of prefixesUsedInInputTokens(source.tokens, source.prefixes)) usedInInput.add(pfx);
|
|
5300
|
-
}
|
|
4756
|
+
const usedInInput =
|
|
4757
|
+
mergedDocument.usedPrefixes instanceof Set ? new Set(mergedDocument.usedPrefixes) : new Set();
|
|
5301
4758
|
const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
|
|
5302
4759
|
|
|
5303
4760
|
// Ensure log:trace uses the same compact prefix set as the output.
|
|
@@ -6465,10 +5922,6 @@
|
|
|
6465
5922
|
termsEqualNoIntDecimal,
|
|
6466
5923
|
});
|
|
6467
5924
|
|
|
6468
|
-
try {
|
|
6469
|
-
registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
|
|
6470
|
-
} catch (_) {}
|
|
6471
|
-
|
|
6472
5925
|
// Initialize proof/output helpers (implemented in lib/explain.js).
|
|
6473
5926
|
const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
|
|
6474
5927
|
applySubstTerm,
|
|
@@ -10614,13 +10067,101 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
|
|
|
10614
10067
|
};
|
|
10615
10068
|
}
|
|
10616
10069
|
|
|
10070
|
+
function prefixesUsedInTokens(tokens, prefEnv) {
|
|
10071
|
+
const used = new Set();
|
|
10072
|
+
const toks = Array.isArray(tokens) ? tokens : [];
|
|
10073
|
+
const prefixes = prefEnv && prefEnv.map ? prefEnv.map : {};
|
|
10074
|
+
|
|
10075
|
+
function maybeAddFromQName(name) {
|
|
10076
|
+
if (typeof name !== 'string') return;
|
|
10077
|
+
if (!name.includes(':')) return;
|
|
10078
|
+
if (name.startsWith('_:')) return; // blank node
|
|
10079
|
+
|
|
10080
|
+
// Split only on the first ':'; the empty prefix is valid for ":foo".
|
|
10081
|
+
const idx = name.indexOf(':');
|
|
10082
|
+
const p = name.slice(0, idx);
|
|
10083
|
+
|
|
10084
|
+
// Ignore strings like "http://..." unless that prefix is actually defined.
|
|
10085
|
+
if (!Object.prototype.hasOwnProperty.call(prefixes, p)) return;
|
|
10086
|
+
|
|
10087
|
+
used.add(p);
|
|
10088
|
+
}
|
|
10089
|
+
|
|
10090
|
+
for (let i = 0; i < toks.length; i++) {
|
|
10091
|
+
const t = toks[i];
|
|
10092
|
+
if (!t) continue;
|
|
10093
|
+
|
|
10094
|
+
// Skip @prefix ... .
|
|
10095
|
+
if (t.typ === 'AtPrefix') {
|
|
10096
|
+
while (i < toks.length && toks[i].typ !== 'Dot' && toks[i].typ !== 'EOF') i++;
|
|
10097
|
+
continue;
|
|
10098
|
+
}
|
|
10099
|
+
|
|
10100
|
+
// Skip @base ... .
|
|
10101
|
+
if (t.typ === 'AtBase') {
|
|
10102
|
+
while (i < toks.length && toks[i].typ !== 'Dot' && toks[i].typ !== 'EOF') i++;
|
|
10103
|
+
continue;
|
|
10104
|
+
}
|
|
10105
|
+
|
|
10106
|
+
// Skip SPARQL/Turtle PREFIX pfx: <iri>
|
|
10107
|
+
if (
|
|
10108
|
+
t.typ === 'Ident' &&
|
|
10109
|
+
typeof t.value === 'string' &&
|
|
10110
|
+
t.value.toLowerCase() === 'prefix' &&
|
|
10111
|
+
toks[i + 1] &&
|
|
10112
|
+
toks[i + 1].typ === 'Ident' &&
|
|
10113
|
+
typeof toks[i + 1].value === 'string' &&
|
|
10114
|
+
toks[i + 1].value.endsWith(':') &&
|
|
10115
|
+
toks[i + 2] &&
|
|
10116
|
+
(toks[i + 2].typ === 'IriRef' || toks[i + 2].typ === 'Ident')
|
|
10117
|
+
) {
|
|
10118
|
+
i += 2;
|
|
10119
|
+
continue;
|
|
10120
|
+
}
|
|
10121
|
+
|
|
10122
|
+
// Skip SPARQL BASE <iri>
|
|
10123
|
+
if (
|
|
10124
|
+
t.typ === 'Ident' &&
|
|
10125
|
+
typeof t.value === 'string' &&
|
|
10126
|
+
t.value.toLowerCase() === 'base' &&
|
|
10127
|
+
toks[i + 1] &&
|
|
10128
|
+
toks[i + 1].typ === 'IriRef'
|
|
10129
|
+
) {
|
|
10130
|
+
i += 1;
|
|
10131
|
+
continue;
|
|
10132
|
+
}
|
|
10133
|
+
|
|
10134
|
+
// Count QNames in identifiers, including datatypes like xsd:integer.
|
|
10135
|
+
if (t.typ === 'Ident') maybeAddFromQName(t.value);
|
|
10136
|
+
}
|
|
10137
|
+
|
|
10138
|
+
return used;
|
|
10139
|
+
}
|
|
10140
|
+
|
|
10617
10141
|
function parseN3Text(text, opts = {}) {
|
|
10618
|
-
const { baseIri = '', label = '<input>' } = opts || {};
|
|
10142
|
+
const { baseIri = '', label = '<input>', keepSourceArtifacts = true, collectUsedPrefixes = false } = opts || {};
|
|
10619
10143
|
const tokens = lex(text);
|
|
10620
10144
|
const parser = new Parser(tokens);
|
|
10621
10145
|
if (baseIri) parser.prefixes.setBase(baseIri);
|
|
10622
10146
|
const [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
|
|
10623
|
-
|
|
10147
|
+
|
|
10148
|
+
const doc = { prefixes, triples, frules, brules, logQueryRules, label };
|
|
10149
|
+
|
|
10150
|
+
if (collectUsedPrefixes) {
|
|
10151
|
+
Object.defineProperty(doc, 'usedPrefixes', {
|
|
10152
|
+
value: prefixesUsedInTokens(tokens, prefixes),
|
|
10153
|
+
enumerable: false,
|
|
10154
|
+
writable: false,
|
|
10155
|
+
configurable: true,
|
|
10156
|
+
});
|
|
10157
|
+
}
|
|
10158
|
+
|
|
10159
|
+
if (keepSourceArtifacts) {
|
|
10160
|
+
doc.tokens = tokens;
|
|
10161
|
+
doc.text = text;
|
|
10162
|
+
}
|
|
10163
|
+
|
|
10164
|
+
return doc;
|
|
10624
10165
|
}
|
|
10625
10166
|
|
|
10626
10167
|
function sourceBlankPrefix(sourceIndex) {
|
|
@@ -10679,16 +10220,27 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
|
|
|
10679
10220
|
return out;
|
|
10680
10221
|
}
|
|
10681
10222
|
|
|
10682
|
-
|
|
10223
|
+
const out = {
|
|
10683
10224
|
prefixes: doc.prefixes,
|
|
10684
10225
|
triples: (doc.triples || []).map(cloneTriple),
|
|
10685
10226
|
frules: (doc.frules || []).map(cloneRule),
|
|
10686
10227
|
brules: (doc.brules || []).map(cloneRule),
|
|
10687
10228
|
logQueryRules: (doc.logQueryRules || []).map(cloneRule),
|
|
10688
|
-
tokens: doc.tokens,
|
|
10689
|
-
text: doc.text,
|
|
10690
10229
|
label: doc.label,
|
|
10691
10230
|
};
|
|
10231
|
+
|
|
10232
|
+
if (doc.usedPrefixes instanceof Set) {
|
|
10233
|
+
Object.defineProperty(out, 'usedPrefixes', {
|
|
10234
|
+
value: new Set(doc.usedPrefixes),
|
|
10235
|
+
enumerable: false,
|
|
10236
|
+
writable: false,
|
|
10237
|
+
configurable: true,
|
|
10238
|
+
});
|
|
10239
|
+
}
|
|
10240
|
+
if (Object.prototype.hasOwnProperty.call(doc, 'tokens')) out.tokens = doc.tokens;
|
|
10241
|
+
if (Object.prototype.hasOwnProperty.call(doc, 'text')) out.text = doc.text;
|
|
10242
|
+
|
|
10243
|
+
return out;
|
|
10692
10244
|
}
|
|
10693
10245
|
|
|
10694
10246
|
function mergePrefixEnvs(target, source) {
|
|
@@ -10707,9 +10259,10 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
|
|
|
10707
10259
|
function mergeParsedDocuments(docs, opts = {}) {
|
|
10708
10260
|
const documents = Array.isArray(docs) ? docs : [];
|
|
10709
10261
|
const scopeBlankNodes = typeof opts.scopeBlankNodes === 'boolean' ? opts.scopeBlankNodes : documents.length > 1;
|
|
10262
|
+
const keepSources = !!opts.keepSources || !!opts.keepSourceArtifacts;
|
|
10710
10263
|
|
|
10711
10264
|
const merged = emptyParsedDocument();
|
|
10712
|
-
const mergedSources = [];
|
|
10265
|
+
const mergedSources = keepSources ? [] : null;
|
|
10713
10266
|
|
|
10714
10267
|
for (let i = 0; i < documents.length; i++) {
|
|
10715
10268
|
const originalDoc = documents[i] || emptyParsedDocument();
|
|
@@ -10720,15 +10273,30 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
|
|
|
10720
10273
|
merged.frules.push(...(doc.frules || []));
|
|
10721
10274
|
merged.brules.push(...(doc.brules || []));
|
|
10722
10275
|
merged.logQueryRules.push(...(doc.logQueryRules || []));
|
|
10723
|
-
|
|
10276
|
+
|
|
10277
|
+
if (doc.usedPrefixes instanceof Set) {
|
|
10278
|
+
if (!(merged.usedPrefixes instanceof Set)) {
|
|
10279
|
+
Object.defineProperty(merged, 'usedPrefixes', {
|
|
10280
|
+
value: new Set(),
|
|
10281
|
+
enumerable: false,
|
|
10282
|
+
writable: false,
|
|
10283
|
+
configurable: true,
|
|
10284
|
+
});
|
|
10285
|
+
}
|
|
10286
|
+
for (const pfx of doc.usedPrefixes) merged.usedPrefixes.add(pfx);
|
|
10287
|
+
}
|
|
10288
|
+
|
|
10289
|
+
if (keepSources) mergedSources.push(doc);
|
|
10724
10290
|
}
|
|
10725
10291
|
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10292
|
+
if (keepSources) {
|
|
10293
|
+
Object.defineProperty(merged, 'sources', {
|
|
10294
|
+
value: mergedSources,
|
|
10295
|
+
enumerable: false,
|
|
10296
|
+
writable: false,
|
|
10297
|
+
configurable: true,
|
|
10298
|
+
});
|
|
10299
|
+
}
|
|
10732
10300
|
|
|
10733
10301
|
return merged;
|
|
10734
10302
|
}
|
|
@@ -10760,14 +10328,17 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
|
|
|
10760
10328
|
if (!isN3SourceListInput(input)) return null;
|
|
10761
10329
|
const sources = input.sources.map(normalizeN3SourceItem);
|
|
10762
10330
|
const defaultBaseIri = typeof opts.baseIri === 'string' ? opts.baseIri : '';
|
|
10763
|
-
const parsed = sources.map((source
|
|
10331
|
+
const parsed = sources.map((source) =>
|
|
10764
10332
|
parseN3Text(source.text, {
|
|
10765
10333
|
label: source.label,
|
|
10766
10334
|
baseIri: source.baseIri || (sources.length === 1 ? defaultBaseIri : ''),
|
|
10335
|
+
collectUsedPrefixes: true,
|
|
10336
|
+
keepSourceArtifacts: !!opts.keepSourceArtifacts,
|
|
10767
10337
|
}),
|
|
10768
10338
|
);
|
|
10769
10339
|
return mergeParsedDocuments(parsed, {
|
|
10770
10340
|
scopeBlankNodes: typeof input.scopeBlankNodes === 'boolean' ? input.scopeBlankNodes : parsed.length > 1,
|
|
10341
|
+
keepSources: !!opts.keepSourceArtifacts,
|
|
10771
10342
|
});
|
|
10772
10343
|
}
|
|
10773
10344
|
|
|
@@ -10776,6 +10347,7 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
|
|
|
10776
10347
|
parseN3Text,
|
|
10777
10348
|
mergeParsedDocuments,
|
|
10778
10349
|
scopeBlankNodesInDocument,
|
|
10350
|
+
prefixesUsedInTokens,
|
|
10779
10351
|
isN3SourceListInput,
|
|
10780
10352
|
parseN3SourceList,
|
|
10781
10353
|
};
|