@dallaylaen/ski-interpreter 2.8.0 → 2.8.2
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 +22 -0
- package/bin/ski.js +17 -0
- package/example/collatz.ski +30 -0
- package/example/eval.js +34 -0
- package/example/example-quests.json +83 -0
- package/example/mini-quest.html +42 -0
- package/example/reverse-linked-list.ski +14 -0
- package/example/t-foo-bar.ski +5 -0
- package/lib/ski-interpreter.cjs.js +78 -61
- package/lib/ski-interpreter.cjs.js.map +2 -2
- package/lib/ski-interpreter.min.js +5 -5
- package/lib/ski-interpreter.min.js.map +3 -3
- package/lib/ski-interpreter.mjs +78 -61
- package/lib/ski-interpreter.mjs.map +2 -2
- package/lib/ski-quest.min.js +4 -4
- package/lib/ski-quest.min.js.map +3 -3
- package/lib/types/extras.d.ts +37 -13
- package/lib/types/index.d.ts +1 -0
- package/package.json +2 -1
- package/lib/types/toposort.d.ts +0 -29
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ 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.2] - 2026-04-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `examples/` are now included in the package
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- removed stale .d.ts file from lib/
|
|
17
|
+
|
|
18
|
+
## [2.8.1] - 2026-04-26
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `SKI.extras.equiv(expr1, expr2, options: InferOptions?)` to check if two expressions are computationally equivalent.
|
|
23
|
+
- `./bin/ski.js compare <expr1> <expr2>` command to compare two expressions for equivalence.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `Expr.infer()` keeps adding variables to nonterminating expressions
|
|
28
|
+
to catch e.g. `CK(WWW)` (never terminates but eqivalent to I).
|
|
29
|
+
|
|
8
30
|
## [2.8.0] - 2026-04-23
|
|
9
31
|
|
|
10
32
|
### BREAKING CHANGES
|
package/bin/ski.js
CHANGED
|
@@ -71,6 +71,23 @@ program
|
|
|
71
71
|
inferExpression(expression);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
program
|
|
75
|
+
.command('compare <expr1> <expr2>')
|
|
76
|
+
.description('Check if two expressions are equivalent')
|
|
77
|
+
.action((expr1, expr2) => {
|
|
78
|
+
const ski = new SKI();
|
|
79
|
+
const e1 = ski.parse(expr1);
|
|
80
|
+
const e2 = ski.parse(expr2);
|
|
81
|
+
const res = SKI.extras.equiv(e1, e2, runOptions);
|
|
82
|
+
if (res.equal)
|
|
83
|
+
console.log('Both expressions are equivalent to ' + res.canonical[0].format(format));
|
|
84
|
+
else
|
|
85
|
+
console.log(`Expressions differ:\n${res.canonical[0].format(format)}\n vs \n${res.canonical[1].format(format)}`);
|
|
86
|
+
|
|
87
|
+
console.log(`// ${res.steps} step(s)`);
|
|
88
|
+
process.exit(res.equal ? 0 : 1);
|
|
89
|
+
});
|
|
90
|
+
|
|
74
91
|
// Extract subcommand
|
|
75
92
|
program
|
|
76
93
|
.command('extract <target> <terms...>')
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// This file generates a trace of a Collatz 3n+1 sequence,
|
|
2
|
+
// producing HUGE number of steps with very deep recursion,
|
|
3
|
+
// and can thus be used as a rough performance benchmark.
|
|
4
|
+
|
|
5
|
+
// `calc f n` == `f n`, provided n is a number, except that `n` is
|
|
6
|
+
// forced into a redex position before being fed to `f`,
|
|
7
|
+
// thus eliminating possible duplicate computations.
|
|
8
|
+
|
|
9
|
+
T=CI;
|
|
10
|
+
V=BCT;
|
|
11
|
+
calc=BBB(T 0)(V(CB+));
|
|
12
|
+
if=BBB(B(BW)C)C;
|
|
13
|
+
half=C(V(C(V(T(KI))(C(B+)K)))(K 0))K;
|
|
14
|
+
collatz=if(VCK)half(B(V+ 1)(B 3));
|
|
15
|
+
dec=BBBCV(C(BT)+)(K 0)I;
|
|
16
|
+
M=WI;
|
|
17
|
+
wait4=BBB(BCC)(BCC);
|
|
18
|
+
nil=KI;
|
|
19
|
+
lst=BS(C(BB));
|
|
20
|
+
fold=M(B(SI)(wait4M))(B(C(BV(BBBK))nil)(C(BBlst)));
|
|
21
|
+
L=BWB;
|
|
22
|
+
lazyy=M(B(SI)(wait4M));
|
|
23
|
+
lesseq=C(BB(VT(KK)))(VT(K(KI)));
|
|
24
|
+
P=M(C(BL(WC)));
|
|
25
|
+
R=CC;
|
|
26
|
+
v3=BBBCV;
|
|
27
|
+
|
|
28
|
+
BML(B(S(B(BB(B(Bcalc(SV))))(Cif (Knil)))) (B(S(CB)))) (C lesseq 1) (collatz) 127
|
|
29
|
+
// BML(B(S(B(BB(B((SV))))(Cif (Knil)))) (B(S(CB)))) (C lesseq 1) (collatz) 27
|
|
30
|
+
|
package/example/eval.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A minimal example of evaluating a SKI expression.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { SKI } = require('../lib/ski-interpreter.cjs');
|
|
10
|
+
// SKI is the only export and contains other goodies as subkeys
|
|
11
|
+
|
|
12
|
+
const [node, path, src] = process.argv;
|
|
13
|
+
|
|
14
|
+
if (!src) {
|
|
15
|
+
console.log(`Usage: ${node} ${path} <src>`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ski = new SKI();
|
|
20
|
+
|
|
21
|
+
// add some extra terms
|
|
22
|
+
ski.add('M', 'SII'); // an alias
|
|
23
|
+
ski.add('L', x => y => x.apply(y.apply(y))) // ditto but with a 'Native' js impl
|
|
24
|
+
|
|
25
|
+
const expr = ski.parse(src);
|
|
26
|
+
|
|
27
|
+
// expr.step() = 1 reduction, expr.run() = reduce to the end
|
|
28
|
+
// expr.walk() = an iterator
|
|
29
|
+
// all three return an object with { steps, expr, final } where:
|
|
30
|
+
// - steps = the number of reductions performed so far
|
|
31
|
+
// - expr = the resulting expression
|
|
32
|
+
// - final = boolean indicating if the expression is in normal form (no more reductions possible)
|
|
33
|
+
for (const step of expr.walk())
|
|
34
|
+
console.log(`[${step.steps}] ${step.expr}`);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "dull6XUf",
|
|
3
|
+
"created_at": "2026-02-27T00:00:00",
|
|
4
|
+
"name": "Example quests",
|
|
5
|
+
"intro": "<p>A small template collection of combinatory logic exercises. Feel free to use this file as a starting point for your own quest chapter.</p>",
|
|
6
|
+
"content": [
|
|
7
|
+
{
|
|
8
|
+
"id": "YpgCLTl8",
|
|
9
|
+
"created_at": "2026-02-27T00:00:00",
|
|
10
|
+
"name": "I from SK",
|
|
11
|
+
"intro": [
|
|
12
|
+
"<p>The identity combinator <code>I</code> satisfies <code>I x = x</code>.",
|
|
13
|
+
"It can actually be derived from <code>S</code> and <code>K</code> alone —",
|
|
14
|
+
"no built-in <code>I</code> needed.</p>",
|
|
15
|
+
"<p>Find a term built only from <code>S</code> and <code>K</code> that behaves like <code>I</code>.</p>"
|
|
16
|
+
],
|
|
17
|
+
"hint": "Think about what <code>S K K x</code> reduces to.",
|
|
18
|
+
"allow": "SK",
|
|
19
|
+
"input": "phi",
|
|
20
|
+
"cases": [
|
|
21
|
+
[ "phi x", "x" ],
|
|
22
|
+
[ "phi (K x y)", "K x y" ]
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "vQsBd85P",
|
|
27
|
+
"created_at": "2026-02-27T00:00:00",
|
|
28
|
+
"name": "NAND for K/KI",
|
|
29
|
+
"intro": [
|
|
30
|
+
"<p>Using the Church encoding, <code>K</code> represents <b>true</b> and <code>KI</code> represents <b>false</b>.</p>",
|
|
31
|
+
"<p>Implement <code>nand</code>: it should return <code>K</code> (true) for every pair of inputs",
|
|
32
|
+
"except when <i>both</i> are <code>K</code> (true), in which case it returns <code>KI</code> (false).</p>",
|
|
33
|
+
"<p>Apply the result to <code>true</code> and <code>false</code> witnesses to observe it.</p>"
|
|
34
|
+
],
|
|
35
|
+
"input": "phi",
|
|
36
|
+
"cases": [
|
|
37
|
+
[ "phi K K true false", "false" ],
|
|
38
|
+
[ "phi (KI) K true false", "true" ],
|
|
39
|
+
[ "phi K (KI) true false", "true" ],
|
|
40
|
+
[ "phi (KI) (KI) true false", "true" ]
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "IgVco9YW",
|
|
45
|
+
"created_at": "2026-02-27T00:00:00",
|
|
46
|
+
"name": "Linear basis from I and your own combinator",
|
|
47
|
+
"intro": [
|
|
48
|
+
"<p>It can be shown that <code>I</code>, <code>B</code>, and <code>C</code> (or <code>T</code>)",
|
|
49
|
+
"form a complete basis for <i>linear</i> combinators — those that use every argument",
|
|
50
|
+
"exactly once.</p>",
|
|
51
|
+
"<p>Surprisingly, <code>B</code> and <code>C</code> can be merged into a single combinator.",
|
|
52
|
+
"Your task is to <i>declare</i> such a combinator <code>P</code> (lambdas are allowed here),",
|
|
53
|
+
"and then <i>prove</i> the basis is complete by implementing <code>B</code> and <code>T</code>",
|
|
54
|
+
"using only <code>I</code> and your <code>P</code>.</p>"
|
|
55
|
+
],
|
|
56
|
+
"input": [
|
|
57
|
+
{
|
|
58
|
+
"name": "P",
|
|
59
|
+
"lambdas": true,
|
|
60
|
+
"allow": "I-I",
|
|
61
|
+
"note": "Declare your own linear combinator (lambdas allowed, no built-ins)."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "B",
|
|
65
|
+
"lambdas": false,
|
|
66
|
+
"allow": "I",
|
|
67
|
+
"note": "Implement <code>B a b c = a (b c)</code> using <code>P</code> and <code>I</code>."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "T",
|
|
71
|
+
"lambdas": false,
|
|
72
|
+
"allow": "I",
|
|
73
|
+
"note": "Implement <code>T a b = b a</code> using <code>P</code> and <code>I</code>."
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"cases": [
|
|
77
|
+
[ { "caps": { "linear": true } }, "P" ],
|
|
78
|
+
[ "B a b c", "a (b c)" ],
|
|
79
|
+
[ "T a b", "b a" ]
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"/>
|
|
5
|
+
<title>Example Quests - Simple Kombinator Interpreter</title>
|
|
6
|
+
<script src="../docs/build/js/ski-quest.min.js"></script>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: sans-serif;
|
|
10
|
+
max-width: 860px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 1em;
|
|
13
|
+
}
|
|
14
|
+
#inventory {
|
|
15
|
+
border: 1px solid #ccc;
|
|
16
|
+
padding: 0.5em 1em;
|
|
17
|
+
margin-bottom: 1em;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<h1>Example Quests</h1>
|
|
23
|
+
|
|
24
|
+
<div id="inventory">
|
|
25
|
+
<b>Inventory</b>
|
|
26
|
+
<ul id="known"></ul>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div id="quest"></div>
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
const page = new QuestPage({
|
|
33
|
+
storePrefix: 'example-quests',
|
|
34
|
+
baseUrl: '.',
|
|
35
|
+
contentBox: document.getElementById('quest'),
|
|
36
|
+
inventoryBox: document.getElementById('known'),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
page.loadChapters(['example-quests.json']);
|
|
40
|
+
</script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -357,11 +357,9 @@ var Expr = class _Expr {
|
|
|
357
357
|
let steps = 0;
|
|
358
358
|
let expr = this;
|
|
359
359
|
main: for (let i = 0; i < options.maxArgs; i++) {
|
|
360
|
-
const next = expr.run({ max: options.max - steps, maxSize: options.maxSize });
|
|
360
|
+
const next = expr.run({ max: Math.max((options.max - steps) / 2, 10), maxSize: options.maxSize });
|
|
361
361
|
steps += next.steps;
|
|
362
|
-
if (
|
|
363
|
-
break;
|
|
364
|
-
if (firstVar(next.expr)) {
|
|
362
|
+
if (next.final && firstVar(next.expr)) {
|
|
365
363
|
expr = next.expr;
|
|
366
364
|
if (!expr.any((e) => !(e instanceof FreeVar || e instanceof App)))
|
|
367
365
|
return maybeLambda(probe, expr, { steps });
|
|
@@ -2087,6 +2085,77 @@ function canonize(term, options = {}) {
|
|
|
2087
2085
|
}
|
|
2088
2086
|
|
|
2089
2087
|
// src/extras.ts
|
|
2088
|
+
var formatSchema = {
|
|
2089
|
+
html: (x) => typeof x === "boolean" ? void 0 : "must be a boolean",
|
|
2090
|
+
terse: (x) => typeof x === "boolean" ? void 0 : "must be a boolean",
|
|
2091
|
+
space: (x) => typeof x === "string" ? void 0 : "must be a string",
|
|
2092
|
+
brackets: isStringPair,
|
|
2093
|
+
var: isStringPair,
|
|
2094
|
+
around: isStringPair,
|
|
2095
|
+
redex: isStringPair,
|
|
2096
|
+
lambda: isStringTriple,
|
|
2097
|
+
inventory: (x) => {
|
|
2098
|
+
if (typeof x !== "object" || x === null || x.constructor !== Object)
|
|
2099
|
+
return "must be an object, not " + (x?.constructor?.name ?? typeof x);
|
|
2100
|
+
const refined = x;
|
|
2101
|
+
for (const key of Object.keys(refined)) {
|
|
2102
|
+
if (!(refined[key] instanceof Expr))
|
|
2103
|
+
return "key " + key + "is not an Expr";
|
|
2104
|
+
}
|
|
2105
|
+
return void 0;
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
function checkFormatOptions(raw) {
|
|
2109
|
+
if (raw === null || raw === void 0)
|
|
2110
|
+
return { value: {} };
|
|
2111
|
+
if (typeof raw !== "object" || Array.isArray(raw) || raw.constructor !== Object)
|
|
2112
|
+
return { error: { object: "Format options must be an object, not " + (raw?.constructor?.name ?? typeof raw) } };
|
|
2113
|
+
const rec = raw;
|
|
2114
|
+
const error = {};
|
|
2115
|
+
for (const key in rec) {
|
|
2116
|
+
if (formatSchema[key]) {
|
|
2117
|
+
const err = formatSchema[key](rec[key]);
|
|
2118
|
+
if (err)
|
|
2119
|
+
error[key] = err;
|
|
2120
|
+
} else
|
|
2121
|
+
error[key] = "unknown option";
|
|
2122
|
+
}
|
|
2123
|
+
return Object.keys(error).length > 0 ? { error } : { value: rec };
|
|
2124
|
+
}
|
|
2125
|
+
function equiv(e1, e2, options = {}) {
|
|
2126
|
+
let steps = 0;
|
|
2127
|
+
const [n1, n2] = [e1, e2].map((x) => x.traverse((e) => {
|
|
2128
|
+
const props = e.infer(options);
|
|
2129
|
+
steps += props.steps ?? 0;
|
|
2130
|
+
return props.expr;
|
|
2131
|
+
}));
|
|
2132
|
+
const normal = !!(n1 && n2);
|
|
2133
|
+
return {
|
|
2134
|
+
steps,
|
|
2135
|
+
normal,
|
|
2136
|
+
equal: normal ? n1.equals(n2) : false,
|
|
2137
|
+
canonical: [n1, n2]
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
function declare(expr, env) {
|
|
2141
|
+
return expr.declare({ inventory: env });
|
|
2142
|
+
}
|
|
2143
|
+
function deepFormat(obj, options = {}) {
|
|
2144
|
+
if (obj instanceof Expr)
|
|
2145
|
+
return obj.format(options);
|
|
2146
|
+
if (obj instanceof Quest)
|
|
2147
|
+
return "Quest(" + obj.name + ")";
|
|
2148
|
+
if (obj instanceof Quest.Case)
|
|
2149
|
+
return "Quest.Case";
|
|
2150
|
+
if (Array.isArray(obj))
|
|
2151
|
+
return obj.map((item) => deepFormat(item, options));
|
|
2152
|
+
if (typeof obj !== "object" || obj === null || obj.constructor !== Object)
|
|
2153
|
+
return obj;
|
|
2154
|
+
const out = {};
|
|
2155
|
+
for (const key in obj)
|
|
2156
|
+
out[key] = deepFormat(obj[key], options);
|
|
2157
|
+
return out;
|
|
2158
|
+
}
|
|
2090
2159
|
function search(seed, options, predicate) {
|
|
2091
2160
|
const {
|
|
2092
2161
|
depth = 16,
|
|
@@ -2150,65 +2219,13 @@ function search(seed, options, predicate) {
|
|
|
2150
2219
|
}
|
|
2151
2220
|
return { total, probed, gen: depth, ...options.retain ? { cache } : {} };
|
|
2152
2221
|
}
|
|
2153
|
-
function
|
|
2154
|
-
|
|
2155
|
-
return obj.format(options);
|
|
2156
|
-
if (obj instanceof Quest)
|
|
2157
|
-
return "Quest(" + obj.name + ")";
|
|
2158
|
-
if (obj instanceof Quest.Case)
|
|
2159
|
-
return "Quest.Case";
|
|
2160
|
-
if (Array.isArray(obj))
|
|
2161
|
-
return obj.map((item) => deepFormat(item, options));
|
|
2162
|
-
if (typeof obj !== "object" || obj === null || obj.constructor !== Object)
|
|
2163
|
-
return obj;
|
|
2164
|
-
const out = {};
|
|
2165
|
-
for (const key in obj)
|
|
2166
|
-
out[key] = deepFormat(obj[key], options);
|
|
2167
|
-
return out;
|
|
2168
|
-
}
|
|
2169
|
-
function declare(expr, env) {
|
|
2170
|
-
return expr.declare({ inventory: env });
|
|
2222
|
+
function isStringPair(x) {
|
|
2223
|
+
return Array.isArray(x) && x.length === 2 && typeof x[0] === "string" && typeof x[1] === "string" ? void 0 : "must be a pair of strings";
|
|
2171
2224
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
var schema = {
|
|
2175
|
-
html: (x) => typeof x === "boolean" ? void 0 : "must be a boolean",
|
|
2176
|
-
terse: (x) => typeof x === "boolean" ? void 0 : "must be a boolean",
|
|
2177
|
-
space: (x) => typeof x === "string" ? void 0 : "must be a string",
|
|
2178
|
-
brackets: isStringPair,
|
|
2179
|
-
var: isStringPair,
|
|
2180
|
-
around: isStringPair,
|
|
2181
|
-
redex: isStringPair,
|
|
2182
|
-
lambda: isStringTriple,
|
|
2183
|
-
inventory: (x) => {
|
|
2184
|
-
if (typeof x !== "object" || x === null || x.constructor !== Object)
|
|
2185
|
-
return "must be an object, not " + (x?.constructor?.name ?? typeof x);
|
|
2186
|
-
const refined = x;
|
|
2187
|
-
for (const key of Object.keys(refined)) {
|
|
2188
|
-
if (!(refined[key] instanceof Expr))
|
|
2189
|
-
return "key " + key + "is not an Expr";
|
|
2190
|
-
}
|
|
2191
|
-
return void 0;
|
|
2192
|
-
}
|
|
2193
|
-
};
|
|
2194
|
-
function checkFormatOptions(raw) {
|
|
2195
|
-
if (raw === null || raw === void 0)
|
|
2196
|
-
return { value: {} };
|
|
2197
|
-
if (typeof raw !== "object" || Array.isArray(raw) || raw.constructor !== Object)
|
|
2198
|
-
return { error: { object: "Format options must be an object, not " + (raw?.constructor?.name ?? typeof raw) } };
|
|
2199
|
-
const rec = raw;
|
|
2200
|
-
const error = {};
|
|
2201
|
-
for (const key in rec) {
|
|
2202
|
-
if (schema[key]) {
|
|
2203
|
-
const err = schema[key](rec[key]);
|
|
2204
|
-
if (err)
|
|
2205
|
-
error[key] = err;
|
|
2206
|
-
} else
|
|
2207
|
-
error[key] = "unknown option";
|
|
2208
|
-
}
|
|
2209
|
-
return Object.keys(error).length > 0 ? { error } : { value: rec };
|
|
2225
|
+
function isStringTriple(x) {
|
|
2226
|
+
return 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";
|
|
2210
2227
|
}
|
|
2211
|
-
var extras = { search, deepFormat, declare, toposort, checkFormatOptions };
|
|
2228
|
+
var extras = { search, deepFormat, declare, toposort, checkFormatOptions, equiv };
|
|
2212
2229
|
|
|
2213
2230
|
// src/index.ts
|
|
2214
2231
|
extras.toposort = toposort;
|