@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 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
- console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
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 res = SKI.extras.search(seed, { tries: options.maxTries, depth: options.maxDepth }, (e, p) => {
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 1;
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 (res.expr) {
324
- console.log(`Found ${res.expr.format(format)} after ${res.total} tries.`);
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
- console.error(`No equivalent expression found for ${target} after ${res.total} tries.`);
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
+
@@ -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 &mdash;",
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 &mdash; 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>
@@ -0,0 +1,14 @@
1
+
2
+ T=CI;
3
+ V=BCT;
4
+ L=BWB;
5
+
6
+ nil = KI;
7
+
8
+ fix = WSL;
9
+
10
+ rev = V(fix (B(B(B(BC)(CBV))B)V)) nil;
11
+
12
+ rev (V a (V b (V c (V d (V e nil)))))
13
+
14
+
@@ -0,0 +1,5 @@
1
+ // This is an example file written in SKI
2
+ // usage: ./bin/ski.js <this file>
3
+ // The expression below evaluates to bar(foo)
4
+
5
+ S(K(SI))K foo bar
@@ -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
- const date = new Date(this.meta?.created_at);
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 (date.getTime() < (/* @__PURE__ */ new Date("2024-07-15")).getTime() || date.getTime() > (/* @__PURE__ */ new Date()).getTime())
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 = 0 } = maybeProbe(term);
2185
- if (res > 0)
2186
- return { expr: term, total, probed, gen: 1 };
2187
- else if (res < 0)
2188
- continue;
2189
- cache[0].push(term);
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
- if (options.progress) {
2194
- options.progress({ gen, total, probed, step: true });
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
- return { total, probed, gen, ...options.retain ? { cache } : {} };
2202
- if (options.progress && total - lastProgress >= progressInterval) {
2203
- options.progress({ gen, total, probed, step: false });
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 ((res ?? 0) > 0)
2209
- return { expr: term, total, probed, gen, ...options.retain ? { cache } : {} };
2210
- else if ((res ?? 0) < 0)
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 offset = infer && props ? (props.expr ? 0 : 3) + (props.dup ? 1 : 0) + (props.proper ? 0 : 1) : 0;
2213
- if (!cache[gen + offset])
2214
- cache[gen + offset] = [];
2215
- cache[gen + offset].push(term);
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
- return { total, probed, gen: depth, ...options.retain ? { cache } : {} };
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";