@dallaylaen/ski-interpreter 2.8.2 → 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 +25 -0
- package/bin/ski.js +38 -15
- package/example/collatz.ski +1 -1
- package/example/find-monobase.js +108 -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 +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ 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
|
+
|
|
8
33
|
## [2.8.2] - 2026-04-26
|
|
9
34
|
|
|
10
35
|
### 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
|
}
|
package/example/collatz.ski
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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";
|