@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/parser.js
DELETED
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Combinatory logic simulator
|
|
3
|
-
*/
|
|
4
|
-
'use strict';
|
|
5
|
-
|
|
6
|
-
const { Tokenizer, restrict } = require('./util');
|
|
7
|
-
const classes = require('./expr');
|
|
8
|
-
|
|
9
|
-
const { Expr, Native, Alias, FreeVar, Lambda, Church } = classes;
|
|
10
|
-
const { native, declare } = Expr;
|
|
11
|
-
|
|
12
|
-
class Empty extends Expr {
|
|
13
|
-
apply (...args) {
|
|
14
|
-
return args.length ? args.shift().apply(...args) : this;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
postParse () {
|
|
18
|
-
throw new Error('Attempt to use empty expression () as a term');
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
class PartialLambda extends Empty {
|
|
23
|
-
// TODO mutable! rewrite ro when have time
|
|
24
|
-
constructor (term, known = {}) {
|
|
25
|
-
super();
|
|
26
|
-
this.impl = new Empty();
|
|
27
|
-
if (term instanceof FreeVar)
|
|
28
|
-
this.terms = [term];
|
|
29
|
-
else if (term instanceof PartialLambda) {
|
|
30
|
-
if (!(term.impl instanceof FreeVar))
|
|
31
|
-
throw new Error('Expected FreeVar->...->FreeVar->Expr');
|
|
32
|
-
this.terms = [...term.terms, term.impl];
|
|
33
|
-
} else
|
|
34
|
-
throw new Error('Expected FreeVar or PartialLambda');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
apply (term, ...tail) {
|
|
38
|
-
if (term === null || tail.length !== 0 )
|
|
39
|
-
throw new Error('bad syntax in partial lambda expr');
|
|
40
|
-
this.impl = this.impl.apply(term);
|
|
41
|
-
return this;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
postParse () {
|
|
45
|
-
return new Lambda(this.terms, this.impl);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// uncomment if debugging with prints
|
|
49
|
-
/* toString () {
|
|
50
|
-
return this.terms.join('->') + '->' + (this.impl ?? '???');
|
|
51
|
-
} */
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function postParse (expr) {
|
|
55
|
-
return expr.postParse ? expr.postParse() : expr;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const combChars = new Tokenizer(
|
|
59
|
-
'[()]', '[A-Z]', '[a-z_][a-z_0-9]*', '\\b[0-9]+\\b', '->', '\\+'
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
class SKI {
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
65
|
-
* @param {{
|
|
66
|
-
* allow?: string,
|
|
67
|
-
* numbers?: boolean,
|
|
68
|
-
* lambdas?: boolean,
|
|
69
|
-
* terms?: { [key: string]: Expr|string} | string[],
|
|
70
|
-
* annotate?: boolean,
|
|
71
|
-
* }} [options]
|
|
72
|
-
*/
|
|
73
|
-
constructor (options = {}) {
|
|
74
|
-
this.annotate = options.annotate ?? false;
|
|
75
|
-
this.known = { ...native };
|
|
76
|
-
this.hasNumbers = true;
|
|
77
|
-
this.hasLambdas = true;
|
|
78
|
-
this.allow = new Set(Object.keys(this.known));
|
|
79
|
-
|
|
80
|
-
// Import terms, if any. Omit native ones
|
|
81
|
-
if (Array.isArray(options.terms))
|
|
82
|
-
this.bulkAdd(options.terms);
|
|
83
|
-
else if (options.terms) {
|
|
84
|
-
for (const name in options.terms) {
|
|
85
|
-
// Native terms already handled by allow
|
|
86
|
-
if (!options.terms[name].match(/^Native:/))
|
|
87
|
-
this.add(name, options.terms[name]);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Finally, impose restrictions
|
|
92
|
-
// We must do it after recreating terms, or else terms reliant on forbidden terms will fail
|
|
93
|
-
this.hasNumbers = options.numbers ?? true;
|
|
94
|
-
this.hasLambdas = options.lambdas ?? true;
|
|
95
|
-
if (options.allow)
|
|
96
|
-
this.restrict(options.allow);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @desc Declare a new term
|
|
101
|
-
* If the first argument is an Alias, it is added as is.
|
|
102
|
-
* Otherwise, a new Alias or Native term (depending on impl type) is created.
|
|
103
|
-
* If note is not provided and this.annotate is true, an automatic note is generated.
|
|
104
|
-
*
|
|
105
|
-
* If impl is a function, it should have signature (Expr) => ... => Expr
|
|
106
|
-
* (see typedef Partial at top of expr.js)
|
|
107
|
-
*
|
|
108
|
-
* @example ski.add('T', 'S(K(SI))K', 'swap combinator')
|
|
109
|
-
* @example ski.add( ski.parse('T = S(K(SI))K') ) // ditto but one-arg form
|
|
110
|
-
* @example ski.add('T', x => y => y.apply(x), 'swap combinator') // heavy artillery
|
|
111
|
-
* @example ski.add('Y', function (f) { return f.apply(this.apply(f)); }, 'Y combinator')
|
|
112
|
-
*
|
|
113
|
-
* @param {Alias|String} term
|
|
114
|
-
* @param {String|Expr|function(Expr):Partial} [impl]
|
|
115
|
-
* @param {String} [note]
|
|
116
|
-
* @return {SKI} chainable
|
|
117
|
-
*/
|
|
118
|
-
add (term, impl, note ) {
|
|
119
|
-
term = this._named(term, impl);
|
|
120
|
-
|
|
121
|
-
if (this.annotate && note === undefined) {
|
|
122
|
-
const guess = term.infer();
|
|
123
|
-
if (guess.expr)
|
|
124
|
-
note = guess.expr.format({ terse: true, html: true, lambda: ['', ' ↦ ', ''] });
|
|
125
|
-
}
|
|
126
|
-
if (note !== undefined)
|
|
127
|
-
term.note = note;
|
|
128
|
-
|
|
129
|
-
if (this.known[term.name])
|
|
130
|
-
this.known[term.name].outdated = true;
|
|
131
|
-
this.known[term.name] = term;
|
|
132
|
-
this.allow.add(term.name);
|
|
133
|
-
|
|
134
|
-
return this;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
_named (term, impl) {
|
|
138
|
-
if (term instanceof Alias)
|
|
139
|
-
return new Alias(term.name, term.impl, { canonize: true });
|
|
140
|
-
if (typeof term !== 'string')
|
|
141
|
-
throw new Error('add(): term must be an Alias or a string');
|
|
142
|
-
if (impl === undefined)
|
|
143
|
-
throw new Error('add(): impl must be provided when term is a string');
|
|
144
|
-
if (typeof impl === 'string')
|
|
145
|
-
return new Alias(term, this.parse(impl), { canonize: true });
|
|
146
|
-
if (impl instanceof Expr)
|
|
147
|
-
return new Alias(term, impl, { canonize: true });
|
|
148
|
-
if (typeof impl === 'function')
|
|
149
|
-
return new Native(term, impl);
|
|
150
|
-
// idk what this is
|
|
151
|
-
throw new Error('add(): impl must be an Expr, a string, or a function with a signature Expr => ... => Expr');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
maybeAdd (name, impl) {
|
|
155
|
-
if (this.known[name])
|
|
156
|
-
this.allow.add(name);
|
|
157
|
-
else
|
|
158
|
-
this.add(name, impl);
|
|
159
|
-
return this;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* @desc Declare and remove multiple terms at once
|
|
164
|
-
* term=impl adds term
|
|
165
|
-
* term= removes term
|
|
166
|
-
* @param {string[]]} list
|
|
167
|
-
* @return {SKI} chainable
|
|
168
|
-
*/
|
|
169
|
-
bulkAdd (list) {
|
|
170
|
-
for (const item of list) {
|
|
171
|
-
const m = item.match(/^([A-Z]|[a-z][a-z_0-9]*)\s*=\s*(.*)$/s);
|
|
172
|
-
// TODO check all declarations before applying any (but we might need earlier terms for parsing later ones)
|
|
173
|
-
if (!m)
|
|
174
|
-
throw new Error('bulkAdd: invalid declaration: ' + item);
|
|
175
|
-
if (m[2] === '')
|
|
176
|
-
this.remove(m[1]);
|
|
177
|
-
else
|
|
178
|
-
this.add(m[1], this.parse(m[2]));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return this;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Restrict the interpreter to given terms. Terms prepended with '+' will be added
|
|
186
|
-
* and terms preceeded with '-' will be removed.
|
|
187
|
-
* @example ski.restrict('SK') // use the basis
|
|
188
|
-
* @example ski.restrict('+I') // allow I now
|
|
189
|
-
* @example ski.restrict('-SKI +BCKW' ); // switch basis
|
|
190
|
-
* @example ski.restrict('-foo -bar'); // forbid some user functions
|
|
191
|
-
* @param {string} spec
|
|
192
|
-
* @return {SKI} chainable
|
|
193
|
-
*/
|
|
194
|
-
restrict (spec) {
|
|
195
|
-
this.allow = restrict(this.allow, spec);
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
*
|
|
201
|
-
* @param {string} spec
|
|
202
|
-
* @return {string}
|
|
203
|
-
*/
|
|
204
|
-
showRestrict (spec = '+') {
|
|
205
|
-
const out = [];
|
|
206
|
-
let prevShort = true;
|
|
207
|
-
for (const term of [...restrict(this.allow, spec)].sort()) {
|
|
208
|
-
const nextShort = term.match(/^[A-Z]$/);
|
|
209
|
-
if (out.length && !(prevShort && nextShort))
|
|
210
|
-
out.push(' ');
|
|
211
|
-
out.push(term);
|
|
212
|
-
prevShort = nextShort;
|
|
213
|
-
}
|
|
214
|
-
return out.join('');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
*
|
|
219
|
-
* @param {String} name
|
|
220
|
-
* @return {SKI}
|
|
221
|
-
*/
|
|
222
|
-
remove (name) {
|
|
223
|
-
this.known[name].outdated = true;
|
|
224
|
-
delete this.known[name];
|
|
225
|
-
this.allow.delete(name);
|
|
226
|
-
return this;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
*
|
|
231
|
-
* @return {{[key:string]: Native|Alias}}
|
|
232
|
-
*/
|
|
233
|
-
getTerms () {
|
|
234
|
-
const out = {};
|
|
235
|
-
for (const name of Object.keys(this.known)) {
|
|
236
|
-
if (this.allow.has(name))
|
|
237
|
-
out[name] = this.known[name];
|
|
238
|
-
}
|
|
239
|
-
return out;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Export term declarations for use in bulkAdd().
|
|
244
|
-
* @returns {string[]}
|
|
245
|
-
*/
|
|
246
|
-
declare () {
|
|
247
|
-
// TODO accept argument to declare specific terms only
|
|
248
|
-
return declare(this.getTerms());
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
*
|
|
253
|
-
* @param {string} source
|
|
254
|
-
* @param {Object} [options]
|
|
255
|
-
* @param {{[keys: string]: Expr}} [options.env]
|
|
256
|
-
* @param {any} [options.scope]
|
|
257
|
-
* @param {boolean} [options.numbers]
|
|
258
|
-
* @param {boolean} [options.lambdas]
|
|
259
|
-
* @param {string} [options.allow]
|
|
260
|
-
* @return {Expr}
|
|
261
|
-
*/
|
|
262
|
-
parse (source, options = {}) {
|
|
263
|
-
if (typeof source !== 'string')
|
|
264
|
-
throw new Error('parse: source must be a string, got ' + typeof source);
|
|
265
|
-
|
|
266
|
-
const lines = source.replace(/\/\/[^\n]*$/gm, '')
|
|
267
|
-
.split(/\s*;[\s;]*/).filter( s => s.match(/\S/));
|
|
268
|
-
|
|
269
|
-
const jar = { ...options.env };
|
|
270
|
-
|
|
271
|
-
let expr = new Empty();
|
|
272
|
-
for (const item of lines) {
|
|
273
|
-
const [_, save, str] = item.match(/^(?:\s*([A-Z]|[a-z][a-z_0-9]*)\s*=\s*)?(.*)$/s);
|
|
274
|
-
if (expr instanceof Alias)
|
|
275
|
-
expr.outdated = true;
|
|
276
|
-
expr = this.parseLine(str, jar, options);
|
|
277
|
-
|
|
278
|
-
if (save !== undefined) {
|
|
279
|
-
if (jar[save] !== undefined)
|
|
280
|
-
throw new Error('Attempt to redefine a known term: ' + save);
|
|
281
|
-
expr = new Alias(save, expr);
|
|
282
|
-
jar[save] = expr;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// console.log('parsed line:', item, '; got:', expr,'; jar now: ', jar);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return expr;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
*
|
|
293
|
-
* @param {String} source S(KI)I
|
|
294
|
-
* @param {{[keys: string]: Expr}} env
|
|
295
|
-
* @param {Object} [options]
|
|
296
|
-
* @param {{[keys: string]: Expr}} [options.env] - unused, see 'env' argument
|
|
297
|
-
* @param {any} [options.scope]
|
|
298
|
-
* @param {boolean} [options.numbers]
|
|
299
|
-
* @param {boolean} [options.lambdas]
|
|
300
|
-
* @param {string} [options.allow]
|
|
301
|
-
* @return {Expr} parsed expression
|
|
302
|
-
*/
|
|
303
|
-
parseLine (source, env = {}, options = {}) {
|
|
304
|
-
const opt = {
|
|
305
|
-
numbers: options.numbers ?? this.hasNumbers,
|
|
306
|
-
lambdas: options.lambdas ?? this.hasLambdas,
|
|
307
|
-
allow: restrict(this.allow, options.allow),
|
|
308
|
-
};
|
|
309
|
-
// make sure '+' usage is in sync with numerals
|
|
310
|
-
opt.numbers ? opt.allow.add('+') : opt.allow.delete('+');
|
|
311
|
-
|
|
312
|
-
const tokens = combChars.split(source);
|
|
313
|
-
|
|
314
|
-
const empty = new Empty();
|
|
315
|
-
/** @type {Expr[]} */
|
|
316
|
-
const stack = [empty];
|
|
317
|
-
const context = options.scope || SKI; // default is global unbound vars
|
|
318
|
-
|
|
319
|
-
// TODO each token should carry along its position in source
|
|
320
|
-
for (const c of tokens) {
|
|
321
|
-
// console.log("parseLine: found "+c+"; stack =", stack.join(", "));
|
|
322
|
-
if (c === '(')
|
|
323
|
-
stack.push(empty);
|
|
324
|
-
else if (c === ')') {
|
|
325
|
-
if (stack.length < 2)
|
|
326
|
-
throw new Error('unbalanced input: extra closing parenthesis' + source);
|
|
327
|
-
const x = postParse(stack.pop());
|
|
328
|
-
const f = stack.pop();
|
|
329
|
-
stack.push(f.apply(x));
|
|
330
|
-
} else if (c === '->') {
|
|
331
|
-
if (!opt.lambdas)
|
|
332
|
-
throw new Error('Lambdas not supported, allow them explicitly');
|
|
333
|
-
stack.push(new PartialLambda(stack.pop(), env));
|
|
334
|
-
} else if (c.match(/^[0-9]+$/)) {
|
|
335
|
-
if (!opt.numbers)
|
|
336
|
-
throw new Error('Church numbers not supported, allow them explicitly');
|
|
337
|
-
const f = stack.pop();
|
|
338
|
-
stack.push(f.apply(new Church(c)));
|
|
339
|
-
} else {
|
|
340
|
-
const f = stack.pop();
|
|
341
|
-
if (!env[c] && this.known[c] && !opt.allow.has(c)) {
|
|
342
|
-
throw new Error('Term \'' + c + '\' is not in the restricted set '
|
|
343
|
-
+ [...opt.allow].sort().join(' '));
|
|
344
|
-
}
|
|
345
|
-
// look in temp vars first, then in known terms, then fallback to creating free var
|
|
346
|
-
const x = env[c] ?? this.known[c] ?? (env[c] = new FreeVar(c, context));
|
|
347
|
-
stack.push(f.apply(x));
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (stack.length !== 1) {
|
|
352
|
-
throw new Error('unbalanced input: missing '
|
|
353
|
-
+ (stack.length - 1) + ' closing parenthesis:' + source);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return postParse(stack.pop());
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
toJSON () {
|
|
360
|
-
return {
|
|
361
|
-
version: '1.1.1', // set to incremented package.json version whenever SKI serialization changes
|
|
362
|
-
allow: this.showRestrict('+'),
|
|
363
|
-
numbers: this.hasNumbers,
|
|
364
|
-
lambdas: this.hasLambdas,
|
|
365
|
-
annotate: this.annotate,
|
|
366
|
-
terms: this.declare(),
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Create shortcuts for common terms
|
|
372
|
-
|
|
373
|
-
SKI.classes = classes;
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* @desc Create a proxy object that generates variables on demand,
|
|
377
|
-
* with names corresponding to the property accessed.
|
|
378
|
-
* Different invocations will return distinct variables,
|
|
379
|
-
* even if with the same name.
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
* @example const {x, y, z} = SKI.vars();
|
|
383
|
-
* x.name; // 'x'
|
|
384
|
-
* x instanceof FreeVar; // true
|
|
385
|
-
* x.apply(y).apply(z); // x(y)(z)
|
|
386
|
-
*
|
|
387
|
-
* @return {{[key: string]: FreeVar}}
|
|
388
|
-
*/
|
|
389
|
-
|
|
390
|
-
SKI.vars = function (context = {}) {
|
|
391
|
-
const cache = {};
|
|
392
|
-
return new Proxy({}, {
|
|
393
|
-
get: (target, name) => {
|
|
394
|
-
if (!(name in cache))
|
|
395
|
-
cache[name] = new FreeVar(name, context);
|
|
396
|
-
return cache[name];
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Convert a number to Church encoding
|
|
403
|
-
* @param {number} n
|
|
404
|
-
* @return {Church}
|
|
405
|
-
*/
|
|
406
|
-
SKI.church = n => new Church(n);
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
*
|
|
410
|
-
* @type {{[key: string]: Native}}
|
|
411
|
-
*/
|
|
412
|
-
|
|
413
|
-
for (const name in native)
|
|
414
|
-
SKI[name] = native[name];
|
|
415
|
-
SKI.native = native;
|
|
416
|
-
SKI.declare = declare;
|
|
417
|
-
|
|
418
|
-
module.exports = { SKI };
|