@mark1russell7/coeval 0.21.0 → 0.23.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 CHANGED
@@ -58,12 +58,14 @@ export interface ScheduleEngine {
58
58
  read(id: string): Result;
59
59
  }
60
60
 
61
- /** What a new engine gets beside its machine: the context of the host, and the type of a bare root value. */
61
+ /** What a new engine gets beside its machine. These are the context of the host, the type of a bare root value and the `self` of each node. */
62
62
  export interface ScheduleEngineStart {
63
63
  /** The first context of the host, without the root `world`. */
64
64
  readonly context: Context;
65
65
  /** The type of a bare root value, when the options of the harness name one. */
66
66
  readonly declaredType?: string;
67
+ /** The `self` of each node, when the options of the harness have `selves`. It has the form of the option of `schedule`. */
68
+ readonly selfOf?: (id: string, host: Context) => unknown;
67
69
  }
68
70
 
69
71
  /**
@@ -110,6 +112,12 @@ export interface AgreeScheduleOptions {
110
112
  readonly values?: readonly unknown[];
111
113
  /** The contexts of the host at the start of the runs, one after the other. */
112
114
  readonly contexts?: readonly Context[];
115
+ /**
116
+ * The values of the `self` of a node (SCHED.NODE-AGREES). With them, the option `selfOf` of each engine gives each
117
+ * node its own `self`. Some nodes then share one document, and a change of data can give a node a new `self`. The
118
+ * value `undefined` keeps the `self` of the host. The default gives each node the context of the host.
119
+ */
120
+ readonly selves?: readonly unknown[];
113
121
  /** The number of runs. The default is 100. */
114
122
  readonly count?: number;
115
123
  /** The number of commands in one run. The default is 30. */
@@ -155,6 +163,10 @@ export interface ScheduleAgreementCounts {
155
163
  readonly same: number;
156
164
  /** The new machines. */
157
165
  readonly machines: number;
166
+ /** The changes of data with a new `self` of a node, only with the option `selves`. */
167
+ readonly selves: number;
168
+ /** The documents that a node shares with another node, only with the option `selves`. */
169
+ readonly shared: number;
158
170
  }
159
171
 
160
172
  /** The outcome of `agreeSchedule`: what the harness reached, or the first disagreement. */
@@ -266,7 +278,7 @@ function shuffle<T>(items: readonly T[], rng: Random): T[] {
266
278
  * This function evaluates each node of a graph from scratch, with the meaning of D31. Each node gets a new plan and
267
279
  * one value. The algorithm of Tarjan over the demands finds the cycles. The function keeps nothing between calls.
268
280
  */
269
- function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base: Context, declaredType: string | undefined): ReadonlyMap<string, Result> {
281
+ function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base: Context, declaredType: string | undefined, own: ReadonlyMap<string, unknown> | undefined): ReadonlyMap<string, Result> {
270
282
  const declared = declaredType === undefined ? {} : { declaredType };
271
283
  const values = new Map<string, Result>();
272
284
  const index = new Map<string, number>();
@@ -298,7 +310,8 @@ function fromScratch(machine: Machine, docs: ReadonlyMap<string, unknown>, base:
298
310
  lowest.set(id, index.get(id) as number);
299
311
  component.push(id);
300
312
  evaluating.push(id);
301
- let result = prepare(machine, docs.get(id), declared).evaluate(ctx);
313
+ const self = own?.get(id);
314
+ let result = prepare(machine, docs.get(id), declared).evaluate(self === undefined ? ctx : { ...ctx, self });
302
315
  evaluating.pop();
303
316
  if ((lowest.get(id) as number) < (index.get(id) as number)) result = cycleOf(id);
304
317
  else if (component.splice(component.indexOf(id)).length > 1 || selfReads.has(id)) result = cycleOf(id);
@@ -340,7 +353,8 @@ function named(result: Result): string {
340
353
  /**
341
354
  * This function examines an engine of a host against an evaluation from scratch (SCHED.AGREES, R4 P4, D58). Each run
342
355
  * 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.
356
+ * edits, new nodes, removed nodes, the same document again, new machines and changes of data. The option `selves`
357
+ * adds a `self` for each node and documents that nodes share (SCHED.NODE-AGREES).
344
358
  *
345
359
  * - Each read command evaluates the whole graph from scratch two times, with the meaning of D31.
346
360
  * - It then demands each node in a random order, or one node, and compares each answer with `sameResult`.
@@ -366,6 +380,7 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
366
380
  const hostNames = options.names ?? ["a", "b", "x", "n"];
367
381
  const hostValues = options.values ?? standardValues();
368
382
  const contexts = options.contexts ?? standardContexts();
383
+ const selves = options.selves;
369
384
  const total = options.count ?? 100;
370
385
  const steps = options.steps ?? 30;
371
386
  const first = options.seed ?? 9_000_011;
@@ -376,7 +391,7 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
376
391
  if (isClosure(a) || isClosure(b)) return isClosure(a) && isClosure(b) && docEquals(closureParams(a), closureParams(b));
377
392
  return undefined;
378
393
  };
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 };
394
+ 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, selves: 0, shared: 0 };
380
395
  const declared = declaredType === undefined ? {} : { declaredType };
381
396
  for (let run = 0; run < total; run += 1) {
382
397
  const seed = first + run;
@@ -385,7 +400,18 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
385
400
  let at = 0;
386
401
  let machine = machines[0] as Machine;
387
402
  let made = 0;
403
+ const own = selves === undefined ? undefined : new Map<string, unknown>();
404
+ const pool: unknown[] = [];
388
405
  const make = (): unknown => {
406
+ if (selves !== undefined && pool.length > 0 && rng.chance(0.4)) {
407
+ counts.shared += 1;
408
+ return rng.pick(pool);
409
+ }
410
+ const doc = generated();
411
+ pool.push(doc);
412
+ return doc;
413
+ };
414
+ const generated = (): unknown => {
389
415
  if (rng.chance(wired)) return wire(rng, node);
390
416
  const roll = rng.int(10);
391
417
  if (roll === 0) return value(rng);
@@ -395,15 +421,16 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
395
421
  return runs && roll === 9 ? { type: "log", value: [doc] } : doc;
396
422
  };
397
423
  const docs = new Map<string, unknown>(nodes.map((id) => [id, make()]));
424
+ if (own !== undefined && selves !== undefined) for (const id of nodes) own.set(id, rng.pick(selves));
398
425
  const start = contexts[run % contexts.length] as Context;
399
426
  let base: Context = { ...start, names: { ...start.names } };
400
- const host = engine(machine, { ...declared, context: base });
427
+ const host = engine(machine, { ...declared, context: base, ...(own === undefined ? {} : { selfOf: (id: string) => own.get(id) }) });
401
428
  for (const [id, doc] of docs) host.set(id, doc);
402
429
  for (let step = 0; step < steps; step += 1) {
403
430
  const what = rng.int(12);
404
431
  if (what < 4) {
405
- const fresh = fromScratch(machine, docs, base, declaredType);
406
- const again = fromScratch(machine, docs, base, declaredType);
432
+ const fresh = fromScratch(machine, docs, base, declaredType, own);
433
+ const again = fromScratch(machine, docs, base, declaredType, own);
407
434
  const ids = shuffle([...docs.keys()], rng);
408
435
  // Half of the reads demand each node in a random order, and the other half demand one node.
409
436
  for (const id of rng.chance(0.5) ? ids : ids.slice(0, 1)) {
@@ -428,9 +455,15 @@ export function agreeSchedule(engine: ScheduleEngineFactory, options: AgreeSched
428
455
  }
429
456
  }
430
457
  } else if (what < 6) {
431
- base = { ...base, names: change(base, rng, hostNames, hostValues) };
458
+ if (own !== undefined && selves !== undefined && rng.chance(0.5)) {
459
+ // A new `self` of one node, with the same context object of the host.
460
+ own.set(rng.pick(nodes), rng.pick(selves));
461
+ counts.selves += 1;
462
+ } else {
463
+ base = { ...base, names: change(base, rng, hostNames, hostValues) };
464
+ counts.data += 1;
465
+ }
432
466
  host.changeData(base);
433
- counts.data += 1;
434
467
  } else if (what === 6) {
435
468
  // A change of data that changes no value: the same context object again.
436
469
  host.changeData(base);
package/src/parts.ts CHANGED
@@ -41,10 +41,13 @@ export type Split =
41
41
  | { readonly ok: false; readonly reason: string; readonly at: readonly Key[] };
42
42
 
43
43
  class Bad {
44
- constructor(
45
- readonly reason: string,
46
- readonly at: readonly Key[],
47
- ) {}
44
+ readonly reason: string;
45
+ readonly at: readonly Key[];
46
+
47
+ constructor(reason: string, at: readonly Key[]) {
48
+ this.reason = reason;
49
+ this.at = at;
50
+ }
48
51
  }
49
52
 
50
53
  /** This function sets an own data property. Only the key `__proto__` needs `defineProperty`. */
package/src/providers.ts CHANGED
@@ -150,6 +150,7 @@ function unionOf(types: readonly ValueType[]): ValueType {
150
150
 
151
151
  /** The state of one reading of a schema: the root, the issues and the definitions that it read. */
152
152
  class SchemaReader {
153
+ readonly root: unknown;
153
154
  readonly issues: TypeIssue[] = [];
154
155
  /** The type of each definition that a reference reached. Each reading of a definition is sound, thus the first stays. */
155
156
  readonly defs = new Map<string, ValueType>();
@@ -158,7 +159,9 @@ class SchemaReader {
158
159
  /** The number of schemas with an own `$id` around the current schema. A reference inside them has another base. */
159
160
  inner = 0;
160
161
 
161
- constructor(readonly root: unknown) {}
162
+ constructor(root: unknown) {
163
+ this.root = root;
164
+ }
162
165
 
163
166
  issue(at: readonly Key[], reason: string): void {
164
167
  this.issues.push({ at, reason });
package/src/reference.ts CHANGED
@@ -1,9 +1,23 @@
1
1
  import { type Envelope, type Json, type Key, docEquals, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
2
2
  import { ask } from "./ladder.ts";
3
- import { type AttributeDeclaration, type CapsRule, type CircularRule, type Kind, type TreeContract, circularOf, collectOf, equationType, lookupKind } from "./world.ts";
3
+ import {
4
+ type AttributeDeclaration,
5
+ type CapsRule,
6
+ type CircularRule,
7
+ type Kind,
8
+ type TreeContract,
9
+ GENERATION_LIMIT,
10
+ atArgsOf,
11
+ atRuleOf,
12
+ circularOf,
13
+ collectOf,
14
+ documentKey,
15
+ equationType,
16
+ lookupKind,
17
+ } from "./world.ts";
4
18
  import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
5
19
  import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, ROOTS, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
6
- import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
20
+ import { type AttributeRung, AT_READERS, STD_EVALUATORS } from "./stdlib.ts";
7
21
  import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
8
22
  import {
9
23
  type AllOptions,
@@ -113,6 +127,8 @@ interface Env {
113
127
  scope: Scope;
114
128
  /** The count of the drives without a wait that are active. In a run, an effect under one does not reach the host (RUN.HELD). */
115
129
  held: number;
130
+ /** The count of the applications of closures that run at this time: the clock of `call` (FORMULA.CALL-DEPTH, D125). */
131
+ applying: number;
116
132
  /** The record of the reads. A nested evaluation for an equation of a tree keeps the record of its tree. */
117
133
  readonly reading: Reading | undefined;
118
134
  }
@@ -674,6 +690,7 @@ function envOf(machine: Machine, options: ReferenceOptions, running: boolean, ro
674
690
  verb: options.verb ?? "eval",
675
691
  scope: { prefix: "", counts: new Map(), alls: 0 },
676
692
  held: 0,
693
+ applying: 0,
677
694
  reading: outer ?? (options.reads === undefined ? undefined : { list: options.reads, root }),
678
695
  };
679
696
  }
@@ -879,6 +896,55 @@ function collectIn(node: RefNode, name: string, declared: unknown): Result {
879
896
  return visit(node) ?? acc;
880
897
  }
881
898
 
899
+ /** TREE.HIGHER-DEPTH: the depth of each root of a generated document in the reference. */
900
+ const refGenerations = /* @__PURE__ */ new WeakMap<RefNode, number>();
901
+
902
+ /** TREE.HIGHER-DEPTH: the depth of the nearest root of a generated document at or above a node of the reference, or 0. */
903
+ function generationIn(node: RefNode): number {
904
+ for (let at: RefNode | undefined = node; at !== undefined; at = at.up) {
905
+ const depth = refGenerations.get(at);
906
+ if (depth !== undefined) return depth;
907
+ }
908
+ return 0;
909
+ }
910
+
911
+ /**
912
+ * TREE.HIGHER: a higher-order attribute. The equation gives a document as data, and the document evaluates with the
913
+ * node as `self`. The reference compiles each document again, thus a structural key is not a part of the meaning.
914
+ */
915
+ function higherOf(node: RefNode, name: string, term: Json): Result {
916
+ const value = equationOf(node, term, {});
917
+ if (!value.ok) return value;
918
+ const text = nameOfAttribute(node, name);
919
+ if (documentKey(value.value) === undefined) return fail("#VALUE!", `The equation of the higher-order attribute ${text} gives a value that is not JSON data.`);
920
+ const depth = generationIn(node) + 1;
921
+ if (depth > GENERATION_LIMIT) return fail("#DEPTH!", `The higher-order attribute ${text} is under more than ${GENERATION_LIMIT} generated documents.`);
922
+ const root = equationOf(node, value.value as Json, {});
923
+ if (!root.ok) return root;
924
+ const made = refNodeOf(root.value);
925
+ if (made === undefined) return fail("#VALUE!", `The document of the higher-order attribute ${text} gives no node of the tree.`);
926
+ refGenerations.set(made, depth);
927
+ return root;
928
+ }
929
+
930
+ /** TREE.FORWARD: the last rung. The attribute `forwards` gives a node, and that node answers through its own ladder. */
931
+ function forwardOf(node: RefNode, name: string): Result | undefined {
932
+ const target: unknown = node.contract?.forwards;
933
+ if (target === undefined || target === name) return undefined;
934
+ const text = nameOfAttribute(node, name);
935
+ if (typeof target !== "string") return fail("#VALUE!", `The attribute ${text} has a contract with a bad field forwards. It must be a text.`);
936
+ const value = attributeOf(node, target);
937
+ if (value === undefined || !value.ok) return value;
938
+ const to = refNodeOf(value.value);
939
+ if (to === undefined) return fail("#VALUE!", `The attribute ${target} that ${text} forwards to gives no node of a tree.`);
940
+ if (!refGenerations.has(to)) {
941
+ const depth = generationIn(node) + 1;
942
+ if (depth > GENERATION_LIMIT) return fail("#DEPTH!", `The attribute ${text} forwards through more than ${GENERATION_LIMIT} generated documents.`);
943
+ refGenerations.set(to, depth);
944
+ }
945
+ return attributeOf(to, name);
946
+ }
947
+
882
948
  /**
883
949
  * TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
884
950
  * answer, and the next rung then tries.
@@ -909,9 +975,11 @@ const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string
909
975
  const declared = termIn(node.contract?.attributes, name);
910
976
  if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
911
977
  if (declared?.collect !== undefined) return collectIn(node, name, declared.collect);
912
- return declared?.default === undefined ? undefined : equationOf(node, declared.default, {});
978
+ if (declared?.default !== undefined) return equationOf(node, declared.default, {});
979
+ return declared?.higher === undefined ? undefined : higherOf(node, name, declared.higher);
913
980
  },
914
981
  ],
982
+ ["forward", forwardOf],
915
983
  ];
916
984
 
917
985
  /** TREE.LADDER: the answer of the first rung that answers, or `undefined`. */
@@ -1069,6 +1137,53 @@ function attributeOf(node: RefNode, name: string): Result | undefined {
1069
1137
  return result;
1070
1138
  }
1071
1139
 
1140
+ /** TREE.AT: one instance of an attribute with parameters of a node. Its result is `undefined` while it evaluates. */
1141
+ interface RefInstance {
1142
+ readonly attr: string;
1143
+ readonly names: unknown;
1144
+ result: Result | undefined;
1145
+ }
1146
+
1147
+ /** TREE.AT: the instances of each node of the reference. Two arguments are the same instance by DOC.EQUALS. */
1148
+ const refInstances = /* @__PURE__ */ new WeakMap<RefNode, RefInstance[]>();
1149
+
1150
+ /**
1151
+ * TREE.AT: the instance of an attribute with parameters, or `undefined` when the kind has no entry in `tree.at`. The
1152
+ * equation sees the arguments as names. A demand of an instance in evaluation gives `#CYCLE!`, also in a fixed point.
1153
+ */
1154
+ function instanceOf(node: RefNode, attr: string, args: unknown): Result | undefined {
1155
+ const declared = termIn(node.contract?.at, attr);
1156
+ if (declared === undefined) return undefined;
1157
+ const rule = atRuleOf(declared);
1158
+ if (!rule.ok) return fail("#VALUE!", `The attribute ${nameOfAttribute(node, attr)} has a bad entry tree.at. ${rule.reason}`);
1159
+ const given = atArgsOf(rule.params, args);
1160
+ if (!given.ok) return fail("#VALUE!", `The step tree.at of ${nameOfAttribute(node, attr)} has bad arguments. ${given.reason}`);
1161
+ const name = `${attr} with ${given.key}`;
1162
+ const list = refInstances.get(node) ?? [];
1163
+ refInstances.set(node, list);
1164
+ const found = list.find((item) => item.attr === attr && docEquals(item.names, given.names));
1165
+ if (found?.result !== undefined) return found.result;
1166
+ if (found !== undefined) {
1167
+ const from = inEvaluation.findIndex((entry) => entry.node === node && entry.name === name);
1168
+ const steps = inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name));
1169
+ return fail("#CYCLE!", `The attributes form a cycle: ${[...steps, nameOfAttribute(node, name)].join(" -> ")}.`);
1170
+ }
1171
+ const item: RefInstance = { attr, names: given.names, result: undefined };
1172
+ list.push(item);
1173
+ const entry = { node, name, circular: false, tentative: false };
1174
+ inEvaluation.push(entry);
1175
+ let result: Result | undefined;
1176
+ try {
1177
+ result = equationOf(node, rule.equation, given.names);
1178
+ } finally {
1179
+ inEvaluation.pop();
1180
+ // A result that read an approximation is not kept (TREE.CIRCULAR).
1181
+ if (result === undefined || entry.tentative) list.splice(list.indexOf(item), 1);
1182
+ else item.result = result;
1183
+ }
1184
+ return result;
1185
+ }
1186
+
1072
1187
  /** TREE.RESULT: the JSON of a node. `open` holds the nodes and the containers whose JSON is in progress. */
1073
1188
  function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
1074
1189
  const record: Record<string, unknown> = {};
@@ -1157,6 +1272,7 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
1157
1272
  };
1158
1273
  node.thing = self;
1159
1274
  refNodes.set(self, node);
1275
+ AT_READERS.set(self, (attr, args) => instanceOf(node, attr, args));
1160
1276
  if (nested) return ok(self);
1161
1277
  try {
1162
1278
  return jsonOfNode(node, self, []);
@@ -1285,13 +1401,22 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
1285
1401
  }
1286
1402
  const names = params as readonly string[];
1287
1403
  const body = partNamed(parts, "body") as Part;
1404
+ // FORMULA.CALL-DEPTH: the evaluation that makes the closure counts its applications. A quarter of maxDepth is the limit.
1405
+ const clock = activeEnvs[activeEnvs.length - 1] ?? { applying: 0, maxDepth: 1000 };
1406
+ const limit = Math.ceil(clock.maxDepth / 4);
1288
1407
  const closure = (...args: readonly unknown[]): unknown => {
1289
- const scope: Record<string, unknown> = {};
1290
- for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
1291
- names.forEach((name, index) => setField(scope, name, args[index]));
1292
- const result = body.evaluate({ ...context, names: scope });
1293
- if (!result.ok) throw new Failure(result.error);
1294
- return result.value;
1408
+ if (clock.applying >= limit) throw new Failure(fail("#DEPTH!", `The applications of functions nest deeper than ${limit} levels.`).error);
1409
+ clock.applying += 1;
1410
+ try {
1411
+ const scope: Record<string, unknown> = {};
1412
+ for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
1413
+ names.forEach((name, index) => setField(scope, name, args[index]));
1414
+ const result = body.evaluate({ ...context, names: scope });
1415
+ if (!result.ok) throw new Failure(result.error);
1416
+ return result.value;
1417
+ } finally {
1418
+ clock.applying -= 1;
1419
+ }
1295
1420
  };
1296
1421
  Object.defineProperty(closure, CLOSURE, { value: { params: names } });
1297
1422
  return ok(closure);