@dallaylaen/ski-interpreter 2.4.0 → 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,18 @@ 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
+
8
20
  ## [2.4.0] - 2026-03-06
9
21
 
10
22
  ### BREAKING CHANGES
package/README.md CHANGED
@@ -99,12 +99,14 @@ npm install @dallaylaen/ski-interpreter
99
99
  * `--verbose` - Show all evaluation steps
100
100
  * Example: `ski file script.ski`
101
101
 
102
+ * **`infer <expression>`** - try to find equivalent lambda expression and display its properties if found.
103
+
102
104
  * **`extract <expression> <known term> ...`** -
103
105
  Replace parts of the expression that are equivalent to the known terms with the respective terms. Known terms must be normalizable.
104
106
 
105
107
  * **`search <expression> <known term> ...`** -
106
108
  Attempt to brute force an equivalent of the _expression_ using only the _known terms_.
107
- Only normalizing terms are currently supported.
109
+ Only normalizable terms are currently supported.
108
110
 
109
111
  * **`quest-lint <files...>`** - Validate quest definition files
110
112
  * `--solution <file>` - Load solutions from a JSON file for verification
@@ -150,7 +152,7 @@ const final = expr.run({max: 1000}); // { steps: 42, expr: '...' }
150
152
  const iterator = expr.walk();
151
153
 
152
154
  // applying expressions
153
- const result = expr.run({max: 1000}, arg1, arg2 ...);
155
+ const result = expr.run({max: 1000}, arg1, arg2, ...);
154
156
  // same sa
155
157
  expr.apply(arg1).apply(arg2).run();
156
158
  // or simply
package/bin/ski.js CHANGED
@@ -41,12 +41,12 @@ program
41
41
  evaluateFile(filepath, options.verbose);
42
42
  });
43
43
 
44
- // Search subcommand
44
+ // Infer subcommand
45
45
  program
46
- .command('search <target> <terms...>')
47
- .description('Search for an expression equivalent to target using known terms')
48
- .action((target, terms) => {
49
- 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);
50
50
  });
51
51
 
52
52
  // Extract subcommand
@@ -57,6 +57,14 @@ program
57
57
  extractExpression(target, terms);
58
58
  });
59
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
+
60
68
  // Quest-check subcommand
61
69
  program
62
70
  .command('quest-lint <files...>')
@@ -156,6 +164,41 @@ function processLine (source, ski, verbose, onErr) {
156
164
  }
157
165
  }
158
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
+
159
202
  async function questCheck (files, solutionFile) {
160
203
  try {
161
204
  // Load solutions if provided
@@ -93,7 +93,8 @@ function prepareWrapper(label) {
93
93
  // src/expr.ts
94
94
  var DEFAULTS = {
95
95
  max: 1e3,
96
- maxArgs: 32
96
+ maxArgs: 32,
97
+ maxSize: 1e6
97
98
  };
98
99
  var ORDER = {
99
100
  "leftmost-outermost": "LO",
@@ -115,6 +116,7 @@ var Expr = class _Expr {
115
116
  static {
116
117
  this.native = native;
117
118
  }
119
+ // rough estimate of the number of nodes in the tree
118
120
  /**
119
121
  *
120
122
  * @desc Define properties of the term based on user supplied options and/or inference results.
@@ -314,9 +316,12 @@ var Expr = class _Expr {
314
316
  */
315
317
  infer(options = {}) {
316
318
  const skipNames = {};
319
+ const skipSkip = /* @__PURE__ */ new Set();
317
320
  this.traverse((e) => {
318
- if (e instanceof Named)
321
+ if (e instanceof Named && !skipSkip.has(e))
319
322
  skipNames[e.name] = true;
323
+ if (e instanceof Lambda)
324
+ skipSkip.add(e.arg);
320
325
  return void 0;
321
326
  });
322
327
  return this._infer({
@@ -539,6 +544,8 @@ var Expr = class _Expr {
539
544
  final = true;
540
545
  break;
541
546
  }
547
+ if ((next.expr.size ?? 1) > (opt.maxSize ?? DEFAULTS.maxSize))
548
+ break;
542
549
  steps += next.steps;
543
550
  expr = next.expr;
544
551
  }
@@ -557,7 +564,7 @@ var Expr = class _Expr {
557
564
  let steps = 0;
558
565
  let expr = this;
559
566
  let final = false;
560
- while (steps < max) {
567
+ while (steps < max && (expr.size ?? 1) < (options.maxSize ?? DEFAULTS.maxSize)) {
561
568
  const next = expr.step();
562
569
  if (!next.changed)
563
570
  final = true;
@@ -778,6 +785,7 @@ var App = class _App extends Expr {
778
785
  super();
779
786
  this.arg = arg;
780
787
  this.fun = fun;
788
+ this.size = (fun.size ?? 1) + (arg.size ?? 1);
781
789
  }
782
790
  /** @property {boolean} [final] */
783
791
  _traverse_descend(options, change) {
@@ -950,6 +958,7 @@ var Lambda = class _Lambda extends Expr {
950
958
  this.arg = local;
951
959
  this.impl = impl.subst(arg, local) ?? impl;
952
960
  this.arity = 1;
961
+ this.size = (impl.size ?? 1) + 1;
953
962
  }
954
963
  invoke(arg) {
955
964
  return this.impl.subst(this.arg, arg) ?? this.impl;
@@ -1036,6 +1045,7 @@ var Alias = class extends Named {
1036
1045
  this._setup(options);
1037
1046
  this.terminal = options.terminal ?? this.props?.proper;
1038
1047
  this.invoke = waitn(impl, this.arity ?? 0);
1048
+ this.size = impl.size;
1039
1049
  }
1040
1050
  /**
1041
1051
  * @property {boolean} [outdated] - whether the alias is outdated