@dallaylaen/ski-interpreter 1.2.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 +13 -0
- package/README.md +20 -18
- package/lib/expr.js +121 -100
- package/lib/parser.js +36 -16
- package/lib/quest.js +43 -13
- package/package.json +3 -2
- package/types/lib/expr.d.ts +79 -38
- package/types/lib/parser.d.ts +15 -5
- package/types/lib/quest.d.ts +36 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-01-25
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
- Remove `Expr.reduce()` method for good (too ambiguous). See also `Expr.invoke()` below.
|
|
13
|
+
- Remove `onApply` hook from `Native` combinators.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Expr: Add `invoke(arg: Expr)` method implementing actual rewriting rules.
|
|
18
|
+
- SKI: `add(term, impl, note?)` method now accepts a function as `impl` to define native combinators directly.
|
|
19
|
+
- Improved jsdoc somewhat.
|
|
20
|
+
|
|
8
21
|
## [1.2.0] - 2025-12-14
|
|
9
22
|
|
|
10
23
|
### BREAKING CHANGES
|
package/README.md
CHANGED
|
@@ -46,6 +46,25 @@ that has enough arguments is executed and the step ends there.
|
|
|
46
46
|
Lambda terms are lazy, i.e. the body is not touched until
|
|
47
47
|
all free variables are bound.
|
|
48
48
|
|
|
49
|
+
# Playground
|
|
50
|
+
|
|
51
|
+
https://dallaylaen.github.io/ski-interpreter/
|
|
52
|
+
|
|
53
|
+
* all of the above features (except comparison and JS-native terms) in your browser
|
|
54
|
+
* expressions have permalinks
|
|
55
|
+
* can configure verbosity & executeion speed
|
|
56
|
+
|
|
57
|
+
# Quests
|
|
58
|
+
|
|
59
|
+
https://dallaylaen.github.io/ski-interpreter/quest.html
|
|
60
|
+
|
|
61
|
+
This page contains small tasks of increasing complexity.
|
|
62
|
+
Each task requires the user to build a combinator with specific properties.
|
|
63
|
+
|
|
64
|
+
# CLI
|
|
65
|
+
|
|
66
|
+
REPL comes with the package as [bin/ski.js](bin/ski.js).
|
|
67
|
+
|
|
49
68
|
# Installation
|
|
50
69
|
|
|
51
70
|
```bash
|
|
@@ -100,24 +119,6 @@ const [x, y] = SKI.free('x', 'y'); // free variables
|
|
|
100
119
|
SKI.church(5).apply(x, y).run().expr + ''; // 'x(x(x(x(x y))))'
|
|
101
120
|
```
|
|
102
121
|
|
|
103
|
-
# Playground
|
|
104
|
-
|
|
105
|
-
https://dallaylaen.github.io/ski-interpreter/
|
|
106
|
-
|
|
107
|
-
* all of the above features (except comparison and JS-native terms) in your browser
|
|
108
|
-
* expressions have permalinks
|
|
109
|
-
* can configure verbosity & executeion speed
|
|
110
|
-
|
|
111
|
-
# Quests
|
|
112
|
-
|
|
113
|
-
https://dallaylaen.github.io/ski-interpreter/quest.html
|
|
114
|
-
|
|
115
|
-
This page contains small tasks of increasing complexity.
|
|
116
|
-
Each task requires the user to build a combinator with specific properties.
|
|
117
|
-
|
|
118
|
-
# CLI
|
|
119
|
-
|
|
120
|
-
REPL comes with the package as [bin/ski.js](bin/ski.js).
|
|
121
122
|
|
|
122
123
|
|
|
123
124
|
|
|
@@ -131,6 +132,7 @@ REPL comes with the package as [bin/ski.js](bin/ski.js).
|
|
|
131
132
|
* "To Mock The Mockingbird" by Raymond Smulian.
|
|
132
133
|
* [combinator birds](https://www.angelfire.com/tx4/cus/combinator/birds.html) by [Chris Rathman](https://www.angelfire.com/tx4/cus/index.html)
|
|
133
134
|
* [Fun with combinators](https://doisinkidney.com/posts/2020-10-17-ski.html) by [@oisdk](https://github.com/oisdk)
|
|
135
|
+
* [Conbinatris](https://dirk.rave.org/combinatris/) by Dirk van Deun
|
|
134
136
|
|
|
135
137
|
# License and copyright
|
|
136
138
|
|
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.
|
|
@@ -242,16 +246,6 @@ class Expr {
|
|
|
242
246
|
return this;
|
|
243
247
|
}
|
|
244
248
|
|
|
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
249
|
/**
|
|
256
250
|
* Replace all instances of plug in the expression with value and return the resulting expression,
|
|
257
251
|
* or null if no changes could be made.
|
|
@@ -268,7 +262,30 @@ class Expr {
|
|
|
268
262
|
}
|
|
269
263
|
|
|
270
264
|
/**
|
|
271
|
-
|
|
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) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @desc iterate one step of a calculation.
|
|
272
289
|
* @return {{expr: Expr, steps: number, changed: boolean}}
|
|
273
290
|
*/
|
|
274
291
|
step () { return { expr: this, steps: 0, changed: false } }
|
|
@@ -432,7 +449,7 @@ class Expr {
|
|
|
432
449
|
var: ['<var>', '</var>'],
|
|
433
450
|
lambda: ['', '->', ''],
|
|
434
451
|
around: ['', ''],
|
|
435
|
-
redex: ['
|
|
452
|
+
redex: ['', ''],
|
|
436
453
|
}
|
|
437
454
|
: {
|
|
438
455
|
brackets: ['(', ')'],
|
|
@@ -544,12 +561,6 @@ class App extends Expr {
|
|
|
544
561
|
return this.fun._firstVar();
|
|
545
562
|
}
|
|
546
563
|
|
|
547
|
-
apply (...args) {
|
|
548
|
-
if (args.length === 0)
|
|
549
|
-
return this;
|
|
550
|
-
return new App(this, ...args);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
564
|
expand () {
|
|
554
565
|
return this.fun.expand().apply(this.arg.expand());
|
|
555
566
|
}
|
|
@@ -576,34 +587,48 @@ class App extends Expr {
|
|
|
576
587
|
step () {
|
|
577
588
|
// normal reduction order: first try root, then at most 1 step
|
|
578
589
|
if (!this.final) {
|
|
579
|
-
if
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
//
|
|
587
|
-
|
|
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
|
|
588
598
|
const fun = this.fun.step();
|
|
589
599
|
if (fun.changed)
|
|
590
600
|
return { expr: fun.expr.apply(this.arg), steps: fun.steps, changed: true };
|
|
591
601
|
|
|
602
|
+
// descend into arg
|
|
592
603
|
const arg = this.arg.step();
|
|
593
604
|
if (arg.changed)
|
|
594
605
|
return { expr: this.fun.apply(arg.expr), steps: arg.steps, changed: true };
|
|
595
606
|
|
|
596
|
-
|
|
607
|
+
// mark as irreducible
|
|
608
|
+
this.final = true; // mark as irreducible at root
|
|
597
609
|
}
|
|
610
|
+
|
|
598
611
|
return { expr: this, steps: 0, changed: false };
|
|
599
612
|
}
|
|
600
613
|
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
}
|
|
603
628
|
}
|
|
604
629
|
|
|
605
630
|
split () {
|
|
606
|
-
//
|
|
631
|
+
// leftover from array-based older design
|
|
607
632
|
return [this.fun, this.arg];
|
|
608
633
|
}
|
|
609
634
|
|
|
@@ -707,51 +732,36 @@ class FreeVar extends Named {
|
|
|
707
732
|
}
|
|
708
733
|
}
|
|
709
734
|
|
|
710
|
-
/**
|
|
711
|
-
* @typedef {function(Expr): Expr | AnyArity} AnyArity
|
|
712
|
-
*/
|
|
713
|
-
|
|
714
735
|
class Native extends Named {
|
|
715
736
|
/**
|
|
716
|
-
* @desc A
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
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
|
+
*
|
|
721
748
|
* @param {String} name
|
|
722
|
-
* @param {
|
|
723
|
-
* @param {{note
|
|
749
|
+
* @param {Partial} impl
|
|
750
|
+
* @param {{note?: string, arity?: number, canonize?: boolean, apply?: function(Expr):(Expr|null) }} [opt]
|
|
724
751
|
*/
|
|
725
752
|
constructor (name, impl, opt = {}) {
|
|
726
753
|
super(name);
|
|
727
754
|
// setup essentials
|
|
728
|
-
this.
|
|
729
|
-
if (opt.apply)
|
|
730
|
-
this.onApply = opt.apply;
|
|
731
|
-
this.arity = opt.arity ?? 1;
|
|
755
|
+
this.invoke = impl;
|
|
732
756
|
|
|
757
|
+
// TODO guess lazily (on demand, only once); app capabilities such as discard and duplicate
|
|
733
758
|
// try to bootstrap and guess some of our properties
|
|
734
759
|
const guess = (opt.canonize ?? true) ? this.guess() : { normal: false };
|
|
735
760
|
|
|
736
|
-
|
|
737
|
-
this.arity = guess.arity || 1;
|
|
738
|
-
|
|
761
|
+
this.arity = opt.arity || guess.arity || 1;
|
|
739
762
|
this.note = opt.note ?? guess.expr?.format({ terse: true, html: true, lambda: ['', ' ↦ ', ''] });
|
|
740
763
|
}
|
|
741
764
|
|
|
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
765
|
_rski (options) {
|
|
756
766
|
if (this === native.I || this === native.K || this === native.S || (options.steps >= options.max))
|
|
757
767
|
return this;
|
|
@@ -761,21 +771,6 @@ class Native extends Named {
|
|
|
761
771
|
options.steps++;
|
|
762
772
|
return canon._rski(options);
|
|
763
773
|
}
|
|
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
774
|
}
|
|
780
775
|
|
|
781
776
|
const native = {};
|
|
@@ -785,9 +780,20 @@ function addNative (name, impl, opt) {
|
|
|
785
780
|
|
|
786
781
|
class Lambda extends Expr {
|
|
787
782
|
/**
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
+
*/
|
|
791
797
|
constructor (arg, impl) {
|
|
792
798
|
if (Array.isArray(arg)) {
|
|
793
799
|
// check args before everything
|
|
@@ -834,16 +840,11 @@ class Lambda extends Expr {
|
|
|
834
840
|
return { normal: false, steps };
|
|
835
841
|
|
|
836
842
|
const push = nthvar(preArgs.length + options.index);
|
|
837
|
-
return this.
|
|
843
|
+
return this.invoke(push)._guess(options, [...preArgs, push], steps + 1);
|
|
838
844
|
}
|
|
839
845
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
return null;
|
|
843
|
-
|
|
844
|
-
const [head, ...tail] = input;
|
|
845
|
-
|
|
846
|
-
return (this.impl.subst(this.arg, head) ?? this.impl).apply(...tail);
|
|
846
|
+
invoke (arg) {
|
|
847
|
+
return this.impl.subst(this.arg, arg) ?? this.impl;
|
|
847
848
|
}
|
|
848
849
|
|
|
849
850
|
subst (search, replace) {
|
|
@@ -894,7 +895,7 @@ class Lambda extends Expr {
|
|
|
894
895
|
|
|
895
896
|
const t = new FreeVar('t');
|
|
896
897
|
|
|
897
|
-
return other.
|
|
898
|
+
return other.invoke(t).equals(this.invoke(t));
|
|
898
899
|
}
|
|
899
900
|
|
|
900
901
|
contains (other) {
|
|
@@ -920,6 +921,11 @@ class Lambda extends Expr {
|
|
|
920
921
|
}
|
|
921
922
|
|
|
922
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
|
+
*/
|
|
923
929
|
constructor (n) {
|
|
924
930
|
const p = Number.parseInt(n);
|
|
925
931
|
if (!(p >= 0))
|
|
@@ -949,9 +955,23 @@ class Church extends Native {
|
|
|
949
955
|
}
|
|
950
956
|
}
|
|
951
957
|
|
|
958
|
+
function waitn (expr, n) {
|
|
959
|
+
return arg => n <= 1 ? expr.apply(arg) : waitn(expr.apply(arg), n - 1);
|
|
960
|
+
}
|
|
961
|
+
|
|
952
962
|
class Alias extends Named {
|
|
953
963
|
/**
|
|
954
|
-
* @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
|
+
*
|
|
955
975
|
* @param {String} name
|
|
956
976
|
* @param {Expr} impl
|
|
957
977
|
* @param {{canonize: boolean?, max: number?, maxArgs: number?, note: string?, terminal: boolean?}} [options]
|
|
@@ -970,6 +990,7 @@ class Alias extends Named {
|
|
|
970
990
|
this.proper = guess.proper ?? false;
|
|
971
991
|
this.terminal = options.terminal ?? this.proper;
|
|
972
992
|
this.canonical = guess.expr;
|
|
993
|
+
this.invoke = waitn(impl, this.arity);
|
|
973
994
|
}
|
|
974
995
|
|
|
975
996
|
getSymbols () {
|
|
@@ -1006,12 +1027,6 @@ class Alias extends Named {
|
|
|
1006
1027
|
return { expr: this.impl, steps: 0, changed: true };
|
|
1007
1028
|
}
|
|
1008
1029
|
|
|
1009
|
-
reduce (args) {
|
|
1010
|
-
if (args.length < this.arity)
|
|
1011
|
-
return null;
|
|
1012
|
-
return this.impl.apply(...args);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
1030
|
_firstVar () {
|
|
1016
1031
|
return this.impl._firstVar();
|
|
1017
1032
|
}
|
|
@@ -1040,6 +1055,7 @@ class Alias extends Named {
|
|
|
1040
1055
|
}
|
|
1041
1056
|
|
|
1042
1057
|
_declare (output, inventory, seen) {
|
|
1058
|
+
// this is part of the 'declare' function, see below
|
|
1043
1059
|
// only once
|
|
1044
1060
|
if (seen.has(this))
|
|
1045
1061
|
return;
|
|
@@ -1064,10 +1080,15 @@ addNative('B', x => y => z => x.apply(y.apply(z)));
|
|
|
1064
1080
|
addNative('C', x => y => z => x.apply(z).apply(y));
|
|
1065
1081
|
addNative('W', x => y => x.apply(y).apply(y));
|
|
1066
1082
|
|
|
1067
|
-
addNative(
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
+
);
|
|
1071
1092
|
|
|
1072
1093
|
// A global value meaning "lambda is used somewhere in this expression"
|
|
1073
1094
|
// Can't be used (at least for now) to construct lambda expressions, or anything at all.
|
package/lib/parser.js
CHANGED
|
@@ -89,29 +89,32 @@ class SKI {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
+
* @desc Declare a new term
|
|
93
|
+
* If the first argument is an Alias, it is added as is.
|
|
94
|
+
* Otherwise, a new Alias or Native term (depending on impl type) is created.
|
|
95
|
+
* If note is not provided and this.annotate is true, an automatic note is generated.
|
|
96
|
+
*
|
|
97
|
+
* If impl is a function, it should have signature (Expr) => ... => Expr
|
|
98
|
+
* (see typedef Partial at top of expr.js)
|
|
99
|
+
*
|
|
100
|
+
* @example ski.add('T', 'S(K(SI))K', 'swap combinator')
|
|
101
|
+
* @example ski.add( ski.parse('T = S(K(SI))K') ) // ditto but one-arg form
|
|
102
|
+
* @example ski.add('T', x => y => y.apply(x), 'swap combinator') // heavy artillery
|
|
103
|
+
* @example ski.add('Y', function (f) { return f.apply(this.apply(f)); }, 'Y combinator')
|
|
92
104
|
*
|
|
93
105
|
* @param {Alias|String} term
|
|
94
|
-
* @param {Expr|
|
|
106
|
+
* @param {String|Expr|function(Expr):Partial} [impl]
|
|
95
107
|
* @param {String} [note]
|
|
96
108
|
* @return {SKI} chainable
|
|
97
109
|
*/
|
|
98
110
|
add (term, impl, note ) {
|
|
99
|
-
|
|
100
|
-
if (typeof impl === 'string')
|
|
101
|
-
term = new Alias(term, this.parse(impl), { canonize: true });
|
|
102
|
-
else if (impl instanceof Expr)
|
|
103
|
-
term = new Alias(term, impl, { canonize: true });
|
|
104
|
-
else
|
|
105
|
-
throw new Error('add: term must be an Alias or a string and impl must be an Expr or a string');
|
|
106
|
-
} else if (term instanceof Alias)
|
|
107
|
-
term = new Alias(term.name, term.impl, { canonize: true });
|
|
108
|
-
|
|
109
|
-
// This should normally be unreachable but let's keep just in case
|
|
110
|
-
if (!(term instanceof Alias))
|
|
111
|
-
throw new Error('add: term must be an Alias or a string (accompanied with an implementation)');
|
|
111
|
+
term = this._named(term, impl);
|
|
112
112
|
|
|
113
|
-
if (this.annotate && note === undefined
|
|
114
|
-
|
|
113
|
+
if (this.annotate && note === undefined) {
|
|
114
|
+
const guess = term.guess();
|
|
115
|
+
if (guess.expr)
|
|
116
|
+
note = guess.expr.format({ terse: true, html: true, lambda: ['', ' ↦ ', ''] });
|
|
117
|
+
}
|
|
115
118
|
if (note !== undefined)
|
|
116
119
|
term.note = note;
|
|
117
120
|
|
|
@@ -123,6 +126,23 @@ class SKI {
|
|
|
123
126
|
return this;
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
_named (term, impl) {
|
|
130
|
+
if (term instanceof Alias)
|
|
131
|
+
return new Alias(term.name, term.impl, { canonize: true });
|
|
132
|
+
if (typeof term !== 'string')
|
|
133
|
+
throw new Error('add(): term must be an Alias or a string');
|
|
134
|
+
if (impl === undefined)
|
|
135
|
+
throw new Error('add(): impl must be provided when term is a string');
|
|
136
|
+
if (typeof impl === 'string')
|
|
137
|
+
return new Alias(term, this.parse(impl), { canonize: true });
|
|
138
|
+
if (impl instanceof Expr)
|
|
139
|
+
return new Alias(term, impl, { canonize: true });
|
|
140
|
+
if (typeof impl === 'function')
|
|
141
|
+
return new Native(term, impl);
|
|
142
|
+
// idk what this is
|
|
143
|
+
throw new Error('add(): impl must be an Expr, a string, or a function with a signature Expr => ... => Expr');
|
|
144
|
+
}
|
|
145
|
+
|
|
126
146
|
maybeAdd (name, impl) {
|
|
127
147
|
if (this.known[name])
|
|
128
148
|
this.allow.add(name);
|
package/lib/quest.js
CHANGED
|
@@ -225,16 +225,25 @@ class Quest {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
class Case {
|
|
228
|
+
/**
|
|
229
|
+
* @param {FreeVar[]} input
|
|
230
|
+
* @param {{
|
|
231
|
+
* max?: number,
|
|
232
|
+
* note?: string,
|
|
233
|
+
* vars?: {string: Expr},
|
|
234
|
+
* engine: SKI
|
|
235
|
+
* }} options
|
|
236
|
+
*/
|
|
228
237
|
constructor (input, options) {
|
|
229
238
|
this.max = options.max ?? 1000;
|
|
230
239
|
this.note = options.note;
|
|
231
|
-
this.vars = { ...(options.vars ?? {}) }; //
|
|
240
|
+
this.vars = { ...(options.vars ?? {}) }; // note: context already contains input placeholders
|
|
232
241
|
this.input = input;
|
|
233
242
|
this.engine = options.engine;
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
parse (src) {
|
|
237
|
-
return new
|
|
246
|
+
return new Subst(this.engine.parse(src, this.vars), this.input);
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
/**
|
|
@@ -263,17 +272,15 @@ class ExprCase extends Case {
|
|
|
263
272
|
|
|
264
273
|
super(input, options);
|
|
265
274
|
|
|
266
|
-
[this.e1, this.e2] = terms.map(
|
|
275
|
+
[this.e1, this.e2] = terms.map( s => this.parse(s) );
|
|
267
276
|
}
|
|
268
277
|
|
|
269
|
-
check (...
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
278
|
+
check (...args) {
|
|
279
|
+
const e1 = this.e1.apply(args);
|
|
280
|
+
const r1 = e1.run({ max: this.max });
|
|
281
|
+
const e2 = this.e2.apply(args);
|
|
282
|
+
const r2 = e2.run({ max: this.max });
|
|
273
283
|
|
|
274
|
-
const start = subst(this.e1, expr);
|
|
275
|
-
const r1 = start.run({ max: this.max });
|
|
276
|
-
const r2 = subst(this.e2, expr).run({ max: this.max });
|
|
277
284
|
let reason = null;
|
|
278
285
|
if (!r1.final || !r2.final)
|
|
279
286
|
reason = 'failed to reach normal form in ' + this.max + ' steps';
|
|
@@ -285,11 +292,11 @@ class ExprCase extends Case {
|
|
|
285
292
|
pass: !reason,
|
|
286
293
|
reason,
|
|
287
294
|
steps: r1.steps,
|
|
288
|
-
start,
|
|
295
|
+
start: e1,
|
|
289
296
|
found: r1.expr,
|
|
290
297
|
expected: r2.expr,
|
|
291
298
|
note: this.note,
|
|
292
|
-
args
|
|
299
|
+
args,
|
|
293
300
|
case: this,
|
|
294
301
|
};
|
|
295
302
|
}
|
|
@@ -335,7 +342,7 @@ class PropertyCase extends Case {
|
|
|
335
342
|
}
|
|
336
343
|
|
|
337
344
|
check (...expr) {
|
|
338
|
-
const start = this.expr.apply(
|
|
345
|
+
const start = this.expr.apply(expr);
|
|
339
346
|
const r = start.run({ max: this.max });
|
|
340
347
|
const guess = r.expr.guess({ max: this.max });
|
|
341
348
|
|
|
@@ -358,6 +365,29 @@ class PropertyCase extends Case {
|
|
|
358
365
|
}
|
|
359
366
|
}
|
|
360
367
|
|
|
368
|
+
class Subst {
|
|
369
|
+
/**
|
|
370
|
+
* @descr A placeholder object with exactly n free variables to be substituted later.
|
|
371
|
+
* @param {Expr} expr
|
|
372
|
+
* @param {FreeVar[]} vars
|
|
373
|
+
*/
|
|
374
|
+
constructor (expr, vars) {
|
|
375
|
+
this.expr = expr;
|
|
376
|
+
this.vars = vars;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
apply (list) {
|
|
380
|
+
if (list.length !== this.vars.length)
|
|
381
|
+
throw new Error('Subst: expected ' + this.vars.length + ' terms, got ' + list.length);
|
|
382
|
+
|
|
383
|
+
let expr = this.expr;
|
|
384
|
+
for (let i = 0; i < this.vars.length; i++)
|
|
385
|
+
expr = expr.subst(this.vars[i], list[i]) ?? expr;
|
|
386
|
+
|
|
387
|
+
return expr;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
361
391
|
function list2str (str) {
|
|
362
392
|
if (str === undefined)
|
|
363
393
|
return str;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dallaylaen/ski-interpreter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Simple Kombinator Interpreter - a combinatory logic & lambda calculus parser and interpreter. Supports SKI, BCKW, Church numerals, and setting up assertions ('quests') involving all of the above.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"combinatory logic",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types",
|
|
20
20
|
"test": "npx nyc mocha",
|
|
21
21
|
"minify": "npx esbuild --bundle ./index.js --outfile=docs/build/js/ski-interpreter.min.js --minify --sourcemap",
|
|
22
|
-
"build": "
|
|
22
|
+
"build-site": "npx esbuild --bundle ./site-src/index.js --outfile=docs/build/js/util.min.js --minify --sourcemap",
|
|
23
|
+
"build": "npm run lint && npm run types && npm run test && npm run minify && npm run build-site"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"package.json",
|
package/types/lib/expr.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type Partial = Expr | ((arg0: Expr) => Partial);
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {Expr | function(Expr): Partial} Partial
|
|
4
|
+
*/
|
|
2
5
|
export class Expr {
|
|
3
6
|
/**
|
|
4
7
|
* postprocess term after parsing. typically return self but may return other term or die
|
|
@@ -131,13 +134,6 @@ export class Expr {
|
|
|
131
134
|
steps: number;
|
|
132
135
|
}>;
|
|
133
136
|
_rski(options: any): this;
|
|
134
|
-
/**
|
|
135
|
-
* Apply self to list of given args.
|
|
136
|
-
* Normally, only native combinators know how to do it.
|
|
137
|
-
* @param {Expr[]} args
|
|
138
|
-
* @return {Expr|null}
|
|
139
|
-
*/
|
|
140
|
-
reduce(args: Expr[]): Expr | null;
|
|
141
137
|
/**
|
|
142
138
|
* Replace all instances of plug in the expression with value and return the resulting expression,
|
|
143
139
|
* or null if no changes could be made.
|
|
@@ -151,7 +147,27 @@ export class Expr {
|
|
|
151
147
|
*/
|
|
152
148
|
subst(search: Expr, replace: Expr): Expr | null;
|
|
153
149
|
/**
|
|
154
|
-
|
|
150
|
+
* @desc Apply term reduction rules, if any, to the given argument.
|
|
151
|
+
* A returned value of null means no reduction is possible.
|
|
152
|
+
* A returned value of Expr means the reduction is complete and the application
|
|
153
|
+
* of this and arg can be replaced with the result.
|
|
154
|
+
* A returned value of a function means that further arguments are needed,
|
|
155
|
+
* and can be cached for when they arrive.
|
|
156
|
+
*
|
|
157
|
+
* This method is between apply() which merely glues terms together,
|
|
158
|
+
* and step() which reduces the whole expression.
|
|
159
|
+
*
|
|
160
|
+
* foo.invoke(bar) is what happens inside foo.apply(bar).step() before
|
|
161
|
+
* reduction of either foo or bar is attempted.
|
|
162
|
+
*
|
|
163
|
+
* The name 'invoke' was chosen to avoid confusion with either 'apply' or 'reduce'.
|
|
164
|
+
*
|
|
165
|
+
* @param {Expr} arg
|
|
166
|
+
* @returns {Partial | null}
|
|
167
|
+
*/
|
|
168
|
+
invoke(arg: Expr): Partial | null;
|
|
169
|
+
/**
|
|
170
|
+
* @desc iterate one step of a calculation.
|
|
155
171
|
* @return {{expr: Expr, steps: number, changed: boolean}}
|
|
156
172
|
*/
|
|
157
173
|
step(): {
|
|
@@ -279,7 +295,6 @@ export class App extends Expr {
|
|
|
279
295
|
arity: any;
|
|
280
296
|
weight(): any;
|
|
281
297
|
_firstVar(): any;
|
|
282
|
-
apply(...args: any[]): App;
|
|
283
298
|
expand(): any;
|
|
284
299
|
subst(search: any, replace: any): any;
|
|
285
300
|
/**
|
|
@@ -289,7 +304,7 @@ export class App extends Expr {
|
|
|
289
304
|
expr: Expr;
|
|
290
305
|
steps: number;
|
|
291
306
|
};
|
|
292
|
-
|
|
307
|
+
invoke(arg: any): any;
|
|
293
308
|
split(): any[];
|
|
294
309
|
_aslist(): any[];
|
|
295
310
|
equals(other: any): any;
|
|
@@ -304,14 +319,25 @@ export class FreeVar extends Named {
|
|
|
304
319
|
}
|
|
305
320
|
export class Lambda extends Expr {
|
|
306
321
|
/**
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
322
|
+
* @desc Lambda abstraction of arg over impl.
|
|
323
|
+
* Upon evaluation, all occurrences of 'arg' within 'impl' will be replaced
|
|
324
|
+
* with the provided argument.
|
|
325
|
+
*
|
|
326
|
+
* Note that 'arg' will be replaced by a localized placeholder, so the original
|
|
327
|
+
* variable can be used elsewhere without interference.
|
|
328
|
+
* Listing symbols contained in the lambda will omit such placeholder.
|
|
329
|
+
*
|
|
330
|
+
* Legacy ([FreeVar], impl) constructor is supported but deprecated.
|
|
331
|
+
* It will create a nested lambda expression.
|
|
332
|
+
*
|
|
333
|
+
* @param {FreeVar} arg
|
|
334
|
+
* @param {Expr} impl
|
|
335
|
+
*/
|
|
336
|
+
constructor(arg: FreeVar, impl: Expr);
|
|
311
337
|
arg: FreeVar;
|
|
312
338
|
impl: Expr;
|
|
313
339
|
arity: number;
|
|
314
|
-
|
|
340
|
+
invoke(arg: any): Expr;
|
|
315
341
|
subst(search: any, replace: any): Lambda;
|
|
316
342
|
expand(): Lambda;
|
|
317
343
|
_rski(options: any): any;
|
|
@@ -319,37 +345,47 @@ export class Lambda extends Expr {
|
|
|
319
345
|
_format(options: any, nargs: any): string;
|
|
320
346
|
_braced(first: any): boolean;
|
|
321
347
|
}
|
|
322
|
-
/**
|
|
323
|
-
* @typedef {function(Expr): Expr | AnyArity} AnyArity
|
|
324
|
-
*/
|
|
325
348
|
export class Native extends Named {
|
|
326
349
|
/**
|
|
327
|
-
* @desc A
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
350
|
+
* @desc A named term with a known rewriting rule.
|
|
351
|
+
* 'impl' is a function with signature Expr => Expr => ... => Expr
|
|
352
|
+
* (see typedef Partial).
|
|
353
|
+
* This is how S, K, I, and company are implemented.
|
|
354
|
+
*
|
|
355
|
+
* Note that as of current something like a=>b=>b(a) is not possible,
|
|
356
|
+
* use full form instead: a=>b=>b.apply(a).
|
|
357
|
+
*
|
|
358
|
+
* @example new Native('K', x => y => x); // constant
|
|
359
|
+
* @example new Native('Y', function(f) { return f.apply(this.apply(f)); }); // self-application
|
|
360
|
+
*
|
|
332
361
|
* @param {String} name
|
|
333
|
-
* @param {
|
|
334
|
-
* @param {{note
|
|
362
|
+
* @param {Partial} impl
|
|
363
|
+
* @param {{note?: string, arity?: number, canonize?: boolean, apply?: function(Expr):(Expr|null) }} [opt]
|
|
335
364
|
*/
|
|
336
|
-
constructor(name: string, impl:
|
|
337
|
-
note
|
|
338
|
-
arity
|
|
339
|
-
canonize
|
|
340
|
-
apply
|
|
365
|
+
constructor(name: string, impl: Partial, opt?: {
|
|
366
|
+
note?: string;
|
|
367
|
+
arity?: number;
|
|
368
|
+
canonize?: boolean;
|
|
369
|
+
apply?: (arg0: Expr) => (Expr | null);
|
|
341
370
|
});
|
|
342
|
-
|
|
343
|
-
onApply: (arg0: Expr) => (Expr | null);
|
|
371
|
+
invoke: Partial;
|
|
344
372
|
arity: any;
|
|
345
373
|
note: any;
|
|
346
|
-
apply(...args: any[]): Expr;
|
|
347
374
|
_rski(options: any): any;
|
|
348
|
-
reduce(args: any): any;
|
|
349
375
|
}
|
|
350
376
|
export class Alias extends Named {
|
|
351
377
|
/**
|
|
352
|
-
* @desc
|
|
378
|
+
* @desc A named alias for an existing expression.
|
|
379
|
+
*
|
|
380
|
+
* Upon evaluation, the alias expands into the original expression,
|
|
381
|
+
* unless it has a known arity > 0 and is marked terminal,
|
|
382
|
+
* in which case it waits for enough arguments before expanding.
|
|
383
|
+
*
|
|
384
|
+
* A hidden mutable property 'outdated' is used to silently
|
|
385
|
+
* replace the alias with its definition in all contexts.
|
|
386
|
+
* This is used when declaring named terms in an interpreter,
|
|
387
|
+
* to avoid confusion between old and new terms with the same name.
|
|
388
|
+
*
|
|
353
389
|
* @param {String} name
|
|
354
390
|
* @param {Expr} impl
|
|
355
391
|
* @param {{canonize: boolean?, max: number?, maxArgs: number?, note: string?, terminal: boolean?}} [options]
|
|
@@ -367,6 +403,7 @@ export class Alias extends Named {
|
|
|
367
403
|
proper: any;
|
|
368
404
|
terminal: any;
|
|
369
405
|
canonical: any;
|
|
406
|
+
invoke: (arg: any) => any;
|
|
370
407
|
subst(search: any, replace: any): any;
|
|
371
408
|
/**
|
|
372
409
|
*
|
|
@@ -376,14 +413,18 @@ export class Alias extends Named {
|
|
|
376
413
|
expr: Expr;
|
|
377
414
|
steps: number;
|
|
378
415
|
};
|
|
379
|
-
reduce(args: any): Expr;
|
|
380
416
|
equals(other: any): any;
|
|
381
417
|
_rski(options: any): Expr;
|
|
382
418
|
_braced(first: any): boolean;
|
|
383
419
|
_format(options: any, nargs: any): string | void;
|
|
384
420
|
}
|
|
385
421
|
export class Church extends Native {
|
|
386
|
-
|
|
422
|
+
/**
|
|
423
|
+
* @desc Church numeral representing non-negative integer n:
|
|
424
|
+
* n f x = f(f(...(f x)...)) with f applied n times.
|
|
425
|
+
* @param {number} n
|
|
426
|
+
*/
|
|
427
|
+
constructor(n: number);
|
|
387
428
|
n: any;
|
|
388
429
|
arity: number;
|
|
389
430
|
equals(other: any): boolean;
|
package/types/lib/parser.d.ts
CHANGED
|
@@ -24,16 +24,26 @@ export class SKI {
|
|
|
24
24
|
hasLambdas: boolean;
|
|
25
25
|
allow: any;
|
|
26
26
|
/**
|
|
27
|
+
* @desc Declare a new term
|
|
28
|
+
* If the first argument is an Alias, it is added as is.
|
|
29
|
+
* Otherwise, a new Alias or Native term (depending on impl type) is created.
|
|
30
|
+
* If note is not provided and this.annotate is true, an automatic note is generated.
|
|
31
|
+
*
|
|
32
|
+
* If impl is a function, it should have signature (Expr) => ... => Expr
|
|
33
|
+
* (see typedef Partial at top of expr.js)
|
|
34
|
+
*
|
|
35
|
+
* @example ski.add('T', 'S(K(SI))K', 'swap combinator')
|
|
36
|
+
* @example ski.add( ski.parse('T = S(K(SI))K') ) // ditto but one-arg form
|
|
37
|
+
* @example ski.add('T', x => y => y.apply(x), 'swap combinator') // heavy artillery
|
|
38
|
+
* @example ski.add('Y', function (f) { return f.apply(this.apply(f)); }, 'Y combinator')
|
|
27
39
|
*
|
|
28
40
|
* @param {Alias|String} term
|
|
29
|
-
* @param {Expr|
|
|
41
|
+
* @param {String|Expr|function(Expr):Partial} [impl]
|
|
30
42
|
* @param {String} [note]
|
|
31
43
|
* @return {SKI} chainable
|
|
32
44
|
*/
|
|
33
|
-
add(term: Alias | string, impl?:
|
|
34
|
-
|
|
35
|
-
fast: boolean | null;
|
|
36
|
-
}], note?: string): SKI;
|
|
45
|
+
add(term: Alias | string, impl?: string | Expr | ((arg0: Expr) => Partial), note?: string): SKI;
|
|
46
|
+
_named(term: any, impl: any): Native | Alias;
|
|
37
47
|
maybeAdd(name: any, impl: any): this;
|
|
38
48
|
/**
|
|
39
49
|
* @desc Declare and remove multiple terms at once
|
package/types/lib/quest.d.ts
CHANGED
|
@@ -144,13 +144,31 @@ export class Quest {
|
|
|
144
144
|
show(): TestCase[];
|
|
145
145
|
}
|
|
146
146
|
declare class Case {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
/**
|
|
148
|
+
* @param {FreeVar[]} input
|
|
149
|
+
* @param {{
|
|
150
|
+
* max?: number,
|
|
151
|
+
* note?: string,
|
|
152
|
+
* vars?: {string: Expr},
|
|
153
|
+
* engine: SKI
|
|
154
|
+
* }} options
|
|
155
|
+
*/
|
|
156
|
+
constructor(input: typeof import("./expr").FreeVar[], options: {
|
|
157
|
+
max?: number;
|
|
158
|
+
note?: string;
|
|
159
|
+
vars?: {
|
|
160
|
+
string: typeof import("./expr").Expr;
|
|
161
|
+
};
|
|
162
|
+
engine: SKI;
|
|
163
|
+
});
|
|
164
|
+
max: number;
|
|
165
|
+
note: string;
|
|
166
|
+
vars: {
|
|
167
|
+
string?: typeof import("./expr").Expr;
|
|
168
|
+
};
|
|
169
|
+
input: typeof import("./expr").FreeVar[];
|
|
170
|
+
engine: SKI;
|
|
171
|
+
parse(src: any): Subst;
|
|
154
172
|
/**
|
|
155
173
|
* @param {Expr} expr
|
|
156
174
|
* @return {CaseResult}
|
|
@@ -158,4 +176,15 @@ declare class Case {
|
|
|
158
176
|
check(...expr: typeof import("./expr").Expr): CaseResult;
|
|
159
177
|
}
|
|
160
178
|
import { SKI } from "./parser";
|
|
179
|
+
declare class Subst {
|
|
180
|
+
/**
|
|
181
|
+
* @descr A placeholder object with exactly n free variables to be substituted later.
|
|
182
|
+
* @param {Expr} expr
|
|
183
|
+
* @param {FreeVar[]} vars
|
|
184
|
+
*/
|
|
185
|
+
constructor(expr: typeof import("./expr").Expr, vars: typeof import("./expr").FreeVar[]);
|
|
186
|
+
expr: typeof import("./expr").Expr;
|
|
187
|
+
vars: typeof import("./expr").FreeVar[];
|
|
188
|
+
apply(list: any): typeof import("./expr").Expr;
|
|
189
|
+
}
|
|
161
190
|
export {};
|