@mark1russell7/coeval 0.14.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/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))) {