@dallaylaen/ski-interpreter 2.4.1 → 2.5.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,32 @@ 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.5.0] - 2026-03-15
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ - `affine: true` in quests now means "has no duplicating subterms" rather than "non-duplicating as a whole."
13
+ Aliases are exempted from this check, so a solution `SK`
14
+ will not pass as S is duplicating, but `false=SK; false`
15
+ will because `false` (as a whole term) _is_ affine.
16
+
17
+ ### Added
18
+
19
+ - `SKI.schemas.FormatOptions`: zod schema for validating format options.
20
+ - `bin/ski.js`: global `--format` option.
21
+ - `bin/ski.js`: `!format` command in the REPL.
22
+ - `bin/ski.js`: `--verbose` is now a global flag with a toggle in the REPL.
23
+ - `bin/ski.js`: `--max`, `--max-size`, and `--max-args` arguments.
24
+ - `bin/ski.js search`: `--max-depth` and `--max-tries` options.
25
+ - New quest: affine extract from list.
26
+ - `zod` added as a dependency.
27
+
28
+ ### Fixed
29
+
30
+ - `infer()` now adheres to `maxSize`.
31
+ - Avoid adding context to parsed saved terms.
32
+ - `TraverseValue` signature now includes `void` so that `return undefined` in `traverse`/`fold` is no longer necessary.
33
+
8
34
  ## [2.4.1] - 2026-03-06
9
35
 
10
36
  ### Added
package/bin/ski.js CHANGED
@@ -7,38 +7,59 @@ const { SKI } = require('../lib/ski-interpreter.cjs');
7
7
  const { Quest } = SKI;
8
8
  const { version } = require('../package.json');
9
9
 
10
+ const runOptions = {};
11
+ let format = {};
12
+ let verbose = false;
13
+
10
14
  const program = new Command();
11
15
 
12
16
  program
13
17
  .name('ski')
14
18
  .description('Simple Kombinator Interpreter - a combinatory logic & lambda calculus parser and interpreter')
15
- .version(version);
19
+ .version(version)
20
+ .option('-v, --verbose', 'Show all evaluation steps', () => { verbose = true; })
21
+ .option('--format <json>', 'Format for output expressions', setFormat)
22
+ .option('--max <number>', 'Limit computation steps', raw => {
23
+ const n = Number.parseInt(raw);
24
+ if (Number.isNaN(n) || n <= 0)
25
+ throw new Error('--max requires a positive integer');
26
+ runOptions.max = n;
27
+ })
28
+ .option('--max-size <number>', 'Limit expression\'s total footprint during computations', raw => {
29
+ const n = Number.parseInt(raw);
30
+ if (Number.isNaN(n) || n <= 0)
31
+ throw new Error('--max-size requires a positive integer');
32
+ runOptions.maxSize = n;
33
+ })
34
+ .option('--max-args <number>', 'Limit probed arguments when inferring terms', raw => {
35
+ const n = Number.parseInt(raw);
36
+ if (Number.isNaN(n) || n <= 0)
37
+ throw new Error('--max-args requires a positive integer');
38
+ runOptions.maxArgs = n;
39
+ });
16
40
 
17
41
  // REPL subcommand
18
42
  program
19
43
  .command('repl')
20
44
  .description('Start interactive REPL')
21
- .option('--verbose', 'Show all evaluation steps')
22
45
  .action((options) => {
23
- startRepl(options.verbose);
46
+ startRepl(options);
24
47
  });
25
48
 
26
49
  // Eval subcommand
27
50
  program
28
51
  .command('eval <expression>')
29
52
  .description('Evaluate a single expression')
30
- .option('--verbose', 'Show all evaluation steps')
31
53
  .action((expression, options) => {
32
- evaluateExpression(expression, options.verbose);
54
+ evaluateExpression(expression, options);
33
55
  });
34
56
 
35
57
  // File subcommand
36
58
  program
37
59
  .command('file <filepath>')
38
60
  .description('Evaluate expressions from a file')
39
- .option('--verbose', 'Show all evaluation steps')
40
61
  .action((filepath, options) => {
41
- evaluateFile(filepath, options.verbose);
62
+ evaluateFile(filepath);
42
63
  });
43
64
 
44
65
  // Infer subcommand
@@ -61,9 +82,9 @@ program
61
82
  program
62
83
  .command('search <target> <terms...>')
63
84
  .description('Search for an expression equivalent to target using known terms')
64
- .action((target, terms) => {
65
- searchExpression(target, terms);
66
- });
85
+ .option('--max-depth <number>', 'Limit search depth', toInt('--max-depth'))
86
+ .option('--max-tries <number>', 'Limit total terms probed', toInt('--max-tries'))
87
+ .action(searchExpression);
67
88
 
68
89
  // Quest-check subcommand
69
90
  program
@@ -80,9 +101,9 @@ program
80
101
  .parse(process.argv);
81
102
 
82
103
  if (!process.argv.slice(2).length)
83
- startRepl(false);
104
+ startRepl();
84
105
 
85
- function startRepl (verbose) {
106
+ function startRepl () {
86
107
  const readline = require('readline');
87
108
  const ski = new SKI();
88
109
 
@@ -100,7 +121,7 @@ function startRepl (verbose) {
100
121
  if (str.startsWith('!'))
101
122
  handleCommand(str, ski);
102
123
  else {
103
- processLine(str, ski, verbose, err => {
124
+ processLine(str, ski, err => {
104
125
  console.log('' + err);
105
126
  });
106
127
  }
@@ -116,19 +137,19 @@ function startRepl (verbose) {
116
137
  rl.prompt();
117
138
  }
118
139
 
119
- function evaluateExpression (expression, verbose) {
140
+ function evaluateExpression (expression) {
120
141
  const ski = new SKI();
121
- processLine(expression, ski, verbose, err => {
142
+ processLine(expression, ski, err => {
122
143
  console.error('' + err);
123
144
  process.exit(3);
124
145
  });
125
146
  }
126
147
 
127
- function evaluateFile (filepath, verbose) {
148
+ function evaluateFile (filepath) {
128
149
  const ski = new SKI();
129
150
  fs.readFile(filepath, 'utf8')
130
151
  .then(source => {
131
- processLine(source, ski, verbose, err => {
152
+ processLine(source, ski, err => {
132
153
  console.error('' + err);
133
154
  process.exit(3);
134
155
  });
@@ -139,7 +160,7 @@ function evaluateFile (filepath, verbose) {
139
160
  });
140
161
  }
141
162
 
142
- function processLine (source, ski, verbose, onErr) {
163
+ function processLine (source, ski, onErr) {
143
164
  if (!source.match(/\S/))
144
165
  return; // nothing to see here
145
166
 
@@ -149,12 +170,12 @@ function processLine (source, ski, verbose, onErr) {
149
170
  const isAlias = expr instanceof SKI.classes.Alias;
150
171
  const aliasName = isAlias ? expr.name : null;
151
172
 
152
- for (const state of expr.walk()) {
173
+ for (const state of expr.walk(runOptions)) {
153
174
  if (state.final)
154
175
  console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
155
176
 
156
177
  if (verbose || state.final)
157
- console.log('' + state.expr.format());
178
+ console.log(state.expr.format(format));
158
179
 
159
180
  if (state.final && isAlias && aliasName)
160
181
  ski.add(aliasName, state.expr);
@@ -168,7 +189,7 @@ function inferExpression (expression) {
168
189
  const ski = new SKI();
169
190
 
170
191
  const expr = ski.parse(expression);
171
- const guess = expr.infer();
192
+ const guess = expr.infer(runOptions);
172
193
 
173
194
  if (guess.normal) {
174
195
  displayInfer(guess);
@@ -177,7 +198,9 @@ function inferExpression (expression) {
177
198
  // hard case...
178
199
  let steps = guess.steps;
179
200
  const canon = expr.traverse(e => {
180
- const g = e.infer();
201
+ if (e === expr)
202
+ return; // already tried
203
+ const g = e.infer(runOptions);
181
204
  steps += g.steps;
182
205
  return g.expr;
183
206
  });
@@ -191,7 +214,7 @@ function inferExpression (expression) {
191
214
  */
192
215
  function displayInfer (guess) {
193
216
  if (guess.expr)
194
- console.log(guess.expr.format());
217
+ console.log(guess.expr.format(format));
195
218
 
196
219
  for (const key of ['normal', 'proper', 'arity', 'discard', 'duplicate', 'steps']) {
197
220
  if (guess[key] !== undefined)
@@ -263,11 +286,10 @@ async function questCheck (files, solutionFile) {
263
286
  }
264
287
  }
265
288
 
266
- function searchExpression (targetStr, termStrs) {
289
+ function searchExpression (targetStr, termStrs, options) {
267
290
  const ski = new SKI();
268
- const jar = {};
269
- const target = ski.parse(targetStr, { vars: jar });
270
- const seed = termStrs.map(s => ski.parse(s, { vars: jar }));
291
+ const target = ski.parse(targetStr);
292
+ const seed = termStrs.map(s => ski.parse(s));
271
293
 
272
294
  const { expr } = target.infer();
273
295
  if (!expr) {
@@ -275,7 +297,7 @@ function searchExpression (targetStr, termStrs) {
275
297
  process.exit(1);
276
298
  }
277
299
 
278
- const res = SKI.extras.search(seed, { tries: 10_000_000, depth: 100 }, (e, p) => {
300
+ const res = SKI.extras.search(seed, { tries: options.maxTries, depth: options.maxDepth }, (e, p) => {
279
301
  if (!p.expr)
280
302
  return -1;
281
303
  if (p.expr.equals(expr))
@@ -284,7 +306,7 @@ function searchExpression (targetStr, termStrs) {
284
306
  });
285
307
 
286
308
  if (res.expr) {
287
- console.log(`Found ${res.expr} after ${res.total} tries.`);
309
+ console.log(`Found ${res.expr.format(format)} after ${res.total} tries.`);
288
310
  process.exit(0);
289
311
  } else {
290
312
  console.error(`No equivalent expression found for ${target} after ${res.total} tries.`);
@@ -297,7 +319,7 @@ function extractExpression (targetStr, termStrs) {
297
319
  const expr = ski.parse(targetStr);
298
320
  const pairs = termStrs
299
321
  .map(s => ski.parse(s))
300
- .map(e => [e.infer().expr, e]);
322
+ .map(e => [e.infer(runOptions).expr, e]);
301
323
 
302
324
  const uncanonical = pairs.filter(pair => !pair[0]);
303
325
  if (uncanonical.length) {
@@ -307,25 +329,22 @@ function extractExpression (targetStr, termStrs) {
307
329
  }
308
330
 
309
331
  const replaced = expr.traverse(e => {
310
- const canon = e.infer().expr;
332
+ const canon = e.infer(runOptions).expr;
311
333
  if (canon) {
312
334
  for (const [lambda, term] of pairs) {
313
335
  if (canon.equals(lambda))
314
336
  return term;
315
337
  }
316
338
  }
317
- return null;
318
339
  });
319
340
 
320
- if (replaced)
321
- console.log(replaced.toString());
322
- else
341
+ console.log((replaced ?? expr).format(format));
342
+ if (!replaced)
323
343
  console.log('// unchanged');
324
344
  }
325
345
 
326
346
  function handleCommand (input, ski) {
327
- const parts = input.trim().split(/\s+/);
328
- const cmd = parts[0];
347
+ const [_, cmd, arg] = input.match(/^\s*(\S+)(?:\s+(.*\S))?\s*$/);
329
348
 
330
349
  const dispatch = {
331
350
  '!ls': () => {
@@ -339,9 +358,21 @@ function handleCommand (input, ski) {
339
358
  console.log(` ${name} ${term.props?.expr ?? '(native)'}`);
340
359
  }
341
360
  },
361
+ '!verbose': flag => {
362
+ verbose = !(['off', 'false', '0', '-'].includes(flag));
363
+ console.log('// verbose is ' + (verbose ? 'on' : 'off'));
364
+ },
365
+ '!format': options => {
366
+ if (options)
367
+ setFormat(options);
368
+ else
369
+ console.log('Format: ' + JSON.stringify(format, null, 2));
370
+ },
342
371
  '!help': () => {
343
372
  console.log('Available commands:');
344
373
  console.log(' !ls - List term inventory');
374
+ console.log(' !verbose [on|off] - Toggle verbose mode (show all evaluation steps)');
375
+ console.log(' !format [json] - Set or show output format options');
345
376
  console.log(' !help - Show this help message');
346
377
  },
347
378
  '': () => {
@@ -350,5 +381,22 @@ function handleCommand (input, ski) {
350
381
  }
351
382
  };
352
383
 
353
- (dispatch[cmd] || dispatch[''])(...parts.slice(1));
384
+ try {
385
+ (dispatch[cmd] || dispatch[''])(arg)
386
+ } catch (err) {
387
+ console.error(`Error executing command ${cmd}:`, err.message);
388
+ }
389
+ }
390
+
391
+ function setFormat (options) {
392
+ format = SKI.schemas.FormatOptions.parse(JSON.parse(options));
393
+ }
394
+
395
+ function toInt (comment) {
396
+ return function (str) {
397
+ const n = Number.parseInt(str);
398
+ if (Number.isNaN(n) || n <= 0)
399
+ throw new Error(comment + ' requires positive integer');
400
+ return n;
401
+ }
354
402
  }