@dallaylaen/ski-interpreter 1.0.0 → 1.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 +57 -0
- package/README.md +8 -7
- package/lib/expr.js +321 -235
- package/lib/parser.js +3 -0
- package/lib/quest.js +72 -16
- package/lib/util.js +10 -6
- package/package.json +11 -4
- package/types/lib/expr.d.ts +75 -46
- package/types/lib/quest.d.ts +29 -7
- package/types/lib/util.d.ts +1 -1
package/lib/expr.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { skipDup, isSubset } = require('./util');
|
|
2
4
|
|
|
3
5
|
const globalOptions = {
|
|
4
6
|
terse: true,
|
|
@@ -13,7 +15,6 @@ class Expr {
|
|
|
13
15
|
constructor () {
|
|
14
16
|
if (new.target === Expr)
|
|
15
17
|
throw new Error('Attempt to instantiate abstract class Expr');
|
|
16
|
-
this.arity = Infinity;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -79,6 +80,43 @@ class Expr {
|
|
|
79
80
|
return new Map([[this, 1]]);
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
* @desc Given a list of pairs of term, replaces every subtree
|
|
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
|
|
90
|
+
*
|
|
91
|
+
* @param {(Expr | [find: Expr, replace: Expr])[]} terms
|
|
92
|
+
* @param {Object} [opt] - options
|
|
93
|
+
* @return {Expr}
|
|
94
|
+
*/
|
|
95
|
+
replace (terms, opt = {}) {
|
|
96
|
+
const pairs = [];
|
|
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;
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
/**
|
|
83
121
|
* @desc rought estimate of the complexity of the term
|
|
84
122
|
* @return {number}
|
|
@@ -88,57 +126,75 @@ class Expr {
|
|
|
88
126
|
}
|
|
89
127
|
|
|
90
128
|
/**
|
|
129
|
+
* @desc Try to find an equivalent lambda term for the expression,
|
|
130
|
+
* returning also the term's arity and some other properties.
|
|
131
|
+
*
|
|
132
|
+
* This is used internally when declaring a Native term,
|
|
133
|
+
* unless {canonize: false} is used.
|
|
134
|
+
*
|
|
135
|
+
* As of current it only recognizes terms that have a normal form,
|
|
136
|
+
* perhaps after adding some variables. This may change in the future.
|
|
91
137
|
*
|
|
92
|
-
*
|
|
138
|
+
* Use lambdify() if you want to get a lambda term in any case.
|
|
139
|
+
*
|
|
140
|
+
* @param {{max: number?, maxArgs: number?}} options
|
|
93
141
|
* @return {{
|
|
94
|
-
*
|
|
95
|
-
*
|
|
142
|
+
* normal: boolean,
|
|
143
|
+
* steps: number,
|
|
144
|
+
* expr: Expr?,
|
|
96
145
|
* arity: number?,
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* skip: Set<number
|
|
146
|
+
* proper: boolean?,
|
|
147
|
+
* discard: boolean?,
|
|
148
|
+
* duplicate: boolean?,
|
|
149
|
+
* skip: Set<number>?,
|
|
150
|
+
* dup: Set<number>?
|
|
101
151
|
* }}
|
|
102
152
|
*/
|
|
103
|
-
|
|
153
|
+
guess (options = {}) {
|
|
104
154
|
const max = options.max ?? globalOptions.max;
|
|
105
155
|
const maxArgs = options.maxArgs ?? globalOptions.maxArgs;
|
|
156
|
+
const out = this._guess({ max, maxArgs, index: 0 });
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
106
159
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const symbols = expr.getSymbols();
|
|
119
|
-
const skip = missingIndices(jar, symbols);
|
|
120
|
-
const proper = isSubset(symbols.keys(), new Set(jar));
|
|
121
|
-
const duplicates = [...symbols.entries()].filter(([_, v]) => v > 1);
|
|
122
|
-
const linear = proper && skip.size === 0 && duplicates.length === 0;
|
|
123
|
-
return {
|
|
124
|
-
arity: i,
|
|
125
|
-
found: true,
|
|
126
|
-
canonical: maybeLambda(jar, expr),
|
|
127
|
-
proper,
|
|
128
|
-
linear,
|
|
129
|
-
steps,
|
|
130
|
-
...(skip.size ? { skip } : {}),
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
const next = new FreeVar('abcdefgh'[i] ?? 'x' + i);
|
|
134
|
-
jar.push(next);
|
|
135
|
-
expr = expr.apply(next);
|
|
160
|
+
_guess (options, preArgs = [], steps = 0) {
|
|
161
|
+
if (preArgs.length > options.maxArgs || steps > options.max)
|
|
162
|
+
return { normal: false, steps };
|
|
163
|
+
|
|
164
|
+
// happy case
|
|
165
|
+
if (this.freeOnly()) {
|
|
166
|
+
return {
|
|
167
|
+
normal: true,
|
|
168
|
+
steps,
|
|
169
|
+
...maybeLambda(preArgs, this),
|
|
170
|
+
};
|
|
136
171
|
}
|
|
137
172
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
173
|
+
// try reaching the normal form
|
|
174
|
+
const next = this.run({ max: (options.max - steps) / 3 });
|
|
175
|
+
steps += next.steps;
|
|
176
|
+
if (!next.final)
|
|
177
|
+
return { normal: false, steps };
|
|
178
|
+
|
|
179
|
+
// normal form != this, redo exercise
|
|
180
|
+
if (next.steps !== 0)
|
|
181
|
+
return next.expr._guess(options, preArgs, steps);
|
|
182
|
+
|
|
183
|
+
if (this._firstVar())
|
|
184
|
+
return { normal: false, steps };
|
|
185
|
+
|
|
186
|
+
const push = nthvar(preArgs.length + options.index);
|
|
187
|
+
return this.apply(push)._guess(options, [...preArgs, push], steps);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_aslist () {
|
|
191
|
+
return [this];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_firstVar () {
|
|
195
|
+
// boolean, whether the expression starts with a free variable
|
|
196
|
+
// used by guess()
|
|
197
|
+
return false;
|
|
142
198
|
}
|
|
143
199
|
|
|
144
200
|
/**
|
|
@@ -197,16 +253,6 @@ class Expr {
|
|
|
197
253
|
return this;
|
|
198
254
|
}
|
|
199
255
|
|
|
200
|
-
/**
|
|
201
|
-
* @desc Whether the term will reduce further if given more arguments.
|
|
202
|
-
* In practice, equivalent to "starts with a FreeVar"
|
|
203
|
-
* Used by canonize (duh...)
|
|
204
|
-
* @return {boolean}
|
|
205
|
-
*/
|
|
206
|
-
wantsArgs () {
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
256
|
/**
|
|
211
257
|
* Apply self to list of given args.
|
|
212
258
|
* Normally, only native combinators know how to do it.
|
|
@@ -249,7 +295,8 @@ class Expr {
|
|
|
249
295
|
}
|
|
250
296
|
let expr = args ? this.apply(...args) : this;
|
|
251
297
|
let steps = opt.steps ?? 0;
|
|
252
|
-
|
|
298
|
+
// make sure we make at least 1 step, to tell whether we've reached the normal form
|
|
299
|
+
const max = Math.max(opt.max ?? globalOptions.max, 1) + steps;
|
|
253
300
|
let final = false;
|
|
254
301
|
for (; steps < max; ) {
|
|
255
302
|
const next = expr.step();
|
|
@@ -293,28 +340,38 @@ class Expr {
|
|
|
293
340
|
}
|
|
294
341
|
|
|
295
342
|
/**
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
343
|
+
*
|
|
344
|
+
* @param {Expr} other
|
|
345
|
+
* @return {boolean}
|
|
346
|
+
*/
|
|
300
347
|
equals (other) {
|
|
301
|
-
|
|
348
|
+
if (this === other)
|
|
349
|
+
return true;
|
|
350
|
+
if (other instanceof Alias)
|
|
351
|
+
return other.equals(this);
|
|
352
|
+
return false;
|
|
302
353
|
}
|
|
303
354
|
|
|
304
355
|
contains (other) {
|
|
305
356
|
return this === other || this.equals(other);
|
|
306
357
|
}
|
|
307
358
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
359
|
+
/**
|
|
360
|
+
* @desc Assert expression equality. Can be used in tests.
|
|
361
|
+
* @param {Expr} expected
|
|
362
|
+
* @param {string} comment
|
|
363
|
+
*/
|
|
364
|
+
expect (expected, comment = '') {
|
|
365
|
+
comment = comment ? comment + ': ' : '';
|
|
366
|
+
if (!(expected instanceof Expr))
|
|
367
|
+
throw new Error(comment + 'attempt to expect a combinator to equal something else: ' + expected);
|
|
368
|
+
if (this.equals(expected))
|
|
312
369
|
return;
|
|
313
370
|
|
|
314
371
|
// TODO wanna use AssertionError but webpack doesn't recognize it
|
|
315
372
|
// still the below hack works for mocha-based tests.
|
|
316
|
-
const poorMans = new Error('
|
|
317
|
-
poorMans.expected =
|
|
373
|
+
const poorMans = new Error(comment + 'found term ' + this + ' but expected ' + expected);
|
|
374
|
+
poorMans.expected = expected.toString();
|
|
318
375
|
poorMans.actual = this.toString();
|
|
319
376
|
throw poorMans;
|
|
320
377
|
}
|
|
@@ -330,10 +387,11 @@ class Expr {
|
|
|
330
387
|
}
|
|
331
388
|
|
|
332
389
|
/**
|
|
333
|
-
*
|
|
390
|
+
* @desc Whether the expression needs parentheses when printed.
|
|
391
|
+
* @param {boolean} [first] - whether this is the first term in a sequence
|
|
334
392
|
* @return {boolean}
|
|
335
393
|
*/
|
|
336
|
-
needsParens () {
|
|
394
|
+
needsParens (first) {
|
|
337
395
|
return false;
|
|
338
396
|
}
|
|
339
397
|
|
|
@@ -346,22 +404,6 @@ class Expr {
|
|
|
346
404
|
}
|
|
347
405
|
}
|
|
348
406
|
|
|
349
|
-
/**
|
|
350
|
-
* Constants that define when whitespace between terms may be omitted in App.toString()
|
|
351
|
-
*/
|
|
352
|
-
const BITS = 4;
|
|
353
|
-
const [T_UNKNOWN, T_PARENS, T_UPPER, T_LOWER]
|
|
354
|
-
= (function * () { for (let i = 0; ; yield i++); })();
|
|
355
|
-
const canLump = new Set([
|
|
356
|
-
(T_PARENS << BITS) + T_PARENS,
|
|
357
|
-
(T_PARENS << BITS) + T_UPPER,
|
|
358
|
-
(T_UPPER << BITS) + T_PARENS,
|
|
359
|
-
(T_UPPER << BITS) + T_UPPER,
|
|
360
|
-
(T_UPPER << BITS) + T_LOWER,
|
|
361
|
-
(T_LOWER << BITS) + T_PARENS,
|
|
362
|
-
(T_UNKNOWN << BITS) + T_PARENS,
|
|
363
|
-
]);
|
|
364
|
-
|
|
365
407
|
class App extends Expr {
|
|
366
408
|
/**
|
|
367
409
|
* @desc Application of fun() to args.
|
|
@@ -373,67 +415,104 @@ class App extends Expr {
|
|
|
373
415
|
if (args.length === 0)
|
|
374
416
|
throw new Error('Attempt to create an application with no arguments (likely interpreter bug)');
|
|
375
417
|
super();
|
|
376
|
-
|
|
377
|
-
this.
|
|
418
|
+
|
|
419
|
+
this.arg = args.pop();
|
|
420
|
+
this.fun = args.length ? new App(fun, ...args) : fun;
|
|
378
421
|
this.final = false;
|
|
422
|
+
this.arity = this.fun.arity > 0 ? this.fun.arity - 1 : 0;
|
|
379
423
|
}
|
|
380
424
|
|
|
381
425
|
weight () {
|
|
382
|
-
return this.
|
|
426
|
+
return this.fun.weight() + this.arg.weight();
|
|
383
427
|
}
|
|
384
428
|
|
|
385
429
|
getSymbols () {
|
|
386
430
|
const out = this.fun.getSymbols();
|
|
387
|
-
for (const
|
|
388
|
-
|
|
389
|
-
out.set(key, (out.get(key) ?? 0) + value);
|
|
390
|
-
}
|
|
431
|
+
for (const [key, value] of this.arg.getSymbols())
|
|
432
|
+
out.set(key, (out.get(key) ?? 0) + value);
|
|
391
433
|
return out;
|
|
392
434
|
}
|
|
393
435
|
|
|
394
|
-
|
|
395
|
-
|
|
436
|
+
_guess (options, preArgs = [], steps = 0) {
|
|
437
|
+
if (preArgs.length > options.maxArgs || steps > options.max)
|
|
438
|
+
return { normal: false, steps };
|
|
439
|
+
|
|
440
|
+
/*
|
|
441
|
+
* inside and App there are 3 main possibilities:
|
|
442
|
+
* 1) The parent guess() actually is able to do the job. Then we just proxy the result.
|
|
443
|
+
* 2) Both `fun` and `arg` form good enough lambda terms. Then lump them together & return.
|
|
444
|
+
* 3) We literally have no idea, so we just pick the shortest defined term from the above.
|
|
445
|
+
*/
|
|
446
|
+
|
|
447
|
+
const proxy = super._guess(options, preArgs, steps);
|
|
448
|
+
if (proxy.normal)
|
|
449
|
+
return proxy;
|
|
450
|
+
steps = proxy.steps; // reimport extra iterations
|
|
451
|
+
|
|
452
|
+
const [first, ...list] = this._aslist();
|
|
453
|
+
if (!(first instanceof FreeVar))
|
|
454
|
+
return { normal: false, steps }
|
|
455
|
+
// TODO maybe do it later
|
|
456
|
+
|
|
457
|
+
let discard = false;
|
|
458
|
+
let duplicate = false;
|
|
459
|
+
const out = [];
|
|
460
|
+
for (const term of list) {
|
|
461
|
+
const guess = term._guess({
|
|
462
|
+
...options,
|
|
463
|
+
maxArgs: options.maxArgs - preArgs.length,
|
|
464
|
+
max: options.max - steps,
|
|
465
|
+
index: preArgs.length + options.index,
|
|
466
|
+
});
|
|
467
|
+
steps += guess.steps;
|
|
468
|
+
if (!guess.normal)
|
|
469
|
+
return { normal: false, steps };
|
|
470
|
+
out.push(guess.expr);
|
|
471
|
+
discard = discard || guess.discard;
|
|
472
|
+
duplicate = duplicate || guess.duplicate;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
normal: true,
|
|
477
|
+
steps,
|
|
478
|
+
...maybeLambda(preArgs, new App(first, ...out), {
|
|
479
|
+
discard,
|
|
480
|
+
duplicate,
|
|
481
|
+
}),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
_firstVar () {
|
|
486
|
+
return this.fun._firstVar();
|
|
396
487
|
}
|
|
397
488
|
|
|
398
489
|
apply (...args) {
|
|
399
490
|
if (args.length === 0)
|
|
400
491
|
return this;
|
|
401
|
-
return
|
|
492
|
+
return new App(this, ...args);
|
|
402
493
|
}
|
|
403
494
|
|
|
404
495
|
expand () {
|
|
405
|
-
return this.fun.expand().apply(
|
|
496
|
+
return this.fun.expand().apply(this.arg.expand());
|
|
406
497
|
}
|
|
407
498
|
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
499
|
+
_replace (pairs, opt) {
|
|
500
|
+
const maybe = super._replace(pairs, opt);
|
|
501
|
+
if (maybe)
|
|
502
|
+
return maybe;
|
|
503
|
+
const [fun, arg] = this.split();
|
|
504
|
+
return (fun._replace(pairs, opt) ?? fun).apply(arg._replace(pairs, opt) ?? arg);
|
|
414
505
|
}
|
|
415
506
|
|
|
416
507
|
renameVars (seq) {
|
|
417
|
-
|
|
418
|
-
const args = this.args.map(x => x.renameVars(seq));
|
|
419
|
-
return fun.apply(...args);
|
|
508
|
+
return this.fun.renameVars(seq).apply(this.arg.renameVars(seq));
|
|
420
509
|
}
|
|
421
510
|
|
|
422
511
|
subst (plug, value) {
|
|
423
512
|
const fun = this.fun.subst(plug, value);
|
|
424
|
-
|
|
425
|
-
const args = [];
|
|
426
|
-
for (const x of this.args) {
|
|
427
|
-
const next = x.subst(plug, value);
|
|
428
|
-
if (next === null)
|
|
429
|
-
args.push(x);
|
|
430
|
-
else {
|
|
431
|
-
args.push(next);
|
|
432
|
-
change++;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
513
|
+
const arg = this.arg.subst(plug, value);
|
|
435
514
|
|
|
436
|
-
return
|
|
515
|
+
return (fun || arg) ? (fun ?? this.fun).apply(arg ?? this.arg) : null;
|
|
437
516
|
}
|
|
438
517
|
|
|
439
518
|
/**
|
|
@@ -443,34 +522,39 @@ class App extends Expr {
|
|
|
443
522
|
step () {
|
|
444
523
|
// normal reduction order: first try root, then at most 1 step
|
|
445
524
|
if (!this.final) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
525
|
+
if (this.arity === 0) {
|
|
526
|
+
// aha! we have just fulfilled some previous function's argument demands
|
|
527
|
+
const reduced = this.fun.reduce([this.arg]);
|
|
528
|
+
// should always be true, but whatever
|
|
529
|
+
if (reduced)
|
|
530
|
+
return { expr: reduced, steps: 1, changed: true };
|
|
531
|
+
}
|
|
450
532
|
// now try recursing
|
|
451
533
|
|
|
452
534
|
const fun = this.fun.step();
|
|
453
535
|
if (fun.changed)
|
|
454
|
-
return { expr: fun.expr.apply(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
args[i] = next.expr;
|
|
462
|
-
return { expr: this.fun.apply(...args), steps: next.steps, changed: true };
|
|
463
|
-
}
|
|
536
|
+
return { expr: fun.expr.apply(this.arg), steps: fun.steps, changed: true };
|
|
537
|
+
|
|
538
|
+
const arg = this.arg.step();
|
|
539
|
+
if (arg.changed)
|
|
540
|
+
return { expr: this.fun.apply(arg.expr), steps: arg.steps, changed: true };
|
|
541
|
+
|
|
542
|
+
this.final = true;
|
|
464
543
|
}
|
|
465
|
-
this.final = true;
|
|
466
544
|
return { expr: this, steps: 0, changed: false };
|
|
467
545
|
}
|
|
468
546
|
|
|
547
|
+
reduce (args) {
|
|
548
|
+
return this.fun.reduce([this.arg, ...args]);
|
|
549
|
+
}
|
|
550
|
+
|
|
469
551
|
split () {
|
|
470
552
|
// pretend we are an elegant (cons fun arg) and not a sleazy imperative array
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
553
|
+
return [this.fun, this.arg];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
_aslist () {
|
|
557
|
+
return [...this.fun._aslist(), this.arg];
|
|
474
558
|
}
|
|
475
559
|
|
|
476
560
|
/**
|
|
@@ -481,63 +565,39 @@ class App extends Expr {
|
|
|
481
565
|
_rski (options) {
|
|
482
566
|
if (options.steps >= options.max)
|
|
483
567
|
return this;
|
|
484
|
-
return this.fun._rski(options).apply(
|
|
568
|
+
return this.fun._rski(options).apply(this.arg._rski(options));
|
|
485
569
|
}
|
|
486
570
|
|
|
487
571
|
equals (other) {
|
|
488
572
|
if (!(other instanceof App))
|
|
489
|
-
return
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (!this.fun.equals(other.fun))
|
|
493
|
-
return false;
|
|
494
|
-
for (let i = 0; i < this.args.length; i++) {
|
|
495
|
-
if (!this.args[i].equals(other.args[i]))
|
|
496
|
-
return false;
|
|
497
|
-
}
|
|
498
|
-
return true;
|
|
573
|
+
return super.equals(other);
|
|
574
|
+
|
|
575
|
+
return this.fun.equals(other.fun) && this.arg.equals(other.arg);
|
|
499
576
|
}
|
|
500
577
|
|
|
501
578
|
contains (other) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
return super.contains(other);
|
|
579
|
+
return this.fun.contains(other) || this.arg.contains(other) || super.contains(other);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
needsParens (first) {
|
|
583
|
+
return !first;
|
|
509
584
|
}
|
|
510
585
|
|
|
511
586
|
toString (opt = {}) {
|
|
587
|
+
const fun = this.fun.toString(opt);
|
|
588
|
+
const root = this.fun.needsParens(true) ? '(' + fun + ')' : fun;
|
|
512
589
|
if (opt.terse ?? globalOptions.terse) {
|
|
513
|
-
|
|
514
|
-
let
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
// no special treatment for numerals, skip
|
|
525
|
-
;
|
|
526
|
-
else if (out.length !== 0 || term.needsParens()) {
|
|
527
|
-
s = '(' + s + ')';
|
|
528
|
-
newType = T_PARENS;
|
|
529
|
-
}
|
|
530
|
-
if (!canLump.has((oldType << BITS) | newType) && out.length > 0)
|
|
531
|
-
out.push(' ');
|
|
532
|
-
out.push(s);
|
|
533
|
-
oldType = newType;
|
|
534
|
-
}
|
|
535
|
-
return out.join('');
|
|
536
|
-
} else {
|
|
537
|
-
const fun = this.fun.toString(opt);
|
|
538
|
-
const root = this.fun.needsParens() ? '(' + fun + ')' : fun;
|
|
539
|
-
return root + this.args.map(x => '(' + x.toString(opt) + ')').join('');
|
|
540
|
-
}
|
|
590
|
+
// terse mode: omit whitespace and parens if possible
|
|
591
|
+
let arg = this.arg.toString(opt);
|
|
592
|
+
if (this.arg.needsParens(false))
|
|
593
|
+
arg = '(' + arg + ')';
|
|
594
|
+
const space = (root.match(/\)$/) || arg.match(/^\(/))
|
|
595
|
+
|| (root.match(/[A-Z]$/) && arg.match(/^[a-z]/i))
|
|
596
|
+
? ''
|
|
597
|
+
: ' ';
|
|
598
|
+
return root + space + arg;
|
|
599
|
+
} else
|
|
600
|
+
return root + '(' + this.arg.toString(opt) + ')';
|
|
541
601
|
}
|
|
542
602
|
}
|
|
543
603
|
|
|
@@ -576,8 +636,8 @@ class FreeVar extends Named {
|
|
|
576
636
|
return 0;
|
|
577
637
|
}
|
|
578
638
|
|
|
579
|
-
|
|
580
|
-
return
|
|
639
|
+
_firstVar () {
|
|
640
|
+
return true;
|
|
581
641
|
}
|
|
582
642
|
|
|
583
643
|
toString ( opt = {} ) {
|
|
@@ -609,12 +669,12 @@ class Native extends Named {
|
|
|
609
669
|
this.arity = opt.arity ?? 1;
|
|
610
670
|
|
|
611
671
|
// try to bootstrap and guess some of our properties
|
|
612
|
-
const guess = (opt.canonize ?? true) ? this.
|
|
672
|
+
const guess = (opt.canonize ?? true) ? this.guess() : { normal: false };
|
|
613
673
|
|
|
614
674
|
if (!opt.arity)
|
|
615
675
|
this.arity = guess.arity || 1;
|
|
616
676
|
|
|
617
|
-
this.note = opt.note ?? guess.
|
|
677
|
+
this.note = opt.note ?? guess.expr?.toString({ terse: true, html: true });
|
|
618
678
|
}
|
|
619
679
|
|
|
620
680
|
apply (...args) {
|
|
@@ -633,7 +693,7 @@ class Native extends Named {
|
|
|
633
693
|
_rski (options) {
|
|
634
694
|
if (this === native.I || this === native.K || this === native.S || (options.steps >= options.max))
|
|
635
695
|
return this;
|
|
636
|
-
const canon = this.
|
|
696
|
+
const canon = this.guess().expr;
|
|
637
697
|
if (!canon)
|
|
638
698
|
return this;
|
|
639
699
|
options.steps++;
|
|
@@ -711,6 +771,14 @@ class Lambda extends Expr {
|
|
|
711
771
|
return this.impl.weight() + 1;
|
|
712
772
|
}
|
|
713
773
|
|
|
774
|
+
_guess (options, preArgs = [], steps = 0) {
|
|
775
|
+
if (preArgs.length > options.maxArgs)
|
|
776
|
+
return { normal: false, steps };
|
|
777
|
+
|
|
778
|
+
const push = nthvar(preArgs.length + options.index);
|
|
779
|
+
return this.reduce([push])._guess(options, [...preArgs, push], steps + 1);
|
|
780
|
+
}
|
|
781
|
+
|
|
714
782
|
reduce (input) {
|
|
715
783
|
if (input.length === 0)
|
|
716
784
|
return null;
|
|
@@ -762,9 +830,17 @@ class Lambda extends Expr {
|
|
|
762
830
|
throw new Error('Don\'t know how to convert to SKI' + this);
|
|
763
831
|
}
|
|
764
832
|
|
|
833
|
+
_replace (pairs, opt) {
|
|
834
|
+
const maybe = super._replace(pairs, opt);
|
|
835
|
+
if (maybe)
|
|
836
|
+
return maybe;
|
|
837
|
+
// TODO filter out terms containing this.arg
|
|
838
|
+
return new Lambda(this.arg, this.impl._replace(pairs, opt) ?? this.impl);
|
|
839
|
+
}
|
|
840
|
+
|
|
765
841
|
equals (other) {
|
|
766
842
|
if (!(other instanceof Lambda))
|
|
767
|
-
return
|
|
843
|
+
return super.equals(other);
|
|
768
844
|
|
|
769
845
|
const t = new FreeVar('t');
|
|
770
846
|
|
|
@@ -780,7 +856,7 @@ class Lambda extends Expr {
|
|
|
780
856
|
return this.arg.toString(opt) + mapsto + this.impl.toString(opt);
|
|
781
857
|
}
|
|
782
858
|
|
|
783
|
-
needsParens () {
|
|
859
|
+
needsParens (first) {
|
|
784
860
|
return true;
|
|
785
861
|
}
|
|
786
862
|
}
|
|
@@ -807,7 +883,7 @@ class Church extends Native {
|
|
|
807
883
|
equals (other) {
|
|
808
884
|
if (other instanceof Church)
|
|
809
885
|
return this.n === other.n;
|
|
810
|
-
return
|
|
886
|
+
return super.equals(other);
|
|
811
887
|
}
|
|
812
888
|
}
|
|
813
889
|
|
|
@@ -826,12 +902,12 @@ class Alias extends Named {
|
|
|
826
902
|
this.note = options.note;
|
|
827
903
|
|
|
828
904
|
const guess = options.canonize
|
|
829
|
-
? impl.
|
|
830
|
-
: {
|
|
831
|
-
this.arity = (guess.
|
|
905
|
+
? impl.guess({ max: options.max, maxArgs: options.maxArgs })
|
|
906
|
+
: { normal: false };
|
|
907
|
+
this.arity = (guess.proper && guess.arity) || 0;
|
|
832
908
|
this.proper = guess.proper ?? false;
|
|
833
909
|
this.terminal = options.terminal ?? this.proper;
|
|
834
|
-
this.canonical = guess.
|
|
910
|
+
this.canonical = guess.expr;
|
|
835
911
|
}
|
|
836
912
|
|
|
837
913
|
getSymbols () {
|
|
@@ -850,6 +926,10 @@ class Alias extends Named {
|
|
|
850
926
|
return this.impl.subst(plug, value);
|
|
851
927
|
}
|
|
852
928
|
|
|
929
|
+
_guess (options, preArgs = [], steps = 0) {
|
|
930
|
+
return this.impl._guess(options, preArgs, steps);
|
|
931
|
+
}
|
|
932
|
+
|
|
853
933
|
/**
|
|
854
934
|
*
|
|
855
935
|
* @return {{expr: Expr, steps: number}}
|
|
@@ -868,8 +948,8 @@ class Alias extends Named {
|
|
|
868
948
|
return this.impl.apply(...args);
|
|
869
949
|
}
|
|
870
950
|
|
|
871
|
-
|
|
872
|
-
return this.impl.
|
|
951
|
+
_firstVar () {
|
|
952
|
+
return this.impl._firstVar();
|
|
873
953
|
}
|
|
874
954
|
|
|
875
955
|
equals (other) {
|
|
@@ -888,7 +968,7 @@ class Alias extends Named {
|
|
|
888
968
|
return this.outdated ? this.impl.toString(opt) : super.toString(opt);
|
|
889
969
|
}
|
|
890
970
|
|
|
891
|
-
needsParens () {
|
|
971
|
+
needsParens (first) {
|
|
892
972
|
return this.outdated ? this.impl.needsParens() : false;
|
|
893
973
|
}
|
|
894
974
|
}
|
|
@@ -906,15 +986,25 @@ addNative('+', x => y => z => y.apply(x.apply(y, z)), {
|
|
|
906
986
|
apply: arg => arg instanceof Church ? new Church(arg.n + 1) : null
|
|
907
987
|
});
|
|
908
988
|
|
|
909
|
-
function maybeLambda (args, expr) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
989
|
+
function maybeLambda (args, expr, caps = {}) {
|
|
990
|
+
const sym = expr.getSymbols();
|
|
991
|
+
|
|
992
|
+
const [skip, dup] = skipDup(args, sym);
|
|
993
|
+
|
|
994
|
+
return {
|
|
995
|
+
expr: args.length ? new Lambda(args, expr) : expr,
|
|
996
|
+
...(caps.synth ? {} : { arity: args.length }),
|
|
997
|
+
...(skip.size ? { skip } : {}),
|
|
998
|
+
...(dup.size ? { dup } : {}),
|
|
999
|
+
duplicate: !!dup.size || caps.duplicate || false,
|
|
1000
|
+
discard: !!skip.size || caps.discard || false,
|
|
1001
|
+
proper: isSubset(sym.keys(), new Set(args)),
|
|
1002
|
+
};
|
|
913
1003
|
}
|
|
914
1004
|
|
|
915
1005
|
function naiveCanonize (expr) {
|
|
916
1006
|
if (expr instanceof App)
|
|
917
|
-
return naiveCanonize(expr.fun).apply(
|
|
1007
|
+
return naiveCanonize(expr.fun).apply(naiveCanonize(expr.arg));
|
|
918
1008
|
|
|
919
1009
|
if (expr instanceof Lambda)
|
|
920
1010
|
return new Lambda(expr.arg, naiveCanonize(expr.impl));
|
|
@@ -922,9 +1012,9 @@ function naiveCanonize (expr) {
|
|
|
922
1012
|
if (expr instanceof Alias)
|
|
923
1013
|
return naiveCanonize(expr.impl);
|
|
924
1014
|
|
|
925
|
-
const canon = expr.
|
|
926
|
-
if (canon.
|
|
927
|
-
return canon.
|
|
1015
|
+
const canon = expr.guess();
|
|
1016
|
+
if (canon.expr)
|
|
1017
|
+
return canon.expr;
|
|
928
1018
|
|
|
929
1019
|
throw new Error('Failed to canonize expression: ' + expr);
|
|
930
1020
|
}
|
|
@@ -934,66 +1024,62 @@ function naiveCanonize (expr) {
|
|
|
934
1024
|
* @param {Expr} expr
|
|
935
1025
|
* @param {{max: number?, maxArgs: number?}} options
|
|
936
1026
|
* @param {number} maxWeight
|
|
937
|
-
* @
|
|
1027
|
+
* @yields {{expr: Expr, steps: number?, comment: string?}}
|
|
938
1028
|
*/
|
|
939
|
-
function * simplifyLambda (expr, options = {},
|
|
1029
|
+
function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
|
|
940
1030
|
// expr is a lambda, free variable, or an application thereof
|
|
941
1031
|
// we want to find an equivalent lambda term with less weight
|
|
942
1032
|
// which we do sequentially from leaves to the root of the AST
|
|
943
1033
|
|
|
1034
|
+
yield { expr, steps: state.steps, comment: '(self)' };
|
|
1035
|
+
|
|
944
1036
|
// short-circuit
|
|
945
|
-
if (expr.freeOnly())
|
|
946
|
-
if (expr.weight() < maxWeight)
|
|
947
|
-
yield { expr, steps: 0, comment: 'only free vars' };
|
|
1037
|
+
if (expr.freeOnly())
|
|
948
1038
|
return;
|
|
949
|
-
}
|
|
950
1039
|
|
|
951
|
-
let
|
|
952
|
-
|
|
1040
|
+
let maxWeight = expr.weight();
|
|
1041
|
+
|
|
1042
|
+
if (expr instanceof Lambda) {
|
|
1043
|
+
for (const term of simplifyLambda(expr.impl, options, state)) {
|
|
1044
|
+
const candidate = new Lambda(expr.arg, term.expr);
|
|
1045
|
+
if (candidate.weight() < maxWeight) {
|
|
1046
|
+
maxWeight = candidate.weight();
|
|
1047
|
+
yield { expr: candidate, steps: state.steps, comment: '(lambda)' + term.comment };
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
953
1051
|
|
|
954
1052
|
// fun * arg Descartes product
|
|
955
1053
|
if (expr instanceof App) {
|
|
956
1054
|
// try to split into fun+arg, then try canonization but exposing each step
|
|
957
|
-
|
|
1055
|
+
let [fun, arg] = expr.split();
|
|
958
1056
|
|
|
959
|
-
for (const term of simplifyLambda(fun, options,
|
|
1057
|
+
for (const term of simplifyLambda(fun, options, state)) {
|
|
960
1058
|
const candidate = term.expr.apply(arg);
|
|
961
|
-
steps = savedSteps + term.steps;
|
|
962
1059
|
if (candidate.weight() < maxWeight) {
|
|
963
1060
|
maxWeight = candidate.weight();
|
|
964
|
-
|
|
1061
|
+
fun = term.expr;
|
|
1062
|
+
yield { expr: candidate, steps: state.steps, comment: '(fun)' + term.comment };
|
|
965
1063
|
}
|
|
966
1064
|
}
|
|
967
|
-
savedSteps = steps;
|
|
968
1065
|
|
|
969
|
-
for (const term of simplifyLambda(arg, options,
|
|
1066
|
+
for (const term of simplifyLambda(arg, options, state)) {
|
|
970
1067
|
const candidate = fun.apply(term.expr);
|
|
971
|
-
steps = savedSteps + term.steps;
|
|
972
1068
|
if (candidate.weight() < maxWeight) {
|
|
973
1069
|
maxWeight = candidate.weight();
|
|
974
|
-
yield { expr: candidate, steps:
|
|
1070
|
+
yield { expr: candidate, steps: state.steps, comment: '(arg)' + term.comment };
|
|
975
1071
|
}
|
|
976
1072
|
}
|
|
977
|
-
savedSteps = steps;
|
|
978
1073
|
}
|
|
979
1074
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
steps = savedSteps + term.steps;
|
|
986
|
-
yield { expr: candidate, steps: term.steps, comment: term.comment + '(lambda)' };
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
savedSteps = steps;
|
|
990
|
-
}
|
|
1075
|
+
const canon = expr.guess({ max: options.max, maxArgs: options.maxArgs });
|
|
1076
|
+
state.steps += canon.steps;
|
|
1077
|
+
if (canon.expr && canon.expr.weight() < maxWeight)
|
|
1078
|
+
yield { expr: canon.expr, steps: state.steps, comment: '(canonical)' };
|
|
1079
|
+
}
|
|
991
1080
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
maxWeight = canon.canonical.weight();
|
|
995
|
-
yield { expr: canon.canonical, steps: savedSteps + canon.steps, comment: 'canonical' };
|
|
996
|
-
}
|
|
1081
|
+
function nthvar (n) {
|
|
1082
|
+
return new FreeVar('abcdefgh'[n] ?? 'x' + n);
|
|
997
1083
|
}
|
|
998
1084
|
|
|
999
1085
|
// A global value meaning "lambda is used somewhere in this expression"
|