@dallaylaen/ski-interpreter 1.3.0 → 2.1.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 +55 -0
- package/README.md +132 -45
- package/bin/ski.js +96 -97
- package/index.js +14 -6
- package/lib/expr.js +449 -287
- package/lib/extras.js +140 -0
- package/lib/internal.js +105 -0
- package/lib/parser.js +78 -31
- package/lib/quest.js +93 -59
- package/package.json +2 -2
- package/types/index.d.ts +2 -2
- package/types/lib/expr.d.ts +232 -108
- package/types/lib/extras.d.ts +57 -0
- package/types/lib/internal.d.ts +52 -0
- package/types/lib/parser.d.ts +68 -45
- package/types/lib/quest.d.ts +80 -59
- package/lib/util.js +0 -78
- package/types/lib/util.d.ts +0 -13
package/lib/expr.js
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { unwrap, prepareWrapper } = require('./internal');
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
terse: true,
|
|
5
|
+
const DEFAULTS = {
|
|
7
6
|
max: 1000,
|
|
8
7
|
maxArgs: 32,
|
|
9
8
|
};
|
|
10
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @template T
|
|
12
|
+
* @typedef {T | { value: T?, action: string } | null} ActionWrapper
|
|
13
|
+
*/
|
|
14
|
+
const control = {
|
|
15
|
+
descend: prepareWrapper('descend'),
|
|
16
|
+
prune: prepareWrapper('prune'),
|
|
17
|
+
stop: prepareWrapper('stop'),
|
|
18
|
+
};
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
21
|
* @typedef {Expr | function(Expr): Partial} Partial
|
|
13
22
|
*/
|
|
14
23
|
|
|
15
24
|
class Expr {
|
|
16
25
|
/**
|
|
17
|
-
* @descr A
|
|
26
|
+
* @descr A combinatory logic expression.
|
|
27
|
+
*
|
|
28
|
+
* Applications, variables, and other terms like combinators per se
|
|
29
|
+
* are subclasses of this class.
|
|
30
|
+
*
|
|
31
|
+
* @abstract
|
|
32
|
+
* @property {{
|
|
33
|
+
* scope?: any,
|
|
34
|
+
* env?: { [key: string]: Expr },
|
|
35
|
+
* src?: string,
|
|
36
|
+
* parser: object,
|
|
37
|
+
* }} [context] // TODO proper type
|
|
18
38
|
*/
|
|
19
39
|
constructor () {
|
|
20
40
|
if (new.target === Expr)
|
|
@@ -22,107 +42,86 @@ class Expr {
|
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @desc apply self to zero or more terms and return the resulting term,
|
|
34
|
-
* without performing any calculations whatsoever
|
|
35
|
-
* @param {Expr} args
|
|
36
|
-
* @return {Expr}
|
|
37
|
-
*/
|
|
45
|
+
* @desc apply self to zero or more terms and return the resulting term,
|
|
46
|
+
* without performing any calculations whatsoever
|
|
47
|
+
* @param {Expr} args
|
|
48
|
+
* @return {Expr}
|
|
49
|
+
*/
|
|
38
50
|
apply (...args) {
|
|
39
|
-
|
|
51
|
+
let expr = this;
|
|
52
|
+
for (const arg of args)
|
|
53
|
+
expr = new App(expr, arg);
|
|
54
|
+
return expr;
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
* expand all terms but don't perform any calculations
|
|
59
|
+
* @return {Expr}
|
|
60
|
+
*/
|
|
46
61
|
expand () {
|
|
47
62
|
return this;
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* @return {Set<FreeVar>}
|
|
53
|
-
*/
|
|
54
|
-
freeVars () {
|
|
55
|
-
const symbols = this.getSymbols();
|
|
56
|
-
const out = new Set();
|
|
57
|
-
for (const [key, _] of symbols) {
|
|
58
|
-
if (key instanceof FreeVar)
|
|
59
|
-
out.add(key);
|
|
60
|
-
}
|
|
61
|
-
return out;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
hasLambda () {
|
|
65
|
-
const sym = this.getSymbols();
|
|
66
|
-
return sym.has(Expr.lambdaPlaceholder);
|
|
65
|
+
freeOnly () {
|
|
66
|
+
return !this.any(e => !(e instanceof FreeVar || e instanceof App));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
/**
|
|
70
|
+
* @desc Traverse the expression tree, applying change() to each node.
|
|
71
|
+
* If change() returns an Expr, the node is replaced with that value.
|
|
72
|
+
* Otherwise, the node is left descended further (if applicable)
|
|
73
|
+
* or left unchanged.
|
|
74
|
+
*
|
|
75
|
+
* Returns null if no changes were made, or the new expression otherwise.
|
|
76
|
+
*
|
|
77
|
+
* @param {(e:Expr) => (Expr|null)} change
|
|
78
|
+
* @returns {Expr|null}
|
|
79
|
+
*/
|
|
80
|
+
traverse (change) {
|
|
81
|
+
return change(this);
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
/**
|
|
78
|
-
* @desc
|
|
79
|
-
*
|
|
80
|
-
* @
|
|
85
|
+
* @desc Returns true if predicate() is true for any subterm of the expression, false otherwise.
|
|
86
|
+
*
|
|
87
|
+
* @param {(e: Expr) => boolean} predicate
|
|
88
|
+
* @returns {boolean}
|
|
81
89
|
*/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return new Map([[this, 1]]);
|
|
90
|
+
any (predicate) {
|
|
91
|
+
return predicate(this);
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* If a simgle term is given, it is duplicated into a pair.
|
|
95
|
+
* @desc Fold the expression into a single value by recursively applying combine() to its subterms.
|
|
96
|
+
* Nodes are traversed in leftmost-outermost order, i.e. the same order as reduction steps are taken.
|
|
91
97
|
*
|
|
92
|
-
*
|
|
93
|
-
* and replaced them with I
|
|
98
|
+
* null or undefined return value from combine() means "keep current value and descend further".
|
|
94
99
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
100
|
+
* SKI.control provides primitives to control the folding flow:
|
|
101
|
+
* - SKI.control.prune(value) means "use value and don't descend further into this branch";
|
|
102
|
+
* - SKI.control.stop(value) means "stop folding immediately and return value".
|
|
103
|
+
* - SKI.control.descend(value) is the default behavior, meaning "use value and descend further".
|
|
104
|
+
*
|
|
105
|
+
* This method is experimental and may change in the future.
|
|
106
|
+
*
|
|
107
|
+
* @experimental
|
|
108
|
+
* @template T
|
|
109
|
+
* @param {T} initial
|
|
110
|
+
* @param {(acc: T, expr: Expr) => ActionWrapper<T>} combine
|
|
111
|
+
* @returns {T}
|
|
98
112
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (const entry of terms) {
|
|
104
|
-
const pair = (Array.isArray(entry) ? entry : [entry, entry]);
|
|
105
|
-
pair[0] = pair[0].guess(opt).expr;
|
|
106
|
-
if (!pair[0])
|
|
107
|
-
throw new Error('Failed to canonize term ' + entry);
|
|
108
|
-
if (pair.length !== 2)
|
|
109
|
-
throw new Error('Expected a pair of terms to replace, got ' + entry);
|
|
110
|
-
pairs.push(pair);
|
|
111
|
-
}
|
|
112
|
-
return this._replace(pairs, opt) ?? this;
|
|
113
|
+
|
|
114
|
+
fold (initial, combine) {
|
|
115
|
+
const [value, _] = unwrap(this._fold(initial, combine));
|
|
116
|
+
return value ?? initial;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const [canon, term] of pairs) {
|
|
118
|
-
if (check.equals(canon))
|
|
119
|
-
return term;
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
119
|
+
_fold (initial, combine) {
|
|
120
|
+
return combine(initial, this);
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
/**
|
|
125
|
-
* @desc
|
|
124
|
+
* @desc rough estimate of the complexity of the term
|
|
126
125
|
* @return {number}
|
|
127
126
|
*/
|
|
128
127
|
weight () {
|
|
@@ -130,18 +129,18 @@ class Expr {
|
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
/**
|
|
133
|
-
* @desc Try to find an equivalent lambda term for the expression,
|
|
132
|
+
* @desc Try to empirically find an equivalent lambda term for the expression,
|
|
134
133
|
* returning also the term's arity and some other properties.
|
|
135
134
|
*
|
|
136
|
-
* This is used internally when declaring a Native term,
|
|
135
|
+
* This is used internally when declaring a Native / Alias term,
|
|
137
136
|
* unless {canonize: false} is used.
|
|
138
137
|
*
|
|
139
138
|
* As of current it only recognizes terms that have a normal form,
|
|
140
139
|
* perhaps after adding some variables. This may change in the future.
|
|
141
140
|
*
|
|
142
|
-
* Use
|
|
141
|
+
* Use toLambda() if you want to get a lambda term in any case.
|
|
143
142
|
*
|
|
144
|
-
* @param {{max
|
|
143
|
+
* @param {{max?: number, maxArgs?: number}} options
|
|
145
144
|
* @return {{
|
|
146
145
|
* normal: boolean,
|
|
147
146
|
* steps: number,
|
|
@@ -154,14 +153,22 @@ class Expr {
|
|
|
154
153
|
* dup?: Set<number>,
|
|
155
154
|
* }}
|
|
156
155
|
*/
|
|
157
|
-
|
|
158
|
-
const max = options.max ??
|
|
159
|
-
const maxArgs = options.maxArgs ??
|
|
160
|
-
const out = this.
|
|
156
|
+
infer (options = {}) {
|
|
157
|
+
const max = options.max ?? DEFAULTS.max;
|
|
158
|
+
const maxArgs = options.maxArgs ?? DEFAULTS.maxArgs;
|
|
159
|
+
const out = this._infer({ max, maxArgs, index: 0 });
|
|
161
160
|
return out;
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
|
|
163
|
+
/**
|
|
164
|
+
*
|
|
165
|
+
* @param {{max: number, maxArgs: number, index: number}} options
|
|
166
|
+
* @param {FreeVar[]} preArgs
|
|
167
|
+
* @param {number} steps
|
|
168
|
+
* @returns {{normal: boolean, steps: number}|{normal: boolean, steps: number}|{normal: boolean, steps: number, expr: Lambda|*, arity?: *, skip?: Set<any>, dup?: Set<any>, duplicate, discard, proper: boolean}|*|{normal: boolean, steps: number}}
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
_infer (options, preArgs = [], steps = 0) {
|
|
165
172
|
if (preArgs.length > options.maxArgs || steps > options.max)
|
|
166
173
|
return { normal: false, steps };
|
|
167
174
|
|
|
@@ -182,29 +189,45 @@ class Expr {
|
|
|
182
189
|
|
|
183
190
|
// normal form != this, redo exercise
|
|
184
191
|
if (next.steps !== 0)
|
|
185
|
-
return next.expr.
|
|
192
|
+
return next.expr._infer(options, preArgs, steps);
|
|
186
193
|
|
|
187
|
-
|
|
194
|
+
// adding more args won't help, bail out
|
|
195
|
+
// if we're an App, the App's _infer will take care of further args
|
|
196
|
+
if (this.unroll()[0] instanceof FreeVar)
|
|
188
197
|
return { normal: false, steps };
|
|
189
198
|
|
|
199
|
+
// try adding more arguments, maybe we'll get a normal form then
|
|
190
200
|
const push = nthvar(preArgs.length + options.index);
|
|
191
|
-
return this.apply(push).
|
|
201
|
+
return this.apply(push)._infer(options, [...preArgs, push], steps);
|
|
192
202
|
}
|
|
193
203
|
|
|
194
|
-
|
|
204
|
+
/**
|
|
205
|
+
* @desc Expand an expression into a list of terms
|
|
206
|
+
* that give the initial expression when applied from left to right:
|
|
207
|
+
* ((a, b), (c, d)) => [a, b, (c, d)]
|
|
208
|
+
*
|
|
209
|
+
* This can be thought of as an opposite of apply:
|
|
210
|
+
* fun.apply(...arg).unroll() is exactly [fun, ...args]
|
|
211
|
+
* (even if ...arg is in fact empty).
|
|
212
|
+
*
|
|
213
|
+
* @returns {Expr[]}
|
|
214
|
+
*/
|
|
215
|
+
unroll () {
|
|
216
|
+
// currently only used by infer() but may be useful
|
|
217
|
+
// to convert binary App trees to n-ary or smth
|
|
195
218
|
return [this];
|
|
196
219
|
}
|
|
197
220
|
|
|
198
|
-
_firstVar () {
|
|
199
|
-
// boolean, whether the expression starts with a free variable
|
|
200
|
-
// used by guess()
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
221
|
/**
|
|
205
222
|
* @desc Returns a series of lambda terms equivalent to the given expression,
|
|
206
223
|
* up to the provided computation steps limit,
|
|
207
224
|
* in decreasing weight order.
|
|
225
|
+
*
|
|
226
|
+
* Unlike infer(), this method will always return something,
|
|
227
|
+
* even if the expression has no normal form.
|
|
228
|
+
*
|
|
229
|
+
* See also Expr.walk() and Expr.toSKI().
|
|
230
|
+
*
|
|
208
231
|
* @param {{
|
|
209
232
|
* max?: number,
|
|
210
233
|
* maxArgs?: number,
|
|
@@ -216,17 +239,29 @@ class Expr {
|
|
|
216
239
|
* @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
|
|
217
240
|
* @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
|
|
218
241
|
*/
|
|
219
|
-
*
|
|
220
|
-
const expr =
|
|
242
|
+
* toLambda (options = {}) {
|
|
243
|
+
const expr = this.traverse(e => {
|
|
244
|
+
if (e instanceof FreeVar || e instanceof App || e instanceof Lambda || e instanceof Alias)
|
|
245
|
+
return null; // no change
|
|
246
|
+
const guess = e.infer({ max: options.max, maxArgs: options.maxArgs });
|
|
247
|
+
if (!guess.normal)
|
|
248
|
+
throw new Error('Failed to infer an equivalent lambda term for ' + e);
|
|
249
|
+
return guess.expr;
|
|
250
|
+
}) ?? this;
|
|
221
251
|
yield * simplifyLambda(expr, options);
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
/**
|
|
225
|
-
* @desc
|
|
226
|
-
*
|
|
255
|
+
* @desc Rewrite the expression into S, K, and I combinators step by step.
|
|
256
|
+
* Returns an iterator yielding the intermediate expressions,
|
|
257
|
+
* along with the number of steps taken to reach them.
|
|
258
|
+
*
|
|
259
|
+
* See also Expr.walk() and Expr.toLambda().
|
|
260
|
+
*
|
|
261
|
+
* @param {{max?: number}} [options]
|
|
227
262
|
* @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
|
|
228
263
|
*/
|
|
229
|
-
*
|
|
264
|
+
* toSKI (options = {}) {
|
|
230
265
|
// TODO options.max is not actually max, it's the number of steps in one iteration
|
|
231
266
|
let steps = 0;
|
|
232
267
|
let expr = this;
|
|
@@ -242,6 +277,12 @@ class Expr {
|
|
|
242
277
|
}
|
|
243
278
|
}
|
|
244
279
|
|
|
280
|
+
/**
|
|
281
|
+
* @desc Internal method for toSKI, which performs one step of the conversion.
|
|
282
|
+
* @param {{max: number, steps: number}} options
|
|
283
|
+
* @returns {Expr}
|
|
284
|
+
* @private
|
|
285
|
+
*/
|
|
245
286
|
_rski (options) {
|
|
246
287
|
return this;
|
|
247
288
|
}
|
|
@@ -252,7 +293,7 @@ class Expr {
|
|
|
252
293
|
* Lambda terms and applications will never match if used as plug
|
|
253
294
|
* as they are impossible co compare without extensive computations.
|
|
254
295
|
* Typically used on variables but can also be applied to other terms, e.g. aliases.
|
|
255
|
-
* See also Expr.
|
|
296
|
+
* See also Expr.traverse().
|
|
256
297
|
* @param {Expr} search
|
|
257
298
|
* @param {Expr} replace
|
|
258
299
|
* @return {Expr|null}
|
|
@@ -285,19 +326,19 @@ class Expr {
|
|
|
285
326
|
}
|
|
286
327
|
|
|
287
328
|
/**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
329
|
+
* @desc iterate one step of a calculation.
|
|
330
|
+
* @return {{expr: Expr, steps: number, changed: boolean}}
|
|
331
|
+
*/
|
|
291
332
|
step () { return { expr: this, steps: 0, changed: false } }
|
|
292
333
|
|
|
293
334
|
/**
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
335
|
+
* @desc Run uninterrupted sequence of step() applications
|
|
336
|
+
* until the expression is irreducible, or max number of steps is reached.
|
|
337
|
+
* Default number of steps = 1000.
|
|
338
|
+
* @param {{max: number?, steps: number?, throw: boolean?}|Expr} [opt]
|
|
339
|
+
* @param {Expr} args
|
|
340
|
+
* @return {{expr: Expr, steps: number, final: boolean}}
|
|
341
|
+
*/
|
|
301
342
|
run (opt = {}, ...args) {
|
|
302
343
|
if (opt instanceof Expr) {
|
|
303
344
|
args.unshift(opt);
|
|
@@ -306,7 +347,7 @@ class Expr {
|
|
|
306
347
|
let expr = args ? this.apply(...args) : this;
|
|
307
348
|
let steps = opt.steps ?? 0;
|
|
308
349
|
// make sure we make at least 1 step, to tell whether we've reached the normal form
|
|
309
|
-
const max = Math.max(opt.max ??
|
|
350
|
+
const max = Math.max(opt.max ?? DEFAULTS.max, 1) + steps;
|
|
310
351
|
let final = false;
|
|
311
352
|
for (; steps < max; ) {
|
|
312
353
|
const next = expr.step();
|
|
@@ -323,11 +364,11 @@ class Expr {
|
|
|
323
364
|
}
|
|
324
365
|
|
|
325
366
|
/**
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
367
|
+
* Execute step() while possible, yielding a brief description of events after each step.
|
|
368
|
+
* Mnemonics: like run() but slower.
|
|
369
|
+
* @param {{max: number?}} options
|
|
370
|
+
* @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
|
|
371
|
+
*/
|
|
331
372
|
* walk (options = {}) {
|
|
332
373
|
const max = options.max ?? Infinity;
|
|
333
374
|
let steps = 0;
|
|
@@ -350,20 +391,49 @@ class Expr {
|
|
|
350
391
|
}
|
|
351
392
|
|
|
352
393
|
/**
|
|
394
|
+
* @desc True is the expressions are identical, false otherwise.
|
|
395
|
+
* Aliases are expanded.
|
|
396
|
+
* Bound variables in lambda terms are renamed consistently.
|
|
397
|
+
* However, no reductions are attempted.
|
|
398
|
+
*
|
|
399
|
+
* E.g. a->b->a == x->y->x is true, but a->b->a == K is false.
|
|
353
400
|
*
|
|
354
401
|
* @param {Expr} other
|
|
355
402
|
* @return {boolean}
|
|
356
403
|
*/
|
|
357
404
|
equals (other) {
|
|
358
|
-
|
|
359
|
-
return true;
|
|
360
|
-
if (other instanceof Alias)
|
|
361
|
-
return other.equals(this);
|
|
362
|
-
return false;
|
|
405
|
+
return !this.diff(other);
|
|
363
406
|
}
|
|
364
407
|
|
|
365
|
-
|
|
366
|
-
|
|
408
|
+
/**
|
|
409
|
+
* @desc Recursively compare two expressions and return a string
|
|
410
|
+
* describing the first point of difference.
|
|
411
|
+
* Returns null if expressions are identical.
|
|
412
|
+
*
|
|
413
|
+
* Aliases are expanded.
|
|
414
|
+
* Bound variables in lambda terms are renamed consistently.
|
|
415
|
+
* However, no reductions are attempted.
|
|
416
|
+
*
|
|
417
|
+
* Members of the FreeVar class are considered different
|
|
418
|
+
* even if they have the same name, unless they are the same object.
|
|
419
|
+
* To somewhat alleviate confusion, the output will include
|
|
420
|
+
* the internal id of the variable in square brackets.
|
|
421
|
+
*
|
|
422
|
+
* @example "K(S != I)" is the result of comparing "KS" and "KI"
|
|
423
|
+
* @example "S(K([x[13] != x[14]]))K"
|
|
424
|
+
*
|
|
425
|
+
* @param {Expr} other
|
|
426
|
+
* @param {boolean} [swap] If true, the order of expressions is reversed in the output.
|
|
427
|
+
* @returns {string|null}
|
|
428
|
+
*/
|
|
429
|
+
diff (other, swap = false) {
|
|
430
|
+
if (this === other)
|
|
431
|
+
return null;
|
|
432
|
+
if (other instanceof Alias)
|
|
433
|
+
return other.impl.diff(this, !swap);
|
|
434
|
+
return swap
|
|
435
|
+
? '[' + other + ' != ' + this + ']'
|
|
436
|
+
: '[' + this + ' != ' + other + ']';
|
|
367
437
|
}
|
|
368
438
|
|
|
369
439
|
/**
|
|
@@ -375,12 +445,13 @@ class Expr {
|
|
|
375
445
|
comment = comment ? comment + ': ' : '';
|
|
376
446
|
if (!(expected instanceof Expr))
|
|
377
447
|
throw new Error(comment + 'attempt to expect a combinator to equal something else: ' + expected);
|
|
378
|
-
|
|
379
|
-
|
|
448
|
+
const diff = this.diff(expected);
|
|
449
|
+
if (!diff)
|
|
450
|
+
return; // all good
|
|
380
451
|
|
|
381
452
|
// TODO wanna use AssertionError but webpack doesn't recognize it
|
|
382
453
|
// still the below hack works for mocha-based tests.
|
|
383
|
-
const poorMans = new Error(comment +
|
|
454
|
+
const poorMans = new Error(comment + diff);
|
|
384
455
|
poorMans.expected = expected + '';
|
|
385
456
|
poorMans.actual = this + '';
|
|
386
457
|
throw poorMans;
|
|
@@ -404,6 +475,12 @@ class Expr {
|
|
|
404
475
|
return false;
|
|
405
476
|
}
|
|
406
477
|
|
|
478
|
+
/**
|
|
479
|
+
* @desc Whether the expression can be printed without a space when followed by arg.
|
|
480
|
+
* @param {Expr} arg
|
|
481
|
+
* @returns {boolean}
|
|
482
|
+
* @private
|
|
483
|
+
*/
|
|
407
484
|
_unspaced (arg) {
|
|
408
485
|
return this._braced(true);
|
|
409
486
|
}
|
|
@@ -416,10 +493,11 @@ class Expr {
|
|
|
416
493
|
*
|
|
417
494
|
* @param {Object} [options] - formatting options
|
|
418
495
|
* @param {boolean} [options.terse] - whether to use terse formatting (omitting unnecessary spaces and parentheses)
|
|
419
|
-
* @param {boolean} [options.html] - whether to default to HTML tags & entities
|
|
496
|
+
* @param {boolean} [options.html] - whether to default to HTML tags & entities.
|
|
497
|
+
* If a named term has fancyName property set, it will be used instead of name in this mode.
|
|
420
498
|
* @param {[string, string]} [options.brackets] - wrappers for application arguments, typically ['(', ')']
|
|
421
499
|
* @param {[string, string]} [options.var] - wrappers for variable names
|
|
422
|
-
* (will default to <var> and </var> in html mode)
|
|
500
|
+
* (will default to <var> and </var> in html mode).
|
|
423
501
|
* @param {[string, string, string]} [options.lambda] - wrappers for lambda abstractions, e.g. ['λ', '.', '']
|
|
424
502
|
* where the middle string is placed between argument and body
|
|
425
503
|
* default is ['', '->', ''] or ['', '->', ''] for html
|
|
@@ -440,9 +518,8 @@ class Expr {
|
|
|
440
518
|
* @example foo.format({ inventory: { T } }) // use T as a named term, expand all others
|
|
441
519
|
*
|
|
442
520
|
*/
|
|
443
|
-
|
|
444
521
|
format (options = {}) {
|
|
445
|
-
const
|
|
522
|
+
const fallback = options.html
|
|
446
523
|
? {
|
|
447
524
|
brackets: ['(', ')'],
|
|
448
525
|
space: ' ',
|
|
@@ -460,39 +537,49 @@ class Expr {
|
|
|
460
537
|
redex: ['', ''],
|
|
461
538
|
}
|
|
462
539
|
return this._format({
|
|
463
|
-
terse: options.terse ??
|
|
464
|
-
brackets: options.brackets ??
|
|
465
|
-
space: options.space ??
|
|
466
|
-
var: options.var ??
|
|
467
|
-
lambda: options.lambda ??
|
|
468
|
-
around: options.around ??
|
|
469
|
-
redex: options.redex ??
|
|
540
|
+
terse: options.terse ?? true,
|
|
541
|
+
brackets: options.brackets ?? fallback.brackets,
|
|
542
|
+
space: options.space ?? fallback.space,
|
|
543
|
+
var: options.var ?? fallback.var,
|
|
544
|
+
lambda: options.lambda ?? fallback.lambda,
|
|
545
|
+
around: options.around ?? fallback.around,
|
|
546
|
+
redex: options.redex ?? fallback.redex,
|
|
470
547
|
inventory: options.inventory, // TODO better name
|
|
548
|
+
html: options.html ?? false,
|
|
471
549
|
}, 0);
|
|
472
550
|
}
|
|
473
551
|
|
|
552
|
+
/**
|
|
553
|
+
* @desc Internal method for format(), which performs the actual formatting.
|
|
554
|
+
* @param {Object} options
|
|
555
|
+
* @param {number} nargs
|
|
556
|
+
* @returns {string}
|
|
557
|
+
* @private
|
|
558
|
+
*/
|
|
474
559
|
_format (options, nargs) {
|
|
475
560
|
throw new Error( 'No _format() method defined in class ' + this.constructor.name );
|
|
476
561
|
}
|
|
477
562
|
|
|
478
563
|
// output: string[] /* appended */, inventory: { [key: string]: Expr }, seen: Set<Expr>
|
|
479
564
|
_declare (output, inventory, seen) {}
|
|
565
|
+
|
|
566
|
+
toJSON () {
|
|
567
|
+
return this.format();
|
|
568
|
+
}
|
|
480
569
|
}
|
|
481
570
|
|
|
482
571
|
class App extends Expr {
|
|
483
572
|
/**
|
|
484
573
|
* @desc Application of fun() to args.
|
|
485
|
-
* Never ever use new App(fun,
|
|
574
|
+
* Never ever use new App(fun, arg) directly, use fun.apply(...args) instead.
|
|
486
575
|
* @param {Expr} fun
|
|
487
|
-
* @param {Expr}
|
|
576
|
+
* @param {Expr} arg
|
|
488
577
|
*/
|
|
489
|
-
constructor (fun,
|
|
490
|
-
if (args.length === 0)
|
|
491
|
-
throw new Error('Attempt to create an application with no arguments (likely interpreter bug)');
|
|
578
|
+
constructor (fun, arg) {
|
|
492
579
|
super();
|
|
493
580
|
|
|
494
|
-
this.arg =
|
|
495
|
-
this.fun =
|
|
581
|
+
this.arg = arg;
|
|
582
|
+
this.fun = fun;
|
|
496
583
|
this.final = false;
|
|
497
584
|
this.arity = this.fun.arity > 0 ? this.fun.arity - 1 : 0;
|
|
498
585
|
}
|
|
@@ -501,30 +588,23 @@ class App extends Expr {
|
|
|
501
588
|
return this.fun.weight() + this.arg.weight();
|
|
502
589
|
}
|
|
503
590
|
|
|
504
|
-
|
|
505
|
-
const out = this.fun.getSymbols();
|
|
506
|
-
for (const [key, value] of this.arg.getSymbols())
|
|
507
|
-
out.set(key, (out.get(key) ?? 0) + value);
|
|
508
|
-
return out;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
_guess (options, preArgs = [], steps = 0) {
|
|
591
|
+
_infer (options, preArgs = [], steps = 0) {
|
|
512
592
|
if (preArgs.length > options.maxArgs || steps > options.max)
|
|
513
593
|
return { normal: false, steps };
|
|
514
594
|
|
|
515
595
|
/*
|
|
516
596
|
* inside and App there are 3 main possibilities:
|
|
517
|
-
* 1) The parent
|
|
597
|
+
* 1) The parent infer() actually is able to do the job. Then we just proxy the result.
|
|
518
598
|
* 2) Both `fun` and `arg` form good enough lambda terms. Then lump them together & return.
|
|
519
599
|
* 3) We literally have no idea, so we just pick the shortest defined term from the above.
|
|
520
600
|
*/
|
|
521
601
|
|
|
522
|
-
const proxy = super.
|
|
602
|
+
const proxy = super._infer(options, preArgs, steps);
|
|
523
603
|
if (proxy.normal)
|
|
524
604
|
return proxy;
|
|
525
605
|
steps = proxy.steps; // reimport extra iterations
|
|
526
606
|
|
|
527
|
-
const [first, ...list] = this.
|
|
607
|
+
const [first, ...list] = this.unroll();
|
|
528
608
|
if (!(first instanceof FreeVar))
|
|
529
609
|
return { normal: false, steps }
|
|
530
610
|
// TODO maybe do it later
|
|
@@ -533,7 +613,7 @@ class App extends Expr {
|
|
|
533
613
|
let duplicate = false;
|
|
534
614
|
const out = [];
|
|
535
615
|
for (const term of list) {
|
|
536
|
-
const guess = term.
|
|
616
|
+
const guess = term._infer({
|
|
537
617
|
...options,
|
|
538
618
|
maxArgs: options.maxArgs - preArgs.length,
|
|
539
619
|
max: options.max - steps,
|
|
@@ -550,27 +630,48 @@ class App extends Expr {
|
|
|
550
630
|
return {
|
|
551
631
|
normal: true,
|
|
552
632
|
steps,
|
|
553
|
-
...maybeLambda(preArgs,
|
|
633
|
+
...maybeLambda(preArgs, first.apply(...out), {
|
|
554
634
|
discard,
|
|
555
635
|
duplicate,
|
|
556
636
|
}),
|
|
557
637
|
};
|
|
558
638
|
}
|
|
559
639
|
|
|
560
|
-
_firstVar () {
|
|
561
|
-
return this.fun._firstVar();
|
|
562
|
-
}
|
|
563
|
-
|
|
564
640
|
expand () {
|
|
565
641
|
return this.fun.expand().apply(this.arg.expand());
|
|
566
642
|
}
|
|
567
643
|
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
if (
|
|
571
|
-
return
|
|
572
|
-
|
|
573
|
-
|
|
644
|
+
traverse (change) {
|
|
645
|
+
const replaced = change(this);
|
|
646
|
+
if (replaced instanceof Expr)
|
|
647
|
+
return replaced;
|
|
648
|
+
|
|
649
|
+
const fun = this.fun.traverse(change);
|
|
650
|
+
const arg = this.arg.traverse(change);
|
|
651
|
+
|
|
652
|
+
if (!fun && !arg)
|
|
653
|
+
return null; // no changes
|
|
654
|
+
|
|
655
|
+
return (fun ?? this.fun).apply(arg ?? this.arg);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
any (predicate) {
|
|
659
|
+
return predicate(this) || this.fun.any(predicate) || this.arg.any(predicate);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
_fold (initial, combine) {
|
|
663
|
+
const [value = initial, action = 'descend'] = unwrap(combine(initial, this));
|
|
664
|
+
if (action === 'prune')
|
|
665
|
+
return value;
|
|
666
|
+
if (action === 'stop')
|
|
667
|
+
return control.stop(value);
|
|
668
|
+
const [fValue = value, fAction = 'descend'] = unwrap(this.fun._fold(value, combine));
|
|
669
|
+
if (fAction === 'stop')
|
|
670
|
+
return control.stop(fValue);
|
|
671
|
+
const [aValue = fValue, aAction = 'descend'] = unwrap(this.arg._fold(fValue, combine));
|
|
672
|
+
if (aAction === 'stop')
|
|
673
|
+
return control.stop(aValue);
|
|
674
|
+
return aValue;
|
|
574
675
|
}
|
|
575
676
|
|
|
576
677
|
subst (search, replace) {
|
|
@@ -627,13 +728,8 @@ class App extends Expr {
|
|
|
627
728
|
}
|
|
628
729
|
}
|
|
629
730
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
return [this.fun, this.arg];
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
_aslist () {
|
|
636
|
-
return [...this.fun._aslist(), this.arg];
|
|
731
|
+
unroll () {
|
|
732
|
+
return [...this.fun.unroll(), this.arg];
|
|
637
733
|
}
|
|
638
734
|
|
|
639
735
|
/**
|
|
@@ -647,15 +743,17 @@ class App extends Expr {
|
|
|
647
743
|
return this.fun._rski(options).apply(this.arg._rski(options));
|
|
648
744
|
}
|
|
649
745
|
|
|
650
|
-
|
|
746
|
+
diff (other, swap = false) {
|
|
651
747
|
if (!(other instanceof App))
|
|
652
|
-
return super.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
748
|
+
return super.diff(other, swap);
|
|
749
|
+
|
|
750
|
+
const fun = this.fun.diff(other.fun, swap);
|
|
751
|
+
if (fun)
|
|
752
|
+
return fun + '(...)';
|
|
753
|
+
const arg = this.arg.diff(other.arg, swap);
|
|
754
|
+
if (arg)
|
|
755
|
+
return this.fun + '(' + arg + ')';
|
|
756
|
+
return null;
|
|
659
757
|
}
|
|
660
758
|
|
|
661
759
|
_braced (first) {
|
|
@@ -685,9 +783,10 @@ class App extends Expr {
|
|
|
685
783
|
|
|
686
784
|
class Named extends Expr {
|
|
687
785
|
/**
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
786
|
+
* @desc An abstract class representing a term named 'name'.
|
|
787
|
+
*
|
|
788
|
+
* @param {String} name
|
|
789
|
+
*/
|
|
691
790
|
constructor (name) {
|
|
692
791
|
super();
|
|
693
792
|
if (typeof name !== 'string' || name.length === 0)
|
|
@@ -705,30 +804,64 @@ class Named extends Expr {
|
|
|
705
804
|
}
|
|
706
805
|
|
|
707
806
|
_format (options, nargs) {
|
|
807
|
+
// NOTE fancyName is not yet official and may change name or meaning
|
|
808
|
+
const name = options.html ? this.fancyName ?? this.name : this.name;
|
|
708
809
|
return this.arity > 0 && this.arity <= nargs
|
|
709
|
-
? options.redex[0] +
|
|
710
|
-
:
|
|
810
|
+
? options.redex[0] + name + options.redex[1]
|
|
811
|
+
: name;
|
|
711
812
|
}
|
|
712
813
|
}
|
|
713
814
|
|
|
714
815
|
let freeId = 0;
|
|
715
816
|
|
|
716
817
|
class FreeVar extends Named {
|
|
717
|
-
|
|
818
|
+
/**
|
|
819
|
+
* @desc A named variable.
|
|
820
|
+
*
|
|
821
|
+
* Given the functional nature of combinatory logic, variables are treated
|
|
822
|
+
* as functions that we don't know how to evaluate just yet.
|
|
823
|
+
*
|
|
824
|
+
* By default, two different variables even with the same name are considered different.
|
|
825
|
+
* They display it via a hidden id property.
|
|
826
|
+
*
|
|
827
|
+
* If a scope object is given, however, two variables with the same name and scope
|
|
828
|
+
* are considered identical.
|
|
829
|
+
*
|
|
830
|
+
* @param {string} name - name of the variable
|
|
831
|
+
* @param {any} scope - an object representing where the variable belongs to.
|
|
832
|
+
*/
|
|
833
|
+
constructor (name, scope) {
|
|
718
834
|
super(name);
|
|
719
835
|
this.id = ++freeId;
|
|
836
|
+
// TODO temp compatibility mode
|
|
837
|
+
this.scope = scope === undefined ? this : scope;
|
|
720
838
|
}
|
|
721
839
|
|
|
722
840
|
weight () {
|
|
723
841
|
return 0;
|
|
724
842
|
}
|
|
725
843
|
|
|
726
|
-
|
|
727
|
-
|
|
844
|
+
diff (other, swap = false) {
|
|
845
|
+
if (!(other instanceof FreeVar))
|
|
846
|
+
return super.diff(other, swap);
|
|
847
|
+
if (this.name === other.name && this.scope === other.scope)
|
|
848
|
+
return null;
|
|
849
|
+
const lhs = this.name + '[' + this.id + ']';
|
|
850
|
+
const rhs = other.name + '[' + other.id + ']';
|
|
851
|
+
return swap
|
|
852
|
+
? '[' + rhs + ' != ' + lhs + ']'
|
|
853
|
+
: '[' + lhs + ' != ' + rhs + ']';
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
subst (search, replace) {
|
|
857
|
+
if (search instanceof FreeVar && search.name === this.name && search.scope === this.scope)
|
|
858
|
+
return replace;
|
|
859
|
+
return null;
|
|
728
860
|
}
|
|
729
861
|
|
|
730
862
|
_format (options, nargs) {
|
|
731
|
-
|
|
863
|
+
const name = options.html ? this.fancyName ?? this.name : this.name;
|
|
864
|
+
return options.var[0] + name + options.var[1];
|
|
732
865
|
}
|
|
733
866
|
}
|
|
734
867
|
|
|
@@ -754,9 +887,9 @@ class Native extends Named {
|
|
|
754
887
|
// setup essentials
|
|
755
888
|
this.invoke = impl;
|
|
756
889
|
|
|
757
|
-
// TODO
|
|
758
|
-
// try to bootstrap and
|
|
759
|
-
const guess = (opt.canonize ?? true) ? this.
|
|
890
|
+
// TODO infer lazily (on demand, only once); app capabilities such as discard and duplicate
|
|
891
|
+
// try to bootstrap and infer some of our properties
|
|
892
|
+
const guess = (opt.canonize ?? true) ? this.infer() : { normal: false };
|
|
760
893
|
|
|
761
894
|
this.arity = opt.arity || guess.arity || 1;
|
|
762
895
|
this.note = opt.note ?? guess.expr?.format({ terse: true, html: true, lambda: ['', ' ↦ ', ''] });
|
|
@@ -765,7 +898,7 @@ class Native extends Named {
|
|
|
765
898
|
_rski (options) {
|
|
766
899
|
if (this === native.I || this === native.K || this === native.S || (options.steps >= options.max))
|
|
767
900
|
return this;
|
|
768
|
-
const canon = this.
|
|
901
|
+
const canon = this.infer().expr;
|
|
769
902
|
if (!canon)
|
|
770
903
|
return this;
|
|
771
904
|
options.steps++;
|
|
@@ -773,6 +906,11 @@ class Native extends Named {
|
|
|
773
906
|
}
|
|
774
907
|
}
|
|
775
908
|
|
|
909
|
+
// predefined global combinator list
|
|
910
|
+
// it is required by toSKI method, otherwise it could've as well be in parse.js
|
|
911
|
+
/**
|
|
912
|
+
* @type {{[key: string]: Native}}
|
|
913
|
+
*/
|
|
776
914
|
const native = {};
|
|
777
915
|
function addNative (name, impl, opt) {
|
|
778
916
|
native[name] = new Native(name, impl, opt);
|
|
@@ -817,36 +955,59 @@ class Lambda extends Expr {
|
|
|
817
955
|
|
|
818
956
|
super();
|
|
819
957
|
|
|
820
|
-
// localize argument variable
|
|
821
|
-
const local = new FreeVar(arg.name);
|
|
958
|
+
// localize argument variable and bind it to oneself
|
|
959
|
+
const local = new FreeVar(arg.name, this);
|
|
822
960
|
this.arg = local;
|
|
823
961
|
this.impl = impl.subst(arg, local) ?? impl;
|
|
824
962
|
this.arity = 1;
|
|
825
963
|
}
|
|
826
964
|
|
|
827
|
-
getSymbols () {
|
|
828
|
-
const out = this.impl.getSymbols();
|
|
829
|
-
out.delete(this.arg);
|
|
830
|
-
out.set(Expr.lambdaPlaceholder, (out.get(Expr.lambdaPlaceholder) ?? 0) + 1);
|
|
831
|
-
return out;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
965
|
weight () {
|
|
835
966
|
return this.impl.weight() + 1;
|
|
836
967
|
}
|
|
837
968
|
|
|
838
|
-
|
|
969
|
+
_infer (options, preArgs = [], steps = 0) {
|
|
839
970
|
if (preArgs.length > options.maxArgs)
|
|
840
971
|
return { normal: false, steps };
|
|
841
972
|
|
|
842
973
|
const push = nthvar(preArgs.length + options.index);
|
|
843
|
-
return this.invoke(push).
|
|
974
|
+
return this.invoke(push)._infer(options, [...preArgs, push], steps + 1);
|
|
844
975
|
}
|
|
845
976
|
|
|
846
977
|
invoke (arg) {
|
|
847
978
|
return this.impl.subst(this.arg, arg) ?? this.impl;
|
|
848
979
|
}
|
|
849
980
|
|
|
981
|
+
traverse (change) {
|
|
982
|
+
const replaced = change(this);
|
|
983
|
+
if (replaced instanceof Expr)
|
|
984
|
+
return replaced;
|
|
985
|
+
|
|
986
|
+
// alas no proper shielding of self.arg is possible
|
|
987
|
+
const impl = this.impl.traverse(change);
|
|
988
|
+
|
|
989
|
+
if (!impl)
|
|
990
|
+
return null; // no changes
|
|
991
|
+
|
|
992
|
+
return new Lambda(this.arg, impl);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
any (predicate) {
|
|
996
|
+
return predicate(this) || this.impl.any(predicate);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
_fold (initial, combine) {
|
|
1000
|
+
const [value = initial, action = 'descend'] = unwrap(combine(initial, this));
|
|
1001
|
+
if (action === 'prune')
|
|
1002
|
+
return value;
|
|
1003
|
+
if (action === 'stop')
|
|
1004
|
+
return control.stop(value);
|
|
1005
|
+
const [iValue, iAction] = unwrap(this.impl._fold(value, combine));
|
|
1006
|
+
if (iAction === 'stop')
|
|
1007
|
+
return control.stop(iValue);
|
|
1008
|
+
return iValue ?? value;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
850
1011
|
subst (search, replace) {
|
|
851
1012
|
if (search === this.arg)
|
|
852
1013
|
return null;
|
|
@@ -865,41 +1026,32 @@ class Lambda extends Expr {
|
|
|
865
1026
|
options.steps++;
|
|
866
1027
|
if (impl === this.arg)
|
|
867
1028
|
return native.I;
|
|
868
|
-
if (!impl.
|
|
1029
|
+
if (!impl.any(e => e === this.arg))
|
|
869
1030
|
return native.K.apply(impl);
|
|
870
1031
|
if (impl instanceof App) {
|
|
871
|
-
const
|
|
1032
|
+
const { fun, arg } = impl;
|
|
872
1033
|
// try eta reduction
|
|
873
|
-
if (
|
|
874
|
-
return
|
|
1034
|
+
if (arg === this.arg && !fun.any(e => e === this.arg))
|
|
1035
|
+
return fun._rski(options);
|
|
875
1036
|
// fall back to S
|
|
876
1037
|
return native.S.apply(
|
|
877
|
-
(new Lambda(this.arg,
|
|
878
|
-
(new Lambda(this.arg,
|
|
1038
|
+
(new Lambda(this.arg, fun))._rski(options),
|
|
1039
|
+
(new Lambda(this.arg, arg))._rski(options)
|
|
879
1040
|
);
|
|
880
1041
|
}
|
|
881
1042
|
throw new Error('Don\'t know how to convert to SKI' + this);
|
|
882
1043
|
}
|
|
883
1044
|
|
|
884
|
-
|
|
885
|
-
const maybe = super._replace(pairs, opt);
|
|
886
|
-
if (maybe)
|
|
887
|
-
return maybe;
|
|
888
|
-
// TODO filter out terms containing this.arg
|
|
889
|
-
return new Lambda(this.arg, this.impl._replace(pairs, opt) ?? this.impl);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
equals (other) {
|
|
1045
|
+
diff (other, swap = false) {
|
|
893
1046
|
if (!(other instanceof Lambda))
|
|
894
|
-
return super.
|
|
895
|
-
|
|
896
|
-
const t = new FreeVar('t');
|
|
1047
|
+
return super.diff(other, swap);
|
|
897
1048
|
|
|
898
|
-
|
|
899
|
-
}
|
|
1049
|
+
const t = new FreeVar('t'); // TODO better placeholder name
|
|
900
1050
|
|
|
901
|
-
|
|
902
|
-
|
|
1051
|
+
const diff = this.invoke(t).diff(other.invoke(t), swap);
|
|
1052
|
+
if (diff)
|
|
1053
|
+
return '(t->' + diff + ')'; // parentheses required to avoid ambiguity
|
|
1054
|
+
return null;
|
|
903
1055
|
}
|
|
904
1056
|
|
|
905
1057
|
_format (options, nargs) {
|
|
@@ -944,10 +1096,14 @@ class Church extends Native {
|
|
|
944
1096
|
this.arity = 2;
|
|
945
1097
|
}
|
|
946
1098
|
|
|
947
|
-
|
|
948
|
-
if (other instanceof Church)
|
|
949
|
-
return
|
|
950
|
-
|
|
1099
|
+
diff (other, swap = false) {
|
|
1100
|
+
if (!(other instanceof Church))
|
|
1101
|
+
return super.diff(other, swap);
|
|
1102
|
+
if (this.n === other.n)
|
|
1103
|
+
return null;
|
|
1104
|
+
return swap
|
|
1105
|
+
? '[' + other.n + ' != ' + this.n + ']'
|
|
1106
|
+
: '[' + this.n + ' != ' + other.n + ']';
|
|
951
1107
|
}
|
|
952
1108
|
|
|
953
1109
|
_unspaced (arg) {
|
|
@@ -984,7 +1140,7 @@ class Alias extends Named {
|
|
|
984
1140
|
this.note = options.note;
|
|
985
1141
|
|
|
986
1142
|
const guess = options.canonize
|
|
987
|
-
? impl.
|
|
1143
|
+
? impl.infer({ max: options.max, maxArgs: options.maxArgs })
|
|
988
1144
|
: { normal: false };
|
|
989
1145
|
this.arity = (guess.proper && guess.arity) || 0;
|
|
990
1146
|
this.proper = guess.proper ?? false;
|
|
@@ -993,10 +1149,6 @@ class Alias extends Named {
|
|
|
993
1149
|
this.invoke = waitn(impl, this.arity);
|
|
994
1150
|
}
|
|
995
1151
|
|
|
996
|
-
getSymbols () {
|
|
997
|
-
return this.terminal ? new Map([[this, 1]]) : this.impl.getSymbols();
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
1152
|
weight () {
|
|
1001
1153
|
return this.terminal ? 1 : this.impl.weight();
|
|
1002
1154
|
}
|
|
@@ -1005,14 +1157,34 @@ class Alias extends Named {
|
|
|
1005
1157
|
return this.impl.expand();
|
|
1006
1158
|
}
|
|
1007
1159
|
|
|
1160
|
+
traverse (change) {
|
|
1161
|
+
return change(this) ?? this.impl.traverse(change);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
any (predicate) {
|
|
1165
|
+
return predicate(this) || this.impl.any(predicate);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
_fold (initial, combine) {
|
|
1169
|
+
const [value = initial, action = 'descend'] = unwrap(combine(initial, this));
|
|
1170
|
+
if (action === 'prune')
|
|
1171
|
+
return value;
|
|
1172
|
+
if (action === 'stop')
|
|
1173
|
+
return control.stop(value);
|
|
1174
|
+
const [iValue, iAction] = unwrap(this.impl._fold(value, combine));
|
|
1175
|
+
if (iAction === 'stop')
|
|
1176
|
+
return control.stop(iValue);
|
|
1177
|
+
return iValue ?? value;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1008
1180
|
subst (search, replace) {
|
|
1009
1181
|
if (this === search)
|
|
1010
1182
|
return replace;
|
|
1011
1183
|
return this.impl.subst(search, replace);
|
|
1012
1184
|
}
|
|
1013
1185
|
|
|
1014
|
-
|
|
1015
|
-
return this.impl.
|
|
1186
|
+
_infer (options, preArgs = [], steps = 0) {
|
|
1187
|
+
return this.impl._infer(options, preArgs, steps);
|
|
1016
1188
|
}
|
|
1017
1189
|
|
|
1018
1190
|
/**
|
|
@@ -1027,16 +1199,10 @@ class Alias extends Named {
|
|
|
1027
1199
|
return { expr: this.impl, steps: 0, changed: true };
|
|
1028
1200
|
}
|
|
1029
1201
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
equals (other) {
|
|
1035
|
-
return other.equals(this.impl);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
contains (other) {
|
|
1039
|
-
return this.impl.contains(other);
|
|
1202
|
+
diff (other, swap = false) {
|
|
1203
|
+
if (this === other)
|
|
1204
|
+
return null;
|
|
1205
|
+
return other.diff(this.impl, !swap);
|
|
1040
1206
|
}
|
|
1041
1207
|
|
|
1042
1208
|
_rski (options) {
|
|
@@ -1090,16 +1256,6 @@ addNative(
|
|
|
1090
1256
|
}
|
|
1091
1257
|
);
|
|
1092
1258
|
|
|
1093
|
-
// A global value meaning "lambda is used somewhere in this expression"
|
|
1094
|
-
// Can't be used (at least for now) to construct lambda expressions, or anything at all.
|
|
1095
|
-
// See also getSymbols().
|
|
1096
|
-
Expr.lambdaPlaceholder = new Native('->', x => x, {
|
|
1097
|
-
arity: 1,
|
|
1098
|
-
canonize: false,
|
|
1099
|
-
note: 'Lambda placeholder',
|
|
1100
|
-
apply: x => { throw new Error('Attempt to use a placeholder in expression') }
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
1259
|
// utility functions dependent on Expr* classes, in alphabetical order
|
|
1104
1260
|
|
|
1105
1261
|
/**
|
|
@@ -1157,9 +1313,28 @@ function declare (inventory) {
|
|
|
1157
1313
|
}
|
|
1158
1314
|
|
|
1159
1315
|
function maybeLambda (args, expr, caps = {}) {
|
|
1160
|
-
const
|
|
1316
|
+
const count = new Array(args.length).fill(0);
|
|
1317
|
+
let proper = true;
|
|
1318
|
+
expr.traverse(e => {
|
|
1319
|
+
if (e instanceof FreeVar) {
|
|
1320
|
+
const index = args.findIndex(a => a.name === e.name);
|
|
1321
|
+
if (index >= 0) {
|
|
1322
|
+
count[index]++;
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (!(e instanceof App))
|
|
1327
|
+
proper = false;
|
|
1328
|
+
});
|
|
1161
1329
|
|
|
1162
|
-
const
|
|
1330
|
+
const skip = new Set();
|
|
1331
|
+
const dup = new Set();
|
|
1332
|
+
for (let i = 0; i < args.length; i++) {
|
|
1333
|
+
if (count[i] === 0)
|
|
1334
|
+
skip.add(i);
|
|
1335
|
+
else if (count[i] > 1)
|
|
1336
|
+
dup.add(i);
|
|
1337
|
+
}
|
|
1163
1338
|
|
|
1164
1339
|
return {
|
|
1165
1340
|
expr: args.length ? new Lambda(args, expr) : expr,
|
|
@@ -1168,27 +1343,10 @@ function maybeLambda (args, expr, caps = {}) {
|
|
|
1168
1343
|
...(dup.size ? { dup } : {}),
|
|
1169
1344
|
duplicate: !!dup.size || caps.duplicate || false,
|
|
1170
1345
|
discard: !!skip.size || caps.discard || false,
|
|
1171
|
-
proper
|
|
1346
|
+
proper,
|
|
1172
1347
|
};
|
|
1173
1348
|
}
|
|
1174
1349
|
|
|
1175
|
-
function naiveCanonize (expr) {
|
|
1176
|
-
if (expr instanceof App)
|
|
1177
|
-
return naiveCanonize(expr.fun).apply(naiveCanonize(expr.arg));
|
|
1178
|
-
|
|
1179
|
-
if (expr instanceof Lambda)
|
|
1180
|
-
return new Lambda(expr.arg, naiveCanonize(expr.impl));
|
|
1181
|
-
|
|
1182
|
-
if (expr instanceof Alias)
|
|
1183
|
-
return naiveCanonize(expr.impl);
|
|
1184
|
-
|
|
1185
|
-
const canon = expr.guess();
|
|
1186
|
-
if (canon.expr)
|
|
1187
|
-
return canon.expr;
|
|
1188
|
-
|
|
1189
|
-
throw new Error('Failed to canonize expression: ' + expr);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
1350
|
function nthvar (n) {
|
|
1193
1351
|
return new FreeVar('abcdefgh'[n] ?? 'x' + n);
|
|
1194
1352
|
}
|
|
@@ -1226,7 +1384,7 @@ function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
|
|
|
1226
1384
|
// fun * arg Descartes product
|
|
1227
1385
|
if (expr instanceof App) {
|
|
1228
1386
|
// try to split into fun+arg, then try canonization but exposing each step
|
|
1229
|
-
let
|
|
1387
|
+
let { fun, arg } = expr;
|
|
1230
1388
|
|
|
1231
1389
|
for (const term of simplifyLambda(fun, options, state)) {
|
|
1232
1390
|
const candidate = term.expr.apply(arg);
|
|
@@ -1246,10 +1404,14 @@ function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
|
|
|
1246
1404
|
}
|
|
1247
1405
|
}
|
|
1248
1406
|
|
|
1249
|
-
const canon = expr.
|
|
1407
|
+
const canon = expr.infer({ max: options.max, maxArgs: options.maxArgs });
|
|
1250
1408
|
state.steps += canon.steps;
|
|
1251
1409
|
if (canon.expr && canon.expr.weight() < maxWeight)
|
|
1252
1410
|
yield { expr: canon.expr, steps: state.steps, comment: '(canonical)' };
|
|
1253
1411
|
}
|
|
1254
1412
|
|
|
1255
|
-
|
|
1413
|
+
Expr.declare = declare;
|
|
1414
|
+
Expr.native = native;
|
|
1415
|
+
Expr.control = control;
|
|
1416
|
+
|
|
1417
|
+
module.exports = { Expr, App, Named, FreeVar, Lambda, Native, Alias, Church };
|