@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 +27 -0
- package/bin/ski.js +7 -10
- package/lib/ski-interpreter.cjs.js +74 -60
- package/lib/ski-interpreter.cjs.js.map +3 -3
- package/lib/ski-interpreter.min.js +5 -5
- package/lib/ski-interpreter.min.js.map +4 -4
- package/lib/ski-interpreter.mjs +74 -60
- package/lib/ski-interpreter.mjs.map +3 -3
- package/lib/ski-quest.min.js +5 -5
- package/lib/ski-quest.min.js.map +4 -4
- package/lib/types/expr.d.ts +33 -6
- package/lib/types/extras.d.ts +4 -10
- package/lib/types/index.d.ts +2 -5
- package/lib/types/parser.d.ts +11 -13
- package/package.json +1 -1
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
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
if (
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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 =
|
|
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(
|
|
1245
|
-
if (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(
|
|
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.
|
|
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(
|
|
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
|
|
1538
|
-
* @param
|
|
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
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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 (
|
|
1559
|
-
|
|
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
|
-
|
|
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";
|