@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 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
- * @desc iterate one step of calculation in accordance with known rules.
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: ['', '-&gt;', ''],
434
451
  around: ['', ''],
435
- redex: ['<b>', '</b>'],
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 (this.arity === 0) {
580
- // aha! we have just fulfilled some previous function's argument demands
581
- const reduced = this.fun.reduce([this.arg]);
582
- // should always be true, but whatever
583
- if (reduced)
584
- return { expr: reduced, steps: 1, changed: true };
585
- }
586
- // now try recursing
587
-
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
- this.final = true;
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
- reduce (args) {
602
- return this.fun.reduce([this.arg, ...args]);
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
- // pretend we are an elegant (cons fun arg) and not a sleazy imperative array
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 term named 'name' that converts next 'arity' arguments into
717
- * an expression returned by 'impl' function
718
- * If an apply: Expr=>Expr|null function is given, it will be attempted upon application
719
- * before building an App object. This allows to plug in argument coercions,
720
- * e.g. instantly perform a numeric operation natively if the next term is a number.
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 {AnyArity} impl
723
- * @param {{note: string?, arity: number?, canonize: boolean?, apply: function(Expr):(Expr|null) }} [opt]
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.impl = impl;
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
- if (!opt.arity)
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: ['', ' &mapsto; ', ''] });
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
- * @param {FreeVar|FreeVar[]} arg
789
- * @param {Expr} impl
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.reduce([push])._guess(options, [...preArgs, push], steps + 1);
843
+ return this.invoke(push)._guess(options, [...preArgs, push], steps + 1);
838
844
  }
839
845
 
840
- reduce (input) {
841
- if (input.length === 0)
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.reduce([t]).equals(this.reduce([t]));
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 An existing expression under a different name.
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('+', x => y => z => y.apply(x.apply(y, z)), {
1068
- note: '<var>n</var> &mapsto; <var>n</var> + 1 <i>or</i> SB',
1069
- apply: arg => arg instanceof Church ? new Church(arg.n + 1) : null
1070
- });
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|String|[number, function(...Expr): Expr, {note: string?, fast: boolean?}]} [impl]
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
- if (typeof term === 'string') {
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 && term.canonical)
114
- note = term.canonical.format({ terse: true, html: true, lambda: ['', ' &mapsto; ', ''] });
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: ['', ' &mapsto; ', ''] });
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 ?? {}) }; // shallow copy to avoid modifying the original
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 Lambda(this.input, this.engine.parse(src, this.vars));
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(src => this.parse(src));
275
+ [this.e1, this.e2] = terms.map( s => this.parse(s) );
267
276
  }
268
277
 
269
- check (...expr) {
270
- // we do it the fancy way and instead of just "apply" to avoid
271
- // displaying (foo->foo this that)(user input) as 1st step
272
- const subst = (outer, inner) => outer.reduce(inner) ?? outer.apply(...inner);
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: expr,
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(...expr);
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.2.0",
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": "npm run lint && npm run types && npm run test && npm run minify"
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",
@@ -1,4 +1,7 @@
1
- export type AnyArity = (arg0: Expr) => Expr | AnyArity;
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
- * @desc iterate one step of calculation in accordance with known rules.
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
- reduce(args: any): any;
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
- * @param {FreeVar|FreeVar[]} arg
308
- * @param {Expr} impl
309
- */
310
- constructor(arg: FreeVar | FreeVar[], impl: Expr);
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
- reduce(input: any): Expr;
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 term named 'name' that converts next 'arity' arguments into
328
- * an expression returned by 'impl' function
329
- * If an apply: Expr=>Expr|null function is given, it will be attempted upon application
330
- * before building an App object. This allows to plug in argument coercions,
331
- * e.g. instantly perform a numeric operation natively if the next term is a number.
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 {AnyArity} impl
334
- * @param {{note: string?, arity: number?, canonize: boolean?, apply: function(Expr):(Expr|null) }} [opt]
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: AnyArity, opt?: {
337
- note: string | null;
338
- arity: number | null;
339
- canonize: boolean | null;
340
- apply: (arg0: Expr) => (Expr | null);
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
- impl: AnyArity;
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 An existing expression under a different name.
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
- constructor(n: any);
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;
@@ -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|String|[number, function(...Expr): Expr, {note: string?, fast: boolean?}]} [impl]
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?: Expr | string | [number, (...args: Expr[]) => Expr, {
34
- note: string | null;
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
@@ -144,13 +144,31 @@ export class Quest {
144
144
  show(): TestCase[];
145
145
  }
146
146
  declare class Case {
147
- constructor(input: any, options: any);
148
- max: any;
149
- note: any;
150
- vars: any;
151
- input: any;
152
- engine: any;
153
- parse(src: any): import("./expr").Lambda;
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 {};