@dallaylaen/ski-interpreter 1.0.1 → 1.1.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
@@ -191,6 +191,9 @@ class SKI {
191
191
  * @return {Expr}
192
192
  */
193
193
  parse (source, vars = {}, options = {}) {
194
+ if (typeof source !== 'string')
195
+ throw new Error('parse: source must be a string, got ' + typeof source);
196
+
194
197
  const lines = source.replace(/\/\/[^\n]*$/gm, '')
195
198
  .split(/\s*;[\s;]*/).filter( s => s.match(/\S/));
196
199
 
package/lib/quest.js CHANGED
@@ -15,6 +15,26 @@ const { Expr, FreeVar, Alias, Lambda } = SKI.classes;
15
15
  * }} CaseResult
16
16
  */
17
17
 
18
+ /**
19
+ * @typedef {{
20
+ * linear: boolean?,
21
+ * affine: boolean?,
22
+ * normal: boolean?,
23
+ * proper: boolean?,
24
+ * discard: boolean?,
25
+ * duplicate: boolean?,
26
+ * arity: number?,
27
+ * }} Capability
28
+ */
29
+
30
+ /**
31
+ * @typedef {
32
+ * [string, string]
33
+ * | [{max: number?}, string, string]
34
+ * | [{caps: Capability, max: number?}, string]
35
+ * } TestCase
36
+ */
37
+
18
38
  class Quest {
19
39
  /**
20
40
  * @description A combinator problem with a set of test cases for the proposed solution.
@@ -27,7 +47,7 @@ class Quest {
27
47
  * vars: string[]?,
28
48
  * engine: SKI?,
29
49
  * engineFull: SKI?,
30
- * cases: [{max: number?, note: string?, feedInput: boolean, lambdas: boolean?}|string[], ...string[][]]?
50
+ * cases: TestCase[],
31
51
  * }} options
32
52
  */
33
53
  constructor (options = {}) {
@@ -124,8 +144,8 @@ class Quest {
124
144
 
125
145
  const input = this.input.map( t => t.placeholder );
126
146
  this.cases.push(
127
- opt.linear
128
- ? new LinearCase(input, opt, terms)
147
+ opt.caps
148
+ ? new PropertyCase(input, opt, terms)
129
149
  : new ExprCase(input, opt, terms)
130
150
  );
131
151
  return this;
@@ -275,30 +295,66 @@ class ExprCase extends Case {
275
295
  }
276
296
  }
277
297
 
278
- class LinearCase extends Case {
298
+ const knownCaps = {
299
+ normal: true,
300
+ proper: true,
301
+ discard: true,
302
+ duplicate: true,
303
+ linear: true,
304
+ affine: true,
305
+ arity: true,
306
+ }
307
+
308
+ class PropertyCase extends Case {
279
309
  // test that an expression uses all of its inputs exactly once
280
310
  constructor (input, options, terms) {
281
311
  super(input, options);
312
+ if (terms.length > 1)
313
+ throw new Error('PropertyCase accepts exactly 1 string');
314
+ if (!options.caps || typeof options.caps !== 'object' || !Object.keys(options.caps).length)
315
+ throw new Error('PropertyCase requires a caps object with at least one capability');
316
+ const unknown = Object.keys(options.caps).filter( c => !knownCaps[c] );
317
+ if (unknown.length)
318
+ throw new Error('PropertyCase: don\'t know how to test these capabilities: ' + unknown.join(', '));
319
+
282
320
  this.expr = this.parse(terms[0]);
321
+ this.caps = options.caps;
322
+
323
+ if (this.caps.linear) {
324
+ delete this.caps.linear;
325
+ this.caps.duplicate = false;
326
+ this.caps.discard = false;
327
+ this.caps.normal = true;
328
+ }
329
+
330
+ if (this.caps.affine) {
331
+ delete this.caps.affine;
332
+ this.caps.normal = true;
333
+ this.caps.duplicate = false;
334
+ }
283
335
  }
284
336
 
285
337
  check (...expr) {
286
338
  const start = this.expr.apply(...expr);
287
339
  const r = start.run({ max: this.max });
288
- const arity = r.expr.canonize();
289
- const reason = arity.linear
290
- ? null
291
- : 'expected a linear expression, i.e. such that uses all inputs exactly once';
340
+ const guess = r.expr.guess({ max: this.max });
341
+
342
+ const reason = [];
343
+ for (const cap in this.caps) {
344
+ if (guess[cap] !== this.caps[cap])
345
+ reason.push('expected property ' + cap + ' to be ' + this.caps[cap] + ', found ' + guess[cap]);
346
+ }
347
+
292
348
  return {
293
- pass: !reason,
294
- reason,
295
- steps: r.steps,
349
+ pass: !reason.length,
350
+ reason: reason ? reason.join('\n') : null,
351
+ steps: r.steps,
296
352
  start,
297
- found: r.expr,
298
- case: this,
299
- note: this.note,
300
- args: expr,
301
- }
353
+ found: r.expr,
354
+ case: this,
355
+ note: this.note,
356
+ args: expr,
357
+ };
302
358
  }
303
359
  }
304
360
 
package/lib/util.js CHANGED
@@ -54,13 +54,17 @@ function restrict (set, spec) {
54
54
  return out;
55
55
  }
56
56
 
57
- function missingIndices (arr, set) {
58
- const out = new Set();
57
+ function skipDup (arr, map) {
58
+ const skip = new Set();
59
+ const dup = new Set();
59
60
  for (let n = 0; n < arr.length; n++) {
60
- if (!set.has(arr[n]))
61
- out.add(n);
61
+ const count = map.get(arr[n]) ?? 0;
62
+ if (!count)
63
+ skip.add(n);
64
+ else if (count > 1)
65
+ dup.add(n);
62
66
  }
63
- return out;
67
+ return [skip, dup];
64
68
  }
65
69
 
66
70
  function isSubset (a, b) {
@@ -71,4 +75,4 @@ function isSubset (a, b) {
71
75
  return true;
72
76
  }
73
77
 
74
- module.exports = { Tokenizer, restrict, missingIndices, isSubset };
78
+ module.exports = { Tokenizer, restrict, skipDup, isSubset };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dallaylaen/ski-interpreter",
3
- "version": "1.0.1",
3
+ "version": "1.1.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",
@@ -15,12 +15,17 @@
15
15
  "ski": "bin/ski.js"
16
16
  },
17
17
  "scripts": {
18
- "test": "nyc mocha"
18
+ "lint": "npx eslint lib --ext .js,.ts",
19
+ "types": "npx -p typescript tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir types",
20
+ "test": "npx nyc mocha",
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"
19
23
  },
20
24
  "files": [
21
25
  "package.json",
22
26
  "README.md",
23
27
  "LICENSE",
28
+ "CHANGELOG.md",
24
29
  "lib",
25
30
  "types"
26
31
  ],
@@ -35,12 +40,14 @@
35
40
  },
36
41
  "homepage": "https://dallaylaen.github.io/ski-interpreter/index.html",
37
42
  "devDependencies": {
43
+ "chai": "^4.3.7",
44
+ "esbuild": "^0.25.10",
38
45
  "eslint": "^8.57.0",
39
46
  "eslint-config-standard": "^17.1.0",
40
47
  "eslint-plugin-align-assignments": "^1.1.2",
41
48
  "eslint-plugin-import": "^2.29.1",
42
- "typescript": "^5.8.2",
43
49
  "mocha": "^10.2.0",
44
- "nyc": "^15.1.0"
50
+ "nyc": "^15.1.0",
51
+ "typescript": "^5.8.2"
45
52
  }
46
53
  }
@@ -1,6 +1,5 @@
1
1
  export type AnyArity = (arg0: Expr) => Expr | AnyArity;
2
2
  export class Expr {
3
- arity: number;
4
3
  /**
5
4
  * postprocess term after parsing. typically return self but may return other term or die
6
5
  * @return {Expr}
@@ -31,37 +30,67 @@ export class Expr {
31
30
  * @return {Map<Expr, number>}
32
31
  */
33
32
  getSymbols(): Map<Expr, number>;
33
+ /**
34
+ * @desc Given a list of pairs of term, replaces every subtree
35
+ * that is equivalent to the first term in pair with the second one.
36
+ * If a simgle term is given, it is duplicated into a pair.
37
+ *
38
+ * @example S(SKK)(SKS).replace('I') = SII // we found 2 subtrees equivalent to I
39
+ * and replaced them with I
40
+ *
41
+ * @param {(Expr | [find: Expr, replace: Expr])[]} terms
42
+ * @param {Object} [opt] - options
43
+ * @return {Expr}
44
+ */
45
+ replace(terms: (Expr | [find: Expr, replace: Expr])[], opt?: any): Expr;
46
+ _replace(pairs: any, opt: any): any;
34
47
  /**
35
48
  * @desc rought estimate of the complexity of the term
36
49
  * @return {number}
37
50
  */
38
51
  weight(): number;
39
52
  /**
53
+ * @desc Try to find an equivalent lambda term for the expression,
54
+ * returning also the term's arity and some other properties.
55
+ *
56
+ * This is used internally when declaring a Native term,
57
+ * unless {canonize: false} is used.
58
+ *
59
+ * As of current it only recognizes terms that have a normal form,
60
+ * perhaps after adding some variables. This may change in the future.
61
+ *
62
+ * Use lambdify() if you want to get a lambda term in any case.
40
63
  *
41
- * @param {{max: number?, maxArgs: number?, bestGuess?: Expr}} options
64
+ * @param {{max: number?, maxArgs: number?}} options
42
65
  * @return {{
43
- * found: boolean,
44
- * proper: boolean,
66
+ * normal: boolean,
67
+ * steps: number,
68
+ * expr: Expr?,
45
69
  * arity: number?,
46
- * linear: boolean?,
47
- * canonical?: Expr,
48
- * steps: number?,
49
- * skip: Set<number>?
70
+ * proper: boolean?,
71
+ * discard: boolean?,
72
+ * duplicate: boolean?,
73
+ * skip: Set<number>?,
74
+ * dup: Set<number>?
50
75
  * }}
51
76
  */
52
- canonize(options?: {
77
+ guess(options?: {
53
78
  max: number | null;
54
79
  maxArgs: number | null;
55
- bestGuess?: Expr;
56
80
  }): {
57
- found: boolean;
58
- proper: boolean;
81
+ normal: boolean;
82
+ steps: number;
83
+ expr: Expr | null;
59
84
  arity: number | null;
60
- linear: boolean | null;
61
- canonical?: Expr;
62
- steps: number | null;
85
+ proper: boolean | null;
86
+ discard: boolean | null;
87
+ duplicate: boolean | null;
63
88
  skip: Set<number> | null;
89
+ dup: Set<number> | null;
64
90
  };
91
+ _guess(options: any, preArgs?: any[], steps?: number): any;
92
+ _aslist(): this[];
93
+ _firstVar(): boolean;
65
94
  /**
66
95
  * @desc Returns a series of lambda terms equivalent to the given expression,
67
96
  * up to the provided computation steps limit,
@@ -110,13 +139,6 @@ export class Expr {
110
139
  */
111
140
  renameVars(seq: IterableIterator<string>): Expr;
112
141
  _rski(options: any): this;
113
- /**
114
- * @desc Whether the term will reduce further if given more arguments.
115
- * In practice, equivalent to "starts with a FreeVar"
116
- * Used by canonize (duh...)
117
- * @return {boolean}
118
- */
119
- wantsArgs(): boolean;
120
142
  /**
121
143
  * Apply self to list of given args.
122
144
  * Normally, only native combinators know how to do it.
@@ -172,13 +194,18 @@ export class Expr {
172
194
  steps: number;
173
195
  }>;
174
196
  /**
175
- *
176
- * @param {Expr} other
177
- * @return {boolean}
178
- */
197
+ *
198
+ * @param {Expr} other
199
+ * @return {boolean}
200
+ */
179
201
  equals(other: Expr): boolean;
180
202
  contains(other: any): boolean;
181
- expect(other: any): void;
203
+ /**
204
+ * @desc Assert expression equality. Can be used in tests.
205
+ * @param {Expr} expected
206
+ * @param {string} comment
207
+ */
208
+ expect(expected: Expr, comment?: string): void;
182
209
  /**
183
210
  * @param {{terse: boolean?, html: boolean?}} [options]
184
211
  * @return {string} string representation of the expression
@@ -188,10 +215,11 @@ export class Expr {
188
215
  html: boolean | null;
189
216
  }): string;
190
217
  /**
191
- *
218
+ * @desc Whether the expression needs parentheses when printed.
219
+ * @param {boolean} [first] - whether this is the first term in a sequence
192
220
  * @return {boolean}
193
221
  */
194
- needsParens(): boolean;
222
+ needsParens(first?: boolean): boolean;
195
223
  /**
196
224
  *
197
225
  * @return {string}
@@ -209,22 +237,16 @@ export class App extends Expr {
209
237
  * @param {Expr} args
210
238
  */
211
239
  constructor(fun: Expr, ...args: Expr);
212
- fun: Expr;
213
- args: Expr;
240
+ arg: any;
241
+ fun: any;
214
242
  final: boolean;
215
- weight(): Expr;
216
- apply(...args: any[]): any;
217
- canonize(options?: {}): {
218
- found: boolean;
219
- proper: boolean;
220
- arity: number | null;
221
- linear: boolean | null;
222
- canonical?: Expr;
223
- steps: number | null;
224
- skip: Set<number> | null;
225
- };
226
- renameVars(seq: any): Expr;
227
- subst(plug: any, value: any): Expr;
243
+ arity: any;
244
+ weight(): any;
245
+ _firstVar(): any;
246
+ apply(...args: any[]): App;
247
+ expand(): any;
248
+ renameVars(seq: any): any;
249
+ subst(plug: any, value: any): any;
228
250
  /**
229
251
  * @return {{expr: Expr, steps: number}}
230
252
  */
@@ -232,8 +254,12 @@ export class App extends Expr {
232
254
  expr: Expr;
233
255
  steps: number;
234
256
  };
257
+ reduce(args: any): any;
235
258
  split(): any[];
236
- equals(other: any): boolean;
259
+ _aslist(): any[];
260
+ equals(other: any): any;
261
+ contains(other: any): any;
262
+ needsParens(first: any): boolean;
237
263
  toString(opt?: {}): string;
238
264
  }
239
265
  export class FreeVar extends Named {
@@ -250,6 +276,7 @@ export class Lambda extends Expr {
250
276
  constructor(arg: FreeVar | FreeVar[], impl: Expr);
251
277
  arg: FreeVar;
252
278
  impl: Expr;
279
+ arity: number;
253
280
  reduce(input: any): Expr;
254
281
  subst(plug: any, value: any): Lambda;
255
282
  expand(): Lambda;
@@ -257,6 +284,7 @@ export class Lambda extends Expr {
257
284
  _rski(options: any): any;
258
285
  equals(other: any): boolean;
259
286
  toString(opt?: {}): string;
287
+ needsParens(first: any): boolean;
260
288
  }
261
289
  /**
262
290
  * @typedef {function(Expr): Expr | AnyArity} AnyArity
@@ -283,7 +311,7 @@ export class Native extends Named {
283
311
  arity: any;
284
312
  note: any;
285
313
  apply(...args: any[]): Expr;
286
- _rski(options: any): Expr | this;
314
+ _rski(options: any): any;
287
315
  reduce(args: any): any;
288
316
  }
289
317
  export class Alias extends Named {
@@ -319,6 +347,7 @@ export class Alias extends Named {
319
347
  equals(other: any): any;
320
348
  _rski(options: any): Expr;
321
349
  toString(opt: any): string;
350
+ needsParens(first: any): boolean;
322
351
  }
323
352
  export class Church extends Native {
324
353
  constructor(n: any);
@@ -9,6 +9,15 @@ export type CaseResult = {
9
9
  args: typeof import("./expr").Expr[];
10
10
  case: Case;
11
11
  };
12
+ export type Capability = {
13
+ linear: boolean | null;
14
+ affine: boolean | null;
15
+ normal: boolean | null;
16
+ proper: boolean | null;
17
+ discard: boolean | null;
18
+ duplicate: boolean | null;
19
+ arity: number | null;
20
+ };
12
21
  /**
13
22
  * @typedef {{
14
23
  * pass: boolean,
@@ -22,6 +31,24 @@ export type CaseResult = {
22
31
  * case: Case
23
32
  * }} CaseResult
24
33
  */
34
+ /**
35
+ * @typedef {{
36
+ * linear: boolean?,
37
+ * affine: boolean?,
38
+ * normal: boolean?,
39
+ * proper: boolean?,
40
+ * discard: boolean?,
41
+ * duplicate: boolean?,
42
+ * arity: number?,
43
+ * }} Capability
44
+ */
45
+ /**
46
+ * @typedef {
47
+ * [string, string]
48
+ * | [{max: number?}, string, string]
49
+ * | [{caps: Capability, max: number?}, string]
50
+ * } TestCase
51
+ */
25
52
  export class Quest {
26
53
  /**
27
54
  * @description A combinator problem with a set of test cases for the proposed solution.
@@ -34,7 +61,7 @@ export class Quest {
34
61
  * vars: string[]?,
35
62
  * engine: SKI?,
36
63
  * engineFull: SKI?,
37
- * cases: [{max: number?, note: string?, feedInput: boolean, lambdas: boolean?}|string[], ...string[][]]?
64
+ * cases: TestCase[],
38
65
  * }} options
39
66
  */
40
67
  constructor(options?: {
@@ -46,12 +73,7 @@ export class Quest {
46
73
  vars: string[] | null;
47
74
  engine: SKI | null;
48
75
  engineFull: SKI | null;
49
- cases: [{
50
- max: number | null;
51
- note: string | null;
52
- feedInput: boolean;
53
- lambdas: boolean | null;
54
- } | string[], ...string[][]] | null;
76
+ cases: TestCase[];
55
77
  });
56
78
  engine: SKI;
57
79
  engineFull: SKI;
@@ -9,5 +9,5 @@ export class Tokenizer {
9
9
  split(str: string): string[];
10
10
  }
11
11
  export function restrict(set: any, spec: any): any;
12
- export function missingIndices(arr: any, set: any): any;
12
+ export function skipDup(arr: any, map: any): any[];
13
13
  export function isSubset(a: any, b: any): boolean;