@mark1russell7/coeval 0.6.1 → 0.6.3

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.1",
4
+ "version": "0.6.3",
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",
@@ -36,6 +36,7 @@
36
36
  "devDependencies": {
37
37
  "@mark1russell7/cue": "github:mark1russell7/cue",
38
38
  "@types/node": "^26.6.4",
39
+ "esbuild": "^0.28.2",
39
40
  "typescript": "^7.0.2",
40
41
  "vitest": "^5.0.3"
41
42
  },
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,6 +21,12 @@ 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
+ }
29
+
24
30
  /** The options of `agree`. */
25
31
  export interface AgreeOptions {
26
32
  /** The type of a bare root value. */
@@ -34,9 +40,15 @@ export interface AgreeOptions {
34
40
  *
35
41
  * @param doc - The document.
36
42
  * @param context - The context.
43
+ * @param input - The mock of the host, when `agree` has one.
37
44
  * @returns The result, or the result with the effects.
38
45
  */
39
- readonly other?: (doc: unknown, context: Context) => Result | TierResult;
46
+ readonly other?: (doc: unknown, context: Context, input: TierInput) => Result | TierResult;
47
+ /**
48
+ * The mock of the host (RUN.MOCK, AGREE.MOCK). Each tier gets it, thus the tiers agree on documents with effects. A
49
+ * mock must give the same answer to the same effect, because each tier asks it again.
50
+ */
51
+ readonly perform?: Perform;
40
52
  /**
41
53
  * This function compares two values that the rules of `sameValue` do not know, for example two dates.
42
54
  *
@@ -80,8 +92,9 @@ export type Agreement =
80
92
  * errors have the same code and place. Each other object is the same only as itself, unless the rule of the caller
81
93
  * tells otherwise.
82
94
  *
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.
95
+ * `docEquals` and `sameResult` use the same walk (D20). `docEquals` has no rules for closures, for `Held` values and
96
+ * for a rule of the caller. `sameResult` is for an early cutoff, thus a closure is the same there only as itself, and
97
+ * two errors also need the same reason.
85
98
  *
86
99
  * @param a - The first value.
87
100
  * @param b - The second value.
@@ -89,50 +102,17 @@ export type Agreement =
89
102
  * @returns `true` when the values are the same.
90
103
  */
91
104
  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;
105
+ return equalWith(a, b, equal === undefined ? sameInTiers : (x, y) => equal(x, y) ?? sameInTiers(x, y));
106
+ }
107
+
108
+ /** The rules of `sameValue` for the values that two tiers make again: closures and held errors. */
109
+ function sameInTiers(x: unknown, y: unknown): boolean | undefined {
110
+ if (isClosure(x) || isClosure(y)) return isClosure(x) && isClosure(y) && docEquals(closureParams(x), closureParams(y));
111
+ if (x instanceof Held || y instanceof Held) return x instanceof Held && y instanceof Held && errorDiffers(x.error, y.error, false) === undefined;
112
+ return undefined;
133
113
  }
134
114
 
135
- const CLOSURE = Symbol.for("coeval.closure");
115
+ const CLOSURE = /* @__PURE__ */ Symbol.for("coeval.closure");
136
116
 
137
117
  function closureParams(fn: object): unknown {
138
118
  return (fn as { readonly [CLOSURE]?: { readonly params: readonly string[] } })[CLOSURE]?.params;
@@ -165,7 +145,10 @@ function resultDiffers(a: Result, b: Result, options: AgreeOptions): string | un
165
145
  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
146
  }
167
147
 
168
- /** This function gives the difference of two lists of effects, or `undefined` when they agree (EFFECT.ORDER). */
148
+ /**
149
+ * This function gives the difference of two lists of effects, or `undefined` when they agree (EFFECT.ORDER). When both
150
+ * tiers record the address or the outcome of an effect, these must agree too (AGREE.MOCK).
151
+ */
169
152
  function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: AgreeOptions): string | undefined {
170
153
  if (a.length !== b.length) return `The plan causes ${a.length} effects, and the tier causes ${b.length}.`;
171
154
  for (let index = 0; index < a.length; index += 1) {
@@ -174,6 +157,11 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
174
157
  if (x.kind !== y.kind || !docEquals(x.place as readonly Key[], y.place as readonly Key[]) || !sameValue(x.payload, y.payload, options.equal)) {
175
158
  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
159
  }
160
+ if (x.address !== undefined && y.address !== undefined && x.address !== y.address) {
161
+ return `The effect ${index} has the address ${x.address} in the plan, and ${y.address} in the tier.`;
162
+ }
163
+ const why = x.outcome !== undefined && y.outcome !== undefined ? resultDiffers(x.outcome, y.outcome, options) : undefined;
164
+ if (why !== undefined) return `The outcome of the effect ${index} differs. ${why}`;
177
165
  }
178
166
  return undefined;
179
167
  }
@@ -182,21 +170,22 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
182
170
  * This function evaluates a document with a plan and with other tiers, and compares the results (PLAN.AGREES). Two
183
171
  * results agree when they have the same value by `sameValue`, or errors with the same code, and the same place when
184
172
  * 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`.
173
+ * own tier with the option `other`. Each tier gets the mock of the option `perform` (AGREE.MOCK).
186
174
  *
187
175
  * @param machine - The world and the code of the evaluators.
188
176
  * @param doc - The document.
189
177
  * @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.
178
+ * @param options - The tiers, the definitions for the reference, the rules of the comparison, the second tier and the mock.
191
179
  * @returns The result of the plan, or the first tier that disagrees and the reason.
192
180
  */
193
181
  export function agree(machine: Machine, doc: unknown, context: Context = makeContext(), options: AgreeOptions = {}): Agreement {
194
182
  const declared = options.declaredType === undefined ? {} : { declaredType: options.declaredType };
183
+ const mock = options.perform === undefined ? {} : { perform: options.perform };
195
184
  const observe = options.observe;
196
185
  observe?.();
197
186
  const plan = prepare(machine, doc, declared);
198
187
  const effects: Effect[] = [];
199
- const result = plan.evaluate(context, { effects });
188
+ const result = plan.evaluate(context, { effects, ...mock });
200
189
  const saw = observe?.();
201
190
 
202
191
  const differs = (other: Result, otherEffects: readonly Effect[] | undefined): string | undefined => {
@@ -210,13 +199,13 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
210
199
  let other: Result;
211
200
  let otherEffects: Effect[] = [];
212
201
  if (tier === "reference") {
213
- other = reference(machine, doc, context, { ...declared, effects: otherEffects, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
202
+ other = reference(machine, doc, context, { ...declared, ...mock, effects: otherEffects, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
214
203
  } else if (tier === "traced") {
215
- const traced = evaluate(machine, doc, context, options.declaredType);
204
+ const traced = evaluate(machine, doc, context, { ...declared, ...mock });
216
205
  other = traced.result;
217
206
  otherEffects = [...traced.effects];
218
207
  } else {
219
- other = plan.evaluate(context, { effects: otherEffects });
208
+ other = plan.evaluate(context, { effects: otherEffects, ...mock });
220
209
  }
221
210
  const why = differs(other, otherEffects);
222
211
  if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
@@ -225,7 +214,7 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
225
214
  if (options.other !== undefined) {
226
215
  let given: Result | TierResult;
227
216
  try {
228
- given = options.other(doc, context);
217
+ given = options.other(doc, context, mock);
229
218
  } catch (error) {
230
219
  given = thrown(error, "the tier");
231
220
  }
package/src/deref.ts CHANGED
@@ -6,7 +6,7 @@ import type { ErrorCode } from "./errors.ts";
6
6
  * The brand of a host thing: an object of the host that answers the steps of a path itself. The brand is a
7
7
  * global symbol, thus two copies of the kernel in one program know the things of each other.
8
8
  */
9
- export const THING: unique symbol = Symbol.for("coeval.thing") as never;
9
+ export const THING: unique symbol = /* @__PURE__ */ Symbol.for("coeval.thing") as never;
10
10
 
11
11
  /**
12
12
  * A host thing: an object that answers the steps of a path itself, for example a node of a live tree. It is the
@@ -17,12 +17,22 @@ export interface HostThing {
17
17
  /** A short text for a reader, for example `node card`. */
18
18
  readonly label: string;
19
19
  /**
20
- * This function answers one step.
20
+ * This function answers one step. A field that holds `undefined` also gives `undefined`. The function `has` tells
21
+ * the two cases apart (DEREF.HOST-UNDEFINED).
21
22
  *
22
23
  * @param key - The key of the step.
23
24
  * @returns What the step reaches, or `undefined` when it reaches nothing.
24
25
  */
25
26
  deref(key: Key): Thing | undefined;
27
+ /**
28
+ * This optional function tells if the thing has a field for a key. The kernel asks it only when `deref` gives
29
+ * `undefined`. Then `true` tells that the field holds `undefined`. A thing without `has` reads `undefined` as a
30
+ * missing field.
31
+ *
32
+ * @param key - The key of the step.
33
+ * @returns `true` when the thing has the field.
34
+ */
35
+ has?(key: Key): boolean;
26
36
  /**
27
37
  * This function gives the thing as a value, when a reference ends on it.
28
38
  *
@@ -34,7 +44,7 @@ export interface HostThing {
34
44
  * path without one call for each step.
35
45
  *
36
46
  * @param keys - The keys.
37
- * @returns What the keys reach, or `undefined` when they reach nothing.
47
+ * @returns What the keys reach, or `undefined` when they reach nothing. `{ thing: undefined }` tells that the keys reach a field that holds `undefined`.
38
48
  */
39
49
  resolve?(keys: readonly Key[]): { readonly thing: Thing } | undefined;
40
50
  }
@@ -183,7 +193,8 @@ function plainStep(thing: unknown, key: Key): { readonly found: true; readonly v
183
193
  export function derefStep(nav: Navigator, thing: Thing, key: Key): { readonly thing: Thing; readonly by: HopBy } | undefined {
184
194
  if (isHostThing(thing)) {
185
195
  const reached = thing.deref(key);
186
- return reached === undefined ? undefined : { thing: reached, by: "own" };
196
+ // DEREF.HOST-UNDEFINED: `has` tells a field that holds undefined from a missing field.
197
+ return reached !== undefined || thing.has?.(key) === true ? { thing: reached, by: "own" } : undefined;
187
198
  }
188
199
  if (nav.plain !== true && isEnvelope(thing)) {
189
200
  const envelope = thing as JsonRecord & Envelope;
@@ -220,7 +231,7 @@ export function isRef(thing: unknown): thing is Envelope & { readonly value: rea
220
231
  return path !== undefined && path.length === 1 && path[0] === "ref";
221
232
  }
222
233
 
223
- const worlds = new WeakMap<World, Json>();
234
+ const worlds = /* @__PURE__ */ new WeakMap<World, Json>();
224
235
 
225
236
  /**
226
237
  * This function makes the thing of the root `world`: the entries of the world as JSON. An alias is a reference
@@ -256,7 +267,7 @@ export function context(parts: Partial<Context> = {}): Context {
256
267
  }
257
268
 
258
269
  const MAX_COLLAPSE = 64;
259
- const NO_HOPS: readonly Hop[] = Object.freeze([]);
270
+ const NO_HOPS: readonly Hop[] = /* @__PURE__ */ Object.freeze([]);
260
271
 
261
272
  /** A value that can be a reference document: a record with the type `ref`. The full check comes after. */
262
273
  function mayBeRef(thing: unknown): boolean {
package/src/doc.ts CHANGED
@@ -230,20 +230,41 @@ export function fromJson(value: unknown): Envelope | undefined {
230
230
  * with this function does not keep a stale host value.
231
231
  *
232
232
  * The function does not use the stack for the depth, thus a deep document cannot overflow it. A cycle of lists or
233
- * records does not make it loop. `sameValue` of `agree` uses the same rules, and it adds rules for closures and for
234
- * a rule of the caller.
233
+ * records does not make it loop. `sameValue` of `agree` and `sameResult` use the same walk, and each adds its rules
234
+ * for other values (D20).
235
235
  *
236
236
  * @param a - The first value.
237
237
  * @param b - The second value.
238
238
  * @returns `true` when the values are equal.
239
239
  */
240
240
  export function docEquals(a: unknown, b: unknown): boolean {
241
+ return equalWith(a, b, undefined);
242
+ }
243
+
244
+ /** A rule for two values that are not the same by `Object.is`: `true`, `false`, or `undefined` for the next rule. */
245
+ export type SameRule = (a: unknown, b: unknown) => boolean | undefined;
246
+
247
+ /**
248
+ * This function is the one walk of `docEquals`, `sameValue` and `sameResult` (D20). It compares two values by the
249
+ * rules of `docEquals`. A rule of the caller answers first for each pair that is not the same by `Object.is`.
250
+ *
251
+ * @param a - The first value.
252
+ * @param b - The second value.
253
+ * @param rule - The rule of the caller. Its `undefined` gives the rules of `docEquals`.
254
+ * @returns `true` when the values are equal.
255
+ */
256
+ export function equalWith(a: unknown, b: unknown, rule: SameRule | undefined): boolean {
241
257
  const stack: [unknown, unknown][] = [[a, b]];
242
258
  // The pairs of containers that the comparison already visits. It is made at the first pair of containers.
243
259
  let seen: Map<object, Set<object>> | undefined;
244
260
  while (stack.length > 0) {
245
261
  const [x, y] = stack.pop() as [unknown, unknown];
246
262
  if (Object.is(x, y)) continue;
263
+ const told = rule?.(x, y);
264
+ if (told !== undefined) {
265
+ if (told) continue;
266
+ return false;
267
+ }
247
268
  const list = Array.isArray(x);
248
269
  if (list !== Array.isArray(y)) return false;
249
270
  // An object that is not a list and not a plain record is a host value: Object.is above compared it.
package/src/errors.ts CHANGED
@@ -16,7 +16,8 @@ export type KernelCode =
16
16
  | "#CAP!"
17
17
  | "#CALC!"
18
18
  | "#DEPTH!"
19
- | "#ABORT";
19
+ | "#ABORT"
20
+ | "#REPLAY!";
20
21
 
21
22
  /** The code of an error value. A language can add its own codes. Each code starts with `#`. */
22
23
  export type ErrorCode = KernelCode | `#${string}`;