@dallaylaen/ski-interpreter 2.4.0 → 2.5.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 +38 -0
- package/README.md +4 -2
- package/bin/ski.js +128 -37
- package/lib/ski-interpreter.cjs.js +13876 -53
- package/lib/ski-interpreter.cjs.js.map +4 -4
- package/lib/ski-interpreter.esm.js +13882 -53
- package/lib/ski-interpreter.esm.js.map +4 -4
- package/lib/ski-interpreter.min.js +42 -3
- package/lib/ski-interpreter.min.js.map +4 -4
- package/lib/ski-quest.min.js +42 -3
- package/lib/ski-quest.min.js.map +4 -4
- package/lib/types/expr.d.ts +27 -19
- package/lib/types/index.d.ts +17 -4
- package/lib/types/internal.d.ts +7 -7
- package/lib/types/parser.d.ts +2 -0
- package/lib/types/quest.d.ts +1 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ 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.5.0] - 2026-03-15
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
- `affine: true` in quests now means "has no duplicating subterms" rather than "non-duplicating as a whole."
|
|
13
|
+
Aliases are exempted from this check, so a solution `SK`
|
|
14
|
+
will not pass as S is duplicating, but `false=SK; false`
|
|
15
|
+
will because `false` (as a whole term) _is_ affine.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `SKI.schemas.FormatOptions`: zod schema for validating format options.
|
|
20
|
+
- `bin/ski.js`: global `--format` option.
|
|
21
|
+
- `bin/ski.js`: `!format` command in the REPL.
|
|
22
|
+
- `bin/ski.js`: `--verbose` is now a global flag with a toggle in the REPL.
|
|
23
|
+
- `bin/ski.js`: `--max`, `--max-size`, and `--max-args` arguments.
|
|
24
|
+
- `bin/ski.js search`: `--max-depth` and `--max-tries` options.
|
|
25
|
+
- New quest: affine extract from list.
|
|
26
|
+
- `zod` added as a dependency.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `infer()` now adheres to `maxSize`.
|
|
31
|
+
- Avoid adding context to parsed saved terms.
|
|
32
|
+
- `TraverseValue` signature now includes `void` so that `return undefined` in `traverse`/`fold` is no longer necessary.
|
|
33
|
+
|
|
34
|
+
## [2.4.1] - 2026-03-06
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- expr.run({maxSize: number ?? 1_000_000}) to prevent runaway reductions (fixes #14).
|
|
39
|
+
- `ski.js infer <expression>` command to infer properties of an expression.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- quests use shorter Y combinator form internally (#13)
|
|
44
|
+
- history in the playground is scrolled to the bottom.
|
|
45
|
+
|
|
8
46
|
## [2.4.0] - 2026-03-06
|
|
9
47
|
|
|
10
48
|
### BREAKING CHANGES
|
package/README.md
CHANGED
|
@@ -99,12 +99,14 @@ npm install @dallaylaen/ski-interpreter
|
|
|
99
99
|
* `--verbose` - Show all evaluation steps
|
|
100
100
|
* Example: `ski file script.ski`
|
|
101
101
|
|
|
102
|
+
* **`infer <expression>`** - try to find equivalent lambda expression and display its properties if found.
|
|
103
|
+
|
|
102
104
|
* **`extract <expression> <known term> ...`** -
|
|
103
105
|
Replace parts of the expression that are equivalent to the known terms with the respective terms. Known terms must be normalizable.
|
|
104
106
|
|
|
105
107
|
* **`search <expression> <known term> ...`** -
|
|
106
108
|
Attempt to brute force an equivalent of the _expression_ using only the _known terms_.
|
|
107
|
-
Only
|
|
109
|
+
Only normalizable terms are currently supported.
|
|
108
110
|
|
|
109
111
|
* **`quest-lint <files...>`** - Validate quest definition files
|
|
110
112
|
* `--solution <file>` - Load solutions from a JSON file for verification
|
|
@@ -150,7 +152,7 @@ const final = expr.run({max: 1000}); // { steps: 42, expr: '...' }
|
|
|
150
152
|
const iterator = expr.walk();
|
|
151
153
|
|
|
152
154
|
// applying expressions
|
|
153
|
-
const result = expr.run({max: 1000}, arg1, arg2 ...);
|
|
155
|
+
const result = expr.run({max: 1000}, arg1, arg2, ...);
|
|
154
156
|
// same sa
|
|
155
157
|
expr.apply(arg1).apply(arg2).run();
|
|
156
158
|
// or simply
|
package/bin/ski.js
CHANGED
|
@@ -7,46 +7,67 @@ const { SKI } = require('../lib/ski-interpreter.cjs');
|
|
|
7
7
|
const { Quest } = SKI;
|
|
8
8
|
const { version } = require('../package.json');
|
|
9
9
|
|
|
10
|
+
const runOptions = {};
|
|
11
|
+
let format = {};
|
|
12
|
+
let verbose = false;
|
|
13
|
+
|
|
10
14
|
const program = new Command();
|
|
11
15
|
|
|
12
16
|
program
|
|
13
17
|
.name('ski')
|
|
14
18
|
.description('Simple Kombinator Interpreter - a combinatory logic & lambda calculus parser and interpreter')
|
|
15
|
-
.version(version)
|
|
19
|
+
.version(version)
|
|
20
|
+
.option('-v, --verbose', 'Show all evaluation steps', () => { verbose = true; })
|
|
21
|
+
.option('--format <json>', 'Format for output expressions', setFormat)
|
|
22
|
+
.option('--max <number>', 'Limit computation steps', raw => {
|
|
23
|
+
const n = Number.parseInt(raw);
|
|
24
|
+
if (Number.isNaN(n) || n <= 0)
|
|
25
|
+
throw new Error('--max requires a positive integer');
|
|
26
|
+
runOptions.max = n;
|
|
27
|
+
})
|
|
28
|
+
.option('--max-size <number>', 'Limit expression\'s total footprint during computations', raw => {
|
|
29
|
+
const n = Number.parseInt(raw);
|
|
30
|
+
if (Number.isNaN(n) || n <= 0)
|
|
31
|
+
throw new Error('--max-size requires a positive integer');
|
|
32
|
+
runOptions.maxSize = n;
|
|
33
|
+
})
|
|
34
|
+
.option('--max-args <number>', 'Limit probed arguments when inferring terms', raw => {
|
|
35
|
+
const n = Number.parseInt(raw);
|
|
36
|
+
if (Number.isNaN(n) || n <= 0)
|
|
37
|
+
throw new Error('--max-args requires a positive integer');
|
|
38
|
+
runOptions.maxArgs = n;
|
|
39
|
+
});
|
|
16
40
|
|
|
17
41
|
// REPL subcommand
|
|
18
42
|
program
|
|
19
43
|
.command('repl')
|
|
20
44
|
.description('Start interactive REPL')
|
|
21
|
-
.option('--verbose', 'Show all evaluation steps')
|
|
22
45
|
.action((options) => {
|
|
23
|
-
startRepl(options
|
|
46
|
+
startRepl(options);
|
|
24
47
|
});
|
|
25
48
|
|
|
26
49
|
// Eval subcommand
|
|
27
50
|
program
|
|
28
51
|
.command('eval <expression>')
|
|
29
52
|
.description('Evaluate a single expression')
|
|
30
|
-
.option('--verbose', 'Show all evaluation steps')
|
|
31
53
|
.action((expression, options) => {
|
|
32
|
-
evaluateExpression(expression, options
|
|
54
|
+
evaluateExpression(expression, options);
|
|
33
55
|
});
|
|
34
56
|
|
|
35
57
|
// File subcommand
|
|
36
58
|
program
|
|
37
59
|
.command('file <filepath>')
|
|
38
60
|
.description('Evaluate expressions from a file')
|
|
39
|
-
.option('--verbose', 'Show all evaluation steps')
|
|
40
61
|
.action((filepath, options) => {
|
|
41
|
-
evaluateFile(filepath
|
|
62
|
+
evaluateFile(filepath);
|
|
42
63
|
});
|
|
43
64
|
|
|
44
|
-
//
|
|
65
|
+
// Infer subcommand
|
|
45
66
|
program
|
|
46
|
-
.command('
|
|
47
|
-
.description('
|
|
48
|
-
.action((
|
|
49
|
-
|
|
67
|
+
.command('infer <expression>')
|
|
68
|
+
.description('Find a canonical form of the expression and its properties')
|
|
69
|
+
.action((expression) => {
|
|
70
|
+
inferExpression(expression);
|
|
50
71
|
});
|
|
51
72
|
|
|
52
73
|
// Extract subcommand
|
|
@@ -57,6 +78,14 @@ program
|
|
|
57
78
|
extractExpression(target, terms);
|
|
58
79
|
});
|
|
59
80
|
|
|
81
|
+
// Search subcommand
|
|
82
|
+
program
|
|
83
|
+
.command('search <target> <terms...>')
|
|
84
|
+
.description('Search for an expression equivalent to target using known terms')
|
|
85
|
+
.option('--max-depth <number>', 'Limit search depth', toInt('--max-depth'))
|
|
86
|
+
.option('--max-tries <number>', 'Limit total terms probed', toInt('--max-tries'))
|
|
87
|
+
.action(searchExpression);
|
|
88
|
+
|
|
60
89
|
// Quest-check subcommand
|
|
61
90
|
program
|
|
62
91
|
.command('quest-lint <files...>')
|
|
@@ -72,9 +101,9 @@ program
|
|
|
72
101
|
.parse(process.argv);
|
|
73
102
|
|
|
74
103
|
if (!process.argv.slice(2).length)
|
|
75
|
-
startRepl(
|
|
104
|
+
startRepl();
|
|
76
105
|
|
|
77
|
-
function startRepl (
|
|
106
|
+
function startRepl () {
|
|
78
107
|
const readline = require('readline');
|
|
79
108
|
const ski = new SKI();
|
|
80
109
|
|
|
@@ -92,7 +121,7 @@ function startRepl (verbose) {
|
|
|
92
121
|
if (str.startsWith('!'))
|
|
93
122
|
handleCommand(str, ski);
|
|
94
123
|
else {
|
|
95
|
-
processLine(str, ski,
|
|
124
|
+
processLine(str, ski, err => {
|
|
96
125
|
console.log('' + err);
|
|
97
126
|
});
|
|
98
127
|
}
|
|
@@ -108,19 +137,19 @@ function startRepl (verbose) {
|
|
|
108
137
|
rl.prompt();
|
|
109
138
|
}
|
|
110
139
|
|
|
111
|
-
function evaluateExpression (expression
|
|
140
|
+
function evaluateExpression (expression) {
|
|
112
141
|
const ski = new SKI();
|
|
113
|
-
processLine(expression, ski,
|
|
142
|
+
processLine(expression, ski, err => {
|
|
114
143
|
console.error('' + err);
|
|
115
144
|
process.exit(3);
|
|
116
145
|
});
|
|
117
146
|
}
|
|
118
147
|
|
|
119
|
-
function evaluateFile (filepath
|
|
148
|
+
function evaluateFile (filepath) {
|
|
120
149
|
const ski = new SKI();
|
|
121
150
|
fs.readFile(filepath, 'utf8')
|
|
122
151
|
.then(source => {
|
|
123
|
-
processLine(source, ski,
|
|
152
|
+
processLine(source, ski, err => {
|
|
124
153
|
console.error('' + err);
|
|
125
154
|
process.exit(3);
|
|
126
155
|
});
|
|
@@ -131,7 +160,7 @@ function evaluateFile (filepath, verbose) {
|
|
|
131
160
|
});
|
|
132
161
|
}
|
|
133
162
|
|
|
134
|
-
function processLine (source, ski,
|
|
163
|
+
function processLine (source, ski, onErr) {
|
|
135
164
|
if (!source.match(/\S/))
|
|
136
165
|
return; // nothing to see here
|
|
137
166
|
|
|
@@ -141,12 +170,12 @@ function processLine (source, ski, verbose, onErr) {
|
|
|
141
170
|
const isAlias = expr instanceof SKI.classes.Alias;
|
|
142
171
|
const aliasName = isAlias ? expr.name : null;
|
|
143
172
|
|
|
144
|
-
for (const state of expr.walk()) {
|
|
173
|
+
for (const state of expr.walk(runOptions)) {
|
|
145
174
|
if (state.final)
|
|
146
175
|
console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
|
|
147
176
|
|
|
148
177
|
if (verbose || state.final)
|
|
149
|
-
console.log(
|
|
178
|
+
console.log(state.expr.format(format));
|
|
150
179
|
|
|
151
180
|
if (state.final && isAlias && aliasName)
|
|
152
181
|
ski.add(aliasName, state.expr);
|
|
@@ -156,6 +185,43 @@ function processLine (source, ski, verbose, onErr) {
|
|
|
156
185
|
}
|
|
157
186
|
}
|
|
158
187
|
|
|
188
|
+
function inferExpression (expression) {
|
|
189
|
+
const ski = new SKI();
|
|
190
|
+
|
|
191
|
+
const expr = ski.parse(expression);
|
|
192
|
+
const guess = expr.infer(runOptions);
|
|
193
|
+
|
|
194
|
+
if (guess.normal) {
|
|
195
|
+
displayInfer(guess);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// hard case...
|
|
199
|
+
let steps = guess.steps;
|
|
200
|
+
const canon = expr.traverse(e => {
|
|
201
|
+
if (e === expr)
|
|
202
|
+
return; // already tried
|
|
203
|
+
const g = e.infer(runOptions);
|
|
204
|
+
steps += g.steps;
|
|
205
|
+
return g.expr;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
displayInfer({ expr: canon, steps, normal: false, proper: false });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
*
|
|
213
|
+
* @param {TermInfo} guess
|
|
214
|
+
*/
|
|
215
|
+
function displayInfer (guess) {
|
|
216
|
+
if (guess.expr)
|
|
217
|
+
console.log(guess.expr.format(format));
|
|
218
|
+
|
|
219
|
+
for (const key of ['normal', 'proper', 'arity', 'discard', 'duplicate', 'steps']) {
|
|
220
|
+
if (guess[key] !== undefined)
|
|
221
|
+
console.log(`// ${key}: ${guess[key]}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
159
225
|
async function questCheck (files, solutionFile) {
|
|
160
226
|
try {
|
|
161
227
|
// Load solutions if provided
|
|
@@ -220,11 +286,10 @@ async function questCheck (files, solutionFile) {
|
|
|
220
286
|
}
|
|
221
287
|
}
|
|
222
288
|
|
|
223
|
-
function searchExpression (targetStr, termStrs) {
|
|
289
|
+
function searchExpression (targetStr, termStrs, options) {
|
|
224
290
|
const ski = new SKI();
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
const seed = termStrs.map(s => ski.parse(s, { vars: jar }));
|
|
291
|
+
const target = ski.parse(targetStr);
|
|
292
|
+
const seed = termStrs.map(s => ski.parse(s));
|
|
228
293
|
|
|
229
294
|
const { expr } = target.infer();
|
|
230
295
|
if (!expr) {
|
|
@@ -232,7 +297,7 @@ function searchExpression (targetStr, termStrs) {
|
|
|
232
297
|
process.exit(1);
|
|
233
298
|
}
|
|
234
299
|
|
|
235
|
-
const res = SKI.extras.search(seed, { tries:
|
|
300
|
+
const res = SKI.extras.search(seed, { tries: options.maxTries, depth: options.maxDepth }, (e, p) => {
|
|
236
301
|
if (!p.expr)
|
|
237
302
|
return -1;
|
|
238
303
|
if (p.expr.equals(expr))
|
|
@@ -241,7 +306,7 @@ function searchExpression (targetStr, termStrs) {
|
|
|
241
306
|
});
|
|
242
307
|
|
|
243
308
|
if (res.expr) {
|
|
244
|
-
console.log(`Found ${res.expr} after ${res.total} tries.`);
|
|
309
|
+
console.log(`Found ${res.expr.format(format)} after ${res.total} tries.`);
|
|
245
310
|
process.exit(0);
|
|
246
311
|
} else {
|
|
247
312
|
console.error(`No equivalent expression found for ${target} after ${res.total} tries.`);
|
|
@@ -254,7 +319,7 @@ function extractExpression (targetStr, termStrs) {
|
|
|
254
319
|
const expr = ski.parse(targetStr);
|
|
255
320
|
const pairs = termStrs
|
|
256
321
|
.map(s => ski.parse(s))
|
|
257
|
-
.map(e => [e.infer().expr, e]);
|
|
322
|
+
.map(e => [e.infer(runOptions).expr, e]);
|
|
258
323
|
|
|
259
324
|
const uncanonical = pairs.filter(pair => !pair[0]);
|
|
260
325
|
if (uncanonical.length) {
|
|
@@ -264,25 +329,22 @@ function extractExpression (targetStr, termStrs) {
|
|
|
264
329
|
}
|
|
265
330
|
|
|
266
331
|
const replaced = expr.traverse(e => {
|
|
267
|
-
const canon = e.infer().expr;
|
|
332
|
+
const canon = e.infer(runOptions).expr;
|
|
268
333
|
if (canon) {
|
|
269
334
|
for (const [lambda, term] of pairs) {
|
|
270
335
|
if (canon.equals(lambda))
|
|
271
336
|
return term;
|
|
272
337
|
}
|
|
273
338
|
}
|
|
274
|
-
return null;
|
|
275
339
|
});
|
|
276
340
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
else
|
|
341
|
+
console.log((replaced ?? expr).format(format));
|
|
342
|
+
if (!replaced)
|
|
280
343
|
console.log('// unchanged');
|
|
281
344
|
}
|
|
282
345
|
|
|
283
346
|
function handleCommand (input, ski) {
|
|
284
|
-
const
|
|
285
|
-
const cmd = parts[0];
|
|
347
|
+
const [_, cmd, arg] = input.match(/^\s*(\S+)(?:\s+(.*\S))?\s*$/);
|
|
286
348
|
|
|
287
349
|
const dispatch = {
|
|
288
350
|
'!ls': () => {
|
|
@@ -296,9 +358,21 @@ function handleCommand (input, ski) {
|
|
|
296
358
|
console.log(` ${name} ${term.props?.expr ?? '(native)'}`);
|
|
297
359
|
}
|
|
298
360
|
},
|
|
361
|
+
'!verbose': flag => {
|
|
362
|
+
verbose = !(['off', 'false', '0', '-'].includes(flag));
|
|
363
|
+
console.log('// verbose is ' + (verbose ? 'on' : 'off'));
|
|
364
|
+
},
|
|
365
|
+
'!format': options => {
|
|
366
|
+
if (options)
|
|
367
|
+
setFormat(options);
|
|
368
|
+
else
|
|
369
|
+
console.log('Format: ' + JSON.stringify(format, null, 2));
|
|
370
|
+
},
|
|
299
371
|
'!help': () => {
|
|
300
372
|
console.log('Available commands:');
|
|
301
373
|
console.log(' !ls - List term inventory');
|
|
374
|
+
console.log(' !verbose [on|off] - Toggle verbose mode (show all evaluation steps)');
|
|
375
|
+
console.log(' !format [json] - Set or show output format options');
|
|
302
376
|
console.log(' !help - Show this help message');
|
|
303
377
|
},
|
|
304
378
|
'': () => {
|
|
@@ -307,5 +381,22 @@ function handleCommand (input, ski) {
|
|
|
307
381
|
}
|
|
308
382
|
};
|
|
309
383
|
|
|
310
|
-
|
|
384
|
+
try {
|
|
385
|
+
(dispatch[cmd] || dispatch[''])(arg)
|
|
386
|
+
} catch (err) {
|
|
387
|
+
console.error(`Error executing command ${cmd}:`, err.message);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function setFormat (options) {
|
|
392
|
+
format = SKI.schemas.FormatOptions.parse(JSON.parse(options));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function toInt (comment) {
|
|
396
|
+
return function (str) {
|
|
397
|
+
const n = Number.parseInt(str);
|
|
398
|
+
if (Number.isNaN(n) || n <= 0)
|
|
399
|
+
throw new Error(comment + ' requires positive integer');
|
|
400
|
+
return n;
|
|
401
|
+
}
|
|
311
402
|
}
|