@mark1russell7/coeval 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/coeval",
4
- "version": "0.6.2",
4
+ "version": "0.7.0",
5
5
  "description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
6
6
  "type": "module",
7
7
  "main": "./build/index.js",
package/src/agree.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { type Key, docEquals, isRecord } from "./doc.ts";
1
+ import { type Key, docEquals, equalWith } from "./doc.ts";
2
2
  import type { Context } from "./deref.ts";
3
3
  import { type EvalError, type Result, isStackOverflow, thrown } from "./errors.ts";
4
- import { type Effect, type Machine, evaluate, makeContext, prepare } from "./evaluate.ts";
4
+ import { type Effect, type Machine, type Perform, evaluate, makeContext, prepare } from "./evaluate.ts";
5
5
  import { Held, isClosure } from "./formula.ts";
6
6
  import { type Definitions, reference } from "./reference.ts";
7
7
 
@@ -21,10 +21,24 @@ export interface TierResult {
21
21
  readonly effects?: readonly Effect[];
22
22
  }
23
23
 
24
+ /** What `agree` gives the second tier of a language beside the document and the context. */
25
+ export interface TierInput {
26
+ /** The mock of the host, when `agree` has one (AGREE.MOCK). The tier answers each effect with it. */
27
+ readonly perform?: Perform;
28
+ /** The verb of `agree`, when its options name one (VERB.AGREE). A tier that does not serve the verb fails. */
29
+ readonly verb?: string;
30
+ }
31
+
24
32
  /** The options of `agree`. */
25
33
  export interface AgreeOptions {
26
34
  /** The type of a bare root value. */
27
35
  readonly declaredType?: string;
36
+ /**
37
+ * The verb of each tier, as in `prepare`. The default is `eval`. The plan, the reference, the traced evaluation and a
38
+ * second evaluation each ask the ladder for this verb (VERB.AGREE). The second tier of a language gets it in its
39
+ * input.
40
+ */
41
+ readonly verb?: string;
28
42
  /** The definitional code of the compiled evaluators of the machine, for the reference. */
29
43
  readonly definitions?: Definitions;
30
44
  /** The tiers to compare with the plan. The default is the reference, the traced evaluation and a second evaluation. */
@@ -34,9 +48,15 @@ export interface AgreeOptions {
34
48
  *
35
49
  * @param doc - The document.
36
50
  * @param context - The context.
51
+ * @param input - The mock of the host and the verb, when `agree` has them.
37
52
  * @returns The result, or the result with the effects.
38
53
  */
39
- readonly other?: (doc: unknown, context: Context) => Result | TierResult;
54
+ readonly other?: (doc: unknown, context: Context, input: TierInput) => Result | TierResult;
55
+ /**
56
+ * The mock of the host (RUN.MOCK, AGREE.MOCK). Each tier gets it, thus the tiers agree on documents with effects. A
57
+ * mock must give the same answer to the same effect, because each tier asks it again.
58
+ */
59
+ readonly perform?: Perform;
40
60
  /**
41
61
  * This function compares two values that the rules of `sameValue` do not know, for example two dates.
42
62
  *
@@ -80,8 +100,9 @@ export type Agreement =
80
100
  * errors have the same code and place. Each other object is the same only as itself, unless the rule of the caller
81
101
  * tells otherwise.
82
102
  *
83
- * `docEquals` uses the same rules for lists, plain records and the other objects. It has no rules for closures, for
84
- * `Held` values and for a rule of the caller. Thus two closures are equal there only when they are the same function.
103
+ * `docEquals` and `sameResult` use the same walk (D20). `docEquals` has no rules for closures, for `Held` values and
104
+ * for a rule of the caller. `sameResult` is for an early cutoff, thus a closure is the same there only as itself, and
105
+ * two errors also need the same reason.
85
106
  *
86
107
  * @param a - The first value.
87
108
  * @param b - The second value.
@@ -89,47 +110,14 @@ export type Agreement =
89
110
  * @returns `true` when the values are the same.
90
111
  */
91
112
  export function sameValue(a: unknown, b: unknown, equal?: (a: unknown, b: unknown) => boolean | undefined): boolean {
92
- const stack: (readonly [unknown, unknown])[] = [[a, b]];
93
- const seen = new Map<object, Set<object>>();
94
- while (stack.length > 0) {
95
- const [x, y] = stack.pop() as readonly [unknown, unknown];
96
- if (Object.is(x, y)) continue;
97
- const told = equal?.(x, y);
98
- if (told !== undefined) {
99
- if (told) continue;
100
- return false;
101
- }
102
- if (typeof x !== "object" && typeof x !== "function") return false;
103
- if (typeof y !== "object" && typeof y !== "function") return false;
104
- if (x === null || y === null) return false;
105
- // A pair that the comparison already visits is the same, unless another pair shows a difference.
106
- const pairs = seen.get(x) ?? new Set<object>();
107
- if (pairs.has(y)) continue;
108
- pairs.add(y);
109
- seen.set(x, pairs);
110
- if (isClosure(x) || isClosure(y)) {
111
- if (!(isClosure(x) && isClosure(y))) return false;
112
- stack.push([closureParams(x), closureParams(y)]);
113
- continue;
114
- }
115
- if (x instanceof Held || y instanceof Held) {
116
- if (!(x instanceof Held && y instanceof Held) || errorDiffers(x.error, y.error, false) !== undefined) return false;
117
- continue;
118
- }
119
- if (Array.isArray(x) || Array.isArray(y)) {
120
- if (!Array.isArray(x) || !Array.isArray(y) || x.length !== y.length) return false;
121
- for (let index = 0; index < x.length; index += 1) stack.push([x[index], y[index]]);
122
- continue;
123
- }
124
- if (!isRecord(x) || !isRecord(y)) return false;
125
- const keys = Object.keys(x);
126
- if (keys.length !== Object.keys(y).length) return false;
127
- for (const key of keys) {
128
- if (!Object.hasOwn(y, key)) return false;
129
- stack.push([x[key], y[key]]);
130
- }
131
- }
132
- return true;
113
+ return equalWith(a, b, equal === undefined ? sameInTiers : (x, y) => equal(x, y) ?? sameInTiers(x, y));
114
+ }
115
+
116
+ /** The rules of `sameValue` for the values that two tiers make again: closures and held errors. */
117
+ function sameInTiers(x: unknown, y: unknown): boolean | undefined {
118
+ if (isClosure(x) || isClosure(y)) return isClosure(x) && isClosure(y) && docEquals(closureParams(x), closureParams(y));
119
+ if (x instanceof Held || y instanceof Held) return x instanceof Held && y instanceof Held && errorDiffers(x.error, y.error, false) === undefined;
120
+ return undefined;
133
121
  }
134
122
 
135
123
  const CLOSURE = /* @__PURE__ */ Symbol.for("coeval.closure");
@@ -165,7 +153,10 @@ function resultDiffers(a: Result, b: Result, options: AgreeOptions): string | un
165
153
  return a.ok ? `The plan gives a value, and the tier gives ${codeAndPlace((b as { readonly error: EvalError }).error)}.` : `The plan gives ${codeAndPlace(a.error)}, and the tier gives a value.`;
166
154
  }
167
155
 
168
- /** This function gives the difference of two lists of effects, or `undefined` when they agree (EFFECT.ORDER). */
156
+ /**
157
+ * This function gives the difference of two lists of effects, or `undefined` when they agree (EFFECT.ORDER). When both
158
+ * tiers record the address or the outcome of an effect, these must agree too (AGREE.MOCK).
159
+ */
169
160
  function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: AgreeOptions): string | undefined {
170
161
  if (a.length !== b.length) return `The plan causes ${a.length} effects, and the tier causes ${b.length}.`;
171
162
  for (let index = 0; index < a.length; index += 1) {
@@ -174,6 +165,11 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
174
165
  if (x.kind !== y.kind || !docEquals(x.place as readonly Key[], y.place as readonly Key[]) || !sameValue(x.payload, y.payload, options.equal)) {
175
166
  return `The effect ${index} differs: ${x.kind} at [${x.place.join(", ")}] in the plan, and ${y.kind} at [${y.place.join(", ")}] in the tier.`;
176
167
  }
168
+ if (x.address !== undefined && y.address !== undefined && x.address !== y.address) {
169
+ return `The effect ${index} has the address ${x.address} in the plan, and ${y.address} in the tier.`;
170
+ }
171
+ const why = x.outcome !== undefined && y.outcome !== undefined ? resultDiffers(x.outcome, y.outcome, options) : undefined;
172
+ if (why !== undefined) return `The outcome of the effect ${index} differs. ${why}`;
177
173
  }
178
174
  return undefined;
179
175
  }
@@ -182,21 +178,26 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
182
178
  * This function evaluates a document with a plan and with other tiers, and compares the results (PLAN.AGREES). Two
183
179
  * results agree when they have the same value by `sameValue`, or errors with the same code, and the same place when
184
180
  * both have one. The effects must have the same kinds, places and payloads, in the same order. A language gives its
185
- * own tier with the option `other`.
181
+ * own tier with the option `other`. Each tier gets the mock of the option `perform` (AGREE.MOCK).
186
182
  *
187
183
  * @param machine - The world and the code of the evaluators.
188
184
  * @param doc - The document.
189
185
  * @param context - The names and the capabilities at the root.
190
- * @param options - The tiers, the definitions for the reference, the rules of the comparison and the second tier.
186
+ * @param options - The tiers, the definitions for the reference, the rules of the comparison, the second tier and the mock.
191
187
  * @returns The result of the plan, or the first tier that disagrees and the reason.
192
188
  */
193
189
  export function agree(machine: Machine, doc: unknown, context: Context = makeContext(), options: AgreeOptions = {}): Agreement {
194
- const declared = options.declaredType === undefined ? {} : { declaredType: options.declaredType };
190
+ const declared = {
191
+ ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
192
+ ...(options.verb === undefined ? {} : { verb: options.verb }),
193
+ };
194
+ const mock = options.perform === undefined ? {} : { perform: options.perform };
195
+ const tierInput: TierInput = { ...mock, ...(options.verb === undefined ? {} : { verb: options.verb }) };
195
196
  const observe = options.observe;
196
197
  observe?.();
197
198
  const plan = prepare(machine, doc, declared);
198
199
  const effects: Effect[] = [];
199
- const result = plan.evaluate(context, { effects });
200
+ const result = plan.evaluate(context, { effects, ...mock });
200
201
  const saw = observe?.();
201
202
 
202
203
  const differs = (other: Result, otherEffects: readonly Effect[] | undefined): string | undefined => {
@@ -210,13 +211,13 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
210
211
  let other: Result;
211
212
  let otherEffects: Effect[] = [];
212
213
  if (tier === "reference") {
213
- other = reference(machine, doc, context, { ...declared, effects: otherEffects, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
214
+ other = reference(machine, doc, context, { ...declared, ...mock, effects: otherEffects, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
214
215
  } else if (tier === "traced") {
215
- const traced = evaluate(machine, doc, context, options.declaredType);
216
+ const traced = evaluate(machine, doc, context, { ...declared, ...mock });
216
217
  other = traced.result;
217
218
  otherEffects = [...traced.effects];
218
219
  } else {
219
- other = plan.evaluate(context, { effects: otherEffects });
220
+ other = plan.evaluate(context, { effects: otherEffects, ...mock });
220
221
  }
221
222
  const why = differs(other, otherEffects);
222
223
  if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
@@ -225,7 +226,7 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
225
226
  if (options.other !== undefined) {
226
227
  let given: Result | TierResult;
227
228
  try {
228
- given = options.other(doc, context);
229
+ given = options.other(doc, context, tierInput);
229
230
  } catch (error) {
230
231
  given = thrown(error, "the tier");
231
232
  }