@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/lib/expr.js CHANGED
@@ -1,4 +1,6 @@
1
- const { missingIndices, isSubset } = require('./util');
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
- * @param {{max: number?, maxArgs: number?, bestGuess?: Expr}} options
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
- * found: boolean,
95
- * proper: boolean,
142
+ * normal: boolean,
143
+ * steps: number,
144
+ * expr: Expr?,
96
145
  * arity: number?,
97
- * linear: boolean?,
98
- * canonical?: Expr,
99
- * steps: number?,
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
- canonize (options = {}) {
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
- let steps = 0;
108
- let expr = this;
109
- const jar = [];
110
- for (let i = 0; i < maxArgs; i++) {
111
- const calc = expr.run({ max });
112
- steps += calc.steps;
113
- if (!calc.final)
114
- break;
115
- expr = calc.expr;
116
- if (!expr.wantsArgs()) {
117
- // found!
118
- const symbols = expr.getSymbols();
119
- const skip = missingIndices(jar, symbols);
120
- const proper = isSubset(symbols.keys(), new Set(jar));
121
- const duplicates = [...symbols.entries()].filter(([_, v]) => v > 1);
122
- const linear = proper && skip.size === 0 && duplicates.length === 0;
123
- return {
124
- arity: i,
125
- found: true,
126
- canonical: maybeLambda(jar, expr),
127
- proper,
128
- linear,
129
- steps,
130
- ...(skip.size ? { skip } : {}),
131
- };
132
- }
133
- const next = new FreeVar('abcdefgh'[i] ?? 'x' + i);
134
- jar.push(next);
135
- expr = expr.apply(next);
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
- const fallback = { found: false, proper: false, steps };
139
- if (options.bestGuess)
140
- fallback.canonical = options.bestGuess;
141
- return fallback;
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
- const max = (opt.max ?? globalOptions.max) + steps;
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
- * @param {Expr} other
298
- * @return {boolean}
299
- */
343
+ *
344
+ * @param {Expr} other
345
+ * @return {boolean}
346
+ */
300
347
  equals (other) {
301
- return this === other;
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
- expect (other) {
309
- if (!(other instanceof Expr))
310
- throw new Error('Attempt to expect a combinator to equal something else: ' + other);
311
- if (this.equals(other))
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('Found term ' + this + ' but expected ' + other);
317
- poorMans.expected = other.toString();
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
- this.fun = fun;
377
- this.args = args;
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.args.reduce((acc, x) => acc + x.weight(), this.fun.weight());
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 term of this.args) {
388
- for (const [key, value] of term.getSymbols())
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
- wantsArgs () {
395
- return this.fun.wantsArgs();
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 this.fun.apply( ...this.args, ...args);
492
+ return new App(this, ...args);
402
493
  }
403
494
 
404
495
  expand () {
405
- return this.fun.expand().apply(...this.args.map(x => x.expand()));
496
+ return this.fun.expand().apply(this.arg.expand());
406
497
  }
407
498
 
408
- canonize (options = {}) {
409
- const [fun, arg] = this.split().map(x => x.canonize(options).canonical);
410
- return super.canonize({
411
- ...options,
412
- ...(fun && arg ? { bestGuess: fun.apply(arg) } : {})
413
- });
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
- const fun = this.fun.renameVars(seq);
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
- let change = fun === null ? 0 : 1;
425
- const args = [];
426
- for (const x of this.args) {
427
- const next = x.subst(plug, value);
428
- if (next === null)
429
- args.push(x);
430
- else {
431
- args.push(next);
432
- change++;
433
- }
434
- }
513
+ const arg = this.arg.subst(plug, value);
435
514
 
436
- return change ? (fun ?? this.fun).apply(...args) : null;
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
- const reduced = this.fun.reduce(this.args);
447
- if (reduced)
448
- return { expr: reduced, steps: 1, changed: true };
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(...this.args), steps: fun.steps, changed: true };
455
-
456
- for (let i = 0; i < this.args.length; i++) {
457
- const next = this.args[i].step();
458
- if (!next.changed)
459
- continue;
460
- const args = this.args.slice();
461
- args[i] = next.expr;
462
- return { expr: this.fun.apply(...args), steps: next.steps, changed: true };
463
- }
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
- const args = this.args.slice();
472
- const last = args.pop();
473
- return [this.fun.apply(...args), last];
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(...this.args.map(x => x._rski(options)));
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 false;
490
- if (other.args.length !== this.args.length)
491
- return false;
492
- if (!this.fun.equals(other.fun))
493
- return false;
494
- for (let i = 0; i < this.args.length; i++) {
495
- if (!this.args[i].equals(other.args[i]))
496
- return false;
497
- }
498
- return true;
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
- if (this.fun.contains(other))
503
- return true;
504
- for (const subtree of this.args) {
505
- if (subtree.contains(other))
506
- return true;
507
- }
508
- return super.contains(other);
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
- const out = [];
514
- let oldType = 0;
515
- // stupid ad-hoc state machine, see above for constant definitions
516
- for (const term of [this.fun, ...this.args]) {
517
- let s = term.toString(opt);
518
- let newType = T_UNKNOWN;
519
- if (s.match(/^[A-Z]$/))
520
- newType = T_UPPER;
521
- else if (term instanceof FreeVar || s.match(/^[a-z][a-z_0-9]*$/))
522
- newType = T_LOWER;
523
- else if (s.match(/^[0-9]+$/))
524
- // no special treatment for numerals, skip
525
- ;
526
- else if (out.length !== 0 || term.needsParens()) {
527
- s = '(' + s + ')';
528
- newType = T_PARENS;
529
- }
530
- if (!canLump.has((oldType << BITS) | newType) && out.length > 0)
531
- out.push(' ');
532
- out.push(s);
533
- oldType = newType;
534
- }
535
- return out.join('');
536
- } else {
537
- const fun = this.fun.toString(opt);
538
- const root = this.fun.needsParens() ? '(' + fun + ')' : fun;
539
- return root + this.args.map(x => '(' + x.toString(opt) + ')').join('');
540
- }
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
- wantsArgs () {
580
- return false;
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.canonize() : { found: false };
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.canonical?.toString({ terse: true, html: true });
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.canonize().canonical;
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 false;
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 false;
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.canonize({ max: options.max, maxArgs: options.maxArgs })
830
- : { found: false };
831
- this.arity = (guess.found && guess.proper && guess.arity) || 0;
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.canonical;
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
- wantsArgs () {
872
- return this.impl.wantsArgs();
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
- if (args.length === 0)
911
- return expr;
912
- return new Lambda(args, expr);
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(...expr.args.map(naiveCanonize));
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.canonize();
926
- if (canon.canonical)
927
- return canon.canonical;
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
- * @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
1027
+ * @yields {{expr: Expr, steps: number?, comment: string?}}
938
1028
  */
939
- function * simplifyLambda (expr, options = {}, maxWeight = Infinity) {
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 steps = 0;
952
- let savedSteps = 0;
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
- const [fun, arg] = expr.split();
1055
+ let [fun, arg] = expr.split();
958
1056
 
959
- for (const term of simplifyLambda(fun, options, maxWeight - 1)) {
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
- yield { expr: candidate, steps: term.steps, comment: term.comment + '(app)' };
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, maxWeight - 1)) {
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: term.steps, comment: term.comment + '(app)' };
1070
+ yield { expr: candidate, steps: state.steps, comment: '(arg)' + term.comment };
975
1071
  }
976
1072
  }
977
- savedSteps = steps;
978
1073
  }
979
1074
 
980
- if (expr instanceof Lambda) {
981
- for (const term of simplifyLambda(expr.impl, options, maxWeight - 1)) {
982
- const candidate = new Lambda(expr.arg, term.expr);
983
- if (candidate.weight() < maxWeight) {
984
- maxWeight = candidate.weight();
985
- steps = savedSteps + term.steps;
986
- yield { expr: candidate, steps: term.steps, comment: term.comment + '(lambda)' };
987
- }
988
- }
989
- savedSteps = steps;
990
- }
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
- const canon = expr.canonize({ max: options.max, maxArgs: options.maxArgs });
993
- if (canon.canonical && canon.canonical.weight() < maxWeight) {
994
- maxWeight = canon.canonical.weight();
995
- yield { expr: canon.canonical, steps: savedSteps + canon.steps, comment: 'canonical' };
996
- }
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"