eyeling 1.23.2 → 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 +16 -6
- package/dist/browser/eyeling.browser.js +0 -476
- 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 +1 -473
- package/lib/engine.js +0 -4
- package/package.json +1 -1
- package/test/examples.test.js +33 -10
package/HANDBOOK.md
CHANGED
|
@@ -1891,9 +1891,17 @@ A few practical defaults are worth remembering:
|
|
|
1891
1891
|
Custom builtins can be loaded explicitly from the CLI:
|
|
1892
1892
|
|
|
1893
1893
|
```bash
|
|
1894
|
-
npx eyeling --builtin
|
|
1894
|
+
npx eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
|
|
1895
1895
|
```
|
|
1896
1896
|
|
|
1897
|
+
Example-specific builtins live under `examples/builtin/`. When the examples test runner sees `examples/builtin/<stem>.js` next to `examples/<stem>.n3`, it auto-loads that builtin for the matching example by running the same command shape a user would run manually:
|
|
1898
|
+
|
|
1899
|
+
```bash
|
|
1900
|
+
node eyeling.js --builtin examples/builtin/queens.js examples/queens.n3
|
|
1901
|
+
```
|
|
1902
|
+
|
|
1903
|
+
Examples that do not need a custom builtin should not add a matching file under `examples/builtin/`. Examples that do need one should ship it there and let the examples test runner load it uniformly. For example, `examples/sudoku.n3` is paired with `examples/builtin/sudoku.js`, and `examples/queens.n3` is paired with `examples/builtin/queens.js`.
|
|
1904
|
+
|
|
1897
1905
|
### 14.2 The bundled Node CLI/runtime (`eyeling.js`)
|
|
1898
1906
|
|
|
1899
1907
|
The bundle contains the whole engine. The CLI path is the “canonical behavior”:
|
|
@@ -2376,14 +2384,16 @@ That API keeps the extension boundary explicit: custom builtins get the operatio
|
|
|
2376
2384
|
|
|
2377
2385
|
### 16.6 A shipped example: the Sudoku builtin
|
|
2378
2386
|
|
|
2379
|
-
The repository
|
|
2387
|
+
The repository ships a Sudoku example program (`examples/sudoku.n3`) together with its example-specific builtin module (`examples/builtin/sudoku.js`).
|
|
2380
2388
|
|
|
2381
|
-
|
|
2389
|
+
Run it explicitly like this:
|
|
2382
2390
|
|
|
2383
2391
|
```bash
|
|
2384
|
-
eyeling sudoku.n3
|
|
2392
|
+
eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
|
|
2385
2393
|
```
|
|
2386
2394
|
|
|
2395
|
+
`npm run test:examples` uses the same convention automatically: when it sees `examples/builtin/sudoku.js` next to `examples/sudoku.n3`, it loads that module for the Sudoku example.
|
|
2396
|
+
|
|
2387
2397
|
That example is useful for two reasons:
|
|
2388
2398
|
|
|
2389
2399
|
- it shows a realistic domain-specific builtin implemented outside the core builtin switchboard
|
|
@@ -2567,10 +2577,10 @@ It also supports **custom builtin modules**.
|
|
|
2567
2577
|
- From JavaScript: `reason({ builtinModules: ['./my-builtins.js'] }, input)`
|
|
2568
2578
|
- Programmatically in-process: `registerBuiltin(...)`, `registerBuiltinModule(...)`, `loadBuiltinModule(...)`
|
|
2569
2579
|
|
|
2570
|
-
A concrete shipped example is the Sudoku builtin
|
|
2580
|
+
A concrete shipped example is the Sudoku builtin paired with `examples/sudoku.n3`:
|
|
2571
2581
|
|
|
2572
2582
|
```bash
|
|
2573
|
-
eyeling sudoku.n3
|
|
2583
|
+
eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
|
|
2574
2584
|
```
|
|
2575
2585
|
|
|
2576
2586
|
References:
|
|
@@ -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
|
|
@@ -6394,10 +5922,6 @@
|
|
|
6394
5922
|
termsEqualNoIntDecimal,
|
|
6395
5923
|
});
|
|
6396
5924
|
|
|
6397
|
-
try {
|
|
6398
|
-
registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
|
|
6399
|
-
} catch (_) {}
|
|
6400
|
-
|
|
6401
5925
|
// Initialize proof/output helpers (implemented in lib/explain.js).
|
|
6402
5926
|
const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
|
|
6403
5927
|
applySubstTerm,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Example-specific builtin module for examples/queens.n3.
|
|
4
|
+
//
|
|
5
|
+
// The N3 file keeps the example interface declarative:
|
|
6
|
+
// (16 0) queens:render ?Text
|
|
7
|
+
// (16 0) queens:count ?Count
|
|
8
|
+
//
|
|
9
|
+
// This JavaScript module supplies the intentionally specialized hot loop. The
|
|
10
|
+
// solver is the same 32-bit bit-mask kernel used in the standalone queens.js
|
|
11
|
+
// benchmark: columns and diagonals are represented as integer masks, so a whole
|
|
12
|
+
// row's legal moves are computed with a few bitwise operations.
|
|
13
|
+
//
|
|
14
|
+
// Why use a builtin instead of pure N3 rules?
|
|
15
|
+
// * A pure N3 generator for 16-Queens would create an enormous search tree.
|
|
16
|
+
// * The builtin keeps the example useful for performance demonstrations.
|
|
17
|
+
// * It also shows the intended pattern for expensive domain-specific kernels:
|
|
18
|
+
// put the tight computation in a custom builtin, then let N3 describe how
|
|
19
|
+
// the result is connected to the rest of the knowledge graph.
|
|
20
|
+
//
|
|
21
|
+
module.exports = ({ registerBuiltin, internLiteral, unifyTerm, applySubstTerm, parseNumericLiteralInfo, terms }) => {
|
|
22
|
+
const { ListTerm } = terms;
|
|
23
|
+
const NS = 'http://example.org/queens#';
|
|
24
|
+
|
|
25
|
+
// Cache by "N/MAX_PRINT" because the same N3 run may ask for both the count
|
|
26
|
+
// and the rendered report. Without this, queens:count and queens:render would
|
|
27
|
+
// solve the same board twice.
|
|
28
|
+
const resultCache = new Map();
|
|
29
|
+
|
|
30
|
+
function integerValue(term) {
|
|
31
|
+
const info = parseNumericLiteralInfo(term);
|
|
32
|
+
if (!info) return null;
|
|
33
|
+
if (info.kind === 'bigint') {
|
|
34
|
+
const n = Number(info.value);
|
|
35
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
36
|
+
}
|
|
37
|
+
if (info.kind === 'number' && Number.isInteger(info.value)) return info.value;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function solveNQueens(n, maxPrint) {
|
|
42
|
+
if (!Number.isInteger(n) || n <= 0 || n > 31) {
|
|
43
|
+
throw new RangeError('queens:count expects 1 <= N <= 31');
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isInteger(maxPrint) || maxPrint < 0) {
|
|
46
|
+
throw new RangeError('queens:count expects MAX_PRINT >= 0');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// JavaScript bitwise operators work on signed 32-bit integers. N <= 31 is
|
|
50
|
+
// therefore the safe range for this compact benchmark implementation.
|
|
51
|
+
const allColumns = Math.pow(2, n) - 1;
|
|
52
|
+
const board = new Array(n).fill(-1);
|
|
53
|
+
const printed = [];
|
|
54
|
+
let count = 0;
|
|
55
|
+
|
|
56
|
+
function boardText() {
|
|
57
|
+
const lines = [];
|
|
58
|
+
for (let row = 0; row < n; row++) {
|
|
59
|
+
const cells = [];
|
|
60
|
+
for (let col = 0; col < n; col++) cells.push(col === board[row] ? 'Q' : '.');
|
|
61
|
+
lines.push(cells.join(' '));
|
|
62
|
+
}
|
|
63
|
+
lines.push(`As column positions by row: [${board.map((col) => col + 1).join(', ')}]`);
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function search(row, columns, diagLeft, diagRight) {
|
|
68
|
+
if (row === n) {
|
|
69
|
+
count++;
|
|
70
|
+
if (count <= maxPrint) printed.push(`Solution ${count}:\n${boardText()}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// All legal columns for this row in one expression.
|
|
75
|
+
let available = allColumns & ~(columns | diagLeft | diagRight);
|
|
76
|
+
while (available !== 0) {
|
|
77
|
+
// Pick and clear the lowest set bit.
|
|
78
|
+
const position = available & -available;
|
|
79
|
+
available ^= position;
|
|
80
|
+
|
|
81
|
+
board[row] = Math.clz32(position) ^ 31;
|
|
82
|
+
search(row + 1, columns | position, (diagLeft | position) << 1, (diagRight | position) >> 1);
|
|
83
|
+
board[row] = -1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
search(0, 0, 0, 0);
|
|
88
|
+
return { count, printed };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
registerBuiltin(NS + 'count', ({ goal, subst }) => {
|
|
92
|
+
const subject = applySubstTerm(goal.s, subst);
|
|
93
|
+
if (!(subject instanceof ListTerm) || subject.elems.length !== 2) return [];
|
|
94
|
+
|
|
95
|
+
const n = integerValue(applySubstTerm(subject.elems[0], subst));
|
|
96
|
+
const maxPrint = integerValue(applySubstTerm(subject.elems[1], subst));
|
|
97
|
+
if (n == null || maxPrint == null) return [];
|
|
98
|
+
|
|
99
|
+
const key = `${n}/${maxPrint}`;
|
|
100
|
+
let result = resultCache.get(key);
|
|
101
|
+
if (!result) {
|
|
102
|
+
result = solveNQueens(n, maxPrint);
|
|
103
|
+
resultCache.set(key, result);
|
|
104
|
+
}
|
|
105
|
+
const { count } = result;
|
|
106
|
+
const lit = internLiteral(String(count));
|
|
107
|
+
const next = unifyTerm(goal.o, lit, subst);
|
|
108
|
+
return next ? [next] : [];
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
registerBuiltin(NS + 'render', ({ goal, subst }) => {
|
|
112
|
+
const subject = applySubstTerm(goal.s, subst);
|
|
113
|
+
if (!(subject instanceof ListTerm) || subject.elems.length !== 2) return [];
|
|
114
|
+
|
|
115
|
+
const n = integerValue(applySubstTerm(subject.elems[0], subst));
|
|
116
|
+
const maxPrint = integerValue(applySubstTerm(subject.elems[1], subst));
|
|
117
|
+
if (n == null || maxPrint == null) return [];
|
|
118
|
+
|
|
119
|
+
const key = `${n}/${maxPrint}`;
|
|
120
|
+
let result = resultCache.get(key);
|
|
121
|
+
if (!result) {
|
|
122
|
+
result = solveNQueens(n, maxPrint);
|
|
123
|
+
resultCache.set(key, result);
|
|
124
|
+
}
|
|
125
|
+
const { count, printed } = result;
|
|
126
|
+
const body = [
|
|
127
|
+
`Solving ${n}-Queens...`,
|
|
128
|
+
`Printing at most ${maxPrint} solution(s).`,
|
|
129
|
+
'',
|
|
130
|
+
...printed,
|
|
131
|
+
...(printed.length ? [''] : []),
|
|
132
|
+
`Total solutions for ${n}-Queens: ${count}`,
|
|
133
|
+
'',
|
|
134
|
+
].join('\n');
|
|
135
|
+
|
|
136
|
+
// Eyeling string literals are represented by their quoted lexical form.
|
|
137
|
+
const lit = internLiteral(JSON.stringify(body));
|
|
138
|
+
const next = unifyTerm(goal.o, lit, subst);
|
|
139
|
+
return next ? [next] : [];
|
|
140
|
+
});
|
|
141
|
+
};
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Example-specific builtin module for examples/sudoku.n3.
|
|
4
|
+
//
|
|
5
|
+
// The N3 file keeps the Sudoku report declarative: it asks predicates in the
|
|
6
|
+
// http://example.org/sudoku-builtin# namespace for facts such as the normalized
|
|
7
|
+
// puzzle, solution, move counts, and validation checks.
|
|
8
|
+
//
|
|
9
|
+
// This JavaScript module supplies the specialized Sudoku search/verification
|
|
10
|
+
// kernel. It is loaded by the examples test runner in the same uniform way as
|
|
11
|
+
// examples/builtin/queens.js:
|
|
12
|
+
//
|
|
13
|
+
// node eyeling.js --builtin examples/builtin/sudoku.js examples/sudoku.n3
|
|
14
|
+
//
|
|
15
|
+
// Keeping this under examples/builtin/ makes the example self-contained and
|
|
16
|
+
// avoids registering example-specific predicates in the core runtime.
|
|
17
|
+
|
|
3
18
|
module.exports = function registerSudokuBuiltins(api) {
|
|
4
19
|
const { registerBuiltin, internLiteral, termToJsString, unifyTerm, terms } = api;
|
|
5
20
|
const { Var } = terms;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Solving 14-Queens...
|
|
2
|
+
Printing at most 1 solution(s).
|
|
3
|
+
|
|
4
|
+
Solution 1:
|
|
5
|
+
Q . . . . . . . . . . . . .
|
|
6
|
+
. . Q . . . . . . . . . . .
|
|
7
|
+
. . . . Q . . . . . . . . .
|
|
8
|
+
. . . . . . Q . . . . . . .
|
|
9
|
+
. . . . . . . . . . . Q . .
|
|
10
|
+
. . . . . . . . . Q . . . .
|
|
11
|
+
. . . . . . . . . . . . Q .
|
|
12
|
+
. . . Q . . . . . . . . . .
|
|
13
|
+
. . . . . . . . . . . . . Q
|
|
14
|
+
. . . . . . . . Q . . . . .
|
|
15
|
+
. Q . . . . . . . . . . . .
|
|
16
|
+
. . . . . Q . . . . . . . .
|
|
17
|
+
. . . . . . . Q . . . . . .
|
|
18
|
+
. . . . . . . . . . Q . . .
|
|
19
|
+
As column positions by row: [1, 3, 5, 7, 12, 10, 13, 4, 14, 9, 2, 6, 8, 11]
|
|
20
|
+
|
|
21
|
+
Total solutions for 14-Queens: 365596
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# N-Queens benchmark wrapper for Eyeling.
|
|
2
|
+
#
|
|
3
|
+
# Run:
|
|
4
|
+
# node eyeling.js --builtin examples/builtin/queens.js examples/queens.n3
|
|
5
|
+
#
|
|
6
|
+
# The tight bit-mask search kernel lives in examples/builtin/queens.js.
|
|
7
|
+
# Pure N3 can express N-Queens declaratively, but a 16x16 exhaustive count is
|
|
8
|
+
# not a practical pure-N3 benchmark for the current engine.
|
|
9
|
+
|
|
10
|
+
@prefix : <http://example.org/queens#>.
|
|
11
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
12
|
+
|
|
13
|
+
:run :n 14; :maxPrint 1.
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
:run :n ?N; :maxPrint ?MaxPrint.
|
|
17
|
+
(?N ?MaxPrint) :render ?Report.
|
|
18
|
+
} log:query {
|
|
19
|
+
:answer log:outputString ?Report.
|
|
20
|
+
}.
|
package/examples/sudoku.n3
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# ==================================================
|
|
2
2
|
# sudoku.n3
|
|
3
3
|
#
|
|
4
|
-
# A
|
|
4
|
+
# A Sudoku solver and report generator.
|
|
5
|
+
#
|
|
6
|
+
# Run:
|
|
7
|
+
# node eyeling.js --builtin examples/builtin/sudoku.js examples/sudoku.n3
|
|
8
|
+
#
|
|
9
|
+
# The specialized solver lives in examples/builtin/sudoku.js. The N3 below
|
|
10
|
+
# describes the report and validation checks around that solver.
|
|
5
11
|
#
|
|
6
12
|
# Edit :case :puzzle to solve another puzzle.
|
|
7
13
|
# Accepted blanks: 0, ., _
|
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
|
|
@@ -6373,10 +5905,6 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
|
|
|
6373
5905
|
termsEqualNoIntDecimal,
|
|
6374
5906
|
});
|
|
6375
5907
|
|
|
6376
|
-
try {
|
|
6377
|
-
registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
|
|
6378
|
-
} catch (_) {}
|
|
6379
|
-
|
|
6380
5908
|
// Initialize proof/output helpers (implemented in lib/explain.js).
|
|
6381
5909
|
const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
|
|
6382
5910
|
applySubstTerm,
|
|
@@ -10770,7 +10298,7 @@ function parseN3SourceList(input, opts = {}) {
|
|
|
10770
10298
|
if (!isN3SourceListInput(input)) return null;
|
|
10771
10299
|
const sources = input.sources.map(normalizeN3SourceItem);
|
|
10772
10300
|
const defaultBaseIri = typeof opts.baseIri === 'string' ? opts.baseIri : '';
|
|
10773
|
-
const parsed = sources.map(source =>
|
|
10301
|
+
const parsed = sources.map((source) =>
|
|
10774
10302
|
parseN3Text(source.text, {
|
|
10775
10303
|
label: source.label,
|
|
10776
10304
|
baseIri: source.baseIri || (sources.length === 1 ? defaultBaseIri : ''),
|
package/lib/engine.js
CHANGED
|
@@ -610,10 +610,6 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
|
|
|
610
610
|
termsEqualNoIntDecimal,
|
|
611
611
|
});
|
|
612
612
|
|
|
613
|
-
try {
|
|
614
|
-
registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
|
|
615
|
-
} catch (_) {}
|
|
616
|
-
|
|
617
613
|
// Initialize proof/output helpers (implemented in lib/explain.js).
|
|
618
614
|
const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
|
|
619
615
|
applySubstTerm,
|
package/package.json
CHANGED
package/test/examples.test.js
CHANGED
|
@@ -96,6 +96,14 @@ function resolveExpectedPath(outputDir, inputFile) {
|
|
|
96
96
|
return path.join(outputDir, candidates[0]);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function resolveExampleBuiltinPath(root, inputFile) {
|
|
100
|
+
const stem = path.basename(inputFile, path.extname(inputFile));
|
|
101
|
+
const rel = path.join('examples', 'builtin', `${stem}.js`);
|
|
102
|
+
const abs = path.join(root, rel);
|
|
103
|
+
if (!fs.existsSync(abs)) return null;
|
|
104
|
+
return { abs, rel };
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
function main() {
|
|
100
108
|
const suiteStart = Date.now();
|
|
101
109
|
|
|
@@ -170,17 +178,32 @@ function main() {
|
|
|
170
178
|
const tmpDir = mkTmpDir();
|
|
171
179
|
const generatedPath = path.join(tmpDir, 'generated.n3');
|
|
172
180
|
|
|
173
|
-
// Run eyeling on this file
|
|
181
|
+
// Run eyeling on this file. If examples/builtin/<stem>.js exists,
|
|
182
|
+
// load it for the matching examples/<stem>.n3 file. Builtin-backed examples
|
|
183
|
+
// run from the repository root so the command shape matches documented usage:
|
|
184
|
+
// node eyeling.js --builtin examples/builtin/foo.js examples/foo.n3
|
|
185
|
+
const builtin = resolveExampleBuiltinPath(root, file);
|
|
174
186
|
const outFd = fs.openSync(generatedPath, 'w');
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
let r;
|
|
188
|
+
try {
|
|
189
|
+
if (builtin) {
|
|
190
|
+
r = cp.spawnSync(nodePath, [eyelingJsPath, '-d', '--builtin', builtin.rel, path.join('examples', file)], {
|
|
191
|
+
cwd: root,
|
|
192
|
+
stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
|
|
193
|
+
maxBuffer: 200 * 1024 * 1024,
|
|
194
|
+
encoding: 'utf8',
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
r = cp.spawnSync(nodePath, [eyelingJsPath, '-d', file], {
|
|
198
|
+
cwd: examplesDir,
|
|
199
|
+
stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
|
|
200
|
+
maxBuffer: 200 * 1024 * 1024,
|
|
201
|
+
encoding: 'utf8',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
fs.closeSync(outFd);
|
|
206
|
+
}
|
|
184
207
|
|
|
185
208
|
const rc = r.status == null ? 1 : r.status;
|
|
186
209
|
|