@dallaylaen/ski-interpreter 1.1.0 → 1.3.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
@@ -8,6 +8,10 @@ const globalOptions = {
8
8
  maxArgs: 32,
9
9
  };
10
10
 
11
+ /**
12
+ * @typedef {Expr | function(Expr): Partial} Partial
13
+ */
14
+
11
15
  class Expr {
12
16
  /**
13
17
  * @descr A generic combinatory logic expression.
@@ -141,13 +145,13 @@ class Expr {
141
145
  * @return {{
142
146
  * normal: boolean,
143
147
  * steps: number,
144
- * expr: Expr?,
145
- * arity: number?,
146
- * proper: boolean?,
147
- * discard: boolean?,
148
- * duplicate: boolean?,
149
- * skip: Set<number>?,
150
- * dup: Set<number>?
148
+ * expr?: Expr,
149
+ * arity?: number,
150
+ * proper?: boolean,
151
+ * discard?: boolean,
152
+ * duplicate?: boolean,
153
+ * skip?: Set<number>,
154
+ * dup?: Set<number>,
151
155
  * }}
152
156
  */
153
157
  guess (options = {}) {
@@ -202,12 +206,12 @@ class Expr {
202
206
  * up to the provided computation steps limit,
203
207
  * in decreasing weight order.
204
208
  * @param {{
205
- * max: number?,
206
- * maxArgs: number?,
207
- * varGen: function(void): FreeVar?,
208
- * steps: number?,
209
- * html: boolean?,
210
- * latin: number?,
209
+ * max?: number,
210
+ * maxArgs?: number,
211
+ * varGen?: function(void): FreeVar,
212
+ * steps?: number,
213
+ * html?: boolean,
214
+ * latin?: number,
211
215
  * }} options
212
216
  * @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
213
217
  * @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
@@ -238,44 +242,50 @@ class Expr {
238
242
  }
239
243
  }
240
244
 
241
- /**
242
- * @desc Rename free variables in the expression using the given sequence
243
- * This is for eye-candy only, as the interpreter knows darn well hot to distinguish vars,
244
- * regardless of names.
245
- * @param {IterableIterator<string>} seq
246
- * @return {Expr}
247
- */
248
- renameVars (seq) {
249
- return this;
250
- }
251
-
252
245
  _rski (options) {
253
246
  return this;
254
247
  }
255
248
 
256
249
  /**
257
- * Apply self to list of given args.
258
- * Normally, only native combinators know how to do it.
259
- * @param {Expr[]} args
260
- * @return {Expr|null}
261
- */
262
- reduce (args) {
263
- return null;
250
+ * Replace all instances of plug in the expression with value and return the resulting expression,
251
+ * or null if no changes could be made.
252
+ * Lambda terms and applications will never match if used as plug
253
+ * as they are impossible co compare without extensive computations.
254
+ * Typically used on variables but can also be applied to other terms, e.g. aliases.
255
+ * See also Expr.replace().
256
+ * @param {Expr} search
257
+ * @param {Expr} replace
258
+ * @return {Expr|null}
259
+ */
260
+ subst (search, replace) {
261
+ return this === search ? replace : null;
264
262
  }
265
263
 
266
264
  /**
267
- * Replace all instances of free vars with corresponding values and return the resulting expression.
268
- * return null if no changes could be made.
269
- * @param {FreeVar} plug
270
- * @param {Expr} value
271
- * @return {Expr|null}
272
- */
273
- subst (plug, value) {
265
+ * @desc Apply term reduction rules, if any, to the given argument.
266
+ * A returned value of null means no reduction is possible.
267
+ * A returned value of Expr means the reduction is complete and the application
268
+ * of this and arg can be replaced with the result.
269
+ * A returned value of a function means that further arguments are needed,
270
+ * and can be cached for when they arrive.
271
+ *
272
+ * This method is between apply() which merely glues terms together,
273
+ * and step() which reduces the whole expression.
274
+ *
275
+ * foo.invoke(bar) is what happens inside foo.apply(bar).step() before
276
+ * reduction of either foo or bar is attempted.
277
+ *
278
+ * The name 'invoke' was chosen to avoid confusion with either 'apply' or 'reduce'.
279
+ *
280
+ * @param {Expr} arg
281
+ * @returns {Partial | null}
282
+ */
283
+ invoke (arg) {
274
284
  return null;
275
285
  }
276
286
 
277
287
  /**
278
- * @desc iterate one step of calculation in accordance with known rules.
288
+ * @desc iterate one step of a calculation.
279
289
  * @return {{expr: Expr, steps: number, changed: boolean}}
280
290
  */
281
291
  step () { return { expr: this, steps: 0, changed: false } }
@@ -371,19 +381,18 @@ class Expr {
371
381
  // TODO wanna use AssertionError but webpack doesn't recognize it
372
382
  // still the below hack works for mocha-based tests.
373
383
  const poorMans = new Error(comment + 'found term ' + this + ' but expected ' + expected);
374
- poorMans.expected = expected.toString();
375
- poorMans.actual = this.toString();
384
+ poorMans.expected = expected + '';
385
+ poorMans.actual = this + '';
376
386
  throw poorMans;
377
387
  }
378
388
 
379
389
  /**
380
- * @param {{terse: boolean?, html: boolean?}} [options]
381
- * @return {string} string representation of the expression
390
+ * @desc Returns string representation of the expression.
391
+ * Same as format() without options.
392
+ * @return {string}
382
393
  */
383
- toString (options = {}) {
384
- // uncomment the following line if you want to debug the parser with prints
385
- // return this.constructor.name
386
- throw new Error( 'No toString() method defined in class ' + this.constructor.name );
394
+ toString () {
395
+ return this.format();
387
396
  }
388
397
 
389
398
  /**
@@ -391,17 +400,83 @@ class Expr {
391
400
  * @param {boolean} [first] - whether this is the first term in a sequence
392
401
  * @return {boolean}
393
402
  */
394
- needsParens (first) {
403
+ _braced (first) {
395
404
  return false;
396
405
  }
397
406
 
407
+ _unspaced (arg) {
408
+ return this._braced(true);
409
+ }
410
+
398
411
  /**
412
+ * @desc Stringify the expression with fancy formatting options.
413
+ * Said options mostly include wrappers around various constructs in form of ['(', ')'],
414
+ * as well as terse and html flags that set up the defaults.
415
+ * Format without options is equivalent to toString() and can be parsed back.
416
+ *
417
+ * @param {Object} [options] - formatting options
418
+ * @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
420
+ * @param {[string, string]} [options.brackets] - wrappers for application arguments, typically ['(', ')']
421
+ * @param {[string, string]} [options.var] - wrappers for variable names
422
+ * (will default to &lt;var&gt; and &lt;/var&gt; in html mode)
423
+ * @param {[string, string, string]} [options.lambda] - wrappers for lambda abstractions, e.g. ['&lambda;', '.', '']
424
+ * where the middle string is placed between argument and body
425
+ * default is ['', '->', ''] or ['', '-&gt;', ''] for html
426
+ * @param {[string, string]} [options.around] - wrappers around (sub-)expressions.
427
+ * individual applications will not be wrapped, i.e. (a b c) but not ((a b) c)
428
+ * @param {[string, string]} [options.redex] - wrappers around the starting term(s) that have enough arguments to be reduced
429
+ * @param {Object<string, Expr>} [options.inventory] - if given, output aliases in the set as their names
430
+ * and any other aliases as the expansion of their definitions.
431
+ * The default is a cryptic and fragile mechanism dependent on a hidden mutable property.
432
+ * @returns {string}
433
+ *
434
+ * @example foo.format() // equivalent to foo.toString()
435
+ * @example foo.format({terse: false}) // spell out all parentheses
436
+ * @example foo.format({html: true}) // use HTML tags and entities
437
+ * @example foo.format({ around: ['(', ')'], brackets: ['', ''], lambda: ['(', '->', ')'] }) // lisp style, still back-parsable
438
+ * @exapmle foo.format({ lambda: ['&lambda;', '.', ''] }) // pretty-print for the math department
439
+ * @example foo.format({ lambda: ['', '=>', ''], terse: false }) // make it javascript
440
+ * @example foo.format({ inventory: { T } }) // use T as a named term, expand all others
399
441
  *
400
- * @return {string}
401
442
  */
402
- toJSON () {
403
- return this.expand().toString({ terse: false });
443
+
444
+ format (options = {}) {
445
+ const defaults = options.html
446
+ ? {
447
+ brackets: ['(', ')'],
448
+ space: ' ',
449
+ var: ['<var>', '</var>'],
450
+ lambda: ['', '-&gt;', ''],
451
+ around: ['', ''],
452
+ redex: ['', ''],
453
+ }
454
+ : {
455
+ brackets: ['(', ')'],
456
+ space: ' ',
457
+ var: ['', ''],
458
+ lambda: ['', '->', ''],
459
+ around: ['', ''],
460
+ redex: ['', ''],
461
+ }
462
+ return this._format({
463
+ terse: options.terse ?? globalOptions.terse,
464
+ brackets: options.brackets ?? defaults.brackets,
465
+ space: options.space ?? defaults.space,
466
+ var: options.var ?? defaults.var,
467
+ lambda: options.lambda ?? defaults.lambda,
468
+ around: options.around ?? defaults.around,
469
+ redex: options.redex ?? defaults.redex,
470
+ inventory: options.inventory, // TODO better name
471
+ }, 0);
472
+ }
473
+
474
+ _format (options, nargs) {
475
+ throw new Error( 'No _format() method defined in class ' + this.constructor.name );
404
476
  }
477
+
478
+ // output: string[] /* appended */, inventory: { [key: string]: Expr }, seen: Set<Expr>
479
+ _declare (output, inventory, seen) {}
405
480
  }
406
481
 
407
482
  class App extends Expr {
@@ -486,12 +561,6 @@ class App extends Expr {
486
561
  return this.fun._firstVar();
487
562
  }
488
563
 
489
- apply (...args) {
490
- if (args.length === 0)
491
- return this;
492
- return new App(this, ...args);
493
- }
494
-
495
564
  expand () {
496
565
  return this.fun.expand().apply(this.arg.expand());
497
566
  }
@@ -504,13 +573,9 @@ class App extends Expr {
504
573
  return (fun._replace(pairs, opt) ?? fun).apply(arg._replace(pairs, opt) ?? arg);
505
574
  }
506
575
 
507
- renameVars (seq) {
508
- return this.fun.renameVars(seq).apply(this.arg.renameVars(seq));
509
- }
510
-
511
- subst (plug, value) {
512
- const fun = this.fun.subst(plug, value);
513
- const arg = this.arg.subst(plug, value);
576
+ subst (search, replace) {
577
+ const fun = this.fun.subst(search, replace);
578
+ const arg = this.arg.subst(search, replace);
514
579
 
515
580
  return (fun || arg) ? (fun ?? this.fun).apply(arg ?? this.arg) : null;
516
581
  }
@@ -522,34 +587,48 @@ class App extends Expr {
522
587
  step () {
523
588
  // normal reduction order: first try root, then at most 1 step
524
589
  if (!this.final) {
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
- }
532
- // now try recursing
533
-
590
+ // try to apply rewriting rules, if applicable, at first
591
+ const partial = this.fun.invoke(this.arg);
592
+ if (partial instanceof Expr)
593
+ return { expr: partial, steps: 1, changed: true };
594
+ else if (typeof partial === 'function')
595
+ this.invoke = partial; // cache for next time
596
+
597
+ // descend into the leftmost term
534
598
  const fun = this.fun.step();
535
599
  if (fun.changed)
536
600
  return { expr: fun.expr.apply(this.arg), steps: fun.steps, changed: true };
537
601
 
602
+ // descend into arg
538
603
  const arg = this.arg.step();
539
604
  if (arg.changed)
540
605
  return { expr: this.fun.apply(arg.expr), steps: arg.steps, changed: true };
541
606
 
542
- this.final = true;
607
+ // mark as irreducible
608
+ this.final = true; // mark as irreducible at root
543
609
  }
610
+
544
611
  return { expr: this, steps: 0, changed: false };
545
612
  }
546
613
 
547
- reduce (args) {
548
- return this.fun.reduce([this.arg, ...args]);
614
+ invoke (arg) {
615
+ // propagate invocation towards the root term,
616
+ // caching partial applications as we go
617
+ const partial = this.fun.invoke(this.arg);
618
+ if (partial instanceof Expr)
619
+ return partial.apply(arg);
620
+ else if (typeof partial === 'function') {
621
+ this.invoke = partial;
622
+ return partial(arg);
623
+ } else {
624
+ // invoke = null => we're uncomputable, cache for next time
625
+ this.invoke = _ => null;
626
+ return null;
627
+ }
549
628
  }
550
629
 
551
630
  split () {
552
- // pretend we are an elegant (cons fun arg) and not a sleazy imperative array
631
+ // leftover from array-based older design
553
632
  return [this.fun, this.arg];
554
633
  }
555
634
 
@@ -579,25 +658,28 @@ class App extends Expr {
579
658
  return this.fun.contains(other) || this.arg.contains(other) || super.contains(other);
580
659
  }
581
660
 
582
- needsParens (first) {
661
+ _braced (first) {
583
662
  return !first;
584
663
  }
585
664
 
586
- toString (opt = {}) {
587
- const fun = this.fun.toString(opt);
588
- const root = this.fun.needsParens(true) ? '(' + fun + ')' : fun;
589
- if (opt.terse ?? globalOptions.terse) {
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) + ')';
665
+ _format (options, nargs) {
666
+ const fun = this.fun._format(options, nargs + 1);
667
+ const arg = this.arg._format(options, 0);
668
+ const wrap = nargs ? ['', ''] : options.around;
669
+ // TODO ignore terse for now
670
+ if (options.terse && !this.arg._braced(false))
671
+ return wrap[0] + fun + (this.fun._unspaced(this.arg) ? '' : options.space) + arg + wrap[1];
672
+ else
673
+ return wrap[0] + fun + options.brackets[0] + arg + options.brackets[1] + wrap[1];
674
+ }
675
+
676
+ _declare (output, inventory, seen) {
677
+ this.fun._declare(output, inventory, seen);
678
+ this.arg._declare(output, inventory, seen);
679
+ }
680
+
681
+ _unspaced (arg) {
682
+ return this.arg._braced(false) ? true : this.arg._unspaced(arg);
601
683
  }
602
684
  }
603
685
 
@@ -613,8 +695,19 @@ class Named extends Expr {
613
695
  this.name = name;
614
696
  }
615
697
 
616
- toString () {
617
- return this.name;
698
+ _unspaced (arg) {
699
+ return !!(
700
+ (arg instanceof Named) && (
701
+ (this.name.match(/^[A-Z+]$/) && arg.name.match(/^[a-z+]/i))
702
+ || (this.name.match(/^[a-z+]/i) && arg.name.match(/^[A-Z+]$/))
703
+ )
704
+ );
705
+ }
706
+
707
+ _format (options, nargs) {
708
+ return this.arity > 0 && this.arity <= nargs
709
+ ? options.redex[0] + this.name + options.redex[1]
710
+ : this.name;
618
711
  }
619
712
  }
620
713
 
@@ -626,12 +719,6 @@ class FreeVar extends Named {
626
719
  this.id = ++freeId;
627
720
  }
628
721
 
629
- subst (plug, value) {
630
- if (this === plug)
631
- return value;
632
- return null;
633
- }
634
-
635
722
  weight () {
636
723
  return 0;
637
724
  }
@@ -640,54 +727,39 @@ class FreeVar extends Named {
640
727
  return true;
641
728
  }
642
729
 
643
- toString ( opt = {} ) {
644
- return (opt.html && /^[a-z]$/.test(this.name)) ? '<var>' + this.name + '</var>' : this.name;
730
+ _format (options, nargs) {
731
+ return options.var[0] + this.name + options.var[1];
645
732
  }
646
733
  }
647
734
 
648
- /**
649
- * @typedef {function(Expr): Expr | AnyArity} AnyArity
650
- */
651
-
652
735
  class Native extends Named {
653
736
  /**
654
- * @desc A term named 'name' that converts next 'arity' arguments into
655
- * an expression returned by 'impl' function
656
- * If an apply: Expr=>Expr|null function is given, it will be attempted upon application
657
- * before building an App object. This allows to plug in argument coercions,
658
- * e.g. instantly perform a numeric operation natively if the next term is a number.
737
+ * @desc A named term with a known rewriting rule.
738
+ * 'impl' is a function with signature Expr => Expr => ... => Expr
739
+ * (see typedef Partial).
740
+ * This is how S, K, I, and company are implemented.
741
+ *
742
+ * Note that as of current something like a=>b=>b(a) is not possible,
743
+ * use full form instead: a=>b=>b.apply(a).
744
+ *
745
+ * @example new Native('K', x => y => x); // constant
746
+ * @example new Native('Y', function(f) { return f.apply(this.apply(f)); }); // self-application
747
+ *
659
748
  * @param {String} name
660
- * @param {AnyArity} impl
661
- * @param {{note: string?, arity: number?, canonize: boolean?, apply: function(Expr):(Expr|null) }} [opt]
749
+ * @param {Partial} impl
750
+ * @param {{note?: string, arity?: number, canonize?: boolean, apply?: function(Expr):(Expr|null) }} [opt]
662
751
  */
663
752
  constructor (name, impl, opt = {}) {
664
753
  super(name);
665
754
  // setup essentials
666
- this.impl = impl;
667
- if (opt.apply)
668
- this.onApply = opt.apply;
669
- this.arity = opt.arity ?? 1;
755
+ this.invoke = impl;
670
756
 
757
+ // TODO guess lazily (on demand, only once); app capabilities such as discard and duplicate
671
758
  // try to bootstrap and guess some of our properties
672
759
  const guess = (opt.canonize ?? true) ? this.guess() : { normal: false };
673
760
 
674
- if (!opt.arity)
675
- this.arity = guess.arity || 1;
676
-
677
- this.note = opt.note ?? guess.expr?.toString({ terse: true, html: true });
678
- }
679
-
680
- apply (...args) {
681
- if (this.onApply && args.length >= 1) {
682
- if (typeof this.onApply !== 'function') {
683
- throw new Error('Native combinator ' + this + ' has an invalid onApply property of type'
684
- + typeof this.onApply + ': ' + this.onApply);
685
- }
686
- const subst = this.onApply(args[0]);
687
- if (subst instanceof Expr)
688
- return subst.apply(...args.slice(1));
689
- }
690
- return super.apply(...args);
761
+ this.arity = opt.arity || guess.arity || 1;
762
+ this.note = opt.note ?? guess.expr?.format({ terse: true, html: true, lambda: ['', ' &mapsto; ', ''] });
691
763
  }
692
764
 
693
765
  _rski (options) {
@@ -699,25 +771,6 @@ class Native extends Named {
699
771
  options.steps++;
700
772
  return canon._rski(options);
701
773
  }
702
-
703
- reduce (args) {
704
- if (args.length < this.arity)
705
- return null;
706
- let egde = 0;
707
- let step = this.impl;
708
- while (typeof step === 'function') {
709
- if (egde >= args.length)
710
- return null;
711
- step = step(args[egde++]);
712
- }
713
- if (!(step instanceof Expr))
714
- throw new Error('Native combinator ' + this + ' reduced to a non-expression: ' + step);
715
- return step.apply(...args.slice(egde));
716
- }
717
-
718
- toJSON () {
719
- return 'Native:' + this.name;
720
- }
721
774
  }
722
775
 
723
776
  const native = {};
@@ -727,9 +780,20 @@ function addNative (name, impl, opt) {
727
780
 
728
781
  class Lambda extends Expr {
729
782
  /**
730
- * @param {FreeVar|FreeVar[]} arg
731
- * @param {Expr} impl
732
- */
783
+ * @desc Lambda abstraction of arg over impl.
784
+ * Upon evaluation, all occurrences of 'arg' within 'impl' will be replaced
785
+ * with the provided argument.
786
+ *
787
+ * Note that 'arg' will be replaced by a localized placeholder, so the original
788
+ * variable can be used elsewhere without interference.
789
+ * Listing symbols contained in the lambda will omit such placeholder.
790
+ *
791
+ * Legacy ([FreeVar], impl) constructor is supported but deprecated.
792
+ * It will create a nested lambda expression.
793
+ *
794
+ * @param {FreeVar} arg
795
+ * @param {Expr} impl
796
+ */
733
797
  constructor (arg, impl) {
734
798
  if (Array.isArray(arg)) {
735
799
  // check args before everything
@@ -776,37 +840,24 @@ class Lambda extends Expr {
776
840
  return { normal: false, steps };
777
841
 
778
842
  const push = nthvar(preArgs.length + options.index);
779
- return this.reduce([push])._guess(options, [...preArgs, push], steps + 1);
843
+ return this.invoke(push)._guess(options, [...preArgs, push], steps + 1);
780
844
  }
781
845
 
782
- reduce (input) {
783
- if (input.length === 0)
784
- return null;
785
-
786
- const [head, ...tail] = input;
787
-
788
- return (this.impl.subst(this.arg, head) ?? this.impl).apply(...tail);
846
+ invoke (arg) {
847
+ return this.impl.subst(this.arg, arg) ?? this.impl;
789
848
  }
790
849
 
791
- subst (plug, value) {
792
- if (plug === this.arg)
850
+ subst (search, replace) {
851
+ if (search === this.arg)
793
852
  return null;
794
- const change = this.impl.subst(plug, value);
795
- if (change)
796
- return new Lambda(this.arg, change);
797
- return null;
853
+ const change = this.impl.subst(search, replace);
854
+ return change ? new Lambda(this.arg, change) : null;
798
855
  }
799
856
 
800
857
  expand () {
801
858
  return new Lambda(this.arg, this.impl.expand());
802
859
  }
803
860
 
804
- renameVars (seq) {
805
- const arg = new FreeVar(seq.next().value);
806
- const impl = this.impl.subst(this.arg, arg) ?? this.impl;
807
- return new Lambda(arg, impl.renameVars(seq));
808
- }
809
-
810
861
  _rski (options) {
811
862
  const impl = this.impl._rski(options);
812
863
  if (options.steps >= options.max)
@@ -844,24 +895,37 @@ class Lambda extends Expr {
844
895
 
845
896
  const t = new FreeVar('t');
846
897
 
847
- return other.reduce([t]).equals(this.reduce([t]));
898
+ return other.invoke(t).equals(this.invoke(t));
848
899
  }
849
900
 
850
901
  contains (other) {
851
902
  return this.equals(other) || this.impl.contains(other);
852
903
  }
853
904
 
854
- toString (opt = {}) {
855
- const mapsto = opt.html ? ' &mapsto; ' : '->';
856
- return this.arg.toString(opt) + mapsto + this.impl.toString(opt);
905
+ _format (options, nargs) {
906
+ return (nargs > 0 ? options.brackets[0] : '')
907
+ + options.lambda[0]
908
+ + this.arg._format(options, 0) // TODO highlight redex if nargs > 0
909
+ + options.lambda[1]
910
+ + this.impl._format(options, 0) + options.lambda[2]
911
+ + (nargs > 0 ? options.brackets[1] : '');
857
912
  }
858
913
 
859
- needsParens (first) {
914
+ _declare (output, inventory, seen) {
915
+ this.impl._declare(output, inventory, seen);
916
+ }
917
+
918
+ _braced (first) {
860
919
  return true;
861
920
  }
862
921
  }
863
922
 
864
923
  class Church extends Native {
924
+ /**
925
+ * @desc Church numeral representing non-negative integer n:
926
+ * n f x = f(f(...(f x)...)) with f applied n times.
927
+ * @param {number} n
928
+ */
865
929
  constructor (n) {
866
930
  const p = Number.parseInt(n);
867
931
  if (!(p >= 0))
@@ -885,11 +949,29 @@ class Church extends Native {
885
949
  return this.n === other.n;
886
950
  return super.equals(other);
887
951
  }
952
+
953
+ _unspaced (arg) {
954
+ return false;
955
+ }
956
+ }
957
+
958
+ function waitn (expr, n) {
959
+ return arg => n <= 1 ? expr.apply(arg) : waitn(expr.apply(arg), n - 1);
888
960
  }
889
961
 
890
962
  class Alias extends Named {
891
963
  /**
892
- * @desc An existing expression under a different name.
964
+ * @desc A named alias for an existing expression.
965
+ *
966
+ * Upon evaluation, the alias expands into the original expression,
967
+ * unless it has a known arity > 0 and is marked terminal,
968
+ * in which case it waits for enough arguments before expanding.
969
+ *
970
+ * A hidden mutable property 'outdated' is used to silently
971
+ * replace the alias with its definition in all contexts.
972
+ * This is used when declaring named terms in an interpreter,
973
+ * to avoid confusion between old and new terms with the same name.
974
+ *
893
975
  * @param {String} name
894
976
  * @param {Expr} impl
895
977
  * @param {{canonize: boolean?, max: number?, maxArgs: number?, note: string?, terminal: boolean?}} [options]
@@ -908,6 +990,7 @@ class Alias extends Named {
908
990
  this.proper = guess.proper ?? false;
909
991
  this.terminal = options.terminal ?? this.proper;
910
992
  this.canonical = guess.expr;
993
+ this.invoke = waitn(impl, this.arity);
911
994
  }
912
995
 
913
996
  getSymbols () {
@@ -922,8 +1005,10 @@ class Alias extends Named {
922
1005
  return this.impl.expand();
923
1006
  }
924
1007
 
925
- subst (plug, value) {
926
- return this.impl.subst(plug, value);
1008
+ subst (search, replace) {
1009
+ if (this === search)
1010
+ return replace;
1011
+ return this.impl.subst(search, replace);
927
1012
  }
928
1013
 
929
1014
  _guess (options, preArgs = [], steps = 0) {
@@ -942,12 +1027,6 @@ class Alias extends Named {
942
1027
  return { expr: this.impl, steps: 0, changed: true };
943
1028
  }
944
1029
 
945
- reduce (args) {
946
- if (args.length < this.arity)
947
- return null;
948
- return this.impl.apply(...args);
949
- }
950
-
951
1030
  _firstVar () {
952
1031
  return this.impl._firstVar();
953
1032
  }
@@ -964,15 +1043,35 @@ class Alias extends Named {
964
1043
  return this.impl._rski(options);
965
1044
  }
966
1045
 
967
- toString (opt) {
968
- return this.outdated ? this.impl.toString(opt) : super.toString(opt);
1046
+ _braced (first) {
1047
+ return this.outdated ? this.impl._braced(first) : false;
1048
+ }
1049
+
1050
+ _format (options, nargs) {
1051
+ const outdated = options.inventory
1052
+ ? options.inventory[this.name] !== this
1053
+ : this.outdated;
1054
+ return outdated ? this.impl._format(options, nargs) : super._format(options, nargs);
969
1055
  }
970
1056
 
971
- needsParens (first) {
972
- return this.outdated ? this.impl.needsParens() : false;
1057
+ _declare (output, inventory, seen) {
1058
+ // this is part of the 'declare' function, see below
1059
+ // only once
1060
+ if (seen.has(this))
1061
+ return;
1062
+ seen.add(this);
1063
+
1064
+ // topological order
1065
+ this.impl._declare(output, inventory, seen);
1066
+
1067
+ // only declare if in inventory and matches
1068
+ if (inventory[this.name] === this)
1069
+ output.push(this.name + '=' + this.impl.format({ terse: true, inventory }));
973
1070
  }
974
1071
  }
975
1072
 
1073
+ // ----- Expr* classes end here -----
1074
+
976
1075
  // declare native combinators
977
1076
  addNative('I', x => x);
978
1077
  addNative('K', x => _ => x);
@@ -981,11 +1080,82 @@ addNative('B', x => y => z => x.apply(y.apply(z)));
981
1080
  addNative('C', x => y => z => x.apply(z).apply(y));
982
1081
  addNative('W', x => y => x.apply(y).apply(y));
983
1082
 
984
- addNative('+', x => y => z => y.apply(x.apply(y, z)), {
985
- note: '<var>n</var> &mapsto; <var>n</var> + 1 <i>or</i> SB',
986
- apply: arg => arg instanceof Church ? new Church(arg.n + 1) : null
1083
+ addNative(
1084
+ '+',
1085
+ n => n instanceof Church
1086
+ ? new Church(n.n + 1)
1087
+ : f => x => f.apply(n.apply(f, x)),
1088
+ {
1089
+ note: 'Increase a Church numeral argument by 1, otherwise n => f => x => f(n f x)',
1090
+ }
1091
+ );
1092
+
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') }
987
1101
  });
988
1102
 
1103
+ // utility functions dependent on Expr* classes, in alphabetical order
1104
+
1105
+ /**
1106
+ *
1107
+ * @param {Expr[]} inventory
1108
+ * @return {string[]}
1109
+ */
1110
+ function declare (inventory) {
1111
+ const misnamed = Object.keys(inventory)
1112
+ .filter(s => !(inventory[s] instanceof Named && inventory[s].name === s))
1113
+ .map(s => s + ' = ' + inventory[s]);
1114
+ if (misnamed.length > 0)
1115
+ throw new Error('Inventory must be a hash of named terms with matching names: ' + misnamed.join(', '));
1116
+
1117
+ inventory = { ...inventory }; // shallow copy to avoid mutating input
1118
+
1119
+ // If any aliases mask native terms, those cannot be easily restored.
1120
+ // Moreover, subsequent terms may refer to both native term and and the conflicting alias.
1121
+ // Therefore, we will instead rename such aliases to something else
1122
+ // and only restore them at the end.
1123
+ const detour = [];
1124
+ let tmpId = 1;
1125
+ for (const name in native) {
1126
+ if (!(inventory[name] instanceof Alias))
1127
+ continue;
1128
+ while ('temp' + tmpId in inventory)
1129
+ tmpId++;
1130
+ const temp = 'temp' + tmpId;
1131
+ const orig = inventory[name];
1132
+ delete inventory[name];
1133
+ const masked = new Alias(temp, orig);
1134
+ for (const key in inventory)
1135
+ inventory[key] = inventory[key].subst(orig, masked) ?? inventory[key];
1136
+
1137
+ inventory[temp] = masked;
1138
+ detour.push([name, temp]);
1139
+ }
1140
+
1141
+ // only want to declare aliases
1142
+ const terms = Object.values(inventory)
1143
+ .filter(s => s instanceof Alias)
1144
+ .sort((a, b) => a.name.localeCompare(b.name));
1145
+
1146
+ const out = [];
1147
+ const seen = new Set();
1148
+ for (const term of terms)
1149
+ term._declare(out, inventory, seen);
1150
+
1151
+ for (const [name, temp] of detour) {
1152
+ out.push(name + '=' + temp); // rename
1153
+ out.push(temp + '='); // delete
1154
+ }
1155
+
1156
+ return out;
1157
+ }
1158
+
989
1159
  function maybeLambda (args, expr, caps = {}) {
990
1160
  const sym = expr.getSymbols();
991
1161
 
@@ -1019,6 +1189,10 @@ function naiveCanonize (expr) {
1019
1189
  throw new Error('Failed to canonize expression: ' + expr);
1020
1190
  }
1021
1191
 
1192
+ function nthvar (n) {
1193
+ return new FreeVar('abcdefgh'[n] ?? 'x' + n);
1194
+ }
1195
+
1022
1196
  /**
1023
1197
  *
1024
1198
  * @param {Expr} expr
@@ -1078,18 +1252,4 @@ function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
1078
1252
  yield { expr: canon.expr, steps: state.steps, comment: '(canonical)' };
1079
1253
  }
1080
1254
 
1081
- function nthvar (n) {
1082
- return new FreeVar('abcdefgh'[n] ?? 'x' + n);
1083
- }
1084
-
1085
- // A global value meaning "lambda is used somewhere in this expression"
1086
- // Can't be used (at least for now) to construct lambda expressions, or anything at all.
1087
- // See also getSymbols().
1088
- Expr.lambdaPlaceholder = new Native('->', x => x, {
1089
- arity: 1,
1090
- canonize: false,
1091
- note: 'Lambda placeholder',
1092
- apply: x => { throw new Error('Attempt to use a placeholder in expression') }
1093
- });
1094
-
1095
- module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church, globalOptions, native };
1255
+ module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church, globalOptions, native, declare };