@dallaylaen/ski-interpreter 1.1.0 → 1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2025-12-14
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ - Remove `toString()` options, use `format()` instead.
13
+ - Make `needsParens()` private (should've been to begin with)
14
+ - Remove unused `renameVars()` method.
15
+ - Remove Expr.`toJSON()`
16
+
17
+ ### Added
18
+
19
+ - SKI: `toJSON()` now recreates declarations exactly, preserving named subexpressions.
20
+ - SKI: `declare()` / `bulkAdd()` methods to export/import term definitions.
21
+ - Expr: `format(options?)` method for pretty-printing expressions with various options: html, verbosity, custom lambdas, custom brackets etc.
22
+ - Expr: `subst(find, replace)` now works for any type of find except application and lambdas.
23
+ - Playground: permalinks now use #hash instead of a ?query string. (Old links still supported).
24
+ - Playground: togglable frames around subexpressions & variable/redex highlighting.
25
+
8
26
  ## [1.1.0] - 2025-12-07
9
27
 
10
28
  ### BREAKING CHANGES
@@ -21,7 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
39
  - Expr: guess() method to normalize terms.
22
40
  Returns an object with `normal`: boolean and `steps`:
23
41
  number properties, as well as optional `expr`: Expr -
24
- equivalent lambda expression; `arity`: number,
42
+ equivalent lambda expression; `arity`: number,
25
43
  and other properties.
26
44
  - Expr: replace(terms: Expr[], options: {}) replaces
27
45
  subtrees with matching canonical form (if they have one).
package/bin/ski.js CHANGED
@@ -69,7 +69,7 @@ function runLine(onErr) {
69
69
  if (state.final && !options.q)
70
70
  console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
71
71
  if (options.v || state.final)
72
- console.log('' + state.expr.toString({terse: options.t}));
72
+ console.log('' + state.expr.format({terse: options.t}));
73
73
  if (state.final && expr instanceof SKI.classes.Alias)
74
74
  ski.add(expr.name, state.expr);
75
75
  }
package/lib/expr.js CHANGED
@@ -141,13 +141,13 @@ class Expr {
141
141
  * @return {{
142
142
  * normal: boolean,
143
143
  * steps: number,
144
- * expr: Expr?,
145
- * arity: number?,
146
- * proper: boolean?,
147
- * discard: boolean?,
148
- * duplicate: boolean?,
149
- * skip: Set<number>?,
150
- * dup: Set<number>?
144
+ * expr?: Expr,
145
+ * arity?: number,
146
+ * proper?: boolean,
147
+ * discard?: boolean,
148
+ * duplicate?: boolean,
149
+ * skip?: Set<number>,
150
+ * dup?: Set<number>,
151
151
  * }}
152
152
  */
153
153
  guess (options = {}) {
@@ -202,12 +202,12 @@ class Expr {
202
202
  * up to the provided computation steps limit,
203
203
  * in decreasing weight order.
204
204
  * @param {{
205
- * max: number?,
206
- * maxArgs: number?,
207
- * varGen: function(void): FreeVar?,
208
- * steps: number?,
209
- * html: boolean?,
210
- * latin: number?,
205
+ * max?: number,
206
+ * maxArgs?: number,
207
+ * varGen?: function(void): FreeVar,
208
+ * steps?: number,
209
+ * html?: boolean,
210
+ * latin?: number,
211
211
  * }} options
212
212
  * @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
213
213
  * @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
@@ -238,17 +238,6 @@ class Expr {
238
238
  }
239
239
  }
240
240
 
241
- /**
242
- * @desc Rename free variables in the expression using the given sequence
243
- * This is for eye-candy only, as the interpreter knows darn well hot to distinguish vars,
244
- * regardless of names.
245
- * @param {IterableIterator<string>} seq
246
- * @return {Expr}
247
- */
248
- renameVars (seq) {
249
- return this;
250
- }
251
-
252
241
  _rski (options) {
253
242
  return this;
254
243
  }
@@ -264,14 +253,18 @@ class Expr {
264
253
  }
265
254
 
266
255
  /**
267
- * Replace all instances of free vars with corresponding values and return the resulting expression.
268
- * return null if no changes could be made.
269
- * @param {FreeVar} plug
270
- * @param {Expr} value
271
- * @return {Expr|null}
272
- */
273
- subst (plug, value) {
274
- return null;
256
+ * Replace all instances of plug in the expression with value and return the resulting expression,
257
+ * or null if no changes could be made.
258
+ * Lambda terms and applications will never match if used as plug
259
+ * as they are impossible co compare without extensive computations.
260
+ * Typically used on variables but can also be applied to other terms, e.g. aliases.
261
+ * See also Expr.replace().
262
+ * @param {Expr} search
263
+ * @param {Expr} replace
264
+ * @return {Expr|null}
265
+ */
266
+ subst (search, replace) {
267
+ return this === search ? replace : null;
275
268
  }
276
269
 
277
270
  /**
@@ -371,19 +364,18 @@ class Expr {
371
364
  // TODO wanna use AssertionError but webpack doesn't recognize it
372
365
  // still the below hack works for mocha-based tests.
373
366
  const poorMans = new Error(comment + 'found term ' + this + ' but expected ' + expected);
374
- poorMans.expected = expected.toString();
375
- poorMans.actual = this.toString();
367
+ poorMans.expected = expected + '';
368
+ poorMans.actual = this + '';
376
369
  throw poorMans;
377
370
  }
378
371
 
379
372
  /**
380
- * @param {{terse: boolean?, html: boolean?}} [options]
381
- * @return {string} string representation of the expression
373
+ * @desc Returns string representation of the expression.
374
+ * Same as format() without options.
375
+ * @return {string}
382
376
  */
383
- toString (options = {}) {
384
- // uncomment the following line if you want to debug the parser with prints
385
- // return this.constructor.name
386
- throw new Error( 'No toString() method defined in class ' + this.constructor.name );
377
+ toString () {
378
+ return this.format();
387
379
  }
388
380
 
389
381
  /**
@@ -391,17 +383,83 @@ class Expr {
391
383
  * @param {boolean} [first] - whether this is the first term in a sequence
392
384
  * @return {boolean}
393
385
  */
394
- needsParens (first) {
386
+ _braced (first) {
395
387
  return false;
396
388
  }
397
389
 
390
+ _unspaced (arg) {
391
+ return this._braced(true);
392
+ }
393
+
398
394
  /**
395
+ * @desc Stringify the expression with fancy formatting options.
396
+ * Said options mostly include wrappers around various constructs in form of ['(', ')'],
397
+ * as well as terse and html flags that set up the defaults.
398
+ * Format without options is equivalent to toString() and can be parsed back.
399
+ *
400
+ * @param {Object} [options] - formatting options
401
+ * @param {boolean} [options.terse] - whether to use terse formatting (omitting unnecessary spaces and parentheses)
402
+ * @param {boolean} [options.html] - whether to default to HTML tags & entities
403
+ * @param {[string, string]} [options.brackets] - wrappers for application arguments, typically ['(', ')']
404
+ * @param {[string, string]} [options.var] - wrappers for variable names
405
+ * (will default to &lt;var&gt; and &lt;/var&gt; in html mode)
406
+ * @param {[string, string, string]} [options.lambda] - wrappers for lambda abstractions, e.g. ['&lambda;', '.', '']
407
+ * where the middle string is placed between argument and body
408
+ * default is ['', '->', ''] or ['', '-&gt;', ''] for html
409
+ * @param {[string, string]} [options.around] - wrappers around (sub-)expressions.
410
+ * individual applications will not be wrapped, i.e. (a b c) but not ((a b) c)
411
+ * @param {[string, string]} [options.redex] - wrappers around the starting term(s) that have enough arguments to be reduced
412
+ * @param {Object<string, Expr>} [options.inventory] - if given, output aliases in the set as their names
413
+ * and any other aliases as the expansion of their definitions.
414
+ * The default is a cryptic and fragile mechanism dependent on a hidden mutable property.
415
+ * @returns {string}
416
+ *
417
+ * @example foo.format() // equivalent to foo.toString()
418
+ * @example foo.format({terse: false}) // spell out all parentheses
419
+ * @example foo.format({html: true}) // use HTML tags and entities
420
+ * @example foo.format({ around: ['(', ')'], brackets: ['', ''], lambda: ['(', '->', ')'] }) // lisp style, still back-parsable
421
+ * @exapmle foo.format({ lambda: ['&lambda;', '.', ''] }) // pretty-print for the math department
422
+ * @example foo.format({ lambda: ['', '=>', ''], terse: false }) // make it javascript
423
+ * @example foo.format({ inventory: { T } }) // use T as a named term, expand all others
399
424
  *
400
- * @return {string}
401
425
  */
402
- toJSON () {
403
- return this.expand().toString({ terse: false });
426
+
427
+ format (options = {}) {
428
+ const defaults = options.html
429
+ ? {
430
+ brackets: ['(', ')'],
431
+ space: ' ',
432
+ var: ['<var>', '</var>'],
433
+ lambda: ['', '-&gt;', ''],
434
+ around: ['', ''],
435
+ redex: ['<b>', '</b>'],
436
+ }
437
+ : {
438
+ brackets: ['(', ')'],
439
+ space: ' ',
440
+ var: ['', ''],
441
+ lambda: ['', '->', ''],
442
+ around: ['', ''],
443
+ redex: ['', ''],
444
+ }
445
+ return this._format({
446
+ terse: options.terse ?? globalOptions.terse,
447
+ brackets: options.brackets ?? defaults.brackets,
448
+ space: options.space ?? defaults.space,
449
+ var: options.var ?? defaults.var,
450
+ lambda: options.lambda ?? defaults.lambda,
451
+ around: options.around ?? defaults.around,
452
+ redex: options.redex ?? defaults.redex,
453
+ inventory: options.inventory, // TODO better name
454
+ }, 0);
455
+ }
456
+
457
+ _format (options, nargs) {
458
+ throw new Error( 'No _format() method defined in class ' + this.constructor.name );
404
459
  }
460
+
461
+ // output: string[] /* appended */, inventory: { [key: string]: Expr }, seen: Set<Expr>
462
+ _declare (output, inventory, seen) {}
405
463
  }
406
464
 
407
465
  class App extends Expr {
@@ -504,13 +562,9 @@ class App extends Expr {
504
562
  return (fun._replace(pairs, opt) ?? fun).apply(arg._replace(pairs, opt) ?? arg);
505
563
  }
506
564
 
507
- renameVars (seq) {
508
- return this.fun.renameVars(seq).apply(this.arg.renameVars(seq));
509
- }
510
-
511
- subst (plug, value) {
512
- const fun = this.fun.subst(plug, value);
513
- const arg = this.arg.subst(plug, value);
565
+ subst (search, replace) {
566
+ const fun = this.fun.subst(search, replace);
567
+ const arg = this.arg.subst(search, replace);
514
568
 
515
569
  return (fun || arg) ? (fun ?? this.fun).apply(arg ?? this.arg) : null;
516
570
  }
@@ -579,25 +633,28 @@ class App extends Expr {
579
633
  return this.fun.contains(other) || this.arg.contains(other) || super.contains(other);
580
634
  }
581
635
 
582
- needsParens (first) {
636
+ _braced (first) {
583
637
  return !first;
584
638
  }
585
639
 
586
- toString (opt = {}) {
587
- const fun = this.fun.toString(opt);
588
- const root = this.fun.needsParens(true) ? '(' + fun + ')' : fun;
589
- if (opt.terse ?? globalOptions.terse) {
590
- // terse mode: omit whitespace and parens if possible
591
- let arg = this.arg.toString(opt);
592
- if (this.arg.needsParens(false))
593
- arg = '(' + arg + ')';
594
- const space = (root.match(/\)$/) || arg.match(/^\(/))
595
- || (root.match(/[A-Z]$/) && arg.match(/^[a-z]/i))
596
- ? ''
597
- : ' ';
598
- return root + space + arg;
599
- } else
600
- return root + '(' + this.arg.toString(opt) + ')';
640
+ _format (options, nargs) {
641
+ const fun = this.fun._format(options, nargs + 1);
642
+ const arg = this.arg._format(options, 0);
643
+ const wrap = nargs ? ['', ''] : options.around;
644
+ // TODO ignore terse for now
645
+ if (options.terse && !this.arg._braced(false))
646
+ return wrap[0] + fun + (this.fun._unspaced(this.arg) ? '' : options.space) + arg + wrap[1];
647
+ else
648
+ return wrap[0] + fun + options.brackets[0] + arg + options.brackets[1] + wrap[1];
649
+ }
650
+
651
+ _declare (output, inventory, seen) {
652
+ this.fun._declare(output, inventory, seen);
653
+ this.arg._declare(output, inventory, seen);
654
+ }
655
+
656
+ _unspaced (arg) {
657
+ return this.arg._braced(false) ? true : this.arg._unspaced(arg);
601
658
  }
602
659
  }
603
660
 
@@ -613,8 +670,19 @@ class Named extends Expr {
613
670
  this.name = name;
614
671
  }
615
672
 
616
- toString () {
617
- return this.name;
673
+ _unspaced (arg) {
674
+ return !!(
675
+ (arg instanceof Named) && (
676
+ (this.name.match(/^[A-Z+]$/) && arg.name.match(/^[a-z+]/i))
677
+ || (this.name.match(/^[a-z+]/i) && arg.name.match(/^[A-Z+]$/))
678
+ )
679
+ );
680
+ }
681
+
682
+ _format (options, nargs) {
683
+ return this.arity > 0 && this.arity <= nargs
684
+ ? options.redex[0] + this.name + options.redex[1]
685
+ : this.name;
618
686
  }
619
687
  }
620
688
 
@@ -626,12 +694,6 @@ class FreeVar extends Named {
626
694
  this.id = ++freeId;
627
695
  }
628
696
 
629
- subst (plug, value) {
630
- if (this === plug)
631
- return value;
632
- return null;
633
- }
634
-
635
697
  weight () {
636
698
  return 0;
637
699
  }
@@ -640,8 +702,8 @@ class FreeVar extends Named {
640
702
  return true;
641
703
  }
642
704
 
643
- toString ( opt = {} ) {
644
- return (opt.html && /^[a-z]$/.test(this.name)) ? '<var>' + this.name + '</var>' : this.name;
705
+ _format (options, nargs) {
706
+ return options.var[0] + this.name + options.var[1];
645
707
  }
646
708
  }
647
709
 
@@ -674,7 +736,7 @@ class Native extends Named {
674
736
  if (!opt.arity)
675
737
  this.arity = guess.arity || 1;
676
738
 
677
- this.note = opt.note ?? guess.expr?.toString({ terse: true, html: true });
739
+ this.note = opt.note ?? guess.expr?.format({ terse: true, html: true, lambda: ['', ' &mapsto; ', ''] });
678
740
  }
679
741
 
680
742
  apply (...args) {
@@ -714,10 +776,6 @@ class Native extends Named {
714
776
  throw new Error('Native combinator ' + this + ' reduced to a non-expression: ' + step);
715
777
  return step.apply(...args.slice(egde));
716
778
  }
717
-
718
- toJSON () {
719
- return 'Native:' + this.name;
720
- }
721
779
  }
722
780
 
723
781
  const native = {};
@@ -788,25 +846,17 @@ class Lambda extends Expr {
788
846
  return (this.impl.subst(this.arg, head) ?? this.impl).apply(...tail);
789
847
  }
790
848
 
791
- subst (plug, value) {
792
- if (plug === this.arg)
849
+ subst (search, replace) {
850
+ if (search === this.arg)
793
851
  return null;
794
- const change = this.impl.subst(plug, value);
795
- if (change)
796
- return new Lambda(this.arg, change);
797
- return null;
852
+ const change = this.impl.subst(search, replace);
853
+ return change ? new Lambda(this.arg, change) : null;
798
854
  }
799
855
 
800
856
  expand () {
801
857
  return new Lambda(this.arg, this.impl.expand());
802
858
  }
803
859
 
804
- renameVars (seq) {
805
- const arg = new FreeVar(seq.next().value);
806
- const impl = this.impl.subst(this.arg, arg) ?? this.impl;
807
- return new Lambda(arg, impl.renameVars(seq));
808
- }
809
-
810
860
  _rski (options) {
811
861
  const impl = this.impl._rski(options);
812
862
  if (options.steps >= options.max)
@@ -851,12 +901,20 @@ class Lambda extends Expr {
851
901
  return this.equals(other) || this.impl.contains(other);
852
902
  }
853
903
 
854
- toString (opt = {}) {
855
- const mapsto = opt.html ? ' &mapsto; ' : '->';
856
- return this.arg.toString(opt) + mapsto + this.impl.toString(opt);
904
+ _format (options, nargs) {
905
+ return (nargs > 0 ? options.brackets[0] : '')
906
+ + options.lambda[0]
907
+ + this.arg._format(options, 0) // TODO highlight redex if nargs > 0
908
+ + options.lambda[1]
909
+ + this.impl._format(options, 0) + options.lambda[2]
910
+ + (nargs > 0 ? options.brackets[1] : '');
857
911
  }
858
912
 
859
- needsParens (first) {
913
+ _declare (output, inventory, seen) {
914
+ this.impl._declare(output, inventory, seen);
915
+ }
916
+
917
+ _braced (first) {
860
918
  return true;
861
919
  }
862
920
  }
@@ -885,6 +943,10 @@ class Church extends Native {
885
943
  return this.n === other.n;
886
944
  return super.equals(other);
887
945
  }
946
+
947
+ _unspaced (arg) {
948
+ return false;
949
+ }
888
950
  }
889
951
 
890
952
  class Alias extends Named {
@@ -922,8 +984,10 @@ class Alias extends Named {
922
984
  return this.impl.expand();
923
985
  }
924
986
 
925
- subst (plug, value) {
926
- return this.impl.subst(plug, value);
987
+ subst (search, replace) {
988
+ if (this === search)
989
+ return replace;
990
+ return this.impl.subst(search, replace);
927
991
  }
928
992
 
929
993
  _guess (options, preArgs = [], steps = 0) {
@@ -964,15 +1028,34 @@ class Alias extends Named {
964
1028
  return this.impl._rski(options);
965
1029
  }
966
1030
 
967
- toString (opt) {
968
- return this.outdated ? this.impl.toString(opt) : super.toString(opt);
1031
+ _braced (first) {
1032
+ return this.outdated ? this.impl._braced(first) : false;
969
1033
  }
970
1034
 
971
- needsParens (first) {
972
- return this.outdated ? this.impl.needsParens() : false;
1035
+ _format (options, nargs) {
1036
+ const outdated = options.inventory
1037
+ ? options.inventory[this.name] !== this
1038
+ : this.outdated;
1039
+ return outdated ? this.impl._format(options, nargs) : super._format(options, nargs);
1040
+ }
1041
+
1042
+ _declare (output, inventory, seen) {
1043
+ // only once
1044
+ if (seen.has(this))
1045
+ return;
1046
+ seen.add(this);
1047
+
1048
+ // topological order
1049
+ this.impl._declare(output, inventory, seen);
1050
+
1051
+ // only declare if in inventory and matches
1052
+ if (inventory[this.name] === this)
1053
+ output.push(this.name + '=' + this.impl.format({ terse: true, inventory }));
973
1054
  }
974
1055
  }
975
1056
 
1057
+ // ----- Expr* classes end here -----
1058
+
976
1059
  // declare native combinators
977
1060
  addNative('I', x => x);
978
1061
  addNative('K', x => _ => x);
@@ -986,6 +1069,72 @@ addNative('+', x => y => z => y.apply(x.apply(y, z)), {
986
1069
  apply: arg => arg instanceof Church ? new Church(arg.n + 1) : null
987
1070
  });
988
1071
 
1072
+ // A global value meaning "lambda is used somewhere in this expression"
1073
+ // Can't be used (at least for now) to construct lambda expressions, or anything at all.
1074
+ // See also getSymbols().
1075
+ Expr.lambdaPlaceholder = new Native('->', x => x, {
1076
+ arity: 1,
1077
+ canonize: false,
1078
+ note: 'Lambda placeholder',
1079
+ apply: x => { throw new Error('Attempt to use a placeholder in expression') }
1080
+ });
1081
+
1082
+ // utility functions dependent on Expr* classes, in alphabetical order
1083
+
1084
+ /**
1085
+ *
1086
+ * @param {Expr[]} inventory
1087
+ * @return {string[]}
1088
+ */
1089
+ function declare (inventory) {
1090
+ const misnamed = Object.keys(inventory)
1091
+ .filter(s => !(inventory[s] instanceof Named && inventory[s].name === s))
1092
+ .map(s => s + ' = ' + inventory[s]);
1093
+ if (misnamed.length > 0)
1094
+ throw new Error('Inventory must be a hash of named terms with matching names: ' + misnamed.join(', '));
1095
+
1096
+ inventory = { ...inventory }; // shallow copy to avoid mutating input
1097
+
1098
+ // If any aliases mask native terms, those cannot be easily restored.
1099
+ // Moreover, subsequent terms may refer to both native term and and the conflicting alias.
1100
+ // Therefore, we will instead rename such aliases to something else
1101
+ // and only restore them at the end.
1102
+ const detour = [];
1103
+ let tmpId = 1;
1104
+ for (const name in native) {
1105
+ if (!(inventory[name] instanceof Alias))
1106
+ continue;
1107
+ while ('temp' + tmpId in inventory)
1108
+ tmpId++;
1109
+ const temp = 'temp' + tmpId;
1110
+ const orig = inventory[name];
1111
+ delete inventory[name];
1112
+ const masked = new Alias(temp, orig);
1113
+ for (const key in inventory)
1114
+ inventory[key] = inventory[key].subst(orig, masked) ?? inventory[key];
1115
+
1116
+ inventory[temp] = masked;
1117
+ detour.push([name, temp]);
1118
+ }
1119
+
1120
+ // only want to declare aliases
1121
+ const terms = Object.values(inventory)
1122
+ .filter(s => s instanceof Alias)
1123
+ .sort((a, b) => a.name.localeCompare(b.name));
1124
+
1125
+ const out = [];
1126
+ const seen = new Set();
1127
+ for (const term of terms)
1128
+ term._declare(out, inventory, seen);
1129
+
1130
+ for (const [name, temp] of detour) {
1131
+ out.push(name + '=' + temp); // rename
1132
+ out.push(temp + '='); // delete
1133
+ }
1134
+
1135
+ return out;
1136
+ }
1137
+
989
1138
  function maybeLambda (args, expr, caps = {}) {
990
1139
  const sym = expr.getSymbols();
991
1140
 
@@ -1019,6 +1168,10 @@ function naiveCanonize (expr) {
1019
1168
  throw new Error('Failed to canonize expression: ' + expr);
1020
1169
  }
1021
1170
 
1171
+ function nthvar (n) {
1172
+ return new FreeVar('abcdefgh'[n] ?? 'x' + n);
1173
+ }
1174
+
1022
1175
  /**
1023
1176
  *
1024
1177
  * @param {Expr} expr
@@ -1078,18 +1231,4 @@ function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
1078
1231
  yield { expr: canon.expr, steps: state.steps, comment: '(canonical)' };
1079
1232
  }
1080
1233
 
1081
- function nthvar (n) {
1082
- return new FreeVar('abcdefgh'[n] ?? 'x' + n);
1083
- }
1084
-
1085
- // A global value meaning "lambda is used somewhere in this expression"
1086
- // Can't be used (at least for now) to construct lambda expressions, or anything at all.
1087
- // See also getSymbols().
1088
- Expr.lambdaPlaceholder = new Native('->', x => x, {
1089
- arity: 1,
1090
- canonize: false,
1091
- note: 'Lambda placeholder',
1092
- apply: x => { throw new Error('Attempt to use a placeholder in expression') }
1093
- });
1094
-
1095
- module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church, globalOptions, native };
1234
+ module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church, globalOptions, native, declare };
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
@@ -107,12 +111,14 @@ class SKI {
107
111
  throw new Error('add: term must be an Alias or a string (accompanied with an implementation)');
108
112
 
109
113
  if (this.annotate && note === undefined && term.canonical)
110
- note = term.canonical.toString({ terse: true, html: true });
114
+ note = term.canonical.format({ terse: true, html: true, lambda: ['', ' &mapsto; ', ''] });
111
115
  if (note !== undefined)
112
116
  term.note = note;
113
117
 
114
- this.known['' + term] = term;
115
- this.allow.add('' + term);
118
+ if (this.known[term.name])
119
+ this.known[term.name].outdated = true;
120
+ this.known[term.name] = term;
121
+ this.allow.add(term.name);
116
122
 
117
123
  return this;
118
124
  }
@@ -125,6 +131,28 @@ class SKI {
125
131
  return this;
126
132
  }
127
133
 
134
+ /**
135
+ * @desc Declare and remove multiple terms at once
136
+ * term=impl adds term
137
+ * term= removes term
138
+ * @param {string[]]} list
139
+ * @return {SKI} chainable
140
+ */
141
+ bulkAdd (list) {
142
+ for (const item of list) {
143
+ const m = item.match(/^([A-Z]|[a-z][a-z_0-9]*)\s*=\s*(.*)$/s);
144
+ // TODO check all declarations before applying any (but we might need earlier terms for parsing later ones)
145
+ if (!m)
146
+ throw new Error('bulkAdd: invalid declaration: ' + item);
147
+ if (m[2] === '')
148
+ this.remove(m[1]);
149
+ else
150
+ this.add(m[1], this.parse(m[2]));
151
+ }
152
+
153
+ return this;
154
+ }
155
+
128
156
  /**
129
157
  * Restrict the interpreter to given terms. Terms prepended with '+' will be added
130
158
  * and terms preceeded with '-' will be removed.
@@ -183,6 +211,15 @@ class SKI {
183
211
  return out;
184
212
  }
185
213
 
214
+ /**
215
+ * Export term declarations for use in bulkAdd().
216
+ * @returns {string[]}
217
+ */
218
+ declare () {
219
+ // TODO accept argument to declare specific terms only
220
+ return declare(this.getTerms());
221
+ }
222
+
186
223
  /**
187
224
  *
188
225
  * @param {string} source
@@ -290,11 +327,12 @@ class SKI {
290
327
 
291
328
  toJSON () {
292
329
  return {
330
+ version: '1.1.1', // set to incremented package.json version whenever SKI serialization changes
293
331
  allow: this.showRestrict('+'),
294
332
  numbers: this.hasNumbers,
295
333
  lambdas: this.hasLambdas,
296
- terms: this.getTerms(),
297
334
  annotate: this.annotate,
335
+ terms: this.declare(),
298
336
  }
299
337
  }
300
338
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dallaylaen/ski-interpreter",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",
@@ -65,13 +65,13 @@ export class Expr {
65
65
  * @return {{
66
66
  * normal: boolean,
67
67
  * steps: number,
68
- * expr: Expr?,
69
- * arity: number?,
70
- * proper: boolean?,
71
- * discard: boolean?,
72
- * duplicate: boolean?,
73
- * skip: Set<number>?,
74
- * dup: Set<number>?
68
+ * expr?: Expr,
69
+ * arity?: number,
70
+ * proper?: boolean,
71
+ * discard?: boolean,
72
+ * duplicate?: boolean,
73
+ * skip?: Set<number>,
74
+ * dup?: Set<number>,
75
75
  * }}
76
76
  */
77
77
  guess(options?: {
@@ -80,13 +80,13 @@ export class Expr {
80
80
  }): {
81
81
  normal: boolean;
82
82
  steps: number;
83
- expr: Expr | null;
84
- arity: number | null;
85
- proper: boolean | null;
86
- discard: boolean | null;
87
- duplicate: boolean | null;
88
- skip: Set<number> | null;
89
- dup: Set<number> | null;
83
+ expr?: Expr;
84
+ arity?: number;
85
+ proper?: boolean;
86
+ discard?: boolean;
87
+ duplicate?: boolean;
88
+ skip?: Set<number>;
89
+ dup?: Set<number>;
90
90
  };
91
91
  _guess(options: any, preArgs?: any[], steps?: number): any;
92
92
  _aslist(): this[];
@@ -96,23 +96,23 @@ export class Expr {
96
96
  * up to the provided computation steps limit,
97
97
  * in decreasing weight order.
98
98
  * @param {{
99
- * max: number?,
100
- * maxArgs: number?,
101
- * varGen: function(void): FreeVar?,
102
- * steps: number?,
103
- * html: boolean?,
104
- * latin: number?,
99
+ * max?: number,
100
+ * maxArgs?: number,
101
+ * varGen?: function(void): FreeVar,
102
+ * steps?: number,
103
+ * html?: boolean,
104
+ * latin?: number,
105
105
  * }} options
106
106
  * @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
107
107
  * @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
108
108
  */
109
109
  lambdify(options?: {
110
- max: number | null;
111
- maxArgs: number | null;
112
- varGen: (arg0: void) => FreeVar | null;
113
- steps: number | null;
114
- html: boolean | null;
115
- latin: number | null;
110
+ max?: number;
111
+ maxArgs?: number;
112
+ varGen?: (arg0: void) => FreeVar;
113
+ steps?: number;
114
+ html?: boolean;
115
+ latin?: number;
116
116
  }): IterableIterator<{
117
117
  expr: Expr;
118
118
  steps: number | null;
@@ -130,14 +130,6 @@ export class Expr {
130
130
  expr: Expr;
131
131
  steps: number;
132
132
  }>;
133
- /**
134
- * @desc Rename free variables in the expression using the given sequence
135
- * This is for eye-candy only, as the interpreter knows darn well hot to distinguish vars,
136
- * regardless of names.
137
- * @param {IterableIterator<string>} seq
138
- * @return {Expr}
139
- */
140
- renameVars(seq: IterableIterator<string>): Expr;
141
133
  _rski(options: any): this;
142
134
  /**
143
135
  * Apply self to list of given args.
@@ -147,13 +139,17 @@ export class Expr {
147
139
  */
148
140
  reduce(args: Expr[]): Expr | null;
149
141
  /**
150
- * Replace all instances of free vars with corresponding values and return the resulting expression.
151
- * return null if no changes could be made.
152
- * @param {FreeVar} plug
153
- * @param {Expr} value
154
- * @return {Expr|null}
155
- */
156
- subst(plug: FreeVar, value: Expr): Expr | null;
142
+ * Replace all instances of plug in the expression with value and return the resulting expression,
143
+ * or null if no changes could be made.
144
+ * Lambda terms and applications will never match if used as plug
145
+ * as they are impossible co compare without extensive computations.
146
+ * Typically used on variables but can also be applied to other terms, e.g. aliases.
147
+ * See also Expr.replace().
148
+ * @param {Expr} search
149
+ * @param {Expr} replace
150
+ * @return {Expr|null}
151
+ */
152
+ subst(search: Expr, replace: Expr): Expr | null;
157
153
  /**
158
154
  * @desc iterate one step of calculation in accordance with known rules.
159
155
  * @return {{expr: Expr, steps: number, changed: boolean}}
@@ -207,24 +203,64 @@ export class Expr {
207
203
  */
208
204
  expect(expected: Expr, comment?: string): void;
209
205
  /**
210
- * @param {{terse: boolean?, html: boolean?}} [options]
211
- * @return {string} string representation of the expression
206
+ * @desc Returns string representation of the expression.
207
+ * Same as format() without options.
208
+ * @return {string}
212
209
  */
213
- toString(options?: {
214
- terse: boolean | null;
215
- html: boolean | null;
216
- }): string;
210
+ toString(): string;
217
211
  /**
218
212
  * @desc Whether the expression needs parentheses when printed.
219
213
  * @param {boolean} [first] - whether this is the first term in a sequence
220
214
  * @return {boolean}
221
215
  */
222
- needsParens(first?: boolean): boolean;
216
+ _braced(first?: boolean): boolean;
217
+ _unspaced(arg: any): boolean;
223
218
  /**
219
+ * @desc Stringify the expression with fancy formatting options.
220
+ * Said options mostly include wrappers around various constructs in form of ['(', ')'],
221
+ * as well as terse and html flags that set up the defaults.
222
+ * Format without options is equivalent to toString() and can be parsed back.
223
+ *
224
+ * @param {Object} [options] - formatting options
225
+ * @param {boolean} [options.terse] - whether to use terse formatting (omitting unnecessary spaces and parentheses)
226
+ * @param {boolean} [options.html] - whether to default to HTML tags & entities
227
+ * @param {[string, string]} [options.brackets] - wrappers for application arguments, typically ['(', ')']
228
+ * @param {[string, string]} [options.var] - wrappers for variable names
229
+ * (will default to &lt;var&gt; and &lt;/var&gt; in html mode)
230
+ * @param {[string, string, string]} [options.lambda] - wrappers for lambda abstractions, e.g. ['&lambda;', '.', '']
231
+ * where the middle string is placed between argument and body
232
+ * default is ['', '->', ''] or ['', '-&gt;', ''] for html
233
+ * @param {[string, string]} [options.around] - wrappers around (sub-)expressions.
234
+ * individual applications will not be wrapped, i.e. (a b c) but not ((a b) c)
235
+ * @param {[string, string]} [options.redex] - wrappers around the starting term(s) that have enough arguments to be reduced
236
+ * @param {Object<string, Expr>} [options.inventory] - if given, output aliases in the set as their names
237
+ * and any other aliases as the expansion of their definitions.
238
+ * The default is a cryptic and fragile mechanism dependent on a hidden mutable property.
239
+ * @returns {string}
240
+ *
241
+ * @example foo.format() // equivalent to foo.toString()
242
+ * @example foo.format({terse: false}) // spell out all parentheses
243
+ * @example foo.format({html: true}) // use HTML tags and entities
244
+ * @example foo.format({ around: ['(', ')'], brackets: ['', ''], lambda: ['(', '->', ')'] }) // lisp style, still back-parsable
245
+ * @exapmle foo.format({ lambda: ['&lambda;', '.', ''] }) // pretty-print for the math department
246
+ * @example foo.format({ lambda: ['', '=>', ''], terse: false }) // make it javascript
247
+ * @example foo.format({ inventory: { T } }) // use T as a named term, expand all others
224
248
  *
225
- * @return {string}
226
249
  */
227
- toJSON(): string;
250
+ format(options?: {
251
+ terse?: boolean;
252
+ html?: boolean;
253
+ brackets?: [string, string];
254
+ var?: [string, string];
255
+ lambda?: [string, string, string];
256
+ around?: [string, string];
257
+ redex?: [string, string];
258
+ inventory?: {
259
+ [x: string]: Expr;
260
+ };
261
+ }): string;
262
+ _format(options: any, nargs: any): void;
263
+ _declare(output: any, inventory: any, seen: any): void;
228
264
  }
229
265
  export namespace Expr {
230
266
  let lambdaPlaceholder: Native;
@@ -245,8 +281,7 @@ export class App extends Expr {
245
281
  _firstVar(): any;
246
282
  apply(...args: any[]): App;
247
283
  expand(): any;
248
- renameVars(seq: any): any;
249
- subst(plug: any, value: any): any;
284
+ subst(search: any, replace: any): any;
250
285
  /**
251
286
  * @return {{expr: Expr, steps: number}}
252
287
  */
@@ -259,14 +294,13 @@ export class App extends Expr {
259
294
  _aslist(): any[];
260
295
  equals(other: any): any;
261
296
  contains(other: any): any;
262
- needsParens(first: any): boolean;
263
- toString(opt?: {}): string;
297
+ _braced(first: any): boolean;
298
+ _format(options: any, nargs: any): any;
299
+ _unspaced(arg: any): any;
264
300
  }
265
301
  export class FreeVar extends Named {
266
302
  constructor(name: any);
267
303
  id: number;
268
- subst(plug: any, value: any): any;
269
- toString(opt?: {}): string;
270
304
  }
271
305
  export class Lambda extends Expr {
272
306
  /**
@@ -278,13 +312,12 @@ export class Lambda extends Expr {
278
312
  impl: Expr;
279
313
  arity: number;
280
314
  reduce(input: any): Expr;
281
- subst(plug: any, value: any): Lambda;
315
+ subst(search: any, replace: any): Lambda;
282
316
  expand(): Lambda;
283
- renameVars(seq: any): Lambda;
284
317
  _rski(options: any): any;
285
318
  equals(other: any): boolean;
286
- toString(opt?: {}): string;
287
- needsParens(first: any): boolean;
319
+ _format(options: any, nargs: any): string;
320
+ _braced(first: any): boolean;
288
321
  }
289
322
  /**
290
323
  * @typedef {function(Expr): Expr | AnyArity} AnyArity
@@ -334,7 +367,7 @@ export class Alias extends Named {
334
367
  proper: any;
335
368
  terminal: any;
336
369
  canonical: any;
337
- subst(plug: any, value: any): Expr;
370
+ subst(search: any, replace: any): any;
338
371
  /**
339
372
  *
340
373
  * @return {{expr: Expr, steps: number}}
@@ -346,8 +379,8 @@ export class Alias extends Named {
346
379
  reduce(args: any): Expr;
347
380
  equals(other: any): any;
348
381
  _rski(options: any): Expr;
349
- toString(opt: any): string;
350
- needsParens(first: any): boolean;
382
+ _braced(first: any): boolean;
383
+ _format(options: any, nargs: any): string | void;
351
384
  }
352
385
  export class Church extends Native {
353
386
  constructor(n: any);
@@ -361,6 +394,12 @@ export namespace globalOptions {
361
394
  let maxArgs: number;
362
395
  }
363
396
  export const native: {};
397
+ /**
398
+ *
399
+ * @param {Expr[]} inventory
400
+ * @return {string[]}
401
+ */
402
+ export function declare(inventory: Expr[]): string[];
364
403
  declare class Named extends Expr {
365
404
  /**
366
405
  * @desc a constant named 'name'
@@ -368,6 +407,6 @@ declare class Named extends Expr {
368
407
  */
369
408
  constructor(name: string);
370
409
  name: string;
371
- toString(): string;
410
+ _format(options: any, nargs: any): string;
372
411
  }
373
412
  export {};
@@ -2,21 +2,21 @@ export class SKI {
2
2
  /**
3
3
  *
4
4
  * @param {{
5
- * allow: string?,
6
- * numbers: boolean?,
7
- * lambdas: boolean?,
8
- * terms: { [key: string]: Expr|string}?,
9
- * annotate: boolean?,
5
+ * allow?: string,
6
+ * numbers?: boolean,
7
+ * lambdas?: boolean,
8
+ * terms?: { [key: string]: Expr|string} | string[],
9
+ * annotate?: boolean,
10
10
  * }} [options]
11
11
  */
12
12
  constructor(options?: {
13
- allow: string | null;
14
- numbers: boolean | null;
15
- lambdas: boolean | null;
16
- terms: {
13
+ allow?: string;
14
+ numbers?: boolean;
15
+ lambdas?: boolean;
16
+ terms?: {
17
17
  [key: string]: Expr | string;
18
- } | null;
19
- annotate: boolean | null;
18
+ } | string[];
19
+ annotate?: boolean;
20
20
  });
21
21
  annotate: boolean;
22
22
  known: {};
@@ -35,6 +35,14 @@ export class SKI {
35
35
  fast: boolean | null;
36
36
  }], note?: string): SKI;
37
37
  maybeAdd(name: any, impl: any): this;
38
+ /**
39
+ * @desc Declare and remove multiple terms at once
40
+ * term=impl adds term
41
+ * term= removes term
42
+ * @param {string[]]} list
43
+ * @return {SKI} chainable
44
+ */
45
+ bulkAdd(list: any): SKI;
38
46
  /**
39
47
  * Restrict the interpreter to given terms. Terms prepended with '+' will be added
40
48
  * and terms preceeded with '-' will be removed.
@@ -65,6 +73,11 @@ export class SKI {
65
73
  getTerms(): {
66
74
  [key: string]: Native | Alias;
67
75
  };
76
+ /**
77
+ * Export term declarations for use in bulkAdd().
78
+ * @returns {string[]}
79
+ */
80
+ declare(): string[];
68
81
  /**
69
82
  *
70
83
  * @param {string} source
@@ -94,13 +107,12 @@ export class SKI {
94
107
  allow: string | null;
95
108
  }): Expr;
96
109
  toJSON(): {
110
+ version: string;
97
111
  allow: string;
98
112
  numbers: boolean;
99
113
  lambdas: boolean;
100
- terms: {
101
- [key: string]: Native | Alias;
102
- };
103
114
  annotate: boolean;
115
+ terms: string[];
104
116
  };
105
117
  }
106
118
  export namespace SKI {