@dallaylaen/ski-interpreter 2.7.0 → 2.8.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,33 @@ 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.8.0] - 2026-04-23
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ - `parse()` no longer auto-inlines aliases unless they are redefined; known aliases are preserved by name.
13
+ - `toposort()` signature changed to use options object instead of positional parameters.
14
+
15
+ ### Added
16
+
17
+ - `Expr.declare()` — emits a complete, round-trippable declaration of an expression including its dependencies.
18
+ - `parse(..., { canonize: true })` — calculates properties of intermediate aliases during parsing.
19
+ - `annotate()` — public rename of the semi-official `_setup()` method, with added documentation.
20
+ - Quest UI: "reveal solution" button added.
21
+ - Quest UI: raw user input is now saved when a quest is solved.
22
+ - CLI (`bin/ski.js`): outputs full declarations instead of bare expressions.
23
+
24
+ ### Changed
25
+
26
+ - `toJSON()` now outputs back-parsable declaration instead of just string.
27
+ - `toposort` moved into `expr.ts`.
28
+ - `extras.ts` delegates to `Expr.declare()` instead of re-implementing the logic.
29
+ - Internal type `{ ... }` renamed to `Record<string, Named>` for clarity.
30
+
31
+ ### Fixed
32
+
33
+ - `Expr.toLambda` was incorrectly preparing term (#24).
34
+
8
35
  ## [2.7.0] - 2026-04-03
9
36
 
10
37
  ### BREAKING CHANGES
package/bin/ski.js CHANGED
@@ -166,20 +166,17 @@ function processLine (source, ski, onErr) {
166
166
  return; // nothing to see here
167
167
 
168
168
  try {
169
- const expr = ski.parse(source);
169
+ const expr = ski.parse(source, { canonize: true });
170
+ if (expr instanceof SKI.classes.Alias)
171
+ ski.add(expr);
170
172
  const t0 = new Date();
171
- const isAlias = expr instanceof SKI.classes.Alias;
172
- const aliasName = isAlias ? expr.name : null;
173
173
 
174
174
  for (const state of expr.walk(runOptions)) {
175
- if (state.final)
175
+ if (state.final) {
176
176
  console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
177
-
178
- if (verbose || state.final)
179
- console.log(state.expr.format(format));
180
-
181
- if (state.final && isAlias && aliasName)
182
- ski.add(aliasName, state.expr);
177
+ console.log(state.expr.declare({ ...format, inventory: ski.getTerms() }));
178
+ } else if (verbose)
179
+ console.log(state.expr.format(format) + ';');
183
180
  }
184
181
  } catch (err) {
185
182
  onErr(err);
@@ -111,10 +111,13 @@ var control = {
111
111
  var native = {};
112
112
  var Expr = class _Expr {
113
113
  /**
114
+ * Add metadata based on user-supplied values and/or the properties of the term itself.
114
115
  *
115
- * Define properties of the term based on user supplied options and/or inference results.
116
- * Typically useful for declaring Native and Alias terms.
117
- * @protected
116
+ * Typically applied to named terms shortly after instantiation.
117
+ *
118
+ * Experimental. Name and meaning may change in the future.
119
+ *
120
+ * @experimental
118
121
  * @param {Object} options
119
122
  * @param {string} [options.note] - a brief description what the term does
120
123
  * @param {number} [options.arity] - number of arguments the term is waiting for (if known)
@@ -124,7 +127,7 @@ var Expr = class _Expr {
124
127
  * @param {number} [options.maxArgs] - maximum number of arguments for inference, if canonize is true
125
128
  * @return {this}
126
129
  */
127
- _setup(options = {}) {
130
+ annotate(options = {}) {
128
131
  if (options.fancy !== void 0 && this instanceof Named)
129
132
  this.fancyName = options.fancy;
130
133
  if (options.note !== void 0)
@@ -428,7 +431,9 @@ var Expr = class _Expr {
428
431
  */
429
432
  *toLambda(options = {}) {
430
433
  let expr = this.traverse((e) => {
431
- if (e instanceof FreeVar || e instanceof App || e instanceof Lambda || e instanceof Alias)
434
+ if (e instanceof FreeVar)
435
+ return e;
436
+ if (e instanceof App || e instanceof Lambda || e instanceof Alias)
432
437
  return null;
433
438
  const guess = e.infer({ max: options.max, maxArgs: options.maxArgs });
434
439
  if (!guess.normal)
@@ -794,16 +799,26 @@ var Expr = class _Expr {
794
799
  diag(indent = "") {
795
800
  return indent + this.constructor.name + ": " + this;
796
801
  }
802
+ declare(options = {}) {
803
+ const { declaration: d = ["", "=", "; "], ...format } = options;
804
+ const res = toposort({ list: [this], env: format.inventory });
805
+ return res.list.map((s) => {
806
+ if (s instanceof Alias)
807
+ return d[0] + s.name + d[1] + s.impl.format({ ...format, inventory: res.env });
808
+ if (s instanceof FreeVar)
809
+ return d[0] + s.name + d[1];
810
+ return s.format({ ...format, inventory: res.env });
811
+ }).join(d[2]);
812
+ }
797
813
  /**
798
814
  * Convert the expression to a JSON-serializable format.
799
815
  * Sadly the format is not yet finalized and may change in the future.
800
816
  *
801
817
  * @experimental
802
- * @returns {string}
803
818
  * @sealed
804
819
  */
805
820
  toJSON() {
806
- return this.format();
821
+ return this.declare();
807
822
  }
808
823
  };
809
824
  var App = class _App extends Expr {
@@ -970,7 +985,7 @@ var Native = class extends Named {
970
985
  constructor(name, impl, opt = {}) {
971
986
  super(name);
972
987
  this.invoke = impl;
973
- this._setup({ canonize: true, ...opt });
988
+ this.annotate({ canonize: true, ...opt });
974
989
  }
975
990
  };
976
991
  var Lambda = class _Lambda extends Expr {
@@ -1077,7 +1092,7 @@ var Alias = class extends Named {
1077
1092
  if (!(impl instanceof Expr))
1078
1093
  throw new Error("Attempt to create an alias for a non-expression: " + impl);
1079
1094
  this.impl = impl;
1080
- this._setup(options);
1095
+ this.annotate(options);
1081
1096
  this.invoke = waitn(options.inline ? 0 : this.arity ?? 0)(impl);
1082
1097
  this.size = impl.size;
1083
1098
  if (options.inline)
@@ -1215,40 +1230,43 @@ function maybeLambda(args, expr, caps) {
1215
1230
  function nthvar(n) {
1216
1231
  return new FreeVar("abcdefgh"[n] ?? "x" + n);
1217
1232
  }
1218
- var classes = { Expr, App, Named, FreeVar, Native, Lambda, Church, Alias };
1219
-
1220
- // src/toposort.ts
1221
- function toposort(list, env) {
1222
- if (list instanceof Expr)
1223
- list = [list];
1224
- if (env) {
1225
- if (!list)
1226
- list = Object.keys(env).sort().map((k) => env[k]);
1227
- } else {
1228
- if (!list)
1229
- return { list: [], env: {} };
1230
- env = {};
1231
- for (const item of list) {
1232
- if (!(item instanceof Named))
1233
- continue;
1234
- if (env[item.name])
1235
- throw new Error("duplicate name " + item);
1236
- env[item.name] = item;
1233
+ function toposort(options) {
1234
+ if (typeof options !== "object" || options === null || Array.isArray(options) || options instanceof Expr)
1235
+ throw new Error("positional arguments to toposort are deprecated, use { list: ..., env: ... } instead");
1236
+ const allow = options.allow ? { ...options.allow } : null;
1237
+ const env = { ...options.env ?? {} };
1238
+ const list = options.list instanceof Expr ? [options.list] : options.list ?? [];
1239
+ if (allow) {
1240
+ for (const term of list) {
1241
+ if (term instanceof Named)
1242
+ allow[term.name] = term;
1237
1243
  }
1238
1244
  }
1245
+ for (const term of list) {
1246
+ if (term instanceof Named && env[term.name] === term)
1247
+ delete env[term.name];
1248
+ }
1239
1249
  const out = [];
1240
- const seen = /* @__PURE__ */ new Set();
1250
+ const seen = new Set(Object.values(env));
1241
1251
  const rec = (term) => {
1242
1252
  if (seen.has(term))
1243
1253
  return;
1244
- term.fold(false, (acc, e) => {
1245
- if (e !== term && e instanceof Named && env[e.name] === e) {
1254
+ term.fold(void 0, (_, e) => {
1255
+ if (!(e instanceof Named))
1256
+ return;
1257
+ if (allow && allow[e.name] !== e)
1258
+ return;
1259
+ if (!allow && e instanceof Alias && e.inline)
1260
+ return;
1261
+ if (e !== term) {
1246
1262
  rec(e);
1247
- return control.prune(false);
1263
+ return control.prune();
1248
1264
  }
1249
1265
  });
1250
1266
  out.push(term);
1251
1267
  seen.add(term);
1268
+ if (term instanceof Named)
1269
+ env[term.name] ||= term;
1252
1270
  };
1253
1271
  for (const term of list)
1254
1272
  rec(term);
@@ -1257,6 +1275,7 @@ function toposort(list, env) {
1257
1275
  env
1258
1276
  };
1259
1277
  }
1278
+ var classes = { Expr, App, Named, FreeVar, Native, Lambda, Church, Alias };
1260
1279
 
1261
1280
  // src/parser.ts
1262
1281
  var Empty = class extends Expr {
@@ -1359,7 +1378,7 @@ var Parser = class {
1359
1378
  add(term, impl, options) {
1360
1379
  const named = this._named(term, impl);
1361
1380
  const opts = typeof options === "string" ? { note: options, canonize: false } : options ?? {};
1362
- named._setup({ canonize: this.annotate, ...opts });
1381
+ named.annotate({ canonize: this.annotate, ...opts });
1363
1382
  const old = this.known[named.name];
1364
1383
  if (old instanceof Alias)
1365
1384
  old.makeInline();
@@ -1504,7 +1523,7 @@ var Parser = class {
1504
1523
  env[temp.name] = temp;
1505
1524
  delete env[name];
1506
1525
  }
1507
- const list = toposort(void 0, env).list;
1526
+ const list = toposort({ list: Object.values(env), allow: {} }).list;
1508
1527
  const detour = /* @__PURE__ */ new Map();
1509
1528
  if (Object.keys(needDetour).length) {
1510
1529
  const rework = (expr) => {
@@ -1531,14 +1550,15 @@ var Parser = class {
1531
1550
  return out;
1532
1551
  }
1533
1552
  /**
1534
- * @template T
1535
1553
  * @param {string} source
1536
1554
  * @param {Object} [options]
1537
- * @param {{[keys: string]: Expr}} [options.env]
1538
- * @param {T} [options.scope]
1539
- * @param {boolean} [options.numbers]
1540
- * @param {boolean} [options.lambdas]
1541
- * @param {string} [options.allow]
1555
+ * @param [options.env] - additional
1556
+ * @param [options.scope] - assign this scope to unknown free variables
1557
+ * @param {boolean} [options.numbers] - whether numbers are allowed
1558
+ * @param {boolean} [options.lambdas] - whether lambdas are allowed
1559
+ * @param {string} [options.allow] - restrict known terms
1560
+ * @param [options.canonize] - whether to calculate canonical form, arity, and properties
1561
+ * of intermediate aliases
1542
1562
  * @return {Expr}
1543
1563
  */
1544
1564
  parse(source, options = {}) {
@@ -1548,18 +1568,19 @@ var Parser = class {
1548
1568
  const jar = { ...options.env };
1549
1569
  let expr = new Empty();
1550
1570
  for (const item of lines) {
1551
- if (expr instanceof Alias)
1552
- expr.makeInline();
1553
- const def = item.match(/^([A-Z]|[a-z][a-z_0-9]*)\s*=(.*)$/s);
1554
- if (def && def[2] === "")
1555
- expr = new FreeVar(def[1], options.scope ?? FreeVar.global);
1571
+ const [_, name, def] = item.match(/^([A-Z]|[a-z][a-z_0-9]*)\s*=(.*)$/s) || [];
1572
+ if (name !== void 0) {
1573
+ if (jar[name] instanceof Alias && jar[name] !== options.env?.[name]) {
1574
+ jar[name].makeInline();
1575
+ }
1576
+ delete jar[name];
1577
+ }
1578
+ if (def === "")
1579
+ expr = new FreeVar(name, options.scope ?? FreeVar.global);
1556
1580
  else
1557
1581
  expr = this.parseLine(item, jar, options);
1558
- if (def) {
1559
- if (jar[def[1]] !== void 0)
1560
- throw new Error("Attempt to redefine a known term: " + def[1]);
1561
- jar[def[1]] = expr;
1562
- }
1582
+ if (name)
1583
+ jar[name] = expr;
1563
1584
  }
1564
1585
  if (this.addContext) {
1565
1586
  if (expr instanceof Named)
@@ -1591,7 +1612,7 @@ var Parser = class {
1591
1612
  parseLine(source, env = {}, options = {}) {
1592
1613
  const aliased = source.match(/^\s*([A-Z]|[a-z][a-z_0-9]*)\s*=\s*(.*)$/s);
1593
1614
  if (aliased)
1594
- return new Alias(aliased[1], this.parseLine(aliased[2], env, options));
1615
+ return new Alias(aliased[1], this.parseLine(aliased[2], env, options), { canonize: options.canonize });
1595
1616
  const opt = {
1596
1617
  numbers: options.numbers ?? this.hasNumbers,
1597
1618
  lambdas: options.lambdas ?? this.hasLambdas,
@@ -2145,15 +2166,8 @@ function deepFormat(obj, options = {}) {
2145
2166
  out[key] = deepFormat(obj[key], options);
2146
2167
  return out;
2147
2168
  }
2148
- function declare(expr, env = {}) {
2149
- const res = toposort([expr], env);
2150
- return res.list.map((s) => {
2151
- if (s instanceof Alias)
2152
- return s.name + "=" + s.impl.format({ inventory: res.env });
2153
- if (s instanceof FreeVar)
2154
- return s.name + "=";
2155
- return s.format({ inventory: res.env });
2156
- }).join("; ");
2169
+ function declare(expr, env) {
2170
+ return expr.declare({ inventory: env });
2157
2171
  }
2158
2172
  var isStringPair = (x) => Array.isArray(x) && x.length === 2 && typeof x[0] === "string" && typeof x[1] === "string" ? void 0 : "must be a pair of strings";
2159
2173
  var isStringTriple = (x) => Array.isArray(x) && x.length === 3 && typeof x[0] === "string" && typeof x[1] === "string" && typeof x[2] === "string" ? void 0 : "must be a triplet of strings";