@dallaylaen/ski-interpreter 2.2.1 → 2.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.
@@ -101,12 +101,19 @@ var require_expr = __commonJS({
101
101
  max: 1e3,
102
102
  maxArgs: 32
103
103
  };
104
+ var ORDER = {
105
+ "leftmost-outermost": "LO",
106
+ "leftmost-innermost": "LI",
107
+ LO: "LO",
108
+ LI: "LI"
109
+ };
104
110
  var control = {
105
111
  descend: prepareWrapper("descend"),
106
112
  prune: prepareWrapper("prune"),
107
113
  redo: prepareWrapper("redo"),
108
114
  stop: prepareWrapper("stop")
109
115
  };
116
+ var native = {};
110
117
  var Expr = class _Expr {
111
118
  /**
112
119
  * @descr A combinatory logic expression.
@@ -121,10 +128,43 @@ var require_expr = __commonJS({
121
128
  * src?: string,
122
129
  * parser: object,
123
130
  * }} [context]
131
+ * @property {number} [arity] - number of arguments the term is waiting for (if known)
132
+ * @property {string} [note] - a brief description what the term does
133
+ * @property {string} [fancyName] - how to display in html mode, e.g. φ instead of 'f'
134
+ * Typically only applicable to descendants of Named.
135
+ * @property {TermInfo} [props] - properties inferred from the term's behavior
124
136
  */
125
- constructor() {
126
- if (new.target === _Expr)
127
- throw new Error("Attempt to instantiate abstract class Expr");
137
+ /**
138
+ *
139
+ * @desc Define properties of the term based on user supplied options and/or inference results.
140
+ * Typically useful for declaring Native and Alias terms.
141
+ * @private
142
+ * @param {Object} options
143
+ * @param {string} [options.note] - a brief description what the term does
144
+ * @param {number} [options.arity] - number of arguments the term is waiting for (if known)
145
+ * @param {string} [options.fancy] - how to display in html mode, e.g. φ instead of 'f'
146
+ * @param {boolean} [options.canonize] - whether to try to infer the properties
147
+ * @param {number} [options.max] - maximum number of steps for inference, if canonize is true
148
+ * @param {number} [options.maxArgs] - maximum number of arguments for inference, if canonize is true
149
+ * @return {this}
150
+ */
151
+ _setup(options = {}) {
152
+ if (options.fancy !== void 0)
153
+ this.fancyName = options.fancyName;
154
+ if (options.note !== void 0)
155
+ this.note = options.note;
156
+ if (options.arity !== void 0)
157
+ this.arity = options.arity;
158
+ if (options.canonize) {
159
+ const guess = this.infer(options);
160
+ if (guess.normal) {
161
+ this.arity = this.arity ?? guess.arity;
162
+ this.note = this.note ?? guess.expr.format({ html: true, lambda: ["", " ↦ ", ""] });
163
+ delete guess.steps;
164
+ this.props = guess;
165
+ }
166
+ }
167
+ return this;
128
168
  }
129
169
  /**
130
170
  * @desc apply self to zero or more terms and return the resulting term,
@@ -158,18 +198,67 @@ var require_expr = __commonJS({
158
198
  /**
159
199
  * @desc Traverse the expression tree, applying change() to each node.
160
200
  * If change() returns an Expr, the node is replaced with that value.
161
- * Otherwise, the node is descended further (if applicable)
162
- * or left unchanged.
201
+ * A null/undefined value is interpreted as
202
+ * "descend further if applicable, or leave the node unchanged".
203
+ *
204
+ * Returned values may be decorated:
205
+ *
206
+ * SKI.control.prune will suppress further descending even if nothing was returned
207
+ * SKI.control.stop will terminate further changes.
208
+ * SKI.control.redo will apply the callback to the returned subtree, recursively.
163
209
  *
164
- * The traversal order is leftmost-outermost (LO), i.e. the same order as reduction steps are taken.
210
+ * Note that if redo was applied at least once to a subtree, a null return from the same subtree
211
+ * will be replaced by the last non-null value returned.
212
+ *
213
+ * The traversal order is leftmost-outermost, unless options.order = 'leftmost-innermost' is specified.
214
+ * Short aliases 'LO' and 'LI' (case-sensitive) are also accepted.
165
215
  *
166
216
  * Returns null if no changes were made, or the new expression otherwise.
167
217
  *
168
- * @param {(e:Expr) => (Expr|null)} change
218
+ * @param {{
219
+ * order?: 'LO' | 'LI' | 'leftmost-outermost' | 'leftmost-innermost',
220
+ * }} [options]
221
+ * @param {(e:Expr) => TraverseValue<Expr>} change
169
222
  * @returns {Expr|null}
170
223
  */
171
- traverse(change) {
172
- return change(this);
224
+ traverse(options, change) {
225
+ if (typeof options === "function") {
226
+ change = options;
227
+ options = {};
228
+ }
229
+ const order = ORDER[options.order ?? "LO"];
230
+ if (order === void 0)
231
+ throw new Error("Unknown traversal order: " + options.order);
232
+ const [expr, _] = unwrap(this._traverse_redo({ order }, change));
233
+ return expr;
234
+ }
235
+ /**
236
+ * @private
237
+ * @param {Object} options
238
+ * @param {(e:Expr) => TraverseValue<Expr>} change
239
+ * @returns {TraverseValue<Expr>}
240
+ */
241
+ _traverse_redo(options, change) {
242
+ let action;
243
+ let expr = this;
244
+ let prev;
245
+ do {
246
+ prev = expr;
247
+ const next = options.order === "LI" ? expr._traverse_descend(options, change) ?? change(expr) : change(expr) ?? expr._traverse_descend(options, change);
248
+ [expr, action] = unwrap(next);
249
+ } while (expr && action === control.redo);
250
+ if (!expr && prev !== this)
251
+ expr = prev;
252
+ return action ? action(expr) : expr;
253
+ }
254
+ /**
255
+ * @private
256
+ * @param {Object} options
257
+ * @param {(e:Expr) => TraverseValue<Expr>} change
258
+ * @returns {TraverseValue<Expr>}
259
+ */
260
+ _traverse_descend(options, change) {
261
+ return null;
173
262
  }
174
263
  /**
175
264
  * @desc Returns true if predicate() is true for any subterm of the expression, false otherwise.
@@ -233,60 +322,61 @@ var require_expr = __commonJS({
233
322
  * Use toLambda() if you want to get a lambda term in any case.
234
323
  *
235
324
  * @param {{max?: number, maxArgs?: number}} options
236
- * @return {{
237
- * normal: boolean,
238
- * steps: number,
239
- * expr?: Expr,
240
- * arity?: number,
241
- * proper?: boolean,
242
- * discard?: boolean,
243
- * duplicate?: boolean,
244
- * skip?: Set<number>,
245
- * dup?: Set<number>,
246
- * }}
325
+ * @return {TermInfo}
247
326
  */
248
327
  infer(options = {}) {
249
- const max = options.max ?? DEFAULTS.max;
250
- const maxArgs = options.maxArgs ?? DEFAULTS.maxArgs;
251
- const out = this._infer({ max, maxArgs, index: 0 });
252
- return out;
328
+ return this._infer({
329
+ max: options.max ?? DEFAULTS.max,
330
+ maxArgs: options.maxArgs ?? DEFAULTS.maxArgs
331
+ }, 0);
253
332
  }
254
333
  /**
255
- *
256
- * @param {{max: number, maxArgs: number, index: number}} options
257
- * @param {FreeVar[]} preArgs
258
- * @param {number} steps
259
- * @returns {{
260
- * normal: boolean,
261
- * steps: number,
262
- * expr?: Expr,
263
- * arity?: number,
264
- * skip?: Set<number>,
265
- * dup?: Set<number>,
266
- * duplicate, discard, proper: boolean
267
- * }
334
+ * @desc Internal method for infer(), which performs the actual inference.
335
+ * @param {{max: number, maxArgs: number}} options
336
+ * @param {number} nargs - var index to avoid name clashes
337
+ * @returns {TermInfo}
268
338
  * @private
269
339
  */
270
- _infer(options, preArgs = [], steps = 0) {
271
- if (preArgs.length > options.maxArgs || steps > options.max)
272
- return { normal: false, steps };
273
- if (this.freeOnly()) {
274
- return {
275
- normal: true,
276
- steps,
277
- ...maybeLambda(preArgs, this)
278
- };
340
+ _infer(options, nargs) {
341
+ const probe = [];
342
+ let steps = 0;
343
+ let expr = this;
344
+ main: for (let i = 0; i < options.maxArgs; i++) {
345
+ const next = expr.run({ max: options.max - steps });
346
+ steps += next.steps;
347
+ if (!next.final)
348
+ break;
349
+ if (firstVar(next.expr)) {
350
+ expr = next.expr;
351
+ if (!expr.any((e) => !(e instanceof FreeVar || e instanceof App)))
352
+ return maybeLambda(probe, expr, { steps });
353
+ const list = expr.unroll();
354
+ let discard = false;
355
+ let duplicate = false;
356
+ const acc = [];
357
+ for (let j = 1; j < list.length; j++) {
358
+ const sub = list[j]._infer(
359
+ { maxArgs: options.maxArgs - nargs, max: options.max - steps },
360
+ // limit recursion
361
+ nargs + i
362
+ // avoid variable name clashes
363
+ );
364
+ steps += sub.steps;
365
+ if (!sub.expr)
366
+ break main;
367
+ if (sub.discard)
368
+ discard = true;
369
+ if (sub.duplicate)
370
+ duplicate = true;
371
+ acc.push(sub.expr);
372
+ }
373
+ return maybeLambda(probe, list[0].apply(...acc), { discard, duplicate, steps });
374
+ }
375
+ const push = nthvar(nargs + i);
376
+ probe.push(push);
377
+ expr = next.expr.apply(push);
279
378
  }
280
- const next = this.run({ max: (options.max - steps) / 3 });
281
- steps += next.steps;
282
- if (!next.final)
283
- return { normal: false, steps };
284
- if (next.steps !== 0)
285
- return next.expr._infer(options, preArgs, steps);
286
- if (this.unroll()[0] instanceof FreeVar)
287
- return { normal: false, steps };
288
- const push = nthvar(preArgs.length + options.index);
289
- return this.apply(push)._infer(options, [...preArgs, push], steps);
379
+ return { normal: false, proper: false, steps };
290
380
  }
291
381
  /**
292
382
  * @desc Expand an expression into a list of terms
@@ -324,7 +414,7 @@ var require_expr = __commonJS({
324
414
  * @return {IterableIterator<{expr: Expr, steps?: number, comment?: string}>}
325
415
  */
326
416
  *toLambda(options = {}) {
327
- const expr = this.traverse((e) => {
417
+ let expr = this.traverse((e) => {
328
418
  if (e instanceof FreeVar || e instanceof App || e instanceof Lambda || e instanceof Alias)
329
419
  return null;
330
420
  const guess = e.infer({ max: options.max, maxArgs: options.maxArgs });
@@ -332,7 +422,25 @@ var require_expr = __commonJS({
332
422
  throw new Error("Failed to infer an equivalent lambda term for " + e);
333
423
  return guess.expr;
334
424
  }) ?? this;
335
- yield* simplifyLambda(expr, options);
425
+ const seen = /* @__PURE__ */ new Set();
426
+ let steps = 0;
427
+ while (expr) {
428
+ const next = expr.traverse({ order: "LI" }, (e) => {
429
+ if (seen.has(e))
430
+ return null;
431
+ if (e instanceof App && e.fun instanceof Lambda) {
432
+ const guess = e.infer({ max: options.max, maxArgs: options.maxArgs });
433
+ steps += guess.steps;
434
+ if (!guess.normal) {
435
+ seen.add(e);
436
+ return null;
437
+ }
438
+ return control.stop(guess.expr);
439
+ }
440
+ });
441
+ yield { expr, steps };
442
+ expr = next;
443
+ }
336
444
  }
337
445
  /**
338
446
  * @desc Rewrite the expression into S, K, and I combinators step by step.
@@ -345,28 +453,31 @@ var require_expr = __commonJS({
345
453
  * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
346
454
  */
347
455
  *toSKI(options = {}) {
456
+ let expr = this.traverse((e) => {
457
+ if (e instanceof FreeVar || e instanceof App || e instanceof Lambda || e instanceof Alias)
458
+ return null;
459
+ return e.infer().expr;
460
+ }) ?? this;
348
461
  let steps = 0;
349
- let expr = this;
350
- while (true) {
351
- const opt = { max: options.max ?? 1, steps: 0 };
352
- const next = expr._rski(opt);
353
- const final = opt.steps === 0;
354
- yield { expr, steps, final };
355
- if (final)
356
- break;
462
+ while (expr) {
463
+ const next = expr.traverse({ order: "LI" }, (e) => {
464
+ if (!(e instanceof Lambda) || e.impl instanceof Lambda)
465
+ return null;
466
+ if (e.impl === e.arg)
467
+ return control.stop(native.I);
468
+ if (!e.impl.any((t) => t === e.arg))
469
+ return control.stop(native.K.apply(e.impl));
470
+ if (!(e.impl instanceof App))
471
+ throw new Error("toSKI: assert failed: lambda body is of unexpected type " + e.impl.constructor.name);
472
+ if (e.impl.arg === e.arg && !e.impl.fun.any((t) => t === e.arg))
473
+ return control.stop(e.impl.fun);
474
+ return control.stop(native.S.apply(new Lambda(e.arg, e.impl.fun), new Lambda(e.arg, e.impl.arg)));
475
+ });
476
+ yield { expr, steps, final: !next };
477
+ steps++;
357
478
  expr = next;
358
- steps += opt.steps;
359
479
  }
360
480
  }
361
- /**
362
- * @desc Internal method for toSKI, which performs one step of the conversion.
363
- * @param {{max: number, steps: number}} options
364
- * @returns {Expr}
365
- * @private
366
- */
367
- _rski(options) {
368
- return this;
369
- }
370
481
  /**
371
482
  * Replace all instances of plug in the expression with value and return the resulting expression,
372
483
  * or null if no changes could be made.
@@ -506,19 +617,24 @@ var require_expr = __commonJS({
506
617
  }
507
618
  /**
508
619
  * @desc Assert expression equality. Can be used in tests.
509
- * @param {Expr} expected
620
+ *
621
+ * `this` is the expected value and the argument is the actual one.
622
+ * Mnemonic: the expected value is always a combinator, the actual one may be anything.
623
+ *
624
+ * @param {Expr} actual
510
625
  * @param {string} comment
511
626
  */
512
- expect(expected, comment = "") {
627
+ expect(actual, comment = "") {
513
628
  comment = comment ? comment + ": " : "";
514
- if (!(expected instanceof _Expr))
515
- throw new Error(comment + "attempt to expect a combinator to equal something else: " + expected);
516
- const diff = this.diff(expected);
629
+ if (!(actual instanceof _Expr)) {
630
+ throw new Error(comment + "Expected a combinator but found " + (actual?.constructor?.name ?? typeof actual));
631
+ }
632
+ const diff = this.diff(actual);
517
633
  if (!diff)
518
634
  return;
519
635
  const poorMans = new Error(comment + diff);
520
- poorMans.expected = expected + "";
521
- poorMans.actual = this + "";
636
+ poorMans.expected = this.diag();
637
+ poorMans.actual = actual.diag();
522
638
  throw poorMans;
523
639
  }
524
640
  /**
@@ -646,7 +762,7 @@ var require_expr = __commonJS({
646
762
  if (e instanceof Lambda)
647
763
  return [`${indent}Lambda (${e.arg}[${e.arg.id}]):`, ...rec(e.impl, indent + " ")];
648
764
  if (e instanceof Alias)
649
- return [`Alias (${e.name}):`, ...rec(e.impl, indent + " ")];
765
+ return [`${indent}Alias (${e.name}): \\`, ...rec(e.impl, indent)];
650
766
  if (e instanceof FreeVar)
651
767
  return [`${indent}FreeVar: ${e.name}[${e.id}]`];
652
768
  return [`${indent}${e.constructor.name}: ${e}`];
@@ -673,57 +789,20 @@ var require_expr = __commonJS({
673
789
  super();
674
790
  this.arg = arg;
675
791
  this.fun = fun;
676
- this.final = false;
677
- this.arity = this.fun.arity > 0 ? this.fun.arity - 1 : 0;
678
792
  }
793
+ /** @property {boolean} [final] */
679
794
  weight() {
680
795
  return this.fun.weight() + this.arg.weight();
681
796
  }
682
- _infer(options, preArgs = [], steps = 0) {
683
- if (preArgs.length > options.maxArgs || steps > options.max)
684
- return { normal: false, steps };
685
- const proxy = super._infer(options, preArgs, steps);
686
- if (proxy.normal)
687
- return proxy;
688
- steps = proxy.steps;
689
- const [first, ...list] = this.unroll();
690
- if (!(first instanceof FreeVar))
691
- return { normal: false, steps };
692
- let discard = false;
693
- let duplicate = false;
694
- const out = [];
695
- for (const term of list) {
696
- const guess = term._infer({
697
- ...options,
698
- maxArgs: options.maxArgs - preArgs.length,
699
- max: options.max - steps,
700
- index: preArgs.length + options.index
701
- });
702
- steps += guess.steps;
703
- if (!guess.normal)
704
- return { normal: false, steps };
705
- out.push(guess.expr);
706
- discard = discard || guess.discard;
707
- duplicate = duplicate || guess.duplicate;
708
- }
709
- return {
710
- normal: true,
711
- steps,
712
- ...maybeLambda(preArgs, first.apply(...out), {
713
- discard,
714
- duplicate
715
- })
716
- };
717
- }
718
- traverse(change) {
719
- const replaced = change(this);
720
- if (replaced instanceof Expr)
721
- return replaced;
722
- const fun = this.fun.traverse(change);
723
- const arg = this.arg.traverse(change);
724
- if (!fun && !arg)
725
- return null;
726
- return (fun ?? this.fun).apply(arg ?? this.arg);
797
+ _traverse_descend(options, change) {
798
+ const [fun, fAction] = unwrap(this.fun._traverse_redo(options, change));
799
+ if (fAction === control.stop)
800
+ return control.stop(fun ? fun.apply(this.arg) : null);
801
+ const [arg, aAction] = unwrap(this.arg._traverse_redo(options, change));
802
+ const final = fun || arg ? (fun ?? this.fun).apply(arg ?? this.arg) : null;
803
+ if (aAction === control.stop)
804
+ return control.stop(final);
805
+ return final;
727
806
  }
728
807
  any(predicate) {
729
808
  return predicate(this) || this.fun.any(predicate) || this.arg.any(predicate);
@@ -782,11 +861,6 @@ var require_expr = __commonJS({
782
861
  unroll() {
783
862
  return [...this.fun.unroll(), this.arg];
784
863
  }
785
- _rski(options) {
786
- if (options.steps >= options.max)
787
- return this;
788
- return this.fun._rski(options).apply(this.arg._rski(options));
789
- }
790
864
  diff(other, swap = false) {
791
865
  if (!(other instanceof _App))
792
866
  return super.diff(other, swap);
@@ -848,6 +922,8 @@ var require_expr = __commonJS({
848
922
  * If a scope object is given, however, two variables with the same name and scope
849
923
  * are considered identical.
850
924
  *
925
+ * By convention, FreeVar.global is a constant denoting a global unbound variable.
926
+ *
851
927
  * @param {string} name - name of the variable
852
928
  * @param {any} scope - an object representing where the variable belongs to.
853
929
  */
@@ -878,6 +954,7 @@ var require_expr = __commonJS({
878
954
  return options.var[0] + name + options.var[1];
879
955
  }
880
956
  };
957
+ FreeVar.global = ["global"];
881
958
  var Native = class extends Named {
882
959
  /**
883
960
  * @desc A named term with a known rewriting rule.
@@ -893,29 +970,14 @@ var require_expr = __commonJS({
893
970
  *
894
971
  * @param {String} name
895
972
  * @param {Partial} impl
896
- * @param {{note?: string, arity?: number, canonize?: boolean, apply?: function(Expr):(Expr|null) }} [opt]
973
+ * @param {{note?: string, arity?: number, canonize?: boolean }} [opt]
897
974
  */
898
975
  constructor(name, impl, opt = {}) {
899
976
  super(name);
900
977
  this.invoke = impl;
901
- const guess = opt.canonize ?? true ? this.infer() : { normal: false };
902
- this.arity = opt.arity || guess.arity || 1;
903
- this.note = opt.note ?? guess.expr?.format({ terse: true, html: true, lambda: ["", " &mapsto; ", ""] });
904
- }
905
- _rski(options) {
906
- if (this === native.I || this === native.K || this === native.S || options.steps >= options.max)
907
- return this;
908
- const canon = this.infer().expr;
909
- if (!canon)
910
- return this;
911
- options.steps++;
912
- return canon._rski(options);
978
+ this._setup({ canonize: true, ...opt });
913
979
  }
914
980
  };
915
- var native = {};
916
- function addNative(name, impl, opt) {
917
- native[name] = new Native(name, impl, opt);
918
- }
919
981
  var Lambda = class _Lambda extends Expr {
920
982
  /**
921
983
  * @desc Lambda abstraction of arg over impl.
@@ -956,23 +1018,13 @@ var require_expr = __commonJS({
956
1018
  weight() {
957
1019
  return this.impl.weight() + 1;
958
1020
  }
959
- _infer(options, preArgs = [], steps = 0) {
960
- if (preArgs.length > options.maxArgs)
961
- return { normal: false, steps };
962
- const push = nthvar(preArgs.length + options.index);
963
- return this.invoke(push)._infer(options, [...preArgs, push], steps + 1);
964
- }
965
1021
  invoke(arg) {
966
1022
  return this.impl.subst(this.arg, arg) ?? this.impl;
967
1023
  }
968
- traverse(change) {
969
- const replaced = change(this);
970
- if (replaced instanceof Expr)
971
- return replaced;
972
- const impl = this.impl.traverse(change);
973
- if (!impl)
974
- return null;
975
- return new _Lambda(this.arg, impl);
1024
+ _traverse_descend(options, change) {
1025
+ const [impl, iAction] = unwrap(this.impl._traverse_redo(options, change));
1026
+ const final = impl ? new _Lambda(this.arg, impl) : null;
1027
+ return iAction === control.stop ? control.stop(final) : final;
976
1028
  }
977
1029
  any(predicate) {
978
1030
  return predicate(this) || this.impl.any(predicate);
@@ -994,26 +1046,6 @@ var require_expr = __commonJS({
994
1046
  const change = this.impl.subst(search, replace);
995
1047
  return change ? new _Lambda(this.arg, change) : null;
996
1048
  }
997
- _rski(options) {
998
- const impl = this.impl._rski(options);
999
- if (options.steps >= options.max)
1000
- return new _Lambda(this.arg, impl);
1001
- options.steps++;
1002
- if (impl === this.arg)
1003
- return native.I;
1004
- if (!impl.any((e) => e === this.arg))
1005
- return native.K.apply(impl);
1006
- if (impl instanceof App) {
1007
- const { fun, arg } = impl;
1008
- if (arg === this.arg && !fun.any((e) => e === this.arg))
1009
- return fun._rski(options);
1010
- return native.S.apply(
1011
- new _Lambda(this.arg, fun)._rski(options),
1012
- new _Lambda(this.arg, arg)._rski(options)
1013
- );
1014
- }
1015
- throw new Error("Don't know how to convert to SKI" + this);
1016
- }
1017
1049
  diff(other, swap = false) {
1018
1050
  if (!(other instanceof _Lambda))
1019
1051
  return super.diff(other, swap);
@@ -1030,25 +1062,24 @@ var require_expr = __commonJS({
1030
1062
  return true;
1031
1063
  }
1032
1064
  };
1033
- var Church = class _Church extends Native {
1065
+ var Church = class _Church extends Expr {
1034
1066
  /**
1035
1067
  * @desc Church numeral representing non-negative integer n:
1036
1068
  * n f x = f(f(...(f x)...)) with f applied n times.
1037
1069
  * @param {number} n
1038
1070
  */
1039
1071
  constructor(n) {
1040
- const p = Number.parseInt(n);
1041
- if (!(p >= 0))
1072
+ n = Number.parseInt(n);
1073
+ if (!(n >= 0))
1042
1074
  throw new Error("Church number must be a non-negative integer");
1043
- const name = "" + p;
1044
- const impl = (x) => (y) => {
1075
+ super();
1076
+ this.invoke = (x) => (y) => {
1045
1077
  let expr = y;
1046
- for (let i = p; i-- > 0; )
1078
+ for (let i = n; i-- > 0; )
1047
1079
  expr = x.apply(expr);
1048
1080
  return expr;
1049
1081
  };
1050
- super(name, impl, { arity: 2, canonize: false, note: name });
1051
- this.n = p;
1082
+ this.n = n;
1052
1083
  this.arity = 2;
1053
1084
  }
1054
1085
  diff(other, swap = false) {
@@ -1061,6 +1092,9 @@ var require_expr = __commonJS({
1061
1092
  _unspaced(arg) {
1062
1093
  return false;
1063
1094
  }
1095
+ _format(options, nargs) {
1096
+ return nargs >= 2 ? options.redex[0] + this.n + options.redex[1] : this.n + "";
1097
+ }
1064
1098
  };
1065
1099
  function waitn(expr, n) {
1066
1100
  return (arg) => n <= 1 ? expr.apply(arg) : waitn(expr.apply(arg), n - 1);
@@ -1087,14 +1121,9 @@ var require_expr = __commonJS({
1087
1121
  if (!(impl instanceof Expr))
1088
1122
  throw new Error("Attempt to create an alias for a non-expression: " + impl);
1089
1123
  this.impl = impl;
1090
- if (options.note)
1091
- this.note = options.note;
1092
- const guess = options.canonize ? impl.infer({ max: options.max, maxArgs: options.maxArgs }) : { normal: false };
1093
- this.arity = guess.proper && guess.arity || 0;
1094
- this.proper = guess.proper ?? false;
1095
- this.terminal = options.terminal ?? this.proper;
1096
- this.canonical = guess.expr;
1097
- this.invoke = waitn(impl, this.arity);
1124
+ this._setup(options);
1125
+ this.terminal = options.terminal ?? this.props?.proper;
1126
+ this.invoke = waitn(impl, this.arity ?? 0);
1098
1127
  }
1099
1128
  /**
1100
1129
  * @property {boolean} [outdated] - whether the alias is outdated
@@ -1108,8 +1137,8 @@ var require_expr = __commonJS({
1108
1137
  weight() {
1109
1138
  return this.terminal ? 1 : this.impl.weight();
1110
1139
  }
1111
- traverse(change) {
1112
- return change(this) ?? this.impl.traverse(change);
1140
+ _traverse_descend(options, change) {
1141
+ return this.impl._traverse_redo(options, change);
1113
1142
  }
1114
1143
  any(predicate) {
1115
1144
  return predicate(this) || this.impl.any(predicate);
@@ -1130,9 +1159,6 @@ var require_expr = __commonJS({
1130
1159
  return replace;
1131
1160
  return this.impl.subst(search, replace);
1132
1161
  }
1133
- _infer(options, preArgs = [], steps = 0) {
1134
- return this.impl._infer(options, preArgs, steps);
1135
- }
1136
1162
  // DO NOT REMOVE TYPE or tsc chokes with
1137
1163
  // TS2527: The inferred type of 'Alias' references an inaccessible 'this' type.
1138
1164
  /**
@@ -1148,9 +1174,6 @@ var require_expr = __commonJS({
1148
1174
  return null;
1149
1175
  return other.diff(this.impl, !swap);
1150
1176
  }
1151
- _rski(options) {
1152
- return this.impl._rski(options);
1153
- }
1154
1177
  _braced(first) {
1155
1178
  return this.outdated ? this.impl._braced(first) : false;
1156
1179
  }
@@ -1159,6 +1182,9 @@ var require_expr = __commonJS({
1159
1182
  return outdated ? this.impl._format(options, nargs) : super._format(options, nargs);
1160
1183
  }
1161
1184
  };
1185
+ function addNative(name, impl, opt) {
1186
+ native[name] = new Native(name, impl, opt);
1187
+ }
1162
1188
  addNative("I", (x) => x);
1163
1189
  addNative("K", (x) => (_) => x);
1164
1190
  addNative("S", (x) => (y) => (z) => x.apply(z, y.apply(z)));
@@ -1172,6 +1198,11 @@ var require_expr = __commonJS({
1172
1198
  note: "Increase a Church numeral argument by 1, otherwise n => f => x => f(n f x)"
1173
1199
  }
1174
1200
  );
1201
+ function firstVar(expr) {
1202
+ while (expr instanceof App)
1203
+ expr = expr.fun;
1204
+ return expr instanceof FreeVar;
1205
+ }
1175
1206
  function maybeLambda(args, expr, caps = {}) {
1176
1207
  const count = new Array(args.length).fill(0);
1177
1208
  let proper = true;
@@ -1195,8 +1226,10 @@ var require_expr = __commonJS({
1195
1226
  dup.add(i);
1196
1227
  }
1197
1228
  return {
1229
+ normal: true,
1230
+ steps: caps.steps,
1198
1231
  expr: args.length ? new Lambda(args, expr) : expr,
1199
- ...caps.synth ? {} : { arity: args.length },
1232
+ arity: args.length,
1200
1233
  ...skip.size ? { skip } : {},
1201
1234
  ...dup.size ? { dup } : {},
1202
1235
  duplicate: !!dup.size || caps.duplicate || false,
@@ -1207,43 +1240,6 @@ var require_expr = __commonJS({
1207
1240
  function nthvar(n) {
1208
1241
  return new FreeVar("abcdefgh"[n] ?? "x" + n);
1209
1242
  }
1210
- function* simplifyLambda(expr, options = {}, state = { steps: 0 }) {
1211
- yield { expr, steps: state.steps, comment: "(self)" };
1212
- if (expr.freeOnly())
1213
- return;
1214
- let maxWeight = expr.weight();
1215
- if (expr instanceof Lambda) {
1216
- for (const term of simplifyLambda(expr.impl, options, state)) {
1217
- const candidate = new Lambda(expr.arg, term.expr);
1218
- if (candidate.weight() < maxWeight) {
1219
- maxWeight = candidate.weight();
1220
- yield { expr: candidate, steps: state.steps, comment: "(lambda)" + term.comment };
1221
- }
1222
- }
1223
- }
1224
- if (expr instanceof App) {
1225
- let { fun, arg } = expr;
1226
- for (const term of simplifyLambda(fun, options, state)) {
1227
- const candidate = term.expr.apply(arg);
1228
- if (candidate.weight() < maxWeight) {
1229
- maxWeight = candidate.weight();
1230
- fun = term.expr;
1231
- yield { expr: candidate, steps: state.steps, comment: "(fun)" + term.comment };
1232
- }
1233
- }
1234
- for (const term of simplifyLambda(arg, options, state)) {
1235
- const candidate = fun.apply(term.expr);
1236
- if (candidate.weight() < maxWeight) {
1237
- maxWeight = candidate.weight();
1238
- yield { expr: candidate, steps: state.steps, comment: "(arg)" + term.comment };
1239
- }
1240
- }
1241
- }
1242
- const canon = expr.infer({ max: options.max, maxArgs: options.maxArgs });
1243
- state.steps += canon.steps;
1244
- if (canon.expr && canon.expr.weight() < maxWeight)
1245
- yield { expr: canon.expr, steps: state.steps, comment: "(canonical)" };
1246
- }
1247
1243
  function toposort(list, env) {
1248
1244
  if (list instanceof Expr)
1249
1245
  list = [list];
@@ -1347,7 +1343,7 @@ var require_parser = __commonJS({
1347
1343
  "->",
1348
1344
  "\\+"
1349
1345
  );
1350
- var SKI2 = class _SKI {
1346
+ var SKI2 = class {
1351
1347
  /**
1352
1348
  *
1353
1349
  * @param {{
@@ -1393,18 +1389,18 @@ var require_parser = __commonJS({
1393
1389
  *
1394
1390
  * @param {Alias|String} term
1395
1391
  * @param {String|Expr|function(Expr):Partial} [impl]
1396
- * @param {String} [note]
1392
+ * @param {object|string} [options]
1393
+ * @param {string} [options.note] - optional annotation for the term, default is auto-generated if this.annotate is true
1394
+ * @param {boolean} [options.canonize] - whether to canonize the term's implementation, default is this.annotate
1395
+ * @param {boolean} [options.fancy] - alternative HTML-friendly name for the term
1396
+ * @param {number} [options.arity] - custom arity for the term, default is inferred from the implementation
1397
1397
  * @return {SKI} chainable
1398
1398
  */
1399
- add(term, impl, note) {
1399
+ add(term, impl, options) {
1400
1400
  term = this._named(term, impl);
1401
- if (this.annotate && note === void 0) {
1402
- const guess = term.infer();
1403
- if (guess.expr)
1404
- note = guess.expr.format({ terse: true, html: true, lambda: ["", " &mapsto; ", ""] });
1405
- }
1406
- if (note !== void 0)
1407
- term.note = note;
1401
+ if (typeof options === "string")
1402
+ options = { note: options, canonize: false };
1403
+ term._setup({ canonize: this.annotate, ...options });
1408
1404
  if (this.known[term.name])
1409
1405
  this.known[term.name].outdated = true;
1410
1406
  this.known[term.name] = term;
@@ -1596,7 +1592,7 @@ var require_parser = __commonJS({
1596
1592
  expr.outdated = true;
1597
1593
  const def = item.match(/^([A-Z]|[a-z][a-z_0-9]*)\s*=(.*)$/s);
1598
1594
  if (def && def[2] === "")
1599
- expr = new FreeVar(def[1], options.scope ?? _SKI);
1595
+ expr = new FreeVar(def[1], options.scope ?? FreeVar.global);
1600
1596
  else
1601
1597
  expr = this.parseLine(item, jar, options);
1602
1598
  if (def) {
@@ -1641,7 +1637,7 @@ var require_parser = __commonJS({
1641
1637
  const tokens = combChars.split(source);
1642
1638
  const empty = new Empty();
1643
1639
  const stack = [empty];
1644
- const context = options.scope || _SKI;
1640
+ const context = options.scope || FreeVar.global;
1645
1641
  for (const c of tokens) {
1646
1642
  if (c === "(")
1647
1643
  stack.push(empty);
@@ -1859,6 +1855,62 @@ var require_quest = __commonJS({
1859
1855
  return { pass: false, details: [], exception: e, steps: 0, input };
1860
1856
  }
1861
1857
  }
1858
+ verify(options) {
1859
+ const findings = this.verifyMeta(options);
1860
+ if (options.solutions) {
1861
+ const solCheck = this.verifySolutions(options.solutions);
1862
+ if (solCheck)
1863
+ findings.solutions = solCheck;
1864
+ }
1865
+ if (options.seen) {
1866
+ if (!this.id)
1867
+ findings.seen = "No id in quest " + (this.name ?? "(unnamed)");
1868
+ if (options.seen.has(this.id))
1869
+ findings.seen = "Duplicate quest id " + this.id;
1870
+ options.seen.add(this.id);
1871
+ }
1872
+ return Object.keys(findings).length ? findings : null;
1873
+ }
1874
+ /**
1875
+ * @desc Verify that solutions that are expected to pass/fail do so.
1876
+ * @param {SelfCheck|{[key: string]: SelfCheck}} dataset
1877
+ * @return {{shouldPass: {input: string[], result: QuestResult}[], shouldFail: {input: string[], result: QuestResult}[]} | null}
1878
+ */
1879
+ verifySolutions(dataset) {
1880
+ if (typeof dataset === "object" && !Array.isArray(dataset?.accepted) && !Array.isArray(dataset?.rejected)) {
1881
+ if (!this.id || !dataset[this.id])
1882
+ return null;
1883
+ }
1884
+ const { accepted = [], rejected = [] } = dataset[this.id] ?? dataset;
1885
+ const ret = { shouldPass: [], shouldFail: [] };
1886
+ for (const input of accepted) {
1887
+ const result = this.check(...input);
1888
+ if (!result.pass)
1889
+ ret.shouldPass.push({ input, result });
1890
+ }
1891
+ for (const input of rejected) {
1892
+ const result = this.check(...input);
1893
+ if (result.pass)
1894
+ ret.shouldFail.push({ input, result });
1895
+ }
1896
+ return ret.shouldFail.length + ret.shouldPass.length ? ret : null;
1897
+ }
1898
+ verifyMeta(options = {}) {
1899
+ const findings = {};
1900
+ for (const field of ["name", "intro"]) {
1901
+ const found = checkHtml(this[field]);
1902
+ if (found)
1903
+ findings[field] = found;
1904
+ }
1905
+ if (options.date) {
1906
+ const date = new Date(this.meta.created_at);
1907
+ if (isNaN(date))
1908
+ findings.date = "invalid date format: " + this.meta.created_at;
1909
+ else if (date < /* @__PURE__ */ new Date("2024-07-15") || date > /* @__PURE__ */ new Date())
1910
+ findings.date = "date out of range: " + this.meta.created_at;
1911
+ }
1912
+ return findings;
1913
+ }
1862
1914
  /**
1863
1915
  *
1864
1916
  * @return {TestCase[]}
@@ -2009,11 +2061,67 @@ var require_quest = __commonJS({
2009
2061
  return expr;
2010
2062
  }
2011
2063
  };
2064
+ var Group = class {
2065
+ constructor(options) {
2066
+ this.name = options.name;
2067
+ this.intro = list2str(options.intro);
2068
+ this.id = options.id;
2069
+ if (options.content)
2070
+ this.content = options.content.map((c) => c instanceof Quest2 ? c : new Quest2(c));
2071
+ }
2072
+ verify(options) {
2073
+ const findings = {};
2074
+ const id = checkId(this.id, options.seen);
2075
+ if (id)
2076
+ findings[this.id] = id;
2077
+ for (const field of ["name", "intro"]) {
2078
+ const found = checkHtml(this[field]);
2079
+ if (found)
2080
+ findings[field] = found;
2081
+ }
2082
+ findings.content = this.content.map((q) => q.verify(options));
2083
+ return findings;
2084
+ }
2085
+ };
2012
2086
  function list2str(str) {
2013
2087
  if (str === void 0 || typeof str === "string")
2014
2088
  return str;
2015
2089
  return Array.isArray(str) ? str.join(" ") : "" + str;
2016
2090
  }
2091
+ function checkId(id, seen) {
2092
+ if (id === void 0)
2093
+ return "missing";
2094
+ if (typeof id !== "string" && typeof id !== "number")
2095
+ return "is a " + typeof id;
2096
+ if (seen) {
2097
+ if (seen.has(id))
2098
+ return "duplicate id " + id;
2099
+ seen.add(id);
2100
+ }
2101
+ }
2102
+ function checkHtml(str) {
2103
+ if (str === void 0)
2104
+ return "missing";
2105
+ if (typeof str !== "string")
2106
+ return "not a string but " + typeof str;
2107
+ const tagStack = [];
2108
+ const tagRegex = /<\/?([a-z]+)(?:\s[^>]*)?>/gi;
2109
+ let match;
2110
+ while ((match = tagRegex.exec(str)) !== null) {
2111
+ const [fullTag, tagName] = match;
2112
+ if (fullTag.startsWith("</")) {
2113
+ if (tagStack.length === 0 || tagStack.pop() !== tagName)
2114
+ return `Unmatched closing tag: </${tagName}>`;
2115
+ } else {
2116
+ tagStack.push(tagName);
2117
+ }
2118
+ }
2119
+ if (tagStack.length > 0)
2120
+ return `Unclosed tags: ${tagStack.join(", ")}`;
2121
+ return null;
2122
+ }
2123
+ Quest2.Group = Group;
2124
+ Quest2.Case = Case;
2017
2125
  module2.exports = { Quest: Quest2 };
2018
2126
  }
2019
2127
  });
@@ -2023,6 +2131,7 @@ var require_extras = __commonJS({
2023
2131
  "src/extras.js"(exports2, module2) {
2024
2132
  "use strict";
2025
2133
  var { Expr, Alias, FreeVar } = require_expr();
2134
+ var { Quest: Quest2 } = require_quest();
2026
2135
  function search(seed, options, predicate) {
2027
2136
  const {
2028
2137
  depth = 16,
@@ -2088,6 +2197,10 @@ var require_extras = __commonJS({
2088
2197
  function deepFormat(obj, options = {}) {
2089
2198
  if (obj instanceof Expr)
2090
2199
  return obj.format(options);
2200
+ if (obj instanceof Quest2)
2201
+ return "Quest(" + obj.name + ")";
2202
+ if (obj instanceof Quest2.Case)
2203
+ return "Quest.Case";
2091
2204
  if (Array.isArray(obj))
2092
2205
  return obj.map(deepFormat);
2093
2206
  if (typeof obj !== "object" || obj === null || obj.constructor !== Object)