@dallaylaen/ski-interpreter 2.8.0 → 2.8.2

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,28 @@ 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.8.2] - 2026-04-26
9
+
10
+ ### Added
11
+
12
+ - `examples/` are now included in the package
13
+
14
+ ### Fixed
15
+
16
+ - removed stale .d.ts file from lib/
17
+
18
+ ## [2.8.1] - 2026-04-26
19
+
20
+ ### Added
21
+
22
+ - `SKI.extras.equiv(expr1, expr2, options: InferOptions?)` to check if two expressions are computationally equivalent.
23
+ - `./bin/ski.js compare <expr1> <expr2>` command to compare two expressions for equivalence.
24
+
25
+ ### Changed
26
+
27
+ - `Expr.infer()` keeps adding variables to nonterminating expressions
28
+ to catch e.g. `CK(WWW)` (never terminates but eqivalent to I).
29
+
8
30
  ## [2.8.0] - 2026-04-23
9
31
 
10
32
  ### BREAKING CHANGES
package/bin/ski.js CHANGED
@@ -71,6 +71,23 @@ program
71
71
  inferExpression(expression);
72
72
  });
73
73
 
74
+ program
75
+ .command('compare <expr1> <expr2>')
76
+ .description('Check if two expressions are equivalent')
77
+ .action((expr1, expr2) => {
78
+ const ski = new SKI();
79
+ const e1 = ski.parse(expr1);
80
+ const e2 = ski.parse(expr2);
81
+ const res = SKI.extras.equiv(e1, e2, runOptions);
82
+ if (res.equal)
83
+ console.log('Both expressions are equivalent to ' + res.canonical[0].format(format));
84
+ else
85
+ console.log(`Expressions differ:\n${res.canonical[0].format(format)}\n vs \n${res.canonical[1].format(format)}`);
86
+
87
+ console.log(`// ${res.steps} step(s)`);
88
+ process.exit(res.equal ? 0 : 1);
89
+ });
90
+
74
91
  // Extract subcommand
75
92
  program
76
93
  .command('extract <target> <terms...>')
@@ -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=if(VCK)half(B(V+ 1)(B 3));
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,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
@@ -357,11 +357,9 @@ var Expr = class _Expr {
357
357
  let steps = 0;
358
358
  let expr = this;
359
359
  main: for (let i = 0; i < options.maxArgs; i++) {
360
- const next = expr.run({ max: options.max - steps, maxSize: options.maxSize });
360
+ const next = expr.run({ max: Math.max((options.max - steps) / 2, 10), maxSize: options.maxSize });
361
361
  steps += next.steps;
362
- if (!next.final)
363
- break;
364
- if (firstVar(next.expr)) {
362
+ if (next.final && firstVar(next.expr)) {
365
363
  expr = next.expr;
366
364
  if (!expr.any((e) => !(e instanceof FreeVar || e instanceof App)))
367
365
  return maybeLambda(probe, expr, { steps });
@@ -2087,6 +2085,77 @@ function canonize(term, options = {}) {
2087
2085
  }
2088
2086
 
2089
2087
  // src/extras.ts
2088
+ var formatSchema = {
2089
+ html: (x) => typeof x === "boolean" ? void 0 : "must be a boolean",
2090
+ terse: (x) => typeof x === "boolean" ? void 0 : "must be a boolean",
2091
+ space: (x) => typeof x === "string" ? void 0 : "must be a string",
2092
+ brackets: isStringPair,
2093
+ var: isStringPair,
2094
+ around: isStringPair,
2095
+ redex: isStringPair,
2096
+ lambda: isStringTriple,
2097
+ inventory: (x) => {
2098
+ if (typeof x !== "object" || x === null || x.constructor !== Object)
2099
+ return "must be an object, not " + (x?.constructor?.name ?? typeof x);
2100
+ const refined = x;
2101
+ for (const key of Object.keys(refined)) {
2102
+ if (!(refined[key] instanceof Expr))
2103
+ return "key " + key + "is not an Expr";
2104
+ }
2105
+ return void 0;
2106
+ }
2107
+ };
2108
+ function checkFormatOptions(raw) {
2109
+ if (raw === null || raw === void 0)
2110
+ return { value: {} };
2111
+ if (typeof raw !== "object" || Array.isArray(raw) || raw.constructor !== Object)
2112
+ return { error: { object: "Format options must be an object, not " + (raw?.constructor?.name ?? typeof raw) } };
2113
+ const rec = raw;
2114
+ const error = {};
2115
+ for (const key in rec) {
2116
+ if (formatSchema[key]) {
2117
+ const err = formatSchema[key](rec[key]);
2118
+ if (err)
2119
+ error[key] = err;
2120
+ } else
2121
+ error[key] = "unknown option";
2122
+ }
2123
+ return Object.keys(error).length > 0 ? { error } : { value: rec };
2124
+ }
2125
+ function equiv(e1, e2, options = {}) {
2126
+ let steps = 0;
2127
+ const [n1, n2] = [e1, e2].map((x) => x.traverse((e) => {
2128
+ const props = e.infer(options);
2129
+ steps += props.steps ?? 0;
2130
+ return props.expr;
2131
+ }));
2132
+ const normal = !!(n1 && n2);
2133
+ return {
2134
+ steps,
2135
+ normal,
2136
+ equal: normal ? n1.equals(n2) : false,
2137
+ canonical: [n1, n2]
2138
+ };
2139
+ }
2140
+ function declare(expr, env) {
2141
+ return expr.declare({ inventory: env });
2142
+ }
2143
+ function deepFormat(obj, options = {}) {
2144
+ if (obj instanceof Expr)
2145
+ return obj.format(options);
2146
+ if (obj instanceof Quest)
2147
+ return "Quest(" + obj.name + ")";
2148
+ if (obj instanceof Quest.Case)
2149
+ return "Quest.Case";
2150
+ if (Array.isArray(obj))
2151
+ return obj.map((item) => deepFormat(item, options));
2152
+ if (typeof obj !== "object" || obj === null || obj.constructor !== Object)
2153
+ return obj;
2154
+ const out = {};
2155
+ for (const key in obj)
2156
+ out[key] = deepFormat(obj[key], options);
2157
+ return out;
2158
+ }
2090
2159
  function search(seed, options, predicate) {
2091
2160
  const {
2092
2161
  depth = 16,
@@ -2150,65 +2219,13 @@ function search(seed, options, predicate) {
2150
2219
  }
2151
2220
  return { total, probed, gen: depth, ...options.retain ? { cache } : {} };
2152
2221
  }
2153
- function deepFormat(obj, options = {}) {
2154
- if (obj instanceof Expr)
2155
- return obj.format(options);
2156
- if (obj instanceof Quest)
2157
- return "Quest(" + obj.name + ")";
2158
- if (obj instanceof Quest.Case)
2159
- return "Quest.Case";
2160
- if (Array.isArray(obj))
2161
- return obj.map((item) => deepFormat(item, options));
2162
- if (typeof obj !== "object" || obj === null || obj.constructor !== Object)
2163
- return obj;
2164
- const out = {};
2165
- for (const key in obj)
2166
- out[key] = deepFormat(obj[key], options);
2167
- return out;
2168
- }
2169
- function declare(expr, env) {
2170
- return expr.declare({ inventory: env });
2222
+ function isStringPair(x) {
2223
+ return Array.isArray(x) && x.length === 2 && typeof x[0] === "string" && typeof x[1] === "string" ? void 0 : "must be a pair of strings";
2171
2224
  }
2172
- var isStringPair = (x) => Array.isArray(x) && x.length === 2 && typeof x[0] === "string" && typeof x[1] === "string" ? void 0 : "must be a pair of strings";
2173
- var isStringTriple = (x) => Array.isArray(x) && x.length === 3 && typeof x[0] === "string" && typeof x[1] === "string" && typeof x[2] === "string" ? void 0 : "must be a triplet of strings";
2174
- var schema = {
2175
- html: (x) => typeof x === "boolean" ? void 0 : "must be a boolean",
2176
- terse: (x) => typeof x === "boolean" ? void 0 : "must be a boolean",
2177
- space: (x) => typeof x === "string" ? void 0 : "must be a string",
2178
- brackets: isStringPair,
2179
- var: isStringPair,
2180
- around: isStringPair,
2181
- redex: isStringPair,
2182
- lambda: isStringTriple,
2183
- inventory: (x) => {
2184
- if (typeof x !== "object" || x === null || x.constructor !== Object)
2185
- return "must be an object, not " + (x?.constructor?.name ?? typeof x);
2186
- const refined = x;
2187
- for (const key of Object.keys(refined)) {
2188
- if (!(refined[key] instanceof Expr))
2189
- return "key " + key + "is not an Expr";
2190
- }
2191
- return void 0;
2192
- }
2193
- };
2194
- function checkFormatOptions(raw) {
2195
- if (raw === null || raw === void 0)
2196
- return { value: {} };
2197
- if (typeof raw !== "object" || Array.isArray(raw) || raw.constructor !== Object)
2198
- return { error: { object: "Format options must be an object, not " + (raw?.constructor?.name ?? typeof raw) } };
2199
- const rec = raw;
2200
- const error = {};
2201
- for (const key in rec) {
2202
- if (schema[key]) {
2203
- const err = schema[key](rec[key]);
2204
- if (err)
2205
- error[key] = err;
2206
- } else
2207
- error[key] = "unknown option";
2208
- }
2209
- return Object.keys(error).length > 0 ? { error } : { value: rec };
2225
+ function isStringTriple(x) {
2226
+ return Array.isArray(x) && x.length === 3 && typeof x[0] === "string" && typeof x[1] === "string" && typeof x[2] === "string" ? void 0 : "must be a triplet of strings";
2210
2227
  }
2211
- var extras = { search, deepFormat, declare, toposort, checkFormatOptions };
2228
+ var extras = { search, deepFormat, declare, toposort, checkFormatOptions, equiv };
2212
2229
 
2213
2230
  // src/index.ts
2214
2231
  extras.toposort = toposort;