@dallaylaen/ski-interpreter 2.2.0 → 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.
@@ -59,24 +59,34 @@ var require_internal = __commonJS({
59
59
  }
60
60
  return out;
61
61
  }
62
- var ActionWrapper = class {
62
+ var TraverseControl = class {
63
63
  /**
64
+ * @desc A wrapper for values returned by fold/traverse callbacks
65
+ * which instructs the traversal to alter its behavior while
66
+ * retaining the value in question.
67
+ *
68
+ * This class is instantiated internally be `SKI.control.*` functions,
69
+ * and is not intended to be used directly by client code.
70
+ *
64
71
  * @template T
65
72
  * @param {T} value
66
- * @param {string} action
73
+ * @param {function(T): TraverseControl<T>} decoration
67
74
  */
68
- constructor(value, action) {
75
+ constructor(value, decoration) {
69
76
  this.value = value;
70
- this.action = action;
77
+ this.decoration = decoration;
71
78
  }
72
79
  };
73
80
  function unwrap(value) {
74
- if (value instanceof ActionWrapper)
75
- return [value.value ?? void 0, value.action];
81
+ if (value instanceof TraverseControl)
82
+ return [value.value ?? void 0, value.decoration];
76
83
  return [value ?? void 0, void 0];
77
84
  }
78
- function prepareWrapper(action) {
79
- return (value) => new ActionWrapper(value, action);
85
+ function prepareWrapper(label) {
86
+ const fun = (value) => new TraverseControl(value, fun);
87
+ fun.label = label;
88
+ fun.toString = () => "TraverseControl::" + label;
89
+ return fun;
80
90
  }
81
91
  module2.exports = { Tokenizer, restrict, unwrap, prepareWrapper };
82
92
  }
@@ -91,11 +101,19 @@ var require_expr = __commonJS({
91
101
  max: 1e3,
92
102
  maxArgs: 32
93
103
  };
104
+ var ORDER = {
105
+ "leftmost-outermost": "LO",
106
+ "leftmost-innermost": "LI",
107
+ LO: "LO",
108
+ LI: "LI"
109
+ };
94
110
  var control = {
95
111
  descend: prepareWrapper("descend"),
96
112
  prune: prepareWrapper("prune"),
113
+ redo: prepareWrapper("redo"),
97
114
  stop: prepareWrapper("stop")
98
115
  };
116
+ var native = {};
99
117
  var Expr = class _Expr {
100
118
  /**
101
119
  * @descr A combinatory logic expression.
@@ -109,11 +127,44 @@ var require_expr = __commonJS({
109
127
  * env?: { [key: string]: Expr },
110
128
  * src?: string,
111
129
  * parser: object,
112
- * }} [context] // TODO proper type
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. &phi; instead of 'f'
134
+ * Typically only applicable to descendants of Named.
135
+ * @property {TermInfo} [props] - properties inferred from the term's behavior
113
136
  */
114
- constructor() {
115
- if (new.target === _Expr)
116
- 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. &phi; 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: ["", " &mapsto; ", ""] });
163
+ delete guess.steps;
164
+ this.props = guess;
165
+ }
166
+ }
167
+ return this;
117
168
  }
118
169
  /**
119
170
  * @desc apply self to zero or more terms and return the resulting term,
@@ -137,22 +188,77 @@ var require_expr = __commonJS({
137
188
  return e.impl.expand();
138
189
  }) ?? this;
139
190
  }
191
+ /**
192
+ * @desc Returns true if the expression contains only free variables and applications, false otherwise.
193
+ * @returns {boolean}
194
+ */
140
195
  freeOnly() {
141
196
  return !this.any((e) => !(e instanceof FreeVar || e instanceof App));
142
197
  }
143
198
  /**
144
199
  * @desc Traverse the expression tree, applying change() to each node.
145
200
  * If change() returns an Expr, the node is replaced with that value.
146
- * Otherwise, the node is left descended further (if applicable)
147
- * 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.
209
+ *
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.
148
215
  *
149
216
  * Returns null if no changes were made, or the new expression otherwise.
150
217
  *
151
- * @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
152
222
  * @returns {Expr|null}
153
223
  */
154
- traverse(change) {
155
- 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;
156
262
  }
157
263
  /**
158
264
  * @desc Returns true if predicate() is true for any subterm of the expression, false otherwise.
@@ -179,18 +285,25 @@ var require_expr = __commonJS({
179
285
  * @experimental
180
286
  * @template T
181
287
  * @param {T} initial
182
- * @param {(acc: T, expr: Expr) => ActionWrapper<T>} combine
288
+ * @param {(acc: T, expr: Expr) => TraverseValue<T>} combine
183
289
  * @returns {T}
184
290
  */
185
291
  fold(initial, combine) {
186
292
  const [value, _] = unwrap(this._fold(initial, combine));
187
293
  return value ?? initial;
188
294
  }
295
+ /**
296
+ * @template T
297
+ * @param {T} initial
298
+ * @param {(acc: T, expr: Expr) => TraverseValue<T>} combine
299
+ * @returns {TraverseValue<T>}
300
+ * @private
301
+ */
189
302
  _fold(initial, combine) {
190
303
  return combine(initial, this);
191
304
  }
192
305
  /**
193
- * @desc rough estimate of the complexity of the term
306
+ * @desc rough estimate of the term's complexity
194
307
  * @return {number}
195
308
  */
196
309
  weight() {
@@ -209,52 +322,61 @@ var require_expr = __commonJS({
209
322
  * Use toLambda() if you want to get a lambda term in any case.
210
323
  *
211
324
  * @param {{max?: number, maxArgs?: number}} options
212
- * @return {{
213
- * normal: boolean,
214
- * steps: number,
215
- * expr?: Expr,
216
- * arity?: number,
217
- * proper?: boolean,
218
- * discard?: boolean,
219
- * duplicate?: boolean,
220
- * skip?: Set<number>,
221
- * dup?: Set<number>,
222
- * }}
325
+ * @return {TermInfo}
223
326
  */
224
327
  infer(options = {}) {
225
- const max = options.max ?? DEFAULTS.max;
226
- const maxArgs = options.maxArgs ?? DEFAULTS.maxArgs;
227
- const out = this._infer({ max, maxArgs, index: 0 });
228
- return out;
328
+ return this._infer({
329
+ max: options.max ?? DEFAULTS.max,
330
+ maxArgs: options.maxArgs ?? DEFAULTS.maxArgs
331
+ }, 0);
229
332
  }
230
333
  /**
231
- *
232
- * @param {{max: number, maxArgs: number, index: number}} options
233
- * @param {FreeVar[]} preArgs
234
- * @param {number} steps
235
- * @returns {{normal: boolean, steps: number}|{normal: boolean, steps: number}|{normal: boolean, steps: number, expr: Lambda|*, arity?: *, skip?: Set<any>, dup?: Set<any>, duplicate, discard, proper: boolean}|*|{normal: boolean, steps: number}}
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}
236
338
  * @private
237
339
  */
238
- _infer(options, preArgs = [], steps = 0) {
239
- if (preArgs.length > options.maxArgs || steps > options.max)
240
- return { normal: false, steps };
241
- if (this.freeOnly()) {
242
- return {
243
- normal: true,
244
- steps,
245
- ...maybeLambda(preArgs, this)
246
- };
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);
247
378
  }
248
- const next = this.run({ max: (options.max - steps) / 3 });
249
- steps += next.steps;
250
- if (!next.final)
251
- return { normal: false, steps };
252
- if (next.steps !== 0)
253
- return next.expr._infer(options, preArgs, steps);
254
- if (this.unroll()[0] instanceof FreeVar)
255
- return { normal: false, steps };
256
- const push = nthvar(preArgs.length + options.index);
257
- return this.apply(push)._infer(options, [...preArgs, push], steps);
379
+ return { normal: false, proper: false, steps };
258
380
  }
259
381
  /**
260
382
  * @desc Expand an expression into a list of terms
@@ -289,10 +411,10 @@ var require_expr = __commonJS({
289
411
  * latin?: number,
290
412
  * }} options
291
413
  * @param {number} [maxWeight] - maximum allowed weight of terms in the sequence
292
- * @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
414
+ * @return {IterableIterator<{expr: Expr, steps?: number, comment?: string}>}
293
415
  */
294
416
  *toLambda(options = {}) {
295
- const expr = this.traverse((e) => {
417
+ let expr = this.traverse((e) => {
296
418
  if (e instanceof FreeVar || e instanceof App || e instanceof Lambda || e instanceof Alias)
297
419
  return null;
298
420
  const guess = e.infer({ max: options.max, maxArgs: options.maxArgs });
@@ -300,7 +422,25 @@ var require_expr = __commonJS({
300
422
  throw new Error("Failed to infer an equivalent lambda term for " + e);
301
423
  return guess.expr;
302
424
  }) ?? this;
303
- 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
+ }
304
444
  }
305
445
  /**
306
446
  * @desc Rewrite the expression into S, K, and I combinators step by step.
@@ -313,28 +453,31 @@ var require_expr = __commonJS({
313
453
  * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
314
454
  */
315
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;
316
461
  let steps = 0;
317
- let expr = this;
318
- while (true) {
319
- const opt = { max: options.max ?? 1, steps: 0 };
320
- const next = expr._rski(opt);
321
- const final = opt.steps === 0;
322
- yield { expr, steps, final };
323
- if (final)
324
- 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++;
325
478
  expr = next;
326
- steps += opt.steps;
327
479
  }
328
480
  }
329
- /**
330
- * @desc Internal method for toSKI, which performs one step of the conversion.
331
- * @param {{max: number, steps: number}} options
332
- * @returns {Expr}
333
- * @private
334
- */
335
- _rski(options) {
336
- return this;
337
- }
338
481
  /**
339
482
  * Replace all instances of plug in the expression with value and return the resulting expression,
340
483
  * or null if no changes could be made.
@@ -382,7 +525,7 @@ var require_expr = __commonJS({
382
525
  * @desc Run uninterrupted sequence of step() applications
383
526
  * until the expression is irreducible, or max number of steps is reached.
384
527
  * Default number of steps = 1000.
385
- * @param {{max: number?, steps: number?, throw: boolean?}|Expr} [opt]
528
+ * @param {{max?: number, steps?: number, throw?: boolean}|Expr} [opt]
386
529
  * @param {Expr} args
387
530
  * @return {{expr: Expr, steps: number, final: boolean}}
388
531
  */
@@ -411,7 +554,7 @@ var require_expr = __commonJS({
411
554
  /**
412
555
  * Execute step() while possible, yielding a brief description of events after each step.
413
556
  * Mnemonics: like run() but slower.
414
- * @param {{max: number?}} options
557
+ * @param {{max?: number}} options
415
558
  * @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
416
559
  */
417
560
  *walk(options = {}) {
@@ -474,19 +617,24 @@ var require_expr = __commonJS({
474
617
  }
475
618
  /**
476
619
  * @desc Assert expression equality. Can be used in tests.
477
- * @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
478
625
  * @param {string} comment
479
626
  */
480
- expect(expected, comment = "") {
627
+ expect(actual, comment = "") {
481
628
  comment = comment ? comment + ": " : "";
482
- if (!(expected instanceof _Expr))
483
- throw new Error(comment + "attempt to expect a combinator to equal something else: " + expected);
484
- 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);
485
633
  if (!diff)
486
634
  return;
487
635
  const poorMans = new Error(comment + diff);
488
- poorMans.expected = expected + "";
489
- poorMans.actual = this + "";
636
+ poorMans.expected = this.diag();
637
+ poorMans.actual = actual.diag();
490
638
  throw poorMans;
491
639
  }
492
640
  /**
@@ -586,6 +734,42 @@ var require_expr = __commonJS({
586
734
  _format(options, nargs) {
587
735
  throw new Error("No _format() method defined in class " + this.constructor.name);
588
736
  }
737
+ /**
738
+ * @desc Returns a string representation of the expression tree, with indentation to show structure.
739
+ *
740
+ * Applications are flattened to avoid excessive nesting.
741
+ * Variables include ids to distinguish different instances of the same variable name.
742
+ *
743
+ * May be useful for debugging.
744
+ *
745
+ * @returns {string}
746
+ *
747
+ * @example
748
+ * > console.log(ski.parse('C 5 x (x->x x)').diag())
749
+ * App:
750
+ * Native: C
751
+ * Church: 5
752
+ * FreeVar: x[53]
753
+ * Lambda (x[54]):
754
+ * App:
755
+ * FreeVar: x[54]
756
+ * FreeVar: x[54]
757
+ */
758
+ diag() {
759
+ const rec = (e, indent) => {
760
+ if (e instanceof App)
761
+ return [indent + "App:", ...e.unroll().flatMap((s) => rec(s, indent + " "))];
762
+ if (e instanceof Lambda)
763
+ return [`${indent}Lambda (${e.arg}[${e.arg.id}]):`, ...rec(e.impl, indent + " ")];
764
+ if (e instanceof Alias)
765
+ return [`${indent}Alias (${e.name}): \\`, ...rec(e.impl, indent)];
766
+ if (e instanceof FreeVar)
767
+ return [`${indent}FreeVar: ${e.name}[${e.id}]`];
768
+ return [`${indent}${e.constructor.name}: ${e}`];
769
+ };
770
+ const out = rec(this, "");
771
+ return out.join("\n");
772
+ }
589
773
  /**
590
774
  * @desc Convert the expression to a JSON-serializable format.
591
775
  * @returns {string}
@@ -605,72 +789,35 @@ var require_expr = __commonJS({
605
789
  super();
606
790
  this.arg = arg;
607
791
  this.fun = fun;
608
- this.final = false;
609
- this.arity = this.fun.arity > 0 ? this.fun.arity - 1 : 0;
610
792
  }
793
+ /** @property {boolean} [final] */
611
794
  weight() {
612
795
  return this.fun.weight() + this.arg.weight();
613
796
  }
614
- _infer(options, preArgs = [], steps = 0) {
615
- if (preArgs.length > options.maxArgs || steps > options.max)
616
- return { normal: false, steps };
617
- const proxy = super._infer(options, preArgs, steps);
618
- if (proxy.normal)
619
- return proxy;
620
- steps = proxy.steps;
621
- const [first, ...list] = this.unroll();
622
- if (!(first instanceof FreeVar))
623
- return { normal: false, steps };
624
- let discard = false;
625
- let duplicate = false;
626
- const out = [];
627
- for (const term of list) {
628
- const guess = term._infer({
629
- ...options,
630
- maxArgs: options.maxArgs - preArgs.length,
631
- max: options.max - steps,
632
- index: preArgs.length + options.index
633
- });
634
- steps += guess.steps;
635
- if (!guess.normal)
636
- return { normal: false, steps };
637
- out.push(guess.expr);
638
- discard = discard || guess.discard;
639
- duplicate = duplicate || guess.duplicate;
640
- }
641
- return {
642
- normal: true,
643
- steps,
644
- ...maybeLambda(preArgs, first.apply(...out), {
645
- discard,
646
- duplicate
647
- })
648
- };
649
- }
650
- traverse(change) {
651
- const replaced = change(this);
652
- if (replaced instanceof Expr)
653
- return replaced;
654
- const fun = this.fun.traverse(change);
655
- const arg = this.arg.traverse(change);
656
- if (!fun && !arg)
657
- return null;
658
- 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;
659
806
  }
660
807
  any(predicate) {
661
808
  return predicate(this) || this.fun.any(predicate) || this.arg.any(predicate);
662
809
  }
663
810
  _fold(initial, combine) {
664
811
  const [value = initial, action = "descend"] = unwrap(combine(initial, this));
665
- if (action === "prune")
812
+ if (action === control.prune)
666
813
  return value;
667
- if (action === "stop")
814
+ if (action === control.stop)
668
815
  return control.stop(value);
669
816
  const [fValue = value, fAction = "descend"] = unwrap(this.fun._fold(value, combine));
670
- if (fAction === "stop")
817
+ if (fAction === control.stop)
671
818
  return control.stop(fValue);
672
819
  const [aValue = fValue, aAction = "descend"] = unwrap(this.arg._fold(fValue, combine));
673
- if (aAction === "stop")
820
+ if (aAction === control.stop)
674
821
  return control.stop(aValue);
675
822
  return aValue;
676
823
  }
@@ -714,15 +861,6 @@ var require_expr = __commonJS({
714
861
  unroll() {
715
862
  return [...this.fun.unroll(), this.arg];
716
863
  }
717
- /**
718
- * @desc Convert the expression to SKI combinatory logic
719
- * @return {Expr}
720
- */
721
- _rski(options) {
722
- if (options.steps >= options.max)
723
- return this;
724
- return this.fun._rski(options).apply(this.arg._rski(options));
725
- }
726
864
  diff(other, swap = false) {
727
865
  if (!(other instanceof _App))
728
866
  return super.diff(other, swap);
@@ -784,6 +922,8 @@ var require_expr = __commonJS({
784
922
  * If a scope object is given, however, two variables with the same name and scope
785
923
  * are considered identical.
786
924
  *
925
+ * By convention, FreeVar.global is a constant denoting a global unbound variable.
926
+ *
787
927
  * @param {string} name - name of the variable
788
928
  * @param {any} scope - an object representing where the variable belongs to.
789
929
  */
@@ -814,6 +954,7 @@ var require_expr = __commonJS({
814
954
  return options.var[0] + name + options.var[1];
815
955
  }
816
956
  };
957
+ FreeVar.global = ["global"];
817
958
  var Native = class extends Named {
818
959
  /**
819
960
  * @desc A named term with a known rewriting rule.
@@ -829,29 +970,14 @@ var require_expr = __commonJS({
829
970
  *
830
971
  * @param {String} name
831
972
  * @param {Partial} impl
832
- * @param {{note?: string, arity?: number, canonize?: boolean, apply?: function(Expr):(Expr|null) }} [opt]
973
+ * @param {{note?: string, arity?: number, canonize?: boolean }} [opt]
833
974
  */
834
975
  constructor(name, impl, opt = {}) {
835
976
  super(name);
836
977
  this.invoke = impl;
837
- const guess = opt.canonize ?? true ? this.infer() : { normal: false };
838
- this.arity = opt.arity || guess.arity || 1;
839
- this.note = opt.note ?? guess.expr?.format({ terse: true, html: true, lambda: ["", " &mapsto; ", ""] });
840
- }
841
- _rski(options) {
842
- if (this === native.I || this === native.K || this === native.S || options.steps >= options.max)
843
- return this;
844
- const canon = this.infer().expr;
845
- if (!canon)
846
- return this;
847
- options.steps++;
848
- return canon._rski(options);
978
+ this._setup({ canonize: true, ...opt });
849
979
  }
850
980
  };
851
- var native = {};
852
- function addNative(name, impl, opt) {
853
- native[name] = new Native(name, impl, opt);
854
- }
855
981
  var Lambda = class _Lambda extends Expr {
856
982
  /**
857
983
  * @desc Lambda abstraction of arg over impl.
@@ -892,35 +1018,25 @@ var require_expr = __commonJS({
892
1018
  weight() {
893
1019
  return this.impl.weight() + 1;
894
1020
  }
895
- _infer(options, preArgs = [], steps = 0) {
896
- if (preArgs.length > options.maxArgs)
897
- return { normal: false, steps };
898
- const push = nthvar(preArgs.length + options.index);
899
- return this.invoke(push)._infer(options, [...preArgs, push], steps + 1);
900
- }
901
1021
  invoke(arg) {
902
1022
  return this.impl.subst(this.arg, arg) ?? this.impl;
903
1023
  }
904
- traverse(change) {
905
- const replaced = change(this);
906
- if (replaced instanceof Expr)
907
- return replaced;
908
- const impl = this.impl.traverse(change);
909
- if (!impl)
910
- return null;
911
- 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;
912
1028
  }
913
1029
  any(predicate) {
914
1030
  return predicate(this) || this.impl.any(predicate);
915
1031
  }
916
1032
  _fold(initial, combine) {
917
1033
  const [value = initial, action = "descend"] = unwrap(combine(initial, this));
918
- if (action === "prune")
1034
+ if (action === control.prune)
919
1035
  return value;
920
- if (action === "stop")
1036
+ if (action === control.stop)
921
1037
  return control.stop(value);
922
1038
  const [iValue, iAction] = unwrap(this.impl._fold(value, combine));
923
- if (iAction === "stop")
1039
+ if (iAction === control.stop)
924
1040
  return control.stop(iValue);
925
1041
  return iValue ?? value;
926
1042
  }
@@ -930,26 +1046,6 @@ var require_expr = __commonJS({
930
1046
  const change = this.impl.subst(search, replace);
931
1047
  return change ? new _Lambda(this.arg, change) : null;
932
1048
  }
933
- _rski(options) {
934
- const impl = this.impl._rski(options);
935
- if (options.steps >= options.max)
936
- return new _Lambda(this.arg, impl);
937
- options.steps++;
938
- if (impl === this.arg)
939
- return native.I;
940
- if (!impl.any((e) => e === this.arg))
941
- return native.K.apply(impl);
942
- if (impl instanceof App) {
943
- const { fun, arg } = impl;
944
- if (arg === this.arg && !fun.any((e) => e === this.arg))
945
- return fun._rski(options);
946
- return native.S.apply(
947
- new _Lambda(this.arg, fun)._rski(options),
948
- new _Lambda(this.arg, arg)._rski(options)
949
- );
950
- }
951
- throw new Error("Don't know how to convert to SKI" + this);
952
- }
953
1049
  diff(other, swap = false) {
954
1050
  if (!(other instanceof _Lambda))
955
1051
  return super.diff(other, swap);
@@ -966,25 +1062,24 @@ var require_expr = __commonJS({
966
1062
  return true;
967
1063
  }
968
1064
  };
969
- var Church = class _Church extends Native {
1065
+ var Church = class _Church extends Expr {
970
1066
  /**
971
1067
  * @desc Church numeral representing non-negative integer n:
972
1068
  * n f x = f(f(...(f x)...)) with f applied n times.
973
1069
  * @param {number} n
974
1070
  */
975
1071
  constructor(n) {
976
- const p = Number.parseInt(n);
977
- if (!(p >= 0))
1072
+ n = Number.parseInt(n);
1073
+ if (!(n >= 0))
978
1074
  throw new Error("Church number must be a non-negative integer");
979
- const name = "" + p;
980
- const impl = (x) => (y) => {
1075
+ super();
1076
+ this.invoke = (x) => (y) => {
981
1077
  let expr = y;
982
- for (let i = p; i-- > 0; )
1078
+ for (let i = n; i-- > 0; )
983
1079
  expr = x.apply(expr);
984
1080
  return expr;
985
1081
  };
986
- super(name, impl, { arity: 2, canonize: false, note: name });
987
- this.n = p;
1082
+ this.n = n;
988
1083
  this.arity = 2;
989
1084
  }
990
1085
  diff(other, swap = false) {
@@ -997,6 +1092,9 @@ var require_expr = __commonJS({
997
1092
  _unspaced(arg) {
998
1093
  return false;
999
1094
  }
1095
+ _format(options, nargs) {
1096
+ return nargs >= 2 ? options.redex[0] + this.n + options.redex[1] : this.n + "";
1097
+ }
1000
1098
  };
1001
1099
  function waitn(expr, n) {
1002
1100
  return (arg) => n <= 1 ? expr.apply(arg) : waitn(expr.apply(arg), n - 1);
@@ -1016,39 +1114,43 @@ var require_expr = __commonJS({
1016
1114
  *
1017
1115
  * @param {String} name
1018
1116
  * @param {Expr} impl
1019
- * @param {{canonize: boolean?, max: number?, maxArgs: number?, note: string?, terminal: boolean?}} [options]
1117
+ * @param {{canonize?: boolean, max?: number, maxArgs?: number, note?: string, terminal?: boolean}} [options]
1020
1118
  */
1021
1119
  constructor(name, impl, options = {}) {
1022
1120
  super(name);
1023
1121
  if (!(impl instanceof Expr))
1024
1122
  throw new Error("Attempt to create an alias for a non-expression: " + impl);
1025
1123
  this.impl = impl;
1026
- if (options.note)
1027
- this.note = options.note;
1028
- const guess = options.canonize ? impl.infer({ max: options.max, maxArgs: options.maxArgs }) : { normal: false };
1029
- this.arity = guess.proper && guess.arity || 0;
1030
- this.proper = guess.proper ?? false;
1031
- this.terminal = options.terminal ?? this.proper;
1032
- this.canonical = guess.expr;
1033
- 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);
1034
1127
  }
1128
+ /**
1129
+ * @property {boolean} [outdated] - whether the alias is outdated
1130
+ * and should be replaced with its definition when encountered.
1131
+ * @property {boolean} [terminal] - whether the alias should behave like a standalone term
1132
+ * // TODO better name?
1133
+ * @property {boolean} [proper] - whether the alias is a proper combinator (i.e. contains no free variables or constants)
1134
+ * @property {number} [arity] - the number of arguments the alias waits for before expanding
1135
+ * @property {Expr} [canonical] - equivalent lambda term.
1136
+ */
1035
1137
  weight() {
1036
1138
  return this.terminal ? 1 : this.impl.weight();
1037
1139
  }
1038
- traverse(change) {
1039
- return change(this) ?? this.impl.traverse(change);
1140
+ _traverse_descend(options, change) {
1141
+ return this.impl._traverse_redo(options, change);
1040
1142
  }
1041
1143
  any(predicate) {
1042
1144
  return predicate(this) || this.impl.any(predicate);
1043
1145
  }
1044
1146
  _fold(initial, combine) {
1045
- const [value = initial, action = "descend"] = unwrap(combine(initial, this));
1046
- if (action === "prune")
1147
+ const [value = initial, action] = unwrap(combine(initial, this));
1148
+ if (action === control.prune)
1047
1149
  return value;
1048
- if (action === "stop")
1150
+ if (action === control.stop)
1049
1151
  return control.stop(value);
1050
1152
  const [iValue, iAction] = unwrap(this.impl._fold(value, combine));
1051
- if (iAction === "stop")
1153
+ if (iAction === control.stop)
1052
1154
  return control.stop(iValue);
1053
1155
  return iValue ?? value;
1054
1156
  }
@@ -1057,12 +1159,10 @@ var require_expr = __commonJS({
1057
1159
  return replace;
1058
1160
  return this.impl.subst(search, replace);
1059
1161
  }
1060
- _infer(options, preArgs = [], steps = 0) {
1061
- return this.impl._infer(options, preArgs, steps);
1062
- }
1162
+ // DO NOT REMOVE TYPE or tsc chokes with
1163
+ // TS2527: The inferred type of 'Alias' references an inaccessible 'this' type.
1063
1164
  /**
1064
- *
1065
- * @return {{expr: Expr, steps: number}}
1165
+ * @return {{expr: Expr, steps: number, changed: boolean}}
1066
1166
  */
1067
1167
  step() {
1068
1168
  if (this.arity > 0)
@@ -1074,9 +1174,6 @@ var require_expr = __commonJS({
1074
1174
  return null;
1075
1175
  return other.diff(this.impl, !swap);
1076
1176
  }
1077
- _rski(options) {
1078
- return this.impl._rski(options);
1079
- }
1080
1177
  _braced(first) {
1081
1178
  return this.outdated ? this.impl._braced(first) : false;
1082
1179
  }
@@ -1085,6 +1182,9 @@ var require_expr = __commonJS({
1085
1182
  return outdated ? this.impl._format(options, nargs) : super._format(options, nargs);
1086
1183
  }
1087
1184
  };
1185
+ function addNative(name, impl, opt) {
1186
+ native[name] = new Native(name, impl, opt);
1187
+ }
1088
1188
  addNative("I", (x) => x);
1089
1189
  addNative("K", (x) => (_) => x);
1090
1190
  addNative("S", (x) => (y) => (z) => x.apply(z, y.apply(z)));
@@ -1098,6 +1198,11 @@ var require_expr = __commonJS({
1098
1198
  note: "Increase a Church numeral argument by 1, otherwise n => f => x => f(n f x)"
1099
1199
  }
1100
1200
  );
1201
+ function firstVar(expr) {
1202
+ while (expr instanceof App)
1203
+ expr = expr.fun;
1204
+ return expr instanceof FreeVar;
1205
+ }
1101
1206
  function maybeLambda(args, expr, caps = {}) {
1102
1207
  const count = new Array(args.length).fill(0);
1103
1208
  let proper = true;
@@ -1121,8 +1226,10 @@ var require_expr = __commonJS({
1121
1226
  dup.add(i);
1122
1227
  }
1123
1228
  return {
1229
+ normal: true,
1230
+ steps: caps.steps,
1124
1231
  expr: args.length ? new Lambda(args, expr) : expr,
1125
- ...caps.synth ? {} : { arity: args.length },
1232
+ arity: args.length,
1126
1233
  ...skip.size ? { skip } : {},
1127
1234
  ...dup.size ? { dup } : {},
1128
1235
  duplicate: !!dup.size || caps.duplicate || false,
@@ -1133,43 +1240,6 @@ var require_expr = __commonJS({
1133
1240
  function nthvar(n) {
1134
1241
  return new FreeVar("abcdefgh"[n] ?? "x" + n);
1135
1242
  }
1136
- function* simplifyLambda(expr, options = {}, state = { steps: 0 }) {
1137
- yield { expr, steps: state.steps, comment: "(self)" };
1138
- if (expr.freeOnly())
1139
- return;
1140
- let maxWeight = expr.weight();
1141
- if (expr instanceof Lambda) {
1142
- for (const term of simplifyLambda(expr.impl, options, state)) {
1143
- const candidate = new Lambda(expr.arg, term.expr);
1144
- if (candidate.weight() < maxWeight) {
1145
- maxWeight = candidate.weight();
1146
- yield { expr: candidate, steps: state.steps, comment: "(lambda)" + term.comment };
1147
- }
1148
- }
1149
- }
1150
- if (expr instanceof App) {
1151
- let { fun, arg } = expr;
1152
- for (const term of simplifyLambda(fun, options, state)) {
1153
- const candidate = term.expr.apply(arg);
1154
- if (candidate.weight() < maxWeight) {
1155
- maxWeight = candidate.weight();
1156
- fun = term.expr;
1157
- yield { expr: candidate, steps: state.steps, comment: "(fun)" + term.comment };
1158
- }
1159
- }
1160
- for (const term of simplifyLambda(arg, options, state)) {
1161
- const candidate = fun.apply(term.expr);
1162
- if (candidate.weight() < maxWeight) {
1163
- maxWeight = candidate.weight();
1164
- yield { expr: candidate, steps: state.steps, comment: "(arg)" + term.comment };
1165
- }
1166
- }
1167
- }
1168
- const canon = expr.infer({ max: options.max, maxArgs: options.maxArgs });
1169
- state.steps += canon.steps;
1170
- if (canon.expr && canon.expr.weight() < maxWeight)
1171
- yield { expr: canon.expr, steps: state.steps, comment: "(canonical)" };
1172
- }
1173
1243
  function toposort(list, env) {
1174
1244
  if (list instanceof Expr)
1175
1245
  list = [list];
@@ -1273,7 +1343,7 @@ var require_parser = __commonJS({
1273
1343
  "->",
1274
1344
  "\\+"
1275
1345
  );
1276
- var SKI2 = class _SKI {
1346
+ var SKI2 = class {
1277
1347
  /**
1278
1348
  *
1279
1349
  * @param {{
@@ -1319,24 +1389,31 @@ var require_parser = __commonJS({
1319
1389
  *
1320
1390
  * @param {Alias|String} term
1321
1391
  * @param {String|Expr|function(Expr):Partial} [impl]
1322
- * @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
1323
1397
  * @return {SKI} chainable
1324
1398
  */
1325
- add(term, impl, note) {
1399
+ add(term, impl, options) {
1326
1400
  term = this._named(term, impl);
1327
- if (this.annotate && note === void 0) {
1328
- const guess = term.infer();
1329
- if (guess.expr)
1330
- note = guess.expr.format({ terse: true, html: true, lambda: ["", " &mapsto; ", ""] });
1331
- }
1332
- if (note !== void 0)
1333
- term.note = note;
1401
+ if (typeof options === "string")
1402
+ options = { note: options, canonize: false };
1403
+ term._setup({ canonize: this.annotate, ...options });
1334
1404
  if (this.known[term.name])
1335
1405
  this.known[term.name].outdated = true;
1336
1406
  this.known[term.name] = term;
1337
1407
  this.allow.add(term.name);
1338
1408
  return this;
1339
1409
  }
1410
+ /**
1411
+ * @desc Internal helper for add() that creates an Alias or Native term from the given arguments.
1412
+ * @param {Alias|string} term
1413
+ * @param {string|Expr|function(Expr):Partial} impl
1414
+ * @returns {Native|Alias}
1415
+ * @private
1416
+ */
1340
1417
  _named(term, impl) {
1341
1418
  if (term instanceof Alias)
1342
1419
  return new Alias(term.name, term.impl, { canonize: true });
@@ -1352,6 +1429,16 @@ var require_parser = __commonJS({
1352
1429
  return new Native(term, impl);
1353
1430
  throw new Error("add(): impl must be an Expr, a string, or a function with a signature Expr => ... => Expr");
1354
1431
  }
1432
+ /**
1433
+ * @desc Declare a new term if it is not known, otherwise just allow it.
1434
+ * Currently only used by quests.
1435
+ * Use with caution, this function may change its signature, behavior, or even be removed in the future.
1436
+ *
1437
+ * @experimental
1438
+ * @param {string|Alias} name
1439
+ * @param {string|Expr|function(Expr):Partial} impl
1440
+ * @returns {SKI}
1441
+ */
1355
1442
  maybeAdd(name, impl) {
1356
1443
  if (this.known[name])
1357
1444
  this.allow.add(name);
@@ -1363,7 +1450,7 @@ var require_parser = __commonJS({
1363
1450
  * @desc Declare and remove multiple terms at once
1364
1451
  * term=impl adds term
1365
1452
  * term= removes term
1366
- * @param {string[]]} list
1453
+ * @param {string[]} list
1367
1454
  * @return {SKI} chainable
1368
1455
  */
1369
1456
  bulkAdd(list) {
@@ -1484,11 +1571,11 @@ var require_parser = __commonJS({
1484
1571
  return out;
1485
1572
  }
1486
1573
  /**
1487
- *
1574
+ * @template T
1488
1575
  * @param {string} source
1489
1576
  * @param {Object} [options]
1490
1577
  * @param {{[keys: string]: Expr}} [options.env]
1491
- * @param {any} [options.scope]
1578
+ * @param {T} [options.scope]
1492
1579
  * @param {boolean} [options.numbers]
1493
1580
  * @param {boolean} [options.lambdas]
1494
1581
  * @param {string} [options.allow]
@@ -1505,7 +1592,7 @@ var require_parser = __commonJS({
1505
1592
  expr.outdated = true;
1506
1593
  const def = item.match(/^([A-Z]|[a-z][a-z_0-9]*)\s*=(.*)$/s);
1507
1594
  if (def && def[2] === "")
1508
- expr = new FreeVar(def[1], options.scope ?? _SKI);
1595
+ expr = new FreeVar(def[1], options.scope ?? FreeVar.global);
1509
1596
  else
1510
1597
  expr = this.parseLine(item, jar, options);
1511
1598
  if (def) {
@@ -1524,12 +1611,14 @@ var require_parser = __commonJS({
1524
1611
  return expr;
1525
1612
  }
1526
1613
  /**
1527
- *
1614
+ * @desc Parse a single line of source code, without splitting it into declarations.
1615
+ * Internal, always use parse() instead.
1616
+ * @template T
1528
1617
  * @param {String} source S(KI)I
1529
1618
  * @param {{[keys: string]: Expr}} env
1530
1619
  * @param {Object} [options]
1531
1620
  * @param {{[keys: string]: Expr}} [options.env] - unused, see 'env' argument
1532
- * @param {any} [options.scope]
1621
+ * @param {T} [options.scope]
1533
1622
  * @param {boolean} [options.numbers]
1534
1623
  * @param {boolean} [options.lambdas]
1535
1624
  * @param {string} [options.allow]
@@ -1548,7 +1637,7 @@ var require_parser = __commonJS({
1548
1637
  const tokens = combChars.split(source);
1549
1638
  const empty = new Empty();
1550
1639
  const stack = [empty];
1551
- const context = options.scope || _SKI;
1640
+ const context = options.scope || FreeVar.global;
1552
1641
  for (const c of tokens) {
1553
1642
  if (c === "(")
1554
1643
  stack.push(empty);
@@ -1593,12 +1682,12 @@ var require_parser = __commonJS({
1593
1682
  };
1594
1683
  }
1595
1684
  };
1596
- SKI2.vars = function(context = {}) {
1685
+ SKI2.vars = function(scope = {}) {
1597
1686
  const cache = {};
1598
1687
  return new Proxy({}, {
1599
1688
  get: (target, name) => {
1600
1689
  if (!(name in cache))
1601
- cache[name] = new FreeVar(name, context);
1690
+ cache[name] = new FreeVar(name, scope);
1602
1691
  return cache[name];
1603
1692
  }
1604
1693
  });
@@ -1620,40 +1709,23 @@ var require_quest = __commonJS({
1620
1709
  var { Expr, FreeVar, Alias, Lambda } = SKI2.classes;
1621
1710
  var Quest2 = class {
1622
1711
  /**
1623
- * @description A combinator problem with a set of test cases for the proposed solution.
1624
- * @param {{
1625
- * input: InputSpec | InputSpec[],
1626
- * cases: TestCase[],
1627
- *
1628
- * // the rest is optional
1629
-
1630
- * allow?: string,
1631
- * numbers?: boolean,
1632
- * env?: string[],
1633
- * engine?: SKI,
1634
- * engineFull?: SKI,
1635
- *
1636
- * // metadata, also any fields not listed here will go to quest.meta.???
1637
- * id?: string|number,
1638
- * name?: string,
1639
- * intro?: string|string[], // multiple strings will be concatenated with spaces
1640
- * }} options
1641
- *
1642
- * @example const quest = new Quest({
1643
- * input: 'identity',
1644
- * cases: [
1645
- * ['identity x', 'x'],
1646
- * ],
1647
- * allow: 'SK',
1648
- * intro: 'Find a combinator that behaves like the identity function.',
1649
- * });
1650
- * quest.check('S K K'); // { pass: true, details: [...], ... }
1651
- * quest.check('K S'); // { pass: false, details: [...], ... }
1652
- * quest.check('K x'); // fail! internal variable x is not equal to free variable x,
1653
- * // despite having the same name.
1654
- * quest.check('I'); // fail! I not in the allowed list.
1655
- */
1656
- constructor(options = {}) {
1712
+ * @description A combinator problem with a set of test cases for the proposed solution.
1713
+ * @param {QuestSpec} options
1714
+ * @example const quest = new Quest({
1715
+ * input: 'identity',
1716
+ * cases: [
1717
+ * ['identity x', 'x'],
1718
+ * ],
1719
+ * allow: 'SK',
1720
+ * intro: 'Find a combinator that behaves like the identity function.',
1721
+ * });
1722
+ * quest.check('S K K'); // { pass: true, details: [...], ... }
1723
+ * quest.check('K S'); // { pass: false, details: [...], ... }
1724
+ * quest.check('K x'); // fail! internal variable x is not equal to free variable x,
1725
+ * // despite having the same name.
1726
+ * quest.check('I'); // fail! I not in the allowed list.
1727
+ */
1728
+ constructor(options) {
1657
1729
  const { input, cases, allow, numbers, lambdas, engine, engineFull, ...meta } = options;
1658
1730
  const env = options.env ?? options.vars;
1659
1731
  this.engine = engine ?? new SKI2();
@@ -1746,7 +1818,11 @@ var require_quest = __commonJS({
1746
1818
  numbers: spec.numbers ?? this.restrict.numbers,
1747
1819
  lambdas: spec.lambdas ?? this.restrict.lambdas
1748
1820
  });
1749
- weight += impl.weight();
1821
+ const arsenal = { ...this.engine.getTerms(), ...jar };
1822
+ weight += impl.fold(0, (a, e) => {
1823
+ if (e instanceof SKI2.classes.Named && arsenal[e.name] === e)
1824
+ return SKI2.control.prune(a + 1);
1825
+ });
1750
1826
  const expr = impl instanceof FreeVar ? impl : new Alias(spec.fancy ?? spec.name, impl, { terminal: true, canonize: false });
1751
1827
  jar[spec.name] = expr;
1752
1828
  prepared.push(expr);
@@ -1779,6 +1855,62 @@ var require_quest = __commonJS({
1779
1855
  return { pass: false, details: [], exception: e, steps: 0, input };
1780
1856
  }
1781
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
+ }
1782
1914
  /**
1783
1915
  *
1784
1916
  * @return {TestCase[]}
@@ -1819,10 +1951,10 @@ var require_quest = __commonJS({
1819
1951
  /**
1820
1952
  * @param {FreeVar[]} input
1821
1953
  * @param {{
1822
- * max: number?,
1823
- * note: string?,
1824
- * env: {string: Expr}?,
1825
- * engine: SKI?
1954
+ * max?: number,
1955
+ * note?: string,
1956
+ * env?: {string: Expr},
1957
+ * engine?: SKI
1826
1958
  * }} options
1827
1959
  * @param {[e1: string, e2: string]} terms
1828
1960
  */
@@ -1929,11 +2061,67 @@ var require_quest = __commonJS({
1929
2061
  return expr;
1930
2062
  }
1931
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
+ };
1932
2086
  function list2str(str) {
1933
2087
  if (str === void 0 || typeof str === "string")
1934
2088
  return str;
1935
2089
  return Array.isArray(str) ? str.join(" ") : "" + str;
1936
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;
1937
2125
  module2.exports = { Quest: Quest2 };
1938
2126
  }
1939
2127
  });
@@ -1943,6 +2131,7 @@ var require_extras = __commonJS({
1943
2131
  "src/extras.js"(exports2, module2) {
1944
2132
  "use strict";
1945
2133
  var { Expr, Alias, FreeVar } = require_expr();
2134
+ var { Quest: Quest2 } = require_quest();
1946
2135
  function search(seed, options, predicate) {
1947
2136
  const {
1948
2137
  depth = 16,
@@ -2008,6 +2197,10 @@ var require_extras = __commonJS({
2008
2197
  function deepFormat(obj, options = {}) {
2009
2198
  if (obj instanceof Expr)
2010
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";
2011
2204
  if (Array.isArray(obj))
2012
2205
  return obj.map(deepFormat);
2013
2206
  if (typeof obj !== "object" || obj === null || obj.constructor !== Object)