@dallaylaen/ski-interpreter 1.0.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/LICENSE +21 -0
- package/README.md +138 -0
- package/bin/ski.js +122 -0
- package/index.js +8 -0
- package/lib/expr.js +1009 -0
- package/lib/parser.js +326 -0
- package/lib/quest.js +311 -0
- package/lib/util.js +74 -0
- package/package.json +46 -0
- package/types/index.d.ts +7 -0
- package/types/lib/expr.d.ts +344 -0
- package/types/lib/parser.d.ts +138 -0
- package/types/lib/quest.d.ts +139 -0
- package/types/lib/util.d.ts +13 -0
package/lib/expr.js
ADDED
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
const { missingIndices, isSubset } = require('./util');
|
|
2
|
+
|
|
3
|
+
const globalOptions = {
|
|
4
|
+
terse: true,
|
|
5
|
+
max: 1000,
|
|
6
|
+
maxArgs: 32,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
class Expr {
|
|
10
|
+
/**
|
|
11
|
+
* @descr A generic combinatory logic expression.
|
|
12
|
+
*/
|
|
13
|
+
constructor () {
|
|
14
|
+
if (new.target === Expr)
|
|
15
|
+
throw new Error('Attempt to instantiate abstract class Expr');
|
|
16
|
+
this.arity = Infinity;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* postprocess term after parsing. typically return self but may return other term or die
|
|
21
|
+
* @return {Expr}
|
|
22
|
+
*/
|
|
23
|
+
postParse () {
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @desc apply self to zero or more terms and return the resulting term,
|
|
29
|
+
* without performing any calculations whatsoever
|
|
30
|
+
* @param {Expr} args
|
|
31
|
+
* @return {Expr}
|
|
32
|
+
*/
|
|
33
|
+
apply (...args) {
|
|
34
|
+
return args.length > 0 ? new App(this, ...args) : this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* expand all terms but don't perform any calculations
|
|
39
|
+
* @return {Expr}
|
|
40
|
+
*/
|
|
41
|
+
expand () {
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @desc return all free variables within the term
|
|
47
|
+
* @return {Set<FreeVar>}
|
|
48
|
+
*/
|
|
49
|
+
freeVars () {
|
|
50
|
+
const symbols = this.getSymbols();
|
|
51
|
+
const out = new Set();
|
|
52
|
+
for (const [key, _] of symbols) {
|
|
53
|
+
if (key instanceof FreeVar)
|
|
54
|
+
out.add(key);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
hasLambda () {
|
|
60
|
+
const sym = this.getSymbols();
|
|
61
|
+
return sym.has(Expr.lambdaPlaceholder);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
freeOnly () {
|
|
65
|
+
for (const [key, _] of this.getSymbols()) {
|
|
66
|
+
if (!(key instanceof FreeVar))
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @desc return all terminal values within the term, that is, values not
|
|
74
|
+
* composed of other terms. For example, in S(KI)K, the terminals are S, K, I.
|
|
75
|
+
* @return {Map<Expr, number>}
|
|
76
|
+
*/
|
|
77
|
+
getSymbols () {
|
|
78
|
+
// TODO better name!
|
|
79
|
+
return new Map([[this, 1]]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @desc rought estimate of the complexity of the term
|
|
84
|
+
* @return {number}
|
|
85
|
+
*/
|
|
86
|
+
weight () {
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
*
|
|
92
|
+
* @param {{max: number?, maxArgs: number?, bestGuess?: Expr}} options
|
|
93
|
+
* @return {{
|
|
94
|
+
* found: boolean,
|
|
95
|
+
* proper: boolean,
|
|
96
|
+
* arity: number?,
|
|
97
|
+
* linear: boolean?,
|
|
98
|
+
* canonical?: Expr,
|
|
99
|
+
* steps: number?,
|
|
100
|
+
* skip: Set<number>?
|
|
101
|
+
* }}
|
|
102
|
+
*/
|
|
103
|
+
canonize (options = {}) {
|
|
104
|
+
const max = options.max ?? globalOptions.max;
|
|
105
|
+
const maxArgs = options.maxArgs ?? globalOptions.maxArgs;
|
|
106
|
+
|
|
107
|
+
let steps = 0;
|
|
108
|
+
let expr = this;
|
|
109
|
+
const jar = [];
|
|
110
|
+
for (let i = 0; i < maxArgs; i++) {
|
|
111
|
+
const calc = expr.run({ max });
|
|
112
|
+
steps += calc.steps;
|
|
113
|
+
if (!calc.final)
|
|
114
|
+
break;
|
|
115
|
+
expr = calc.expr;
|
|
116
|
+
if (!expr.wantsArgs()) {
|
|
117
|
+
// found!
|
|
118
|
+
const symbols = expr.getSymbols();
|
|
119
|
+
const skip = missingIndices(jar, symbols);
|
|
120
|
+
const proper = isSubset(symbols.keys(), new Set(jar));
|
|
121
|
+
const duplicates = [...symbols.entries()].filter(([_, v]) => v > 1);
|
|
122
|
+
const linear = proper && skip.size === 0 && duplicates.length === 0;
|
|
123
|
+
return {
|
|
124
|
+
arity: i,
|
|
125
|
+
found: true,
|
|
126
|
+
canonical: maybeLambda(jar, expr),
|
|
127
|
+
proper,
|
|
128
|
+
linear,
|
|
129
|
+
steps,
|
|
130
|
+
...(skip.size ? { skip } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const next = new FreeVar('abcdefgh'[i] ?? 'x' + i);
|
|
134
|
+
jar.push(next);
|
|
135
|
+
expr = expr.apply(next);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fallback = { found: false, proper: false, steps };
|
|
139
|
+
if (options.bestGuess)
|
|
140
|
+
fallback.canonical = options.bestGuess;
|
|
141
|
+
return fallback;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @desc Returns a series of lambda terms equivalent to the given expression,
|
|
146
|
+
* up to the provided computation steps limit,
|
|
147
|
+
* in decreasing weight order.
|
|
148
|
+
* @param {{
|
|
149
|
+
* max: number?,
|
|
150
|
+
* maxArgs: number?,
|
|
151
|
+
* varGen: function(void): FreeVar?,
|
|
152
|
+
* steps: number?,
|
|
153
|
+
* html: boolean?,
|
|
154
|
+
* latin: number?,
|
|
155
|
+
* }} options
|
|
156
|
+
* @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
|
|
157
|
+
* @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
|
|
158
|
+
*/
|
|
159
|
+
* lambdify (options = {}) {
|
|
160
|
+
const expr = naiveCanonize(this, options);
|
|
161
|
+
yield * simplifyLambda(expr, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @desc same semantics as walk() but rewrite step by step instead of computing
|
|
166
|
+
* @param {{max: number?}} options
|
|
167
|
+
* @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
|
|
168
|
+
*/
|
|
169
|
+
* rewriteSKI (options = {}) {
|
|
170
|
+
// TODO options.max is not actually max, it's the number of steps in one iteration
|
|
171
|
+
let steps = 0;
|
|
172
|
+
let expr = this;
|
|
173
|
+
while (true) {
|
|
174
|
+
const opt = { max: options.max ?? 1, steps: 0 };
|
|
175
|
+
const next = expr._rski(opt);
|
|
176
|
+
const final = opt.steps === 0;
|
|
177
|
+
yield { expr, steps, final };
|
|
178
|
+
if (final)
|
|
179
|
+
break;
|
|
180
|
+
expr = next;
|
|
181
|
+
steps += opt.steps;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @desc Rename free variables in the expression using the given sequence
|
|
187
|
+
* This is for eye-candy only, as the interpreter knows darn well hot to distinguish vars,
|
|
188
|
+
* regardless of names.
|
|
189
|
+
* @param {IterableIterator<string>} seq
|
|
190
|
+
* @return {Expr}
|
|
191
|
+
*/
|
|
192
|
+
renameVars (seq) {
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_rski (options) {
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @desc Whether the term will reduce further if given more arguments.
|
|
202
|
+
* In practice, equivalent to "starts with a FreeVar"
|
|
203
|
+
* Used by canonize (duh...)
|
|
204
|
+
* @return {boolean}
|
|
205
|
+
*/
|
|
206
|
+
wantsArgs () {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Apply self to list of given args.
|
|
212
|
+
* Normally, only native combinators know how to do it.
|
|
213
|
+
* @param {Expr[]} args
|
|
214
|
+
* @return {Expr|null}
|
|
215
|
+
*/
|
|
216
|
+
reduce (args) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Replace all instances of free vars with corresponding values and return the resulting expression.
|
|
222
|
+
* return null if no changes could be made.
|
|
223
|
+
* @param {FreeVar} plug
|
|
224
|
+
* @param {Expr} value
|
|
225
|
+
* @return {Expr|null}
|
|
226
|
+
*/
|
|
227
|
+
subst (plug, value) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @desc iterate one step of calculation in accordance with known rules.
|
|
233
|
+
* @return {{expr: Expr, steps: number, changed: boolean}}
|
|
234
|
+
*/
|
|
235
|
+
step () { return { expr: this, steps: 0, changed: false } }
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @desc Run uninterrupted sequence of step() applications
|
|
239
|
+
* until the expression is irreducible, or max number of steps is reached.
|
|
240
|
+
* Default number of steps = 1000.
|
|
241
|
+
* @param {{max: number?, steps: number?, throw: boolean?}|Expr} [opt]
|
|
242
|
+
* @param {Expr} args
|
|
243
|
+
* @return {{expr: Expr, steps: number, final: boolean}}
|
|
244
|
+
*/
|
|
245
|
+
run (opt = {}, ...args) {
|
|
246
|
+
if (opt instanceof Expr) {
|
|
247
|
+
args.unshift(opt);
|
|
248
|
+
opt = {};
|
|
249
|
+
}
|
|
250
|
+
let expr = args ? this.apply(...args) : this;
|
|
251
|
+
let steps = opt.steps ?? 0;
|
|
252
|
+
const max = (opt.max ?? globalOptions.max) + steps;
|
|
253
|
+
let final = false;
|
|
254
|
+
for (; steps < max; ) {
|
|
255
|
+
const next = expr.step();
|
|
256
|
+
if (!next.changed) {
|
|
257
|
+
final = true;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
steps += next.steps;
|
|
261
|
+
expr = next.expr;
|
|
262
|
+
}
|
|
263
|
+
if (opt.throw && !final)
|
|
264
|
+
throw new Error('Failed to compute expression in ' + max + ' steps');
|
|
265
|
+
return { final, steps, expr };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Execute step() while possible, yielding a brief description of events after each step.
|
|
270
|
+
* Mnemonics: like run() but slower.
|
|
271
|
+
* @param {{max: number?}} options
|
|
272
|
+
* @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
|
|
273
|
+
*/
|
|
274
|
+
* walk (options = {}) {
|
|
275
|
+
const max = options.max ?? Infinity;
|
|
276
|
+
let steps = 0;
|
|
277
|
+
let expr = this;
|
|
278
|
+
let final = false;
|
|
279
|
+
|
|
280
|
+
while (steps < max) {
|
|
281
|
+
// 1. calculate
|
|
282
|
+
// 2. yield _unchanged_ expression
|
|
283
|
+
// 3. either advance or stop
|
|
284
|
+
const next = expr.step();
|
|
285
|
+
if (!next.changed)
|
|
286
|
+
final = true;
|
|
287
|
+
yield { expr, steps, final };
|
|
288
|
+
if (final)
|
|
289
|
+
break;
|
|
290
|
+
steps += next.steps;
|
|
291
|
+
expr = next.expr;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
*
|
|
297
|
+
* @param {Expr} other
|
|
298
|
+
* @return {boolean}
|
|
299
|
+
*/
|
|
300
|
+
equals (other) {
|
|
301
|
+
return this === other;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
contains (other) {
|
|
305
|
+
return this === other || this.equals(other);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
expect (other) {
|
|
309
|
+
if (!(other instanceof Expr))
|
|
310
|
+
throw new Error('Attempt to expect a combinator to equal something else: ' + other);
|
|
311
|
+
if (this.equals(other))
|
|
312
|
+
return;
|
|
313
|
+
|
|
314
|
+
// TODO wanna use AssertionError but webpack doesn't recognize it
|
|
315
|
+
// still the below hack works for mocha-based tests.
|
|
316
|
+
const poorMans = new Error('Found term ' + this + ' but expected ' + other);
|
|
317
|
+
poorMans.expected = other.toString();
|
|
318
|
+
poorMans.actual = this.toString();
|
|
319
|
+
throw poorMans;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @param {{terse: boolean?, html: boolean?}} [options]
|
|
324
|
+
* @return {string} string representation of the expression
|
|
325
|
+
*/
|
|
326
|
+
toString (options = {}) {
|
|
327
|
+
// uncomment the following line if you want to debug the parser with prints
|
|
328
|
+
// return this.constructor.name
|
|
329
|
+
throw new Error( 'No toString() method defined in class ' + this.constructor.name );
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
*
|
|
334
|
+
* @return {boolean}
|
|
335
|
+
*/
|
|
336
|
+
needsParens () {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
*
|
|
342
|
+
* @return {string}
|
|
343
|
+
*/
|
|
344
|
+
toJSON () {
|
|
345
|
+
return this.expand().toString({ terse: false });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Constants that define when whitespace between terms may be omitted in App.toString()
|
|
351
|
+
*/
|
|
352
|
+
const BITS = 4;
|
|
353
|
+
const [T_UNKNOWN, T_PARENS, T_UPPER, T_LOWER]
|
|
354
|
+
= (function * () { for (let i = 0; ; yield i++); })();
|
|
355
|
+
const canLump = new Set([
|
|
356
|
+
(T_PARENS << BITS) + T_PARENS,
|
|
357
|
+
(T_PARENS << BITS) + T_UPPER,
|
|
358
|
+
(T_UPPER << BITS) + T_PARENS,
|
|
359
|
+
(T_UPPER << BITS) + T_UPPER,
|
|
360
|
+
(T_UPPER << BITS) + T_LOWER,
|
|
361
|
+
(T_LOWER << BITS) + T_PARENS,
|
|
362
|
+
(T_UNKNOWN << BITS) + T_PARENS,
|
|
363
|
+
]);
|
|
364
|
+
|
|
365
|
+
class App extends Expr {
|
|
366
|
+
/**
|
|
367
|
+
* @desc Application of fun() to args.
|
|
368
|
+
* Never ever use new App(fun, ...args) directly, use fun.apply(...args) instead.
|
|
369
|
+
* @param {Expr} fun
|
|
370
|
+
* @param {Expr} args
|
|
371
|
+
*/
|
|
372
|
+
constructor (fun, ...args) {
|
|
373
|
+
if (args.length === 0)
|
|
374
|
+
throw new Error('Attempt to create an application with no arguments (likely interpreter bug)');
|
|
375
|
+
super();
|
|
376
|
+
this.fun = fun;
|
|
377
|
+
this.args = args;
|
|
378
|
+
this.final = false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
weight () {
|
|
382
|
+
return this.args.reduce((acc, x) => acc + x.weight(), this.fun.weight());
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
getSymbols () {
|
|
386
|
+
const out = this.fun.getSymbols();
|
|
387
|
+
for (const term of this.args) {
|
|
388
|
+
for (const [key, value] of term.getSymbols())
|
|
389
|
+
out.set(key, (out.get(key) ?? 0) + value);
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
wantsArgs () {
|
|
395
|
+
return this.fun.wantsArgs();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
apply (...args) {
|
|
399
|
+
if (args.length === 0)
|
|
400
|
+
return this;
|
|
401
|
+
return this.fun.apply( ...this.args, ...args);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
expand () {
|
|
405
|
+
return this.fun.expand().apply(...this.args.map(x => x.expand()));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
canonize (options = {}) {
|
|
409
|
+
const [fun, arg] = this.split().map(x => x.canonize(options).canonical);
|
|
410
|
+
return super.canonize({
|
|
411
|
+
...options,
|
|
412
|
+
...(fun && arg ? { bestGuess: fun.apply(arg) } : {})
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
renameVars (seq) {
|
|
417
|
+
const fun = this.fun.renameVars(seq);
|
|
418
|
+
const args = this.args.map(x => x.renameVars(seq));
|
|
419
|
+
return fun.apply(...args);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
subst (plug, value) {
|
|
423
|
+
const fun = this.fun.subst(plug, value);
|
|
424
|
+
let change = fun === null ? 0 : 1;
|
|
425
|
+
const args = [];
|
|
426
|
+
for (const x of this.args) {
|
|
427
|
+
const next = x.subst(plug, value);
|
|
428
|
+
if (next === null)
|
|
429
|
+
args.push(x);
|
|
430
|
+
else {
|
|
431
|
+
args.push(next);
|
|
432
|
+
change++;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return change ? (fun ?? this.fun).apply(...args) : null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* @return {{expr: Expr, steps: number}}
|
|
441
|
+
*/
|
|
442
|
+
|
|
443
|
+
step () {
|
|
444
|
+
// normal reduction order: first try root, then at most 1 step
|
|
445
|
+
if (!this.final) {
|
|
446
|
+
const reduced = this.fun.reduce(this.args);
|
|
447
|
+
if (reduced)
|
|
448
|
+
return { expr: reduced, steps: 1, changed: true };
|
|
449
|
+
|
|
450
|
+
// now try recursing
|
|
451
|
+
|
|
452
|
+
const fun = this.fun.step();
|
|
453
|
+
if (fun.changed)
|
|
454
|
+
return { expr: fun.expr.apply(...this.args), steps: fun.steps, changed: true };
|
|
455
|
+
|
|
456
|
+
for (let i = 0; i < this.args.length; i++) {
|
|
457
|
+
const next = this.args[i].step();
|
|
458
|
+
if (!next.changed)
|
|
459
|
+
continue;
|
|
460
|
+
const args = this.args.slice();
|
|
461
|
+
args[i] = next.expr;
|
|
462
|
+
return { expr: this.fun.apply(...args), steps: next.steps, changed: true };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
this.final = true;
|
|
466
|
+
return { expr: this, steps: 0, changed: false };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
split () {
|
|
470
|
+
// pretend we are an elegant (cons fun arg) and not a sleazy imperative array
|
|
471
|
+
const args = this.args.slice();
|
|
472
|
+
const last = args.pop();
|
|
473
|
+
return [this.fun.apply(...args), last];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* @desc Convert the expression to SKI combinatory logic
|
|
478
|
+
* @return {Expr}
|
|
479
|
+
*/
|
|
480
|
+
|
|
481
|
+
_rski (options) {
|
|
482
|
+
if (options.steps >= options.max)
|
|
483
|
+
return this;
|
|
484
|
+
return this.fun._rski(options).apply(...this.args.map(x => x._rski(options)));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
equals (other) {
|
|
488
|
+
if (!(other instanceof App))
|
|
489
|
+
return false;
|
|
490
|
+
if (other.args.length !== this.args.length)
|
|
491
|
+
return false;
|
|
492
|
+
if (!this.fun.equals(other.fun))
|
|
493
|
+
return false;
|
|
494
|
+
for (let i = 0; i < this.args.length; i++) {
|
|
495
|
+
if (!this.args[i].equals(other.args[i]))
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
contains (other) {
|
|
502
|
+
if (this.fun.contains(other))
|
|
503
|
+
return true;
|
|
504
|
+
for (const subtree of this.args) {
|
|
505
|
+
if (subtree.contains(other))
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
return super.contains(other);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
toString (opt = {}) {
|
|
512
|
+
if (opt.terse ?? globalOptions.terse) {
|
|
513
|
+
const out = [];
|
|
514
|
+
let oldType = 0;
|
|
515
|
+
// stupid ad-hoc state machine, see above for constant definitions
|
|
516
|
+
for (const term of [this.fun, ...this.args]) {
|
|
517
|
+
let s = term.toString(opt);
|
|
518
|
+
let newType = T_UNKNOWN;
|
|
519
|
+
if (s.match(/^[A-Z]$/))
|
|
520
|
+
newType = T_UPPER;
|
|
521
|
+
else if (term instanceof FreeVar || s.match(/^[a-z][a-z_0-9]*$/))
|
|
522
|
+
newType = T_LOWER;
|
|
523
|
+
else if (s.match(/^[0-9]+$/))
|
|
524
|
+
// no special treatment for numerals, skip
|
|
525
|
+
;
|
|
526
|
+
else if (out.length !== 0 || term.needsParens()) {
|
|
527
|
+
s = '(' + s + ')';
|
|
528
|
+
newType = T_PARENS;
|
|
529
|
+
}
|
|
530
|
+
if (!canLump.has((oldType << BITS) | newType) && out.length > 0)
|
|
531
|
+
out.push(' ');
|
|
532
|
+
out.push(s);
|
|
533
|
+
oldType = newType;
|
|
534
|
+
}
|
|
535
|
+
return out.join('');
|
|
536
|
+
} else {
|
|
537
|
+
const fun = this.fun.toString(opt);
|
|
538
|
+
const root = this.fun.needsParens() ? '(' + fun + ')' : fun;
|
|
539
|
+
return root + this.args.map(x => '(' + x.toString(opt) + ')').join('');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
class Named extends Expr {
|
|
545
|
+
/**
|
|
546
|
+
* @desc a constant named 'name'
|
|
547
|
+
* @param {String} name
|
|
548
|
+
*/
|
|
549
|
+
constructor (name) {
|
|
550
|
+
super();
|
|
551
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
552
|
+
throw new Error('Attempt to create a named term with improper name');
|
|
553
|
+
this.name = name;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
toString () {
|
|
557
|
+
return this.name;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let freeId = 0;
|
|
562
|
+
|
|
563
|
+
class FreeVar extends Named {
|
|
564
|
+
constructor (name) {
|
|
565
|
+
super(name);
|
|
566
|
+
this.id = ++freeId;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
subst (plug, value) {
|
|
570
|
+
if (this === plug)
|
|
571
|
+
return value;
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
weight () {
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
wantsArgs () {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
toString ( opt = {} ) {
|
|
584
|
+
return (opt.html && /^[a-z]$/.test(this.name)) ? '<var>' + this.name + '</var>' : this.name;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* @typedef {function(Expr): Expr | AnyArity} AnyArity
|
|
590
|
+
*/
|
|
591
|
+
|
|
592
|
+
class Native extends Named {
|
|
593
|
+
/**
|
|
594
|
+
* @desc A term named 'name' that converts next 'arity' arguments into
|
|
595
|
+
* an expression returned by 'impl' function
|
|
596
|
+
* If an apply: Expr=>Expr|null function is given, it will be attempted upon application
|
|
597
|
+
* before building an App object. This allows to plug in argument coercions,
|
|
598
|
+
* e.g. instantly perform a numeric operation natively if the next term is a number.
|
|
599
|
+
* @param {String} name
|
|
600
|
+
* @param {AnyArity} impl
|
|
601
|
+
* @param {{note: string?, arity: number?, canonize: boolean?, apply: function(Expr):(Expr|null) }} [opt]
|
|
602
|
+
*/
|
|
603
|
+
constructor (name, impl, opt = {}) {
|
|
604
|
+
super(name);
|
|
605
|
+
// setup essentials
|
|
606
|
+
this.impl = impl;
|
|
607
|
+
if (opt.apply)
|
|
608
|
+
this.onApply = opt.apply;
|
|
609
|
+
this.arity = opt.arity ?? 1;
|
|
610
|
+
|
|
611
|
+
// try to bootstrap and guess some of our properties
|
|
612
|
+
const guess = (opt.canonize ?? true) ? this.canonize() : { found: false };
|
|
613
|
+
|
|
614
|
+
if (!opt.arity)
|
|
615
|
+
this.arity = guess.arity || 1;
|
|
616
|
+
|
|
617
|
+
this.note = opt.note ?? guess.canonical?.toString({ terse: true, html: true });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
apply (...args) {
|
|
621
|
+
if (this.onApply && args.length >= 1) {
|
|
622
|
+
if (typeof this.onApply !== 'function') {
|
|
623
|
+
throw new Error('Native combinator ' + this + ' has an invalid onApply property of type'
|
|
624
|
+
+ typeof this.onApply + ': ' + this.onApply);
|
|
625
|
+
}
|
|
626
|
+
const subst = this.onApply(args[0]);
|
|
627
|
+
if (subst instanceof Expr)
|
|
628
|
+
return subst.apply(...args.slice(1));
|
|
629
|
+
}
|
|
630
|
+
return super.apply(...args);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
_rski (options) {
|
|
634
|
+
if (this === native.I || this === native.K || this === native.S || (options.steps >= options.max))
|
|
635
|
+
return this;
|
|
636
|
+
const canon = this.canonize().canonical;
|
|
637
|
+
if (!canon)
|
|
638
|
+
return this;
|
|
639
|
+
options.steps++;
|
|
640
|
+
return canon._rski(options);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
reduce (args) {
|
|
644
|
+
if (args.length < this.arity)
|
|
645
|
+
return null;
|
|
646
|
+
let egde = 0;
|
|
647
|
+
let step = this.impl;
|
|
648
|
+
while (typeof step === 'function') {
|
|
649
|
+
if (egde >= args.length)
|
|
650
|
+
return null;
|
|
651
|
+
step = step(args[egde++]);
|
|
652
|
+
}
|
|
653
|
+
if (!(step instanceof Expr))
|
|
654
|
+
throw new Error('Native combinator ' + this + ' reduced to a non-expression: ' + step);
|
|
655
|
+
return step.apply(...args.slice(egde));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
toJSON () {
|
|
659
|
+
return 'Native:' + this.name;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const native = {};
|
|
664
|
+
function addNative (name, impl, opt) {
|
|
665
|
+
native[name] = new Native(name, impl, opt);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
class Lambda extends Expr {
|
|
669
|
+
/**
|
|
670
|
+
* @param {FreeVar|FreeVar[]} arg
|
|
671
|
+
* @param {Expr} impl
|
|
672
|
+
*/
|
|
673
|
+
constructor (arg, impl) {
|
|
674
|
+
if (Array.isArray(arg)) {
|
|
675
|
+
// check args before everything
|
|
676
|
+
if (arg.length === 0)
|
|
677
|
+
throw new Error('empty argument list in lambda constructor');
|
|
678
|
+
|
|
679
|
+
const [my, ...tail] = arg;
|
|
680
|
+
const known = new Set([my.name]);
|
|
681
|
+
|
|
682
|
+
while (tail.length > 0) {
|
|
683
|
+
const last = tail.pop();
|
|
684
|
+
if (known.has(last.name))
|
|
685
|
+
throw new Error('Duplicate free var name ' + last + ' in lambda expression');
|
|
686
|
+
known.add(last.name);
|
|
687
|
+
|
|
688
|
+
// TODO keep track of arity to speed up execution
|
|
689
|
+
impl = new Lambda(last, impl);
|
|
690
|
+
}
|
|
691
|
+
arg = my;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
super();
|
|
695
|
+
|
|
696
|
+
// localize argument variable as it may appear elsewhere
|
|
697
|
+
const local = new FreeVar(arg.name);
|
|
698
|
+
this.arg = local;
|
|
699
|
+
this.impl = impl.subst(arg, local) ?? impl;
|
|
700
|
+
this.arity = 1;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
getSymbols () {
|
|
704
|
+
const out = this.impl.getSymbols();
|
|
705
|
+
out.delete(this.arg);
|
|
706
|
+
out.set(Expr.lambdaPlaceholder, (out.get(Expr.lambdaPlaceholder) ?? 0) + 1);
|
|
707
|
+
return out;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
weight () {
|
|
711
|
+
return this.impl.weight() + 1;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
reduce (input) {
|
|
715
|
+
if (input.length === 0)
|
|
716
|
+
return null;
|
|
717
|
+
|
|
718
|
+
const [head, ...tail] = input;
|
|
719
|
+
|
|
720
|
+
return (this.impl.subst(this.arg, head) ?? this.impl).apply(...tail);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
subst (plug, value) {
|
|
724
|
+
if (plug === this.arg)
|
|
725
|
+
return null;
|
|
726
|
+
const change = this.impl.subst(plug, value);
|
|
727
|
+
if (change)
|
|
728
|
+
return new Lambda(this.arg, change);
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
expand () {
|
|
733
|
+
return new Lambda(this.arg, this.impl.expand());
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
renameVars (seq) {
|
|
737
|
+
const arg = new FreeVar(seq.next().value);
|
|
738
|
+
const impl = this.impl.subst(this.arg, arg) ?? this.impl;
|
|
739
|
+
return new Lambda(arg, impl.renameVars(seq));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
_rski (options) {
|
|
743
|
+
const impl = this.impl._rski(options);
|
|
744
|
+
if (options.steps >= options.max)
|
|
745
|
+
return new Lambda(this.arg, impl);
|
|
746
|
+
options.steps++;
|
|
747
|
+
if (impl === this.arg)
|
|
748
|
+
return native.I;
|
|
749
|
+
if (!impl.getSymbols().has(this.arg))
|
|
750
|
+
return native.K.apply(impl);
|
|
751
|
+
if (impl instanceof App) {
|
|
752
|
+
const [fst, snd] = impl.split();
|
|
753
|
+
// try eta reduction
|
|
754
|
+
if (snd === this.arg && !fst.getSymbols().has(this.arg))
|
|
755
|
+
return fst._rski(options);
|
|
756
|
+
// fall back to S
|
|
757
|
+
return native.S.apply(
|
|
758
|
+
(new Lambda(this.arg, fst))._rski(options),
|
|
759
|
+
(new Lambda(this.arg, snd))._rski(options)
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
throw new Error('Don\'t know how to convert to SKI' + this);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
equals (other) {
|
|
766
|
+
if (!(other instanceof Lambda))
|
|
767
|
+
return false;
|
|
768
|
+
|
|
769
|
+
const t = new FreeVar('t');
|
|
770
|
+
|
|
771
|
+
return other.reduce([t]).equals(this.reduce([t]));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
contains (other) {
|
|
775
|
+
return this.equals(other) || this.impl.contains(other);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
toString (opt = {}) {
|
|
779
|
+
const mapsto = opt.html ? ' ↦ ' : '->';
|
|
780
|
+
return this.arg.toString(opt) + mapsto + this.impl.toString(opt);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
needsParens () {
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
class Church extends Native {
|
|
789
|
+
constructor (n) {
|
|
790
|
+
const p = Number.parseInt(n);
|
|
791
|
+
if (!(p >= 0))
|
|
792
|
+
throw new Error('Church number must be a non-negative integer');
|
|
793
|
+
const name = '' + p;
|
|
794
|
+
const impl = x => y => {
|
|
795
|
+
let expr = y;
|
|
796
|
+
for (let i = p; i-- > 0; )
|
|
797
|
+
expr = x.apply(expr);
|
|
798
|
+
return expr;
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
super(name, impl, { arity: 2, canonize: false, note: name });
|
|
802
|
+
|
|
803
|
+
this.n = p;
|
|
804
|
+
this.arity = 2;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
equals (other) {
|
|
808
|
+
if (other instanceof Church)
|
|
809
|
+
return this.n === other.n;
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
class Alias extends Named {
|
|
815
|
+
/**
|
|
816
|
+
* @desc An existing expression under a different name.
|
|
817
|
+
* @param {String} name
|
|
818
|
+
* @param {Expr} impl
|
|
819
|
+
* @param {{canonize: boolean?, max: number?, maxArgs: number?, note: string?, terminal: boolean?}} [options]
|
|
820
|
+
*/
|
|
821
|
+
constructor (name, impl, options = {}) {
|
|
822
|
+
super(name);
|
|
823
|
+
this.impl = impl;
|
|
824
|
+
|
|
825
|
+
if (options.note)
|
|
826
|
+
this.note = options.note;
|
|
827
|
+
|
|
828
|
+
const guess = options.canonize
|
|
829
|
+
? impl.canonize({ max: options.max, maxArgs: options.maxArgs })
|
|
830
|
+
: { found: false };
|
|
831
|
+
this.arity = (guess.found && guess.proper && guess.arity) || 0;
|
|
832
|
+
this.proper = guess.proper ?? false;
|
|
833
|
+
this.terminal = options.terminal ?? this.proper;
|
|
834
|
+
this.canonical = guess.canonical;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
getSymbols () {
|
|
838
|
+
return this.terminal ? new Map([[this, 1]]) : this.impl.getSymbols();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
weight () {
|
|
842
|
+
return this.terminal ? 1 : this.impl.weight();
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
expand () {
|
|
846
|
+
return this.impl.expand();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
subst (plug, value) {
|
|
850
|
+
return this.impl.subst(plug, value);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
*
|
|
855
|
+
* @return {{expr: Expr, steps: number}}
|
|
856
|
+
*/
|
|
857
|
+
step () {
|
|
858
|
+
// arity known = waiting for args to expand
|
|
859
|
+
if (this.arity > 0)
|
|
860
|
+
return { expr: this, steps: 0, changed: false };
|
|
861
|
+
// expanding is a change but it takes 0 steps
|
|
862
|
+
return { expr: this.impl, steps: 0, changed: true };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
reduce (args) {
|
|
866
|
+
if (args.length < this.arity)
|
|
867
|
+
return null;
|
|
868
|
+
return this.impl.apply(...args);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
wantsArgs () {
|
|
872
|
+
return this.impl.wantsArgs();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
equals (other) {
|
|
876
|
+
return other.equals(this.impl);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
contains (other) {
|
|
880
|
+
return this.impl.contains(other);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
_rski (options) {
|
|
884
|
+
return this.impl._rski(options);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
toString (opt) {
|
|
888
|
+
return this.outdated ? this.impl.toString(opt) : super.toString(opt);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
needsParens () {
|
|
892
|
+
return this.outdated ? this.impl.needsParens() : false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// declare native combinators
|
|
897
|
+
addNative('I', x => x);
|
|
898
|
+
addNative('K', x => _ => x);
|
|
899
|
+
addNative('S', x => y => z => x.apply(z, y.apply(z)));
|
|
900
|
+
addNative('B', x => y => z => x.apply(y.apply(z)));
|
|
901
|
+
addNative('C', x => y => z => x.apply(z).apply(y));
|
|
902
|
+
addNative('W', x => y => x.apply(y).apply(y));
|
|
903
|
+
|
|
904
|
+
addNative('+', x => y => z => y.apply(x.apply(y, z)), {
|
|
905
|
+
note: '<var>n</var> ↦ <var>n</var> + 1 <i>or</i> SB',
|
|
906
|
+
apply: arg => arg instanceof Church ? new Church(arg.n + 1) : null
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
function maybeLambda (args, expr) {
|
|
910
|
+
if (args.length === 0)
|
|
911
|
+
return expr;
|
|
912
|
+
return new Lambda(args, expr);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function naiveCanonize (expr) {
|
|
916
|
+
if (expr instanceof App)
|
|
917
|
+
return naiveCanonize(expr.fun).apply(...expr.args.map(naiveCanonize));
|
|
918
|
+
|
|
919
|
+
if (expr instanceof Lambda)
|
|
920
|
+
return new Lambda(expr.arg, naiveCanonize(expr.impl));
|
|
921
|
+
|
|
922
|
+
if (expr instanceof Alias)
|
|
923
|
+
return naiveCanonize(expr.impl);
|
|
924
|
+
|
|
925
|
+
const canon = expr.canonize();
|
|
926
|
+
if (canon.canonical)
|
|
927
|
+
return canon.canonical;
|
|
928
|
+
|
|
929
|
+
throw new Error('Failed to canonize expression: ' + expr);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
*
|
|
934
|
+
* @param {Expr} expr
|
|
935
|
+
* @param {{max: number?, maxArgs: number?}} options
|
|
936
|
+
* @param {number} maxWeight
|
|
937
|
+
* @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
|
|
938
|
+
*/
|
|
939
|
+
function * simplifyLambda (expr, options = {}, maxWeight = Infinity) {
|
|
940
|
+
// expr is a lambda, free variable, or an application thereof
|
|
941
|
+
// we want to find an equivalent lambda term with less weight
|
|
942
|
+
// which we do sequentially from leaves to the root of the AST
|
|
943
|
+
|
|
944
|
+
// short-circuit
|
|
945
|
+
if (expr.freeOnly()) {
|
|
946
|
+
if (expr.weight() < maxWeight)
|
|
947
|
+
yield { expr, steps: 0, comment: 'only free vars' };
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
let steps = 0;
|
|
952
|
+
let savedSteps = 0;
|
|
953
|
+
|
|
954
|
+
// fun * arg Descartes product
|
|
955
|
+
if (expr instanceof App) {
|
|
956
|
+
// try to split into fun+arg, then try canonization but exposing each step
|
|
957
|
+
const [fun, arg] = expr.split();
|
|
958
|
+
|
|
959
|
+
for (const term of simplifyLambda(fun, options, maxWeight - 1)) {
|
|
960
|
+
const candidate = term.expr.apply(arg);
|
|
961
|
+
steps = savedSteps + term.steps;
|
|
962
|
+
if (candidate.weight() < maxWeight) {
|
|
963
|
+
maxWeight = candidate.weight();
|
|
964
|
+
yield { expr: candidate, steps: term.steps, comment: term.comment + '(app)' };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
savedSteps = steps;
|
|
968
|
+
|
|
969
|
+
for (const term of simplifyLambda(arg, options, maxWeight - 1)) {
|
|
970
|
+
const candidate = fun.apply(term.expr);
|
|
971
|
+
steps = savedSteps + term.steps;
|
|
972
|
+
if (candidate.weight() < maxWeight) {
|
|
973
|
+
maxWeight = candidate.weight();
|
|
974
|
+
yield { expr: candidate, steps: term.steps, comment: term.comment + '(app)' };
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
savedSteps = steps;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (expr instanceof Lambda) {
|
|
981
|
+
for (const term of simplifyLambda(expr.impl, options, maxWeight - 1)) {
|
|
982
|
+
const candidate = new Lambda(expr.arg, term.expr);
|
|
983
|
+
if (candidate.weight() < maxWeight) {
|
|
984
|
+
maxWeight = candidate.weight();
|
|
985
|
+
steps = savedSteps + term.steps;
|
|
986
|
+
yield { expr: candidate, steps: term.steps, comment: term.comment + '(lambda)' };
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
savedSteps = steps;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const canon = expr.canonize({ max: options.max, maxArgs: options.maxArgs });
|
|
993
|
+
if (canon.canonical && canon.canonical.weight() < maxWeight) {
|
|
994
|
+
maxWeight = canon.canonical.weight();
|
|
995
|
+
yield { expr: canon.canonical, steps: savedSteps + canon.steps, comment: 'canonical' };
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// A global value meaning "lambda is used somewhere in this expression"
|
|
1000
|
+
// Can't be used (at least for now) to construct lambda expressions, or anything at all.
|
|
1001
|
+
// See also getSymbols().
|
|
1002
|
+
Expr.lambdaPlaceholder = new Native('->', x => x, {
|
|
1003
|
+
arity: 1,
|
|
1004
|
+
canonize: false,
|
|
1005
|
+
note: 'Lambda placeholder',
|
|
1006
|
+
apply: x => { throw new Error('Attempt to use a placeholder in expression') }
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church, globalOptions, native };
|