@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/README.md +6 -4
- package/build/index.d.ts +189 -22
- package/build/index.d.ts.map +1 -1
- package/build/index.js +390 -105
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/agree.ts +44 -55
- package/src/deref.ts +17 -6
- package/src/doc.ts +23 -2
- package/src/errors.ts +2 -1
- package/src/evaluate.ts +198 -27
- package/src/formula.ts +2 -2
- package/src/index.ts +6 -1
- package/src/merge.ts +2 -2
- package/src/parts.ts +1 -1
- package/src/reference.ts +86 -15
- package/src/replay.ts +159 -0
- package/src/same.ts +42 -0
- package/src/static.ts +1 -1
- package/src/stdlib.ts +10 -6
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.
|
|
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,
|
|
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`
|
|
84
|
-
*
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
/**
|
|
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
|
|
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,
|
|
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
|
-
|
|
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`
|
|
234
|
-
*
|
|
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}`;
|