@dallaylaen/ski-interpreter 2.5.0 → 2.5.2

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,23 @@ 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
+ ## [2.5.2] - 2026-03-27
9
+
10
+ ### Changed
11
+
12
+ - Make `Expr` class abstract (as was always intended)
13
+ and mark its abstract/final/protected methods appropriately.
14
+
15
+ ## [2.5.1] - 2026-03-26
16
+
17
+ ### Changed
18
+
19
+ - make diag() recursive (and simpler)
20
+ - better types, get rid of some type casts (esp in traverse/fold)
21
+ - some doc improvements
22
+ - some test improvements
23
+ - playground: add history removal
24
+
8
25
  ## [2.5.0] - 2026-03-15
9
26
 
10
27
  ### BREAKING CHANGES
package/README.md CHANGED
@@ -7,9 +7,13 @@ This package contains a
7
7
  and [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus)
8
8
  parser and interpreter focused on traceability and inspectability.
9
9
 
10
- It is written in plain JavaScript (with bolted on TypeScript support)
10
+ It is written in TypeScript and JavaScript
11
11
  and can be used in Node.js or in the browser.
12
12
 
13
+ A [playground](https://dallaylaen.github.io/ski-interpreter/)
14
+ and a [quest page](https://dallaylaen.github.io/ski-interpreter/quest.html)
15
+ containing interactive combinatory logic exercises of increasing difficulty are incuded.
16
+
13
17
  # Features:
14
18
 
15
19
  * SKI and BCKW combinators
@@ -40,7 +44,8 @@ and can be used in Node.js or in the browser.
40
44
  * <code>C x y z &mapsto; x z y</code> _// swapping_;
41
45
  * <code>W x y &mapsto; x y y</code> _//duplication_;
42
46
 
43
- The special combinator `+` will increment Church numerals, if they happen to come after it:
47
+ The special combinator `+` will increment Church numerals,
48
+ if they happen to come directly after it:
44
49
 
45
50
  * `+ 0` // 1
46
51
  * `2 + 3` // -> `+(+(3))` -> `+(4)` -> `5`
@@ -99,14 +104,14 @@ npm install @dallaylaen/ski-interpreter
99
104
  * `--verbose` - Show all evaluation steps
100
105
  * Example: `ski file script.ski`
101
106
 
102
- * **`infer <expression>`** - try to find equivalent lambda expression and display its properties if found.
107
+ * **`infer <expression>`** - try to find equivalent lambda expression and display its properties if found.
103
108
 
104
- * **`extract <expression> <known term> ...`** -
109
+ * **`extract <expression> <known term> ...`** -
105
110
  Replace parts of the expression that are equivalent to the known terms with the respective terms. Known terms must be normalizable.
106
111
 
107
- * **`search <expression> <known term> ...`** -
108
- Attempt to brute force an equivalent of the _expression_ using only the _known terms_.
109
- Only normalizable terms are currently supported.
112
+ * **`search <expression> <known term> ...`** -
113
+ Attempt to brute force an equivalent of the _expression_ using only the _known terms_.
114
+ Only normalizable terms are currently supported.
110
115
 
111
116
  * **`quest-lint <files...>`** - Validate quest definition files
112
117
  * `--solution <file>` - Load solutions from a JSON file for verification
@@ -297,6 +302,7 @@ for building interactive quest pages from JSON-encoded quest data;
297
302
  * [@ivanaxe](https://github.com/ivanaxe) for luring me into [icfpc 2011](http://icfpc2011.blogspot.com/2011/06/task-description-contest-starts-now.html) where I was introduced to combinators.
298
303
  * [@akuklev](https://github.com/akuklev) for explaining functional programming to me so many times that I actually got some idea.
299
304
  * [One happy fellow](https://github.com/happyfellow-one) whose [riddle](https://blog.happyfellow.dev/a-riddle/) trolled me into writing an early `traverse` prototype.
305
+ * [Darkwing3125](https://github.com/Darkwing3125) for posting multiple bug reports and feature requests.
300
306
 
301
307
  # Prior art and inspiration
302
308
 
@@ -13889,18 +13889,11 @@ var FormatOptionsSchema = external_exports.object({
13889
13889
  inventory: external_exports.record(external_exports.string(), external_exports.custom((v) => v instanceof Expr)).optional()
13890
13890
  });
13891
13891
  var Expr = class _Expr {
13892
- static {
13893
- this.control = control;
13894
- }
13895
- static {
13896
- this.native = native;
13897
- }
13898
- // rough estimate of the number of nodes in the tree
13899
13892
  /**
13900
13893
  *
13901
13894
  * @desc Define properties of the term based on user supplied options and/or inference results.
13902
13895
  * Typically useful for declaring Native and Alias terms.
13903
- * @private
13896
+ * @protected
13904
13897
  * @param {Object} options
13905
13898
  * @param {string} [options.note] - a brief description what the term does
13906
13899
  * @param {number} [options.arity] - number of arguments the term is waiting for (if known)
@@ -13975,6 +13968,7 @@ var Expr = class _Expr {
13975
13968
  * }} [options]
13976
13969
  * @param {(e:Expr) => TraverseValue<Expr>} change
13977
13970
  * @returns {Expr|null}
13971
+ * @final
13978
13972
  */
13979
13973
  traverse(options, change) {
13980
13974
  if (typeof options === "function") {
@@ -13988,7 +13982,8 @@ var Expr = class _Expr {
13988
13982
  return expr ?? null;
13989
13983
  }
13990
13984
  /**
13991
- * @private
13985
+ * @protected
13986
+ * @final
13992
13987
  * @param {Object} options
13993
13988
  * @param {(e:Expr) => TraverseValue<Expr>} change
13994
13989
  * @returns {TraverseValue<Expr>}
@@ -14007,7 +14002,7 @@ var Expr = class _Expr {
14007
14002
  return action ? action(expr) : expr;
14008
14003
  }
14009
14004
  /**
14010
- * @private
14005
+ * @protected
14011
14006
  * @param {Object} options
14012
14007
  * @param {(e:Expr) => TraverseValue<Expr>} change
14013
14008
  * @returns {TraverseValue<Expr>}
@@ -14037,7 +14032,11 @@ var Expr = class _Expr {
14037
14032
  *
14038
14033
  * This method is experimental and may change in the future.
14039
14034
  *
14035
+ * @example // count the number of nodes in the expression tree
14036
+ * expr.fold(0, (acc, e) => acc + 1);
14037
+ *
14040
14038
  * @experimental
14039
+ * @final
14041
14040
  * @template T
14042
14041
  * @param {T} initial
14043
14042
  * @param {(acc: T, expr: Expr) => TraverseValue<T>} combine
@@ -14047,6 +14046,14 @@ var Expr = class _Expr {
14047
14046
  const [value] = unwrap(this._fold(initial, combine));
14048
14047
  return value ?? initial;
14049
14048
  }
14049
+ /**
14050
+ * @desc Internal method for fold(), which performs the actual folding.
14051
+ * Should be implemented in subclasses having any internal structure.
14052
+ *
14053
+ * @protected
14054
+ * @param initial
14055
+ * @param combine
14056
+ */
14050
14057
  _fold(initial, combine) {
14051
14058
  return combine(initial, this);
14052
14059
  }
@@ -14090,6 +14097,7 @@ var Expr = class _Expr {
14090
14097
  *
14091
14098
  * Use toLambda() if you want to get a lambda term in any case.
14092
14099
  *
14100
+ * @final
14093
14101
  * @param {{max?: number, maxArgs?: number}} options
14094
14102
  * @return {TermInfo}
14095
14103
  */
@@ -14182,6 +14190,7 @@ var Expr = class _Expr {
14182
14190
  *
14183
14191
  * See also Expr.walk() and Expr.toSKI().
14184
14192
  *
14193
+ * @final
14185
14194
  * @param {{
14186
14195
  * max?: number,
14187
14196
  * maxArgs?: number,
@@ -14228,6 +14237,7 @@ var Expr = class _Expr {
14228
14237
  *
14229
14238
  * See also Expr.walk() and Expr.toLambda().
14230
14239
  *
14240
+ * @final
14231
14241
  * @param {{max?: number}} [options]
14232
14242
  * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
14233
14243
  */
@@ -14258,12 +14268,15 @@ var Expr = class _Expr {
14258
14268
  }
14259
14269
  }
14260
14270
  /**
14261
- * Replace all instances of plug in the expression with value and return the resulting expression,
14271
+ * Replace all instances of `search` in the expression with `replace` and return the resulting expression,
14262
14272
  * or null if no changes could be made.
14273
+ *
14263
14274
  * Lambda terms and applications will never match if used as plug
14264
- * as they are impossible co compare without extensive computations.
14275
+ * as they are impossible to compare without extensive computations.
14276
+ *
14265
14277
  * Typically used on variables but can also be applied to other terms, e.g. aliases.
14266
- * See also Expr.traverse().
14278
+ * See also Expr.traverse() for more flexible replacement of subterms.
14279
+ *
14267
14280
  * @param {Expr} search
14268
14281
  * @param {Expr} replace
14269
14282
  * @return {Expr|null}
@@ -14304,6 +14317,7 @@ var Expr = class _Expr {
14304
14317
  * @desc Run uninterrupted sequence of step() applications
14305
14318
  * until the expression is irreducible, or max number of steps is reached.
14306
14319
  * Default number of steps = 1000.
14320
+ * @final
14307
14321
  * @param {{max?: number, steps?: number, throw?: boolean}|Expr} [opt]
14308
14322
  * @param {Expr} args
14309
14323
  * @return {{expr: Expr, steps: number, final: boolean}}
@@ -14334,7 +14348,9 @@ var Expr = class _Expr {
14334
14348
  }
14335
14349
  /**
14336
14350
  * Execute step() while possible, yielding a brief description of events after each step.
14351
+ *
14337
14352
  * Mnemonics: like run() but slower.
14353
+ * @final
14338
14354
  * @param {{max?: number}} options
14339
14355
  * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
14340
14356
  */
@@ -14364,6 +14380,7 @@ var Expr = class _Expr {
14364
14380
  *
14365
14381
  * @param {Expr} other
14366
14382
  * @return {boolean}
14383
+ * @final
14367
14384
  */
14368
14385
  equals(other) {
14369
14386
  return !this.diff(other);
@@ -14382,6 +14399,8 @@ var Expr = class _Expr {
14382
14399
  * To somewhat alleviate confusion, the output will include
14383
14400
  * the internal id of the variable in square brackets.
14384
14401
  *
14402
+ * Do not rely on the exact format of the output as it may change in the future.
14403
+ *
14385
14404
  * @example "K(S != I)" is the result of comparing "KS" and "KI"
14386
14405
  * @example "S(K([x[13] != x[14]]))K"
14387
14406
  *
@@ -14402,6 +14421,11 @@ var Expr = class _Expr {
14402
14421
  * `this` is the expected value and the argument is the actual one.
14403
14422
  * Mnemonic: the expected value is always a combinator, the actual one may be anything.
14404
14423
  *
14424
+ * In case of failure, an error is thrown with a message describing the first point of difference
14425
+ * and `expected` and `actual` properties like in AssertionError.
14426
+ * AssertionError is not used directly to because browsers don't recognize it.
14427
+ *
14428
+ * @final
14405
14429
  * @param {Expr} actual
14406
14430
  * @param {string} comment
14407
14431
  */
@@ -14421,7 +14445,10 @@ var Expr = class _Expr {
14421
14445
  /**
14422
14446
  * @desc Returns string representation of the expression.
14423
14447
  * Same as format() without options.
14448
+ *
14449
+ * Use formatImpl() to override in subclasses.
14424
14450
  * @return {string}
14451
+ * @final
14425
14452
  */
14426
14453
  toString() {
14427
14454
  return this.format();
@@ -14430,6 +14457,7 @@ var Expr = class _Expr {
14430
14457
  * @desc Whether the expression needs parentheses when printed.
14431
14458
  * @param {boolean} [first] - whether this is the first term in a sequence
14432
14459
  * @return {boolean}
14460
+ * @protected
14433
14461
  */
14434
14462
  _braced(_first) {
14435
14463
  return false;
@@ -14438,7 +14466,7 @@ var Expr = class _Expr {
14438
14466
  * @desc Whether the expression can be printed without a space when followed by arg.
14439
14467
  * @param {Expr} arg
14440
14468
  * @returns {boolean}
14441
- * @private
14469
+ * @protected
14442
14470
  */
14443
14471
  _unspaced(arg) {
14444
14472
  return this._braced(true);
@@ -14446,8 +14474,9 @@ var Expr = class _Expr {
14446
14474
  /**
14447
14475
  * @desc Stringify the expression with fancy formatting options.
14448
14476
  * Said options mostly include wrappers around various constructs in form of ['(', ')'],
14449
- * as well as terse and html flags that set up the defaults.
14477
+ * as well as `terse` and `html` flags that fill in appropriate defaults.
14450
14478
  * Format without options is equivalent to toString() and can be parsed back.
14479
+ * @final
14451
14480
  *
14452
14481
  * @param {Object} [options] - formatting options
14453
14482
  * @param {boolean} [options.terse] - whether to use terse formatting (omitting unnecessary spaces and parentheses)
@@ -14492,7 +14521,7 @@ var Expr = class _Expr {
14492
14521
  around: ["", ""],
14493
14522
  redex: ["", ""]
14494
14523
  };
14495
- return this._format({
14524
+ return this.formatImpl({
14496
14525
  terse: options.terse ?? true,
14497
14526
  brackets: options.brackets ?? fallback.brackets,
14498
14527
  space: options.space ?? fallback.space,
@@ -14505,16 +14534,6 @@ var Expr = class _Expr {
14505
14534
  html: options.html ?? false
14506
14535
  }, 0);
14507
14536
  }
14508
- /**
14509
- * @desc Internal method for format(), which performs the actual formatting.
14510
- * @param {Object} options
14511
- * @param {number} nargs
14512
- * @returns {string}
14513
- * @private
14514
- */
14515
- _format(options, nargs) {
14516
- throw new Error("No _format() method defined in class " + this.constructor.name);
14517
- }
14518
14537
  /**
14519
14538
  * @desc Returns a string representation of the expression tree, with indentation to show structure.
14520
14539
  *
@@ -14536,24 +14555,13 @@ var Expr = class _Expr {
14536
14555
  * FreeVar: x[54]
14537
14556
  * FreeVar: x[54]
14538
14557
  */
14539
- diag() {
14540
- const rec = (e, indent) => {
14541
- if (e instanceof App)
14542
- return [indent + "App:", ...e.unroll().flatMap((s) => rec(s, indent + " "))];
14543
- if (e instanceof Lambda)
14544
- return [`${indent}Lambda (${e.arg}[${e.arg.id}]):`, ...rec(e.impl, indent + " ")];
14545
- if (e instanceof Alias)
14546
- return [`${indent}Alias (${e.name}): \\`, ...rec(e.impl, indent)];
14547
- if (e instanceof FreeVar)
14548
- return [`${indent}FreeVar: ${e.name}[${e.id}]`];
14549
- return [`${indent}${e.constructor.name}: ${e}`];
14550
- };
14551
- const out = rec(this, "");
14552
- return out.join("\n");
14558
+ diag(indent = "") {
14559
+ return indent + this.constructor.name + ": " + this;
14553
14560
  }
14554
14561
  /**
14555
14562
  * @desc Convert the expression to a JSON-serializable format.
14556
14563
  * @returns {string}
14564
+ * @final
14557
14565
  */
14558
14566
  toJSON() {
14559
14567
  return this.format();
@@ -14648,15 +14656,18 @@ var App = class _App extends Expr {
14648
14656
  _braced(first) {
14649
14657
  return !first;
14650
14658
  }
14651
- _format(options, nargs) {
14652
- const fun = this.fun._format(options, nargs + 1);
14653
- const arg = this.arg._format(options, 0);
14659
+ formatImpl(options, nargs) {
14660
+ const fun = this.fun.formatImpl(options, nargs + 1);
14661
+ const arg = this.arg.formatImpl(options, 0);
14654
14662
  const wrap = nargs ? ["", ""] : options.around;
14655
14663
  if (options.terse && !this.arg._braced(false))
14656
14664
  return wrap[0] + fun + (this.fun._unspaced(this.arg) ? "" : options.space) + arg + wrap[1];
14657
14665
  else
14658
14666
  return wrap[0] + fun + options.brackets[0] + arg + options.brackets[1] + wrap[1];
14659
14667
  }
14668
+ diag(indent = "") {
14669
+ return indent + "App:\n" + this.unroll().map((e) => e.diag(indent + " ")).join("\n");
14670
+ }
14660
14671
  _unspaced(arg) {
14661
14672
  return this.arg._braced(false) ? true : this.arg._unspaced(arg);
14662
14673
  }
@@ -14671,7 +14682,7 @@ var Named = class _Named extends Expr {
14671
14682
  _unspaced(arg) {
14672
14683
  return !!(arg instanceof _Named && (this.name.match(/^[A-Z+]$/) && arg.name.match(/^[a-z+]/i) || this.name.match(/^[a-z+]/i) && arg.name.match(/^[A-Z+]$/)));
14673
14684
  }
14674
- _format(options, nargs) {
14685
+ formatImpl(options, nargs) {
14675
14686
  const name = options.html ? this.fancyName ?? this.name : this.name;
14676
14687
  return this.arity !== void 0 && this.arity > 0 && this.arity <= nargs ? options.redex[0] + name + options.redex[1] : name;
14677
14688
  }
@@ -14697,10 +14708,13 @@ var FreeVar = class _FreeVar extends Named {
14697
14708
  return replace;
14698
14709
  return null;
14699
14710
  }
14700
- _format(options, nargs) {
14711
+ formatImpl(options, nargs) {
14701
14712
  const name = options.html ? this.fancyName ?? this.name : this.name;
14702
14713
  return options.var[0] + name + options.var[1];
14703
14714
  }
14715
+ diag(indent = "") {
14716
+ return `${indent}FreeVar: ${this.name}[${this.id}]`;
14717
+ }
14704
14718
  static {
14705
14719
  this.global = ["global"];
14706
14720
  }
@@ -14776,8 +14790,12 @@ var Lambda = class _Lambda extends Expr {
14776
14790
  return "(t->" + diff + ")";
14777
14791
  return null;
14778
14792
  }
14779
- _format(options, nargs) {
14780
- return (nargs > 0 ? options.brackets[0] : "") + options.lambda[0] + this.arg._format(options, 0) + options.lambda[1] + this.impl._format(options, 0) + options.lambda[2] + (nargs > 0 ? options.brackets[1] : "");
14793
+ formatImpl(options, nargs) {
14794
+ return (nargs > 0 ? options.brackets[0] : "") + options.lambda[0] + this.arg.formatImpl(options, 0) + options.lambda[1] + this.impl.formatImpl(options, 0) + options.lambda[2] + (nargs > 0 ? options.brackets[1] : "");
14795
+ }
14796
+ diag(indent = "") {
14797
+ return `${indent}Lambda (${this.arg.name}[${this.arg.id}]):
14798
+ ` + this.impl.diag(indent + " ");
14781
14799
  }
14782
14800
  _braced(first) {
14783
14801
  return true;
@@ -14808,7 +14826,7 @@ var Church = class _Church extends Expr {
14808
14826
  _unspaced(arg) {
14809
14827
  return false;
14810
14828
  }
14811
- _format(options, nargs) {
14829
+ formatImpl(options, nargs) {
14812
14830
  return nargs >= 2 ? options.redex[0] + this.n + options.redex[1] : this.n + "";
14813
14831
  }
14814
14832
  };
@@ -14877,9 +14895,13 @@ var Alias = class extends Named {
14877
14895
  _braced(first) {
14878
14896
  return this.outdated ? this.impl._braced(first) : false;
14879
14897
  }
14880
- _format(options, nargs) {
14898
+ formatImpl(options, nargs) {
14881
14899
  const outdated = options.inventory ? options.inventory[this.name] !== this : this.outdated;
14882
- return outdated ? this.impl._format(options, nargs) : super._format(options, nargs);
14900
+ return outdated ? this.impl.formatImpl(options, nargs) : super.formatImpl(options, nargs);
14901
+ }
14902
+ diag(indent = "") {
14903
+ return `${indent}Alias (${this.name}): \\
14904
+ ` + this.impl.diag(indent);
14883
14905
  }
14884
14906
  };
14885
14907
  function addNative(name, impl, opt = {}) {
@@ -14993,6 +15015,9 @@ var Empty = class extends Expr {
14993
15015
  postParse() {
14994
15016
  throw new Error("Attempt to use empty expression () as a term");
14995
15017
  }
15018
+ formatImpl(options, nargs) {
15019
+ return "()";
15020
+ }
14996
15021
  };
14997
15022
  var PartialLambda = class _PartialLambda extends Empty {
14998
15023
  // TODO mutable! rewrite ro when have time
@@ -15242,9 +15267,7 @@ var Parser = class {
15242
15267
  list[j] = rework(list[j]);
15243
15268
  detour.set(needDetour[list[j].name], list[j]);
15244
15269
  env[list[j].name] = list[j];
15245
- console.log(`list[${j}] = ${list[j].name}=${list[j].impl};`);
15246
15270
  }
15247
- console.log("detour:", detour);
15248
15271
  }
15249
15272
  const out = list.map(
15250
15273
  (e) => needDetour[e.name] ? e.name + "=" + needDetour[e.name].name + "=" + e.impl.format({ inventory: env }) : e.name + "=" + e.impl.format({ inventory: env })
@@ -15372,22 +15395,6 @@ var Parser = class {
15372
15395
  /**
15373
15396
  * Public static shortcuts to common functions (see also ./extras.js)
15374
15397
  */
15375
- /**
15376
- * @desc Create a proxy object that generates variables on demand,
15377
- * with names corresponding to the property accessed.
15378
- * Different invocations will return distinct variables,
15379
- * even if with the same name.
15380
- *
15381
- *
15382
- * @example const {x, y, z} = SKI.vars();
15383
- * x.name; // 'x'
15384
- * x instanceof FreeVar; // true
15385
- * x.apply(y).apply(z); // x(y)(z)
15386
- *
15387
- * @template T
15388
- * @param {T} [scope] - optional context to bind the generated variables to
15389
- * @return {{[key: string]: FreeVar}}
15390
- */
15391
15398
  };
15392
15399
  function maybeAlias(name, impl) {
15393
15400
  while (impl instanceof Alias && impl.name === name)
@@ -15931,7 +15938,17 @@ var SKI = class extends Parser {
15931
15938
  static {
15932
15939
  this.W = native.W;
15933
15940
  }
15934
- // variable generator shortcut
15941
+ /**
15942
+ * @desc Create a proxy object that generates variables on demand,
15943
+ * with names corresponding to the property accessed.
15944
+ * Different invocations will return distinct variables,
15945
+ * even if with the same name.
15946
+ *
15947
+ * @example const {x, y, z} = SKI.vars();
15948
+ * x.name; // 'x'
15949
+ * x instanceof FreeVar; // true
15950
+ * x.apply(y).apply(z); // x(y)(z)
15951
+ */
15935
15952
  static vars(scope = {}) {
15936
15953
  const vars = {};
15937
15954
  return new Proxy(vars, {