@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/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: string?,
59
- * numbers: boolean?,
60
- * lambdas: boolean?,
61
- * terms: { [key: string]: Expr|string}?,
62
- * annotate: boolean?,
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
- for (const name in options.terms ?? {}) {
74
- // Native terms already handled by allow
75
- if (!options.terms[name].match(/^Native:/))
76
- this.add(name, options.terms[name]);
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|String|[number, function(...Expr): Expr, {note: string?, fast: boolean?}]} [impl]
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
- if (typeof term === 'string') {
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 && term.canonical)
110
- note = term.canonical.toString({ terse: true, html: true });
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['' + term] = term;
115
- this.allow.add('' + term);
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 ?? {}) }; // shallow copy to avoid modifying the original
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 Lambda(this.input, this.engine.parse(src, this.vars));
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(src => this.parse(src));
275
+ [this.e1, this.e2] = terms.map( s => this.parse(s) );
267
276
  }
268
277
 
269
- check (...expr) {
270
- // we do it the fancy way and instead of just "apply" to avoid
271
- // displaying (foo->foo this that)(user input) as 1st step
272
- const subst = (outer, inner) => outer.reduce(inner) ?? outer.apply(...inner);
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: expr,
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(...expr);
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.1.0",
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": "npm run lint && npm run types && npm run test && npm run minify"
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",