@dallaylaen/ski-interpreter 1.3.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,41 @@ 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
+
8
43
  ## [1.3.0] - 2026-01-25
9
44
 
10
45
  ### 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
@@ -48,15 +57,13 @@ all free variables are bound.
48
57
 
49
58
  # Playground
50
59
 
51
- https://dallaylaen.github.io/ski-interpreter/
60
+ * [Interactive interpreter](https://dallaylaen.github.io/ski-interpreter/)
52
61
 
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
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
56
65
 
57
- # Quests
58
-
59
- https://dallaylaen.github.io/ski-interpreter/quest.html
66
+ * [Quests](https://dallaylaen.github.io/ski-interpreter/quest.html)
60
67
 
61
68
  This page contains small tasks of increasing complexity.
62
69
  Each task requires the user to build a combinator with specific properties.
@@ -73,54 +80,134 @@ npm install @dallaylaen/ski-interpreter
73
80
 
74
81
  # Usage
75
82
 
83
+ ## A minimal example
84
+
85
+ ```javascript
86
+ #!node
87
+
88
+ const { SKI } = require('@dallaylaen/ski-interpreter');
89
+
90
+ // Create a parser instance
91
+ const ski = new SKI();
92
+
93
+ // Parse an expression
94
+ const expr = ski.parse(process.argv[2]);
95
+
96
+ // Evaluate it step by step
97
+ for (const step of expr.walk({max: 100})) {
98
+ console.log(`[${step.steps}] ${step.expr}`);
99
+ }
100
+ ```
101
+
102
+ ## Main features
103
+
76
104
  ```javascript
77
- const {SKI} = require('@dallaylaen/ski-interpreter');
78
- const ski = new SKI(); // the parser
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
79
148
 
80
- // parse an expression
81
- const expr = ski.parse('S(K(SI))K x y');
149
+ The `format` methods of the `Expr` class supports
150
+ a number of options, see [the source code](lib/expr.js) for details.
82
151
 
83
- // run the expression
84
- const result = expr.run({max: 100, throw: true});
85
- console.log('reached '+ result.expr + ' after ' + result.steps + ' steps.');
152
+ ## Variable scoping
86
153
 
87
- // inspect the steps taken to reach the result
88
- for (const step of expr.walk())
89
- console.log(step.expr.toString());
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.
90
156
 
91
- // convert lambda to SKI
92
- const lambda = ski.parse('x->y->z->x z y');
93
- for (const step of lambda.rewriteSKI())
94
- console.log(step.steps + ': ' + step.expr);
157
+ A special `scope` argument may be given to parse to limit the scope. It can be any object.
158
+
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
166
+ ```
95
167
 
96
- // convert combinators to lambda
97
- const combinator = ski.parse('BSC');
98
- for (const step of combinator.lambdify())
99
- console.log(step.steps + ': ' + step.expr);
168
+ Variables can also be created using magic `SKI.vars(scope)` method:
100
169
 
101
- // compare expressions
102
- ski.parse('a->b->a').equals(ski.parse('x->y->x')); // true!
170
+ ```javascript
171
+ const scope = {};
172
+ const {x, y, z} = SKI.vars(scope); // no need to specify names
173
+ ```
103
174
 
104
- const jar = {}; // share free variables with the same names between parser runs
105
- ski.parse('a->b->f a').equals(ski.parse('x->y->f x')); // false
106
- ski.parse('a->b->f a', jar).equals(ski.parse('x->y->f x', jar)); // true
175
+ ## Querying the expressions
107
176
 
108
- // define new terms
109
- ski.add('T', 'S(K(SI))K');
110
- console.log(ski.parse('T x y').run().expr); // prints 'x(y)'
177
+ Expressions are trees, so they can be traversed.
111
178
 
112
- // define terms with JS implementation
113
- const jay = new SKI.classes.Native('J', a=>b=>c=>d=>a.apply(b).apply(a.apply(d).apply(c)));
114
- ski.add('J', jay);
179
+ ```javascript
180
+ expr.any(e => e.equals(SKI.S)); // true if any subexpression is S
115
181
 
116
- // access predefined terms directly
117
- SKI.C.apply(SKI.S); // a term
118
- const [x, y] = SKI.free('x', 'y'); // free variables
119
- SKI.church(5).apply(x, y).run().expr + ''; // 'x(x(x(x(x y))))'
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"
120
186
  ```
121
187
 
188
+ ## Test cases
122
189
 
190
+ The `Quest` class may be used to build and execute test cases for combinators.
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
+ ```
123
209
 
210
+ See [quest page data](docs/quest-data/) for more examples.
124
211
 
125
212
  # Thanks
126
213
 
@@ -138,4 +225,4 @@ SKI.church(5).apply(x, y).run().expr + ''; // 'x(x(x(x(x y))))'
138
225
 
139
226
  This software is free and available under the MIT license.
140
227
 
141
- &copy; Konstantin Uvarin 2024-2025
228
+ &copy; Konstantin Uvarin 2024&ndash;2026