@dallaylaen/ski-interpreter 2.4.1 → 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 +26 -0
- package/bin/ski.js +86 -38
- package/lib/ski-interpreter.cjs.js +13863 -50
- package/lib/ski-interpreter.cjs.js.map +4 -4
- package/lib/ski-interpreter.esm.js +13869 -50
- 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 +23 -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,32 @@ 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
|
+
|
|
8
34
|
## [2.4.1] - 2026-03-06
|
|
9
35
|
|
|
10
36
|
### Added
|
package/bin/ski.js
CHANGED
|
@@ -7,38 +7,59 @@ 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
|
|
@@ -61,9 +82,9 @@ program
|
|
|
61
82
|
program
|
|
62
83
|
.command('search <target> <terms...>')
|
|
63
84
|
.description('Search for an expression equivalent to target using known terms')
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
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);
|
|
67
88
|
|
|
68
89
|
// Quest-check subcommand
|
|
69
90
|
program
|
|
@@ -80,9 +101,9 @@ program
|
|
|
80
101
|
.parse(process.argv);
|
|
81
102
|
|
|
82
103
|
if (!process.argv.slice(2).length)
|
|
83
|
-
startRepl(
|
|
104
|
+
startRepl();
|
|
84
105
|
|
|
85
|
-
function startRepl (
|
|
106
|
+
function startRepl () {
|
|
86
107
|
const readline = require('readline');
|
|
87
108
|
const ski = new SKI();
|
|
88
109
|
|
|
@@ -100,7 +121,7 @@ function startRepl (verbose) {
|
|
|
100
121
|
if (str.startsWith('!'))
|
|
101
122
|
handleCommand(str, ski);
|
|
102
123
|
else {
|
|
103
|
-
processLine(str, ski,
|
|
124
|
+
processLine(str, ski, err => {
|
|
104
125
|
console.log('' + err);
|
|
105
126
|
});
|
|
106
127
|
}
|
|
@@ -116,19 +137,19 @@ function startRepl (verbose) {
|
|
|
116
137
|
rl.prompt();
|
|
117
138
|
}
|
|
118
139
|
|
|
119
|
-
function evaluateExpression (expression
|
|
140
|
+
function evaluateExpression (expression) {
|
|
120
141
|
const ski = new SKI();
|
|
121
|
-
processLine(expression, ski,
|
|
142
|
+
processLine(expression, ski, err => {
|
|
122
143
|
console.error('' + err);
|
|
123
144
|
process.exit(3);
|
|
124
145
|
});
|
|
125
146
|
}
|
|
126
147
|
|
|
127
|
-
function evaluateFile (filepath
|
|
148
|
+
function evaluateFile (filepath) {
|
|
128
149
|
const ski = new SKI();
|
|
129
150
|
fs.readFile(filepath, 'utf8')
|
|
130
151
|
.then(source => {
|
|
131
|
-
processLine(source, ski,
|
|
152
|
+
processLine(source, ski, err => {
|
|
132
153
|
console.error('' + err);
|
|
133
154
|
process.exit(3);
|
|
134
155
|
});
|
|
@@ -139,7 +160,7 @@ function evaluateFile (filepath, verbose) {
|
|
|
139
160
|
});
|
|
140
161
|
}
|
|
141
162
|
|
|
142
|
-
function processLine (source, ski,
|
|
163
|
+
function processLine (source, ski, onErr) {
|
|
143
164
|
if (!source.match(/\S/))
|
|
144
165
|
return; // nothing to see here
|
|
145
166
|
|
|
@@ -149,12 +170,12 @@ function processLine (source, ski, verbose, onErr) {
|
|
|
149
170
|
const isAlias = expr instanceof SKI.classes.Alias;
|
|
150
171
|
const aliasName = isAlias ? expr.name : null;
|
|
151
172
|
|
|
152
|
-
for (const state of expr.walk()) {
|
|
173
|
+
for (const state of expr.walk(runOptions)) {
|
|
153
174
|
if (state.final)
|
|
154
175
|
console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
|
|
155
176
|
|
|
156
177
|
if (verbose || state.final)
|
|
157
|
-
console.log(
|
|
178
|
+
console.log(state.expr.format(format));
|
|
158
179
|
|
|
159
180
|
if (state.final && isAlias && aliasName)
|
|
160
181
|
ski.add(aliasName, state.expr);
|
|
@@ -168,7 +189,7 @@ function inferExpression (expression) {
|
|
|
168
189
|
const ski = new SKI();
|
|
169
190
|
|
|
170
191
|
const expr = ski.parse(expression);
|
|
171
|
-
const guess = expr.infer();
|
|
192
|
+
const guess = expr.infer(runOptions);
|
|
172
193
|
|
|
173
194
|
if (guess.normal) {
|
|
174
195
|
displayInfer(guess);
|
|
@@ -177,7 +198,9 @@ function inferExpression (expression) {
|
|
|
177
198
|
// hard case...
|
|
178
199
|
let steps = guess.steps;
|
|
179
200
|
const canon = expr.traverse(e => {
|
|
180
|
-
|
|
201
|
+
if (e === expr)
|
|
202
|
+
return; // already tried
|
|
203
|
+
const g = e.infer(runOptions);
|
|
181
204
|
steps += g.steps;
|
|
182
205
|
return g.expr;
|
|
183
206
|
});
|
|
@@ -191,7 +214,7 @@ function inferExpression (expression) {
|
|
|
191
214
|
*/
|
|
192
215
|
function displayInfer (guess) {
|
|
193
216
|
if (guess.expr)
|
|
194
|
-
console.log(guess.expr.format());
|
|
217
|
+
console.log(guess.expr.format(format));
|
|
195
218
|
|
|
196
219
|
for (const key of ['normal', 'proper', 'arity', 'discard', 'duplicate', 'steps']) {
|
|
197
220
|
if (guess[key] !== undefined)
|
|
@@ -263,11 +286,10 @@ async function questCheck (files, solutionFile) {
|
|
|
263
286
|
}
|
|
264
287
|
}
|
|
265
288
|
|
|
266
|
-
function searchExpression (targetStr, termStrs) {
|
|
289
|
+
function searchExpression (targetStr, termStrs, options) {
|
|
267
290
|
const ski = new SKI();
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
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));
|
|
271
293
|
|
|
272
294
|
const { expr } = target.infer();
|
|
273
295
|
if (!expr) {
|
|
@@ -275,7 +297,7 @@ function searchExpression (targetStr, termStrs) {
|
|
|
275
297
|
process.exit(1);
|
|
276
298
|
}
|
|
277
299
|
|
|
278
|
-
const res = SKI.extras.search(seed, { tries:
|
|
300
|
+
const res = SKI.extras.search(seed, { tries: options.maxTries, depth: options.maxDepth }, (e, p) => {
|
|
279
301
|
if (!p.expr)
|
|
280
302
|
return -1;
|
|
281
303
|
if (p.expr.equals(expr))
|
|
@@ -284,7 +306,7 @@ function searchExpression (targetStr, termStrs) {
|
|
|
284
306
|
});
|
|
285
307
|
|
|
286
308
|
if (res.expr) {
|
|
287
|
-
console.log(`Found ${res.expr} after ${res.total} tries.`);
|
|
309
|
+
console.log(`Found ${res.expr.format(format)} after ${res.total} tries.`);
|
|
288
310
|
process.exit(0);
|
|
289
311
|
} else {
|
|
290
312
|
console.error(`No equivalent expression found for ${target} after ${res.total} tries.`);
|
|
@@ -297,7 +319,7 @@ function extractExpression (targetStr, termStrs) {
|
|
|
297
319
|
const expr = ski.parse(targetStr);
|
|
298
320
|
const pairs = termStrs
|
|
299
321
|
.map(s => ski.parse(s))
|
|
300
|
-
.map(e => [e.infer().expr, e]);
|
|
322
|
+
.map(e => [e.infer(runOptions).expr, e]);
|
|
301
323
|
|
|
302
324
|
const uncanonical = pairs.filter(pair => !pair[0]);
|
|
303
325
|
if (uncanonical.length) {
|
|
@@ -307,25 +329,22 @@ function extractExpression (targetStr, termStrs) {
|
|
|
307
329
|
}
|
|
308
330
|
|
|
309
331
|
const replaced = expr.traverse(e => {
|
|
310
|
-
const canon = e.infer().expr;
|
|
332
|
+
const canon = e.infer(runOptions).expr;
|
|
311
333
|
if (canon) {
|
|
312
334
|
for (const [lambda, term] of pairs) {
|
|
313
335
|
if (canon.equals(lambda))
|
|
314
336
|
return term;
|
|
315
337
|
}
|
|
316
338
|
}
|
|
317
|
-
return null;
|
|
318
339
|
});
|
|
319
340
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
else
|
|
341
|
+
console.log((replaced ?? expr).format(format));
|
|
342
|
+
if (!replaced)
|
|
323
343
|
console.log('// unchanged');
|
|
324
344
|
}
|
|
325
345
|
|
|
326
346
|
function handleCommand (input, ski) {
|
|
327
|
-
const
|
|
328
|
-
const cmd = parts[0];
|
|
347
|
+
const [_, cmd, arg] = input.match(/^\s*(\S+)(?:\s+(.*\S))?\s*$/);
|
|
329
348
|
|
|
330
349
|
const dispatch = {
|
|
331
350
|
'!ls': () => {
|
|
@@ -339,9 +358,21 @@ function handleCommand (input, ski) {
|
|
|
339
358
|
console.log(` ${name} ${term.props?.expr ?? '(native)'}`);
|
|
340
359
|
}
|
|
341
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
|
+
},
|
|
342
371
|
'!help': () => {
|
|
343
372
|
console.log('Available commands:');
|
|
344
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');
|
|
345
376
|
console.log(' !help - Show this help message');
|
|
346
377
|
},
|
|
347
378
|
'': () => {
|
|
@@ -350,5 +381,22 @@ function handleCommand (input, ski) {
|
|
|
350
381
|
}
|
|
351
382
|
};
|
|
352
383
|
|
|
353
|
-
|
|
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
|
+
}
|
|
354
402
|
}
|