@dallaylaen/ski-interpreter 1.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 ADDED
@@ -0,0 +1,1009 @@
1
+ const { missingIndices, isSubset } = require('./util');
2
+
3
+ const globalOptions = {
4
+ terse: true,
5
+ max: 1000,
6
+ maxArgs: 32,
7
+ };
8
+
9
+ class Expr {
10
+ /**
11
+ * @descr A generic combinatory logic expression.
12
+ */
13
+ constructor () {
14
+ if (new.target === Expr)
15
+ throw new Error('Attempt to instantiate abstract class Expr');
16
+ this.arity = Infinity;
17
+ }
18
+
19
+ /**
20
+ * postprocess term after parsing. typically return self but may return other term or die
21
+ * @return {Expr}
22
+ */
23
+ postParse () {
24
+ return this;
25
+ }
26
+
27
+ /**
28
+ * @desc apply self to zero or more terms and return the resulting term,
29
+ * without performing any calculations whatsoever
30
+ * @param {Expr} args
31
+ * @return {Expr}
32
+ */
33
+ apply (...args) {
34
+ return args.length > 0 ? new App(this, ...args) : this;
35
+ }
36
+
37
+ /**
38
+ * expand all terms but don't perform any calculations
39
+ * @return {Expr}
40
+ */
41
+ expand () {
42
+ return this;
43
+ }
44
+
45
+ /**
46
+ * @desc return all free variables within the term
47
+ * @return {Set<FreeVar>}
48
+ */
49
+ freeVars () {
50
+ const symbols = this.getSymbols();
51
+ const out = new Set();
52
+ for (const [key, _] of symbols) {
53
+ if (key instanceof FreeVar)
54
+ out.add(key);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ hasLambda () {
60
+ const sym = this.getSymbols();
61
+ return sym.has(Expr.lambdaPlaceholder);
62
+ }
63
+
64
+ freeOnly () {
65
+ for (const [key, _] of this.getSymbols()) {
66
+ if (!(key instanceof FreeVar))
67
+ return false;
68
+ }
69
+ return true;
70
+ }
71
+
72
+ /**
73
+ * @desc return all terminal values within the term, that is, values not
74
+ * composed of other terms. For example, in S(KI)K, the terminals are S, K, I.
75
+ * @return {Map<Expr, number>}
76
+ */
77
+ getSymbols () {
78
+ // TODO better name!
79
+ return new Map([[this, 1]]);
80
+ }
81
+
82
+ /**
83
+ * @desc rought estimate of the complexity of the term
84
+ * @return {number}
85
+ */
86
+ weight () {
87
+ return 1;
88
+ }
89
+
90
+ /**
91
+ *
92
+ * @param {{max: number?, maxArgs: number?, bestGuess?: Expr}} options
93
+ * @return {{
94
+ * found: boolean,
95
+ * proper: boolean,
96
+ * arity: number?,
97
+ * linear: boolean?,
98
+ * canonical?: Expr,
99
+ * steps: number?,
100
+ * skip: Set<number>?
101
+ * }}
102
+ */
103
+ canonize (options = {}) {
104
+ const max = options.max ?? globalOptions.max;
105
+ const maxArgs = options.maxArgs ?? globalOptions.maxArgs;
106
+
107
+ let steps = 0;
108
+ let expr = this;
109
+ const jar = [];
110
+ for (let i = 0; i < maxArgs; i++) {
111
+ const calc = expr.run({ max });
112
+ steps += calc.steps;
113
+ if (!calc.final)
114
+ break;
115
+ expr = calc.expr;
116
+ if (!expr.wantsArgs()) {
117
+ // found!
118
+ const symbols = expr.getSymbols();
119
+ const skip = missingIndices(jar, symbols);
120
+ const proper = isSubset(symbols.keys(), new Set(jar));
121
+ const duplicates = [...symbols.entries()].filter(([_, v]) => v > 1);
122
+ const linear = proper && skip.size === 0 && duplicates.length === 0;
123
+ return {
124
+ arity: i,
125
+ found: true,
126
+ canonical: maybeLambda(jar, expr),
127
+ proper,
128
+ linear,
129
+ steps,
130
+ ...(skip.size ? { skip } : {}),
131
+ };
132
+ }
133
+ const next = new FreeVar('abcdefgh'[i] ?? 'x' + i);
134
+ jar.push(next);
135
+ expr = expr.apply(next);
136
+ }
137
+
138
+ const fallback = { found: false, proper: false, steps };
139
+ if (options.bestGuess)
140
+ fallback.canonical = options.bestGuess;
141
+ return fallback;
142
+ }
143
+
144
+ /**
145
+ * @desc Returns a series of lambda terms equivalent to the given expression,
146
+ * up to the provided computation steps limit,
147
+ * in decreasing weight order.
148
+ * @param {{
149
+ * max: number?,
150
+ * maxArgs: number?,
151
+ * varGen: function(void): FreeVar?,
152
+ * steps: number?,
153
+ * html: boolean?,
154
+ * latin: number?,
155
+ * }} options
156
+ * @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
157
+ * @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
158
+ */
159
+ * lambdify (options = {}) {
160
+ const expr = naiveCanonize(this, options);
161
+ yield * simplifyLambda(expr, options);
162
+ }
163
+
164
+ /**
165
+ * @desc same semantics as walk() but rewrite step by step instead of computing
166
+ * @param {{max: number?}} options
167
+ * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
168
+ */
169
+ * rewriteSKI (options = {}) {
170
+ // TODO options.max is not actually max, it's the number of steps in one iteration
171
+ let steps = 0;
172
+ let expr = this;
173
+ while (true) {
174
+ const opt = { max: options.max ?? 1, steps: 0 };
175
+ const next = expr._rski(opt);
176
+ const final = opt.steps === 0;
177
+ yield { expr, steps, final };
178
+ if (final)
179
+ break;
180
+ expr = next;
181
+ steps += opt.steps;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * @desc Rename free variables in the expression using the given sequence
187
+ * This is for eye-candy only, as the interpreter knows darn well hot to distinguish vars,
188
+ * regardless of names.
189
+ * @param {IterableIterator<string>} seq
190
+ * @return {Expr}
191
+ */
192
+ renameVars (seq) {
193
+ return this;
194
+ }
195
+
196
+ _rski (options) {
197
+ return this;
198
+ }
199
+
200
+ /**
201
+ * @desc Whether the term will reduce further if given more arguments.
202
+ * In practice, equivalent to "starts with a FreeVar"
203
+ * Used by canonize (duh...)
204
+ * @return {boolean}
205
+ */
206
+ wantsArgs () {
207
+ return true;
208
+ }
209
+
210
+ /**
211
+ * Apply self to list of given args.
212
+ * Normally, only native combinators know how to do it.
213
+ * @param {Expr[]} args
214
+ * @return {Expr|null}
215
+ */
216
+ reduce (args) {
217
+ return null;
218
+ }
219
+
220
+ /**
221
+ * Replace all instances of free vars with corresponding values and return the resulting expression.
222
+ * return null if no changes could be made.
223
+ * @param {FreeVar} plug
224
+ * @param {Expr} value
225
+ * @return {Expr|null}
226
+ */
227
+ subst (plug, value) {
228
+ return null;
229
+ }
230
+
231
+ /**
232
+ * @desc iterate one step of calculation in accordance with known rules.
233
+ * @return {{expr: Expr, steps: number, changed: boolean}}
234
+ */
235
+ step () { return { expr: this, steps: 0, changed: false } }
236
+
237
+ /**
238
+ * @desc Run uninterrupted sequence of step() applications
239
+ * until the expression is irreducible, or max number of steps is reached.
240
+ * Default number of steps = 1000.
241
+ * @param {{max: number?, steps: number?, throw: boolean?}|Expr} [opt]
242
+ * @param {Expr} args
243
+ * @return {{expr: Expr, steps: number, final: boolean}}
244
+ */
245
+ run (opt = {}, ...args) {
246
+ if (opt instanceof Expr) {
247
+ args.unshift(opt);
248
+ opt = {};
249
+ }
250
+ let expr = args ? this.apply(...args) : this;
251
+ let steps = opt.steps ?? 0;
252
+ const max = (opt.max ?? globalOptions.max) + steps;
253
+ let final = false;
254
+ for (; steps < max; ) {
255
+ const next = expr.step();
256
+ if (!next.changed) {
257
+ final = true;
258
+ break;
259
+ }
260
+ steps += next.steps;
261
+ expr = next.expr;
262
+ }
263
+ if (opt.throw && !final)
264
+ throw new Error('Failed to compute expression in ' + max + ' steps');
265
+ return { final, steps, expr };
266
+ }
267
+
268
+ /**
269
+ * Execute step() while possible, yielding a brief description of events after each step.
270
+ * Mnemonics: like run() but slower.
271
+ * @param {{max: number?}} options
272
+ * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
273
+ */
274
+ * walk (options = {}) {
275
+ const max = options.max ?? Infinity;
276
+ let steps = 0;
277
+ let expr = this;
278
+ let final = false;
279
+
280
+ while (steps < max) {
281
+ // 1. calculate
282
+ // 2. yield _unchanged_ expression
283
+ // 3. either advance or stop
284
+ const next = expr.step();
285
+ if (!next.changed)
286
+ final = true;
287
+ yield { expr, steps, final };
288
+ if (final)
289
+ break;
290
+ steps += next.steps;
291
+ expr = next.expr;
292
+ }
293
+ }
294
+
295
+ /**
296
+ *
297
+ * @param {Expr} other
298
+ * @return {boolean}
299
+ */
300
+ equals (other) {
301
+ return this === other;
302
+ }
303
+
304
+ contains (other) {
305
+ return this === other || this.equals(other);
306
+ }
307
+
308
+ expect (other) {
309
+ if (!(other instanceof Expr))
310
+ throw new Error('Attempt to expect a combinator to equal something else: ' + other);
311
+ if (this.equals(other))
312
+ return;
313
+
314
+ // TODO wanna use AssertionError but webpack doesn't recognize it
315
+ // still the below hack works for mocha-based tests.
316
+ const poorMans = new Error('Found term ' + this + ' but expected ' + other);
317
+ poorMans.expected = other.toString();
318
+ poorMans.actual = this.toString();
319
+ throw poorMans;
320
+ }
321
+
322
+ /**
323
+ * @param {{terse: boolean?, html: boolean?}} [options]
324
+ * @return {string} string representation of the expression
325
+ */
326
+ toString (options = {}) {
327
+ // uncomment the following line if you want to debug the parser with prints
328
+ // return this.constructor.name
329
+ throw new Error( 'No toString() method defined in class ' + this.constructor.name );
330
+ }
331
+
332
+ /**
333
+ *
334
+ * @return {boolean}
335
+ */
336
+ needsParens () {
337
+ return false;
338
+ }
339
+
340
+ /**
341
+ *
342
+ * @return {string}
343
+ */
344
+ toJSON () {
345
+ return this.expand().toString({ terse: false });
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Constants that define when whitespace between terms may be omitted in App.toString()
351
+ */
352
+ const BITS = 4;
353
+ const [T_UNKNOWN, T_PARENS, T_UPPER, T_LOWER]
354
+ = (function * () { for (let i = 0; ; yield i++); })();
355
+ const canLump = new Set([
356
+ (T_PARENS << BITS) + T_PARENS,
357
+ (T_PARENS << BITS) + T_UPPER,
358
+ (T_UPPER << BITS) + T_PARENS,
359
+ (T_UPPER << BITS) + T_UPPER,
360
+ (T_UPPER << BITS) + T_LOWER,
361
+ (T_LOWER << BITS) + T_PARENS,
362
+ (T_UNKNOWN << BITS) + T_PARENS,
363
+ ]);
364
+
365
+ class App extends Expr {
366
+ /**
367
+ * @desc Application of fun() to args.
368
+ * Never ever use new App(fun, ...args) directly, use fun.apply(...args) instead.
369
+ * @param {Expr} fun
370
+ * @param {Expr} args
371
+ */
372
+ constructor (fun, ...args) {
373
+ if (args.length === 0)
374
+ throw new Error('Attempt to create an application with no arguments (likely interpreter bug)');
375
+ super();
376
+ this.fun = fun;
377
+ this.args = args;
378
+ this.final = false;
379
+ }
380
+
381
+ weight () {
382
+ return this.args.reduce((acc, x) => acc + x.weight(), this.fun.weight());
383
+ }
384
+
385
+ getSymbols () {
386
+ const out = this.fun.getSymbols();
387
+ for (const term of this.args) {
388
+ for (const [key, value] of term.getSymbols())
389
+ out.set(key, (out.get(key) ?? 0) + value);
390
+ }
391
+ return out;
392
+ }
393
+
394
+ wantsArgs () {
395
+ return this.fun.wantsArgs();
396
+ }
397
+
398
+ apply (...args) {
399
+ if (args.length === 0)
400
+ return this;
401
+ return this.fun.apply( ...this.args, ...args);
402
+ }
403
+
404
+ expand () {
405
+ return this.fun.expand().apply(...this.args.map(x => x.expand()));
406
+ }
407
+
408
+ canonize (options = {}) {
409
+ const [fun, arg] = this.split().map(x => x.canonize(options).canonical);
410
+ return super.canonize({
411
+ ...options,
412
+ ...(fun && arg ? { bestGuess: fun.apply(arg) } : {})
413
+ });
414
+ }
415
+
416
+ renameVars (seq) {
417
+ const fun = this.fun.renameVars(seq);
418
+ const args = this.args.map(x => x.renameVars(seq));
419
+ return fun.apply(...args);
420
+ }
421
+
422
+ subst (plug, value) {
423
+ const fun = this.fun.subst(plug, value);
424
+ let change = fun === null ? 0 : 1;
425
+ const args = [];
426
+ for (const x of this.args) {
427
+ const next = x.subst(plug, value);
428
+ if (next === null)
429
+ args.push(x);
430
+ else {
431
+ args.push(next);
432
+ change++;
433
+ }
434
+ }
435
+
436
+ return change ? (fun ?? this.fun).apply(...args) : null;
437
+ }
438
+
439
+ /**
440
+ * @return {{expr: Expr, steps: number}}
441
+ */
442
+
443
+ step () {
444
+ // normal reduction order: first try root, then at most 1 step
445
+ if (!this.final) {
446
+ const reduced = this.fun.reduce(this.args);
447
+ if (reduced)
448
+ return { expr: reduced, steps: 1, changed: true };
449
+
450
+ // now try recursing
451
+
452
+ const fun = this.fun.step();
453
+ if (fun.changed)
454
+ return { expr: fun.expr.apply(...this.args), steps: fun.steps, changed: true };
455
+
456
+ for (let i = 0; i < this.args.length; i++) {
457
+ const next = this.args[i].step();
458
+ if (!next.changed)
459
+ continue;
460
+ const args = this.args.slice();
461
+ args[i] = next.expr;
462
+ return { expr: this.fun.apply(...args), steps: next.steps, changed: true };
463
+ }
464
+ }
465
+ this.final = true;
466
+ return { expr: this, steps: 0, changed: false };
467
+ }
468
+
469
+ split () {
470
+ // pretend we are an elegant (cons fun arg) and not a sleazy imperative array
471
+ const args = this.args.slice();
472
+ const last = args.pop();
473
+ return [this.fun.apply(...args), last];
474
+ }
475
+
476
+ /**
477
+ * @desc Convert the expression to SKI combinatory logic
478
+ * @return {Expr}
479
+ */
480
+
481
+ _rski (options) {
482
+ if (options.steps >= options.max)
483
+ return this;
484
+ return this.fun._rski(options).apply(...this.args.map(x => x._rski(options)));
485
+ }
486
+
487
+ equals (other) {
488
+ if (!(other instanceof App))
489
+ return false;
490
+ if (other.args.length !== this.args.length)
491
+ return false;
492
+ if (!this.fun.equals(other.fun))
493
+ return false;
494
+ for (let i = 0; i < this.args.length; i++) {
495
+ if (!this.args[i].equals(other.args[i]))
496
+ return false;
497
+ }
498
+ return true;
499
+ }
500
+
501
+ contains (other) {
502
+ if (this.fun.contains(other))
503
+ return true;
504
+ for (const subtree of this.args) {
505
+ if (subtree.contains(other))
506
+ return true;
507
+ }
508
+ return super.contains(other);
509
+ }
510
+
511
+ toString (opt = {}) {
512
+ if (opt.terse ?? globalOptions.terse) {
513
+ const out = [];
514
+ let oldType = 0;
515
+ // stupid ad-hoc state machine, see above for constant definitions
516
+ for (const term of [this.fun, ...this.args]) {
517
+ let s = term.toString(opt);
518
+ let newType = T_UNKNOWN;
519
+ if (s.match(/^[A-Z]$/))
520
+ newType = T_UPPER;
521
+ else if (term instanceof FreeVar || s.match(/^[a-z][a-z_0-9]*$/))
522
+ newType = T_LOWER;
523
+ else if (s.match(/^[0-9]+$/))
524
+ // no special treatment for numerals, skip
525
+ ;
526
+ else if (out.length !== 0 || term.needsParens()) {
527
+ s = '(' + s + ')';
528
+ newType = T_PARENS;
529
+ }
530
+ if (!canLump.has((oldType << BITS) | newType) && out.length > 0)
531
+ out.push(' ');
532
+ out.push(s);
533
+ oldType = newType;
534
+ }
535
+ return out.join('');
536
+ } else {
537
+ const fun = this.fun.toString(opt);
538
+ const root = this.fun.needsParens() ? '(' + fun + ')' : fun;
539
+ return root + this.args.map(x => '(' + x.toString(opt) + ')').join('');
540
+ }
541
+ }
542
+ }
543
+
544
+ class Named extends Expr {
545
+ /**
546
+ * @desc a constant named 'name'
547
+ * @param {String} name
548
+ */
549
+ constructor (name) {
550
+ super();
551
+ if (typeof name !== 'string' || name.length === 0)
552
+ throw new Error('Attempt to create a named term with improper name');
553
+ this.name = name;
554
+ }
555
+
556
+ toString () {
557
+ return this.name;
558
+ }
559
+ }
560
+
561
+ let freeId = 0;
562
+
563
+ class FreeVar extends Named {
564
+ constructor (name) {
565
+ super(name);
566
+ this.id = ++freeId;
567
+ }
568
+
569
+ subst (plug, value) {
570
+ if (this === plug)
571
+ return value;
572
+ return null;
573
+ }
574
+
575
+ weight () {
576
+ return 0;
577
+ }
578
+
579
+ wantsArgs () {
580
+ return false;
581
+ }
582
+
583
+ toString ( opt = {} ) {
584
+ return (opt.html && /^[a-z]$/.test(this.name)) ? '<var>' + this.name + '</var>' : this.name;
585
+ }
586
+ }
587
+
588
+ /**
589
+ * @typedef {function(Expr): Expr | AnyArity} AnyArity
590
+ */
591
+
592
+ class Native extends Named {
593
+ /**
594
+ * @desc A term named 'name' that converts next 'arity' arguments into
595
+ * an expression returned by 'impl' function
596
+ * If an apply: Expr=>Expr|null function is given, it will be attempted upon application
597
+ * before building an App object. This allows to plug in argument coercions,
598
+ * e.g. instantly perform a numeric operation natively if the next term is a number.
599
+ * @param {String} name
600
+ * @param {AnyArity} impl
601
+ * @param {{note: string?, arity: number?, canonize: boolean?, apply: function(Expr):(Expr|null) }} [opt]
602
+ */
603
+ constructor (name, impl, opt = {}) {
604
+ super(name);
605
+ // setup essentials
606
+ this.impl = impl;
607
+ if (opt.apply)
608
+ this.onApply = opt.apply;
609
+ this.arity = opt.arity ?? 1;
610
+
611
+ // try to bootstrap and guess some of our properties
612
+ const guess = (opt.canonize ?? true) ? this.canonize() : { found: false };
613
+
614
+ if (!opt.arity)
615
+ this.arity = guess.arity || 1;
616
+
617
+ this.note = opt.note ?? guess.canonical?.toString({ terse: true, html: true });
618
+ }
619
+
620
+ apply (...args) {
621
+ if (this.onApply && args.length >= 1) {
622
+ if (typeof this.onApply !== 'function') {
623
+ throw new Error('Native combinator ' + this + ' has an invalid onApply property of type'
624
+ + typeof this.onApply + ': ' + this.onApply);
625
+ }
626
+ const subst = this.onApply(args[0]);
627
+ if (subst instanceof Expr)
628
+ return subst.apply(...args.slice(1));
629
+ }
630
+ return super.apply(...args);
631
+ }
632
+
633
+ _rski (options) {
634
+ if (this === native.I || this === native.K || this === native.S || (options.steps >= options.max))
635
+ return this;
636
+ const canon = this.canonize().canonical;
637
+ if (!canon)
638
+ return this;
639
+ options.steps++;
640
+ return canon._rski(options);
641
+ }
642
+
643
+ reduce (args) {
644
+ if (args.length < this.arity)
645
+ return null;
646
+ let egde = 0;
647
+ let step = this.impl;
648
+ while (typeof step === 'function') {
649
+ if (egde >= args.length)
650
+ return null;
651
+ step = step(args[egde++]);
652
+ }
653
+ if (!(step instanceof Expr))
654
+ throw new Error('Native combinator ' + this + ' reduced to a non-expression: ' + step);
655
+ return step.apply(...args.slice(egde));
656
+ }
657
+
658
+ toJSON () {
659
+ return 'Native:' + this.name;
660
+ }
661
+ }
662
+
663
+ const native = {};
664
+ function addNative (name, impl, opt) {
665
+ native[name] = new Native(name, impl, opt);
666
+ }
667
+
668
+ class Lambda extends Expr {
669
+ /**
670
+ * @param {FreeVar|FreeVar[]} arg
671
+ * @param {Expr} impl
672
+ */
673
+ constructor (arg, impl) {
674
+ if (Array.isArray(arg)) {
675
+ // check args before everything
676
+ if (arg.length === 0)
677
+ throw new Error('empty argument list in lambda constructor');
678
+
679
+ const [my, ...tail] = arg;
680
+ const known = new Set([my.name]);
681
+
682
+ while (tail.length > 0) {
683
+ const last = tail.pop();
684
+ if (known.has(last.name))
685
+ throw new Error('Duplicate free var name ' + last + ' in lambda expression');
686
+ known.add(last.name);
687
+
688
+ // TODO keep track of arity to speed up execution
689
+ impl = new Lambda(last, impl);
690
+ }
691
+ arg = my;
692
+ }
693
+
694
+ super();
695
+
696
+ // localize argument variable as it may appear elsewhere
697
+ const local = new FreeVar(arg.name);
698
+ this.arg = local;
699
+ this.impl = impl.subst(arg, local) ?? impl;
700
+ this.arity = 1;
701
+ }
702
+
703
+ getSymbols () {
704
+ const out = this.impl.getSymbols();
705
+ out.delete(this.arg);
706
+ out.set(Expr.lambdaPlaceholder, (out.get(Expr.lambdaPlaceholder) ?? 0) + 1);
707
+ return out;
708
+ }
709
+
710
+ weight () {
711
+ return this.impl.weight() + 1;
712
+ }
713
+
714
+ reduce (input) {
715
+ if (input.length === 0)
716
+ return null;
717
+
718
+ const [head, ...tail] = input;
719
+
720
+ return (this.impl.subst(this.arg, head) ?? this.impl).apply(...tail);
721
+ }
722
+
723
+ subst (plug, value) {
724
+ if (plug === this.arg)
725
+ return null;
726
+ const change = this.impl.subst(plug, value);
727
+ if (change)
728
+ return new Lambda(this.arg, change);
729
+ return null;
730
+ }
731
+
732
+ expand () {
733
+ return new Lambda(this.arg, this.impl.expand());
734
+ }
735
+
736
+ renameVars (seq) {
737
+ const arg = new FreeVar(seq.next().value);
738
+ const impl = this.impl.subst(this.arg, arg) ?? this.impl;
739
+ return new Lambda(arg, impl.renameVars(seq));
740
+ }
741
+
742
+ _rski (options) {
743
+ const impl = this.impl._rski(options);
744
+ if (options.steps >= options.max)
745
+ return new Lambda(this.arg, impl);
746
+ options.steps++;
747
+ if (impl === this.arg)
748
+ return native.I;
749
+ if (!impl.getSymbols().has(this.arg))
750
+ return native.K.apply(impl);
751
+ if (impl instanceof App) {
752
+ const [fst, snd] = impl.split();
753
+ // try eta reduction
754
+ if (snd === this.arg && !fst.getSymbols().has(this.arg))
755
+ return fst._rski(options);
756
+ // fall back to S
757
+ return native.S.apply(
758
+ (new Lambda(this.arg, fst))._rski(options),
759
+ (new Lambda(this.arg, snd))._rski(options)
760
+ );
761
+ }
762
+ throw new Error('Don\'t know how to convert to SKI' + this);
763
+ }
764
+
765
+ equals (other) {
766
+ if (!(other instanceof Lambda))
767
+ return false;
768
+
769
+ const t = new FreeVar('t');
770
+
771
+ return other.reduce([t]).equals(this.reduce([t]));
772
+ }
773
+
774
+ contains (other) {
775
+ return this.equals(other) || this.impl.contains(other);
776
+ }
777
+
778
+ toString (opt = {}) {
779
+ const mapsto = opt.html ? ' &mapsto; ' : '->';
780
+ return this.arg.toString(opt) + mapsto + this.impl.toString(opt);
781
+ }
782
+
783
+ needsParens () {
784
+ return true;
785
+ }
786
+ }
787
+
788
+ class Church extends Native {
789
+ constructor (n) {
790
+ const p = Number.parseInt(n);
791
+ if (!(p >= 0))
792
+ throw new Error('Church number must be a non-negative integer');
793
+ const name = '' + p;
794
+ const impl = x => y => {
795
+ let expr = y;
796
+ for (let i = p; i-- > 0; )
797
+ expr = x.apply(expr);
798
+ return expr;
799
+ };
800
+
801
+ super(name, impl, { arity: 2, canonize: false, note: name });
802
+
803
+ this.n = p;
804
+ this.arity = 2;
805
+ }
806
+
807
+ equals (other) {
808
+ if (other instanceof Church)
809
+ return this.n === other.n;
810
+ return false;
811
+ }
812
+ }
813
+
814
+ class Alias extends Named {
815
+ /**
816
+ * @desc An existing expression under a different name.
817
+ * @param {String} name
818
+ * @param {Expr} impl
819
+ * @param {{canonize: boolean?, max: number?, maxArgs: number?, note: string?, terminal: boolean?}} [options]
820
+ */
821
+ constructor (name, impl, options = {}) {
822
+ super(name);
823
+ this.impl = impl;
824
+
825
+ if (options.note)
826
+ this.note = options.note;
827
+
828
+ const guess = options.canonize
829
+ ? impl.canonize({ max: options.max, maxArgs: options.maxArgs })
830
+ : { found: false };
831
+ this.arity = (guess.found && guess.proper && guess.arity) || 0;
832
+ this.proper = guess.proper ?? false;
833
+ this.terminal = options.terminal ?? this.proper;
834
+ this.canonical = guess.canonical;
835
+ }
836
+
837
+ getSymbols () {
838
+ return this.terminal ? new Map([[this, 1]]) : this.impl.getSymbols();
839
+ }
840
+
841
+ weight () {
842
+ return this.terminal ? 1 : this.impl.weight();
843
+ }
844
+
845
+ expand () {
846
+ return this.impl.expand();
847
+ }
848
+
849
+ subst (plug, value) {
850
+ return this.impl.subst(plug, value);
851
+ }
852
+
853
+ /**
854
+ *
855
+ * @return {{expr: Expr, steps: number}}
856
+ */
857
+ step () {
858
+ // arity known = waiting for args to expand
859
+ if (this.arity > 0)
860
+ return { expr: this, steps: 0, changed: false };
861
+ // expanding is a change but it takes 0 steps
862
+ return { expr: this.impl, steps: 0, changed: true };
863
+ }
864
+
865
+ reduce (args) {
866
+ if (args.length < this.arity)
867
+ return null;
868
+ return this.impl.apply(...args);
869
+ }
870
+
871
+ wantsArgs () {
872
+ return this.impl.wantsArgs();
873
+ }
874
+
875
+ equals (other) {
876
+ return other.equals(this.impl);
877
+ }
878
+
879
+ contains (other) {
880
+ return this.impl.contains(other);
881
+ }
882
+
883
+ _rski (options) {
884
+ return this.impl._rski(options);
885
+ }
886
+
887
+ toString (opt) {
888
+ return this.outdated ? this.impl.toString(opt) : super.toString(opt);
889
+ }
890
+
891
+ needsParens () {
892
+ return this.outdated ? this.impl.needsParens() : false;
893
+ }
894
+ }
895
+
896
+ // declare native combinators
897
+ addNative('I', x => x);
898
+ addNative('K', x => _ => x);
899
+ addNative('S', x => y => z => x.apply(z, y.apply(z)));
900
+ addNative('B', x => y => z => x.apply(y.apply(z)));
901
+ addNative('C', x => y => z => x.apply(z).apply(y));
902
+ addNative('W', x => y => x.apply(y).apply(y));
903
+
904
+ addNative('+', x => y => z => y.apply(x.apply(y, z)), {
905
+ note: '<var>n</var> &mapsto; <var>n</var> + 1 <i>or</i> SB',
906
+ apply: arg => arg instanceof Church ? new Church(arg.n + 1) : null
907
+ });
908
+
909
+ function maybeLambda (args, expr) {
910
+ if (args.length === 0)
911
+ return expr;
912
+ return new Lambda(args, expr);
913
+ }
914
+
915
+ function naiveCanonize (expr) {
916
+ if (expr instanceof App)
917
+ return naiveCanonize(expr.fun).apply(...expr.args.map(naiveCanonize));
918
+
919
+ if (expr instanceof Lambda)
920
+ return new Lambda(expr.arg, naiveCanonize(expr.impl));
921
+
922
+ if (expr instanceof Alias)
923
+ return naiveCanonize(expr.impl);
924
+
925
+ const canon = expr.canonize();
926
+ if (canon.canonical)
927
+ return canon.canonical;
928
+
929
+ throw new Error('Failed to canonize expression: ' + expr);
930
+ }
931
+
932
+ /**
933
+ *
934
+ * @param {Expr} expr
935
+ * @param {{max: number?, maxArgs: number?}} options
936
+ * @param {number} maxWeight
937
+ * @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
938
+ */
939
+ function * simplifyLambda (expr, options = {}, maxWeight = Infinity) {
940
+ // expr is a lambda, free variable, or an application thereof
941
+ // we want to find an equivalent lambda term with less weight
942
+ // which we do sequentially from leaves to the root of the AST
943
+
944
+ // short-circuit
945
+ if (expr.freeOnly()) {
946
+ if (expr.weight() < maxWeight)
947
+ yield { expr, steps: 0, comment: 'only free vars' };
948
+ return;
949
+ }
950
+
951
+ let steps = 0;
952
+ let savedSteps = 0;
953
+
954
+ // fun * arg Descartes product
955
+ if (expr instanceof App) {
956
+ // try to split into fun+arg, then try canonization but exposing each step
957
+ const [fun, arg] = expr.split();
958
+
959
+ for (const term of simplifyLambda(fun, options, maxWeight - 1)) {
960
+ const candidate = term.expr.apply(arg);
961
+ steps = savedSteps + term.steps;
962
+ if (candidate.weight() < maxWeight) {
963
+ maxWeight = candidate.weight();
964
+ yield { expr: candidate, steps: term.steps, comment: term.comment + '(app)' };
965
+ }
966
+ }
967
+ savedSteps = steps;
968
+
969
+ for (const term of simplifyLambda(arg, options, maxWeight - 1)) {
970
+ const candidate = fun.apply(term.expr);
971
+ steps = savedSteps + term.steps;
972
+ if (candidate.weight() < maxWeight) {
973
+ maxWeight = candidate.weight();
974
+ yield { expr: candidate, steps: term.steps, comment: term.comment + '(app)' };
975
+ }
976
+ }
977
+ savedSteps = steps;
978
+ }
979
+
980
+ if (expr instanceof Lambda) {
981
+ for (const term of simplifyLambda(expr.impl, options, maxWeight - 1)) {
982
+ const candidate = new Lambda(expr.arg, term.expr);
983
+ if (candidate.weight() < maxWeight) {
984
+ maxWeight = candidate.weight();
985
+ steps = savedSteps + term.steps;
986
+ yield { expr: candidate, steps: term.steps, comment: term.comment + '(lambda)' };
987
+ }
988
+ }
989
+ savedSteps = steps;
990
+ }
991
+
992
+ const canon = expr.canonize({ max: options.max, maxArgs: options.maxArgs });
993
+ if (canon.canonical && canon.canonical.weight() < maxWeight) {
994
+ maxWeight = canon.canonical.weight();
995
+ yield { expr: canon.canonical, steps: savedSteps + canon.steps, comment: 'canonical' };
996
+ }
997
+ }
998
+
999
+ // A global value meaning "lambda is used somewhere in this expression"
1000
+ // Can't be used (at least for now) to construct lambda expressions, or anything at all.
1001
+ // See also getSymbols().
1002
+ Expr.lambdaPlaceholder = new Native('->', x => x, {
1003
+ arity: 1,
1004
+ canonize: false,
1005
+ note: 'Lambda placeholder',
1006
+ apply: x => { throw new Error('Attempt to use a placeholder in expression') }
1007
+ });
1008
+
1009
+ module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church, globalOptions, native };