@dallaylaen/ski-interpreter 1.2.0 → 2.0.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,13 +1,14 @@
1
1
  'use strict';
2
2
 
3
- const { skipDup, isSubset } = require('./util');
4
-
5
- const globalOptions = {
6
- terse: true,
3
+ const DEFAULTS = {
7
4
  max: 1000,
8
5
  maxArgs: 32,
9
6
  };
10
7
 
8
+ /**
9
+ * @typedef {Expr | function(Expr): Partial} Partial
10
+ */
11
+
11
12
  class Expr {
12
13
  /**
13
14
  * @descr A generic combinatory logic expression.
@@ -18,103 +19,50 @@ class Expr {
18
19
  }
19
20
 
20
21
  /**
21
- * postprocess term after parsing. typically return self but may return other term or die
22
- * @return {Expr}
23
- */
24
- postParse () {
25
- return this;
26
- }
27
-
28
- /**
29
- * @desc apply self to zero or more terms and return the resulting term,
30
- * without performing any calculations whatsoever
31
- * @param {Expr} args
32
- * @return {Expr}
33
- */
22
+ * @desc apply self to zero or more terms and return the resulting term,
23
+ * without performing any calculations whatsoever
24
+ * @param {Expr} args
25
+ * @return {Expr}
26
+ */
34
27
  apply (...args) {
35
28
  return args.length > 0 ? new App(this, ...args) : this;
36
29
  }
37
30
 
38
31
  /**
39
- * expand all terms but don't perform any calculations
40
- * @return {Expr}
41
- */
32
+ * expand all terms but don't perform any calculations
33
+ * @return {Expr}
34
+ */
42
35
  expand () {
43
36
  return this;
44
37
  }
45
38
 
46
- /**
47
- * @desc return all free variables within the term
48
- * @return {Set<FreeVar>}
49
- */
50
- freeVars () {
51
- const symbols = this.getSymbols();
52
- const out = new Set();
53
- for (const [key, _] of symbols) {
54
- if (key instanceof FreeVar)
55
- out.add(key);
56
- }
57
- return out;
58
- }
59
-
60
- hasLambda () {
61
- const sym = this.getSymbols();
62
- return sym.has(Expr.lambdaPlaceholder);
63
- }
64
-
65
39
  freeOnly () {
66
- for (const [key, _] of this.getSymbols()) {
67
- if (!(key instanceof FreeVar))
68
- return false;
69
- }
70
- return true;
40
+ return !this.any(e => !(e instanceof FreeVar || e instanceof App));
71
41
  }
72
42
 
73
43
  /**
74
- * @desc return all terminal values within the term, that is, values not
75
- * composed of other terms. For example, in S(KI)K, the terminals are S, K, I.
76
- * @return {Map<Expr, number>}
44
+ * @desc Traverse the expression tree, applying change() to each node.
45
+ * If change() returns an Expr, the node is replaced with that value.
46
+ * Otherwise, the node is left descended further (if applicable)
47
+ * or left unchanged.
48
+ *
49
+ * Returns null if no changes were made, or the new expression otherwise.
50
+ *
51
+ * @param {(e:Expr) => (Expr|null)} change
52
+ * @returns {Expr|null}
77
53
  */
78
- getSymbols () {
79
- // TODO better name!
80
- return new Map([[this, 1]]);
54
+ traverse (change) {
55
+ return change(this);
81
56
  }
82
57
 
83
58
  /**
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
59
+ * @desc Returns true if predicate() is true for any subterm of the expression, false otherwise.
90
60
  *
91
- * @param {(Expr | [find: Expr, replace: Expr])[]} terms
92
- * @param {Object} [opt] - options
93
- * @return {Expr}
61
+ * @param {(e: Expr) => boolean} predicate
62
+ * @returns {boolean}
94
63
  */
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;
64
+ any (predicate) {
65
+ return predicate(this);
118
66
  }
119
67
 
120
68
  /**
@@ -126,16 +74,16 @@ class Expr {
126
74
  }
127
75
 
128
76
  /**
129
- * @desc Try to find an equivalent lambda term for the expression,
77
+ * @desc Try to empirically find an equivalent lambda term for the expression,
130
78
  * returning also the term's arity and some other properties.
131
79
  *
132
- * This is used internally when declaring a Native term,
80
+ * This is used internally when declaring a Native / Alias term,
133
81
  * unless {canonize: false} is used.
134
82
  *
135
83
  * As of current it only recognizes terms that have a normal form,
136
84
  * perhaps after adding some variables. This may change in the future.
137
85
  *
138
- * Use lambdify() if you want to get a lambda term in any case.
86
+ * Use toLambda() if you want to get a lambda term in any case.
139
87
  *
140
88
  * @param {{max: number?, maxArgs: number?}} options
141
89
  * @return {{
@@ -150,14 +98,14 @@ class Expr {
150
98
  * dup?: Set<number>,
151
99
  * }}
152
100
  */
153
- guess (options = {}) {
154
- const max = options.max ?? globalOptions.max;
155
- const maxArgs = options.maxArgs ?? globalOptions.maxArgs;
156
- const out = this._guess({ max, maxArgs, index: 0 });
101
+ infer (options = {}) {
102
+ const max = options.max ?? DEFAULTS.max;
103
+ const maxArgs = options.maxArgs ?? DEFAULTS.maxArgs;
104
+ const out = this._infer({ max, maxArgs, index: 0 });
157
105
  return out;
158
106
  }
159
107
 
160
- _guess (options, preArgs = [], steps = 0) {
108
+ _infer (options, preArgs = [], steps = 0) {
161
109
  if (preArgs.length > options.maxArgs || steps > options.max)
162
110
  return { normal: false, steps };
163
111
 
@@ -178,22 +126,27 @@ class Expr {
178
126
 
179
127
  // normal form != this, redo exercise
180
128
  if (next.steps !== 0)
181
- return next.expr._guess(options, preArgs, steps);
129
+ return next.expr._infer(options, preArgs, steps);
182
130
 
131
+ // adding more args won't help, bail out
132
+ // if we're an App, the App's _infer will take care of further args
183
133
  if (this._firstVar())
184
134
  return { normal: false, steps };
185
135
 
136
+ // try adding more arguments, maybe we'll get a normal form then
186
137
  const push = nthvar(preArgs.length + options.index);
187
- return this.apply(push)._guess(options, [...preArgs, push], steps);
138
+ return this.apply(push)._infer(options, [...preArgs, push], steps);
188
139
  }
189
140
 
190
141
  _aslist () {
142
+ // currently only used by infer() but may be useful
143
+ // to convert binary App trees to n-ary or smth
191
144
  return [this];
192
145
  }
193
146
 
194
147
  _firstVar () {
195
148
  // boolean, whether the expression starts with a free variable
196
- // used by guess()
149
+ // only used by infer() as a shortcut to this._aslist()[0] instanceof FreeVar
197
150
  return false;
198
151
  }
199
152
 
@@ -201,6 +154,12 @@ class Expr {
201
154
  * @desc Returns a series of lambda terms equivalent to the given expression,
202
155
  * up to the provided computation steps limit,
203
156
  * in decreasing weight order.
157
+ *
158
+ * Unlike infer(), this method will always return something,
159
+ * even if the expression has no normal form.
160
+ *
161
+ * See also Expr.walk() and Expr.toSKI().
162
+ *
204
163
  * @param {{
205
164
  * max?: number,
206
165
  * maxArgs?: number,
@@ -212,17 +171,22 @@ class Expr {
212
171
  * @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
213
172
  * @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
214
173
  */
215
- * lambdify (options = {}) {
174
+ * toLambda (options = {}) {
216
175
  const expr = naiveCanonize(this, options);
217
176
  yield * simplifyLambda(expr, options);
218
177
  }
219
178
 
220
179
  /**
221
- * @desc same semantics as walk() but rewrite step by step instead of computing
180
+ * @desc Rewrite the expression into S, K, and I combinators step by step.
181
+ * Returns an iterator yielding the intermediate expressions,
182
+ * along with the number of steps taken to reach them.
183
+ *
184
+ * See also Expr.walk() and Expr.toLambda().
185
+ *
222
186
  * @param {{max: number?}} options
223
187
  * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
224
188
  */
225
- * rewriteSKI (options = {}) {
189
+ * toSKI (options = {}) {
226
190
  // TODO options.max is not actually max, it's the number of steps in one iteration
227
191
  let steps = 0;
228
192
  let expr = this;
@@ -242,23 +206,13 @@ class Expr {
242
206
  return this;
243
207
  }
244
208
 
245
- /**
246
- * Apply self to list of given args.
247
- * Normally, only native combinators know how to do it.
248
- * @param {Expr[]} args
249
- * @return {Expr|null}
250
- */
251
- reduce (args) {
252
- return null;
253
- }
254
-
255
209
  /**
256
210
  * Replace all instances of plug in the expression with value and return the resulting expression,
257
211
  * or null if no changes could be made.
258
212
  * Lambda terms and applications will never match if used as plug
259
213
  * as they are impossible co compare without extensive computations.
260
214
  * Typically used on variables but can also be applied to other terms, e.g. aliases.
261
- * See also Expr.replace().
215
+ * See also Expr.traverse().
262
216
  * @param {Expr} search
263
217
  * @param {Expr} replace
264
218
  * @return {Expr|null}
@@ -268,19 +222,42 @@ class Expr {
268
222
  }
269
223
 
270
224
  /**
271
- * @desc iterate one step of calculation in accordance with known rules.
272
- * @return {{expr: Expr, steps: number, changed: boolean}}
273
- */
225
+ * @desc Apply term reduction rules, if any, to the given argument.
226
+ * A returned value of null means no reduction is possible.
227
+ * A returned value of Expr means the reduction is complete and the application
228
+ * of this and arg can be replaced with the result.
229
+ * A returned value of a function means that further arguments are needed,
230
+ * and can be cached for when they arrive.
231
+ *
232
+ * This method is between apply() which merely glues terms together,
233
+ * and step() which reduces the whole expression.
234
+ *
235
+ * foo.invoke(bar) is what happens inside foo.apply(bar).step() before
236
+ * reduction of either foo or bar is attempted.
237
+ *
238
+ * The name 'invoke' was chosen to avoid confusion with either 'apply' or 'reduce'.
239
+ *
240
+ * @param {Expr} arg
241
+ * @returns {Partial | null}
242
+ */
243
+ invoke (arg) {
244
+ return null;
245
+ }
246
+
247
+ /**
248
+ * @desc iterate one step of a calculation.
249
+ * @return {{expr: Expr, steps: number, changed: boolean}}
250
+ */
274
251
  step () { return { expr: this, steps: 0, changed: false } }
275
252
 
276
253
  /**
277
- * @desc Run uninterrupted sequence of step() applications
278
- * until the expression is irreducible, or max number of steps is reached.
279
- * Default number of steps = 1000.
280
- * @param {{max: number?, steps: number?, throw: boolean?}|Expr} [opt]
281
- * @param {Expr} args
282
- * @return {{expr: Expr, steps: number, final: boolean}}
283
- */
254
+ * @desc Run uninterrupted sequence of step() applications
255
+ * until the expression is irreducible, or max number of steps is reached.
256
+ * Default number of steps = 1000.
257
+ * @param {{max: number?, steps: number?, throw: boolean?}|Expr} [opt]
258
+ * @param {Expr} args
259
+ * @return {{expr: Expr, steps: number, final: boolean}}
260
+ */
284
261
  run (opt = {}, ...args) {
285
262
  if (opt instanceof Expr) {
286
263
  args.unshift(opt);
@@ -289,7 +266,7 @@ class Expr {
289
266
  let expr = args ? this.apply(...args) : this;
290
267
  let steps = opt.steps ?? 0;
291
268
  // make sure we make at least 1 step, to tell whether we've reached the normal form
292
- const max = Math.max(opt.max ?? globalOptions.max, 1) + steps;
269
+ const max = Math.max(opt.max ?? DEFAULTS.max, 1) + steps;
293
270
  let final = false;
294
271
  for (; steps < max; ) {
295
272
  const next = expr.step();
@@ -306,11 +283,11 @@ class Expr {
306
283
  }
307
284
 
308
285
  /**
309
- * Execute step() while possible, yielding a brief description of events after each step.
310
- * Mnemonics: like run() but slower.
311
- * @param {{max: number?}} options
312
- * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
313
- */
286
+ * Execute step() while possible, yielding a brief description of events after each step.
287
+ * Mnemonics: like run() but slower.
288
+ * @param {{max: number?}} options
289
+ * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
290
+ */
314
291
  * walk (options = {}) {
315
292
  const max = options.max ?? Infinity;
316
293
  let steps = 0;
@@ -333,20 +310,49 @@ class Expr {
333
310
  }
334
311
 
335
312
  /**
313
+ * @desc True is the expressions are identical, false otherwise.
314
+ * Aliases are expanded.
315
+ * Bound variables in lambda terms are renamed consistently.
316
+ * However, no reductions are attempted.
317
+ *
318
+ * E.g. a->b->a == x->y->x is true, but a->b->a == K is false.
336
319
  *
337
320
  * @param {Expr} other
338
321
  * @return {boolean}
339
322
  */
340
323
  equals (other) {
341
- if (this === other)
342
- return true;
343
- if (other instanceof Alias)
344
- return other.equals(this);
345
- return false;
324
+ return !this.diff(other);
346
325
  }
347
326
 
348
- contains (other) {
349
- return this === other || this.equals(other);
327
+ /**
328
+ * @desc Recursively compare two expressions and return a string
329
+ * describing the first point of difference.
330
+ * Returns null if expressions are identical.
331
+ *
332
+ * Aliases are expanded.
333
+ * Bound variables in lambda terms are renamed consistently.
334
+ * However, no reductions are attempted.
335
+ *
336
+ * Members of the FreeVar class are considered different
337
+ * even if they have the same name, unless they are the same object.
338
+ * To somewhat alleviate confusion, the output will include
339
+ * the internal id of the variable in square brackets.
340
+ *
341
+ * @example "K(S != I)" is the result of comparing "KS" and "KI"
342
+ * @example "S(K([x[13] != x[14]]))K"
343
+ *
344
+ * @param {Expr} other
345
+ * @param {boolean} [swap] If true, the order of expressions is reversed in the output.
346
+ * @returns {string|null}
347
+ */
348
+ diff (other, swap = false) {
349
+ if (this === other)
350
+ return null;
351
+ if (other instanceof Alias)
352
+ return other.impl.diff(this, !swap);
353
+ return swap
354
+ ? '[' + other + ' != ' + this + ']'
355
+ : '[' + this + ' != ' + other + ']';
350
356
  }
351
357
 
352
358
  /**
@@ -358,12 +364,13 @@ class Expr {
358
364
  comment = comment ? comment + ': ' : '';
359
365
  if (!(expected instanceof Expr))
360
366
  throw new Error(comment + 'attempt to expect a combinator to equal something else: ' + expected);
361
- if (this.equals(expected))
362
- return;
367
+ const diff = this.diff(expected);
368
+ if (!diff)
369
+ return; // all good
363
370
 
364
371
  // TODO wanna use AssertionError but webpack doesn't recognize it
365
372
  // still the below hack works for mocha-based tests.
366
- const poorMans = new Error(comment + 'found term ' + this + ' but expected ' + expected);
373
+ const poorMans = new Error(comment + diff);
367
374
  poorMans.expected = expected + '';
368
375
  poorMans.actual = this + '';
369
376
  throw poorMans;
@@ -399,10 +406,11 @@ class Expr {
399
406
  *
400
407
  * @param {Object} [options] - formatting options
401
408
  * @param {boolean} [options.terse] - whether to use terse formatting (omitting unnecessary spaces and parentheses)
402
- * @param {boolean} [options.html] - whether to default to HTML tags & entities
409
+ * @param {boolean} [options.html] - whether to default to HTML tags & entities.
410
+ * If a named term has fancyName property set, it will be used instead of name in this mode.
403
411
  * @param {[string, string]} [options.brackets] - wrappers for application arguments, typically ['(', ')']
404
412
  * @param {[string, string]} [options.var] - wrappers for variable names
405
- * (will default to &lt;var&gt; and &lt;/var&gt; in html mode)
413
+ * (will default to &lt;var&gt; and &lt;/var&gt; in html mode).
406
414
  * @param {[string, string, string]} [options.lambda] - wrappers for lambda abstractions, e.g. ['&lambda;', '.', '']
407
415
  * where the middle string is placed between argument and body
408
416
  * default is ['', '->', ''] or ['', '-&gt;', ''] for html
@@ -425,14 +433,14 @@ class Expr {
425
433
  */
426
434
 
427
435
  format (options = {}) {
428
- const defaults = options.html
436
+ const fallback = options.html
429
437
  ? {
430
438
  brackets: ['(', ')'],
431
439
  space: ' ',
432
440
  var: ['<var>', '</var>'],
433
441
  lambda: ['', '-&gt;', ''],
434
442
  around: ['', ''],
435
- redex: ['<b>', '</b>'],
443
+ redex: ['', ''],
436
444
  }
437
445
  : {
438
446
  brackets: ['(', ')'],
@@ -443,14 +451,15 @@ class Expr {
443
451
  redex: ['', ''],
444
452
  }
445
453
  return this._format({
446
- terse: options.terse ?? globalOptions.terse,
447
- brackets: options.brackets ?? defaults.brackets,
448
- space: options.space ?? defaults.space,
449
- var: options.var ?? defaults.var,
450
- lambda: options.lambda ?? defaults.lambda,
451
- around: options.around ?? defaults.around,
452
- redex: options.redex ?? defaults.redex,
454
+ terse: options.terse ?? true,
455
+ brackets: options.brackets ?? fallback.brackets,
456
+ space: options.space ?? fallback.space,
457
+ var: options.var ?? fallback.var,
458
+ lambda: options.lambda ?? fallback.lambda,
459
+ around: options.around ?? fallback.around,
460
+ redex: options.redex ?? fallback.redex,
453
461
  inventory: options.inventory, // TODO better name
462
+ html: options.html ?? false,
454
463
  }, 0);
455
464
  }
456
465
 
@@ -484,25 +493,18 @@ class App extends Expr {
484
493
  return this.fun.weight() + this.arg.weight();
485
494
  }
486
495
 
487
- getSymbols () {
488
- const out = this.fun.getSymbols();
489
- for (const [key, value] of this.arg.getSymbols())
490
- out.set(key, (out.get(key) ?? 0) + value);
491
- return out;
492
- }
493
-
494
- _guess (options, preArgs = [], steps = 0) {
496
+ _infer (options, preArgs = [], steps = 0) {
495
497
  if (preArgs.length > options.maxArgs || steps > options.max)
496
498
  return { normal: false, steps };
497
499
 
498
500
  /*
499
501
  * inside and App there are 3 main possibilities:
500
- * 1) The parent guess() actually is able to do the job. Then we just proxy the result.
502
+ * 1) The parent infer() actually is able to do the job. Then we just proxy the result.
501
503
  * 2) Both `fun` and `arg` form good enough lambda terms. Then lump them together & return.
502
504
  * 3) We literally have no idea, so we just pick the shortest defined term from the above.
503
505
  */
504
506
 
505
- const proxy = super._guess(options, preArgs, steps);
507
+ const proxy = super._infer(options, preArgs, steps);
506
508
  if (proxy.normal)
507
509
  return proxy;
508
510
  steps = proxy.steps; // reimport extra iterations
@@ -516,7 +518,7 @@ class App extends Expr {
516
518
  let duplicate = false;
517
519
  const out = [];
518
520
  for (const term of list) {
519
- const guess = term._guess({
521
+ const guess = term._infer({
520
522
  ...options,
521
523
  maxArgs: options.maxArgs - preArgs.length,
522
524
  max: options.max - steps,
@@ -544,22 +546,26 @@ class App extends Expr {
544
546
  return this.fun._firstVar();
545
547
  }
546
548
 
547
- apply (...args) {
548
- if (args.length === 0)
549
- return this;
550
- return new App(this, ...args);
551
- }
552
-
553
549
  expand () {
554
550
  return this.fun.expand().apply(this.arg.expand());
555
551
  }
556
552
 
557
- _replace (pairs, opt) {
558
- const maybe = super._replace(pairs, opt);
559
- if (maybe)
560
- return maybe;
561
- const [fun, arg] = this.split();
562
- return (fun._replace(pairs, opt) ?? fun).apply(arg._replace(pairs, opt) ?? arg);
553
+ traverse (change) {
554
+ const replaced = change(this);
555
+ if (replaced instanceof Expr)
556
+ return replaced;
557
+
558
+ const fun = this.fun.traverse(change);
559
+ const arg = this.arg.traverse(change);
560
+
561
+ if (!fun && !arg)
562
+ return null; // no changes
563
+
564
+ return (fun ?? this.fun).apply(arg ?? this.arg);
565
+ }
566
+
567
+ any (predicate) {
568
+ return predicate(this) || this.fun.any(predicate) || this.arg.any(predicate);
563
569
  }
564
570
 
565
571
  subst (search, replace) {
@@ -576,34 +582,48 @@ class App extends Expr {
576
582
  step () {
577
583
  // normal reduction order: first try root, then at most 1 step
578
584
  if (!this.final) {
579
- if (this.arity === 0) {
580
- // aha! we have just fulfilled some previous function's argument demands
581
- const reduced = this.fun.reduce([this.arg]);
582
- // should always be true, but whatever
583
- if (reduced)
584
- return { expr: reduced, steps: 1, changed: true };
585
- }
586
- // now try recursing
587
-
585
+ // try to apply rewriting rules, if applicable, at first
586
+ const partial = this.fun.invoke(this.arg);
587
+ if (partial instanceof Expr)
588
+ return { expr: partial, steps: 1, changed: true };
589
+ else if (typeof partial === 'function')
590
+ this.invoke = partial; // cache for next time
591
+
592
+ // descend into the leftmost term
588
593
  const fun = this.fun.step();
589
594
  if (fun.changed)
590
595
  return { expr: fun.expr.apply(this.arg), steps: fun.steps, changed: true };
591
596
 
597
+ // descend into arg
592
598
  const arg = this.arg.step();
593
599
  if (arg.changed)
594
600
  return { expr: this.fun.apply(arg.expr), steps: arg.steps, changed: true };
595
601
 
596
- this.final = true;
602
+ // mark as irreducible
603
+ this.final = true; // mark as irreducible at root
597
604
  }
605
+
598
606
  return { expr: this, steps: 0, changed: false };
599
607
  }
600
608
 
601
- reduce (args) {
602
- return this.fun.reduce([this.arg, ...args]);
609
+ invoke (arg) {
610
+ // propagate invocation towards the root term,
611
+ // caching partial applications as we go
612
+ const partial = this.fun.invoke(this.arg);
613
+ if (partial instanceof Expr)
614
+ return partial.apply(arg);
615
+ else if (typeof partial === 'function') {
616
+ this.invoke = partial;
617
+ return partial(arg);
618
+ } else {
619
+ // invoke = null => we're uncomputable, cache for next time
620
+ this.invoke = _ => null;
621
+ return null;
622
+ }
603
623
  }
604
624
 
605
625
  split () {
606
- // pretend we are an elegant (cons fun arg) and not a sleazy imperative array
626
+ // leftover from array-based older design
607
627
  return [this.fun, this.arg];
608
628
  }
609
629
 
@@ -622,15 +642,17 @@ class App extends Expr {
622
642
  return this.fun._rski(options).apply(this.arg._rski(options));
623
643
  }
624
644
 
625
- equals (other) {
645
+ diff (other, swap = false) {
626
646
  if (!(other instanceof App))
627
- return super.equals(other);
628
-
629
- return this.fun.equals(other.fun) && this.arg.equals(other.arg);
630
- }
631
-
632
- contains (other) {
633
- return this.fun.contains(other) || this.arg.contains(other) || super.contains(other);
647
+ return super.diff(other, swap);
648
+
649
+ const fun = this.fun.diff(other.fun, swap);
650
+ if (fun)
651
+ return fun + '(...)';
652
+ const arg = this.arg.diff(other.arg, swap);
653
+ if (arg)
654
+ return this.fun + '(' + arg + ')';
655
+ return null;
634
656
  }
635
657
 
636
658
  _braced (first) {
@@ -660,9 +682,10 @@ class App extends Expr {
660
682
 
661
683
  class Named extends Expr {
662
684
  /**
663
- * @desc a constant named 'name'
664
- * @param {String} name
665
- */
685
+ * @desc An abstract class representing a term named 'name'.
686
+ *
687
+ * @param {String} name
688
+ */
666
689
  constructor (name) {
667
690
  super();
668
691
  if (typeof name !== 'string' || name.length === 0)
@@ -680,18 +703,37 @@ class Named extends Expr {
680
703
  }
681
704
 
682
705
  _format (options, nargs) {
706
+ // NOTE fancyName is not yet official and may change name or meaning
707
+ const name = options.html ? this.fancyName ?? this.name : this.name;
683
708
  return this.arity > 0 && this.arity <= nargs
684
- ? options.redex[0] + this.name + options.redex[1]
685
- : this.name;
709
+ ? options.redex[0] + name + options.redex[1]
710
+ : name;
686
711
  }
687
712
  }
688
713
 
689
714
  let freeId = 0;
690
715
 
691
716
  class FreeVar extends Named {
692
- constructor (name) {
717
+ /**
718
+ * @desc A named variable.
719
+ *
720
+ * Given the functional nature of combinatory logic, variables are treated
721
+ * as functions that we don't know how to evaluate just yet.
722
+ *
723
+ * By default, two different variables even with the same name are considered different.
724
+ * They display it via a hidden id property.
725
+ *
726
+ * If a scope object is given, however, two variables with the same name and scope
727
+ * are considered identical.
728
+ *
729
+ * @param {string} name - name of the variable
730
+ * @param {any} scope - an object representing where the variable belongs to.
731
+ */
732
+ constructor (name, scope) {
693
733
  super(name);
694
734
  this.id = ++freeId;
735
+ // TODO temp compatibility mode
736
+ this.scope = scope === undefined ? this : scope;
695
737
  }
696
738
 
697
739
  weight () {
@@ -702,82 +744,76 @@ class FreeVar extends Named {
702
744
  return true;
703
745
  }
704
746
 
747
+ diff (other, swap = false) {
748
+ if (!(other instanceof FreeVar))
749
+ return super.diff(other, swap);
750
+ if (this.name === other.name && this.scope === other.scope)
751
+ return null;
752
+ const lhs = this.name + '[' + this.id + ']';
753
+ const rhs = other.name + '[' + other.id + ']';
754
+ return swap
755
+ ? '[' + rhs + ' != ' + lhs + ']'
756
+ : '[' + lhs + ' != ' + rhs + ']';
757
+ }
758
+
759
+ subst (search, replace) {
760
+ if (search instanceof FreeVar && search.name === this.name && search.scope === this.scope)
761
+ return replace;
762
+ return null;
763
+ }
764
+
705
765
  _format (options, nargs) {
706
- return options.var[0] + this.name + options.var[1];
766
+ const name = options.html ? this.fancyName ?? this.name : this.name;
767
+ return options.var[0] + name + options.var[1];
707
768
  }
708
769
  }
709
770
 
710
- /**
711
- * @typedef {function(Expr): Expr | AnyArity} AnyArity
712
- */
713
-
714
771
  class Native extends Named {
715
772
  /**
716
- * @desc A term named 'name' that converts next 'arity' arguments into
717
- * an expression returned by 'impl' function
718
- * If an apply: Expr=>Expr|null function is given, it will be attempted upon application
719
- * before building an App object. This allows to plug in argument coercions,
720
- * e.g. instantly perform a numeric operation natively if the next term is a number.
773
+ * @desc A named term with a known rewriting rule.
774
+ * 'impl' is a function with signature Expr => Expr => ... => Expr
775
+ * (see typedef Partial).
776
+ * This is how S, K, I, and company are implemented.
777
+ *
778
+ * Note that as of current something like a=>b=>b(a) is not possible,
779
+ * use full form instead: a=>b=>b.apply(a).
780
+ *
781
+ * @example new Native('K', x => y => x); // constant
782
+ * @example new Native('Y', function(f) { return f.apply(this.apply(f)); }); // self-application
783
+ *
721
784
  * @param {String} name
722
- * @param {AnyArity} impl
723
- * @param {{note: string?, arity: number?, canonize: boolean?, apply: function(Expr):(Expr|null) }} [opt]
785
+ * @param {Partial} impl
786
+ * @param {{note?: string, arity?: number, canonize?: boolean, apply?: function(Expr):(Expr|null) }} [opt]
724
787
  */
725
788
  constructor (name, impl, opt = {}) {
726
789
  super(name);
727
790
  // setup essentials
728
- this.impl = impl;
729
- if (opt.apply)
730
- this.onApply = opt.apply;
731
- this.arity = opt.arity ?? 1;
732
-
733
- // try to bootstrap and guess some of our properties
734
- const guess = (opt.canonize ?? true) ? this.guess() : { normal: false };
791
+ this.invoke = impl;
735
792
 
736
- if (!opt.arity)
737
- this.arity = guess.arity || 1;
793
+ // TODO infer lazily (on demand, only once); app capabilities such as discard and duplicate
794
+ // try to bootstrap and infer some of our properties
795
+ const guess = (opt.canonize ?? true) ? this.infer() : { normal: false };
738
796
 
797
+ this.arity = opt.arity || guess.arity || 1;
739
798
  this.note = opt.note ?? guess.expr?.format({ terse: true, html: true, lambda: ['', ' &mapsto; ', ''] });
740
799
  }
741
800
 
742
- apply (...args) {
743
- if (this.onApply && args.length >= 1) {
744
- if (typeof this.onApply !== 'function') {
745
- throw new Error('Native combinator ' + this + ' has an invalid onApply property of type'
746
- + typeof this.onApply + ': ' + this.onApply);
747
- }
748
- const subst = this.onApply(args[0]);
749
- if (subst instanceof Expr)
750
- return subst.apply(...args.slice(1));
751
- }
752
- return super.apply(...args);
753
- }
754
-
755
801
  _rski (options) {
756
802
  if (this === native.I || this === native.K || this === native.S || (options.steps >= options.max))
757
803
  return this;
758
- const canon = this.guess().expr;
804
+ const canon = this.infer().expr;
759
805
  if (!canon)
760
806
  return this;
761
807
  options.steps++;
762
808
  return canon._rski(options);
763
809
  }
764
-
765
- reduce (args) {
766
- if (args.length < this.arity)
767
- return null;
768
- let egde = 0;
769
- let step = this.impl;
770
- while (typeof step === 'function') {
771
- if (egde >= args.length)
772
- return null;
773
- step = step(args[egde++]);
774
- }
775
- if (!(step instanceof Expr))
776
- throw new Error('Native combinator ' + this + ' reduced to a non-expression: ' + step);
777
- return step.apply(...args.slice(egde));
778
- }
779
810
  }
780
811
 
812
+ // predefined global combinator list
813
+ // it is required by toSKI method, otherwise it could've as well be in parse.js
814
+ /**
815
+ * @type {{[key: string]: Native}}
816
+ */
781
817
  const native = {};
782
818
  function addNative (name, impl, opt) {
783
819
  native[name] = new Native(name, impl, opt);
@@ -785,9 +821,20 @@ function addNative (name, impl, opt) {
785
821
 
786
822
  class Lambda extends Expr {
787
823
  /**
788
- * @param {FreeVar|FreeVar[]} arg
789
- * @param {Expr} impl
790
- */
824
+ * @desc Lambda abstraction of arg over impl.
825
+ * Upon evaluation, all occurrences of 'arg' within 'impl' will be replaced
826
+ * with the provided argument.
827
+ *
828
+ * Note that 'arg' will be replaced by a localized placeholder, so the original
829
+ * variable can be used elsewhere without interference.
830
+ * Listing symbols contained in the lambda will omit such placeholder.
831
+ *
832
+ * Legacy ([FreeVar], impl) constructor is supported but deprecated.
833
+ * It will create a nested lambda expression.
834
+ *
835
+ * @param {FreeVar} arg
836
+ * @param {Expr} impl
837
+ */
791
838
  constructor (arg, impl) {
792
839
  if (Array.isArray(arg)) {
793
840
  // check args before everything
@@ -811,39 +858,45 @@ class Lambda extends Expr {
811
858
 
812
859
  super();
813
860
 
814
- // localize argument variable as it may appear elsewhere
815
- const local = new FreeVar(arg.name);
861
+ // localize argument variable and bind it to oneself
862
+ const local = new FreeVar(arg.name, this);
816
863
  this.arg = local;
817
864
  this.impl = impl.subst(arg, local) ?? impl;
818
865
  this.arity = 1;
819
866
  }
820
867
 
821
- getSymbols () {
822
- const out = this.impl.getSymbols();
823
- out.delete(this.arg);
824
- out.set(Expr.lambdaPlaceholder, (out.get(Expr.lambdaPlaceholder) ?? 0) + 1);
825
- return out;
826
- }
827
-
828
868
  weight () {
829
869
  return this.impl.weight() + 1;
830
870
  }
831
871
 
832
- _guess (options, preArgs = [], steps = 0) {
872
+ _infer (options, preArgs = [], steps = 0) {
833
873
  if (preArgs.length > options.maxArgs)
834
874
  return { normal: false, steps };
835
875
 
836
876
  const push = nthvar(preArgs.length + options.index);
837
- return this.reduce([push])._guess(options, [...preArgs, push], steps + 1);
877
+ return this.invoke(push)._infer(options, [...preArgs, push], steps + 1);
838
878
  }
839
879
 
840
- reduce (input) {
841
- if (input.length === 0)
842
- return null;
880
+ invoke (arg) {
881
+ return this.impl.subst(this.arg, arg) ?? this.impl;
882
+ }
883
+
884
+ traverse (change) {
885
+ const replaced = change(this);
886
+ if (replaced instanceof Expr)
887
+ return replaced;
843
888
 
844
- const [head, ...tail] = input;
889
+ // alas no proper shielding of self.arg is possible
890
+ const impl = this.impl.traverse(change);
891
+
892
+ if (!impl)
893
+ return null; // no changes
894
+
895
+ return new Lambda(this.arg, impl);
896
+ }
845
897
 
846
- return (this.impl.subst(this.arg, head) ?? this.impl).apply(...tail);
898
+ any (predicate) {
899
+ return predicate(this) || this.impl.any(predicate);
847
900
  }
848
901
 
849
902
  subst (search, replace) {
@@ -864,12 +917,12 @@ class Lambda extends Expr {
864
917
  options.steps++;
865
918
  if (impl === this.arg)
866
919
  return native.I;
867
- if (!impl.getSymbols().has(this.arg))
920
+ if (!impl.any(e => e === this.arg))
868
921
  return native.K.apply(impl);
869
922
  if (impl instanceof App) {
870
923
  const [fst, snd] = impl.split();
871
924
  // try eta reduction
872
- if (snd === this.arg && !fst.getSymbols().has(this.arg))
925
+ if (snd === this.arg && !fst.any(e => e === this.arg))
873
926
  return fst._rski(options);
874
927
  // fall back to S
875
928
  return native.S.apply(
@@ -880,25 +933,16 @@ class Lambda extends Expr {
880
933
  throw new Error('Don\'t know how to convert to SKI' + this);
881
934
  }
882
935
 
883
- _replace (pairs, opt) {
884
- const maybe = super._replace(pairs, opt);
885
- if (maybe)
886
- return maybe;
887
- // TODO filter out terms containing this.arg
888
- return new Lambda(this.arg, this.impl._replace(pairs, opt) ?? this.impl);
889
- }
890
-
891
- equals (other) {
936
+ diff (other, swap = false) {
892
937
  if (!(other instanceof Lambda))
893
- return super.equals(other);
938
+ return super.diff(other, swap);
894
939
 
895
- const t = new FreeVar('t');
940
+ const t = new FreeVar('t'); // TODO better placeholder name
896
941
 
897
- return other.reduce([t]).equals(this.reduce([t]));
898
- }
899
-
900
- contains (other) {
901
- return this.equals(other) || this.impl.contains(other);
942
+ const diff = this.invoke(t).diff(other.invoke(t), swap);
943
+ if (diff)
944
+ return '(t->' + diff + ')'; // parentheses required to avoid ambiguity
945
+ return null;
902
946
  }
903
947
 
904
948
  _format (options, nargs) {
@@ -920,6 +964,11 @@ class Lambda extends Expr {
920
964
  }
921
965
 
922
966
  class Church extends Native {
967
+ /**
968
+ * @desc Church numeral representing non-negative integer n:
969
+ * n f x = f(f(...(f x)...)) with f applied n times.
970
+ * @param {number} n
971
+ */
923
972
  constructor (n) {
924
973
  const p = Number.parseInt(n);
925
974
  if (!(p >= 0))
@@ -938,10 +987,14 @@ class Church extends Native {
938
987
  this.arity = 2;
939
988
  }
940
989
 
941
- equals (other) {
942
- if (other instanceof Church)
943
- return this.n === other.n;
944
- return super.equals(other);
990
+ diff (other, swap = false) {
991
+ if (!(other instanceof Church))
992
+ return super.diff(other, swap);
993
+ if (this.n === other.n)
994
+ return null;
995
+ return swap
996
+ ? '[' + other.n + ' != ' + this.n + ']'
997
+ : '[' + this.n + ' != ' + other.n + ']';
945
998
  }
946
999
 
947
1000
  _unspaced (arg) {
@@ -949,9 +1002,23 @@ class Church extends Native {
949
1002
  }
950
1003
  }
951
1004
 
1005
+ function waitn (expr, n) {
1006
+ return arg => n <= 1 ? expr.apply(arg) : waitn(expr.apply(arg), n - 1);
1007
+ }
1008
+
952
1009
  class Alias extends Named {
953
1010
  /**
954
- * @desc An existing expression under a different name.
1011
+ * @desc A named alias for an existing expression.
1012
+ *
1013
+ * Upon evaluation, the alias expands into the original expression,
1014
+ * unless it has a known arity > 0 and is marked terminal,
1015
+ * in which case it waits for enough arguments before expanding.
1016
+ *
1017
+ * A hidden mutable property 'outdated' is used to silently
1018
+ * replace the alias with its definition in all contexts.
1019
+ * This is used when declaring named terms in an interpreter,
1020
+ * to avoid confusion between old and new terms with the same name.
1021
+ *
955
1022
  * @param {String} name
956
1023
  * @param {Expr} impl
957
1024
  * @param {{canonize: boolean?, max: number?, maxArgs: number?, note: string?, terminal: boolean?}} [options]
@@ -964,16 +1031,13 @@ class Alias extends Named {
964
1031
  this.note = options.note;
965
1032
 
966
1033
  const guess = options.canonize
967
- ? impl.guess({ max: options.max, maxArgs: options.maxArgs })
1034
+ ? impl.infer({ max: options.max, maxArgs: options.maxArgs })
968
1035
  : { normal: false };
969
1036
  this.arity = (guess.proper && guess.arity) || 0;
970
1037
  this.proper = guess.proper ?? false;
971
1038
  this.terminal = options.terminal ?? this.proper;
972
1039
  this.canonical = guess.expr;
973
- }
974
-
975
- getSymbols () {
976
- return this.terminal ? new Map([[this, 1]]) : this.impl.getSymbols();
1040
+ this.invoke = waitn(impl, this.arity);
977
1041
  }
978
1042
 
979
1043
  weight () {
@@ -984,14 +1048,22 @@ class Alias extends Named {
984
1048
  return this.impl.expand();
985
1049
  }
986
1050
 
1051
+ traverse (change) {
1052
+ return change(this) ?? this.impl.traverse(change);
1053
+ }
1054
+
1055
+ any (predicate) {
1056
+ return predicate(this) || this.impl.any(predicate);
1057
+ }
1058
+
987
1059
  subst (search, replace) {
988
1060
  if (this === search)
989
1061
  return replace;
990
1062
  return this.impl.subst(search, replace);
991
1063
  }
992
1064
 
993
- _guess (options, preArgs = [], steps = 0) {
994
- return this.impl._guess(options, preArgs, steps);
1065
+ _infer (options, preArgs = [], steps = 0) {
1066
+ return this.impl._infer(options, preArgs, steps);
995
1067
  }
996
1068
 
997
1069
  /**
@@ -1006,22 +1078,14 @@ class Alias extends Named {
1006
1078
  return { expr: this.impl, steps: 0, changed: true };
1007
1079
  }
1008
1080
 
1009
- reduce (args) {
1010
- if (args.length < this.arity)
1011
- return null;
1012
- return this.impl.apply(...args);
1013
- }
1014
-
1015
1081
  _firstVar () {
1016
1082
  return this.impl._firstVar();
1017
1083
  }
1018
1084
 
1019
- equals (other) {
1020
- return other.equals(this.impl);
1021
- }
1022
-
1023
- contains (other) {
1024
- return this.impl.contains(other);
1085
+ diff (other, swap = false) {
1086
+ if (this === other)
1087
+ return null;
1088
+ return other.diff(this.impl, !swap);
1025
1089
  }
1026
1090
 
1027
1091
  _rski (options) {
@@ -1040,6 +1104,7 @@ class Alias extends Named {
1040
1104
  }
1041
1105
 
1042
1106
  _declare (output, inventory, seen) {
1107
+ // this is part of the 'declare' function, see below
1043
1108
  // only once
1044
1109
  if (seen.has(this))
1045
1110
  return;
@@ -1064,20 +1129,15 @@ addNative('B', x => y => z => x.apply(y.apply(z)));
1064
1129
  addNative('C', x => y => z => x.apply(z).apply(y));
1065
1130
  addNative('W', x => y => x.apply(y).apply(y));
1066
1131
 
1067
- addNative('+', x => y => z => y.apply(x.apply(y, z)), {
1068
- note: '<var>n</var> &mapsto; <var>n</var> + 1 <i>or</i> SB',
1069
- apply: arg => arg instanceof Church ? new Church(arg.n + 1) : null
1070
- });
1071
-
1072
- // A global value meaning "lambda is used somewhere in this expression"
1073
- // Can't be used (at least for now) to construct lambda expressions, or anything at all.
1074
- // See also getSymbols().
1075
- Expr.lambdaPlaceholder = new Native('->', x => x, {
1076
- arity: 1,
1077
- canonize: false,
1078
- note: 'Lambda placeholder',
1079
- apply: x => { throw new Error('Attempt to use a placeholder in expression') }
1080
- });
1132
+ addNative(
1133
+ '+',
1134
+ n => n instanceof Church
1135
+ ? new Church(n.n + 1)
1136
+ : f => x => f.apply(n.apply(f, x)),
1137
+ {
1138
+ note: 'Increase a Church numeral argument by 1, otherwise n => f => x => f(n f x)',
1139
+ }
1140
+ );
1081
1141
 
1082
1142
  // utility functions dependent on Expr* classes, in alphabetical order
1083
1143
 
@@ -1136,9 +1196,28 @@ function declare (inventory) {
1136
1196
  }
1137
1197
 
1138
1198
  function maybeLambda (args, expr, caps = {}) {
1139
- const sym = expr.getSymbols();
1199
+ const count = new Array(args.length).fill(0);
1200
+ let proper = true;
1201
+ expr.traverse(e => {
1202
+ if (e instanceof FreeVar) {
1203
+ const index = args.findIndex(a => a.name === e.name);
1204
+ if (index >= 0) {
1205
+ count[index]++;
1206
+ return;
1207
+ }
1208
+ }
1209
+ if (!(e instanceof App))
1210
+ proper = false;
1211
+ });
1140
1212
 
1141
- const [skip, dup] = skipDup(args, sym);
1213
+ const skip = new Set();
1214
+ const dup = new Set();
1215
+ for (let i = 0; i < args.length; i++) {
1216
+ if (count[i] === 0)
1217
+ skip.add(i);
1218
+ else if (count[i] > 1)
1219
+ dup.add(i);
1220
+ }
1142
1221
 
1143
1222
  return {
1144
1223
  expr: args.length ? new Lambda(args, expr) : expr,
@@ -1147,7 +1226,7 @@ function maybeLambda (args, expr, caps = {}) {
1147
1226
  ...(dup.size ? { dup } : {}),
1148
1227
  duplicate: !!dup.size || caps.duplicate || false,
1149
1228
  discard: !!skip.size || caps.discard || false,
1150
- proper: isSubset(sym.keys(), new Set(args)),
1229
+ proper,
1151
1230
  };
1152
1231
  }
1153
1232
 
@@ -1161,7 +1240,7 @@ function naiveCanonize (expr) {
1161
1240
  if (expr instanceof Alias)
1162
1241
  return naiveCanonize(expr.impl);
1163
1242
 
1164
- const canon = expr.guess();
1243
+ const canon = expr.infer();
1165
1244
  if (canon.expr)
1166
1245
  return canon.expr;
1167
1246
 
@@ -1225,10 +1304,13 @@ function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
1225
1304
  }
1226
1305
  }
1227
1306
 
1228
- const canon = expr.guess({ max: options.max, maxArgs: options.maxArgs });
1307
+ const canon = expr.infer({ max: options.max, maxArgs: options.maxArgs });
1229
1308
  state.steps += canon.steps;
1230
1309
  if (canon.expr && canon.expr.weight() < maxWeight)
1231
1310
  yield { expr: canon.expr, steps: state.steps, comment: '(canonical)' };
1232
1311
  }
1233
1312
 
1234
- module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church, globalOptions, native, declare };
1313
+ Expr.declare = declare;
1314
+ Expr.native = native;
1315
+
1316
+ module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church };