@mark1russell7/coeval 0.15.0 → 0.16.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/build/index.d.ts +315 -87
- package/build/index.d.ts.map +1 -1
- package/build/index.js +728 -32
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +66 -6
- package/src/deref.ts +27 -9
- package/src/evaluate.ts +198 -16
- package/src/index.ts +10 -0
- package/src/meaning.ts +472 -0
- package/src/reads.ts +9 -2
- package/src/schedule.ts +14 -4
- package/src/stdlib.ts +26 -4
package/src/meaning.ts
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { closureParams } from "./agree.ts";
|
|
2
|
+
import { type Json, type Key, docEquals } from "./doc.ts";
|
|
3
|
+
import { type Context, type HostThing, THING, context as contextOf } from "./deref.ts";
|
|
4
|
+
import { type Result, Failure } from "./errors.ts";
|
|
5
|
+
import { type Machine, prepare } from "./evaluate.ts";
|
|
6
|
+
import { factsOf } from "./facts.ts";
|
|
7
|
+
import { Held, isClosure } from "./formula.ts";
|
|
8
|
+
import { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
|
|
9
|
+
import { sameResult } from "./same.ts";
|
|
10
|
+
import { cycleOf, runSide } from "./schedule.ts";
|
|
11
|
+
import { STD_MACHINE } from "./stdlib.ts";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* One meaning across schedules (S3a, R4 P4, D58). The model is an evaluation of the whole graph from scratch with the
|
|
15
|
+
* meaning of D31. The harness gives an engine random edits, new nodes, removed nodes, new machines and changes of
|
|
16
|
+
* data. After each change, it compares the answers of the engine with the model. Each schedule of the kernel and each
|
|
17
|
+
* engine of a host uses the same generator and the same model.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The smallest interface of the engine of a host that `agreeSchedule` examines (SCHED.AGREES). The demand schedule of
|
|
22
|
+
* `schedule` has this interface.
|
|
23
|
+
*/
|
|
24
|
+
export interface ScheduleEngine {
|
|
25
|
+
/**
|
|
26
|
+
* This function adds a node, or gives a node a new document. The harness also gives the same document object again:
|
|
27
|
+
* a change of the structure without an edit.
|
|
28
|
+
*
|
|
29
|
+
* @param id - The name of the node.
|
|
30
|
+
* @param doc - The document of the node. It does not change in place.
|
|
31
|
+
*/
|
|
32
|
+
set(id: string, doc: unknown): void;
|
|
33
|
+
/**
|
|
34
|
+
* This function removes a node. A later read of the node by another node gives the error of a missing step.
|
|
35
|
+
*
|
|
36
|
+
* @param id - The name of the node.
|
|
37
|
+
*/
|
|
38
|
+
remove(id: string): unknown;
|
|
39
|
+
/**
|
|
40
|
+
* This function gives the engine the new context of the host. The values of a context do not change in place. The
|
|
41
|
+
* same context object again is a change of data that changes no value.
|
|
42
|
+
*
|
|
43
|
+
* @param context - The context of the host, without the root `world`.
|
|
44
|
+
*/
|
|
45
|
+
changeData(context: Context): void;
|
|
46
|
+
/**
|
|
47
|
+
* This function gives the engine a new machine. The harness gives no new machine to an engine without this function.
|
|
48
|
+
*
|
|
49
|
+
* @param machine - The new machine.
|
|
50
|
+
*/
|
|
51
|
+
changeMachine?(machine: Machine): void;
|
|
52
|
+
/**
|
|
53
|
+
* This function demands a node and gives its answer.
|
|
54
|
+
*
|
|
55
|
+
* @param id - The name of the node. The engine has the node.
|
|
56
|
+
* @returns The result of the node.
|
|
57
|
+
*/
|
|
58
|
+
read(id: string): Result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What a new engine gets beside its machine: the context of the host, and the type of a bare root value. */
|
|
62
|
+
export interface ScheduleEngineStart {
|
|
63
|
+
/** The first context of the host, without the root `world`. */
|
|
64
|
+
readonly context: Context;
|
|
65
|
+
/** The type of a bare root value, when the options of the harness name one. */
|
|
66
|
+
readonly declaredType?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This function makes a new engine with no nodes. The harness makes one engine for each run. The function `schedule`
|
|
71
|
+
* has this form.
|
|
72
|
+
*/
|
|
73
|
+
export type ScheduleEngineFactory = (machine: Machine, start: ScheduleEngineStart) => ScheduleEngine;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The options of `agreeSchedule`. The defaults fit `STD_MACHINE`. A host with another machine also gives `generate`,
|
|
77
|
+
* `value` and `wire`, and it sets `runs` to `false` when its world has no kind `log`.
|
|
78
|
+
*/
|
|
79
|
+
export interface AgreeScheduleOptions {
|
|
80
|
+
/** The machines. The first machine starts each run, and the command of a new machine takes the next one. The default is `STD_MACHINE` and a copy. */
|
|
81
|
+
readonly machines?: readonly Machine[];
|
|
82
|
+
/** The type of a bare root value, for each engine, each plan and each fact. */
|
|
83
|
+
readonly declaredType?: string;
|
|
84
|
+
/** The options of the document generator. Each generated path from `names` to a node changes into a path from `world`. */
|
|
85
|
+
readonly generate?: Omit<GenerateOptions, "seed" | "names">;
|
|
86
|
+
/**
|
|
87
|
+
* This function gives a small value document.
|
|
88
|
+
*
|
|
89
|
+
* @param rng - The source of random numbers.
|
|
90
|
+
* @returns The document.
|
|
91
|
+
*/
|
|
92
|
+
readonly value?: (rng: Random) => unknown;
|
|
93
|
+
/**
|
|
94
|
+
* This function gives a document that reads one or two nodes, for dense graphs with many cycles.
|
|
95
|
+
*
|
|
96
|
+
* @param rng - The source of random numbers.
|
|
97
|
+
* @param node - This function gives a reference to a random node.
|
|
98
|
+
* @returns The document.
|
|
99
|
+
*/
|
|
100
|
+
readonly wire?: (rng: Random, node: () => Json) => unknown;
|
|
101
|
+
/** The share of the nodes that `wire` gives. The default is 0.2. */
|
|
102
|
+
readonly wired?: number;
|
|
103
|
+
/** `true`: one generated document in ten goes into the kind `log`, thus the node is on the run side. The default is `true`. */
|
|
104
|
+
readonly runs?: boolean;
|
|
105
|
+
/** The names of the nodes. The default is five names. */
|
|
106
|
+
readonly nodes?: readonly string[];
|
|
107
|
+
/** The names that the context of the host binds at the root `names`. The default is `a`, `b`, `x` and `n`. */
|
|
108
|
+
readonly names?: readonly string[];
|
|
109
|
+
/** The values that a change of data gives a name of the host. The default has equal values, host values and a `Held` error. */
|
|
110
|
+
readonly values?: readonly unknown[];
|
|
111
|
+
/** The contexts of the host at the start of the runs, one after the other. */
|
|
112
|
+
readonly contexts?: readonly Context[];
|
|
113
|
+
/** The number of runs. The default is 100. */
|
|
114
|
+
readonly count?: number;
|
|
115
|
+
/** The number of commands in one run. The default is 30. */
|
|
116
|
+
readonly steps?: number;
|
|
117
|
+
/** The seed of the first run, and one more for each later run. The default is 9000011. */
|
|
118
|
+
readonly seed?: number;
|
|
119
|
+
/**
|
|
120
|
+
* This function compares two values that the rules of `sameResult` do not know. It answers first. Without an answer,
|
|
121
|
+
* two closures of `fn` are the same when they have the same parameters.
|
|
122
|
+
*
|
|
123
|
+
* @param a - The value of the engine, or of the first evaluation from scratch.
|
|
124
|
+
* @param b - The value from scratch.
|
|
125
|
+
* @returns `true` or `false`, or `undefined` for the other rules.
|
|
126
|
+
*/
|
|
127
|
+
readonly equal?: (a: unknown, b: unknown) => boolean | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** What the harness reached: the commands, the reads, and the kinds of answers that it compared. */
|
|
131
|
+
export interface ScheduleAgreementCounts {
|
|
132
|
+
/** The reads of a node. */
|
|
133
|
+
readonly reads: number;
|
|
134
|
+
/** The reads with one answer from scratch, which the harness compared. */
|
|
135
|
+
readonly compared: number;
|
|
136
|
+
/** The reads whose answer from scratch is new at each evaluation. The harness does not compare them. */
|
|
137
|
+
readonly unrepeatable: number;
|
|
138
|
+
/** The compared answers that are a value. */
|
|
139
|
+
readonly values: number;
|
|
140
|
+
/** The compared answers that are the `#CYCLE!` of the node. */
|
|
141
|
+
readonly cycles: number;
|
|
142
|
+
/** The compared answers that are the `#REF!` of a node on the run side. */
|
|
143
|
+
readonly runs: number;
|
|
144
|
+
/** The changes of data with new names of the host. */
|
|
145
|
+
readonly data: number;
|
|
146
|
+
/** The changes of data that change no value. */
|
|
147
|
+
readonly empty: number;
|
|
148
|
+
/** The new documents of a node. */
|
|
149
|
+
readonly edits: number;
|
|
150
|
+
/** The new nodes. */
|
|
151
|
+
readonly additions: number;
|
|
152
|
+
/** The removed nodes. */
|
|
153
|
+
readonly removes: number;
|
|
154
|
+
/** The changes of the structure with the same document object. */
|
|
155
|
+
readonly same: number;
|
|
156
|
+
/** The new machines. */
|
|
157
|
+
readonly machines: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The outcome of `agreeSchedule`: what the harness reached, or the first disagreement. */
|
|
161
|
+
export type ScheduleAgreement =
|
|
162
|
+
| { readonly ok: true; readonly counts: ScheduleAgreementCounts }
|
|
163
|
+
| {
|
|
164
|
+
readonly ok: false;
|
|
165
|
+
/** The seed of the run. */
|
|
166
|
+
readonly seed: number;
|
|
167
|
+
/** The index of the command in the run. */
|
|
168
|
+
readonly step: number;
|
|
169
|
+
/** The node of the read. */
|
|
170
|
+
readonly node: string;
|
|
171
|
+
/** The answer of the engine. */
|
|
172
|
+
readonly engine: Result;
|
|
173
|
+
/** The answer of the evaluation from scratch. */
|
|
174
|
+
readonly scratch: Result;
|
|
175
|
+
/** The documents of the nodes at the read. */
|
|
176
|
+
readonly docs: ReadonlyMap<string, unknown>;
|
|
177
|
+
/** The context of the host at the read. */
|
|
178
|
+
readonly context: Context;
|
|
179
|
+
/** The machine at the read. */
|
|
180
|
+
readonly machine: Machine;
|
|
181
|
+
/** One sentence that tells the difference. */
|
|
182
|
+
readonly reason: string;
|
|
183
|
+
/** What the harness reached up to the read. */
|
|
184
|
+
readonly counts: ScheduleAgreementCounts;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** A list of eight reference types. The generator then gives more references, thus more reads of nodes. */
|
|
188
|
+
function moreRefs(): readonly (readonly Key[])[] {
|
|
189
|
+
return Array.from({ length: 8 }, () => ["ref"]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The default options of the document generator, for `STD_MACHINE`. The kinds `log` and `write` need a capability. */
|
|
193
|
+
function standardGenerate(): Omit<GenerateOptions, "seed" | "names"> {
|
|
194
|
+
return {
|
|
195
|
+
maxSize: 16,
|
|
196
|
+
types: [...typesOf(STD_MACHINE.world).filter((type) => type[0] !== "log" && type[0] !== "write"), ...moreRefs(), ["if"], ["add"]],
|
|
197
|
+
data: (type, rng) => {
|
|
198
|
+
const name = type.join(".");
|
|
199
|
+
if (name === "number") return rng.chance(0.9) ? rng.pick([0, 1, 2, 3.5, -1]) : "1";
|
|
200
|
+
if (name === "text") return rng.chance(0.9) ? rng.pick(["", "a", "b"]) : 1;
|
|
201
|
+
if (name === "bool") return rng.chance(0.9) ? rng.chance(0.5) : 0;
|
|
202
|
+
if (name === "nearest") return rng.pick<Json>([{ type: "panel" }, "panel"]);
|
|
203
|
+
return undefined;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The default `wire`, for `STD_MACHINE`. A list evaluates each part, also after a failed part. */
|
|
209
|
+
function standardWire(rng: Random, node: () => Json): unknown {
|
|
210
|
+
if (rng.chance(0.5)) return { type: "list", value: [node(), node()] };
|
|
211
|
+
return rng.chance(0.5) ? { type: "add", value: [node(), 1] } : node();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The default values of the names of the host. Some are equal by structure, and some are host values. */
|
|
215
|
+
function standardValues(): readonly unknown[] {
|
|
216
|
+
return [0, 1, 2, -0, Number.NaN, "two", null, undefined, [1, 2], { a: 1 }, { a: 2 }, { type: "ref", value: ["names", "a"] }, new Held({ code: "#HELD", reason: "The binding failed." })];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The default contexts of the host at the start of a run. */
|
|
220
|
+
function standardContexts(): readonly Context[] {
|
|
221
|
+
return [
|
|
222
|
+
contextOf({ names: { a: 1, b: [1, "two"], x: { a: 1 }, n: 2 }, caps: ["log"] }),
|
|
223
|
+
contextOf({ names: { a: "two", b: 0, x: null }, self: { a: 1 }, event: { a: 2 } }),
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** This function puts a field on a record, also the field `__proto__`. */
|
|
228
|
+
function put(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
229
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** This function changes each path from `names` to a node into a path from `world`. Some paths read the whole root. */
|
|
233
|
+
function linked(value: unknown, rng: Random, nodes: readonly string[]): unknown {
|
|
234
|
+
if (Array.isArray(value)) {
|
|
235
|
+
const items = value.map((item) => linked(item, rng, nodes));
|
|
236
|
+
if (items[0] === "names" && typeof items[1] === "string" && nodes.includes(items[1])) return rng.chance(0.03) ? ["world"] : ["world", ...items.slice(1)];
|
|
237
|
+
return items;
|
|
238
|
+
}
|
|
239
|
+
if (typeof value !== "object" || value === null) return value;
|
|
240
|
+
const out: Record<string, unknown> = {};
|
|
241
|
+
for (const key of Object.keys(value)) put(out, key, linked((value as Record<string, unknown>)[key], rng, nodes));
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** This function gives new names of the host, with one or two changes. */
|
|
246
|
+
function change(base: Context, rng: Random, names: readonly string[], values: readonly unknown[]): Readonly<Record<string, unknown>> {
|
|
247
|
+
const out: Record<string, unknown> = { ...base.names };
|
|
248
|
+
for (let step = 1 + rng.int(2); step > 0; step -= 1) {
|
|
249
|
+
const name = rng.pick(names);
|
|
250
|
+
if (rng.chance(0.15)) delete out[name];
|
|
251
|
+
else out[name] = rng.pick(values);
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function shuffle<T>(items: readonly T[], rng: Random): T[] {
|
|
257
|
+
const out = [...items];
|
|
258
|
+
for (let index = out.length - 1; index > 0; index -= 1) {
|
|
259
|
+
const other = rng.int(index + 1);
|
|
260
|
+
[out[index], out[other]] = [out[other] as T, out[index] as T];
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* This function evaluates each node of a graph from scratch, with the meaning of D31. Each node gets a new plan and
|
|
267
|
+
* one value. The algorithm of Tarjan over the demands finds the cycles. The function keeps nothing between calls.
|
|
268
|
+
*/
|
|
269
|
+
function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base: Context, declaredType: string | undefined): ReadonlyMap<string, Result> {
|
|
270
|
+
const declared = declaredType === undefined ? {} : { declaredType };
|
|
271
|
+
const values = new Map<string, Result>();
|
|
272
|
+
const index = new Map<string, number>();
|
|
273
|
+
const lowest = new Map<string, number>();
|
|
274
|
+
const component: string[] = [];
|
|
275
|
+
const evaluating: string[] = [];
|
|
276
|
+
const selfReads = new Set<string>();
|
|
277
|
+
const visit = (id: string): Result => {
|
|
278
|
+
const reader = evaluating[evaluating.length - 1];
|
|
279
|
+
const link = (to: number): void => {
|
|
280
|
+
if (reader !== undefined) lowest.set(reader, Math.min(lowest.get(reader) as number, to));
|
|
281
|
+
};
|
|
282
|
+
const done = values.get(id);
|
|
283
|
+
if (done !== undefined) {
|
|
284
|
+
if (component.includes(id)) link(index.get(id) as number);
|
|
285
|
+
return done;
|
|
286
|
+
}
|
|
287
|
+
if (evaluating.includes(id)) {
|
|
288
|
+
link(index.get(id) as number);
|
|
289
|
+
if (reader === id) selfReads.add(id);
|
|
290
|
+
return cycleOf(id);
|
|
291
|
+
}
|
|
292
|
+
if (factsOf(machine, docs.get(id), declared).side === "run") {
|
|
293
|
+
const refused = runSide(id);
|
|
294
|
+
values.set(id, refused);
|
|
295
|
+
return refused;
|
|
296
|
+
}
|
|
297
|
+
index.set(id, index.size);
|
|
298
|
+
lowest.set(id, index.get(id) as number);
|
|
299
|
+
component.push(id);
|
|
300
|
+
evaluating.push(id);
|
|
301
|
+
let result = prepare(machine, docs.get(id), declared).evaluate(ctx);
|
|
302
|
+
evaluating.pop();
|
|
303
|
+
if ((lowest.get(id) as number) < (index.get(id) as number)) result = cycleOf(id);
|
|
304
|
+
else if (component.splice(component.indexOf(id)).length > 1 || selfReads.has(id)) result = cycleOf(id);
|
|
305
|
+
values.set(id, result);
|
|
306
|
+
link(lowest.get(id) as number);
|
|
307
|
+
return result;
|
|
308
|
+
};
|
|
309
|
+
const world: HostThing = {
|
|
310
|
+
[THING]: true,
|
|
311
|
+
label: "nodes",
|
|
312
|
+
deref: (key) => {
|
|
313
|
+
if (!docs.has(String(key))) return undefined;
|
|
314
|
+
const result = visit(String(key));
|
|
315
|
+
if (!result.ok) throw new Failure(result.error);
|
|
316
|
+
return result.value;
|
|
317
|
+
},
|
|
318
|
+
has: (key) => docs.has(String(key)),
|
|
319
|
+
toJson: () => {
|
|
320
|
+
const out: Record<string, unknown> = {};
|
|
321
|
+
for (const id of [...docs.keys()].sort()) {
|
|
322
|
+
if (factsOf(machine, docs.get(id), declared).side === "run") continue;
|
|
323
|
+
const result = visit(id);
|
|
324
|
+
if (!result.ok) throw new Failure(result.error);
|
|
325
|
+
put(out, id, result.value);
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
const ctx: Context = { ...base, world };
|
|
331
|
+
for (const id of [...docs.keys()].sort()) visit(id);
|
|
332
|
+
return values;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** This function names a result for the reason of a disagreement: its error code, or "a value". */
|
|
336
|
+
function named(result: Result): string {
|
|
337
|
+
return result.ok ? "a value" : result.error.code;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* This function examines an engine of a host against an evaluation from scratch (SCHED.AGREES, R4 P4, D58). Each run
|
|
342
|
+
* makes a new engine and a random graph of named nodes from the document generator. Then it gives random commands:
|
|
343
|
+
* edits, new nodes, removed nodes, the same document again, new machines and changes of data.
|
|
344
|
+
*
|
|
345
|
+
* - Each read command evaluates the whole graph from scratch two times, with the meaning of D31.
|
|
346
|
+
* - It then demands each node in a random order, or one node, and compares each answer with `sameResult`.
|
|
347
|
+
* - A read whose answer from scratch differs between the two evaluations is not compared.
|
|
348
|
+
* - Two errors agree when they have the same code, reason and place. Thus an engine gives the `#CYCLE!` and the
|
|
349
|
+
* `#REF!` of D31.
|
|
350
|
+
*
|
|
351
|
+
* The same seed and the same options give the same commands, thus a disagreement occurs again.
|
|
352
|
+
*
|
|
353
|
+
* @param engine - This function makes a new engine for each run, for example `schedule`.
|
|
354
|
+
* @param options - The machines, the generator, the contexts, the number of runs and the rule for host values.
|
|
355
|
+
* @returns What the harness reached, or the first read whose answer differs from the answer from scratch.
|
|
356
|
+
*/
|
|
357
|
+
export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeScheduleOptions = {}): ScheduleAgreement {
|
|
358
|
+
const machines = options.machines ?? [STD_MACHINE, { ...STD_MACHINE }];
|
|
359
|
+
const declaredType = options.declaredType;
|
|
360
|
+
const generateOptions = options.generate ?? standardGenerate();
|
|
361
|
+
const value = options.value ?? ((rng: Random) => ({ type: "number", value: rng.pick([0, 1, 2]) }));
|
|
362
|
+
const wire = options.wire ?? standardWire;
|
|
363
|
+
const wired = options.wired ?? 0.2;
|
|
364
|
+
const runs = options.runs ?? true;
|
|
365
|
+
const nodes = options.nodes ?? ["p", "q", "r", "s", "t"];
|
|
366
|
+
const hostNames = options.names ?? ["a", "b", "x", "n"];
|
|
367
|
+
const hostValues = options.values ?? standardValues();
|
|
368
|
+
const contexts = options.contexts ?? standardContexts();
|
|
369
|
+
const total = options.count ?? 100;
|
|
370
|
+
const steps = options.steps ?? 30;
|
|
371
|
+
const first = options.seed ?? 9_000_011;
|
|
372
|
+
const given = options.equal;
|
|
373
|
+
const equal = (a: unknown, b: unknown): boolean | undefined => {
|
|
374
|
+
const answer = given?.(a, b);
|
|
375
|
+
if (answer !== undefined) return answer;
|
|
376
|
+
if (isClosure(a) || isClosure(b)) return isClosure(a) && isClosure(b) && docEquals(closureParams(a), closureParams(b));
|
|
377
|
+
return undefined;
|
|
378
|
+
};
|
|
379
|
+
const counts = { reads: 0, compared: 0, unrepeatable: 0, values: 0, cycles: 0, runs: 0, data: 0, empty: 0, edits: 0, additions: 0, removes: 0, same: 0, machines: 0 };
|
|
380
|
+
const declared = declaredType === undefined ? {} : { declaredType };
|
|
381
|
+
for (let run = 0; run < total; run += 1) {
|
|
382
|
+
const seed = first + run;
|
|
383
|
+
const rng = random(seed);
|
|
384
|
+
const node = (): Json => ({ type: "ref", value: ["world", rng.pick(nodes)] });
|
|
385
|
+
let at = 0;
|
|
386
|
+
let machine = machines[0] as Machine;
|
|
387
|
+
let made = 0;
|
|
388
|
+
const make = (): unknown => {
|
|
389
|
+
if (rng.chance(wired)) return wire(rng, node);
|
|
390
|
+
const roll = rng.int(10);
|
|
391
|
+
if (roll === 0) return value(rng);
|
|
392
|
+
// Some nodes read only the data of the host, thus the graph is not always one component.
|
|
393
|
+
const names = roll < 5 ? hostNames : [...hostNames, ...nodes, ...nodes, ...nodes];
|
|
394
|
+
const doc = linked(generate(machine.world, { ...generateOptions, names, seed: seed * 31 + made++ }), rng, nodes);
|
|
395
|
+
return runs && roll === 9 ? { type: "log", value: [doc] } : doc;
|
|
396
|
+
};
|
|
397
|
+
const docs = new Map<string, unknown>(nodes.map((id) => [id, make()]));
|
|
398
|
+
const start = contexts[run % contexts.length] as Context;
|
|
399
|
+
let base: Context = { ...start, names: { ...start.names } };
|
|
400
|
+
const host = engine(machine, { ...declared, context: base });
|
|
401
|
+
for (const [id, doc] of docs) host.set(id, doc);
|
|
402
|
+
for (let step = 0; step < steps; step += 1) {
|
|
403
|
+
const what = rng.int(12);
|
|
404
|
+
if (what < 4) {
|
|
405
|
+
const fresh = fromScratch(machine, docs, base, declaredType);
|
|
406
|
+
const again = fromScratch(machine, docs, base, declaredType);
|
|
407
|
+
const ids = shuffle([...docs.keys()], rng);
|
|
408
|
+
// Half of the reads demand each node in a random order, and the other half demand one node.
|
|
409
|
+
for (const id of rng.chance(0.5) ? ids : ids.slice(0, 1)) {
|
|
410
|
+
const got = host.read(id);
|
|
411
|
+
const want = fresh.get(id) as Result;
|
|
412
|
+
counts.reads += 1;
|
|
413
|
+
// A result that is new at each evaluation has no one result to compare.
|
|
414
|
+
if (!sameResult(want, again.get(id) as Result, equal)) {
|
|
415
|
+
counts.unrepeatable += 1;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
counts.compared += 1;
|
|
419
|
+
if (sameResult(want, cycleOf(id))) counts.cycles += 1;
|
|
420
|
+
else if (sameResult(want, runSide(id))) counts.runs += 1;
|
|
421
|
+
else if (want.ok) counts.values += 1;
|
|
422
|
+
if (!sameResult(got, want, equal)) {
|
|
423
|
+
const reason =
|
|
424
|
+
got.ok === want.ok && !got.ok
|
|
425
|
+
? `The engine and the evaluation from scratch give two different errors ${named(got)} and ${named(want)} for the node ${id}.`
|
|
426
|
+
: `The engine gives ${named(got)} for the node ${id}, and the evaluation from scratch gives ${named(want)}.`;
|
|
427
|
+
return { ok: false, seed, step, node: id, engine: got, scratch: want, docs: new Map(docs), context: base, machine, reason, counts: { ...counts } };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} else if (what < 6) {
|
|
431
|
+
base = { ...base, names: change(base, rng, hostNames, hostValues) };
|
|
432
|
+
host.changeData(base);
|
|
433
|
+
counts.data += 1;
|
|
434
|
+
} else if (what === 6) {
|
|
435
|
+
// A change of data that changes no value: the same context object again.
|
|
436
|
+
host.changeData(base);
|
|
437
|
+
counts.empty += 1;
|
|
438
|
+
} else if (what < 9) {
|
|
439
|
+
const id = rng.pick(nodes);
|
|
440
|
+
const doc = make();
|
|
441
|
+
docs.set(id, doc);
|
|
442
|
+
host.set(id, doc);
|
|
443
|
+
counts.edits += 1;
|
|
444
|
+
} else if (what === 9) {
|
|
445
|
+
const absent = nodes.filter((id) => !docs.has(id));
|
|
446
|
+
if (absent.length > 0 && (docs.size <= 2 || rng.chance(0.65))) {
|
|
447
|
+
const id = rng.pick(absent);
|
|
448
|
+
const doc = make();
|
|
449
|
+
docs.set(id, doc);
|
|
450
|
+
host.set(id, doc);
|
|
451
|
+
counts.additions += 1;
|
|
452
|
+
} else {
|
|
453
|
+
const id = rng.pick([...docs.keys()]);
|
|
454
|
+
docs.delete(id);
|
|
455
|
+
host.remove(id);
|
|
456
|
+
counts.removes += 1;
|
|
457
|
+
}
|
|
458
|
+
} else if (what === 10) {
|
|
459
|
+
// The same document object again: a change of the structure without an edit.
|
|
460
|
+
const id = rng.pick([...docs.keys()]);
|
|
461
|
+
host.set(id, docs.get(id));
|
|
462
|
+
counts.same += 1;
|
|
463
|
+
} else if (host.changeMachine !== undefined) {
|
|
464
|
+
at = (at + 1) % machines.length;
|
|
465
|
+
machine = machines[at] as Machine;
|
|
466
|
+
host.changeMachine(machine);
|
|
467
|
+
counts.machines += 1;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return { ok: true, counts };
|
|
472
|
+
}
|
package/src/reads.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { docEquals } from "./doc.ts";
|
|
2
|
-
import { type Result, ok } from "./errors.ts";
|
|
2
|
+
import { type Result, fail, ok } from "./errors.ts";
|
|
3
3
|
import type { Context, Hop } from "./deref.ts";
|
|
4
|
-
import { type Machine, type Plan, type Read, readAgain, rootContext } from "./evaluate.ts";
|
|
4
|
+
import { type Machine, type Plan, type Read, keptAgain, readAgain, rootContext } from "./evaluate.ts";
|
|
5
5
|
import { Held } from "./formula.ts";
|
|
6
6
|
import { sameResult } from "./same.ts";
|
|
7
7
|
|
|
@@ -95,6 +95,13 @@ export function verifyReads(target: Machine | Plan, reads: readonly Read[], cont
|
|
|
95
95
|
if (!read.free) continue;
|
|
96
96
|
const stop = heldStop(root, read.path);
|
|
97
97
|
if (stop !== undefined) return { same: false, index, read, now: stop };
|
|
98
|
+
if (read.kept === true) {
|
|
99
|
+
// A kept read does not step into a host thing with `deref`. A host thing with `known` answers its step (SCHED.CLOSURE).
|
|
100
|
+
const now = keptAgain(machine, root, read.path);
|
|
101
|
+
if (now === undefined) return { same: false, index, read, now: fail("#N/A", "The host does not know the value of the kept read yet.") };
|
|
102
|
+
if (!sameResult(read.reached, now, equal)) return { same: false, index, read, now };
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
98
105
|
const asked = read.hops !== undefined;
|
|
99
106
|
const now = readAgain(machine, root, read.path, asked);
|
|
100
107
|
if (!sameResult(read.reached, now.reached, equal) || (read.hops !== undefined && !sameHops(read.hops, now.hops, equal))) {
|
package/src/schedule.ts
CHANGED
|
@@ -166,12 +166,12 @@ const NO_KEYS: readonly Key[] = /* @__PURE__ */ Object.freeze([]);
|
|
|
166
166
|
const NO_EDGES: readonly NodeEdge[] = /* @__PURE__ */ Object.freeze([]);
|
|
167
167
|
|
|
168
168
|
/** This function gives the error of a node on a cycle. It names only the node, thus each order of demands gives the same error. */
|
|
169
|
-
function cycleOf(id: string): Failed {
|
|
169
|
+
export function cycleOf(id: string): Failed {
|
|
170
170
|
return fail("#CYCLE!", `The node ${id} is on a cycle of reads.`);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/** This function gives the error of a read of a node on the run side. */
|
|
174
|
-
function runSide(id: string): Failed {
|
|
174
|
+
export function runSide(id: string): Failed {
|
|
175
175
|
return fail("#REF!", `The node ${id} is on the run side, thus it has no value for a reader.`);
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -354,10 +354,12 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
354
354
|
|
|
355
355
|
/**
|
|
356
356
|
* This function gives the state of the current data epoch. Each data epoch has a new host thing at the root
|
|
357
|
-
* `world`. Thus a read that reaches the whole root changes at each change of data (READS.KEPT).
|
|
357
|
+
* `world`. Thus a read that reaches the whole root changes at each change of data (READS.KEPT). A kept read of a
|
|
358
|
+
* closure reads a node through `known`, without a demand (SCHED.CLOSURE).
|
|
358
359
|
*/
|
|
359
360
|
const current = (): Epoch => {
|
|
360
361
|
if (now !== undefined) return now;
|
|
362
|
+
const values = new Map<string, Result>();
|
|
361
363
|
const nodesThing: HostThing = {
|
|
362
364
|
[THING]: true,
|
|
363
365
|
label: "nodes",
|
|
@@ -371,10 +373,18 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
371
373
|
},
|
|
372
374
|
has: (key) => docs.has(String(key)),
|
|
373
375
|
toJson: record,
|
|
376
|
+
// A demand from a kept read can reach the node in evaluation, and that cycle is false (SCHED.ONE-MEANING).
|
|
377
|
+
// Thus only a node with its result in this epoch gives a value. Each such value is final (D31).
|
|
378
|
+
known(key) {
|
|
379
|
+
const result = values.get(String(key));
|
|
380
|
+
if (result === undefined) return undefined;
|
|
381
|
+
if (!result.ok) throw new Failure(result.error);
|
|
382
|
+
return { thing: result.value };
|
|
383
|
+
},
|
|
374
384
|
};
|
|
375
385
|
// The context has a world, thus `rootContext` only gives it its grant (CAP.NARROW-CODE). Each evaluation of the
|
|
376
386
|
// epoch then uses this object with no copy.
|
|
377
|
-
now = { context: rootContext(machine, { ...base, world: nodesThing }), values
|
|
387
|
+
now = { context: rootContext(machine, { ...base, world: nodesThing }), values };
|
|
378
388
|
return now;
|
|
379
389
|
};
|
|
380
390
|
|
package/src/stdlib.ts
CHANGED
|
@@ -560,6 +560,18 @@ export function attributeTrace(node: Thing, attribute: string): AttributeTrace |
|
|
|
560
560
|
return stateOf(node)?.trace(attribute);
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
/**
|
|
564
|
+
* This function gives the place of a node of the decorated tree: the keys from the root document to the document of
|
|
565
|
+
* the node (HOP.PLACE). A view puts each hop of a resolution on its node with it. The node keeps its place already,
|
|
566
|
+
* thus the function adds no record.
|
|
567
|
+
*
|
|
568
|
+
* @param thing - A thing, for example the thing of a hop.
|
|
569
|
+
* @returns The keys of the place, or `undefined` for a thing that is not a node of the decorated tree.
|
|
570
|
+
*/
|
|
571
|
+
export function placeOf(thing: Thing): readonly Key[] | undefined {
|
|
572
|
+
return stateOf(thing)?.place;
|
|
573
|
+
}
|
|
574
|
+
|
|
563
575
|
/** The path of the root `self`. A node of the tree resolves it, thus the list of reads holds the read (C4). */
|
|
564
576
|
const SELF_PATH: readonly Json[] = /* @__PURE__ */ Object.freeze(["self"]);
|
|
565
577
|
|
|
@@ -597,6 +609,11 @@ export interface NearestOptions {
|
|
|
597
609
|
readonly matches?: (node: Thing, wanted: Json | undefined) => boolean;
|
|
598
610
|
/** The most hops of one walk. The default is 256. */
|
|
599
611
|
readonly maxHops?: number;
|
|
612
|
+
/**
|
|
613
|
+
* `true`: the walk records one hop for each node on the way before the node that it reaches (HOP.EACH-STEP). Each
|
|
614
|
+
* such hop has the followed key as its step. The default is `false`: one hop for the whole walk.
|
|
615
|
+
*/
|
|
616
|
+
readonly eachStep?: boolean;
|
|
600
617
|
}
|
|
601
618
|
|
|
602
619
|
function hostType(node: Thing, wanted: Json | undefined): boolean {
|
|
@@ -606,23 +623,28 @@ function hostType(node: Thing, wanted: Json | undefined): boolean {
|
|
|
606
623
|
/**
|
|
607
624
|
* This function makes a walker `nearest`. The walker follows a key (the default is `parent`) until the node that it
|
|
608
625
|
* reaches has the wanted type. With `self: true`, the node where the walk starts is the first candidate. A node
|
|
609
|
-
* without that key ends the walk with an error.
|
|
626
|
+
* without that key ends the walk with an error. With `eachStep`, a resolution with hops gets a hop for each node on the way.
|
|
610
627
|
*
|
|
611
|
-
* @param options - The test of a type and the
|
|
628
|
+
* @param options - The test of a type, the most hops and the record of each step.
|
|
612
629
|
* @returns The walker.
|
|
613
630
|
*/
|
|
614
631
|
export function nearestWalker(options: NearestOptions = {}): Walker {
|
|
615
632
|
const matches = options.matches ?? hostType;
|
|
616
633
|
const maxHops = options.maxHops ?? 256;
|
|
617
|
-
|
|
634
|
+
const eachStep = options.eachStep === true;
|
|
635
|
+
return ({ data, here, deref, visit }) => {
|
|
618
636
|
const follow = isRecord(data) && typeof field(data, "follow") === "string" ? String(field(data, "follow")) : "parent";
|
|
619
637
|
const wanted = isRecord(data) ? field(data, "type") : undefined;
|
|
620
638
|
const fromSelf = isRecord(data) && field(data, "self") === true;
|
|
639
|
+
const record = eachStep ? visit : undefined;
|
|
621
640
|
let at: Thing | undefined = here;
|
|
622
641
|
for (let hops = 0; hops < maxHops; hops += 1) {
|
|
623
|
-
|
|
642
|
+
const stepped = hops > 0 || !fromSelf;
|
|
643
|
+
if (stepped) at = at === undefined || at === null ? undefined : deref(at, follow);
|
|
624
644
|
if (at === undefined || at === null) return { ok: false, reason: `No node of the type ${String(wanted)} is on the way.` };
|
|
625
645
|
if (matches(at, wanted)) return { ok: true, thing: at };
|
|
646
|
+
// The hop of the whole walk records the node that the walk reaches. Each node before it gets its own hop.
|
|
647
|
+
if (stepped) record?.(follow, at);
|
|
626
648
|
}
|
|
627
649
|
return { ok: false, reason: "The walk is too long." };
|
|
628
650
|
};
|