@dallaylaen/ski-interpreter 2.1.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/extras.js DELETED
@@ -1,140 +0,0 @@
1
- 'use strict';
2
-
3
- const { Expr } = require('./expr')
4
-
5
- /**
6
- * @desc Extra utilities that do not belong in the core.
7
- */
8
-
9
- /**
10
- * @experimental
11
- * @desc Look for an expression that matches the predicate,
12
- * starting with the seed and applying the terms to one another.
13
- *
14
- * A predicate returning 0 (or nothing) means "keep looking",
15
- * a positive number stands for "found",
16
- * and a negative means "discard this term from further applications".
17
- *
18
- * The order of search is from shortest to longest expressions.
19
- *
20
- * @param {Expr[]} seed
21
- * @param {object} options
22
- * @param {number} [options.depth] - maximum generation to search for
23
- * @param {number} [options.tries] - maximum number of tries before giving up
24
- * @param {boolean} [options.infer] - whether to call infer(), default true.
25
- * @param {number} [options.maxArgs] - arguments in infer()
26
- * @param {number} [options.max] - step limit in infer()
27
- * @param {boolean} [options.noskip] - prevents skipping equivalent terms. Always true if infer is false.
28
- * @param {boolean} [retain] - if true. also add the whole cache to returned value
29
- * @param {({gen: number, total: number, probed: number, step: boolean}) => void} [options.progress]
30
- * @param {number} [options.progressInterval] - minimum number of tries between calls to options.progress, default 1000.
31
- * @param {(e: Expr, props: {}) => number?} predicate
32
- * @return {{expr?: Expr, total: number, probed: number, gen: number, cache?: Expr[][]}}
33
- */
34
- function search (seed, options, predicate) {
35
- const {
36
- depth = 16,
37
- infer = true,
38
- progressInterval = 1000,
39
- } = options;
40
- const hasSeen = infer && !options.noskip;
41
-
42
- // cache[i] = ith generation, 0 is empty
43
- const cache = [[]];
44
- let total = 0;
45
- let probed = 0;
46
- const seen = {};
47
-
48
- const maybeProbe = term => {
49
- total++;
50
- const props = infer ? term.infer({ max: options.max, maxArgs: options.maxArgs }) : null;
51
- if (hasSeen && props.expr) {
52
- if (seen[props.expr])
53
- return { res: -1 };
54
- seen[props.expr] = true;
55
- }
56
- probed++;
57
- const res = predicate(term, props);
58
- return { res, props };
59
- };
60
-
61
- // sieve through the seed
62
- for (const term of seed) {
63
- const { res } = maybeProbe(term);
64
- if (res > 0)
65
- return { expr: term, total, probed, gen: 1 };
66
- else if (res < 0)
67
- continue;
68
-
69
- cache[0].push(term);
70
- }
71
-
72
- let lastProgress;
73
-
74
- for (let gen = 1; gen < depth; gen++) {
75
- if (options.progress) {
76
- options.progress({ gen, total, probed, step: true });
77
- lastProgress = total;
78
- }
79
- for (let i = 0; i < gen; i++) {
80
- for (const a of cache[gen - i - 1] || []) {
81
- for (const b of cache[i] || []) {
82
- if (total >= options.tries)
83
- return { total, probed, gen, ...(options.retain ? { cache } : {}) };
84
- if (options.progress && total - lastProgress >= progressInterval) {
85
- options.progress({ gen, total, probed, step: false });
86
- lastProgress = total;
87
- }
88
- const term = a.apply(b);
89
- const { res, props } = maybeProbe(term);
90
-
91
- if (res > 0)
92
- return { expr: term, total, probed, gen, ...(options.retain ? { cache } : {}) };
93
- else if (res < 0)
94
- continue;
95
-
96
- // if the term is not reducible, it is more likely to be a dead end, so we push it further away
97
- const offset = infer
98
- ? ((props.expr ? 0 : 3) + (props.dup ? 1 : 0) + (props.proper ? 0 : 1))
99
- : 0;
100
- if (!cache[gen + offset])
101
- cache[gen + offset] = [];
102
- cache[gen + offset].push(term);
103
- }
104
- }
105
- }
106
- }
107
-
108
- return { total, probed, gen: depth, ...(options.retain ? { cache } : {}) };
109
- }
110
-
111
- /**
112
- * @desc Recursively replace all instances of Expr in a data structure with
113
- * respective string representation using the format() options.
114
- * Objects of other types and primitive values are eft as is.
115
- *
116
- * May be useful for debugging or diagnostic output.
117
- *
118
- * @experimental
119
- *
120
- * @param {any} obj
121
- * @param {object} [options] - see Expr.format()
122
- * @returns {any}
123
- */
124
- function deepFormat (obj, options = {}) {
125
- if (obj instanceof Expr)
126
- return obj.format(options);
127
- if (Array.isArray(obj))
128
- return obj.map(deepFormat);
129
- if (typeof obj !== 'object' || obj === null || obj.constructor !== Object)
130
- return obj;
131
-
132
- // default = plain object
133
- const out = {};
134
- for (const key in obj)
135
- out[key] = deepFormat(obj[key]);
136
-
137
- return out;
138
- }
139
-
140
- module.exports = { search, deepFormat };
package/lib/internal.js DELETED
@@ -1,105 +0,0 @@
1
- class Tokenizer {
2
- /**
3
- * @desc Create a tokenizer that splits strings into tokens according to the given terms.
4
- * The terms are interpreted as regular expressions, and are sorted by length
5
- * to ensure that longer matches are preferred over shorter ones.
6
- * @param {...string|RegExp} terms
7
- */
8
- constructor (...terms) {
9
- const src = '$|(\\s+)|' + terms
10
- .map(s => '(?:' + s + ')')
11
- .sort((a, b) => b.length - a.length)
12
- .join('|');
13
- this.rex = new RegExp(src, 'gys');
14
- }
15
-
16
- /**
17
- * @desc Split the given string into tokens according to the terms specified in the constructor.
18
- * @param {string} str
19
- * @return {string[]}
20
- */
21
- split (str) {
22
- this.rex.lastIndex = 0;
23
- const list = [...str.matchAll(this.rex)];
24
-
25
- // did we parse everything?
26
- const eol = list.pop();
27
- const last = eol?.index ?? 0;
28
-
29
- if (last !== str.length) {
30
- throw new Error('Unknown tokens at pos ' + last + '/' + str.length
31
- + ' starting with ' + str.substring(last));
32
- }
33
-
34
- // skip whitespace
35
- return list.filter(x => x[1] === undefined).map(x => x[0]);
36
- }
37
- }
38
-
39
- const tokRestrict = new Tokenizer('[-=+]', '[A-Z]', '\\b[a-z_][a-z_0-9]*\\b');
40
-
41
- /**
42
- * @desc Add ot remove tokens from a set according to a spec string.
43
- * The spec string is a sequence of tokens, with each group optionally prefixed
44
- * by one of the operators '=', '+', or '-'.
45
- * The '=' operator resets the set to contain only the following token(s).
46
- * @param {Set<string>} set
47
- * @param {string} [spec]
48
- * @returns {Set<string>}
49
- */
50
- function restrict (set, spec) {
51
- if (!spec)
52
- return set;
53
- let out = new Set([...set]);
54
- const act = {
55
- '=': sym => { out = new Set([sym]); mode = '+'; },
56
- '+': sym => { out.add(sym); },
57
- '-': sym => { out.delete(sym); },
58
- };
59
-
60
- let mode = '=';
61
- for (const sym of tokRestrict.split(spec)) {
62
- if (act[sym])
63
- mode = sym;
64
- else
65
- act[mode](sym);
66
- }
67
- return out;
68
- }
69
-
70
- class ActionWrapper {
71
- /**
72
- * @template T
73
- * @param {T} value
74
- * @param {string} action
75
- */
76
- constructor (value, action) {
77
- this.value = value;
78
- this.action = action;
79
- }
80
- }
81
-
82
- /**
83
- * @private
84
- * @template T
85
- * @param {T|ActionWrapper<T>} value
86
- * @returns {[T?, string|undefined]}
87
- */
88
- function unwrap (value) {
89
- if (value instanceof ActionWrapper)
90
- return [value.value ?? undefined, value.action];
91
- return [value ?? undefined, undefined];
92
- }
93
-
94
- /**
95
- *
96
- * @private
97
- * @template T
98
- * @param {string} action
99
- * @returns {function(T): ActionWrapper<T>}
100
- */
101
- function prepareWrapper (action) {
102
- return value => new ActionWrapper(value, action);
103
- }
104
-
105
- module.exports = { Tokenizer, restrict, unwrap, prepareWrapper };
package/lib/parser.js DELETED
@@ -1,434 +0,0 @@
1
- /**
2
- * Combinatory logic simulator
3
- */
4
- 'use strict';
5
-
6
- const { Tokenizer, restrict } = require('./internal');
7
- const classes = require('./expr');
8
-
9
- const { Expr, Named, 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: ['', ' &mapsto; ', ''] });
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
-
275
- if (expr instanceof Alias)
276
- expr.outdated = true;
277
- expr = (str === '' && save !== undefined)
278
- ? new FreeVar(save, options.scope)
279
- : this.parseLine(str, jar, options);
280
-
281
- if (save !== undefined) {
282
- if (jar[save] !== undefined)
283
- throw new Error('Attempt to redefine a known term: ' + save);
284
- expr = maybeAlias(save, expr);
285
- jar[save] = expr;
286
- }
287
-
288
- // console.log('parsed line:', item, '; got:', expr,'; jar now: ', jar);
289
- }
290
-
291
- expr.context = {
292
- env: jar, // also contains pre-parsed terms
293
- scope: options.scope,
294
- src: source,
295
- parser: this,
296
- };
297
- return expr;
298
- }
299
-
300
- /**
301
- *
302
- * @param {String} source S(KI)I
303
- * @param {{[keys: string]: Expr}} env
304
- * @param {Object} [options]
305
- * @param {{[keys: string]: Expr}} [options.env] - unused, see 'env' argument
306
- * @param {any} [options.scope]
307
- * @param {boolean} [options.numbers]
308
- * @param {boolean} [options.lambdas]
309
- * @param {string} [options.allow]
310
- * @return {Expr} parsed expression
311
- */
312
- parseLine (source, env = {}, options = {}) {
313
- const opt = {
314
- numbers: options.numbers ?? this.hasNumbers,
315
- lambdas: options.lambdas ?? this.hasLambdas,
316
- allow: restrict(this.allow, options.allow),
317
- };
318
- // make sure '+' usage is in sync with numerals
319
- opt.numbers ? opt.allow.add('+') : opt.allow.delete('+');
320
-
321
- const tokens = combChars.split(source);
322
-
323
- const empty = new Empty();
324
- /** @type {Expr[]} */
325
- const stack = [empty];
326
- const context = options.scope || SKI; // default is global unbound vars
327
-
328
- // TODO each token should carry along its position in source
329
- for (const c of tokens) {
330
- // console.log("parseLine: found "+c+"; stack =", stack.join(", "));
331
- if (c === '(')
332
- stack.push(empty);
333
- else if (c === ')') {
334
- if (stack.length < 2)
335
- throw new Error('unbalanced input: extra closing parenthesis' + source);
336
- const x = postParse(stack.pop());
337
- const f = stack.pop();
338
- stack.push(f.apply(x));
339
- } else if (c === '->') {
340
- if (!opt.lambdas)
341
- throw new Error('Lambdas not supported, allow them explicitly');
342
- stack.push(new PartialLambda(stack.pop(), env));
343
- } else if (c.match(/^[0-9]+$/)) {
344
- if (!opt.numbers)
345
- throw new Error('Church numbers not supported, allow them explicitly');
346
- const f = stack.pop();
347
- stack.push(f.apply(new Church(c)));
348
- } else {
349
- const f = stack.pop();
350
- if (!env[c] && this.known[c] && !opt.allow.has(c)) {
351
- throw new Error('Term \'' + c + '\' is not in the restricted set '
352
- + [...opt.allow].sort().join(' '));
353
- }
354
- // look in temp vars first, then in known terms, then fallback to creating free var
355
- const x = env[c] ?? this.known[c] ?? (env[c] = new FreeVar(c, context));
356
- stack.push(f.apply(x));
357
- }
358
- }
359
-
360
- if (stack.length !== 1) {
361
- throw new Error('unbalanced input: missing '
362
- + (stack.length - 1) + ' closing parenthesis:' + source);
363
- }
364
-
365
- return postParse(stack.pop());
366
- }
367
-
368
- toJSON () {
369
- return {
370
- version: '1.1.1', // set to incremented package.json version whenever SKI serialization changes
371
- allow: this.showRestrict('+'),
372
- numbers: this.hasNumbers,
373
- lambdas: this.hasLambdas,
374
- annotate: this.annotate,
375
- terms: this.declare(),
376
- }
377
- }
378
- }
379
-
380
- function maybeAlias (name, expr) {
381
- if (expr instanceof Named && expr.name === name)
382
- return expr;
383
- return new Alias(name, expr);
384
- }
385
-
386
- // Create shortcuts for common terms
387
-
388
- SKI.classes = classes;
389
-
390
- /**
391
- * @desc Create a proxy object that generates variables on demand,
392
- * with names corresponding to the property accessed.
393
- * Different invocations will return distinct variables,
394
- * even if with the same name.
395
- *
396
- *
397
- * @example const {x, y, z} = SKI.vars();
398
- * x.name; // 'x'
399
- * x instanceof FreeVar; // true
400
- * x.apply(y).apply(z); // x(y)(z)
401
- *
402
- * @return {{[key: string]: FreeVar}}
403
- */
404
-
405
- SKI.vars = function (context = {}) {
406
- const cache = {};
407
- return new Proxy({}, {
408
- get: (target, name) => {
409
- if (!(name in cache))
410
- cache[name] = new FreeVar(name, context);
411
- return cache[name];
412
- }
413
- });
414
- };
415
-
416
- /**
417
- * Convert a number to Church encoding
418
- * @param {number} n
419
- * @return {Church}
420
- */
421
- SKI.church = n => new Church(n);
422
-
423
- /**
424
- *
425
- * @type {{[key: string]: Native}}
426
- */
427
-
428
- for (const name in native)
429
- SKI[name] = native[name];
430
- SKI.native = native;
431
- SKI.declare = declare;
432
- SKI.control = Expr.control;
433
-
434
- module.exports = { SKI };