@dallaylaen/ski-interpreter 2.2.1 → 2.3.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 +46 -0
- package/README.md +65 -11
- package/bin/ski.js +267 -80
- package/lib/ski-interpreter.cjs.js +374 -261
- package/lib/ski-interpreter.cjs.js.map +3 -3
- package/lib/ski-interpreter.esm.js +374 -261
- package/lib/ski-interpreter.esm.js.map +2 -2
- package/lib/ski-interpreter.min.js +3 -3
- package/lib/ski-interpreter.min.js.map +3 -3
- package/lib/ski-quest.min.js +3 -3
- package/lib/ski-quest.min.js.map +3 -3
- package/package.json +4 -1
- package/types/src/expr.d.ts +131 -118
- package/types/src/parser.d.ts +6 -2
- package/types/src/quest.d.ts +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ 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.3.1] - 2026-03-01
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Links in documentation.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `ski.js extract <expression> <term> ...` - rewrite the given expression to an equivalent using provided terms where possible.
|
|
15
|
+
- `ski.js search <expression> <term> ...` - brute-force search for an expression equivalent to the given one using the provided terms.
|
|
16
|
+
|
|
17
|
+
## [2.3.0] - 2026-02-28
|
|
18
|
+
|
|
19
|
+
### BREAKING CHANGES
|
|
20
|
+
|
|
21
|
+
- `ski.js` CLI interface changed (see below)
|
|
22
|
+
- Quest page css changes (see below)
|
|
23
|
+
- `diag()` does not indent `Alias` to reduce diffs
|
|
24
|
+
- Swapped `Expr.expect()` arguments: expected.expect(actual).
|
|
25
|
+
Mnemonic: expected is always an expr, actual may be whatever.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `Quest.Group` class and `Quest.verify()` for quest self-check capability.
|
|
30
|
+
- `Quest.selfCheck({ accepted: ..., rejected: ... })` replaces the homegrown `solution` mechanism.
|
|
31
|
+
- `{order: LI|LO}` option to `Expr.traverse()` for left-innermost / left-outermost traversal order.
|
|
32
|
+
- `FreeVar.global` constant as an explicit replacement for the `context=SKI` convention.
|
|
33
|
+
- `TermInfo` typedef for `infer()` results.
|
|
34
|
+
- CLI (`bin/ski.js`) rewritten with `commander`, now supports subcommands: `repl`, `eval`, `file`, `quest-check`.
|
|
35
|
+
- Quest authoring guide added to docs.
|
|
36
|
+
- `expr.expect(actual)` now returns `diag()` as -actual / +expected.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- `infer()` now limits recursion depth to prevent stack overflows (#10).
|
|
41
|
+
- Alias indentation in `diag()` output corrected.
|
|
42
|
+
- Quest page links fixed after previous rewrite.
|
|
43
|
+
- Aliases are no longer canonized without an explicit request.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- `toLambda()` and `toSKI()` rewritten using left-innermost traverse.
|
|
48
|
+
- `Church` is now a descendant of `Expr` instead of `Native` (minor speedup).
|
|
49
|
+
- `Expr.constructor` removed for a minor speedup.
|
|
50
|
+
- `parser.add()` now accepts an options object in place of a plain note string.
|
|
51
|
+
- Quest solutions moved to `data/quest-solutions.json`; removed from quest-data.
|
|
52
|
+
- Quest nav items use stable `.ski-quest-nav-item` class instead of fragile `.ski-quest-index a`.
|
|
53
|
+
|
|
8
54
|
## [2.2.1] - 2026-02-22
|
|
9
55
|
|
|
10
56
|
### Added
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Simple Kombinator Interpreter
|
|
2
2
|
|
|
3
|
+
> **A humane tooling for inhuman logic**
|
|
4
|
+
|
|
3
5
|
This package contains a
|
|
4
6
|
[combinatory logic](https://en.wikipedia.org/wiki/Combinatory_logic)
|
|
5
7
|
and [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus)
|
|
@@ -49,11 +51,12 @@ back to human readable form.
|
|
|
49
51
|
|
|
50
52
|
# Execution strategy
|
|
51
53
|
|
|
52
|
-
Applications and native terms use
|
|
54
|
+
Applications and native terms use leftmost-outermost strategy, i.e. the first term in the tree
|
|
53
55
|
that has enough arguments is executed and the step ends there.
|
|
54
56
|
|
|
55
|
-
Lambda terms are lazy, i.e. the body is not touched
|
|
56
|
-
all free variables are bound.
|
|
57
|
+
Lambda terms are lazy, i.e. the body is not touched whatsoever
|
|
58
|
+
until all the free variables are bound.
|
|
59
|
+
This is consistent with combinator behavior under LO order.
|
|
57
60
|
|
|
58
61
|
# Playground
|
|
59
62
|
|
|
@@ -62,11 +65,13 @@ all free variables are bound.
|
|
|
62
65
|
* all of the above features (except comparison and JS-native terms) in your browser
|
|
63
66
|
* expressions have permalinks
|
|
64
67
|
* can configure verbosity and execution speed
|
|
68
|
+
* switchable visual highlighting of redexes and outline of subexpressions
|
|
65
69
|
|
|
66
70
|
* [Quests](https://dallaylaen.github.io/ski-interpreter/quest.html)
|
|
67
71
|
|
|
68
|
-
This page contains small
|
|
72
|
+
This page contains small combinatory logic exercises of increasing (hopefully) diffuculty.
|
|
69
73
|
Each task requires the user to build a combinator with specific properties.
|
|
74
|
+
New combinators are unlocked as the user progresses.
|
|
70
75
|
|
|
71
76
|
# Installation
|
|
72
77
|
|
|
@@ -76,7 +81,29 @@ npm install @dallaylaen/ski-interpreter
|
|
|
76
81
|
|
|
77
82
|
# CLI
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
[bin/ski.js](bin/ski.js) - also available as `npx ski` - contains several subcommands:
|
|
85
|
+
|
|
86
|
+
## Subcommands
|
|
87
|
+
|
|
88
|
+
* **`repl`** - Start an interactive REPL
|
|
89
|
+
* `--verbose` - Show all evaluation steps
|
|
90
|
+
* Built-in commands (type `!help` in REPL):
|
|
91
|
+
* `!ls` - List all defined terms
|
|
92
|
+
* `!help` - Show available commands
|
|
93
|
+
|
|
94
|
+
* **`eval <expression>`** - Evaluate a single expression
|
|
95
|
+
* `--verbose` - Show all evaluation steps
|
|
96
|
+
* Example: `ski eval "S K K x"`
|
|
97
|
+
|
|
98
|
+
* **`file <filepath>`** - Evaluate expressions from a file
|
|
99
|
+
* `--verbose` - Show all evaluation steps
|
|
100
|
+
* Example: `ski file script.ski`
|
|
101
|
+
|
|
102
|
+
* **`quest-check <files...>`** - Validate quest definition files
|
|
103
|
+
* `--solution <file>` - Load solutions from a JSON file for verification
|
|
104
|
+
* Example: `ski quest-check quest1.json quest2.json --solution solutions.json`
|
|
105
|
+
|
|
106
|
+
If no subcommand is provided, help is displayed.
|
|
80
107
|
|
|
81
108
|
Running `SKI_REPL=1 node -r @dallaylaen/ski-interpreter/bin/ski.js`
|
|
82
109
|
will start a node shell with the `SKI` class available as a global variable.
|
|
@@ -149,12 +176,38 @@ const lambdaSteps = [...skiExpr.toLambda()];
|
|
|
149
176
|
|
|
150
177
|
## Fancy formatting
|
|
151
178
|
|
|
152
|
-
|
|
153
|
-
|
|
179
|
+
`expr.format(options?)` converts an expression to a string with fine-grained
|
|
180
|
+
control over notation. Called without arguments it is equivalent to
|
|
181
|
+
`expr.toString()`.
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
const expr = ski.parse('S K K');
|
|
185
|
+
|
|
186
|
+
expr.format() // 'S K K' (default, terse)
|
|
187
|
+
expr.format({ terse: false }) // 'S(K)(K)' — every argument gets parentheses
|
|
188
|
+
expr.format({ html: true }) // HTML-safe: free vars wrapped in <var>,
|
|
189
|
+
// '->' becomes '->', fancyName used when set
|
|
190
|
+
|
|
191
|
+
// Custom lambda notation
|
|
192
|
+
expr.format({ lambda: ['', '=>', ''], terse: false }) // JavaScript style
|
|
193
|
+
expr.format({ lambda: ['λ', '.', ''] }) // math style
|
|
194
|
+
expr.format({ lambda: ['(', '->', ')'], around: ['(', ')'], brackets: ['', ''] })
|
|
195
|
+
// Lisp style, still parseable
|
|
196
|
+
|
|
197
|
+
// Redex highlighting (e.g. for step-by-step HTML output)
|
|
198
|
+
ski.parse('I x').format({ html: true, redex: ['<b>', '</b>'] })
|
|
199
|
+
// '<b>I</b> <var>x</var>'
|
|
200
|
+
|
|
201
|
+
// inventory: show listed aliases by name, expand everything else
|
|
202
|
+
const { T } = ski.getTerms();
|
|
203
|
+
expr.format({ inventory: { T } }) // keeps T as 'T', expands any other aliases
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The `brackets`, `var`, `around`, and `redex` options each take a `[open, close]`
|
|
207
|
+
pair of strings; `lambda` takes a `[prefix, separator, suffix]` triple.
|
|
154
208
|
|
|
155
|
-
`expr.diag()` will instead output indented
|
|
156
|
-
|
|
157
|
-
and variables labeled for disambiguation.
|
|
209
|
+
`expr.diag()` will instead output an indented expression tree (breadth-first)
|
|
210
|
+
with class information and variables labeled for disambiguation.
|
|
158
211
|
|
|
159
212
|
## Variable scoping
|
|
160
213
|
|
|
@@ -217,7 +270,7 @@ q.check('K'); // fail
|
|
|
217
270
|
q.check('K(K(y x))') // nope! the variable scopes won't match
|
|
218
271
|
```
|
|
219
272
|
|
|
220
|
-
See [quest
|
|
273
|
+
See also [the quest guide](quest-intro.md) for more details on building your own quests or even interactive quest pages.
|
|
221
274
|
|
|
222
275
|
# Package contents
|
|
223
276
|
|
|
@@ -234,6 +287,7 @@ for building interactive quest pages from JSON-encoded quest data;
|
|
|
234
287
|
|
|
235
288
|
* [@ivanaxe](https://github.com/ivanaxe) for luring me into [icfpc 2011](http://icfpc2011.blogspot.com/2011/06/task-description-contest-starts-now.html) where I was introduced to combinators.
|
|
236
289
|
* [@akuklev](https://github.com/akuklev) for explaining functional programming to me so many times that I actually got some idea.
|
|
290
|
+
* [One happy fellow](https://github.com/happyfellow-one) whose [riddle](https://blog.happyfellow.dev/a-riddle/) trolled me into writing an early `traverse` prototype.
|
|
237
291
|
|
|
238
292
|
# Prior art and inspiration
|
|
239
293
|
|
package/bin/ski.js
CHANGED
|
@@ -1,121 +1,308 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --stack-size=20600
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs/promises');
|
|
4
|
+
const { Command } = require('commander');
|
|
4
5
|
|
|
5
6
|
const { SKI } = require('../lib/ski-interpreter.cjs');
|
|
7
|
+
const { Quest } = require('../src/quest.js');
|
|
6
8
|
|
|
7
|
-
const
|
|
9
|
+
const program = new Command();
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
program
|
|
12
|
+
.name('ski')
|
|
13
|
+
.description('Simple Kombinator Interpreter - a combinatory logic & lambda calculus parser and interpreter')
|
|
14
|
+
.version('2.2.1');
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// REPL subcommand
|
|
17
|
+
program
|
|
18
|
+
.command('repl')
|
|
19
|
+
.description('Start interactive REPL')
|
|
20
|
+
.option('--verbose', 'Show all evaluation steps')
|
|
21
|
+
.action((options) => {
|
|
22
|
+
startRepl(options.verbose);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Eval subcommand
|
|
26
|
+
program
|
|
27
|
+
.command('eval <expression>')
|
|
28
|
+
.description('Evaluate a single expression')
|
|
29
|
+
.option('--verbose', 'Show all evaluation steps')
|
|
30
|
+
.action((expression, options) => {
|
|
31
|
+
evaluateExpression(expression, options.verbose);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// File subcommand
|
|
35
|
+
program
|
|
36
|
+
.command('file <filepath>')
|
|
37
|
+
.description('Evaluate expressions from a file')
|
|
38
|
+
.option('--verbose', 'Show all evaluation steps')
|
|
39
|
+
.action((filepath, options) => {
|
|
40
|
+
evaluateFile(filepath, options.verbose);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Search subcommand
|
|
44
|
+
program
|
|
45
|
+
.command('search <target> <terms...>')
|
|
46
|
+
.description('Search for an expression equivalent to target using known terms')
|
|
47
|
+
.action((target, terms) => {
|
|
48
|
+
searchExpression(target, terms);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Extract subcommand
|
|
52
|
+
program
|
|
53
|
+
.command('extract <target> <terms...>')
|
|
54
|
+
.description('Rewrite target expression using known terms where possible')
|
|
55
|
+
.action((target, terms) => {
|
|
56
|
+
extractExpression(target, terms);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Quest-check subcommand
|
|
60
|
+
program
|
|
61
|
+
.command('quest-check <files...>')
|
|
62
|
+
.description('Check quest files for validity')
|
|
63
|
+
.option('--solution <file>', 'Load solutions from file')
|
|
64
|
+
.action((files, options) => {
|
|
65
|
+
questCheck(files, options.solution);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Default to REPL if no command provided
|
|
69
|
+
program
|
|
70
|
+
.showHelpAfterError(true)
|
|
71
|
+
.parse(process.argv);
|
|
18
72
|
|
|
19
|
-
|
|
73
|
+
if (!process.argv.slice(2).length)
|
|
74
|
+
startRepl(false);
|
|
20
75
|
|
|
21
|
-
|
|
22
|
-
// interactive console
|
|
76
|
+
function startRepl (verbose) {
|
|
23
77
|
const readline = require('readline');
|
|
78
|
+
const ski = new SKI();
|
|
79
|
+
|
|
24
80
|
const rl = readline.createInterface({
|
|
25
81
|
input: process.stdin,
|
|
26
82
|
output: process.stdout,
|
|
27
83
|
prompt: '> ',
|
|
28
84
|
terminal: true,
|
|
29
85
|
});
|
|
30
|
-
|
|
31
|
-
|
|
86
|
+
|
|
87
|
+
console.log('Welcome to SKI interactive shell. Known combinators: ' + ski.showRestrict());
|
|
88
|
+
|
|
32
89
|
rl.on('line', str => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
90
|
+
if (str.match(/\S/)) {
|
|
91
|
+
if (str.startsWith('!'))
|
|
92
|
+
handleCommand(str, ski);
|
|
93
|
+
else {
|
|
94
|
+
processLine(str, ski, verbose, err => {
|
|
95
|
+
console.log('' + err);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
40
98
|
}
|
|
41
99
|
rl.prompt();
|
|
42
100
|
});
|
|
101
|
+
|
|
43
102
|
rl.once('close', () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
process.exit(0)
|
|
103
|
+
console.log('Bye, and may thy bird fly high!');
|
|
104
|
+
process.exit(0);
|
|
47
105
|
});
|
|
106
|
+
|
|
48
107
|
rl.prompt();
|
|
49
|
-
}
|
|
50
|
-
const prom = positional.length > 0
|
|
51
|
-
? fs.readFile(positional[0], 'utf8')
|
|
52
|
-
: Promise.resolve(options.e);
|
|
108
|
+
}
|
|
53
109
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
110
|
+
function evaluateExpression (expression, verbose) {
|
|
111
|
+
const ski = new SKI();
|
|
112
|
+
processLine(expression, ski, verbose, err => {
|
|
113
|
+
console.error('' + err);
|
|
114
|
+
process.exit(3);
|
|
57
115
|
});
|
|
58
116
|
}
|
|
59
117
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
118
|
+
function evaluateFile (filepath, verbose) {
|
|
119
|
+
const ski = new SKI();
|
|
120
|
+
fs.readFile(filepath, 'utf8')
|
|
121
|
+
.then(source => {
|
|
122
|
+
processLine(source, ski, verbose, err => {
|
|
123
|
+
console.error('' + err);
|
|
124
|
+
process.exit(3);
|
|
125
|
+
});
|
|
126
|
+
})
|
|
127
|
+
.catch(err => {
|
|
128
|
+
console.error('ski: ' + err);
|
|
129
|
+
process.exit(2);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function processLine (source, ski, verbose, onErr) {
|
|
134
|
+
if (!source.match(/\S/))
|
|
135
|
+
return; // nothing to see here
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const expr = ski.parse(source);
|
|
139
|
+
const t0 = new Date();
|
|
140
|
+
const isAlias = expr instanceof SKI.classes.Alias;
|
|
141
|
+
const aliasName = isAlias ? expr.name : null;
|
|
142
|
+
|
|
143
|
+
for (const state of expr.walk()) {
|
|
144
|
+
if (state.final)
|
|
145
|
+
console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
|
|
146
|
+
|
|
147
|
+
if (verbose || state.final)
|
|
148
|
+
console.log('' + state.expr.format());
|
|
149
|
+
|
|
150
|
+
if (state.final && isAlias && aliasName)
|
|
151
|
+
ski.add(aliasName, state.expr);
|
|
80
152
|
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
onErr(err);
|
|
81
155
|
}
|
|
82
156
|
}
|
|
83
157
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
158
|
+
async function questCheck (files, solutionFile) {
|
|
159
|
+
try {
|
|
160
|
+
// Load solutions if provided
|
|
161
|
+
let solutions = null;
|
|
162
|
+
if (solutionFile) {
|
|
163
|
+
const data = await fs.readFile(solutionFile, 'utf8');
|
|
164
|
+
solutions = JSON.parse(data);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Load and verify each quest file
|
|
168
|
+
let hasErrors = false;
|
|
169
|
+
const seenIds = new Set();
|
|
170
|
+
|
|
171
|
+
for (const file of files) {
|
|
172
|
+
try {
|
|
173
|
+
const data = await fs.readFile(file, 'utf8');
|
|
174
|
+
const questData = JSON.parse(data);
|
|
175
|
+
|
|
176
|
+
// Handle both single quest objects and quest groups
|
|
177
|
+
const entry = Array.isArray(questData) ? { content: questData } : questData;
|
|
100
178
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const opt = {};
|
|
179
|
+
try {
|
|
180
|
+
const group = new Quest.Group(entry);
|
|
104
181
|
|
|
105
|
-
|
|
106
|
-
|
|
182
|
+
// Verify the group
|
|
183
|
+
const findings = group.verify({
|
|
184
|
+
date: true,
|
|
185
|
+
solutions,
|
|
186
|
+
seen: seenIds
|
|
187
|
+
});
|
|
107
188
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
189
|
+
// Check for errors
|
|
190
|
+
const hasGroupErrors = Object.keys(findings).some(key => {
|
|
191
|
+
if (key === 'content') {
|
|
192
|
+
const contentErrors = findings.content?.filter(item => item !== null);
|
|
193
|
+
return contentErrors && contentErrors.length > 0;
|
|
194
|
+
}
|
|
195
|
+
return findings[key];
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (hasGroupErrors) {
|
|
199
|
+
hasErrors = true;
|
|
200
|
+
console.error(`Error in ${file}:`);
|
|
201
|
+
console.error(JSON.stringify(findings, null, 2));
|
|
202
|
+
} else
|
|
203
|
+
console.log(`✓ ${file}`);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
hasErrors = true;
|
|
206
|
+
console.error(`Error parsing quest group in ${file}:`, err.message);
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
hasErrors = true;
|
|
210
|
+
console.error(`Error reading file ${file}:`, err.message);
|
|
211
|
+
}
|
|
111
212
|
}
|
|
112
213
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
214
|
+
// Exit with appropriate code
|
|
215
|
+
process.exit(hasErrors ? 1 : 0);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('Error in quest-check:', err.message);
|
|
218
|
+
process.exit(2);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function searchExpression (targetStr, termStrs) {
|
|
223
|
+
const ski = new SKI();
|
|
224
|
+
const jar = {};
|
|
225
|
+
const target = ski.parse(targetStr, { vars: jar });
|
|
226
|
+
const seed = termStrs.map(s => ski.parse(s, { vars: jar }));
|
|
227
|
+
|
|
228
|
+
const { expr } = target.infer();
|
|
229
|
+
if (!expr) {
|
|
230
|
+
console.error('target expression is not normalizable: ' + target);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const res = SKI.extras.search(seed, { tries: 10_000_000, depth: 100 }, (e, p) => {
|
|
235
|
+
if (!p.expr)
|
|
236
|
+
return -1;
|
|
237
|
+
if (p.expr.equals(expr))
|
|
238
|
+
return 1;
|
|
239
|
+
return 0;
|
|
240
|
+
});
|
|
116
241
|
|
|
117
|
-
|
|
242
|
+
if (res.expr) {
|
|
243
|
+
console.log(`Found ${res.expr} after ${res.total} tries.`);
|
|
244
|
+
process.exit(0);
|
|
245
|
+
} else {
|
|
246
|
+
console.error(`No equivalent expression found for ${target} after ${res.total} tries.`);
|
|
247
|
+
process.exit(1);
|
|
118
248
|
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function extractExpression (targetStr, termStrs) {
|
|
252
|
+
const ski = new SKI();
|
|
253
|
+
const expr = ski.parse(targetStr);
|
|
254
|
+
const pairs = termStrs
|
|
255
|
+
.map(s => ski.parse(s))
|
|
256
|
+
.map(e => [e.infer().expr, e]);
|
|
257
|
+
|
|
258
|
+
const uncanonical = pairs.filter(pair => !pair[0]);
|
|
259
|
+
if (uncanonical.length) {
|
|
260
|
+
console.error('Some expressions could not be canonized: '
|
|
261
|
+
+ uncanonical.map(p => p[1].toString()).join(', '));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const replaced = expr.traverse(e => {
|
|
266
|
+
const canon = e.infer().expr;
|
|
267
|
+
for (const [lambda, term] of pairs) {
|
|
268
|
+
if (canon.equals(lambda))
|
|
269
|
+
return term;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (replaced)
|
|
275
|
+
console.log(replaced.toString());
|
|
276
|
+
else
|
|
277
|
+
console.log('// unchanged');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function handleCommand (input, ski) {
|
|
281
|
+
const parts = input.trim().split(/\s+/);
|
|
282
|
+
const cmd = parts[0];
|
|
283
|
+
|
|
284
|
+
const dispatch = {
|
|
285
|
+
'!ls': () => {
|
|
286
|
+
const terms = ski.getTerms();
|
|
287
|
+
const list = Object.keys(terms).sort();
|
|
288
|
+
for (const name of list) {
|
|
289
|
+
const term = terms[name];
|
|
290
|
+
if (term instanceof SKI.classes.Alias)
|
|
291
|
+
console.log(` ${name} = ${term.impl}`);
|
|
292
|
+
else if (term instanceof SKI.classes.Native)
|
|
293
|
+
console.log(` ${name} ${term.props?.expr ?? '(native)'}`);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
'!help': () => {
|
|
297
|
+
console.log('Available commands:');
|
|
298
|
+
console.log(' !ls - List term inventory');
|
|
299
|
+
console.log(' !help - Show this help message');
|
|
300
|
+
},
|
|
301
|
+
'': () => {
|
|
302
|
+
console.log(`Unknown command: ${cmd}`);
|
|
303
|
+
console.log('Type !help for available commands.');
|
|
304
|
+
}
|
|
305
|
+
};
|
|
119
306
|
|
|
120
|
-
|
|
307
|
+
(dispatch[cmd] || dispatch[''])(...parts.slice(1));
|
|
121
308
|
}
|