@dallaylaen/ski-interpreter 2.8.1 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/bin/ski.js +38 -15
- package/example/collatz.ski +30 -0
- package/example/eval.js +34 -0
- package/example/example-quests.json +83 -0
- package/example/find-monobase.js +108 -0
- package/example/mini-quest.html +42 -0
- package/example/reverse-linked-list.ski +14 -0
- package/example/t-foo-bar.ski +5 -0
- package/lib/ski-interpreter.cjs.js +42 -28
- package/lib/ski-interpreter.cjs.js.map +2 -2
- package/lib/ski-interpreter.min.js +5 -5
- package/lib/ski-interpreter.min.js.map +3 -3
- package/lib/ski-interpreter.mjs +42 -28
- package/lib/ski-interpreter.mjs.map +2 -2
- package/lib/ski-quest.min.js +5 -5
- package/lib/ski-quest.min.js.map +3 -3
- package/lib/types/extras.d.ts +49 -40
- package/lib/types/index.d.ts +1 -1
- package/lib/types/quest.d.ts +1 -0
- package/package.json +2 -1
- package/lib/types/toposort.d.ts +0 -29
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.9.0] - 2026-06-27
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* SKI.extras.search is now a generator, yielding progress ticks as well as found terms, which may now be multiple.
|
|
13
|
+
* Quest page now requires { chapters: [...] } instead of [...] in index.json.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
* Examples: `example/find-monobase.js` to showcase nested search() usage.
|
|
18
|
+
* Quest page: "New!" badge for quests newer than 30 days.
|
|
19
|
+
* Quest page: Reordered for clearer narrative, more quests added.
|
|
20
|
+
* `bin/ski.js search` diplays elapsed time.
|
|
21
|
+
* `bin/ski.js -q` flag to suppress extra output.
|
|
22
|
+
* `bin/ski.js file -` will process STDIN
|
|
23
|
+
* Added a self-hosted [combinator cheat-sheet page](https://dallaylaen.github.io/ski-interpreter/combinator-birds.html) (#29).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
* `bin/ski.js search` output is now parseable (metadata commented out)
|
|
27
|
+
* Better Collatz conjecture in examples (thx @Darkwing3125) fixes #28
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
* Solved quests weren't displayed as solved
|
|
31
|
+
* History button svg (thx @akuklev)
|
|
32
|
+
|
|
33
|
+
## [2.8.2] - 2026-04-26
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `examples/` are now included in the package
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- removed stale .d.ts file from lib/
|
|
42
|
+
|
|
8
43
|
## [2.8.1] - 2026-04-26
|
|
9
44
|
|
|
10
45
|
### Added
|
package/bin/ski.js
CHANGED
|
@@ -11,6 +11,7 @@ const runOptions = {};
|
|
|
11
11
|
/** @type FormatOptions */
|
|
12
12
|
let format = {};
|
|
13
13
|
let verbose = false;
|
|
14
|
+
let quiet = false;
|
|
14
15
|
|
|
15
16
|
const program = new Command();
|
|
16
17
|
|
|
@@ -19,6 +20,7 @@ program
|
|
|
19
20
|
.description('Simple Kombinator Interpreter - a combinatory logic & lambda calculus parser and interpreter')
|
|
20
21
|
.version(version)
|
|
21
22
|
.option('-v, --verbose', 'Show all evaluation steps', () => { verbose = true; })
|
|
23
|
+
.option('-q, --quiet', 'Suppress comment lines in output', () => { quiet = true; })
|
|
22
24
|
.option('--format <json>', 'Format for output expressions', setFormat)
|
|
23
25
|
.option('--max <number>', 'Limit computation steps', raw => {
|
|
24
26
|
const n = Number.parseInt(raw);
|
|
@@ -165,13 +167,19 @@ function evaluateExpression (expression) {
|
|
|
165
167
|
|
|
166
168
|
function evaluateFile (filepath) {
|
|
167
169
|
const ski = new SKI();
|
|
170
|
+
const onErr = err => {
|
|
171
|
+
console.error('' + err);
|
|
172
|
+
process.exit(3);
|
|
173
|
+
};
|
|
174
|
+
if (filepath === '-') {
|
|
175
|
+
let source = '';
|
|
176
|
+
process.stdin.setEncoding('utf8');
|
|
177
|
+
process.stdin.on('data', chunk => { source += chunk; });
|
|
178
|
+
process.stdin.on('end', () => { processLine(source, ski, onErr); });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
168
181
|
fs.readFile(filepath, 'utf8')
|
|
169
|
-
.then(source => {
|
|
170
|
-
processLine(source, ski, err => {
|
|
171
|
-
console.error('' + err);
|
|
172
|
-
process.exit(3);
|
|
173
|
-
});
|
|
174
|
-
})
|
|
182
|
+
.then(source => { processLine(source, ski, onErr); })
|
|
175
183
|
.catch(err => {
|
|
176
184
|
console.error('ski: ' + err);
|
|
177
185
|
process.exit(2);
|
|
@@ -190,7 +198,8 @@ function processLine (source, ski, onErr) {
|
|
|
190
198
|
|
|
191
199
|
for (const state of expr.walk(runOptions)) {
|
|
192
200
|
if (state.final) {
|
|
193
|
-
|
|
201
|
+
if (!quiet)
|
|
202
|
+
console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
|
|
194
203
|
console.log(state.expr.declare({ ...format, inventory: ski.getTerms() }));
|
|
195
204
|
} else if (verbose)
|
|
196
205
|
console.log(state.expr.format(format) + ';');
|
|
@@ -232,7 +241,7 @@ function displayInfer (guess) {
|
|
|
232
241
|
console.log(guess.expr.format(format));
|
|
233
242
|
|
|
234
243
|
for (const key of ['normal', 'proper', 'arity', 'discard', 'duplicate', 'steps']) {
|
|
235
|
-
if (guess[key] !== undefined)
|
|
244
|
+
if (guess[key] !== undefined && !quiet)
|
|
236
245
|
console.log(`// ${key}: ${guess[key]}`);
|
|
237
246
|
}
|
|
238
247
|
}
|
|
@@ -312,19 +321,33 @@ function searchExpression (targetStr, termStrs, options) {
|
|
|
312
321
|
process.exit(1);
|
|
313
322
|
}
|
|
314
323
|
|
|
315
|
-
const
|
|
324
|
+
const t0 = new Date();
|
|
325
|
+
let lastProgress;
|
|
326
|
+
let found;
|
|
327
|
+
for (const progress of SKI.extras.search(seed, { tries: options.maxTries, depth: options.maxDepth }, (e, p) => {
|
|
316
328
|
if (!p.expr)
|
|
317
|
-
return -1;
|
|
329
|
+
return { offset: -1 };
|
|
318
330
|
if (p.expr.equals(expr))
|
|
319
|
-
return
|
|
331
|
+
return { found: true, stop: true };
|
|
320
332
|
return 0;
|
|
321
|
-
})
|
|
333
|
+
})) {
|
|
334
|
+
lastProgress = progress;
|
|
335
|
+
if (progress.found) {
|
|
336
|
+
found = progress.expr;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const elapsed = new Date() - t0;
|
|
341
|
+
const { total = 0 } = lastProgress ?? {};
|
|
322
342
|
|
|
323
|
-
if (
|
|
324
|
-
console.log(
|
|
343
|
+
if (found) {
|
|
344
|
+
console.log(found.format(format));
|
|
345
|
+
if (!quiet)
|
|
346
|
+
console.log(`// Found after ${total} tries in ${elapsed}ms.`);
|
|
325
347
|
process.exit(0);
|
|
326
348
|
} else {
|
|
327
|
-
|
|
349
|
+
if (!quiet)
|
|
350
|
+
console.log(`// No expression was found after ${total} tries in ${elapsed}ms.`);
|
|
328
351
|
process.exit(1);
|
|
329
352
|
}
|
|
330
353
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// This file generates a trace of a Collatz 3n+1 sequence,
|
|
2
|
+
// producing HUGE number of steps with very deep recursion,
|
|
3
|
+
// and can thus be used as a rough performance benchmark.
|
|
4
|
+
|
|
5
|
+
// `calc f n` == `f n`, provided n is a number, except that `n` is
|
|
6
|
+
// forced into a redex position before being fed to `f`,
|
|
7
|
+
// thus eliminating possible duplicate computations.
|
|
8
|
+
|
|
9
|
+
T=CI;
|
|
10
|
+
V=BCT;
|
|
11
|
+
calc=BBB(T 0)(V(CB+));
|
|
12
|
+
if=BBB(B(BW)C)C;
|
|
13
|
+
half=C(V(C(V(T(KI))(C(B+)K)))(K 0))K;
|
|
14
|
+
collatz=C(W(C(CI(B(CB+)C))0))0; // courtesy of @Darkwing3125
|
|
15
|
+
dec=BBBCV(C(BT)+)(K 0)I;
|
|
16
|
+
M=WI;
|
|
17
|
+
wait4=BBB(BCC)(BCC);
|
|
18
|
+
nil=KI;
|
|
19
|
+
lst=BS(C(BB));
|
|
20
|
+
fold=M(B(SI)(wait4M))(B(C(BV(BBBK))nil)(C(BBlst)));
|
|
21
|
+
L=BWB;
|
|
22
|
+
lazyy=M(B(SI)(wait4M));
|
|
23
|
+
lesseq=C(BB(VT(KK)))(VT(K(KI)));
|
|
24
|
+
P=M(C(BL(WC)));
|
|
25
|
+
R=CC;
|
|
26
|
+
v3=BBBCV;
|
|
27
|
+
|
|
28
|
+
BML(B(S(B(BB(B(Bcalc(SV))))(Cif (Knil)))) (B(S(CB)))) (C lesseq 1) (collatz) 127
|
|
29
|
+
// BML(B(S(B(BB(B((SV))))(Cif (Knil)))) (B(S(CB)))) (C lesseq 1) (collatz) 27
|
|
30
|
+
|
package/example/eval.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A minimal example of evaluating a SKI expression.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { SKI } = require('../lib/ski-interpreter.cjs');
|
|
10
|
+
// SKI is the only export and contains other goodies as subkeys
|
|
11
|
+
|
|
12
|
+
const [node, path, src] = process.argv;
|
|
13
|
+
|
|
14
|
+
if (!src) {
|
|
15
|
+
console.log(`Usage: ${node} ${path} <src>`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ski = new SKI();
|
|
20
|
+
|
|
21
|
+
// add some extra terms
|
|
22
|
+
ski.add('M', 'SII'); // an alias
|
|
23
|
+
ski.add('L', x => y => x.apply(y.apply(y))) // ditto but with a 'Native' js impl
|
|
24
|
+
|
|
25
|
+
const expr = ski.parse(src);
|
|
26
|
+
|
|
27
|
+
// expr.step() = 1 reduction, expr.run() = reduce to the end
|
|
28
|
+
// expr.walk() = an iterator
|
|
29
|
+
// all three return an object with { steps, expr, final } where:
|
|
30
|
+
// - steps = the number of reductions performed so far
|
|
31
|
+
// - expr = the resulting expression
|
|
32
|
+
// - final = boolean indicating if the expression is in normal form (no more reductions possible)
|
|
33
|
+
for (const step of expr.walk())
|
|
34
|
+
console.log(`[${step.steps}] ${step.expr}`);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "dull6XUf",
|
|
3
|
+
"created_at": "2026-02-27T00:00:00",
|
|
4
|
+
"name": "Example quests",
|
|
5
|
+
"intro": "<p>A small template collection of combinatory logic exercises. Feel free to use this file as a starting point for your own quest chapter.</p>",
|
|
6
|
+
"content": [
|
|
7
|
+
{
|
|
8
|
+
"id": "YpgCLTl8",
|
|
9
|
+
"created_at": "2026-02-27T00:00:00",
|
|
10
|
+
"name": "I from SK",
|
|
11
|
+
"intro": [
|
|
12
|
+
"<p>The identity combinator <code>I</code> satisfies <code>I x = x</code>.",
|
|
13
|
+
"It can actually be derived from <code>S</code> and <code>K</code> alone —",
|
|
14
|
+
"no built-in <code>I</code> needed.</p>",
|
|
15
|
+
"<p>Find a term built only from <code>S</code> and <code>K</code> that behaves like <code>I</code>.</p>"
|
|
16
|
+
],
|
|
17
|
+
"hint": "Think about what <code>S K K x</code> reduces to.",
|
|
18
|
+
"allow": "SK",
|
|
19
|
+
"input": "phi",
|
|
20
|
+
"cases": [
|
|
21
|
+
[ "phi x", "x" ],
|
|
22
|
+
[ "phi (K x y)", "K x y" ]
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "vQsBd85P",
|
|
27
|
+
"created_at": "2026-02-27T00:00:00",
|
|
28
|
+
"name": "NAND for K/KI",
|
|
29
|
+
"intro": [
|
|
30
|
+
"<p>Using the Church encoding, <code>K</code> represents <b>true</b> and <code>KI</code> represents <b>false</b>.</p>",
|
|
31
|
+
"<p>Implement <code>nand</code>: it should return <code>K</code> (true) for every pair of inputs",
|
|
32
|
+
"except when <i>both</i> are <code>K</code> (true), in which case it returns <code>KI</code> (false).</p>",
|
|
33
|
+
"<p>Apply the result to <code>true</code> and <code>false</code> witnesses to observe it.</p>"
|
|
34
|
+
],
|
|
35
|
+
"input": "phi",
|
|
36
|
+
"cases": [
|
|
37
|
+
[ "phi K K true false", "false" ],
|
|
38
|
+
[ "phi (KI) K true false", "true" ],
|
|
39
|
+
[ "phi K (KI) true false", "true" ],
|
|
40
|
+
[ "phi (KI) (KI) true false", "true" ]
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "IgVco9YW",
|
|
45
|
+
"created_at": "2026-02-27T00:00:00",
|
|
46
|
+
"name": "Linear basis from I and your own combinator",
|
|
47
|
+
"intro": [
|
|
48
|
+
"<p>It can be shown that <code>I</code>, <code>B</code>, and <code>C</code> (or <code>T</code>)",
|
|
49
|
+
"form a complete basis for <i>linear</i> combinators — those that use every argument",
|
|
50
|
+
"exactly once.</p>",
|
|
51
|
+
"<p>Surprisingly, <code>B</code> and <code>C</code> can be merged into a single combinator.",
|
|
52
|
+
"Your task is to <i>declare</i> such a combinator <code>P</code> (lambdas are allowed here),",
|
|
53
|
+
"and then <i>prove</i> the basis is complete by implementing <code>B</code> and <code>T</code>",
|
|
54
|
+
"using only <code>I</code> and your <code>P</code>.</p>"
|
|
55
|
+
],
|
|
56
|
+
"input": [
|
|
57
|
+
{
|
|
58
|
+
"name": "P",
|
|
59
|
+
"lambdas": true,
|
|
60
|
+
"allow": "I-I",
|
|
61
|
+
"note": "Declare your own linear combinator (lambdas allowed, no built-ins)."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "B",
|
|
65
|
+
"lambdas": false,
|
|
66
|
+
"allow": "I",
|
|
67
|
+
"note": "Implement <code>B a b c = a (b c)</code> using <code>P</code> and <code>I</code>."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "T",
|
|
71
|
+
"lambdas": false,
|
|
72
|
+
"allow": "I",
|
|
73
|
+
"note": "Implement <code>T a b = b a</code> using <code>P</code> and <code>I</code>."
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"cases": [
|
|
77
|
+
[ { "caps": { "linear": true } }, "P" ],
|
|
78
|
+
[ "B a b c", "a (b c)" ],
|
|
79
|
+
[ "T a b", "b a" ]
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This script showcases advanced usage of SKI.extras.search.
|
|
5
|
+
*
|
|
6
|
+
* A monobase is a single term that allows to construct a given set of terms (for example, _all of them_)
|
|
7
|
+
* by only applying itself to itself.
|
|
8
|
+
* The most well-known example is perhaps the iota combinator: x->xSK.
|
|
9
|
+
*
|
|
10
|
+
* To achieve this, we apply a brute force search twice: first to find all possible candidate terms,
|
|
11
|
+
* and second to check if the term at hand is actually a monobase.
|
|
12
|
+
*
|
|
13
|
+
* The script keeps track of the target expression max size and reduces the search depth accordingly,
|
|
14
|
+
* to reduce the computational cost.
|
|
15
|
+
*
|
|
16
|
+
* Progress indicators are emitted in the meantime.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { SKI } = require('../src');
|
|
20
|
+
|
|
21
|
+
const ski = new SKI();
|
|
22
|
+
|
|
23
|
+
const { search, deepFormat } = SKI.extras;
|
|
24
|
+
|
|
25
|
+
// Parse CLI arguments: seed seed seed ... -- target target target ...
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const separatorIdx = args.indexOf('--');
|
|
28
|
+
|
|
29
|
+
if (separatorIdx === -1 || separatorIdx === 0 || separatorIdx === args.length - 1) {
|
|
30
|
+
console.error('Usage: node find-monobase.js <seed> [<seed>...] -- <target> [<target>...]');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const seedStrs = args.slice(0, separatorIdx);
|
|
35
|
+
const targetStrs = args.slice(separatorIdx + 1);
|
|
36
|
+
|
|
37
|
+
const seed = seedStrs.map(s => ski.parse(s));
|
|
38
|
+
const target = targetStrs.map(s => ski.parse(s));
|
|
39
|
+
|
|
40
|
+
let best = 12;
|
|
41
|
+
const t0 = new Date();
|
|
42
|
+
|
|
43
|
+
for (const progress of search(seed, { depth: 100, max: 150, tries: 100_000_000, progressInterval: 100_000 }, (expr, prop) => {
|
|
44
|
+
if (!prop.expr)
|
|
45
|
+
return { offset: -1 };
|
|
46
|
+
|
|
47
|
+
// if (!prop.discard || !prop.duplicate)
|
|
48
|
+
// return 0;
|
|
49
|
+
|
|
50
|
+
const term = new SKI.classes.Alias('X', expr);
|
|
51
|
+
const ret = checkBase([term], target, { depth: best, tries: 100_000, maxArgs: 8, max: 150 });
|
|
52
|
+
if (!ret)
|
|
53
|
+
return 0;
|
|
54
|
+
|
|
55
|
+
const maxlen = Math.max(
|
|
56
|
+
...Object.values(ret)
|
|
57
|
+
.map(e => countIn(e, term))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (maxlen <= best) {
|
|
61
|
+
console.log(`New best monobase(${maxlen}): X=${expr} in ${(new Date() - t0) / 1000}s`
|
|
62
|
+
, deepFormat(ret));
|
|
63
|
+
best = maxlen;
|
|
64
|
+
return { found: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return 0; // anyways...
|
|
68
|
+
}))
|
|
69
|
+
console.log((new Date() - t0) / 1000, progress.total, progress.gen);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param { Expr[] } base
|
|
73
|
+
* @param { Expr[] } target
|
|
74
|
+
*
|
|
75
|
+
*
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
function checkBase (base, target, options) {
|
|
79
|
+
const need = {};
|
|
80
|
+
target.forEach(t => { need[t.infer().expr] = t });
|
|
81
|
+
const found = {};
|
|
82
|
+
|
|
83
|
+
// console.log(deepFormat(target));
|
|
84
|
+
|
|
85
|
+
for (const progress of search(base, options, (e, p) => {
|
|
86
|
+
if (!p.expr) return { offset: -1 };
|
|
87
|
+
const maybe = need[p.expr];
|
|
88
|
+
if (!maybe) return 0;
|
|
89
|
+
|
|
90
|
+
// console.log(`found ${maybe} as ${e}`);
|
|
91
|
+
|
|
92
|
+
found[maybe] = e;
|
|
93
|
+
delete need[p.expr];
|
|
94
|
+
return Object.keys(need).length === 0 ? { found: true, stop: true } : 0;
|
|
95
|
+
})) {
|
|
96
|
+
if (progress.found)
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function countIn (expr, target) {
|
|
104
|
+
return expr.fold(0, (acc, e) => {
|
|
105
|
+
if (e === target)
|
|
106
|
+
return SKI.control.prune(acc + 1);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"/>
|
|
5
|
+
<title>Example Quests - Simple Kombinator Interpreter</title>
|
|
6
|
+
<script src="../docs/build/js/ski-quest.min.js"></script>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: sans-serif;
|
|
10
|
+
max-width: 860px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 1em;
|
|
13
|
+
}
|
|
14
|
+
#inventory {
|
|
15
|
+
border: 1px solid #ccc;
|
|
16
|
+
padding: 0.5em 1em;
|
|
17
|
+
margin-bottom: 1em;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<h1>Example Quests</h1>
|
|
23
|
+
|
|
24
|
+
<div id="inventory">
|
|
25
|
+
<b>Inventory</b>
|
|
26
|
+
<ul id="known"></ul>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div id="quest"></div>
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
const page = new QuestPage({
|
|
33
|
+
storePrefix: 'example-quests',
|
|
34
|
+
baseUrl: '.',
|
|
35
|
+
contentBox: document.getElementById('quest'),
|
|
36
|
+
inventoryBox: document.getElementById('known'),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
page.loadChapters(['example-quests.json']);
|
|
40
|
+
</script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -1712,6 +1712,8 @@ var Quest = class {
|
|
|
1712
1712
|
this.intro = list2str(options.intro);
|
|
1713
1713
|
this.id = options.id;
|
|
1714
1714
|
this.meta = meta;
|
|
1715
|
+
if (meta.created_at)
|
|
1716
|
+
this.created = new Date(meta.created_at);
|
|
1715
1717
|
for (const c of cases ?? [])
|
|
1716
1718
|
this.add(...c);
|
|
1717
1719
|
}
|
|
@@ -1859,10 +1861,9 @@ var Quest = class {
|
|
|
1859
1861
|
findings[field] = found;
|
|
1860
1862
|
}
|
|
1861
1863
|
if (options.date) {
|
|
1862
|
-
|
|
1863
|
-
if (isNaN(date.getTime()))
|
|
1864
|
+
if (!this.created || isNaN(this.created.getTime()))
|
|
1864
1865
|
findings.date = "invalid date format: " + this.meta?.created_at;
|
|
1865
|
-
else if (
|
|
1866
|
+
else if (this.created < /* @__PURE__ */ new Date("2024-07-15") || this.created > /* @__PURE__ */ new Date())
|
|
1866
1867
|
findings.date = "date out of range: " + this.meta?.created_at;
|
|
1867
1868
|
}
|
|
1868
1869
|
return findings;
|
|
@@ -2156,7 +2157,7 @@ function deepFormat(obj, options = {}) {
|
|
|
2156
2157
|
out[key] = deepFormat(obj[key], options);
|
|
2157
2158
|
return out;
|
|
2158
2159
|
}
|
|
2159
|
-
function search(seed, options, predicate) {
|
|
2160
|
+
function* search(seed, options, predicate) {
|
|
2160
2161
|
const {
|
|
2161
2162
|
depth = 16,
|
|
2162
2163
|
infer = true,
|
|
@@ -2167,57 +2168,70 @@ function search(seed, options, predicate) {
|
|
|
2167
2168
|
let total = 0;
|
|
2168
2169
|
let probed = 0;
|
|
2169
2170
|
const seen = {};
|
|
2171
|
+
const parseResult = (raw) => {
|
|
2172
|
+
if (raw === null || raw === void 0)
|
|
2173
|
+
return {};
|
|
2174
|
+
if (typeof raw === "number")
|
|
2175
|
+
return raw > 0 ? { found: true, stop: true } : raw < 0 ? { offset: -1 } : {};
|
|
2176
|
+
return raw;
|
|
2177
|
+
};
|
|
2170
2178
|
const maybeProbe = (term) => {
|
|
2171
2179
|
total++;
|
|
2172
2180
|
const props = infer ? term.infer({ max: options.max, maxArgs: options.maxArgs }) : null;
|
|
2173
2181
|
if (hasSeen && props && props.expr) {
|
|
2174
2182
|
const key = String(props.expr);
|
|
2175
2183
|
if (seen[key])
|
|
2176
|
-
return { res: -1, props };
|
|
2184
|
+
return { res: { offset: -1 }, props };
|
|
2177
2185
|
seen[key] = true;
|
|
2178
2186
|
}
|
|
2179
2187
|
probed++;
|
|
2180
|
-
const res = predicate(term, props);
|
|
2188
|
+
const res = parseResult(predicate(term, props));
|
|
2181
2189
|
return { res, props };
|
|
2182
2190
|
};
|
|
2183
2191
|
for (const term of seed) {
|
|
2184
|
-
const { res
|
|
2185
|
-
if (res
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2192
|
+
const { res } = maybeProbe(term);
|
|
2193
|
+
if (res.found)
|
|
2194
|
+
yield { expr: term, found: true, step: false, gen: 0, total, probed, cache };
|
|
2195
|
+
if (res.stop)
|
|
2196
|
+
return;
|
|
2197
|
+
if ((res.offset ?? 0) >= 0)
|
|
2198
|
+
cache[0].push(term);
|
|
2190
2199
|
}
|
|
2191
2200
|
let lastProgress = 0;
|
|
2192
2201
|
for (let gen = 1; gen < depth; gen++) {
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
lastProgress = total;
|
|
2196
|
-
}
|
|
2202
|
+
yield { found: false, step: true, gen, total, probed, cache };
|
|
2203
|
+
lastProgress = total;
|
|
2197
2204
|
for (let i = 0; i < gen; i++) {
|
|
2198
2205
|
for (const a of cache[gen - i - 1] || []) {
|
|
2199
2206
|
for (const b of cache[i] || []) {
|
|
2200
|
-
if (total >= (options.tries ?? Infinity))
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2207
|
+
if (total >= (options.tries ?? Infinity)) {
|
|
2208
|
+
yield { found: false, step: false, gen, total, probed, cache };
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (total - lastProgress >= progressInterval) {
|
|
2212
|
+
yield { found: false, step: false, gen, total, probed, cache };
|
|
2204
2213
|
lastProgress = total;
|
|
2205
2214
|
}
|
|
2206
2215
|
const term = a.apply(b);
|
|
2207
2216
|
const { res, props } = maybeProbe(term);
|
|
2208
|
-
if (
|
|
2209
|
-
|
|
2210
|
-
|
|
2217
|
+
if (res.found)
|
|
2218
|
+
yield { expr: term, found: true, step: false, gen, total, probed, cache };
|
|
2219
|
+
if (res.stop) {
|
|
2220
|
+
yield { found: false, step: false, gen, total, probed, cache };
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
if ((res.offset ?? 0) < 0)
|
|
2211
2224
|
continue;
|
|
2212
|
-
const
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2225
|
+
const autoOffset = infer && props ? (props.expr ? 0 : 3) + (props.dup ? 1 : 0) + (props.proper ? 0 : 1) : 0;
|
|
2226
|
+
const finalOffset = res.offset ?? autoOffset;
|
|
2227
|
+
if (!cache[gen + finalOffset])
|
|
2228
|
+
cache[gen + finalOffset] = [];
|
|
2229
|
+
cache[gen + finalOffset].push(term);
|
|
2216
2230
|
}
|
|
2217
2231
|
}
|
|
2218
2232
|
}
|
|
2219
2233
|
}
|
|
2220
|
-
|
|
2234
|
+
yield { found: false, step: false, gen: depth, total, probed, cache };
|
|
2221
2235
|
}
|
|
2222
2236
|
function isStringPair(x) {
|
|
2223
2237
|
return Array.isArray(x) && x.length === 2 && typeof x[0] === "string" && typeof x[1] === "string" ? void 0 : "must be a pair of strings";
|