@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/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 };
@@ -1,11 +0,0 @@
1
- export class Tokenizer {
2
- constructor(...terms: any[]);
3
- rex: RegExp;
4
- /**
5
- *
6
- * @param {string} str
7
- * @return {string[]}
8
- */
9
- split(str: string): string[];
10
- }
11
- export function restrict(set: any, spec: any): any;