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