@dallaylaen/ski-interpreter 2.0.0 → 2.2.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 +43 -0
- package/README.md +1 -1
- package/bin/ski.js +96 -97
- package/lib/ski-interpreter.cjs.js +2047 -0
- package/lib/ski-interpreter.cjs.js.map +7 -0
- package/lib/ski-interpreter.esm.js +2052 -0
- package/lib/ski-interpreter.esm.js.map +7 -0
- package/package.json +13 -6
- package/types/index.d.ts +3 -7
- package/types/{lib → src}/expr.d.ts +146 -48
- package/types/src/extras.d.ts +58 -0
- package/types/src/internal.d.ts +52 -0
- package/types/{lib → src}/parser.d.ts +11 -4
- package/types/{lib → src}/quest.d.ts +56 -39
- package/index.js +0 -8
- package/lib/expr.js +0 -1316
- package/lib/parser.js +0 -418
- package/lib/quest.js +0 -401
- package/lib/util.js +0 -57
- package/types/lib/util.d.ts +0 -11
package/lib/quest.js
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
const { SKI } = require('./parser');
|
|
2
|
-
const { Expr, FreeVar, Alias, Lambda } = SKI.classes;
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {{
|
|
6
|
-
* pass: boolean,
|
|
7
|
-
* reason: string?,
|
|
8
|
-
* steps: number,
|
|
9
|
-
* start: Expr,
|
|
10
|
-
* found: Expr,
|
|
11
|
-
* expected: Expr,
|
|
12
|
-
* note: string?,
|
|
13
|
-
* args: Expr[],
|
|
14
|
-
* case: Case
|
|
15
|
-
* }} CaseResult
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @typedef {{
|
|
20
|
-
* linear: boolean?,
|
|
21
|
-
* affine: boolean?,
|
|
22
|
-
* normal: boolean?,
|
|
23
|
-
* proper: boolean?,
|
|
24
|
-
* discard: boolean?,
|
|
25
|
-
* duplicate: boolean?,
|
|
26
|
-
* arity: number?,
|
|
27
|
-
* }} Capability
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @typedef {
|
|
32
|
-
* [string, string]
|
|
33
|
-
* | [{max: number?}, string, string]
|
|
34
|
-
* | [{caps: Capability, max: number?}, string]
|
|
35
|
-
* } TestCase
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @typedef {{
|
|
40
|
-
* pass: boolean,
|
|
41
|
-
* details: CaseResult[],
|
|
42
|
-
* expr?: Expr,
|
|
43
|
-
* input: Expr[]|string[],
|
|
44
|
-
* exception?: Error,
|
|
45
|
-
* steps: number,
|
|
46
|
-
* weight?: number
|
|
47
|
-
* }} QuestResult
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
class Quest {
|
|
51
|
-
/**
|
|
52
|
-
* @description A combinator problem with a set of test cases for the proposed solution.
|
|
53
|
-
* @param {{
|
|
54
|
-
* title: string?,
|
|
55
|
-
* descr: string?,
|
|
56
|
-
* subst: string?,
|
|
57
|
-
* allow: string?,
|
|
58
|
-
* numbers: boolean?,
|
|
59
|
-
* vars: string[]?,
|
|
60
|
-
* engine: SKI?,
|
|
61
|
-
* engineFull: SKI?,
|
|
62
|
-
* cases: TestCase[],
|
|
63
|
-
* }} options
|
|
64
|
-
*/
|
|
65
|
-
constructor (options = {}) {
|
|
66
|
-
const { input, vars, cases, allow, numbers, lambdas, subst, engine, engineFull, ...meta } = options;
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
this.engine = engine ?? new SKI();
|
|
70
|
-
this.engineFull = engineFull ?? new SKI();
|
|
71
|
-
this.restrict = { allow, numbers: numbers ?? false, lambdas: lambdas ?? false };
|
|
72
|
-
this.vars = {};
|
|
73
|
-
this.subst = Array.isArray(subst) ? subst : [subst ?? 'phi'];
|
|
74
|
-
|
|
75
|
-
const jar = {};
|
|
76
|
-
|
|
77
|
-
// options.vars is a list of expressions.
|
|
78
|
-
// we suck all free variables + all term declarations from there into this.vars
|
|
79
|
-
// to feed it later to every case's parser.
|
|
80
|
-
for (const term of vars ?? []) {
|
|
81
|
-
const expr = this.engineFull.parse(term, { env: jar, scope: this });
|
|
82
|
-
if (expr instanceof SKI.classes.Alias)
|
|
83
|
-
this.vars[expr.name] = new Alias(expr.name, expr.impl, { terminal: true, canonize: false });
|
|
84
|
-
// Canonized aliases won't expand with insufficient arguments,
|
|
85
|
-
// causing correct solutions to fail, so alas...
|
|
86
|
-
else if (expr instanceof SKI.classes.FreeVar)
|
|
87
|
-
this.vars[expr.name] = expr;
|
|
88
|
-
else
|
|
89
|
-
throw new Error('Unsupported given variable type: ' + term);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
this.input = [];
|
|
93
|
-
for (const term of Array.isArray(input) ? input : [input])
|
|
94
|
-
this.addInput(term);
|
|
95
|
-
if (!this.input.length)
|
|
96
|
-
throw new Error('Quest needs at least one input placeholder');
|
|
97
|
-
if (subst)
|
|
98
|
-
this.input[0].fancy = this.subst[0];
|
|
99
|
-
|
|
100
|
-
this.varsFull = { ...this.vars, ...jar };
|
|
101
|
-
for (const term of this.input) {
|
|
102
|
-
if (term.name in this.varsFull)
|
|
103
|
-
throw new Error('input placeholder name is duplicated or clashes with vars: ' + term.name);
|
|
104
|
-
this.varsFull[term.name] = term.placeholder;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
this.cases = [];
|
|
108
|
-
this.title = meta.title;
|
|
109
|
-
meta.descr = list2str(meta.descr);
|
|
110
|
-
this.descr = meta.descr;
|
|
111
|
-
this.meta = meta;
|
|
112
|
-
|
|
113
|
-
for (const c of cases ?? [])
|
|
114
|
-
this.add(...c);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Display allowed terms based on what engine thinks of this.vars + this.restrict.allow
|
|
119
|
-
* @return {string}
|
|
120
|
-
*/
|
|
121
|
-
allowed () {
|
|
122
|
-
const allow = this.restrict.allow ?? '';
|
|
123
|
-
const vars = Object.keys(this.vars).sort();
|
|
124
|
-
// In case vars are present and restrictions aren't, don't clutter the output with all the known terms
|
|
125
|
-
return allow
|
|
126
|
-
? this.engine.showRestrict(allow + '+' + vars.join(' '))
|
|
127
|
-
: vars.map( s => '+' + s).join(' ');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
addInput (term) {
|
|
131
|
-
if (typeof term !== 'object')
|
|
132
|
-
term = { name: term };
|
|
133
|
-
if (typeof term.name !== 'string')
|
|
134
|
-
throw new Error("quest 'input' field must be a string or a {name: string, ...} object");
|
|
135
|
-
|
|
136
|
-
term.placeholder = new SKI.classes.FreeVar(term.name);
|
|
137
|
-
// TODO more checks
|
|
138
|
-
this.input.push(term);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
*
|
|
143
|
-
* @param {{} | string} opt
|
|
144
|
-
* @param {string} terms
|
|
145
|
-
* @return {Quest}
|
|
146
|
-
*/
|
|
147
|
-
add (opt, ...terms) {
|
|
148
|
-
if (typeof opt === 'string') {
|
|
149
|
-
terms.unshift(opt);
|
|
150
|
-
opt = {};
|
|
151
|
-
} else
|
|
152
|
-
opt = { ...opt };
|
|
153
|
-
|
|
154
|
-
opt.engine = opt.engine ?? this.engineFull;
|
|
155
|
-
opt.vars = opt.vars ?? this.varsFull;
|
|
156
|
-
|
|
157
|
-
const input = this.input.map( t => t.placeholder );
|
|
158
|
-
this.cases.push(
|
|
159
|
-
opt.caps
|
|
160
|
-
? new PropertyCase(input, opt, terms)
|
|
161
|
-
: new ExprCase(input, opt, terms)
|
|
162
|
-
);
|
|
163
|
-
return this;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* @description Statefully parse a list of strings into expressions or fancy aliases thereof.
|
|
168
|
-
* @param {string[]} input
|
|
169
|
-
* @return {{terms: Expr[], weight: number}}
|
|
170
|
-
*/
|
|
171
|
-
prepare (...input) {
|
|
172
|
-
if (input.length !== this.input.length)
|
|
173
|
-
throw new Error('Solutions provided ' + input.length + ' terms where ' + this.input.length + ' are expected');
|
|
174
|
-
|
|
175
|
-
let weight = 0;
|
|
176
|
-
const prepared = [];
|
|
177
|
-
const jar = { ...this.vars };
|
|
178
|
-
for (let i = 0; i < input.length; i++) {
|
|
179
|
-
const spec = this.input[i];
|
|
180
|
-
const impl = this.engine.parse(input[i], {
|
|
181
|
-
env: jar,
|
|
182
|
-
allow: spec.allow ?? this.restrict.allow,
|
|
183
|
-
numbers: spec.numbers ?? this.restrict.numbers,
|
|
184
|
-
lambdas: spec.lambdas ?? this.restrict.lambdas,
|
|
185
|
-
});
|
|
186
|
-
weight += impl.weight();
|
|
187
|
-
const expr = impl instanceof FreeVar
|
|
188
|
-
? impl
|
|
189
|
-
: new Alias(spec.fancy ?? spec.name, impl, { terminal: true, canonize: false });
|
|
190
|
-
jar[spec.name] = expr;
|
|
191
|
-
prepared.push(expr);
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
prepared,
|
|
195
|
-
weight,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
*
|
|
201
|
-
* @param {string} input
|
|
202
|
-
* @return {QuestResult}
|
|
203
|
-
*/
|
|
204
|
-
check (...input) {
|
|
205
|
-
try {
|
|
206
|
-
const { prepared, weight } = this.prepare(...input);
|
|
207
|
-
const details = this.cases.map( c => c.check(...prepared) );
|
|
208
|
-
const pass = details.reduce((acc, val) => acc && val.pass, true);
|
|
209
|
-
const steps = details.reduce((acc, val) => acc + val.steps, 0);
|
|
210
|
-
return {
|
|
211
|
-
expr: prepared[0],
|
|
212
|
-
input: prepared,
|
|
213
|
-
pass,
|
|
214
|
-
steps,
|
|
215
|
-
details,
|
|
216
|
-
weight,
|
|
217
|
-
};
|
|
218
|
-
} catch (e) {
|
|
219
|
-
return { pass: false, details: [], exception: e, steps: 0, input };
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
*
|
|
225
|
-
* @return {TestCase[]}
|
|
226
|
-
*/
|
|
227
|
-
show () {
|
|
228
|
-
return [...this.cases];
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
class Case {
|
|
233
|
-
/**
|
|
234
|
-
* @param {FreeVar[]} input
|
|
235
|
-
* @param {{
|
|
236
|
-
* max?: number,
|
|
237
|
-
* note?: string,
|
|
238
|
-
* vars?: {[key:string]: Expr},
|
|
239
|
-
* engine: SKI
|
|
240
|
-
* }} options
|
|
241
|
-
*/
|
|
242
|
-
constructor (input, options) {
|
|
243
|
-
this.max = options.max ?? 1000;
|
|
244
|
-
this.note = options.note;
|
|
245
|
-
this.vars = { ...(options.vars ?? {}) }; // note: scope already contains input placeholders
|
|
246
|
-
this.input = input;
|
|
247
|
-
this.engine = options.engine;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
parse (src) {
|
|
251
|
-
return new Subst(this.engine.parse(src, { env: this.vars, scope: this }), this.input);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* @param {Expr} expr
|
|
256
|
-
* @return {CaseResult}
|
|
257
|
-
*/
|
|
258
|
-
check ( ...expr ) {
|
|
259
|
-
throw new Error('not implemented');
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
class ExprCase extends Case {
|
|
264
|
-
/**
|
|
265
|
-
* @param {FreeVar[]} input
|
|
266
|
-
* @param {{
|
|
267
|
-
* max: number?,
|
|
268
|
-
* note: string?,
|
|
269
|
-
* vars: {string: Expr}?,
|
|
270
|
-
* engine: SKI?
|
|
271
|
-
* }} options
|
|
272
|
-
* @param {[e1: string, e2: string]} terms
|
|
273
|
-
*/
|
|
274
|
-
constructor (input, options, terms) {
|
|
275
|
-
if (terms.length !== 2)
|
|
276
|
-
throw new Error('Case accepts exactly 2 strings');
|
|
277
|
-
|
|
278
|
-
super(input, options);
|
|
279
|
-
|
|
280
|
-
[this.e1, this.e2] = terms.map( s => this.parse(s) );
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
check (...args) {
|
|
284
|
-
const e1 = this.e1.apply(args);
|
|
285
|
-
const r1 = e1.run({ max: this.max });
|
|
286
|
-
const e2 = this.e2.apply(args);
|
|
287
|
-
const r2 = e2.run({ max: this.max });
|
|
288
|
-
|
|
289
|
-
let reason = null;
|
|
290
|
-
if (!r1.final || !r2.final)
|
|
291
|
-
reason = 'failed to reach normal form in ' + this.max + ' steps';
|
|
292
|
-
else
|
|
293
|
-
reason = r1.expr.diff(r2.expr);
|
|
294
|
-
|
|
295
|
-
return {
|
|
296
|
-
pass: !reason,
|
|
297
|
-
reason,
|
|
298
|
-
steps: r1.steps,
|
|
299
|
-
start: e1,
|
|
300
|
-
found: r1.expr,
|
|
301
|
-
expected: r2.expr,
|
|
302
|
-
note: this.note,
|
|
303
|
-
args,
|
|
304
|
-
case: this,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const knownCaps = {
|
|
310
|
-
normal: true,
|
|
311
|
-
proper: true,
|
|
312
|
-
discard: true,
|
|
313
|
-
duplicate: true,
|
|
314
|
-
linear: true,
|
|
315
|
-
affine: true,
|
|
316
|
-
arity: true,
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
class PropertyCase extends Case {
|
|
320
|
-
// test that an expression uses all of its inputs exactly once
|
|
321
|
-
constructor (input, options, terms) {
|
|
322
|
-
super(input, options);
|
|
323
|
-
if (terms.length > 1)
|
|
324
|
-
throw new Error('PropertyCase accepts exactly 1 string');
|
|
325
|
-
if (!options.caps || typeof options.caps !== 'object' || !Object.keys(options.caps).length)
|
|
326
|
-
throw new Error('PropertyCase requires a caps object with at least one capability');
|
|
327
|
-
const unknown = Object.keys(options.caps).filter( c => !knownCaps[c] );
|
|
328
|
-
if (unknown.length)
|
|
329
|
-
throw new Error('PropertyCase: don\'t know how to test these capabilities: ' + unknown.join(', '));
|
|
330
|
-
|
|
331
|
-
this.expr = this.parse(terms[0]);
|
|
332
|
-
this.caps = options.caps;
|
|
333
|
-
|
|
334
|
-
if (this.caps.linear) {
|
|
335
|
-
delete this.caps.linear;
|
|
336
|
-
this.caps.duplicate = false;
|
|
337
|
-
this.caps.discard = false;
|
|
338
|
-
this.caps.normal = true;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (this.caps.affine) {
|
|
342
|
-
delete this.caps.affine;
|
|
343
|
-
this.caps.normal = true;
|
|
344
|
-
this.caps.duplicate = false;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
check (...expr) {
|
|
349
|
-
const start = this.expr.apply(expr);
|
|
350
|
-
const r = start.run({ max: this.max });
|
|
351
|
-
const guess = r.expr.infer({ max: this.max });
|
|
352
|
-
|
|
353
|
-
const reason = [];
|
|
354
|
-
for (const cap in this.caps) {
|
|
355
|
-
if (guess[cap] !== this.caps[cap])
|
|
356
|
-
reason.push('expected property ' + cap + ' to be ' + this.caps[cap] + ', found ' + guess[cap]);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
pass: !reason.length,
|
|
361
|
-
reason: reason ? reason.join('\n') : null,
|
|
362
|
-
steps: r.steps,
|
|
363
|
-
start,
|
|
364
|
-
found: r.expr,
|
|
365
|
-
case: this,
|
|
366
|
-
note: this.note,
|
|
367
|
-
args: expr,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
class Subst {
|
|
373
|
-
/**
|
|
374
|
-
* @descr A placeholder object with exactly n free variables to be substituted later.
|
|
375
|
-
* @param {Expr} expr
|
|
376
|
-
* @param {FreeVar[]} vars
|
|
377
|
-
*/
|
|
378
|
-
constructor (expr, vars) {
|
|
379
|
-
this.expr = expr;
|
|
380
|
-
this.vars = vars;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
apply (list) {
|
|
384
|
-
if (list.length !== this.vars.length)
|
|
385
|
-
throw new Error('Subst: expected ' + this.vars.length + ' terms, got ' + list.length);
|
|
386
|
-
|
|
387
|
-
let expr = this.expr;
|
|
388
|
-
for (let i = 0; i < this.vars.length; i++)
|
|
389
|
-
expr = expr.subst(this.vars[i], list[i]) ?? expr;
|
|
390
|
-
|
|
391
|
-
return expr;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function list2str (str) {
|
|
396
|
-
if (str === undefined)
|
|
397
|
-
return str;
|
|
398
|
-
return Array.isArray(str) ? str.join(' ') : '' + str;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
module.exports = { Quest };
|
package/lib/util.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
class Tokenizer {
|
|
2
|
-
constructor (...terms) {
|
|
3
|
-
const src = '$|(\\s+)|' + terms
|
|
4
|
-
.map(s => '(?:' + s + ')')
|
|
5
|
-
.sort((a, b) => b.length - a.length)
|
|
6
|
-
.join('|');
|
|
7
|
-
this.rex = new RegExp(src, 'gys');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {string} str
|
|
13
|
-
* @return {string[]}
|
|
14
|
-
*/
|
|
15
|
-
split (str) {
|
|
16
|
-
this.rex.lastIndex = 0;
|
|
17
|
-
const list = [...str.matchAll(this.rex)];
|
|
18
|
-
|
|
19
|
-
// did we parse everything?
|
|
20
|
-
const eol = list.pop();
|
|
21
|
-
const last = eol?.index ?? 0;
|
|
22
|
-
|
|
23
|
-
if (last !== str.length) {
|
|
24
|
-
throw new Error('Unknown tokens at pos ' + last + '/' + str.length
|
|
25
|
-
+ ' starting with ' + str.substring(last));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// skip whitespace
|
|
29
|
-
return list.filter(x => x[1] === undefined).map(x => x[0]);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const tokRestrict = new Tokenizer('[-=+]', '[A-Z]', '\\b[a-z_][a-z_0-9]*\\b');
|
|
34
|
-
function restrict (set, spec) {
|
|
35
|
-
if (!spec)
|
|
36
|
-
return set;
|
|
37
|
-
let out = new Set([...set]);
|
|
38
|
-
let mode = 0;
|
|
39
|
-
const act = [
|
|
40
|
-
sym => { out = new Set([sym]); mode = 1; },
|
|
41
|
-
sym => { out.add(sym); },
|
|
42
|
-
sym => { out.delete(sym); },
|
|
43
|
-
];
|
|
44
|
-
for (const sym of tokRestrict.split(spec)) {
|
|
45
|
-
if (sym === '=')
|
|
46
|
-
mode = 0;
|
|
47
|
-
else if (sym === '+')
|
|
48
|
-
mode = +1;
|
|
49
|
-
else if (sym === '-')
|
|
50
|
-
mode = 2;
|
|
51
|
-
else
|
|
52
|
-
act[mode](sym);
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
module.exports = { Tokenizer, restrict };
|
package/types/lib/util.d.ts
DELETED