@dallaylaen/ski-interpreter 1.1.0 → 1.3.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 +32 -1
- package/README.md +20 -18
- package/bin/ski.js +1 -1
- package/lib/expr.js +373 -213
- package/lib/parser.js +87 -29
- package/lib/quest.js +43 -13
- package/package.json +3 -2
- package/types/lib/expr.d.ts +181 -101
- package/types/lib/parser.d.ts +41 -19
- package/types/lib/quest.d.ts +36 -7
package/lib/parser.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { Tokenizer, restrict } = require('./util');
|
|
6
|
-
const { globalOptions, Expr, App, FreeVar, Lambda, Native, Alias, Church, native } = require('./expr');
|
|
6
|
+
const { globalOptions, Expr, App, FreeVar, Lambda, Native, Alias, Church, native, declare } = require('./expr');
|
|
7
7
|
|
|
8
8
|
class Empty extends Expr {
|
|
9
9
|
apply (...args) {
|
|
@@ -55,11 +55,11 @@ class SKI {
|
|
|
55
55
|
/**
|
|
56
56
|
*
|
|
57
57
|
* @param {{
|
|
58
|
-
* allow
|
|
59
|
-
* numbers
|
|
60
|
-
* lambdas
|
|
61
|
-
* terms
|
|
62
|
-
* annotate
|
|
58
|
+
* allow?: string,
|
|
59
|
+
* numbers?: boolean,
|
|
60
|
+
* lambdas?: boolean,
|
|
61
|
+
* terms?: { [key: string]: Expr|string} | string[],
|
|
62
|
+
* annotate?: boolean,
|
|
63
63
|
* }} [options]
|
|
64
64
|
*/
|
|
65
65
|
constructor (options = {}) {
|
|
@@ -70,10 +70,14 @@ class SKI {
|
|
|
70
70
|
this.allow = new Set(Object.keys(this.known));
|
|
71
71
|
|
|
72
72
|
// Import terms, if any. Omit native ones
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
if (Array.isArray(options.terms))
|
|
74
|
+
this.bulkAdd(options.terms);
|
|
75
|
+
else if (options.terms) {
|
|
76
|
+
for (const name in options.terms) {
|
|
77
|
+
// Native terms already handled by allow
|
|
78
|
+
if (!options.terms[name].match(/^Native:/))
|
|
79
|
+
this.add(name, options.terms[name]);
|
|
80
|
+
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
// Finally, impose restrictions
|
|
@@ -85,38 +89,60 @@ class SKI {
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
|
92
|
+
* @desc Declare a new term
|
|
93
|
+
* If the first argument is an Alias, it is added as is.
|
|
94
|
+
* Otherwise, a new Alias or Native term (depending on impl type) is created.
|
|
95
|
+
* If note is not provided and this.annotate is true, an automatic note is generated.
|
|
96
|
+
*
|
|
97
|
+
* If impl is a function, it should have signature (Expr) => ... => Expr
|
|
98
|
+
* (see typedef Partial at top of expr.js)
|
|
99
|
+
*
|
|
100
|
+
* @example ski.add('T', 'S(K(SI))K', 'swap combinator')
|
|
101
|
+
* @example ski.add( ski.parse('T = S(K(SI))K') ) // ditto but one-arg form
|
|
102
|
+
* @example ski.add('T', x => y => y.apply(x), 'swap combinator') // heavy artillery
|
|
103
|
+
* @example ski.add('Y', function (f) { return f.apply(this.apply(f)); }, 'Y combinator')
|
|
88
104
|
*
|
|
89
105
|
* @param {Alias|String} term
|
|
90
|
-
* @param {Expr|
|
|
106
|
+
* @param {String|Expr|function(Expr):Partial} [impl]
|
|
91
107
|
* @param {String} [note]
|
|
92
108
|
* @return {SKI} chainable
|
|
93
109
|
*/
|
|
94
110
|
add (term, impl, note ) {
|
|
95
|
-
|
|
96
|
-
if (typeof impl === 'string')
|
|
97
|
-
term = new Alias(term, this.parse(impl), { canonize: true });
|
|
98
|
-
else if (impl instanceof Expr)
|
|
99
|
-
term = new Alias(term, impl, { canonize: true });
|
|
100
|
-
else
|
|
101
|
-
throw new Error('add: term must be an Alias or a string and impl must be an Expr or a string');
|
|
102
|
-
} else if (term instanceof Alias)
|
|
103
|
-
term = new Alias(term.name, term.impl, { canonize: true });
|
|
104
|
-
|
|
105
|
-
// This should normally be unreachable but let's keep just in case
|
|
106
|
-
if (!(term instanceof Alias))
|
|
107
|
-
throw new Error('add: term must be an Alias or a string (accompanied with an implementation)');
|
|
111
|
+
term = this._named(term, impl);
|
|
108
112
|
|
|
109
|
-
if (this.annotate && note === undefined
|
|
110
|
-
|
|
113
|
+
if (this.annotate && note === undefined) {
|
|
114
|
+
const guess = term.guess();
|
|
115
|
+
if (guess.expr)
|
|
116
|
+
note = guess.expr.format({ terse: true, html: true, lambda: ['', ' ↦ ', ''] });
|
|
117
|
+
}
|
|
111
118
|
if (note !== undefined)
|
|
112
119
|
term.note = note;
|
|
113
120
|
|
|
114
|
-
this.known[
|
|
115
|
-
|
|
121
|
+
if (this.known[term.name])
|
|
122
|
+
this.known[term.name].outdated = true;
|
|
123
|
+
this.known[term.name] = term;
|
|
124
|
+
this.allow.add(term.name);
|
|
116
125
|
|
|
117
126
|
return this;
|
|
118
127
|
}
|
|
119
128
|
|
|
129
|
+
_named (term, impl) {
|
|
130
|
+
if (term instanceof Alias)
|
|
131
|
+
return new Alias(term.name, term.impl, { canonize: true });
|
|
132
|
+
if (typeof term !== 'string')
|
|
133
|
+
throw new Error('add(): term must be an Alias or a string');
|
|
134
|
+
if (impl === undefined)
|
|
135
|
+
throw new Error('add(): impl must be provided when term is a string');
|
|
136
|
+
if (typeof impl === 'string')
|
|
137
|
+
return new Alias(term, this.parse(impl), { canonize: true });
|
|
138
|
+
if (impl instanceof Expr)
|
|
139
|
+
return new Alias(term, impl, { canonize: true });
|
|
140
|
+
if (typeof impl === 'function')
|
|
141
|
+
return new Native(term, impl);
|
|
142
|
+
// idk what this is
|
|
143
|
+
throw new Error('add(): impl must be an Expr, a string, or a function with a signature Expr => ... => Expr');
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
maybeAdd (name, impl) {
|
|
121
147
|
if (this.known[name])
|
|
122
148
|
this.allow.add(name);
|
|
@@ -125,6 +151,28 @@ class SKI {
|
|
|
125
151
|
return this;
|
|
126
152
|
}
|
|
127
153
|
|
|
154
|
+
/**
|
|
155
|
+
* @desc Declare and remove multiple terms at once
|
|
156
|
+
* term=impl adds term
|
|
157
|
+
* term= removes term
|
|
158
|
+
* @param {string[]]} list
|
|
159
|
+
* @return {SKI} chainable
|
|
160
|
+
*/
|
|
161
|
+
bulkAdd (list) {
|
|
162
|
+
for (const item of list) {
|
|
163
|
+
const m = item.match(/^([A-Z]|[a-z][a-z_0-9]*)\s*=\s*(.*)$/s);
|
|
164
|
+
// TODO check all declarations before applying any (but we might need earlier terms for parsing later ones)
|
|
165
|
+
if (!m)
|
|
166
|
+
throw new Error('bulkAdd: invalid declaration: ' + item);
|
|
167
|
+
if (m[2] === '')
|
|
168
|
+
this.remove(m[1]);
|
|
169
|
+
else
|
|
170
|
+
this.add(m[1], this.parse(m[2]));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
128
176
|
/**
|
|
129
177
|
* Restrict the interpreter to given terms. Terms prepended with '+' will be added
|
|
130
178
|
* and terms preceeded with '-' will be removed.
|
|
@@ -183,6 +231,15 @@ class SKI {
|
|
|
183
231
|
return out;
|
|
184
232
|
}
|
|
185
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Export term declarations for use in bulkAdd().
|
|
236
|
+
* @returns {string[]}
|
|
237
|
+
*/
|
|
238
|
+
declare () {
|
|
239
|
+
// TODO accept argument to declare specific terms only
|
|
240
|
+
return declare(this.getTerms());
|
|
241
|
+
}
|
|
242
|
+
|
|
186
243
|
/**
|
|
187
244
|
*
|
|
188
245
|
* @param {string} source
|
|
@@ -290,11 +347,12 @@ class SKI {
|
|
|
290
347
|
|
|
291
348
|
toJSON () {
|
|
292
349
|
return {
|
|
350
|
+
version: '1.1.1', // set to incremented package.json version whenever SKI serialization changes
|
|
293
351
|
allow: this.showRestrict('+'),
|
|
294
352
|
numbers: this.hasNumbers,
|
|
295
353
|
lambdas: this.hasLambdas,
|
|
296
|
-
terms: this.getTerms(),
|
|
297
354
|
annotate: this.annotate,
|
|
355
|
+
terms: this.declare(),
|
|
298
356
|
}
|
|
299
357
|
}
|
|
300
358
|
}
|
package/lib/quest.js
CHANGED
|
@@ -225,16 +225,25 @@ class Quest {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
class Case {
|
|
228
|
+
/**
|
|
229
|
+
* @param {FreeVar[]} input
|
|
230
|
+
* @param {{
|
|
231
|
+
* max?: number,
|
|
232
|
+
* note?: string,
|
|
233
|
+
* vars?: {string: Expr},
|
|
234
|
+
* engine: SKI
|
|
235
|
+
* }} options
|
|
236
|
+
*/
|
|
228
237
|
constructor (input, options) {
|
|
229
238
|
this.max = options.max ?? 1000;
|
|
230
239
|
this.note = options.note;
|
|
231
|
-
this.vars = { ...(options.vars ?? {}) }; //
|
|
240
|
+
this.vars = { ...(options.vars ?? {}) }; // note: context already contains input placeholders
|
|
232
241
|
this.input = input;
|
|
233
242
|
this.engine = options.engine;
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
parse (src) {
|
|
237
|
-
return new
|
|
246
|
+
return new Subst(this.engine.parse(src, this.vars), this.input);
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
/**
|
|
@@ -263,17 +272,15 @@ class ExprCase extends Case {
|
|
|
263
272
|
|
|
264
273
|
super(input, options);
|
|
265
274
|
|
|
266
|
-
[this.e1, this.e2] = terms.map(
|
|
275
|
+
[this.e1, this.e2] = terms.map( s => this.parse(s) );
|
|
267
276
|
}
|
|
268
277
|
|
|
269
|
-
check (...
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
278
|
+
check (...args) {
|
|
279
|
+
const e1 = this.e1.apply(args);
|
|
280
|
+
const r1 = e1.run({ max: this.max });
|
|
281
|
+
const e2 = this.e2.apply(args);
|
|
282
|
+
const r2 = e2.run({ max: this.max });
|
|
273
283
|
|
|
274
|
-
const start = subst(this.e1, expr);
|
|
275
|
-
const r1 = start.run({ max: this.max });
|
|
276
|
-
const r2 = subst(this.e2, expr).run({ max: this.max });
|
|
277
284
|
let reason = null;
|
|
278
285
|
if (!r1.final || !r2.final)
|
|
279
286
|
reason = 'failed to reach normal form in ' + this.max + ' steps';
|
|
@@ -285,11 +292,11 @@ class ExprCase extends Case {
|
|
|
285
292
|
pass: !reason,
|
|
286
293
|
reason,
|
|
287
294
|
steps: r1.steps,
|
|
288
|
-
start,
|
|
295
|
+
start: e1,
|
|
289
296
|
found: r1.expr,
|
|
290
297
|
expected: r2.expr,
|
|
291
298
|
note: this.note,
|
|
292
|
-
args
|
|
299
|
+
args,
|
|
293
300
|
case: this,
|
|
294
301
|
};
|
|
295
302
|
}
|
|
@@ -335,7 +342,7 @@ class PropertyCase extends Case {
|
|
|
335
342
|
}
|
|
336
343
|
|
|
337
344
|
check (...expr) {
|
|
338
|
-
const start = this.expr.apply(
|
|
345
|
+
const start = this.expr.apply(expr);
|
|
339
346
|
const r = start.run({ max: this.max });
|
|
340
347
|
const guess = r.expr.guess({ max: this.max });
|
|
341
348
|
|
|
@@ -358,6 +365,29 @@ class PropertyCase extends Case {
|
|
|
358
365
|
}
|
|
359
366
|
}
|
|
360
367
|
|
|
368
|
+
class Subst {
|
|
369
|
+
/**
|
|
370
|
+
* @descr A placeholder object with exactly n free variables to be substituted later.
|
|
371
|
+
* @param {Expr} expr
|
|
372
|
+
* @param {FreeVar[]} vars
|
|
373
|
+
*/
|
|
374
|
+
constructor (expr, vars) {
|
|
375
|
+
this.expr = expr;
|
|
376
|
+
this.vars = vars;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
apply (list) {
|
|
380
|
+
if (list.length !== this.vars.length)
|
|
381
|
+
throw new Error('Subst: expected ' + this.vars.length + ' terms, got ' + list.length);
|
|
382
|
+
|
|
383
|
+
let expr = this.expr;
|
|
384
|
+
for (let i = 0; i < this.vars.length; i++)
|
|
385
|
+
expr = expr.subst(this.vars[i], list[i]) ?? expr;
|
|
386
|
+
|
|
387
|
+
return expr;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
361
391
|
function list2str (str) {
|
|
362
392
|
if (str === undefined)
|
|
363
393
|
return str;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dallaylaen/ski-interpreter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Simple Kombinator Interpreter - a combinatory logic & lambda calculus parser and interpreter. Supports SKI, BCKW, Church numerals, and setting up assertions ('quests') involving all of the above.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"combinatory logic",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types",
|
|
20
20
|
"test": "npx nyc mocha",
|
|
21
21
|
"minify": "npx esbuild --bundle ./index.js --outfile=docs/build/js/ski-interpreter.min.js --minify --sourcemap",
|
|
22
|
-
"build": "
|
|
22
|
+
"build-site": "npx esbuild --bundle ./site-src/index.js --outfile=docs/build/js/util.min.js --minify --sourcemap",
|
|
23
|
+
"build": "npm run lint && npm run types && npm run test && npm run minify && npm run build-site"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"package.json",
|