@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/lib/expr.js CHANGED
@@ -1,20 +1,40 @@
1
1
  'use strict';
2
2
 
3
- const { skipDup, isSubset } = require('./util');
3
+ const { unwrap, prepareWrapper } = require('./internal');
4
4
 
5
- const globalOptions = {
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 generic combinatory logic expression.
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
- * postprocess term after parsing. typically return self but may return other term or die
26
- * @return {Expr}
27
- */
28
- postParse () {
29
- return this;
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
- return args.length > 0 ? new App(this, ...args) : this;
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
- * expand all terms but don't perform any calculations
44
- * @return {Expr}
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
- * @desc return all free variables within the term
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
- freeOnly () {
70
- for (const [key, _] of this.getSymbols()) {
71
- if (!(key instanceof FreeVar))
72
- return false;
73
- }
74
- return true;
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 return all terminal values within the term, that is, values not
79
- * composed of other terms. For example, in S(KI)K, the terminals are S, K, I.
80
- * @return {Map<Expr, number>}
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
- getSymbols () {
83
- // TODO better name!
84
- return new Map([[this, 1]]);
90
+ any (predicate) {
91
+ return predicate(this);
85
92
  }
86
93
 
87
94
  /**
88
- * @desc Given a list of pairs of term, replaces every subtree
89
- * that is equivalent to the first term in pair with the second one.
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
- * @example S(SKK)(SKS).replace('I') = SII // we found 2 subtrees equivalent to I
93
- * and replaced them with I
98
+ * null or undefined return value from combine() means "keep current value and descend further".
94
99
  *
95
- * @param {(Expr | [find: Expr, replace: Expr])[]} terms
96
- * @param {Object} [opt] - options
97
- * @return {Expr}
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
- replace (terms, opt = {}) {
100
- const pairs = [];
101
- if (terms.length === 0)
102
- return this; // nothing to replace, return self
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
- _replace (pairs, opt) {
116
- const check = this.guess(opt).expr;
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 rought estimate of the complexity of the term
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 lambdify() if you want to get a lambda term in any case.
141
+ * Use toLambda() if you want to get a lambda term in any case.
143
142
  *
144
- * @param {{max: number?, maxArgs: number?}} options
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
- guess (options = {}) {
158
- const max = options.max ?? globalOptions.max;
159
- const maxArgs = options.maxArgs ?? globalOptions.maxArgs;
160
- const out = this._guess({ max, maxArgs, index: 0 });
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
- _guess (options, preArgs = [], steps = 0) {
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._guess(options, preArgs, steps);
192
+ return next.expr._infer(options, preArgs, steps);
186
193
 
187
- if (this._firstVar())
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)._guess(options, [...preArgs, push], steps);
201
+ return this.apply(push)._infer(options, [...preArgs, push], steps);
192
202
  }
193
203
 
194
- _aslist () {
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
- * lambdify (options = {}) {
220
- const expr = naiveCanonize(this, options);
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 same semantics as walk() but rewrite step by step instead of computing
226
- * @param {{max: number?}} options
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
- * rewriteSKI (options = {}) {
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.replace().
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
- * @desc iterate one step of a calculation.
289
- * @return {{expr: Expr, steps: number, changed: boolean}}
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
- * @desc Run uninterrupted sequence of step() applications
295
- * until the expression is irreducible, or max number of steps is reached.
296
- * Default number of steps = 1000.
297
- * @param {{max: number?, steps: number?, throw: boolean?}|Expr} [opt]
298
- * @param {Expr} args
299
- * @return {{expr: Expr, steps: number, final: boolean}}
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 ?? globalOptions.max, 1) + steps;
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
- * Execute step() while possible, yielding a brief description of events after each step.
327
- * Mnemonics: like run() but slower.
328
- * @param {{max: number?}} options
329
- * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
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
- if (this === other)
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
- contains (other) {
366
- return this === other || this.equals(other);
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
- if (this.equals(expected))
379
- return;
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 + 'found term ' + this + ' but expected ' + expected);
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 &lt;var&gt; and &lt;/var&gt; in html mode)
500
+ * (will default to &lt;var&gt; and &lt;/var&gt; in html mode).
423
501
  * @param {[string, string, string]} [options.lambda] - wrappers for lambda abstractions, e.g. ['&lambda;', '.', '']
424
502
  * where the middle string is placed between argument and body
425
503
  * default is ['', '->', ''] or ['', '-&gt;', ''] 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 defaults = options.html
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 ?? 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,
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, ...args) directly, use fun.apply(...args) instead.
574
+ * Never ever use new App(fun, arg) directly, use fun.apply(...args) instead.
486
575
  * @param {Expr} fun
487
- * @param {Expr} args
576
+ * @param {Expr} arg
488
577
  */
489
- constructor (fun, ...args) {
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 = args.pop();
495
- this.fun = args.length ? new App(fun, ...args) : 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
- getSymbols () {
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 guess() actually is able to do the job. Then we just proxy the result.
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._guess(options, preArgs, steps);
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._aslist();
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._guess({
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, new App(first, ...out), {
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
- _replace (pairs, opt) {
569
- const maybe = super._replace(pairs, opt);
570
- if (maybe)
571
- return maybe;
572
- const [fun, arg] = this.split();
573
- return (fun._replace(pairs, opt) ?? fun).apply(arg._replace(pairs, opt) ?? arg);
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
- split () {
631
- // leftover from array-based older design
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
- equals (other) {
746
+ diff (other, swap = false) {
651
747
  if (!(other instanceof App))
652
- return super.equals(other);
653
-
654
- return this.fun.equals(other.fun) && this.arg.equals(other.arg);
655
- }
656
-
657
- contains (other) {
658
- return this.fun.contains(other) || this.arg.contains(other) || super.contains(other);
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
- * @desc a constant named 'name'
689
- * @param {String} name
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] + this.name + options.redex[1]
710
- : this.name;
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
- constructor (name) {
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
- _firstVar () {
727
- return true;
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
- return options.var[0] + this.name + options.var[1];
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 guess lazily (on demand, only once); app capabilities such as discard and duplicate
758
- // try to bootstrap and guess some of our properties
759
- const guess = (opt.canonize ?? true) ? this.guess() : { normal: false };
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: ['', ' &mapsto; ', ''] });
@@ -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.guess().expr;
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 as it may appear elsewhere
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
- _guess (options, preArgs = [], steps = 0) {
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)._guess(options, [...preArgs, push], steps + 1);
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.getSymbols().has(this.arg))
1029
+ if (!impl.any(e => e === this.arg))
869
1030
  return native.K.apply(impl);
870
1031
  if (impl instanceof App) {
871
- const [fst, snd] = impl.split();
1032
+ const { fun, arg } = impl;
872
1033
  // try eta reduction
873
- if (snd === this.arg && !fst.getSymbols().has(this.arg))
874
- return fst._rski(options);
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, fst))._rski(options),
878
- (new Lambda(this.arg, snd))._rski(options)
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
- _replace (pairs, opt) {
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.equals(other);
895
-
896
- const t = new FreeVar('t');
1047
+ return super.diff(other, swap);
897
1048
 
898
- return other.invoke(t).equals(this.invoke(t));
899
- }
1049
+ const t = new FreeVar('t'); // TODO better placeholder name
900
1050
 
901
- contains (other) {
902
- return this.equals(other) || this.impl.contains(other);
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
- equals (other) {
948
- if (other instanceof Church)
949
- return this.n === other.n;
950
- return super.equals(other);
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.guess({ max: options.max, maxArgs: options.maxArgs })
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
- _guess (options, preArgs = [], steps = 0) {
1015
- return this.impl._guess(options, preArgs, steps);
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
- _firstVar () {
1031
- return this.impl._firstVar();
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 sym = expr.getSymbols();
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 [skip, dup] = skipDup(args, sym);
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: isSubset(sym.keys(), new Set(args)),
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 [fun, arg] = expr.split();
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.guess({ max: options.max, maxArgs: options.maxArgs });
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
- module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church, globalOptions, native, declare };
1413
+ Expr.declare = declare;
1414
+ Expr.native = native;
1415
+ Expr.control = control;
1416
+
1417
+ module.exports = { Expr, App, Named, FreeVar, Lambda, Native, Alias, Church };