@mark1russell7/coeval 0.17.0 → 0.19.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/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.17.0",
4
+ "version": "0.19.0",
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",
@@ -15,7 +15,8 @@
15
15
  "files": [
16
16
  "build",
17
17
  "src",
18
- "!src/**/*.test.ts"
18
+ "!src/**/*.test.ts",
19
+ "!src/**/*.test-support.ts"
19
20
  ],
20
21
  "sideEffects": false,
21
22
  "license": "MIT",
package/src/codec.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Json, isEnvelope, isRecord } from "./doc.ts";
2
2
  import { type Result, describe, fail, ok } from "./errors.ts";
3
3
  import { type Machine, makeContext, prepare } from "./evaluate.ts";
4
- import { type Domain, type DomainCodec, domainOf, member } from "./types.ts";
4
+ import { type Domain, type DomainCodec, domainOf, lineOf, member } from "./types.ts";
5
5
 
6
6
  /*
7
7
  * The codec of the values that JSON cannot write (K140, D40). A document holds such a value as a standard extension:
@@ -71,7 +71,7 @@ function setOwn(target: Record<string, Json>, key: string, value: Json): void {
71
71
  /** The state of one call of `encodeValue`. */
72
72
  interface Encoder {
73
73
  readonly domains: Domains;
74
- /** The domains with a codec, in the order of their names. */
74
+ /** The domains with a codec, in the order of `codecOrder`. */
75
75
  readonly codecs: readonly (readonly [string, DomainCodec])[];
76
76
  /** The answer of `isJson` for each container that it examined. */
77
77
  readonly json: WeakMap<object, boolean>;
@@ -151,7 +151,8 @@ function encodeTerm(value: unknown, encoder: Encoder): Json {
151
151
  *
152
152
  * - A value that JSON writes exactly stays as it is. A literal with the shape of an envelope goes into `lit`.
153
153
  * - `NaN`, an infinity and `-0` become `ext.number`, and `undefined` becomes `ext.undefined`.
154
- * - A member of a domain with a codec becomes `ext.domain`. The domains go in the order of their names.
154
+ * - A member of a domain with a codec becomes `ext.domain`. The codec of a domain under another domain comes first
155
+ * (EXT.SPECIFIC, D109). The order of the names decides between unrelated domains.
155
156
  * - A list or a plain record with such a value becomes the construct `list` or `record`.
156
157
  *
157
158
  * @param machine - The machine, or only its domains.
@@ -161,15 +162,34 @@ function encodeTerm(value: unknown, encoder: Encoder): Json {
161
162
  */
162
163
  export function encodeValue(machine: Pick<Machine, "domains">, value: unknown): Json {
163
164
  const domains = machine.domains;
165
+ return encodeTerm(value, { domains, codecs: codecOrder(domains), json: new WeakMap(), open: new Set() });
166
+ }
167
+
168
+ /**
169
+ * This function gives the domains with a codec in the order of `encodeValue` (EXT.SPECIFIC, D109). The depth of a
170
+ * domain is the count of the other domains with a codec in its line. A deeper domain comes first, and the order of the
171
+ * names decides between domains of the same depth (EXT.TIE). Thus a domain comes before each domain with a codec above it.
172
+ */
173
+ function codecOrder(domains: Domains): readonly (readonly [string, DomainCodec])[] {
164
174
  const codecs: (readonly [string, DomainCodec])[] = [];
165
- if (domains !== undefined) {
166
- for (const name of Object.keys(domains).sort()) {
167
- const domain = domainOf(name, domains);
168
- const codec = domain === undefined ? undefined : codecOf(domain);
169
- if (codec !== undefined) codecs.push([name, codec]);
170
- }
175
+ if (domains === undefined) return codecs;
176
+ let parents = false;
177
+ for (const name of Object.keys(domains).sort()) {
178
+ const domain = domainOf(name, domains);
179
+ const codec = domain === undefined ? undefined : codecOf(domain);
180
+ if (domain === undefined || codec === undefined) continue;
181
+ codecs.push([name, codec]);
182
+ if (Array.isArray(domain.under)) parents = true;
183
+ }
184
+ // Without a parent of a domain with a codec, each depth is zero, thus the order of the names stays.
185
+ if (!parents) return codecs;
186
+ const depth = new Map<string, number>();
187
+ for (const [name] of codecs) {
188
+ const line = lineOf(name, domains);
189
+ depth.set(name, codecs.filter(([other]) => other !== name && line.has(other)).length);
171
190
  }
172
- return encodeTerm(value, { domains, codecs, json: new WeakMap(), open: new Set() });
191
+ // The sort is stable, thus the order of the names stays between domains of the same depth.
192
+ return codecs.sort(([a], [b]) => (depth.get(b) as number) - (depth.get(a) as number));
173
193
  }
174
194
 
175
195
  /**
package/src/conform.ts CHANGED
@@ -33,7 +33,7 @@ import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
33
33
  import { STD_EVALUATORS } from "./stdlib.ts";
34
34
  import { sameValue } from "./agree.ts";
35
35
  import { declaredTruthOf } from "./formula.ts";
36
- import { membersOf, typeIssues, typeText } from "./types.ts";
36
+ import { falseCovers, membersOf, typeIssues, typeText } from "./types.ts";
37
37
  import { type Random, random } from "./generate.ts";
38
38
  import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effectsOfVerb, isKind, isNamespace, lookupKind } from "./world.ts";
39
39
 
@@ -60,8 +60,10 @@ import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effe
60
60
  * - `agree`: compiled code gives the result of its definition for the reference interpreter.
61
61
  * - `truth`: the own `truthy` of a formula profile gives `true` or `false` for each member of its `truthType` (D86).
62
62
  * Only `formula.if`, `formula.and` and `formula.or` of the formula kit get this check.
63
+ * - `cover`: each sample of a covered domain of the machine is in one domain of each cover of the domain (D94). Each
64
+ * evaluator and each runner gets this check, because `fits` trusts the covers for each program.
63
65
  */
64
- export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree" | "truth";
66
+ export type Obligation = "C1" | "C2" | "C3" | "C4" | "C5" | "C6" | "C7" | "C8" | "local" | "agree" | "truth" | "cover";
65
67
 
66
68
  /** The probe of a finding: the document, the result of each probe part, the capabilities and the handler. */
67
69
  export interface ConformProbe {
@@ -136,7 +138,7 @@ export interface ConformOptions {
136
138
  }
137
139
 
138
140
  /** The order of the obligations in the list of findings. */
139
- const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree", "truth"];
141
+ const ORDER: readonly Obligation[] = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "local", "agree", "truth", "cover"];
140
142
 
141
143
  /** A function in the results of the probe parts. It gives the count of its arguments. */
142
144
  const PROBE_FN = (...args: readonly unknown[]): number => args.length;
@@ -1032,7 +1034,8 @@ function examine(env: Env, sample: Sample, contract: Contract): void {
1032
1034
  /**
1033
1035
  * This function checks the declared truth of a profile (D86). The function `truthy` must give `true` or `false` for
1034
1036
  * each member of `truthType`, and it must not throw. The members come from the lists `values` of the domains of the
1035
- * type. A type without such a list gets the values of the results of the palette that are its members.
1037
+ * type. A type without such a list gets its members from the results of the palette and from the samples of the
1038
+ * domains (D99).
1036
1039
  */
1037
1040
  function checkTruth(env: Env, code: unknown, probe: (value: unknown) => ConformProbe): void {
1038
1041
  const truth = declaredTruthOf(code);
@@ -1057,6 +1060,27 @@ function checkTruth(env: Env, code: unknown, probe: (value: unknown) => ConformP
1057
1060
  }
1058
1061
  }
1059
1062
 
1063
+ /**
1064
+ * This function checks the covers of the domains of the machine from samples (D94). Each sample of a covered domain
1065
+ * must be in one domain of each valid cover. The samples of a domain come from its list `values`. A domain without
1066
+ * that list gets its members from the palette and from the samples of the domains, as for the check `truth`.
1067
+ */
1068
+ function checkCovers(env: Env, probe: (value: unknown) => ConformProbe): void {
1069
+ const domains = env.machine.domains;
1070
+ if (domains === undefined) return;
1071
+ const palette = env.results.flatMap((result) => (result.ok ? [result.value] : []));
1072
+ const samples: Record<string, readonly unknown[]> = {};
1073
+ for (const [name, domain] of Object.entries(domains)) if (domain.covers !== undefined) samples[name] = membersOf(name, domains, palette);
1074
+ const found = falseCovers(domains, samples)[0];
1075
+ if (found === undefined) return;
1076
+ report(
1077
+ env,
1078
+ "cover",
1079
+ `The member ${valueText(found.value)} of the domain ${found.domain} is in no domain of its cover ${found.cover.join(", ")}. The check of types trusts the cover, thus it is unsound for ${found.domain}.`,
1080
+ () => probe(found.value),
1081
+ );
1082
+ }
1083
+
1060
1084
  /**
1061
1085
  * This function examines one evaluator or runner of a machine against the obligations of host code (K92, D36). It
1062
1086
  * starts the code directly, without the kernel, with probe parts and a probe runtime. It generates probe documents from
@@ -1067,7 +1091,8 @@ function checkTruth(env: Env, code: unknown, probe: (value: unknown) => ConformP
1067
1091
  * compiled code with its definition. The finding of an obligation names the first probe that breaks it. An empty list
1068
1092
  * means that each probe conforms.
1069
1093
  *
1070
- * The check `truth` tests the declared truth of a formula profile (D86).
1094
+ * The check `truth` tests the declared truth of a formula profile (D86). The check `cover` tests the covers of the
1095
+ * domains of the machine from samples (D94).
1071
1096
  *
1072
1097
  * @param machine - The machine with the code and the world of the kinds.
1073
1098
  * @param name - The name of the evaluator or of the runner. A runner wins over an evaluator of the same name.
@@ -1110,11 +1135,12 @@ export function conforms(machine: Machine, name: string, options: ConformOptions
1110
1135
  examine(env, sample, contract);
1111
1136
  }
1112
1137
  }
1113
- // A probe of the truth gives the member to each part of the first probe document.
1114
- checkTruth(env, code, (value) =>
1138
+ // A probe of the truth or of a cover gives the value to each part of the first probe document.
1139
+ const probeWith = (value: unknown): ConformProbe =>
1115
1140
  first === undefined
1116
1141
  ? { type: typeName(types[0] as TypeRef), doc: { type: types[0] as TypeRef, value: null }, results: {}, waits: [], caps: NO_CAPS, running: runner }
1117
- : probeOf(first, { results: first.split.parts.map(() => ok(value)), waits: [], caps: NO_CAPS, running: runner }),
1118
- );
1142
+ : probeOf(first, { results: first.split.parts.map(() => ok(value)), waits: [], caps: NO_CAPS, running: runner });
1143
+ checkTruth(env, code, probeWith);
1144
+ checkCovers(env, probeWith);
1119
1145
  return ORDER.filter((obligation) => env.found.has(obligation)).map((obligation) => env.found.get(obligation) as Finding);
1120
1146
  }
package/src/durable.ts CHANGED
@@ -2,7 +2,7 @@ import type { Context } from "./deref.ts";
2
2
  import type { Result } from "./errors.ts";
3
3
  import { type Machine, type Plan, type PrepareOptions, type Read, prepare } from "./evaluate.ts";
4
4
  import { type DocFacts, type FactsOptions, factsOf } from "./facts.ts";
5
- import { type VerifyOptions, verifyReads } from "./reads.ts";
5
+ import { type PrintOptions, type ReadPrints, type VerifyOptions, printReads, verifyReads } from "./reads.ts";
6
6
 
7
7
  /*
8
8
  * Two levels of durability (R4 P7, D30). The plan and the facts of a document depend only on the structure: the
@@ -42,6 +42,11 @@ export interface DurableOptions {
42
42
  readonly volatile?: FactsOptions["volatile"];
43
43
  /** The rule of the host for two things, as in `verifyReads`. */
44
44
  readonly equal?: VerifyOptions["equal"];
45
+ /**
46
+ * The check of changes in place (READS.IN-PLACE, D112). With it, the cache keeps the prints of the reads of each
47
+ * pure document, and a verification compares them. `true` gives the default limit. Without it, the cache makes no prints.
48
+ */
49
+ readonly inPlace?: true | PrintOptions;
45
50
  }
46
51
 
47
52
  /**
@@ -154,10 +159,11 @@ function byDocument<T>(make: (doc: unknown) => T): (doc: unknown) => T {
154
159
  * and each result (DURABLE.STRUCTURE).
155
160
  *
156
161
  * The host keeps two promises. In one data epoch, the context of a key gives the same data. The values of a
157
- * context and the documents do not change in place, as PLAN.CACHE and `verifyReads` say.
162
+ * context and the documents do not change in place, as PLAN.CACHE and `verifyReads` say. The option `inPlace`
163
+ * finds a change in place of a value that a pure document read (READS.IN-PLACE).
158
164
  *
159
165
  * @param machine - The machine of the first structure epoch.
160
- * @param options - The type of a bare root value, the verb, the volatile reads and the rule of the host for two things.
166
+ * @param options - The type of a bare root value, the verb, the volatile reads, the rule of the host and the check of changes in place.
161
167
  * @returns The cache.
162
168
  */
163
169
  export function durable(machine: Machine, options: DurableOptions = {}): Durable {
@@ -167,6 +173,9 @@ export function durable(machine: Machine, options: DurableOptions = {}): Durable
167
173
  };
168
174
  const factsOptions: FactsOptions = { ...prepareOptions, ...(options.volatile === undefined ? {} : { volatile: options.volatile }) };
169
175
  const verifyOptions: VerifyOptions = options.equal === undefined ? {} : { equal: options.equal };
176
+ const printOptions: PrintOptions | undefined = options.inPlace === true ? {} : options.inPlace;
177
+ // The prints of the reads of each entry, only with the option `inPlace`. Thus an entry keeps its shape (PLAN.LEAN).
178
+ const printsOf = printOptions === undefined ? undefined : new WeakMap<Entry, ReadPrints>();
170
179
  const counts = { compiles: 0, facts: 0, evaluations: 0, verifications: 0, reuses: 0 };
171
180
  const results = new Map<unknown, Entry>();
172
181
  let current = machine;
@@ -205,7 +214,8 @@ export function durable(machine: Machine, options: DurableOptions = {}): Durable
205
214
  }
206
215
  if (entry.reads !== undefined) {
207
216
  counts.verifications += 1;
208
- if (verifyReads(plan, entry.reads, context, verifyOptions).same) {
217
+ const prints = printsOf?.get(entry);
218
+ if (verifyReads(plan, entry.reads, context, prints === undefined ? verifyOptions : { ...verifyOptions, prints }).same) {
209
219
  entry.data = epochs.data;
210
220
  counts.reuses += 1;
211
221
  return entry.result;
@@ -217,7 +227,9 @@ export function durable(machine: Machine, options: DurableOptions = {}): Durable
217
227
  counts.evaluations += 1;
218
228
  const result = plan.evaluate(context, reads === undefined ? undefined : { reads });
219
229
  // A list that grows by push keeps room for more reads. A copy of a short list is smaller (PLAN.LEAN).
220
- results.set(key, { plan, result, reads: reads === undefined ? undefined : reads.length === 0 ? NO_READS : reads.slice(), data: epochs.data });
230
+ const made: Entry = { plan, result, reads: reads === undefined ? undefined : reads.length === 0 ? NO_READS : reads.slice(), data: epochs.data };
231
+ results.set(key, made);
232
+ if (printsOf !== undefined && made.reads !== undefined) printsOf.set(made, printReads(made.reads, printOptions));
221
233
  return result;
222
234
  },
223
235
  changeData() {
package/src/evaluate.ts CHANGED
@@ -1053,6 +1053,11 @@ class Session implements Runtime {
1053
1053
  private list: Effect[] | undefined;
1054
1054
  tick = 0;
1055
1055
  nextId = 0;
1056
+ /**
1057
+ * The count of the drives without a wait that are active at this time. In a run, an effect under such a drive does
1058
+ * not reach the host (RUN.HELD).
1059
+ */
1060
+ held = 0;
1056
1061
  current: Building | undefined;
1057
1062
  readonly holder: Building | undefined;
1058
1063
  /**
@@ -1265,6 +1270,13 @@ class Session implements Runtime {
1265
1270
  if ("ok" in made) return made;
1266
1271
  const perform = this.perform;
1267
1272
  if (!this.running || perform === undefined) return this.answer(made, place);
1273
+ if (this.held > 0) {
1274
+ // The parent cannot wait for the answer of the host, thus the host does not get the effect (RUN.HELD).
1275
+ made.outcome = fail("#CALC!", heldReason(made.kind), { place });
1276
+ // The drive stops at this wait, and gives the #CALC! of the runner.
1277
+ yield HELD;
1278
+ return made.outcome;
1279
+ }
1268
1280
  let done: unknown;
1269
1281
  try {
1270
1282
  done = perform(made);
@@ -1452,6 +1464,21 @@ export function waitedReason(kindText: string): string {
1452
1464
  return `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`;
1453
1465
  }
1454
1466
 
1467
+ /**
1468
+ * This function gives the reason of an effect that a run does not give to the host: the parent of its runner cannot
1469
+ * wait (RUN.HELD). The plan and the reference give the same reason.
1470
+ *
1471
+ * @param kind - The kind of the effect.
1472
+ * @returns The reason.
1473
+ */
1474
+ export function heldReason(kind: Cap): string {
1475
+ const text = typeof kind === "string" ? kind : kind.join(".");
1476
+ return `The parent of the runner cannot wait, thus the host does not get the effect ${text}.`;
1477
+ }
1478
+
1479
+ /** The wait of an effect that a run does not give to the host. A drive without a wait stops at it (RUN.HELD). */
1480
+ const HELD: Promise<unknown> = /* @__PURE__ */ Promise.resolve(undefined);
1481
+
1455
1482
  /** This function drives steps that cannot wait. A step that waits stops the steps, and gives `#CALC!`. */
1456
1483
  function drive(steps: Steps, kindText: string): Result {
1457
1484
  const step = steps.next();
@@ -2148,10 +2175,19 @@ function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode
2148
2175
  /**
2149
2176
  * This function gives the code of a runner under a parent that cannot wait: its steps, driven without a wait. An
2150
2177
  * evaluation and a run both drive them (RUN.DRIVE-NESTED). Only a step that really waits gives `#CALC!`, at the place
2151
- * of the runner, where the error started (RUN.WAIT).
2178
+ * of the runner, where the error started (RUN.WAIT). In a run, an effect under the runner does not reach the host,
2179
+ * because the parent cannot wait for its answer (RUN.HELD).
2152
2180
  */
2153
2181
  function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code {
2154
- return (ctx, rt) => placed(drive(run(ctx, rt), kindText), place);
2182
+ return (ctx, rt) => {
2183
+ if (!rt.running || !(rt instanceof Session)) return placed(drive(run(ctx, rt), kindText), place);
2184
+ rt.held += 1;
2185
+ try {
2186
+ return placed(drive(run(ctx, rt), kindText), place);
2187
+ } finally {
2188
+ rt.held -= 1;
2189
+ }
2190
+ };
2155
2191
  }
2156
2192
 
2157
2193
  /**
package/src/facts.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Json, type Key, isEnvelope, isKey } from "./doc.ts";
2
- import { type Kind, type LazyRule, type Side, type World, equationType, lookupKind } from "./world.ts";
2
+ import { type Kind, type KindFacts, type LazyRule, type Side, type World, equationType, lookupKind } from "./world.ts";
3
3
  import { answerOf } from "./ladder.ts";
4
4
  import { pathMatches, splitParts } from "./parts.ts";
5
5
  import { type Root, ROOTS, isRef } from "./deref.ts";
@@ -43,9 +43,12 @@ export interface DocFacts {
43
43
  * here. The roots that a kind declares in `reads` are here too, each one at the place of its document.
44
44
  */
45
45
  readonly reads: readonly StaticRead[];
46
- /** `"run"` when one kind of the document is on the run side (FACTS.SIDE). */
46
+ /** `"run"` when one kind of the document is on the run side for the verb of the facts (FACTS.SIDE, FACTS.VERB-SIDE). */
47
47
  readonly side: Side;
48
- /** The kinds of the effects that the contracts declare, as texts with dots, each one time (FACTS.EFFECTS). */
48
+ /**
49
+ * The kinds of the effects that the contracts declare for the verb of the facts, as texts with dots, each one time
50
+ * (FACTS.EFFECTS, FACTS.VERB-EFFECTS).
51
+ */
49
52
  readonly effects: readonly string[];
50
53
  /**
51
54
  * `true` when each kind of the document declares `pure`, and the document has no effect and is not volatile. Then
@@ -63,8 +66,10 @@ export interface FactsOptions {
63
66
  /** The type of a bare root value. */
64
67
  readonly declaredType?: string;
65
68
  /**
66
- * The verb whose evaluators the facts describe, as in `prepare`. The default is `eval`. The contract of a kind
67
- * describes the code of `eval` only. For another verb, only the hook `facts` describes the code (FACTS.VERB).
69
+ * The verb whose evaluators the facts describe, as in `prepare`. The default is `eval`. The marks `pure`,
70
+ * `volatile` and `lazy` of a contract describe the code of `eval` only. For another verb, only the hook `facts`
71
+ * describes the code (FACTS.VERB). The side and the effects come from the entry of the verb in `verbEffects`
72
+ * (FACTS.VERB-EFFECTS, D111).
68
73
  */
69
74
  readonly verb?: string;
70
75
  /**
@@ -92,7 +97,7 @@ function tight<T>(list: readonly T[]): readonly T[] {
92
97
  }
93
98
 
94
99
  /** What a document gives to its parts: its own lazy mark, and the lazy rules of its kind for the parts. */
95
- interface Above {
100
+ export interface Above {
96
101
  readonly lazy: boolean;
97
102
  readonly rules: readonly LazyRule[] | typeof EACH | undefined;
98
103
  /** The position of each part among the parts that a rule `after` covers. It is made at the first need. */
@@ -136,7 +141,7 @@ function positionOf(above: Above, parent: DocVisit, rule: LazyRule, slot: readon
136
141
  }
137
142
 
138
143
  /** This function tells if the lazy rules of a document cover one of its parts. */
139
- function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean {
144
+ export function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean {
140
145
  const rules = above.rules;
141
146
  if (rules === undefined) return false;
142
147
  if (rules === EACH) return true;
@@ -148,6 +153,32 @@ function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean
148
153
  return false;
149
154
  }
150
155
 
156
+ /** The code of a document for the facts: the answer of the hook, and the lazy rules that the code gives its parts. */
157
+ export interface CodeFacts {
158
+ /** `true` when the contract of the kind describes the code. */
159
+ readonly described: boolean;
160
+ /** The answer of the hook `facts` of the machine for the code. */
161
+ readonly answer: KindFacts | undefined;
162
+ /** The lazy rules of the parts (FACTS.LAZY). Code that the contract and the hook do not describe makes each part lazy. */
163
+ readonly rules: Above["rules"];
164
+ }
165
+
166
+ /**
167
+ * This function gives the code of a document of a kind for the facts (FACTS.LAZY). `factsOf` and the graph of
168
+ * attribute instances read the lazy rules with this one function (D107).
169
+ *
170
+ * @param machine - The world and the hook `facts`.
171
+ * @param v - The visit of the document.
172
+ * @param kind - The kind of the document.
173
+ * @param verb - The verb whose code the facts describe.
174
+ * @returns The answer of the hook and the lazy rules.
175
+ */
176
+ export function codeFactsOf(machine: Machine, v: DocVisit, kind: Kind, verb: string): CodeFacts {
177
+ const code = codeOf(machine.world, v.declared ? undefined : v.doc.own, kind, verb, "eval");
178
+ const answer = code.name === undefined ? undefined : machine.facts?.(v.typePath ?? [], code.name);
179
+ return { described: code.described, answer, rules: code.described ? (answer?.lazy ?? kind.lazy) : (answer?.lazy ?? EACH) };
180
+ }
181
+
151
182
  /**
152
183
  * This function gives the static facts of a document, for a schedule (R4 P2, D24). It walks the document one time,
153
184
  * by the contracts of its kinds:
@@ -157,7 +188,7 @@ function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean
157
188
  * declares `reads` adds a read of each of its roots at each document of the kind. The first document of a tree
158
189
  * kind also gets the lazy reads of the equations of its kind (FACTS.EQUATIONS). A read from `self` or from a name
159
190
  * of a rung is not one of them, because it is not free (READS.EQUATION).
160
- * - `side` and `effects`: from the contract of each kind.
191
+ * - `side` and `effects`: from the contract of each kind, for the verb of the facts.
161
192
  * - `pure`: each kind declares `pure`, also in an equation, and the document has no effect and is not volatile.
162
193
  * - `volatile`: a kind, an operation or a step document declares `volatile`, or the host marks a read.
163
194
  *
@@ -166,9 +197,11 @@ function partLazy(above: Above, parent: DocVisit, slot: readonly Key[]): boolean
166
197
  * entry. Thus a document with an own entry for another evaluator is not pure, and each of its parts is lazy. The
167
198
  * hook can still answer for that evaluator.
168
199
  *
169
- * The option `verb` names the verb whose code the facts describe (FACTS.VERB). A contract describes the code of
170
- * `eval`, thus the rule of an own entry applies to each document for another verb. The side and the effects come
171
- * from the contracts for each verb.
200
+ * The option `verb` names the verb whose code the facts describe (FACTS.VERB). The marks of a contract describe the
201
+ * code of `eval`, thus the rule of an own entry applies to each document for another verb. For another verb, the
202
+ * effects are the effects that the entry of the verb in `verbEffects` declares (FACTS.VERB-EFFECTS, D111). A kind is
203
+ * on the run side for that verb when its entry declares an effect (FACTS.VERB-SIDE). A kind without an entry for the
204
+ * verb adds no effect, as in `check` (FACTS.VERB-UNDECLARED). Its code is still not pure without an answer of the hook.
172
205
  *
173
206
  * A read is one resolution of its path. A reference that the resolution reaches and follows is a part of that read
174
207
  * (DEREF.COLLAPSE), not a separate read. The facts depend only on the document and the machine. Thus a host computes
@@ -220,17 +253,15 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
220
253
  above.set(v, { lazy, rules: undefined });
221
254
  return undefined;
222
255
  }
223
- const code = codeOf(world, v.declared ? undefined : v.doc.own, kind, verb, "eval");
224
- const answer = code.name === undefined ? undefined : hook?.(v.typePath ?? [], code.name);
256
+ const { described, answer, rules } = codeFactsOf(machine, v, kind, verb);
257
+ above.set(v, { lazy, rules });
225
258
  let roots: readonly Root[] | undefined;
226
- if (code.described) {
259
+ if (described) {
227
260
  note(answer?.pure ?? kind.pure, answer?.volatile ?? kind.volatile);
228
- above.set(v, { lazy, rules: answer?.lazy ?? kind.lazy });
229
261
  roots = answer?.reads ?? kind.reads;
230
262
  } else {
231
263
  // The contract of the kind does not describe this code. Only the hook can.
232
264
  note(answer?.pure, answer?.volatile);
233
- above.set(v, { lazy, rules: answer?.lazy ?? EACH });
234
265
  roots = answer?.reads;
235
266
  }
236
267
  // The code of the kind resolves each of these roots as a whole, at the place of its document (FACTS.KIND-READS).
@@ -239,13 +270,18 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
239
270
  if (node !== undefined && root === "self") continue;
240
271
  add({ root, keys: NONE, exact: true, lazy, address: false, path: [root], place: tight(place.place), at: tight(place.at) });
241
272
  }
273
+ // check and effectsOf read the declared effects with the same function (D27). The verb of the facts selects the
274
+ // declaration: effects for eval, and the entry of verbEffects for another verb (D49, D111).
275
+ const declared = declaredEffects(v, verb);
276
+ // The field side gives the side of eval. For another verb, a declared effect needs a runner, as in check, thus
277
+ // the run side (FACTS.VERB-SIDE).
278
+ const runs = verb === "eval" ? kind.side === "run" : declared.length > 0;
242
279
  // An equation evaluates without capabilities, thus an effect in it never reaches the host. It is still not pure.
243
- if (kind.side === "run") {
280
+ if (runs) {
244
281
  if (node === undefined) side = "run";
245
282
  else pure = false;
246
283
  }
247
- // check and effectsOf read the declared effects with the same function (D27).
248
- for (const keys of declaredEffects(v)) {
284
+ for (const keys of declared) {
249
285
  if (node !== undefined) {
250
286
  pure = false;
251
287
  continue;
package/src/formula.ts CHANGED
@@ -5,7 +5,7 @@ import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } f
5
5
  import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled, totalCode } from "./evaluate.ts";
6
6
  import { type MachinePart, mergeMachines } from "./merge.ts";
7
7
  import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeIssues, typeText } from "./types.ts";
8
- import type { PartType, TypeInput, TypeRule } from "./typing.ts";
8
+ import type { Declared, PartType, TypeInput, TypeRule } from "./typing.ts";
9
9
  import { decodeDomain, domainTypeOf } from "./codec.ts";
10
10
  import { type StaticRead, factsOf } from "./facts.ts";
11
11
 
@@ -31,6 +31,8 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
31
31
  * - `lazy`: the operations that evaluate their arguments themselves.
32
32
  * - `ext`: the extensions of the namespace `ext`, by their name. The kit has `ext.undefined`, `ext.number` and
33
33
  * `ext.domain`.
34
+ * - `extTypes`: the value type of each extension of `ext`, by its name, for a check of types (TYPE.EXT, D103). An
35
+ * extension without an entry gives `unknown` (TYPE.GRADUAL). An entry for a standard extension has no effect.
34
36
  * - `truthy`: the truth of a value for `if`, `and` and `or`. `undefined` is an error `#VALUE!`. The default
35
37
  * accepts only `true` and `false`.
36
38
  * - `truthType`: the value type of a truth for the own `truthy`, for a check of types (TYPE.TRUTH, D81). A test of
@@ -54,12 +56,13 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
54
56
  *
55
57
  * A check of types trusts the signatures. Each operation must give a member of the result of each signature that
56
58
  * its arguments match. The hook `check` must keep each result in that type. The function `truthy` must give a truth
57
- * for each member of `truthType`.
59
+ * for each member of `truthType`. Each extension must give a member of its type in `extTypes`.
58
60
  */
59
61
  export interface FormulaProfile {
60
62
  readonly ops?: Readonly<Record<string, OpFn>> | ((name: string) => OpFn | undefined);
61
63
  readonly lazy?: Readonly<Record<string, LazyOp>>;
62
64
  readonly ext?: Readonly<Record<string, ExtFn>>;
65
+ readonly extTypes?: Readonly<Record<string, ValueType>>;
63
66
  readonly truthy?: (value: unknown) => boolean | undefined;
64
67
  readonly truthType?: ValueType;
65
68
  readonly otherwise?: unknown;
@@ -232,6 +235,12 @@ function isSignature(value: unknown): value is Signature {
232
235
  return typeof value === "object" && value !== null && Array.isArray((value as { readonly params?: unknown }).params) && Object.hasOwn(value, "to");
233
236
  }
234
237
 
238
+ /** This function tells if two signatures have the same parameters and the same rest parameter. */
239
+ function sameParams(a: Signature, b: Signature): boolean {
240
+ if (!docEquals(a.params, b.params)) return false;
241
+ return a.rest === undefined ? b.rest === undefined : b.rest !== undefined && docEquals(a.rest, b.rest);
242
+ }
243
+
235
244
  /** This function gives the text of a signature, for the reason of an issue. */
236
245
  function signatureText(sig: Signature): string {
237
246
  const params = sig.params.map(typeText);
@@ -272,7 +281,7 @@ function paramsOf(value: unknown): readonly string[] {
272
281
  /**
273
282
  * This function makes the type rules of the formula kit for one profile (TYPE.SYNTH, D25). Each evaluator of the kit
274
283
  * holds its rule. The rules read the rules of the profile that change a type: `truthy`, `truthType`, `logic`,
275
- * `otherwise`, `omit` and `sigs`.
284
+ * `otherwise`, `omit`, `sigs` and `extTypes`.
276
285
  */
277
286
  function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule>> {
278
287
  // The default truth needs a truth value. An own truth needs its declared type, or nothing without a declaration (D81).
@@ -322,6 +331,32 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
322
331
  }
323
332
  return { sigs, bad: sigs.length !== raw.length };
324
333
  };
334
+ /**
335
+ * The declarations of the signatures, for `checkWorld` (TYPE.WORLD, D110). Only a record of signatures lists them.
336
+ * An overload with the parameters of an earlier overload is a problem: the earlier one always fits first.
337
+ */
338
+ const sigsDeclared = (): readonly Declared[] => {
339
+ const sigs = profile.sigs;
340
+ if (sigs === undefined || typeof sigs === "function") return [];
341
+ const out: Declared[] = [];
342
+ for (const name of Object.keys(sigs)) {
343
+ const raw: unknown = sigs[name];
344
+ const list: readonly unknown[] = Array.isArray(raw) ? raw : [raw];
345
+ list.forEach((sig, index) => {
346
+ const at: readonly Key[] = Array.isArray(raw) ? ["sigs", name, index] : ["sigs", name];
347
+ if (!isSignature(sig)) {
348
+ out.push({ at, problem: `A signature of ops.${name} is not a record with params and to. The check does not read it.` });
349
+ return;
350
+ }
351
+ sig.params.forEach((param, position) => out.push({ at: [...at, "params", position], type: param, what: `parameter ${position + 1} of a signature of ops.${name}` }));
352
+ if (sig.rest !== undefined) out.push({ at: [...at, "rest"], type: sig.rest, what: `rest parameter of a signature of ops.${name}` });
353
+ out.push({ at: [...at, "to"], type: sig.to, what: `result of a signature of ops.${name}` });
354
+ const earlier = list.slice(0, index).findIndex((other) => isSignature(other) && sameParams(other, sig));
355
+ if (earlier >= 0) out.push({ at, problem: `The overload ${index + 1} of ops.${name} has the parameters of the overload ${earlier + 1}. Thus the check never selects it.` });
356
+ });
357
+ }
358
+ return out;
359
+ };
325
360
  const logic = (word: "and" | "or"): TypeRule => ({
326
361
  part: (input, path) => truthPart(input, `term ${Number(path[0]) + 1} of ${word}`),
327
362
  synth: (input) => {
@@ -453,6 +488,8 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
453
488
  },
454
489
 
455
490
  "formula.if": {
491
+ // The rule of if declares the truth type of the profile, thus one bad truth type gives one issue in checkWorld.
492
+ declared: () => (declared ? [{ at: ["truthType"], type: truth, what: "truth type of the profile" }] : []),
456
493
  part: (input, path) => {
457
494
  if (path[0] === "test") return truthPart(input, "test of if");
458
495
  return isUnknown(input.expected, input.machine.domains) ? {} : { expect: input.expected, what: `${String(path[0])} branch of if` };
@@ -468,6 +505,7 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
468
505
  "formula.or": logic("or"),
469
506
 
470
507
  "formula.op": {
508
+ declared: sigsDeclared,
471
509
  part: (input, path) => {
472
510
  const read = signatures(input);
473
511
  // Only one signature gives the types of the arguments before their check. Overloads select after it.
@@ -507,10 +545,26 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
507
545
  },
508
546
 
509
547
  "formula.ext": {
548
+ declared: () => {
549
+ const types = profile.extTypes;
550
+ const own = profile.ext;
551
+ if (types === undefined || own === undefined) return [];
552
+ // Only the type of an own extension counts. An entry for a standard extension has no effect (D103).
553
+ return Object.keys(types)
554
+ .filter((name) => Object.hasOwn(own, name))
555
+ .map((name) => ({ at: ["extTypes", name], type: types[name], what: `type of ext.${name} in extTypes` }));
556
+ },
510
557
  synth: (input) => {
511
558
  const name = nameOf(input);
512
559
  const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name);
513
- if (own) return "unknown";
560
+ if (own) {
561
+ // The declared type is a contract. A type with a problem gives unknown, thus one problem gives one issue (D103).
562
+ const types = profile.extTypes;
563
+ const declared = types !== undefined && Object.hasOwn(types, name) ? types[name] : undefined;
564
+ if (declared === undefined) return "unknown";
565
+ const type = input.contract(declared, `type of ext.${name} in extTypes`);
566
+ return typeIssues(type, input.machine.domains).length > 0 ? "unknown" : type;
567
+ }
514
568
  // The standard extensions give the values that JSON cannot write, thus each has a base type (D48).
515
569
  if (name === "undefined") return "undefined";
516
570
  if (name === "number") return "number";
@@ -580,6 +634,7 @@ const PROFILE_KEYS = /* @__PURE__ */ Object.freeze([
580
634
  "ops",
581
635
  "lazy",
582
636
  "ext",
637
+ "extTypes",
583
638
  "truthy",
584
639
  "truthType",
585
640
  "otherwise",