@dallaylaen/ski-interpreter 1.2.0 → 2.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/CHANGELOG.md +48 -0
- package/README.md +138 -49
- package/lib/expr.js +407 -325
- package/lib/parser.js +94 -43
- package/lib/quest.js +63 -29
- package/lib/util.js +1 -22
- package/package.json +3 -2
- package/types/lib/expr.d.ts +210 -118
- package/types/lib/parser.d.ts +76 -48
- package/types/lib/quest.d.ts +58 -25
- package/types/lib/util.d.ts +0 -2
package/lib/expr.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const globalOptions = {
|
|
6
|
-
terse: true,
|
|
3
|
+
const DEFAULTS = {
|
|
7
4
|
max: 1000,
|
|
8
5
|
maxArgs: 32,
|
|
9
6
|
};
|
|
10
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Expr | function(Expr): Partial} Partial
|
|
10
|
+
*/
|
|
11
|
+
|
|
11
12
|
class Expr {
|
|
12
13
|
/**
|
|
13
14
|
* @descr A generic combinatory logic expression.
|
|
@@ -18,103 +19,50 @@ class Expr {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @desc apply self to zero or more terms and return the resulting term,
|
|
30
|
-
* without performing any calculations whatsoever
|
|
31
|
-
* @param {Expr} args
|
|
32
|
-
* @return {Expr}
|
|
33
|
-
*/
|
|
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
|
+
*/
|
|
34
27
|
apply (...args) {
|
|
35
28
|
return args.length > 0 ? new App(this, ...args) : this;
|
|
36
29
|
}
|
|
37
30
|
|
|
38
31
|
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
* expand all terms but don't perform any calculations
|
|
33
|
+
* @return {Expr}
|
|
34
|
+
*/
|
|
42
35
|
expand () {
|
|
43
36
|
return this;
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
/**
|
|
47
|
-
* @desc return all free variables within the term
|
|
48
|
-
* @return {Set<FreeVar>}
|
|
49
|
-
*/
|
|
50
|
-
freeVars () {
|
|
51
|
-
const symbols = this.getSymbols();
|
|
52
|
-
const out = new Set();
|
|
53
|
-
for (const [key, _] of symbols) {
|
|
54
|
-
if (key instanceof FreeVar)
|
|
55
|
-
out.add(key);
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
hasLambda () {
|
|
61
|
-
const sym = this.getSymbols();
|
|
62
|
-
return sym.has(Expr.lambdaPlaceholder);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
39
|
freeOnly () {
|
|
66
|
-
|
|
67
|
-
if (!(key instanceof FreeVar))
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
return true;
|
|
40
|
+
return !this.any(e => !(e instanceof FreeVar || e instanceof App));
|
|
71
41
|
}
|
|
72
42
|
|
|
73
43
|
/**
|
|
74
|
-
* @desc
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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}
|
|
77
53
|
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return new Map([[this, 1]]);
|
|
54
|
+
traverse (change) {
|
|
55
|
+
return change(this);
|
|
81
56
|
}
|
|
82
57
|
|
|
83
58
|
/**
|
|
84
|
-
*
|
|
85
|
-
* that is equivalent to the first term in pair with the second one.
|
|
86
|
-
* If a simgle term is given, it is duplicated into a pair.
|
|
87
|
-
*
|
|
88
|
-
* @example S(SKK)(SKS).replace('I') = SII // we found 2 subtrees equivalent to I
|
|
89
|
-
* and replaced them with I
|
|
59
|
+
* @desc Returns true if predicate() is true for any subterm of the expression, false otherwise.
|
|
90
60
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @return {Expr}
|
|
61
|
+
* @param {(e: Expr) => boolean} predicate
|
|
62
|
+
* @returns {boolean}
|
|
94
63
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (terms.length === 0)
|
|
98
|
-
return this; // nothing to replace, return self
|
|
99
|
-
for (const entry of terms) {
|
|
100
|
-
const pair = (Array.isArray(entry) ? entry : [entry, entry]);
|
|
101
|
-
pair[0] = pair[0].guess(opt).expr;
|
|
102
|
-
if (!pair[0])
|
|
103
|
-
throw new Error('Failed to canonize term ' + entry);
|
|
104
|
-
if (pair.length !== 2)
|
|
105
|
-
throw new Error('Expected a pair of terms to replace, got ' + entry);
|
|
106
|
-
pairs.push(pair);
|
|
107
|
-
}
|
|
108
|
-
return this._replace(pairs, opt) ?? this;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
_replace (pairs, opt) {
|
|
112
|
-
const check = this.guess(opt).expr;
|
|
113
|
-
for (const [canon, term] of pairs) {
|
|
114
|
-
if (check.equals(canon))
|
|
115
|
-
return term;
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
64
|
+
any (predicate) {
|
|
65
|
+
return predicate(this);
|
|
118
66
|
}
|
|
119
67
|
|
|
120
68
|
/**
|
|
@@ -126,16 +74,16 @@ class Expr {
|
|
|
126
74
|
}
|
|
127
75
|
|
|
128
76
|
/**
|
|
129
|
-
* @desc Try to find an equivalent lambda term for the expression,
|
|
77
|
+
* @desc Try to empirically find an equivalent lambda term for the expression,
|
|
130
78
|
* returning also the term's arity and some other properties.
|
|
131
79
|
*
|
|
132
|
-
* This is used internally when declaring a Native term,
|
|
80
|
+
* This is used internally when declaring a Native / Alias term,
|
|
133
81
|
* unless {canonize: false} is used.
|
|
134
82
|
*
|
|
135
83
|
* As of current it only recognizes terms that have a normal form,
|
|
136
84
|
* perhaps after adding some variables. This may change in the future.
|
|
137
85
|
*
|
|
138
|
-
* Use
|
|
86
|
+
* Use toLambda() if you want to get a lambda term in any case.
|
|
139
87
|
*
|
|
140
88
|
* @param {{max: number?, maxArgs: number?}} options
|
|
141
89
|
* @return {{
|
|
@@ -150,14 +98,14 @@ class Expr {
|
|
|
150
98
|
* dup?: Set<number>,
|
|
151
99
|
* }}
|
|
152
100
|
*/
|
|
153
|
-
|
|
154
|
-
const max = options.max ??
|
|
155
|
-
const maxArgs = options.maxArgs ??
|
|
156
|
-
const out = this.
|
|
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 });
|
|
157
105
|
return out;
|
|
158
106
|
}
|
|
159
107
|
|
|
160
|
-
|
|
108
|
+
_infer (options, preArgs = [], steps = 0) {
|
|
161
109
|
if (preArgs.length > options.maxArgs || steps > options.max)
|
|
162
110
|
return { normal: false, steps };
|
|
163
111
|
|
|
@@ -178,22 +126,27 @@ class Expr {
|
|
|
178
126
|
|
|
179
127
|
// normal form != this, redo exercise
|
|
180
128
|
if (next.steps !== 0)
|
|
181
|
-
return next.expr.
|
|
129
|
+
return next.expr._infer(options, preArgs, steps);
|
|
182
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
|
|
183
133
|
if (this._firstVar())
|
|
184
134
|
return { normal: false, steps };
|
|
185
135
|
|
|
136
|
+
// try adding more arguments, maybe we'll get a normal form then
|
|
186
137
|
const push = nthvar(preArgs.length + options.index);
|
|
187
|
-
return this.apply(push).
|
|
138
|
+
return this.apply(push)._infer(options, [...preArgs, push], steps);
|
|
188
139
|
}
|
|
189
140
|
|
|
190
141
|
_aslist () {
|
|
142
|
+
// currently only used by infer() but may be useful
|
|
143
|
+
// to convert binary App trees to n-ary or smth
|
|
191
144
|
return [this];
|
|
192
145
|
}
|
|
193
146
|
|
|
194
147
|
_firstVar () {
|
|
195
148
|
// boolean, whether the expression starts with a free variable
|
|
196
|
-
// used by
|
|
149
|
+
// only used by infer() as a shortcut to this._aslist()[0] instanceof FreeVar
|
|
197
150
|
return false;
|
|
198
151
|
}
|
|
199
152
|
|
|
@@ -201,6 +154,12 @@ class Expr {
|
|
|
201
154
|
* @desc Returns a series of lambda terms equivalent to the given expression,
|
|
202
155
|
* up to the provided computation steps limit,
|
|
203
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
|
+
*
|
|
204
163
|
* @param {{
|
|
205
164
|
* max?: number,
|
|
206
165
|
* maxArgs?: number,
|
|
@@ -212,17 +171,22 @@ class Expr {
|
|
|
212
171
|
* @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
|
|
213
172
|
* @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
|
|
214
173
|
*/
|
|
215
|
-
*
|
|
174
|
+
* toLambda (options = {}) {
|
|
216
175
|
const expr = naiveCanonize(this, options);
|
|
217
176
|
yield * simplifyLambda(expr, options);
|
|
218
177
|
}
|
|
219
178
|
|
|
220
179
|
/**
|
|
221
|
-
* @desc
|
|
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
|
+
*
|
|
222
186
|
* @param {{max: number?}} options
|
|
223
187
|
* @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
|
|
224
188
|
*/
|
|
225
|
-
*
|
|
189
|
+
* toSKI (options = {}) {
|
|
226
190
|
// TODO options.max is not actually max, it's the number of steps in one iteration
|
|
227
191
|
let steps = 0;
|
|
228
192
|
let expr = this;
|
|
@@ -242,23 +206,13 @@ class Expr {
|
|
|
242
206
|
return this;
|
|
243
207
|
}
|
|
244
208
|
|
|
245
|
-
/**
|
|
246
|
-
* Apply self to list of given args.
|
|
247
|
-
* Normally, only native combinators know how to do it.
|
|
248
|
-
* @param {Expr[]} args
|
|
249
|
-
* @return {Expr|null}
|
|
250
|
-
*/
|
|
251
|
-
reduce (args) {
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
209
|
/**
|
|
256
210
|
* Replace all instances of plug in the expression with value and return the resulting expression,
|
|
257
211
|
* or null if no changes could be made.
|
|
258
212
|
* Lambda terms and applications will never match if used as plug
|
|
259
213
|
* as they are impossible co compare without extensive computations.
|
|
260
214
|
* Typically used on variables but can also be applied to other terms, e.g. aliases.
|
|
261
|
-
* See also Expr.
|
|
215
|
+
* See also Expr.traverse().
|
|
262
216
|
* @param {Expr} search
|
|
263
217
|
* @param {Expr} replace
|
|
264
218
|
* @return {Expr|null}
|
|
@@ -268,19 +222,42 @@ class Expr {
|
|
|
268
222
|
}
|
|
269
223
|
|
|
270
224
|
/**
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
*/
|
|
274
251
|
step () { return { expr: this, steps: 0, changed: false } }
|
|
275
252
|
|
|
276
253
|
/**
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
*/
|
|
284
261
|
run (opt = {}, ...args) {
|
|
285
262
|
if (opt instanceof Expr) {
|
|
286
263
|
args.unshift(opt);
|
|
@@ -289,7 +266,7 @@ class Expr {
|
|
|
289
266
|
let expr = args ? this.apply(...args) : this;
|
|
290
267
|
let steps = opt.steps ?? 0;
|
|
291
268
|
// make sure we make at least 1 step, to tell whether we've reached the normal form
|
|
292
|
-
const max = Math.max(opt.max ??
|
|
269
|
+
const max = Math.max(opt.max ?? DEFAULTS.max, 1) + steps;
|
|
293
270
|
let final = false;
|
|
294
271
|
for (; steps < max; ) {
|
|
295
272
|
const next = expr.step();
|
|
@@ -306,11 +283,11 @@ class Expr {
|
|
|
306
283
|
}
|
|
307
284
|
|
|
308
285
|
/**
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
*/
|
|
314
291
|
* walk (options = {}) {
|
|
315
292
|
const max = options.max ?? Infinity;
|
|
316
293
|
let steps = 0;
|
|
@@ -333,20 +310,49 @@ class Expr {
|
|
|
333
310
|
}
|
|
334
311
|
|
|
335
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.
|
|
336
319
|
*
|
|
337
320
|
* @param {Expr} other
|
|
338
321
|
* @return {boolean}
|
|
339
322
|
*/
|
|
340
323
|
equals (other) {
|
|
341
|
-
|
|
342
|
-
return true;
|
|
343
|
-
if (other instanceof Alias)
|
|
344
|
-
return other.equals(this);
|
|
345
|
-
return false;
|
|
324
|
+
return !this.diff(other);
|
|
346
325
|
}
|
|
347
326
|
|
|
348
|
-
|
|
349
|
-
|
|
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 + ']';
|
|
350
356
|
}
|
|
351
357
|
|
|
352
358
|
/**
|
|
@@ -358,12 +364,13 @@ class Expr {
|
|
|
358
364
|
comment = comment ? comment + ': ' : '';
|
|
359
365
|
if (!(expected instanceof Expr))
|
|
360
366
|
throw new Error(comment + 'attempt to expect a combinator to equal something else: ' + expected);
|
|
361
|
-
|
|
362
|
-
|
|
367
|
+
const diff = this.diff(expected);
|
|
368
|
+
if (!diff)
|
|
369
|
+
return; // all good
|
|
363
370
|
|
|
364
371
|
// TODO wanna use AssertionError but webpack doesn't recognize it
|
|
365
372
|
// still the below hack works for mocha-based tests.
|
|
366
|
-
const poorMans = new Error(comment +
|
|
373
|
+
const poorMans = new Error(comment + diff);
|
|
367
374
|
poorMans.expected = expected + '';
|
|
368
375
|
poorMans.actual = this + '';
|
|
369
376
|
throw poorMans;
|
|
@@ -399,10 +406,11 @@ class Expr {
|
|
|
399
406
|
*
|
|
400
407
|
* @param {Object} [options] - formatting options
|
|
401
408
|
* @param {boolean} [options.terse] - whether to use terse formatting (omitting unnecessary spaces and parentheses)
|
|
402
|
-
* @param {boolean} [options.html] - whether to default to HTML tags & entities
|
|
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.
|
|
403
411
|
* @param {[string, string]} [options.brackets] - wrappers for application arguments, typically ['(', ')']
|
|
404
412
|
* @param {[string, string]} [options.var] - wrappers for variable names
|
|
405
|
-
* (will default to <var> and </var> in html mode)
|
|
413
|
+
* (will default to <var> and </var> in html mode).
|
|
406
414
|
* @param {[string, string, string]} [options.lambda] - wrappers for lambda abstractions, e.g. ['λ', '.', '']
|
|
407
415
|
* where the middle string is placed between argument and body
|
|
408
416
|
* default is ['', '->', ''] or ['', '->', ''] for html
|
|
@@ -425,14 +433,14 @@ class Expr {
|
|
|
425
433
|
*/
|
|
426
434
|
|
|
427
435
|
format (options = {}) {
|
|
428
|
-
const
|
|
436
|
+
const fallback = options.html
|
|
429
437
|
? {
|
|
430
438
|
brackets: ['(', ')'],
|
|
431
439
|
space: ' ',
|
|
432
440
|
var: ['<var>', '</var>'],
|
|
433
441
|
lambda: ['', '->', ''],
|
|
434
442
|
around: ['', ''],
|
|
435
|
-
redex: ['
|
|
443
|
+
redex: ['', ''],
|
|
436
444
|
}
|
|
437
445
|
: {
|
|
438
446
|
brackets: ['(', ')'],
|
|
@@ -443,14 +451,15 @@ class Expr {
|
|
|
443
451
|
redex: ['', ''],
|
|
444
452
|
}
|
|
445
453
|
return this._format({
|
|
446
|
-
terse: options.terse ??
|
|
447
|
-
brackets: options.brackets ??
|
|
448
|
-
space: options.space ??
|
|
449
|
-
var: options.var ??
|
|
450
|
-
lambda: options.lambda ??
|
|
451
|
-
around: options.around ??
|
|
452
|
-
redex: options.redex ??
|
|
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,
|
|
453
461
|
inventory: options.inventory, // TODO better name
|
|
462
|
+
html: options.html ?? false,
|
|
454
463
|
}, 0);
|
|
455
464
|
}
|
|
456
465
|
|
|
@@ -484,25 +493,18 @@ class App extends Expr {
|
|
|
484
493
|
return this.fun.weight() + this.arg.weight();
|
|
485
494
|
}
|
|
486
495
|
|
|
487
|
-
|
|
488
|
-
const out = this.fun.getSymbols();
|
|
489
|
-
for (const [key, value] of this.arg.getSymbols())
|
|
490
|
-
out.set(key, (out.get(key) ?? 0) + value);
|
|
491
|
-
return out;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
_guess (options, preArgs = [], steps = 0) {
|
|
496
|
+
_infer (options, preArgs = [], steps = 0) {
|
|
495
497
|
if (preArgs.length > options.maxArgs || steps > options.max)
|
|
496
498
|
return { normal: false, steps };
|
|
497
499
|
|
|
498
500
|
/*
|
|
499
501
|
* inside and App there are 3 main possibilities:
|
|
500
|
-
* 1) The parent
|
|
502
|
+
* 1) The parent infer() actually is able to do the job. Then we just proxy the result.
|
|
501
503
|
* 2) Both `fun` and `arg` form good enough lambda terms. Then lump them together & return.
|
|
502
504
|
* 3) We literally have no idea, so we just pick the shortest defined term from the above.
|
|
503
505
|
*/
|
|
504
506
|
|
|
505
|
-
const proxy = super.
|
|
507
|
+
const proxy = super._infer(options, preArgs, steps);
|
|
506
508
|
if (proxy.normal)
|
|
507
509
|
return proxy;
|
|
508
510
|
steps = proxy.steps; // reimport extra iterations
|
|
@@ -516,7 +518,7 @@ class App extends Expr {
|
|
|
516
518
|
let duplicate = false;
|
|
517
519
|
const out = [];
|
|
518
520
|
for (const term of list) {
|
|
519
|
-
const guess = term.
|
|
521
|
+
const guess = term._infer({
|
|
520
522
|
...options,
|
|
521
523
|
maxArgs: options.maxArgs - preArgs.length,
|
|
522
524
|
max: options.max - steps,
|
|
@@ -544,22 +546,26 @@ class App extends Expr {
|
|
|
544
546
|
return this.fun._firstVar();
|
|
545
547
|
}
|
|
546
548
|
|
|
547
|
-
apply (...args) {
|
|
548
|
-
if (args.length === 0)
|
|
549
|
-
return this;
|
|
550
|
-
return new App(this, ...args);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
549
|
expand () {
|
|
554
550
|
return this.fun.expand().apply(this.arg.expand());
|
|
555
551
|
}
|
|
556
552
|
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
if (
|
|
560
|
-
return
|
|
561
|
-
|
|
562
|
-
|
|
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);
|
|
563
569
|
}
|
|
564
570
|
|
|
565
571
|
subst (search, replace) {
|
|
@@ -576,34 +582,48 @@ class App extends Expr {
|
|
|
576
582
|
step () {
|
|
577
583
|
// normal reduction order: first try root, then at most 1 step
|
|
578
584
|
if (!this.final) {
|
|
579
|
-
if
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
//
|
|
587
|
-
|
|
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
|
|
588
593
|
const fun = this.fun.step();
|
|
589
594
|
if (fun.changed)
|
|
590
595
|
return { expr: fun.expr.apply(this.arg), steps: fun.steps, changed: true };
|
|
591
596
|
|
|
597
|
+
// descend into arg
|
|
592
598
|
const arg = this.arg.step();
|
|
593
599
|
if (arg.changed)
|
|
594
600
|
return { expr: this.fun.apply(arg.expr), steps: arg.steps, changed: true };
|
|
595
601
|
|
|
596
|
-
|
|
602
|
+
// mark as irreducible
|
|
603
|
+
this.final = true; // mark as irreducible at root
|
|
597
604
|
}
|
|
605
|
+
|
|
598
606
|
return { expr: this, steps: 0, changed: false };
|
|
599
607
|
}
|
|
600
608
|
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
}
|
|
603
623
|
}
|
|
604
624
|
|
|
605
625
|
split () {
|
|
606
|
-
//
|
|
626
|
+
// leftover from array-based older design
|
|
607
627
|
return [this.fun, this.arg];
|
|
608
628
|
}
|
|
609
629
|
|
|
@@ -622,15 +642,17 @@ class App extends Expr {
|
|
|
622
642
|
return this.fun._rski(options).apply(this.arg._rski(options));
|
|
623
643
|
}
|
|
624
644
|
|
|
625
|
-
|
|
645
|
+
diff (other, swap = false) {
|
|
626
646
|
if (!(other instanceof App))
|
|
627
|
-
return super.
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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;
|
|
634
656
|
}
|
|
635
657
|
|
|
636
658
|
_braced (first) {
|
|
@@ -660,9 +682,10 @@ class App extends Expr {
|
|
|
660
682
|
|
|
661
683
|
class Named extends Expr {
|
|
662
684
|
/**
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
685
|
+
* @desc An abstract class representing a term named 'name'.
|
|
686
|
+
*
|
|
687
|
+
* @param {String} name
|
|
688
|
+
*/
|
|
666
689
|
constructor (name) {
|
|
667
690
|
super();
|
|
668
691
|
if (typeof name !== 'string' || name.length === 0)
|
|
@@ -680,18 +703,37 @@ class Named extends Expr {
|
|
|
680
703
|
}
|
|
681
704
|
|
|
682
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;
|
|
683
708
|
return this.arity > 0 && this.arity <= nargs
|
|
684
|
-
? options.redex[0] +
|
|
685
|
-
:
|
|
709
|
+
? options.redex[0] + name + options.redex[1]
|
|
710
|
+
: name;
|
|
686
711
|
}
|
|
687
712
|
}
|
|
688
713
|
|
|
689
714
|
let freeId = 0;
|
|
690
715
|
|
|
691
716
|
class FreeVar extends Named {
|
|
692
|
-
|
|
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) {
|
|
693
733
|
super(name);
|
|
694
734
|
this.id = ++freeId;
|
|
735
|
+
// TODO temp compatibility mode
|
|
736
|
+
this.scope = scope === undefined ? this : scope;
|
|
695
737
|
}
|
|
696
738
|
|
|
697
739
|
weight () {
|
|
@@ -702,82 +744,76 @@ class FreeVar extends Named {
|
|
|
702
744
|
return true;
|
|
703
745
|
}
|
|
704
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
|
+
|
|
705
765
|
_format (options, nargs) {
|
|
706
|
-
|
|
766
|
+
const name = options.html ? this.fancyName ?? this.name : this.name;
|
|
767
|
+
return options.var[0] + name + options.var[1];
|
|
707
768
|
}
|
|
708
769
|
}
|
|
709
770
|
|
|
710
|
-
/**
|
|
711
|
-
* @typedef {function(Expr): Expr | AnyArity} AnyArity
|
|
712
|
-
*/
|
|
713
|
-
|
|
714
771
|
class Native extends Named {
|
|
715
772
|
/**
|
|
716
|
-
* @desc A
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
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
|
+
*
|
|
721
784
|
* @param {String} name
|
|
722
|
-
* @param {
|
|
723
|
-
* @param {{note
|
|
785
|
+
* @param {Partial} impl
|
|
786
|
+
* @param {{note?: string, arity?: number, canonize?: boolean, apply?: function(Expr):(Expr|null) }} [opt]
|
|
724
787
|
*/
|
|
725
788
|
constructor (name, impl, opt = {}) {
|
|
726
789
|
super(name);
|
|
727
790
|
// setup essentials
|
|
728
|
-
this.
|
|
729
|
-
if (opt.apply)
|
|
730
|
-
this.onApply = opt.apply;
|
|
731
|
-
this.arity = opt.arity ?? 1;
|
|
732
|
-
|
|
733
|
-
// try to bootstrap and guess some of our properties
|
|
734
|
-
const guess = (opt.canonize ?? true) ? this.guess() : { normal: false };
|
|
791
|
+
this.invoke = impl;
|
|
735
792
|
|
|
736
|
-
|
|
737
|
-
|
|
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 };
|
|
738
796
|
|
|
797
|
+
this.arity = opt.arity || guess.arity || 1;
|
|
739
798
|
this.note = opt.note ?? guess.expr?.format({ terse: true, html: true, lambda: ['', ' ↦ ', ''] });
|
|
740
799
|
}
|
|
741
800
|
|
|
742
|
-
apply (...args) {
|
|
743
|
-
if (this.onApply && args.length >= 1) {
|
|
744
|
-
if (typeof this.onApply !== 'function') {
|
|
745
|
-
throw new Error('Native combinator ' + this + ' has an invalid onApply property of type'
|
|
746
|
-
+ typeof this.onApply + ': ' + this.onApply);
|
|
747
|
-
}
|
|
748
|
-
const subst = this.onApply(args[0]);
|
|
749
|
-
if (subst instanceof Expr)
|
|
750
|
-
return subst.apply(...args.slice(1));
|
|
751
|
-
}
|
|
752
|
-
return super.apply(...args);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
801
|
_rski (options) {
|
|
756
802
|
if (this === native.I || this === native.K || this === native.S || (options.steps >= options.max))
|
|
757
803
|
return this;
|
|
758
|
-
const canon = this.
|
|
804
|
+
const canon = this.infer().expr;
|
|
759
805
|
if (!canon)
|
|
760
806
|
return this;
|
|
761
807
|
options.steps++;
|
|
762
808
|
return canon._rski(options);
|
|
763
809
|
}
|
|
764
|
-
|
|
765
|
-
reduce (args) {
|
|
766
|
-
if (args.length < this.arity)
|
|
767
|
-
return null;
|
|
768
|
-
let egde = 0;
|
|
769
|
-
let step = this.impl;
|
|
770
|
-
while (typeof step === 'function') {
|
|
771
|
-
if (egde >= args.length)
|
|
772
|
-
return null;
|
|
773
|
-
step = step(args[egde++]);
|
|
774
|
-
}
|
|
775
|
-
if (!(step instanceof Expr))
|
|
776
|
-
throw new Error('Native combinator ' + this + ' reduced to a non-expression: ' + step);
|
|
777
|
-
return step.apply(...args.slice(egde));
|
|
778
|
-
}
|
|
779
810
|
}
|
|
780
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
|
+
*/
|
|
781
817
|
const native = {};
|
|
782
818
|
function addNative (name, impl, opt) {
|
|
783
819
|
native[name] = new Native(name, impl, opt);
|
|
@@ -785,9 +821,20 @@ function addNative (name, impl, opt) {
|
|
|
785
821
|
|
|
786
822
|
class Lambda extends Expr {
|
|
787
823
|
/**
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
+
*/
|
|
791
838
|
constructor (arg, impl) {
|
|
792
839
|
if (Array.isArray(arg)) {
|
|
793
840
|
// check args before everything
|
|
@@ -811,39 +858,45 @@ class Lambda extends Expr {
|
|
|
811
858
|
|
|
812
859
|
super();
|
|
813
860
|
|
|
814
|
-
// localize argument variable
|
|
815
|
-
const local = new FreeVar(arg.name);
|
|
861
|
+
// localize argument variable and bind it to oneself
|
|
862
|
+
const local = new FreeVar(arg.name, this);
|
|
816
863
|
this.arg = local;
|
|
817
864
|
this.impl = impl.subst(arg, local) ?? impl;
|
|
818
865
|
this.arity = 1;
|
|
819
866
|
}
|
|
820
867
|
|
|
821
|
-
getSymbols () {
|
|
822
|
-
const out = this.impl.getSymbols();
|
|
823
|
-
out.delete(this.arg);
|
|
824
|
-
out.set(Expr.lambdaPlaceholder, (out.get(Expr.lambdaPlaceholder) ?? 0) + 1);
|
|
825
|
-
return out;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
868
|
weight () {
|
|
829
869
|
return this.impl.weight() + 1;
|
|
830
870
|
}
|
|
831
871
|
|
|
832
|
-
|
|
872
|
+
_infer (options, preArgs = [], steps = 0) {
|
|
833
873
|
if (preArgs.length > options.maxArgs)
|
|
834
874
|
return { normal: false, steps };
|
|
835
875
|
|
|
836
876
|
const push = nthvar(preArgs.length + options.index);
|
|
837
|
-
return this.
|
|
877
|
+
return this.invoke(push)._infer(options, [...preArgs, push], steps + 1);
|
|
838
878
|
}
|
|
839
879
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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;
|
|
843
888
|
|
|
844
|
-
|
|
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
|
+
}
|
|
845
897
|
|
|
846
|
-
|
|
898
|
+
any (predicate) {
|
|
899
|
+
return predicate(this) || this.impl.any(predicate);
|
|
847
900
|
}
|
|
848
901
|
|
|
849
902
|
subst (search, replace) {
|
|
@@ -864,12 +917,12 @@ class Lambda extends Expr {
|
|
|
864
917
|
options.steps++;
|
|
865
918
|
if (impl === this.arg)
|
|
866
919
|
return native.I;
|
|
867
|
-
if (!impl.
|
|
920
|
+
if (!impl.any(e => e === this.arg))
|
|
868
921
|
return native.K.apply(impl);
|
|
869
922
|
if (impl instanceof App) {
|
|
870
923
|
const [fst, snd] = impl.split();
|
|
871
924
|
// try eta reduction
|
|
872
|
-
if (snd === this.arg && !fst.
|
|
925
|
+
if (snd === this.arg && !fst.any(e => e === this.arg))
|
|
873
926
|
return fst._rski(options);
|
|
874
927
|
// fall back to S
|
|
875
928
|
return native.S.apply(
|
|
@@ -880,25 +933,16 @@ class Lambda extends Expr {
|
|
|
880
933
|
throw new Error('Don\'t know how to convert to SKI' + this);
|
|
881
934
|
}
|
|
882
935
|
|
|
883
|
-
|
|
884
|
-
const maybe = super._replace(pairs, opt);
|
|
885
|
-
if (maybe)
|
|
886
|
-
return maybe;
|
|
887
|
-
// TODO filter out terms containing this.arg
|
|
888
|
-
return new Lambda(this.arg, this.impl._replace(pairs, opt) ?? this.impl);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
equals (other) {
|
|
936
|
+
diff (other, swap = false) {
|
|
892
937
|
if (!(other instanceof Lambda))
|
|
893
|
-
return super.
|
|
938
|
+
return super.diff(other, swap);
|
|
894
939
|
|
|
895
|
-
const t = new FreeVar('t');
|
|
940
|
+
const t = new FreeVar('t'); // TODO better placeholder name
|
|
896
941
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
return this.equals(other) || this.impl.contains(other);
|
|
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;
|
|
902
946
|
}
|
|
903
947
|
|
|
904
948
|
_format (options, nargs) {
|
|
@@ -920,6 +964,11 @@ class Lambda extends Expr {
|
|
|
920
964
|
}
|
|
921
965
|
|
|
922
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
|
+
*/
|
|
923
972
|
constructor (n) {
|
|
924
973
|
const p = Number.parseInt(n);
|
|
925
974
|
if (!(p >= 0))
|
|
@@ -938,10 +987,14 @@ class Church extends Native {
|
|
|
938
987
|
this.arity = 2;
|
|
939
988
|
}
|
|
940
989
|
|
|
941
|
-
|
|
942
|
-
if (other instanceof Church)
|
|
943
|
-
return
|
|
944
|
-
|
|
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 + ']';
|
|
945
998
|
}
|
|
946
999
|
|
|
947
1000
|
_unspaced (arg) {
|
|
@@ -949,9 +1002,23 @@ class Church extends Native {
|
|
|
949
1002
|
}
|
|
950
1003
|
}
|
|
951
1004
|
|
|
1005
|
+
function waitn (expr, n) {
|
|
1006
|
+
return arg => n <= 1 ? expr.apply(arg) : waitn(expr.apply(arg), n - 1);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
952
1009
|
class Alias extends Named {
|
|
953
1010
|
/**
|
|
954
|
-
* @desc
|
|
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
|
+
*
|
|
955
1022
|
* @param {String} name
|
|
956
1023
|
* @param {Expr} impl
|
|
957
1024
|
* @param {{canonize: boolean?, max: number?, maxArgs: number?, note: string?, terminal: boolean?}} [options]
|
|
@@ -964,16 +1031,13 @@ class Alias extends Named {
|
|
|
964
1031
|
this.note = options.note;
|
|
965
1032
|
|
|
966
1033
|
const guess = options.canonize
|
|
967
|
-
? impl.
|
|
1034
|
+
? impl.infer({ max: options.max, maxArgs: options.maxArgs })
|
|
968
1035
|
: { normal: false };
|
|
969
1036
|
this.arity = (guess.proper && guess.arity) || 0;
|
|
970
1037
|
this.proper = guess.proper ?? false;
|
|
971
1038
|
this.terminal = options.terminal ?? this.proper;
|
|
972
1039
|
this.canonical = guess.expr;
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
getSymbols () {
|
|
976
|
-
return this.terminal ? new Map([[this, 1]]) : this.impl.getSymbols();
|
|
1040
|
+
this.invoke = waitn(impl, this.arity);
|
|
977
1041
|
}
|
|
978
1042
|
|
|
979
1043
|
weight () {
|
|
@@ -984,14 +1048,22 @@ class Alias extends Named {
|
|
|
984
1048
|
return this.impl.expand();
|
|
985
1049
|
}
|
|
986
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
|
+
|
|
987
1059
|
subst (search, replace) {
|
|
988
1060
|
if (this === search)
|
|
989
1061
|
return replace;
|
|
990
1062
|
return this.impl.subst(search, replace);
|
|
991
1063
|
}
|
|
992
1064
|
|
|
993
|
-
|
|
994
|
-
return this.impl.
|
|
1065
|
+
_infer (options, preArgs = [], steps = 0) {
|
|
1066
|
+
return this.impl._infer(options, preArgs, steps);
|
|
995
1067
|
}
|
|
996
1068
|
|
|
997
1069
|
/**
|
|
@@ -1006,22 +1078,14 @@ class Alias extends Named {
|
|
|
1006
1078
|
return { expr: this.impl, steps: 0, changed: true };
|
|
1007
1079
|
}
|
|
1008
1080
|
|
|
1009
|
-
reduce (args) {
|
|
1010
|
-
if (args.length < this.arity)
|
|
1011
|
-
return null;
|
|
1012
|
-
return this.impl.apply(...args);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
1081
|
_firstVar () {
|
|
1016
1082
|
return this.impl._firstVar();
|
|
1017
1083
|
}
|
|
1018
1084
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
contains (other) {
|
|
1024
|
-
return this.impl.contains(other);
|
|
1085
|
+
diff (other, swap = false) {
|
|
1086
|
+
if (this === other)
|
|
1087
|
+
return null;
|
|
1088
|
+
return other.diff(this.impl, !swap);
|
|
1025
1089
|
}
|
|
1026
1090
|
|
|
1027
1091
|
_rski (options) {
|
|
@@ -1040,6 +1104,7 @@ class Alias extends Named {
|
|
|
1040
1104
|
}
|
|
1041
1105
|
|
|
1042
1106
|
_declare (output, inventory, seen) {
|
|
1107
|
+
// this is part of the 'declare' function, see below
|
|
1043
1108
|
// only once
|
|
1044
1109
|
if (seen.has(this))
|
|
1045
1110
|
return;
|
|
@@ -1064,20 +1129,15 @@ addNative('B', x => y => z => x.apply(y.apply(z)));
|
|
|
1064
1129
|
addNative('C', x => y => z => x.apply(z).apply(y));
|
|
1065
1130
|
addNative('W', x => y => x.apply(y).apply(y));
|
|
1066
1131
|
|
|
1067
|
-
addNative(
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
arity: 1,
|
|
1077
|
-
canonize: false,
|
|
1078
|
-
note: 'Lambda placeholder',
|
|
1079
|
-
apply: x => { throw new Error('Attempt to use a placeholder in expression') }
|
|
1080
|
-
});
|
|
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
|
+
);
|
|
1081
1141
|
|
|
1082
1142
|
// utility functions dependent on Expr* classes, in alphabetical order
|
|
1083
1143
|
|
|
@@ -1136,9 +1196,28 @@ function declare (inventory) {
|
|
|
1136
1196
|
}
|
|
1137
1197
|
|
|
1138
1198
|
function maybeLambda (args, expr, caps = {}) {
|
|
1139
|
-
const
|
|
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
|
+
});
|
|
1140
1212
|
|
|
1141
|
-
const
|
|
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
|
+
}
|
|
1142
1221
|
|
|
1143
1222
|
return {
|
|
1144
1223
|
expr: args.length ? new Lambda(args, expr) : expr,
|
|
@@ -1147,7 +1226,7 @@ function maybeLambda (args, expr, caps = {}) {
|
|
|
1147
1226
|
...(dup.size ? { dup } : {}),
|
|
1148
1227
|
duplicate: !!dup.size || caps.duplicate || false,
|
|
1149
1228
|
discard: !!skip.size || caps.discard || false,
|
|
1150
|
-
proper
|
|
1229
|
+
proper,
|
|
1151
1230
|
};
|
|
1152
1231
|
}
|
|
1153
1232
|
|
|
@@ -1161,7 +1240,7 @@ function naiveCanonize (expr) {
|
|
|
1161
1240
|
if (expr instanceof Alias)
|
|
1162
1241
|
return naiveCanonize(expr.impl);
|
|
1163
1242
|
|
|
1164
|
-
const canon = expr.
|
|
1243
|
+
const canon = expr.infer();
|
|
1165
1244
|
if (canon.expr)
|
|
1166
1245
|
return canon.expr;
|
|
1167
1246
|
|
|
@@ -1225,10 +1304,13 @@ function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
|
|
|
1225
1304
|
}
|
|
1226
1305
|
}
|
|
1227
1306
|
|
|
1228
|
-
const canon = expr.
|
|
1307
|
+
const canon = expr.infer({ max: options.max, maxArgs: options.maxArgs });
|
|
1229
1308
|
state.steps += canon.steps;
|
|
1230
1309
|
if (canon.expr && canon.expr.weight() < maxWeight)
|
|
1231
1310
|
yield { expr: canon.expr, steps: state.steps, comment: '(canonical)' };
|
|
1232
1311
|
}
|
|
1233
1312
|
|
|
1234
|
-
|
|
1313
|
+
Expr.declare = declare;
|
|
1314
|
+
Expr.native = native;
|
|
1315
|
+
|
|
1316
|
+
module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church };
|