@dallaylaen/ski-interpreter 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,54 @@ 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
+ ## [2.0.0] - 2026-02-06
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ - Rename `guess()` to `infer()` for clarity;
13
+ - Remove `replace()` method, use `subst()` or `traverse()` instead;
14
+ - Rename `rewriteSKI` to `toSKI`, `lambdify` to `toLambda` for clarity and consistence;
15
+ - Remove `getSymbols()` method. Partially replaced by traverse();
16
+ - Remove `freeVars()` method (enumeration of vars is problematic with current scope impl);
17
+ - Remove `contains()` method, use `any(e=>e.equals(other))` instead;
18
+ - Remove `Expr.hasLambda()`, use `any(e => e instanceof Lambda)` instead;
19
+ - Remove `postParse()` method (did nothing anyway and was only used in the parser);
20
+ - Replace `parse(src, jar, options)` with `parse(src, options = { ..., env: jar })`;
21
+ - Remove global `SKI.options`;
22
+ - Remove `SKI.free()`, use `const {x, y} = SKI.vars()` instead.
23
+
24
+ ### Added
25
+
26
+ - `expr.traverse(transform: Expr->Expr|null): Expr|null` for term traversal and replacement;
27
+ - `expr.any(predicate: Expr->boolean)` method for matching expressions;
28
+ - expr.diff(expr2) shows exactly where the terms begin differing (or returns null);
29
+ - Parse now has 2 arguments: `ski.parse(src, options={})`:
30
+ - All `parse()` arguments are now immutable;
31
+ - Passing extra terms: `ski.parse(src, { env: { myterm } })`;
32
+ - Variable scope restriction: `ski.parse('x(y)', { scope: myObject })`;
33
+ - `SKI.vars(scope?)` returns a magic proxy for variable declarations;
34
+ - Added semi-official `Named.fancyName` property
35
+ - `@typedef QuestResult` for better type definitions.
36
+
37
+ ### Changed
38
+
39
+ - Parsing without context produces global free vars;
40
+ - Better variable handling with scope/context distinction;
41
+ - Quest system now uses `diff()` instead of `equals()` for more detailed comparisons.
42
+
43
+ ## [1.3.0] - 2026-01-25
44
+
45
+ ### BREAKING CHANGES
46
+
47
+ - Remove `Expr.reduce()` method for good (too ambiguous). See also `Expr.invoke()` below.
48
+ - Remove `onApply` hook from `Native` combinators.
49
+
50
+ ### Added
51
+
52
+ - Expr: Add `invoke(arg: Expr)` method implementing actual rewriting rules.
53
+ - SKI: `add(term, impl, note?)` method now accepts a function as `impl` to define native combinators directly.
54
+ - Improved jsdoc somewhat.
55
+
8
56
  ## [1.2.0] - 2025-12-14
9
57
 
10
58
  ### BREAKING CHANGES
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Simple Kombinator Interpreter
2
2
 
3
- This package contains a
4
- [combinatory logic](https://en.wikipedia.org/wiki/Combinatory_logic)
5
- and [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus)
3
+ This package contains a
4
+ [combinatory logic](https://en.wikipedia.org/wiki/Combinatory_logic)
5
+ and [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus)
6
6
  parser and interpreter focused on traceability and inspectability.
7
7
 
8
8
  It is written in plain JavaScript (with bolted on TypeScript support)
@@ -25,7 +25,7 @@ and can be used in Node.js or in the browser.
25
25
  * Whole non-negative numbers are interpreted as Church numerals, e.g. `5 x y` evaluates to `x(x(x(x(x y))))`. They must also be space-separated from other terms;
26
26
  * `x y z` is the same as `(x y) z` or `x(y)(z)` but **not** `x (y z)`;
27
27
  * Unknown terms are assumed to be free variables;
28
- * Lambda terms are written as `x->y->z->expr`, which is equivalent to
28
+ * Lambda terms are written as `x->y->z->expr`, which is equivalent to
29
29
  `x->(y->(z->expr))` (aka right associative). Free variables in a lambda expression ~~stay in Vegas~~ are isolated from terms with the same name outside it;
30
30
  * X = y z defines a new term.
31
31
 
@@ -38,6 +38,15 @@ and can be used in Node.js or in the browser.
38
38
  * <code>C x y z &mapsto; x z y</code> _// swapping_;
39
39
  * <code>W x y &mapsto; x y y</code> _//duplication_;
40
40
 
41
+ The special combinator `+` will increment Church numerals, if they happen to come after it:
42
+
43
+ * `+ 0` // 1
44
+ * `2 + 3` // -> `+(+(3))` -> `+(4)` -> `5`
45
+
46
+ The `term + 0` idiom may be used to convert
47
+ numbers obtained via computation (e.g. factorials)
48
+ back to human readable form.
49
+
41
50
  # Execution strategy
42
51
 
43
52
  Applications and native terms use normal strategy, i.e. the first term in the tree
@@ -46,6 +55,23 @@ that has enough arguments is executed and the step ends there.
46
55
  Lambda terms are lazy, i.e. the body is not touched until
47
56
  all free variables are bound.
48
57
 
58
+ # Playground
59
+
60
+ * [Interactive interpreter](https://dallaylaen.github.io/ski-interpreter/)
61
+
62
+ * all of the above features (except comparison and JS-native terms) in your browser
63
+ * expressions have permalinks
64
+ * can configure verbosity and execution speed
65
+
66
+ * [Quests](https://dallaylaen.github.io/ski-interpreter/quest.html)
67
+
68
+ This page contains small tasks of increasing complexity.
69
+ Each task requires the user to build a combinator with specific properties.
70
+
71
+ # CLI
72
+
73
+ REPL comes with the package as [bin/ski.js](bin/ski.js).
74
+
49
75
  # Installation
50
76
 
51
77
  ```bash
@@ -54,72 +80,134 @@ npm install @dallaylaen/ski-interpreter
54
80
 
55
81
  # Usage
56
82
 
83
+ ## A minimal example
84
+
57
85
  ```javascript
58
- const {SKI} = require('@dallaylaen/ski-interpreter');
59
- const ski = new SKI(); // the parser
86
+ #!node
60
87
 
61
- // parse an expression
62
- const expr = ski.parse('S(K(SI))K x y');
88
+ const { SKI } = require('@dallaylaen/ski-interpreter');
63
89
 
64
- // run the expression
65
- const result = expr.run({max: 100, throw: true});
66
- console.log('reached '+ result.expr + ' after ' + result.steps + ' steps.');
90
+ // Create a parser instance
91
+ const ski = new SKI();
67
92
 
68
- // inspect the steps taken to reach the result
69
- for (const step of expr.walk())
70
- console.log(step.expr.toString());
93
+ // Parse an expression
94
+ const expr = ski.parse(process.argv[2]);
71
95
 
72
- // convert lambda to SKI
73
- const lambda = ski.parse('x->y->z->x z y');
74
- for (const step of lambda.rewriteSKI())
75
- console.log(step.steps + ': ' + step.expr);
96
+ // Evaluate it step by step
97
+ for (const step of expr.walk({max: 100})) {
98
+ console.log(`[${step.steps}] ${step.expr}`);
99
+ }
100
+ ```
76
101
 
77
- // convert combinators to lambda
78
- const combinator = ski.parse('BSC');
79
- for (const step of combinator.lambdify())
80
- console.log(step.steps + ': ' + step.expr);
102
+ ## Main features
81
103
 
82
- // compare expressions
83
- ski.parse('a->b->a').equals(ski.parse('x->y->x')); // true!
104
+ ```javascript
105
+ const { SKI } = require('@dallaylaen/ski-interpreter');
106
+ const ski = new SKI();
107
+
108
+ const expr = ski.parse(src);
109
+
110
+ // evaluating expressions
111
+ const next = expr.step(); // { steps: 1, expr: '...' }
112
+ const final = expr.run({max: 1000}); // { steps: 42, expr: '...' }
113
+ const iterator = expr.walk();
114
+
115
+ // applying expressions
116
+ const result = expr.run({max: 1000}, arg1, arg2 ...);
117
+ // same sa
118
+ expr.apply(arg1).apply(arg2).run();
119
+ // or simply
120
+ expr.apply(arg1, arg2).run();
121
+
122
+ // equality check
123
+ ski.parse('x->y->x').equals(ski.parse('a->b->a')); // true
124
+ ski.parse('S').equals(SKI.S); // true
125
+ ski.parse('x').apply(ski.parse('y')).equals(ski.parse('x y')); // also true
126
+
127
+ // defining new terms
128
+ ski.add('T', 'CI'); // T x y = C I x y = I y x = y
129
+ ski.add('M', 'x->x x'); // M x = x x
130
+
131
+ // also with native JavaScript implementations:
132
+ ski.add('V', x=>y=>f=>f.apply(x, y), 'pair constructor');
133
+
134
+ ski.getTerms(); // all of the above as an object
135
+
136
+ // converting lambda expressions to SKI
137
+ const lambdaExpr = ski.parse('x->y->x y');
138
+ const steps = [...lambdaExpr.toSKI()];
139
+ // steps[steps.length - 1].expr only contains S, K, I, and free variables, if any
140
+
141
+ // converting SKI expressions to lambda
142
+ const skiExpr = ski.parse('S K K');
143
+ const lambdaSteps = [...skiExpr.toLambda()];
144
+ // lambdaSteps[lambdaSteps.length - 1].expr only contains lambda abstractions and applications
145
+ ```
146
+
147
+ ## Fancy formatting
148
+
149
+ The `format` methods of the `Expr` class supports
150
+ a number of options, see [the source code](lib/expr.js) for details.
84
151
 
85
- const jar = {}; // share free variables with the same names between parser runs
86
- ski.parse('a->b->f a').equals(ski.parse('x->y->f x')); // false
87
- ski.parse('a->b->f a', jar).equals(ski.parse('x->y->f x', jar)); // true
152
+ ## Variable scoping
88
153
 
89
- // define new terms
90
- ski.add('T', 'S(K(SI))K');
91
- console.log(ski.parse('T x y').run().expr); // prints 'x(y)'
154
+ By default, parsed free variables are global and equal to any other variable with the same name.
155
+ Variables inside lambdas are local to said lambda and will not be equal to anything except themselves.
92
156
 
93
- // define terms with JS implementation
94
- const jay = new SKI.classes.Native('J', a=>b=>c=>d=>a.apply(b).apply(a.apply(d).apply(c)));
95
- ski.add('J', jay);
157
+ A special `scope` argument may be given to parse to limit the scope. It can be any object.
96
158
 
97
- // access predefined terms directly
98
- SKI.C.apply(SKI.S); // a term
99
- const [x, y] = SKI.free('x', 'y'); // free variables
100
- SKI.church(5).apply(x, y).run().expr + ''; // 'x(x(x(x(x y))))'
159
+ ```javascript
160
+ const scope1 = {};
161
+ const scope2 = {};
162
+ const expr1 = ski.parse('x y', {scope: scope1});
163
+ const expr2 = ski.parse('x y', {scope: scope2}); // not equal
164
+ const expr3 = ski.parse('x y'); // equal to neither
165
+ const expr4 = ski.parse('x', {scope: scope1}).apply(ski.parse('y', {scope: scope1})); // equal to expr1
101
166
  ```
102
167
 
103
- # Playground
168
+ Variables can also be created using magic `SKI.vars(scope)` method:
104
169
 
105
- https://dallaylaen.github.io/ski-interpreter/
170
+ ```javascript
171
+ const scope = {};
172
+ const {x, y, z} = SKI.vars(scope); // no need to specify names
173
+ ```
106
174
 
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
175
+ ## Querying the expressions
110
176
 
111
- # Quests
177
+ Expressions are trees, so they can be traversed.
112
178
 
113
- https://dallaylaen.github.io/ski-interpreter/quest.html
179
+ ```javascript
180
+ expr.any(e => e.equals(SKI.S)); // true if any subexpression is S
114
181
 
115
- This page contains small tasks of increasing complexity.
116
- Each task requires the user to build a combinator with specific properties.
182
+ expr.traverse(e => e.equals(SKI.I) ? SKI.S.apply(SKI.K, SKI.K) : null);
183
+ // replaces all I's with S K K
184
+ // here a returned `Expr` object replaces the subexpression,
185
+ // whereas `null` means "leave it alone and descend if possible"
186
+ ```
117
187
 
118
- # CLI
188
+ ## Test cases
119
189
 
120
- REPL comes with the package as [bin/ski.js](bin/ski.js).
190
+ The `Quest` class may be used to build and execute test cases for combinators.
121
191
 
192
+ ```javascript
193
+ const { Quest } = require('@dallaylaen/ski-interpreter');
194
+
195
+ const q = new Quest({
196
+ name: 'Test combinator T',
197
+ description: 'T x y should equal y x',
198
+ input: 'T',
199
+ cases: [
200
+ ['T x y', 'y x'],
201
+ ],
202
+ });
203
+
204
+ q.check('CI'); // pass
205
+ q.check('a->b->b a'); // ditto
206
+ q.check('K'); // fail
207
+ q.check('K(K(y x))') // nope! the variable scopes won't match
208
+ ```
122
209
 
210
+ See [quest page data](docs/quest-data/) for more examples.
123
211
 
124
212
  # Thanks
125
213
 
@@ -131,9 +219,10 @@ REPL comes with the package as [bin/ski.js](bin/ski.js).
131
219
  * "To Mock The Mockingbird" by Raymond Smulian.
132
220
  * [combinator birds](https://www.angelfire.com/tx4/cus/combinator/birds.html) by [Chris Rathman](https://www.angelfire.com/tx4/cus/index.html)
133
221
  * [Fun with combinators](https://doisinkidney.com/posts/2020-10-17-ski.html) by [@oisdk](https://github.com/oisdk)
222
+ * [Conbinatris](https://dirk.rave.org/combinatris/) by Dirk van Deun
134
223
 
135
224
  # License and copyright
136
225
 
137
226
  This software is free and available under the MIT license.
138
227
 
139
- &copy; Konstantin Uvarin 2024-2025
228
+ &copy; Konstantin Uvarin 2024&ndash;2026