@dallaylaen/ski-interpreter 2.8.2 → 2.9.1

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,37 @@ 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.1] - 2026-06-28
9
+
10
+ ### Fixed
11
+
12
+ * Empty terms allowed in expressions and lambdas, leading to '' passing the I quest (#30).
13
+
14
+ ## [2.9.0] - 2026-06-27
15
+
16
+ ### BREAKING CHANGES
17
+
18
+ * SKI.extras.search is now a generator, yielding progress ticks as well as found terms, which may now be multiple.
19
+ * Quest page now requires { chapters: [...] } instead of [...] in index.json.
20
+
21
+ ### Added
22
+
23
+ * Examples: `example/find-monobase.js` to showcase nested search() usage.
24
+ * Quest page: "New!" badge for quests newer than 30 days.
25
+ * Quest page: Reordered for clearer narrative, more quests added.
26
+ * `bin/ski.js search` diplays elapsed time.
27
+ * `bin/ski.js -q` flag to suppress extra output.
28
+ * `bin/ski.js file -` will process STDIN
29
+ * Added a self-hosted [combinator cheat-sheet page](https://dallaylaen.github.io/ski-interpreter/combinator-birds.html) (#29).
30
+
31
+ ### Changed
32
+ * `bin/ski.js search` output is now parseable (metadata commented out)
33
+ * Better Collatz conjecture in examples (thx @Darkwing3125) fixes #28
34
+
35
+ ### Fixed
36
+ * Solved quests weren't displayed as solved
37
+ * History button svg (thx @akuklev)
38
+
8
39
  ## [2.8.2] - 2026-04-26
9
40
 
10
41
  ### 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
  }
@@ -11,7 +11,7 @@ V=BCT;
11
11
  calc=BBB(T 0)(V(CB+));
12
12
  if=BBB(B(BW)C)C;
13
13
  half=C(V(C(V(T(KI))(C(B+)K)))(K 0))K;
14
- collatz=if(VCK)half(B(V+ 1)(B 3));
14
+ collatz=C(W(C(CI(B(CB+)C))0))0; // courtesy of @Darkwing3125
15
15
  dec=BBBCV(C(BT)+)(K 0)I;
16
16
  M=WI;
17
17
  wait4=BBB(BCC)(BCC);
@@ -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
+ }
@@ -1308,7 +1308,7 @@ var PartialLambda = class _PartialLambda extends Empty {
1308
1308
  return this;
1309
1309
  }
1310
1310
  postParse() {
1311
- let expr = this.impl;
1311
+ let expr = postParse(this.impl);
1312
1312
  for (let i = this.terms.length; i-- > 0; )
1313
1313
  expr = new Lambda(this.terms[i], expr);
1314
1314
  return expr;
@@ -1319,7 +1319,7 @@ var PartialLambda = class _PartialLambda extends Empty {
1319
1319
  } */
1320
1320
  };
1321
1321
  function postParse(expr) {
1322
- return expr.postParse ? expr.postParse() : expr;
1322
+ return expr instanceof Empty ? expr.postParse() : expr;
1323
1323
  }
1324
1324
  var combChars = new Tokenizer(
1325
1325
  "[()]",
@@ -1591,7 +1591,7 @@ var Parser = class {
1591
1591
  parser: this
1592
1592
  };
1593
1593
  }
1594
- return expr;
1594
+ return postParse(expr);
1595
1595
  }
1596
1596
  /**
1597
1597
  * Parse a single line of source code, without splitting it into declarations.
@@ -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";