@dallaylaen/ski-interpreter 2.3.3 → 2.4.1

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,34 @@ 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.4.1] - 2026-03-06
9
+
10
+ ### Added
11
+
12
+ - expr.run({maxSize: number ?? 1_000_000}) to prevent runaway reductions (fixes #14).
13
+ - `ski.js infer <expression>` command to infer properties of an expression.
14
+
15
+ ### Fixed
16
+
17
+ - quests use shorter Y combinator form internally (#13)
18
+ - history in the playground is scrolled to the bottom.
19
+
20
+ ## [2.4.0] - 2026-03-06
21
+
22
+ ### BREAKING CHANGES
23
+
24
+ - Remove Expr.freeOnly method, use `!this.any(e => !(e instanceof FreeVar || e instanceof App))` instead;
25
+ - Remove Expr.weight() function because it's too ambiguous, use `fold (...)` instead.
26
+ - Remove `{ Quest }` from exports, use `SKI.Quest` instead.
27
+ - Rename `ski.js quest-check` to `ski.js quest-lint` for clarity.
28
+
29
+ ### Added
30
+ - Rewrite the whole thing in TypeScript.
31
+
32
+ ### Fixed
33
+ - Avoid name clashes with existing variables in infer()
34
+ ([#15](https://github.com/dallaylaen/ski-interpreter/issues/15))
35
+
8
36
  ## [2.3.3] - 2026-03-01
9
37
 
10
38
  ### Changed
package/README.md CHANGED
@@ -99,9 +99,18 @@ npm install @dallaylaen/ski-interpreter
99
99
  * `--verbose` - Show all evaluation steps
100
100
  * Example: `ski file script.ski`
101
101
 
102
- * **`quest-check <files...>`** - Validate quest definition files
102
+ * **`infer <expression>`** - try to find equivalent lambda expression and display its properties if found.
103
+
104
+ * **`extract <expression> <known term> ...`** -
105
+ Replace parts of the expression that are equivalent to the known terms with the respective terms. Known terms must be normalizable.
106
+
107
+ * **`search <expression> <known term> ...`** -
108
+ Attempt to brute force an equivalent of the _expression_ using only the _known terms_.
109
+ Only normalizable terms are currently supported.
110
+
111
+ * **`quest-lint <files...>`** - Validate quest definition files
103
112
  * `--solution <file>` - Load solutions from a JSON file for verification
104
- * Example: `ski quest-check quest1.json quest2.json --solution solutions.json`
113
+ * Example: `ski quest-lint quest1.json quest2.json --solution solutions.json`
105
114
 
106
115
  If no subcommand is provided, help is displayed.
107
116
 
@@ -143,7 +152,7 @@ const final = expr.run({max: 1000}); // { steps: 42, expr: '...' }
143
152
  const iterator = expr.walk();
144
153
 
145
154
  // applying expressions
146
- const result = expr.run({max: 1000}, arg1, arg2 ...);
155
+ const result = expr.run({max: 1000}, arg1, arg2, ...);
147
156
  // same sa
148
157
  expr.apply(arg1).apply(arg2).run();
149
158
  // or simply
package/bin/ski.js CHANGED
@@ -4,14 +4,15 @@ const fs = require('node:fs/promises');
4
4
  const { Command } = require('commander');
5
5
 
6
6
  const { SKI } = require('../lib/ski-interpreter.cjs');
7
- const { Quest } = require('../src/quest.js');
7
+ const { Quest } = SKI;
8
+ const { version } = require('../package.json');
8
9
 
9
10
  const program = new Command();
10
11
 
11
12
  program
12
13
  .name('ski')
13
14
  .description('Simple Kombinator Interpreter - a combinatory logic & lambda calculus parser and interpreter')
14
- .version('2.2.1');
15
+ .version(version);
15
16
 
16
17
  // REPL subcommand
17
18
  program
@@ -40,12 +41,12 @@ program
40
41
  evaluateFile(filepath, options.verbose);
41
42
  });
42
43
 
43
- // Search subcommand
44
+ // Infer subcommand
44
45
  program
45
- .command('search <target> <terms...>')
46
- .description('Search for an expression equivalent to target using known terms')
47
- .action((target, terms) => {
48
- searchExpression(target, terms);
46
+ .command('infer <expression>')
47
+ .description('Find a canonical form of the expression and its properties')
48
+ .action((expression) => {
49
+ inferExpression(expression);
49
50
  });
50
51
 
51
52
  // Extract subcommand
@@ -56,9 +57,17 @@ program
56
57
  extractExpression(target, terms);
57
58
  });
58
59
 
60
+ // Search subcommand
61
+ program
62
+ .command('search <target> <terms...>')
63
+ .description('Search for an expression equivalent to target using known terms')
64
+ .action((target, terms) => {
65
+ searchExpression(target, terms);
66
+ });
67
+
59
68
  // Quest-check subcommand
60
69
  program
61
- .command('quest-check <files...>')
70
+ .command('quest-lint <files...>')
62
71
  .description('Check quest files for validity')
63
72
  .option('--solution <file>', 'Load solutions from file')
64
73
  .action((files, options) => {
@@ -155,6 +164,41 @@ function processLine (source, ski, verbose, onErr) {
155
164
  }
156
165
  }
157
166
 
167
+ function inferExpression (expression) {
168
+ const ski = new SKI();
169
+
170
+ const expr = ski.parse(expression);
171
+ const guess = expr.infer();
172
+
173
+ if (guess.normal) {
174
+ displayInfer(guess);
175
+ return;
176
+ }
177
+ // hard case...
178
+ let steps = guess.steps;
179
+ const canon = expr.traverse(e => {
180
+ const g = e.infer();
181
+ steps += g.steps;
182
+ return g.expr;
183
+ });
184
+
185
+ displayInfer({ expr: canon, steps, normal: false, proper: false });
186
+ }
187
+
188
+ /**
189
+ *
190
+ * @param {TermInfo} guess
191
+ */
192
+ function displayInfer (guess) {
193
+ if (guess.expr)
194
+ console.log(guess.expr.format());
195
+
196
+ for (const key of ['normal', 'proper', 'arity', 'discard', 'duplicate', 'steps']) {
197
+ if (guess[key] !== undefined)
198
+ console.log(`// ${key}: ${guess[key]}`);
199
+ }
200
+ }
201
+
158
202
  async function questCheck (files, solutionFile) {
159
203
  try {
160
204
  // Load solutions if provided
@@ -214,7 +258,7 @@ async function questCheck (files, solutionFile) {
214
258
  // Exit with appropriate code
215
259
  process.exit(hasErrors ? 1 : 0);
216
260
  } catch (err) {
217
- console.error('Error in quest-check:', err.message);
261
+ console.error('Error in quest-lint:', err.message);
218
262
  process.exit(2);
219
263
  }
220
264
  }
@@ -264,9 +308,11 @@ function extractExpression (targetStr, termStrs) {
264
308
 
265
309
  const replaced = expr.traverse(e => {
266
310
  const canon = e.infer().expr;
267
- for (const [lambda, term] of pairs) {
268
- if (canon.equals(lambda))
269
- return term;
311
+ if (canon) {
312
+ for (const [lambda, term] of pairs) {
313
+ if (canon.equals(lambda))
314
+ return term;
315
+ }
270
316
  }
271
317
  return null;
272
318
  });