@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/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 };