@dallaylaen/ski-interpreter 2.0.0 → 2.2.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 +43 -0
- package/README.md +1 -1
- package/bin/ski.js +96 -97
- package/lib/ski-interpreter.cjs.js +2047 -0
- package/lib/ski-interpreter.cjs.js.map +7 -0
- package/lib/ski-interpreter.esm.js +2052 -0
- package/lib/ski-interpreter.esm.js.map +7 -0
- package/package.json +13 -6
- package/types/index.d.ts +3 -7
- package/types/{lib → src}/expr.d.ts +146 -48
- package/types/src/extras.d.ts +58 -0
- package/types/src/internal.d.ts +52 -0
- package/types/{lib → src}/parser.d.ts +11 -4
- package/types/{lib → src}/quest.d.ts +56 -39
- package/index.js +0 -8
- package/lib/expr.js +0 -1316
- package/lib/parser.js +0 -418
- package/lib/quest.js +0 -401
- package/lib/util.js +0 -57
- package/types/lib/util.d.ts +0 -11
package/lib/expr.js
DELETED
|
@@ -1,1316 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const DEFAULTS = {
|
|
4
|
-
max: 1000,
|
|
5
|
-
maxArgs: 32,
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {Expr | function(Expr): Partial} Partial
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
class Expr {
|
|
13
|
-
/**
|
|
14
|
-
* @descr A generic combinatory logic expression.
|
|
15
|
-
*/
|
|
16
|
-
constructor () {
|
|
17
|
-
if (new.target === Expr)
|
|
18
|
-
throw new Error('Attempt to instantiate abstract class Expr');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @desc apply self to zero or more terms and return the resulting term,
|
|
23
|
-
* without performing any calculations whatsoever
|
|
24
|
-
* @param {Expr} args
|
|
25
|
-
* @return {Expr}
|
|
26
|
-
*/
|
|
27
|
-
apply (...args) {
|
|
28
|
-
return args.length > 0 ? new App(this, ...args) : this;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* expand all terms but don't perform any calculations
|
|
33
|
-
* @return {Expr}
|
|
34
|
-
*/
|
|
35
|
-
expand () {
|
|
36
|
-
return this;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
freeOnly () {
|
|
40
|
-
return !this.any(e => !(e instanceof FreeVar || e instanceof App));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @desc Traverse the expression tree, applying change() to each node.
|
|
45
|
-
* If change() returns an Expr, the node is replaced with that value.
|
|
46
|
-
* Otherwise, the node is left descended further (if applicable)
|
|
47
|
-
* or left unchanged.
|
|
48
|
-
*
|
|
49
|
-
* Returns null if no changes were made, or the new expression otherwise.
|
|
50
|
-
*
|
|
51
|
-
* @param {(e:Expr) => (Expr|null)} change
|
|
52
|
-
* @returns {Expr|null}
|
|
53
|
-
*/
|
|
54
|
-
traverse (change) {
|
|
55
|
-
return change(this);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @desc Returns true if predicate() is true for any subterm of the expression, false otherwise.
|
|
60
|
-
*
|
|
61
|
-
* @param {(e: Expr) => boolean} predicate
|
|
62
|
-
* @returns {boolean}
|
|
63
|
-
*/
|
|
64
|
-
any (predicate) {
|
|
65
|
-
return predicate(this);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @desc rought estimate of the complexity of the term
|
|
70
|
-
* @return {number}
|
|
71
|
-
*/
|
|
72
|
-
weight () {
|
|
73
|
-
return 1;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @desc Try to empirically find an equivalent lambda term for the expression,
|
|
78
|
-
* returning also the term's arity and some other properties.
|
|
79
|
-
*
|
|
80
|
-
* This is used internally when declaring a Native / Alias term,
|
|
81
|
-
* unless {canonize: false} is used.
|
|
82
|
-
*
|
|
83
|
-
* As of current it only recognizes terms that have a normal form,
|
|
84
|
-
* perhaps after adding some variables. This may change in the future.
|
|
85
|
-
*
|
|
86
|
-
* Use toLambda() if you want to get a lambda term in any case.
|
|
87
|
-
*
|
|
88
|
-
* @param {{max: number?, maxArgs: number?}} options
|
|
89
|
-
* @return {{
|
|
90
|
-
* normal: boolean,
|
|
91
|
-
* steps: number,
|
|
92
|
-
* expr?: Expr,
|
|
93
|
-
* arity?: number,
|
|
94
|
-
* proper?: boolean,
|
|
95
|
-
* discard?: boolean,
|
|
96
|
-
* duplicate?: boolean,
|
|
97
|
-
* skip?: Set<number>,
|
|
98
|
-
* dup?: Set<number>,
|
|
99
|
-
* }}
|
|
100
|
-
*/
|
|
101
|
-
infer (options = {}) {
|
|
102
|
-
const max = options.max ?? DEFAULTS.max;
|
|
103
|
-
const maxArgs = options.maxArgs ?? DEFAULTS.maxArgs;
|
|
104
|
-
const out = this._infer({ max, maxArgs, index: 0 });
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
_infer (options, preArgs = [], steps = 0) {
|
|
109
|
-
if (preArgs.length > options.maxArgs || steps > options.max)
|
|
110
|
-
return { normal: false, steps };
|
|
111
|
-
|
|
112
|
-
// happy case
|
|
113
|
-
if (this.freeOnly()) {
|
|
114
|
-
return {
|
|
115
|
-
normal: true,
|
|
116
|
-
steps,
|
|
117
|
-
...maybeLambda(preArgs, this),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// try reaching the normal form
|
|
122
|
-
const next = this.run({ max: (options.max - steps) / 3 });
|
|
123
|
-
steps += next.steps;
|
|
124
|
-
if (!next.final)
|
|
125
|
-
return { normal: false, steps };
|
|
126
|
-
|
|
127
|
-
// normal form != this, redo exercise
|
|
128
|
-
if (next.steps !== 0)
|
|
129
|
-
return next.expr._infer(options, preArgs, steps);
|
|
130
|
-
|
|
131
|
-
// adding more args won't help, bail out
|
|
132
|
-
// if we're an App, the App's _infer will take care of further args
|
|
133
|
-
if (this._firstVar())
|
|
134
|
-
return { normal: false, steps };
|
|
135
|
-
|
|
136
|
-
// try adding more arguments, maybe we'll get a normal form then
|
|
137
|
-
const push = nthvar(preArgs.length + options.index);
|
|
138
|
-
return this.apply(push)._infer(options, [...preArgs, push], steps);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
_aslist () {
|
|
142
|
-
// currently only used by infer() but may be useful
|
|
143
|
-
// to convert binary App trees to n-ary or smth
|
|
144
|
-
return [this];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
_firstVar () {
|
|
148
|
-
// boolean, whether the expression starts with a free variable
|
|
149
|
-
// only used by infer() as a shortcut to this._aslist()[0] instanceof FreeVar
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* @desc Returns a series of lambda terms equivalent to the given expression,
|
|
155
|
-
* up to the provided computation steps limit,
|
|
156
|
-
* in decreasing weight order.
|
|
157
|
-
*
|
|
158
|
-
* Unlike infer(), this method will always return something,
|
|
159
|
-
* even if the expression has no normal form.
|
|
160
|
-
*
|
|
161
|
-
* See also Expr.walk() and Expr.toSKI().
|
|
162
|
-
*
|
|
163
|
-
* @param {{
|
|
164
|
-
* max?: number,
|
|
165
|
-
* maxArgs?: number,
|
|
166
|
-
* varGen?: function(void): FreeVar,
|
|
167
|
-
* steps?: number,
|
|
168
|
-
* html?: boolean,
|
|
169
|
-
* latin?: number,
|
|
170
|
-
* }} options
|
|
171
|
-
* @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
|
|
172
|
-
* @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
|
|
173
|
-
*/
|
|
174
|
-
* toLambda (options = {}) {
|
|
175
|
-
const expr = naiveCanonize(this, options);
|
|
176
|
-
yield * simplifyLambda(expr, options);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* @desc Rewrite the expression into S, K, and I combinators step by step.
|
|
181
|
-
* Returns an iterator yielding the intermediate expressions,
|
|
182
|
-
* along with the number of steps taken to reach them.
|
|
183
|
-
*
|
|
184
|
-
* See also Expr.walk() and Expr.toLambda().
|
|
185
|
-
*
|
|
186
|
-
* @param {{max: number?}} options
|
|
187
|
-
* @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
|
|
188
|
-
*/
|
|
189
|
-
* toSKI (options = {}) {
|
|
190
|
-
// TODO options.max is not actually max, it's the number of steps in one iteration
|
|
191
|
-
let steps = 0;
|
|
192
|
-
let expr = this;
|
|
193
|
-
while (true) {
|
|
194
|
-
const opt = { max: options.max ?? 1, steps: 0 };
|
|
195
|
-
const next = expr._rski(opt);
|
|
196
|
-
const final = opt.steps === 0;
|
|
197
|
-
yield { expr, steps, final };
|
|
198
|
-
if (final)
|
|
199
|
-
break;
|
|
200
|
-
expr = next;
|
|
201
|
-
steps += opt.steps;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
_rski (options) {
|
|
206
|
-
return this;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Replace all instances of plug in the expression with value and return the resulting expression,
|
|
211
|
-
* or null if no changes could be made.
|
|
212
|
-
* Lambda terms and applications will never match if used as plug
|
|
213
|
-
* as they are impossible co compare without extensive computations.
|
|
214
|
-
* Typically used on variables but can also be applied to other terms, e.g. aliases.
|
|
215
|
-
* See also Expr.traverse().
|
|
216
|
-
* @param {Expr} search
|
|
217
|
-
* @param {Expr} replace
|
|
218
|
-
* @return {Expr|null}
|
|
219
|
-
*/
|
|
220
|
-
subst (search, replace) {
|
|
221
|
-
return this === search ? replace : null;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* @desc Apply term reduction rules, if any, to the given argument.
|
|
226
|
-
* A returned value of null means no reduction is possible.
|
|
227
|
-
* A returned value of Expr means the reduction is complete and the application
|
|
228
|
-
* of this and arg can be replaced with the result.
|
|
229
|
-
* A returned value of a function means that further arguments are needed,
|
|
230
|
-
* and can be cached for when they arrive.
|
|
231
|
-
*
|
|
232
|
-
* This method is between apply() which merely glues terms together,
|
|
233
|
-
* and step() which reduces the whole expression.
|
|
234
|
-
*
|
|
235
|
-
* foo.invoke(bar) is what happens inside foo.apply(bar).step() before
|
|
236
|
-
* reduction of either foo or bar is attempted.
|
|
237
|
-
*
|
|
238
|
-
* The name 'invoke' was chosen to avoid confusion with either 'apply' or 'reduce'.
|
|
239
|
-
*
|
|
240
|
-
* @param {Expr} arg
|
|
241
|
-
* @returns {Partial | null}
|
|
242
|
-
*/
|
|
243
|
-
invoke (arg) {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* @desc iterate one step of a calculation.
|
|
249
|
-
* @return {{expr: Expr, steps: number, changed: boolean}}
|
|
250
|
-
*/
|
|
251
|
-
step () { return { expr: this, steps: 0, changed: false } }
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* @desc Run uninterrupted sequence of step() applications
|
|
255
|
-
* until the expression is irreducible, or max number of steps is reached.
|
|
256
|
-
* Default number of steps = 1000.
|
|
257
|
-
* @param {{max: number?, steps: number?, throw: boolean?}|Expr} [opt]
|
|
258
|
-
* @param {Expr} args
|
|
259
|
-
* @return {{expr: Expr, steps: number, final: boolean}}
|
|
260
|
-
*/
|
|
261
|
-
run (opt = {}, ...args) {
|
|
262
|
-
if (opt instanceof Expr) {
|
|
263
|
-
args.unshift(opt);
|
|
264
|
-
opt = {};
|
|
265
|
-
}
|
|
266
|
-
let expr = args ? this.apply(...args) : this;
|
|
267
|
-
let steps = opt.steps ?? 0;
|
|
268
|
-
// make sure we make at least 1 step, to tell whether we've reached the normal form
|
|
269
|
-
const max = Math.max(opt.max ?? DEFAULTS.max, 1) + steps;
|
|
270
|
-
let final = false;
|
|
271
|
-
for (; steps < max; ) {
|
|
272
|
-
const next = expr.step();
|
|
273
|
-
if (!next.changed) {
|
|
274
|
-
final = true;
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
steps += next.steps;
|
|
278
|
-
expr = next.expr;
|
|
279
|
-
}
|
|
280
|
-
if (opt.throw && !final)
|
|
281
|
-
throw new Error('Failed to compute expression in ' + max + ' steps');
|
|
282
|
-
return { final, steps, expr };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Execute step() while possible, yielding a brief description of events after each step.
|
|
287
|
-
* Mnemonics: like run() but slower.
|
|
288
|
-
* @param {{max: number?}} options
|
|
289
|
-
* @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
|
|
290
|
-
*/
|
|
291
|
-
* walk (options = {}) {
|
|
292
|
-
const max = options.max ?? Infinity;
|
|
293
|
-
let steps = 0;
|
|
294
|
-
let expr = this;
|
|
295
|
-
let final = false;
|
|
296
|
-
|
|
297
|
-
while (steps < max) {
|
|
298
|
-
// 1. calculate
|
|
299
|
-
// 2. yield _unchanged_ expression
|
|
300
|
-
// 3. either advance or stop
|
|
301
|
-
const next = expr.step();
|
|
302
|
-
if (!next.changed)
|
|
303
|
-
final = true;
|
|
304
|
-
yield { expr, steps, final };
|
|
305
|
-
if (final)
|
|
306
|
-
break;
|
|
307
|
-
steps += next.steps;
|
|
308
|
-
expr = next.expr;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* @desc True is the expressions are identical, false otherwise.
|
|
314
|
-
* Aliases are expanded.
|
|
315
|
-
* Bound variables in lambda terms are renamed consistently.
|
|
316
|
-
* However, no reductions are attempted.
|
|
317
|
-
*
|
|
318
|
-
* E.g. a->b->a == x->y->x is true, but a->b->a == K is false.
|
|
319
|
-
*
|
|
320
|
-
* @param {Expr} other
|
|
321
|
-
* @return {boolean}
|
|
322
|
-
*/
|
|
323
|
-
equals (other) {
|
|
324
|
-
return !this.diff(other);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* @desc Recursively compare two expressions and return a string
|
|
329
|
-
* describing the first point of difference.
|
|
330
|
-
* Returns null if expressions are identical.
|
|
331
|
-
*
|
|
332
|
-
* Aliases are expanded.
|
|
333
|
-
* Bound variables in lambda terms are renamed consistently.
|
|
334
|
-
* However, no reductions are attempted.
|
|
335
|
-
*
|
|
336
|
-
* Members of the FreeVar class are considered different
|
|
337
|
-
* even if they have the same name, unless they are the same object.
|
|
338
|
-
* To somewhat alleviate confusion, the output will include
|
|
339
|
-
* the internal id of the variable in square brackets.
|
|
340
|
-
*
|
|
341
|
-
* @example "K(S != I)" is the result of comparing "KS" and "KI"
|
|
342
|
-
* @example "S(K([x[13] != x[14]]))K"
|
|
343
|
-
*
|
|
344
|
-
* @param {Expr} other
|
|
345
|
-
* @param {boolean} [swap] If true, the order of expressions is reversed in the output.
|
|
346
|
-
* @returns {string|null}
|
|
347
|
-
*/
|
|
348
|
-
diff (other, swap = false) {
|
|
349
|
-
if (this === other)
|
|
350
|
-
return null;
|
|
351
|
-
if (other instanceof Alias)
|
|
352
|
-
return other.impl.diff(this, !swap);
|
|
353
|
-
return swap
|
|
354
|
-
? '[' + other + ' != ' + this + ']'
|
|
355
|
-
: '[' + this + ' != ' + other + ']';
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* @desc Assert expression equality. Can be used in tests.
|
|
360
|
-
* @param {Expr} expected
|
|
361
|
-
* @param {string} comment
|
|
362
|
-
*/
|
|
363
|
-
expect (expected, comment = '') {
|
|
364
|
-
comment = comment ? comment + ': ' : '';
|
|
365
|
-
if (!(expected instanceof Expr))
|
|
366
|
-
throw new Error(comment + 'attempt to expect a combinator to equal something else: ' + expected);
|
|
367
|
-
const diff = this.diff(expected);
|
|
368
|
-
if (!diff)
|
|
369
|
-
return; // all good
|
|
370
|
-
|
|
371
|
-
// TODO wanna use AssertionError but webpack doesn't recognize it
|
|
372
|
-
// still the below hack works for mocha-based tests.
|
|
373
|
-
const poorMans = new Error(comment + diff);
|
|
374
|
-
poorMans.expected = expected + '';
|
|
375
|
-
poorMans.actual = this + '';
|
|
376
|
-
throw poorMans;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* @desc Returns string representation of the expression.
|
|
381
|
-
* Same as format() without options.
|
|
382
|
-
* @return {string}
|
|
383
|
-
*/
|
|
384
|
-
toString () {
|
|
385
|
-
return this.format();
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* @desc Whether the expression needs parentheses when printed.
|
|
390
|
-
* @param {boolean} [first] - whether this is the first term in a sequence
|
|
391
|
-
* @return {boolean}
|
|
392
|
-
*/
|
|
393
|
-
_braced (first) {
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
_unspaced (arg) {
|
|
398
|
-
return this._braced(true);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* @desc Stringify the expression with fancy formatting options.
|
|
403
|
-
* Said options mostly include wrappers around various constructs in form of ['(', ')'],
|
|
404
|
-
* as well as terse and html flags that set up the defaults.
|
|
405
|
-
* Format without options is equivalent to toString() and can be parsed back.
|
|
406
|
-
*
|
|
407
|
-
* @param {Object} [options] - formatting options
|
|
408
|
-
* @param {boolean} [options.terse] - whether to use terse formatting (omitting unnecessary spaces and parentheses)
|
|
409
|
-
* @param {boolean} [options.html] - whether to default to HTML tags & entities.
|
|
410
|
-
* If a named term has fancyName property set, it will be used instead of name in this mode.
|
|
411
|
-
* @param {[string, string]} [options.brackets] - wrappers for application arguments, typically ['(', ')']
|
|
412
|
-
* @param {[string, string]} [options.var] - wrappers for variable names
|
|
413
|
-
* (will default to <var> and </var> in html mode).
|
|
414
|
-
* @param {[string, string, string]} [options.lambda] - wrappers for lambda abstractions, e.g. ['λ', '.', '']
|
|
415
|
-
* where the middle string is placed between argument and body
|
|
416
|
-
* default is ['', '->', ''] or ['', '->', ''] for html
|
|
417
|
-
* @param {[string, string]} [options.around] - wrappers around (sub-)expressions.
|
|
418
|
-
* individual applications will not be wrapped, i.e. (a b c) but not ((a b) c)
|
|
419
|
-
* @param {[string, string]} [options.redex] - wrappers around the starting term(s) that have enough arguments to be reduced
|
|
420
|
-
* @param {Object<string, Expr>} [options.inventory] - if given, output aliases in the set as their names
|
|
421
|
-
* and any other aliases as the expansion of their definitions.
|
|
422
|
-
* The default is a cryptic and fragile mechanism dependent on a hidden mutable property.
|
|
423
|
-
* @returns {string}
|
|
424
|
-
*
|
|
425
|
-
* @example foo.format() // equivalent to foo.toString()
|
|
426
|
-
* @example foo.format({terse: false}) // spell out all parentheses
|
|
427
|
-
* @example foo.format({html: true}) // use HTML tags and entities
|
|
428
|
-
* @example foo.format({ around: ['(', ')'], brackets: ['', ''], lambda: ['(', '->', ')'] }) // lisp style, still back-parsable
|
|
429
|
-
* @exapmle foo.format({ lambda: ['λ', '.', ''] }) // pretty-print for the math department
|
|
430
|
-
* @example foo.format({ lambda: ['', '=>', ''], terse: false }) // make it javascript
|
|
431
|
-
* @example foo.format({ inventory: { T } }) // use T as a named term, expand all others
|
|
432
|
-
*
|
|
433
|
-
*/
|
|
434
|
-
|
|
435
|
-
format (options = {}) {
|
|
436
|
-
const fallback = options.html
|
|
437
|
-
? {
|
|
438
|
-
brackets: ['(', ')'],
|
|
439
|
-
space: ' ',
|
|
440
|
-
var: ['<var>', '</var>'],
|
|
441
|
-
lambda: ['', '->', ''],
|
|
442
|
-
around: ['', ''],
|
|
443
|
-
redex: ['', ''],
|
|
444
|
-
}
|
|
445
|
-
: {
|
|
446
|
-
brackets: ['(', ')'],
|
|
447
|
-
space: ' ',
|
|
448
|
-
var: ['', ''],
|
|
449
|
-
lambda: ['', '->', ''],
|
|
450
|
-
around: ['', ''],
|
|
451
|
-
redex: ['', ''],
|
|
452
|
-
}
|
|
453
|
-
return this._format({
|
|
454
|
-
terse: options.terse ?? true,
|
|
455
|
-
brackets: options.brackets ?? fallback.brackets,
|
|
456
|
-
space: options.space ?? fallback.space,
|
|
457
|
-
var: options.var ?? fallback.var,
|
|
458
|
-
lambda: options.lambda ?? fallback.lambda,
|
|
459
|
-
around: options.around ?? fallback.around,
|
|
460
|
-
redex: options.redex ?? fallback.redex,
|
|
461
|
-
inventory: options.inventory, // TODO better name
|
|
462
|
-
html: options.html ?? false,
|
|
463
|
-
}, 0);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
_format (options, nargs) {
|
|
467
|
-
throw new Error( 'No _format() method defined in class ' + this.constructor.name );
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// output: string[] /* appended */, inventory: { [key: string]: Expr }, seen: Set<Expr>
|
|
471
|
-
_declare (output, inventory, seen) {}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
class App extends Expr {
|
|
475
|
-
/**
|
|
476
|
-
* @desc Application of fun() to args.
|
|
477
|
-
* Never ever use new App(fun, ...args) directly, use fun.apply(...args) instead.
|
|
478
|
-
* @param {Expr} fun
|
|
479
|
-
* @param {Expr} args
|
|
480
|
-
*/
|
|
481
|
-
constructor (fun, ...args) {
|
|
482
|
-
if (args.length === 0)
|
|
483
|
-
throw new Error('Attempt to create an application with no arguments (likely interpreter bug)');
|
|
484
|
-
super();
|
|
485
|
-
|
|
486
|
-
this.arg = args.pop();
|
|
487
|
-
this.fun = args.length ? new App(fun, ...args) : fun;
|
|
488
|
-
this.final = false;
|
|
489
|
-
this.arity = this.fun.arity > 0 ? this.fun.arity - 1 : 0;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
weight () {
|
|
493
|
-
return this.fun.weight() + this.arg.weight();
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
_infer (options, preArgs = [], steps = 0) {
|
|
497
|
-
if (preArgs.length > options.maxArgs || steps > options.max)
|
|
498
|
-
return { normal: false, steps };
|
|
499
|
-
|
|
500
|
-
/*
|
|
501
|
-
* inside and App there are 3 main possibilities:
|
|
502
|
-
* 1) The parent infer() actually is able to do the job. Then we just proxy the result.
|
|
503
|
-
* 2) Both `fun` and `arg` form good enough lambda terms. Then lump them together & return.
|
|
504
|
-
* 3) We literally have no idea, so we just pick the shortest defined term from the above.
|
|
505
|
-
*/
|
|
506
|
-
|
|
507
|
-
const proxy = super._infer(options, preArgs, steps);
|
|
508
|
-
if (proxy.normal)
|
|
509
|
-
return proxy;
|
|
510
|
-
steps = proxy.steps; // reimport extra iterations
|
|
511
|
-
|
|
512
|
-
const [first, ...list] = this._aslist();
|
|
513
|
-
if (!(first instanceof FreeVar))
|
|
514
|
-
return { normal: false, steps }
|
|
515
|
-
// TODO maybe do it later
|
|
516
|
-
|
|
517
|
-
let discard = false;
|
|
518
|
-
let duplicate = false;
|
|
519
|
-
const out = [];
|
|
520
|
-
for (const term of list) {
|
|
521
|
-
const guess = term._infer({
|
|
522
|
-
...options,
|
|
523
|
-
maxArgs: options.maxArgs - preArgs.length,
|
|
524
|
-
max: options.max - steps,
|
|
525
|
-
index: preArgs.length + options.index,
|
|
526
|
-
});
|
|
527
|
-
steps += guess.steps;
|
|
528
|
-
if (!guess.normal)
|
|
529
|
-
return { normal: false, steps };
|
|
530
|
-
out.push(guess.expr);
|
|
531
|
-
discard = discard || guess.discard;
|
|
532
|
-
duplicate = duplicate || guess.duplicate;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return {
|
|
536
|
-
normal: true,
|
|
537
|
-
steps,
|
|
538
|
-
...maybeLambda(preArgs, new App(first, ...out), {
|
|
539
|
-
discard,
|
|
540
|
-
duplicate,
|
|
541
|
-
}),
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
_firstVar () {
|
|
546
|
-
return this.fun._firstVar();
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
expand () {
|
|
550
|
-
return this.fun.expand().apply(this.arg.expand());
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
traverse (change) {
|
|
554
|
-
const replaced = change(this);
|
|
555
|
-
if (replaced instanceof Expr)
|
|
556
|
-
return replaced;
|
|
557
|
-
|
|
558
|
-
const fun = this.fun.traverse(change);
|
|
559
|
-
const arg = this.arg.traverse(change);
|
|
560
|
-
|
|
561
|
-
if (!fun && !arg)
|
|
562
|
-
return null; // no changes
|
|
563
|
-
|
|
564
|
-
return (fun ?? this.fun).apply(arg ?? this.arg);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
any (predicate) {
|
|
568
|
-
return predicate(this) || this.fun.any(predicate) || this.arg.any(predicate);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
subst (search, replace) {
|
|
572
|
-
const fun = this.fun.subst(search, replace);
|
|
573
|
-
const arg = this.arg.subst(search, replace);
|
|
574
|
-
|
|
575
|
-
return (fun || arg) ? (fun ?? this.fun).apply(arg ?? this.arg) : null;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* @return {{expr: Expr, steps: number}}
|
|
580
|
-
*/
|
|
581
|
-
|
|
582
|
-
step () {
|
|
583
|
-
// normal reduction order: first try root, then at most 1 step
|
|
584
|
-
if (!this.final) {
|
|
585
|
-
// try to apply rewriting rules, if applicable, at first
|
|
586
|
-
const partial = this.fun.invoke(this.arg);
|
|
587
|
-
if (partial instanceof Expr)
|
|
588
|
-
return { expr: partial, steps: 1, changed: true };
|
|
589
|
-
else if (typeof partial === 'function')
|
|
590
|
-
this.invoke = partial; // cache for next time
|
|
591
|
-
|
|
592
|
-
// descend into the leftmost term
|
|
593
|
-
const fun = this.fun.step();
|
|
594
|
-
if (fun.changed)
|
|
595
|
-
return { expr: fun.expr.apply(this.arg), steps: fun.steps, changed: true };
|
|
596
|
-
|
|
597
|
-
// descend into arg
|
|
598
|
-
const arg = this.arg.step();
|
|
599
|
-
if (arg.changed)
|
|
600
|
-
return { expr: this.fun.apply(arg.expr), steps: arg.steps, changed: true };
|
|
601
|
-
|
|
602
|
-
// mark as irreducible
|
|
603
|
-
this.final = true; // mark as irreducible at root
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
return { expr: this, steps: 0, changed: false };
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
invoke (arg) {
|
|
610
|
-
// propagate invocation towards the root term,
|
|
611
|
-
// caching partial applications as we go
|
|
612
|
-
const partial = this.fun.invoke(this.arg);
|
|
613
|
-
if (partial instanceof Expr)
|
|
614
|
-
return partial.apply(arg);
|
|
615
|
-
else if (typeof partial === 'function') {
|
|
616
|
-
this.invoke = partial;
|
|
617
|
-
return partial(arg);
|
|
618
|
-
} else {
|
|
619
|
-
// invoke = null => we're uncomputable, cache for next time
|
|
620
|
-
this.invoke = _ => null;
|
|
621
|
-
return null;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
split () {
|
|
626
|
-
// leftover from array-based older design
|
|
627
|
-
return [this.fun, this.arg];
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
_aslist () {
|
|
631
|
-
return [...this.fun._aslist(), this.arg];
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* @desc Convert the expression to SKI combinatory logic
|
|
636
|
-
* @return {Expr}
|
|
637
|
-
*/
|
|
638
|
-
|
|
639
|
-
_rski (options) {
|
|
640
|
-
if (options.steps >= options.max)
|
|
641
|
-
return this;
|
|
642
|
-
return this.fun._rski(options).apply(this.arg._rski(options));
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
diff (other, swap = false) {
|
|
646
|
-
if (!(other instanceof App))
|
|
647
|
-
return super.diff(other, swap);
|
|
648
|
-
|
|
649
|
-
const fun = this.fun.diff(other.fun, swap);
|
|
650
|
-
if (fun)
|
|
651
|
-
return fun + '(...)';
|
|
652
|
-
const arg = this.arg.diff(other.arg, swap);
|
|
653
|
-
if (arg)
|
|
654
|
-
return this.fun + '(' + arg + ')';
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
_braced (first) {
|
|
659
|
-
return !first;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
_format (options, nargs) {
|
|
663
|
-
const fun = this.fun._format(options, nargs + 1);
|
|
664
|
-
const arg = this.arg._format(options, 0);
|
|
665
|
-
const wrap = nargs ? ['', ''] : options.around;
|
|
666
|
-
// TODO ignore terse for now
|
|
667
|
-
if (options.terse && !this.arg._braced(false))
|
|
668
|
-
return wrap[0] + fun + (this.fun._unspaced(this.arg) ? '' : options.space) + arg + wrap[1];
|
|
669
|
-
else
|
|
670
|
-
return wrap[0] + fun + options.brackets[0] + arg + options.brackets[1] + wrap[1];
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
_declare (output, inventory, seen) {
|
|
674
|
-
this.fun._declare(output, inventory, seen);
|
|
675
|
-
this.arg._declare(output, inventory, seen);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
_unspaced (arg) {
|
|
679
|
-
return this.arg._braced(false) ? true : this.arg._unspaced(arg);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
class Named extends Expr {
|
|
684
|
-
/**
|
|
685
|
-
* @desc An abstract class representing a term named 'name'.
|
|
686
|
-
*
|
|
687
|
-
* @param {String} name
|
|
688
|
-
*/
|
|
689
|
-
constructor (name) {
|
|
690
|
-
super();
|
|
691
|
-
if (typeof name !== 'string' || name.length === 0)
|
|
692
|
-
throw new Error('Attempt to create a named term with improper name');
|
|
693
|
-
this.name = name;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
_unspaced (arg) {
|
|
697
|
-
return !!(
|
|
698
|
-
(arg instanceof Named) && (
|
|
699
|
-
(this.name.match(/^[A-Z+]$/) && arg.name.match(/^[a-z+]/i))
|
|
700
|
-
|| (this.name.match(/^[a-z+]/i) && arg.name.match(/^[A-Z+]$/))
|
|
701
|
-
)
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
_format (options, nargs) {
|
|
706
|
-
// NOTE fancyName is not yet official and may change name or meaning
|
|
707
|
-
const name = options.html ? this.fancyName ?? this.name : this.name;
|
|
708
|
-
return this.arity > 0 && this.arity <= nargs
|
|
709
|
-
? options.redex[0] + name + options.redex[1]
|
|
710
|
-
: name;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
let freeId = 0;
|
|
715
|
-
|
|
716
|
-
class FreeVar extends Named {
|
|
717
|
-
/**
|
|
718
|
-
* @desc A named variable.
|
|
719
|
-
*
|
|
720
|
-
* Given the functional nature of combinatory logic, variables are treated
|
|
721
|
-
* as functions that we don't know how to evaluate just yet.
|
|
722
|
-
*
|
|
723
|
-
* By default, two different variables even with the same name are considered different.
|
|
724
|
-
* They display it via a hidden id property.
|
|
725
|
-
*
|
|
726
|
-
* If a scope object is given, however, two variables with the same name and scope
|
|
727
|
-
* are considered identical.
|
|
728
|
-
*
|
|
729
|
-
* @param {string} name - name of the variable
|
|
730
|
-
* @param {any} scope - an object representing where the variable belongs to.
|
|
731
|
-
*/
|
|
732
|
-
constructor (name, scope) {
|
|
733
|
-
super(name);
|
|
734
|
-
this.id = ++freeId;
|
|
735
|
-
// TODO temp compatibility mode
|
|
736
|
-
this.scope = scope === undefined ? this : scope;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
weight () {
|
|
740
|
-
return 0;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
_firstVar () {
|
|
744
|
-
return true;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
diff (other, swap = false) {
|
|
748
|
-
if (!(other instanceof FreeVar))
|
|
749
|
-
return super.diff(other, swap);
|
|
750
|
-
if (this.name === other.name && this.scope === other.scope)
|
|
751
|
-
return null;
|
|
752
|
-
const lhs = this.name + '[' + this.id + ']';
|
|
753
|
-
const rhs = other.name + '[' + other.id + ']';
|
|
754
|
-
return swap
|
|
755
|
-
? '[' + rhs + ' != ' + lhs + ']'
|
|
756
|
-
: '[' + lhs + ' != ' + rhs + ']';
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
subst (search, replace) {
|
|
760
|
-
if (search instanceof FreeVar && search.name === this.name && search.scope === this.scope)
|
|
761
|
-
return replace;
|
|
762
|
-
return null;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
_format (options, nargs) {
|
|
766
|
-
const name = options.html ? this.fancyName ?? this.name : this.name;
|
|
767
|
-
return options.var[0] + name + options.var[1];
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
class Native extends Named {
|
|
772
|
-
/**
|
|
773
|
-
* @desc A named term with a known rewriting rule.
|
|
774
|
-
* 'impl' is a function with signature Expr => Expr => ... => Expr
|
|
775
|
-
* (see typedef Partial).
|
|
776
|
-
* This is how S, K, I, and company are implemented.
|
|
777
|
-
*
|
|
778
|
-
* Note that as of current something like a=>b=>b(a) is not possible,
|
|
779
|
-
* use full form instead: a=>b=>b.apply(a).
|
|
780
|
-
*
|
|
781
|
-
* @example new Native('K', x => y => x); // constant
|
|
782
|
-
* @example new Native('Y', function(f) { return f.apply(this.apply(f)); }); // self-application
|
|
783
|
-
*
|
|
784
|
-
* @param {String} name
|
|
785
|
-
* @param {Partial} impl
|
|
786
|
-
* @param {{note?: string, arity?: number, canonize?: boolean, apply?: function(Expr):(Expr|null) }} [opt]
|
|
787
|
-
*/
|
|
788
|
-
constructor (name, impl, opt = {}) {
|
|
789
|
-
super(name);
|
|
790
|
-
// setup essentials
|
|
791
|
-
this.invoke = impl;
|
|
792
|
-
|
|
793
|
-
// TODO infer lazily (on demand, only once); app capabilities such as discard and duplicate
|
|
794
|
-
// try to bootstrap and infer some of our properties
|
|
795
|
-
const guess = (opt.canonize ?? true) ? this.infer() : { normal: false };
|
|
796
|
-
|
|
797
|
-
this.arity = opt.arity || guess.arity || 1;
|
|
798
|
-
this.note = opt.note ?? guess.expr?.format({ terse: true, html: true, lambda: ['', ' ↦ ', ''] });
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
_rski (options) {
|
|
802
|
-
if (this === native.I || this === native.K || this === native.S || (options.steps >= options.max))
|
|
803
|
-
return this;
|
|
804
|
-
const canon = this.infer().expr;
|
|
805
|
-
if (!canon)
|
|
806
|
-
return this;
|
|
807
|
-
options.steps++;
|
|
808
|
-
return canon._rski(options);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// predefined global combinator list
|
|
813
|
-
// it is required by toSKI method, otherwise it could've as well be in parse.js
|
|
814
|
-
/**
|
|
815
|
-
* @type {{[key: string]: Native}}
|
|
816
|
-
*/
|
|
817
|
-
const native = {};
|
|
818
|
-
function addNative (name, impl, opt) {
|
|
819
|
-
native[name] = new Native(name, impl, opt);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
class Lambda extends Expr {
|
|
823
|
-
/**
|
|
824
|
-
* @desc Lambda abstraction of arg over impl.
|
|
825
|
-
* Upon evaluation, all occurrences of 'arg' within 'impl' will be replaced
|
|
826
|
-
* with the provided argument.
|
|
827
|
-
*
|
|
828
|
-
* Note that 'arg' will be replaced by a localized placeholder, so the original
|
|
829
|
-
* variable can be used elsewhere without interference.
|
|
830
|
-
* Listing symbols contained in the lambda will omit such placeholder.
|
|
831
|
-
*
|
|
832
|
-
* Legacy ([FreeVar], impl) constructor is supported but deprecated.
|
|
833
|
-
* It will create a nested lambda expression.
|
|
834
|
-
*
|
|
835
|
-
* @param {FreeVar} arg
|
|
836
|
-
* @param {Expr} impl
|
|
837
|
-
*/
|
|
838
|
-
constructor (arg, impl) {
|
|
839
|
-
if (Array.isArray(arg)) {
|
|
840
|
-
// check args before everything
|
|
841
|
-
if (arg.length === 0)
|
|
842
|
-
throw new Error('empty argument list in lambda constructor');
|
|
843
|
-
|
|
844
|
-
const [my, ...tail] = arg;
|
|
845
|
-
const known = new Set([my.name]);
|
|
846
|
-
|
|
847
|
-
while (tail.length > 0) {
|
|
848
|
-
const last = tail.pop();
|
|
849
|
-
if (known.has(last.name))
|
|
850
|
-
throw new Error('Duplicate free var name ' + last + ' in lambda expression');
|
|
851
|
-
known.add(last.name);
|
|
852
|
-
|
|
853
|
-
// TODO keep track of arity to speed up execution
|
|
854
|
-
impl = new Lambda(last, impl);
|
|
855
|
-
}
|
|
856
|
-
arg = my;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
super();
|
|
860
|
-
|
|
861
|
-
// localize argument variable and bind it to oneself
|
|
862
|
-
const local = new FreeVar(arg.name, this);
|
|
863
|
-
this.arg = local;
|
|
864
|
-
this.impl = impl.subst(arg, local) ?? impl;
|
|
865
|
-
this.arity = 1;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
weight () {
|
|
869
|
-
return this.impl.weight() + 1;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
_infer (options, preArgs = [], steps = 0) {
|
|
873
|
-
if (preArgs.length > options.maxArgs)
|
|
874
|
-
return { normal: false, steps };
|
|
875
|
-
|
|
876
|
-
const push = nthvar(preArgs.length + options.index);
|
|
877
|
-
return this.invoke(push)._infer(options, [...preArgs, push], steps + 1);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
invoke (arg) {
|
|
881
|
-
return this.impl.subst(this.arg, arg) ?? this.impl;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
traverse (change) {
|
|
885
|
-
const replaced = change(this);
|
|
886
|
-
if (replaced instanceof Expr)
|
|
887
|
-
return replaced;
|
|
888
|
-
|
|
889
|
-
// alas no proper shielding of self.arg is possible
|
|
890
|
-
const impl = this.impl.traverse(change);
|
|
891
|
-
|
|
892
|
-
if (!impl)
|
|
893
|
-
return null; // no changes
|
|
894
|
-
|
|
895
|
-
return new Lambda(this.arg, impl);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
any (predicate) {
|
|
899
|
-
return predicate(this) || this.impl.any(predicate);
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
subst (search, replace) {
|
|
903
|
-
if (search === this.arg)
|
|
904
|
-
return null;
|
|
905
|
-
const change = this.impl.subst(search, replace);
|
|
906
|
-
return change ? new Lambda(this.arg, change) : null;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
expand () {
|
|
910
|
-
return new Lambda(this.arg, this.impl.expand());
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
_rski (options) {
|
|
914
|
-
const impl = this.impl._rski(options);
|
|
915
|
-
if (options.steps >= options.max)
|
|
916
|
-
return new Lambda(this.arg, impl);
|
|
917
|
-
options.steps++;
|
|
918
|
-
if (impl === this.arg)
|
|
919
|
-
return native.I;
|
|
920
|
-
if (!impl.any(e => e === this.arg))
|
|
921
|
-
return native.K.apply(impl);
|
|
922
|
-
if (impl instanceof App) {
|
|
923
|
-
const [fst, snd] = impl.split();
|
|
924
|
-
// try eta reduction
|
|
925
|
-
if (snd === this.arg && !fst.any(e => e === this.arg))
|
|
926
|
-
return fst._rski(options);
|
|
927
|
-
// fall back to S
|
|
928
|
-
return native.S.apply(
|
|
929
|
-
(new Lambda(this.arg, fst))._rski(options),
|
|
930
|
-
(new Lambda(this.arg, snd))._rski(options)
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
throw new Error('Don\'t know how to convert to SKI' + this);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
diff (other, swap = false) {
|
|
937
|
-
if (!(other instanceof Lambda))
|
|
938
|
-
return super.diff(other, swap);
|
|
939
|
-
|
|
940
|
-
const t = new FreeVar('t'); // TODO better placeholder name
|
|
941
|
-
|
|
942
|
-
const diff = this.invoke(t).diff(other.invoke(t), swap);
|
|
943
|
-
if (diff)
|
|
944
|
-
return '(t->' + diff + ')'; // parentheses required to avoid ambiguity
|
|
945
|
-
return null;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
_format (options, nargs) {
|
|
949
|
-
return (nargs > 0 ? options.brackets[0] : '')
|
|
950
|
-
+ options.lambda[0]
|
|
951
|
-
+ this.arg._format(options, 0) // TODO highlight redex if nargs > 0
|
|
952
|
-
+ options.lambda[1]
|
|
953
|
-
+ this.impl._format(options, 0) + options.lambda[2]
|
|
954
|
-
+ (nargs > 0 ? options.brackets[1] : '');
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
_declare (output, inventory, seen) {
|
|
958
|
-
this.impl._declare(output, inventory, seen);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
_braced (first) {
|
|
962
|
-
return true;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
class Church extends Native {
|
|
967
|
-
/**
|
|
968
|
-
* @desc Church numeral representing non-negative integer n:
|
|
969
|
-
* n f x = f(f(...(f x)...)) with f applied n times.
|
|
970
|
-
* @param {number} n
|
|
971
|
-
*/
|
|
972
|
-
constructor (n) {
|
|
973
|
-
const p = Number.parseInt(n);
|
|
974
|
-
if (!(p >= 0))
|
|
975
|
-
throw new Error('Church number must be a non-negative integer');
|
|
976
|
-
const name = '' + p;
|
|
977
|
-
const impl = x => y => {
|
|
978
|
-
let expr = y;
|
|
979
|
-
for (let i = p; i-- > 0; )
|
|
980
|
-
expr = x.apply(expr);
|
|
981
|
-
return expr;
|
|
982
|
-
};
|
|
983
|
-
|
|
984
|
-
super(name, impl, { arity: 2, canonize: false, note: name });
|
|
985
|
-
|
|
986
|
-
this.n = p;
|
|
987
|
-
this.arity = 2;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
diff (other, swap = false) {
|
|
991
|
-
if (!(other instanceof Church))
|
|
992
|
-
return super.diff(other, swap);
|
|
993
|
-
if (this.n === other.n)
|
|
994
|
-
return null;
|
|
995
|
-
return swap
|
|
996
|
-
? '[' + other.n + ' != ' + this.n + ']'
|
|
997
|
-
: '[' + this.n + ' != ' + other.n + ']';
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
_unspaced (arg) {
|
|
1001
|
-
return false;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
function waitn (expr, n) {
|
|
1006
|
-
return arg => n <= 1 ? expr.apply(arg) : waitn(expr.apply(arg), n - 1);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
class Alias extends Named {
|
|
1010
|
-
/**
|
|
1011
|
-
* @desc A named alias for an existing expression.
|
|
1012
|
-
*
|
|
1013
|
-
* Upon evaluation, the alias expands into the original expression,
|
|
1014
|
-
* unless it has a known arity > 0 and is marked terminal,
|
|
1015
|
-
* in which case it waits for enough arguments before expanding.
|
|
1016
|
-
*
|
|
1017
|
-
* A hidden mutable property 'outdated' is used to silently
|
|
1018
|
-
* replace the alias with its definition in all contexts.
|
|
1019
|
-
* This is used when declaring named terms in an interpreter,
|
|
1020
|
-
* to avoid confusion between old and new terms with the same name.
|
|
1021
|
-
*
|
|
1022
|
-
* @param {String} name
|
|
1023
|
-
* @param {Expr} impl
|
|
1024
|
-
* @param {{canonize: boolean?, max: number?, maxArgs: number?, note: string?, terminal: boolean?}} [options]
|
|
1025
|
-
*/
|
|
1026
|
-
constructor (name, impl, options = {}) {
|
|
1027
|
-
super(name);
|
|
1028
|
-
this.impl = impl;
|
|
1029
|
-
|
|
1030
|
-
if (options.note)
|
|
1031
|
-
this.note = options.note;
|
|
1032
|
-
|
|
1033
|
-
const guess = options.canonize
|
|
1034
|
-
? impl.infer({ max: options.max, maxArgs: options.maxArgs })
|
|
1035
|
-
: { normal: false };
|
|
1036
|
-
this.arity = (guess.proper && guess.arity) || 0;
|
|
1037
|
-
this.proper = guess.proper ?? false;
|
|
1038
|
-
this.terminal = options.terminal ?? this.proper;
|
|
1039
|
-
this.canonical = guess.expr;
|
|
1040
|
-
this.invoke = waitn(impl, this.arity);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
weight () {
|
|
1044
|
-
return this.terminal ? 1 : this.impl.weight();
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
expand () {
|
|
1048
|
-
return this.impl.expand();
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
traverse (change) {
|
|
1052
|
-
return change(this) ?? this.impl.traverse(change);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
any (predicate) {
|
|
1056
|
-
return predicate(this) || this.impl.any(predicate);
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
subst (search, replace) {
|
|
1060
|
-
if (this === search)
|
|
1061
|
-
return replace;
|
|
1062
|
-
return this.impl.subst(search, replace);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
_infer (options, preArgs = [], steps = 0) {
|
|
1066
|
-
return this.impl._infer(options, preArgs, steps);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
*
|
|
1071
|
-
* @return {{expr: Expr, steps: number}}
|
|
1072
|
-
*/
|
|
1073
|
-
step () {
|
|
1074
|
-
// arity known = waiting for args to expand
|
|
1075
|
-
if (this.arity > 0)
|
|
1076
|
-
return { expr: this, steps: 0, changed: false };
|
|
1077
|
-
// expanding is a change but it takes 0 steps
|
|
1078
|
-
return { expr: this.impl, steps: 0, changed: true };
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
_firstVar () {
|
|
1082
|
-
return this.impl._firstVar();
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
diff (other, swap = false) {
|
|
1086
|
-
if (this === other)
|
|
1087
|
-
return null;
|
|
1088
|
-
return other.diff(this.impl, !swap);
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
_rski (options) {
|
|
1092
|
-
return this.impl._rski(options);
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
_braced (first) {
|
|
1096
|
-
return this.outdated ? this.impl._braced(first) : false;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
_format (options, nargs) {
|
|
1100
|
-
const outdated = options.inventory
|
|
1101
|
-
? options.inventory[this.name] !== this
|
|
1102
|
-
: this.outdated;
|
|
1103
|
-
return outdated ? this.impl._format(options, nargs) : super._format(options, nargs);
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
_declare (output, inventory, seen) {
|
|
1107
|
-
// this is part of the 'declare' function, see below
|
|
1108
|
-
// only once
|
|
1109
|
-
if (seen.has(this))
|
|
1110
|
-
return;
|
|
1111
|
-
seen.add(this);
|
|
1112
|
-
|
|
1113
|
-
// topological order
|
|
1114
|
-
this.impl._declare(output, inventory, seen);
|
|
1115
|
-
|
|
1116
|
-
// only declare if in inventory and matches
|
|
1117
|
-
if (inventory[this.name] === this)
|
|
1118
|
-
output.push(this.name + '=' + this.impl.format({ terse: true, inventory }));
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
// ----- Expr* classes end here -----
|
|
1123
|
-
|
|
1124
|
-
// declare native combinators
|
|
1125
|
-
addNative('I', x => x);
|
|
1126
|
-
addNative('K', x => _ => x);
|
|
1127
|
-
addNative('S', x => y => z => x.apply(z, y.apply(z)));
|
|
1128
|
-
addNative('B', x => y => z => x.apply(y.apply(z)));
|
|
1129
|
-
addNative('C', x => y => z => x.apply(z).apply(y));
|
|
1130
|
-
addNative('W', x => y => x.apply(y).apply(y));
|
|
1131
|
-
|
|
1132
|
-
addNative(
|
|
1133
|
-
'+',
|
|
1134
|
-
n => n instanceof Church
|
|
1135
|
-
? new Church(n.n + 1)
|
|
1136
|
-
: f => x => f.apply(n.apply(f, x)),
|
|
1137
|
-
{
|
|
1138
|
-
note: 'Increase a Church numeral argument by 1, otherwise n => f => x => f(n f x)',
|
|
1139
|
-
}
|
|
1140
|
-
);
|
|
1141
|
-
|
|
1142
|
-
// utility functions dependent on Expr* classes, in alphabetical order
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
*
|
|
1146
|
-
* @param {Expr[]} inventory
|
|
1147
|
-
* @return {string[]}
|
|
1148
|
-
*/
|
|
1149
|
-
function declare (inventory) {
|
|
1150
|
-
const misnamed = Object.keys(inventory)
|
|
1151
|
-
.filter(s => !(inventory[s] instanceof Named && inventory[s].name === s))
|
|
1152
|
-
.map(s => s + ' = ' + inventory[s]);
|
|
1153
|
-
if (misnamed.length > 0)
|
|
1154
|
-
throw new Error('Inventory must be a hash of named terms with matching names: ' + misnamed.join(', '));
|
|
1155
|
-
|
|
1156
|
-
inventory = { ...inventory }; // shallow copy to avoid mutating input
|
|
1157
|
-
|
|
1158
|
-
// If any aliases mask native terms, those cannot be easily restored.
|
|
1159
|
-
// Moreover, subsequent terms may refer to both native term and and the conflicting alias.
|
|
1160
|
-
// Therefore, we will instead rename such aliases to something else
|
|
1161
|
-
// and only restore them at the end.
|
|
1162
|
-
const detour = [];
|
|
1163
|
-
let tmpId = 1;
|
|
1164
|
-
for (const name in native) {
|
|
1165
|
-
if (!(inventory[name] instanceof Alias))
|
|
1166
|
-
continue;
|
|
1167
|
-
while ('temp' + tmpId in inventory)
|
|
1168
|
-
tmpId++;
|
|
1169
|
-
const temp = 'temp' + tmpId;
|
|
1170
|
-
const orig = inventory[name];
|
|
1171
|
-
delete inventory[name];
|
|
1172
|
-
const masked = new Alias(temp, orig);
|
|
1173
|
-
for (const key in inventory)
|
|
1174
|
-
inventory[key] = inventory[key].subst(orig, masked) ?? inventory[key];
|
|
1175
|
-
|
|
1176
|
-
inventory[temp] = masked;
|
|
1177
|
-
detour.push([name, temp]);
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// only want to declare aliases
|
|
1181
|
-
const terms = Object.values(inventory)
|
|
1182
|
-
.filter(s => s instanceof Alias)
|
|
1183
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1184
|
-
|
|
1185
|
-
const out = [];
|
|
1186
|
-
const seen = new Set();
|
|
1187
|
-
for (const term of terms)
|
|
1188
|
-
term._declare(out, inventory, seen);
|
|
1189
|
-
|
|
1190
|
-
for (const [name, temp] of detour) {
|
|
1191
|
-
out.push(name + '=' + temp); // rename
|
|
1192
|
-
out.push(temp + '='); // delete
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
return out;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
function maybeLambda (args, expr, caps = {}) {
|
|
1199
|
-
const count = new Array(args.length).fill(0);
|
|
1200
|
-
let proper = true;
|
|
1201
|
-
expr.traverse(e => {
|
|
1202
|
-
if (e instanceof FreeVar) {
|
|
1203
|
-
const index = args.findIndex(a => a.name === e.name);
|
|
1204
|
-
if (index >= 0) {
|
|
1205
|
-
count[index]++;
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
if (!(e instanceof App))
|
|
1210
|
-
proper = false;
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
const skip = new Set();
|
|
1214
|
-
const dup = new Set();
|
|
1215
|
-
for (let i = 0; i < args.length; i++) {
|
|
1216
|
-
if (count[i] === 0)
|
|
1217
|
-
skip.add(i);
|
|
1218
|
-
else if (count[i] > 1)
|
|
1219
|
-
dup.add(i);
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
return {
|
|
1223
|
-
expr: args.length ? new Lambda(args, expr) : expr,
|
|
1224
|
-
...(caps.synth ? {} : { arity: args.length }),
|
|
1225
|
-
...(skip.size ? { skip } : {}),
|
|
1226
|
-
...(dup.size ? { dup } : {}),
|
|
1227
|
-
duplicate: !!dup.size || caps.duplicate || false,
|
|
1228
|
-
discard: !!skip.size || caps.discard || false,
|
|
1229
|
-
proper,
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
function naiveCanonize (expr) {
|
|
1234
|
-
if (expr instanceof App)
|
|
1235
|
-
return naiveCanonize(expr.fun).apply(naiveCanonize(expr.arg));
|
|
1236
|
-
|
|
1237
|
-
if (expr instanceof Lambda)
|
|
1238
|
-
return new Lambda(expr.arg, naiveCanonize(expr.impl));
|
|
1239
|
-
|
|
1240
|
-
if (expr instanceof Alias)
|
|
1241
|
-
return naiveCanonize(expr.impl);
|
|
1242
|
-
|
|
1243
|
-
const canon = expr.infer();
|
|
1244
|
-
if (canon.expr)
|
|
1245
|
-
return canon.expr;
|
|
1246
|
-
|
|
1247
|
-
throw new Error('Failed to canonize expression: ' + expr);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
function nthvar (n) {
|
|
1251
|
-
return new FreeVar('abcdefgh'[n] ?? 'x' + n);
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
/**
|
|
1255
|
-
*
|
|
1256
|
-
* @param {Expr} expr
|
|
1257
|
-
* @param {{max: number?, maxArgs: number?}} options
|
|
1258
|
-
* @param {number} maxWeight
|
|
1259
|
-
* @yields {{expr: Expr, steps: number?, comment: string?}}
|
|
1260
|
-
*/
|
|
1261
|
-
function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
|
|
1262
|
-
// expr is a lambda, free variable, or an application thereof
|
|
1263
|
-
// we want to find an equivalent lambda term with less weight
|
|
1264
|
-
// which we do sequentially from leaves to the root of the AST
|
|
1265
|
-
|
|
1266
|
-
yield { expr, steps: state.steps, comment: '(self)' };
|
|
1267
|
-
|
|
1268
|
-
// short-circuit
|
|
1269
|
-
if (expr.freeOnly())
|
|
1270
|
-
return;
|
|
1271
|
-
|
|
1272
|
-
let maxWeight = expr.weight();
|
|
1273
|
-
|
|
1274
|
-
if (expr instanceof Lambda) {
|
|
1275
|
-
for (const term of simplifyLambda(expr.impl, options, state)) {
|
|
1276
|
-
const candidate = new Lambda(expr.arg, term.expr);
|
|
1277
|
-
if (candidate.weight() < maxWeight) {
|
|
1278
|
-
maxWeight = candidate.weight();
|
|
1279
|
-
yield { expr: candidate, steps: state.steps, comment: '(lambda)' + term.comment };
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
// fun * arg Descartes product
|
|
1285
|
-
if (expr instanceof App) {
|
|
1286
|
-
// try to split into fun+arg, then try canonization but exposing each step
|
|
1287
|
-
let [fun, arg] = expr.split();
|
|
1288
|
-
|
|
1289
|
-
for (const term of simplifyLambda(fun, options, state)) {
|
|
1290
|
-
const candidate = term.expr.apply(arg);
|
|
1291
|
-
if (candidate.weight() < maxWeight) {
|
|
1292
|
-
maxWeight = candidate.weight();
|
|
1293
|
-
fun = term.expr;
|
|
1294
|
-
yield { expr: candidate, steps: state.steps, comment: '(fun)' + term.comment };
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
for (const term of simplifyLambda(arg, options, state)) {
|
|
1299
|
-
const candidate = fun.apply(term.expr);
|
|
1300
|
-
if (candidate.weight() < maxWeight) {
|
|
1301
|
-
maxWeight = candidate.weight();
|
|
1302
|
-
yield { expr: candidate, steps: state.steps, comment: '(arg)' + term.comment };
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
const canon = expr.infer({ max: options.max, maxArgs: options.maxArgs });
|
|
1308
|
-
state.steps += canon.steps;
|
|
1309
|
-
if (canon.expr && canon.expr.weight() < maxWeight)
|
|
1310
|
-
yield { expr: canon.expr, steps: state.steps, comment: '(canonical)' };
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
Expr.declare = declare;
|
|
1314
|
-
Expr.native = native;
|
|
1315
|
-
|
|
1316
|
-
module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church };
|