@mark1russell7/coeval 0.25.0 → 0.26.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.25.0",
4
+ "version": "0.26.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",
package/src/agree.ts CHANGED
@@ -5,8 +5,9 @@ import { type CheckOptions, typeCheck } from "./static.ts";
5
5
  import { isPrecise, member, typeText } from "./types.ts";
6
6
  import { boundariesOf } from "./typing.ts";
7
7
  import { type Effect, type Machine, type Perform, type Read, type TraceNode, JOURNAL, evaluate, makeContext, prepare, run, sharedCopy } from "./evaluate.ts";
8
- import { Held, isClosure } from "./formula.ts";
8
+ import { type FormulaProfile, Held, isClosure } from "./formula.ts";
9
9
  import { type Definitions, reference, referenceRun } from "./reference.ts";
10
+ import { stepper } from "./stepper.ts";
10
11
  import { type ReplaySettle, recorder, relay, replayer } from "./replay.ts";
11
12
 
12
13
  /**
@@ -21,7 +22,7 @@ import { type ReplaySettle, recorder, relay, replayer } from "./replay.ts";
21
22
  * each such name, and then with the context of `agree`. `agreeRun` does not have this tier.
22
23
  * - `other`: the second tier of a language, from the option `other`.
23
24
  */
24
- export type Tier = "reference" | "traced" | "again" | "folds" | "fixed" | "other";
25
+ export type Tier = "reference" | "traced" | "again" | "folds" | "fixed" | "stepper" | "other";
25
26
 
26
27
  /** What a second tier gives: its result, and its effects when it records them. */
27
28
  export interface TierResult {
@@ -51,9 +52,11 @@ export interface AgreeOptions {
51
52
  readonly definitions?: Definitions;
52
53
  /**
53
54
  * The tiers to compare with the plan. The default is the reference, the traced evaluation, a second evaluation, the
54
- * plan with the other choice of folds and the plan with fixed names.
55
+ * plan with the other choice of folds and the plan with fixed names. The tier `stepper` is not a default.
55
56
  */
56
57
  readonly tiers?: readonly Exclude<Tier, "other">[];
58
+ /** The profile of the formula kit, for the tier `stepper`. */
59
+ readonly profile?: FormulaProfile;
57
60
  /**
58
61
  * This function gives the result of the second tier of a language, for example a compiler to loops.
59
62
  *
@@ -290,6 +293,8 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
290
293
  let otherEffects: Effect[] = [];
291
294
  if (tier === "reference") {
292
295
  other = reference(machine, doc, context, { ...declared, ...mock, effects: otherEffects, ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
296
+ } else if (tier === "stepper") {
297
+ other = stepper(machine, doc, context, { ...declared, ...mock, effects: otherEffects, ...(options.profile === undefined ? {} : { profile: options.profile }), ...(options.definitions === undefined ? {} : { definitions: options.definitions }) });
293
298
  } else if (tier === "traced") {
294
299
  const traced = evaluate(machine, doc, context, { ...declared, ...mock });
295
300
  other = traced.result;
@@ -509,8 +514,8 @@ export async function agreeRun(machine: Machine, doc: unknown, context: Context
509
514
  const replay = replayer(log);
510
515
 
511
516
  for (const tier of options.tiers ?? DEFAULT_TIERS) {
512
- // A run has no tier with fixed names (PLAN.FIXED).
513
- if (tier === "fixed") continue;
517
+ // A run has no tier with fixed names (PLAN.FIXED), and the stepper does no runs.
518
+ if (tier === "fixed" || tier === "stepper") continue;
514
519
  const tierReplay = watched(replay);
515
520
  const host = { perform: tierReplay.perform };
516
521
  let other: Result;
package/src/facts.ts CHANGED
@@ -185,7 +185,8 @@ export function codeFactsOf(machine: Machine, v: DocVisit, kind: Kind, verb: str
185
185
  *
186
186
  * - `reads`: each free reference, with the root and the keys of its path. A read under a lazy part has the mark
187
187
  * `lazy`, for example a read in a branch of `if`. A read after a step document has `exact: false`. A kind that
188
- * declares `reads` adds a read of each of its roots at each document of the kind. The first document of a tree
188
+ * declares `reads` adds a read of each of its roots at each document of the kind. A reference with a step document
189
+ * of the kind also has these reads (FACTS.STEP-READS). The first document of a tree
189
190
  * kind also gets the lazy reads of the equations of its kind (FACTS.EQUATIONS). A read from `self` or from a name
190
191
  * of a rung is not one of them, because it is not free (READS.EQUATION).
191
192
  * - `side` and `effects`: from the contract of each kind, for the verb of the facts.
@@ -232,6 +233,18 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
232
233
  reads.push(read);
233
234
  if (options.volatile?.(read) === true) volatile = true;
234
235
  };
236
+ /**
237
+ * This function adds a read of each declared root at the place of a document (FACTS.KIND-READS, FACTS.STEP-READS).
238
+ * It is one function for the facts, thus a visit makes no new function.
239
+ */
240
+ const declare = (list: readonly Root[] | undefined, node: DocVisit | undefined, lazy: boolean, place: DocVisit): void => {
241
+ if (list === undefined) return;
242
+ for (const root of list) {
243
+ // In an equation, the root self is the node, thus its read is not free (READS.EQUATION).
244
+ if (node !== undefined && root === "self") continue;
245
+ add({ root, keys: NONE, exact: true, lazy, address: false, path: [root], place: tight(place.place), at: tight(place.at) });
246
+ }
247
+ };
235
248
  /**
236
249
  * This function walks a document. Without `node`, it walks the document of the facts. With `node`, it walks an
237
250
  * equation of a tree kind, and each read gets the place of that node: the first document of the kind.
@@ -265,11 +278,7 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
265
278
  roots = answer?.reads;
266
279
  }
267
280
  // The code of the kind resolves each of these roots as a whole, at the place of its document (FACTS.KIND-READS).
268
- for (const root of roots ?? NONE) {
269
- // In an equation, the root self is the node, thus its read is not free (READS.EQUATION).
270
- if (node !== undefined && root === "self") continue;
271
- add({ root, keys: NONE, exact: true, lazy, address: false, path: [root], place: tight(place.place), at: tight(place.at) });
272
- }
281
+ declare(roots, node, lazy, place);
273
282
  // check and effectsOf read the declared effects with the same function (D27). The verb of the facts selects the
274
283
  // declaration: effects for eval, and the entry of verbEffects for another verb (D49, D111).
275
284
  // The answer of the hook for the code of eval wins over the contract, also for an own entry (D118).
@@ -322,6 +331,8 @@ export function factsOf(machine: Machine, doc: unknown, options: FactsOptions =
322
331
  const stepAnswer = walker.name === undefined ? undefined : hook?.(found.path, walker.name);
323
332
  if (walker.described) note(stepAnswer?.pure ?? found.kind.pure, stepAnswer?.volatile ?? found.kind.volatile);
324
333
  else note(stepAnswer?.pure, stepAnswer?.volatile);
334
+ // A walker can resolve a root of its own. The read has the place of the reference (FACTS.STEP-READS).
335
+ declare(stepAnswer?.reads ?? (walker.described ? found.kind.reads : undefined), node, lazy, place);
325
336
  }
326
337
  const first = path[1];
327
338
  // A reference to a name that a kind around it binds reads inside the document: it is not a read. The names of
package/src/formula.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Json, type Key, docEquals, isKey, isRecord, typeName } from "./doc.ts";
2
2
  import { partsOf as docParts } from "./parts.ts";
3
3
  import type { Kind, KindFacts, LazyRule, WorldEntry } from "./world.ts";
4
- import { type CompiledPath, type Context, type Resolution, compilePath, valueOfThing, walkerOf } from "./deref.ts";
4
+ import { type CompiledPath, type Context, type Resolution, type Root, compilePath, valueOfThing, walkerOf } from "./deref.ts";
5
5
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
6
6
  import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled, totalCode } from "./evaluate.ts";
7
7
  import { type MachinePart, mergeMachines } from "./merge.ts";
@@ -51,6 +51,9 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
51
51
  * names no operation.
52
52
  * - `pureExt`: the pure extensions of `ext`, by their name, for `factsOf` and the folds of a plan (PLAN.FOLD, D47).
53
53
  * The default names no extension. A standard extension is pure without a name here.
54
+ * - `extReads`: the roots that an extension of `ext` reads from its context, by its name, for `factsOf`
55
+ * (FACTS.EXT-READS). A read of a root through the context refuses a fold, thus such a pure extension runs at each
56
+ * evaluation.
54
57
  * - `volatile`: the operations whose results can change while their arguments stay the same, for example a clock.
55
58
  * - `sigs`: the signatures of the operations, by their name, for a check of types (TYPE.SIGNATURE). A list gives
56
59
  * overloads. An operation without a signature takes and gives `unknown` (TYPE.GRADUAL).
@@ -74,6 +77,7 @@ export interface FormulaProfile {
74
77
  readonly combine?: (errors: readonly EvalError[]) => EvalError;
75
78
  readonly pure?: true | readonly string[];
76
79
  readonly pureExt?: readonly string[];
80
+ readonly extReads?: Readonly<Record<string, readonly Root[]>>;
77
81
  readonly volatile?: readonly string[];
78
82
  readonly sigs?: Readonly<Record<string, Signature | readonly Signature[]>> | ((name: string) => Signature | readonly Signature[] | undefined);
79
83
  }
@@ -776,6 +780,7 @@ const PROFILE_KEYS = /* @__PURE__ */ Object.freeze([
776
780
  "combine",
777
781
  "pure",
778
782
  "pureExt",
783
+ "extReads",
779
784
  "volatile",
780
785
  "sigs",
781
786
  ] as const satisfies readonly (keyof FormulaProfile)[]);
@@ -1285,7 +1290,11 @@ function makeFacts(profile: FormulaProfile, known: (path: readonly Key[], evalua
1285
1290
  if (code !== "formula.op" && code !== "formula.ext") return undefined;
1286
1291
  const name = path.slice(1).join(".");
1287
1292
  if (code === "formula.ext") {
1288
- if (profile.ext !== undefined && Object.hasOwn(profile.ext, name)) return pureExt.has(name) ? PURE : NOT_PURE;
1293
+ if (profile.ext !== undefined && Object.hasOwn(profile.ext, name)) {
1294
+ const facts = pureExt.has(name) ? PURE : NOT_PURE;
1295
+ const reads = profile.extReads !== undefined && Object.hasOwn(profile.extReads, name) ? profile.extReads[name] : undefined;
1296
+ return reads === undefined ? facts : { ...facts, reads };
1297
+ }
1289
1298
  return Object.hasOwn(STD_EXT, name) ? PURE : undefined;
1290
1299
  }
1291
1300
  // An operation that the language does not have gives #NAME?. The kit gives it no facts.
package/src/index.ts CHANGED
@@ -91,6 +91,7 @@ export {
91
91
  restrictMachine,
92
92
  restrictWorld,
93
93
  } from "./merge.ts";
94
+ export { type Language, type LanguageCode, type LanguageDefinition, compose, defineLanguage } from "./language.ts";
94
95
  export {
95
96
  type Answer,
96
97
  type AskOptions,
@@ -292,6 +293,7 @@ export {
292
293
  print,
293
294
  syntaxFormat,
294
295
  } from "./syntax.ts";
296
+ export { type Configuration, type Frame, type StepperOptions, stepper, stepperNext, stepperStart } from "./stepper.ts";
295
297
  export { type Definitions, type ReferenceOptions, type ReferenceRunOptions, STD_DEFINITIONS, formulaDefinitions, reference, referenceRun } from "./reference.ts";
296
298
  export { type AgreeOptions, type AgreeRunOptions, type Agreement, type AgreeTypesOptions, type TypeAgreement, agreeTypes, type Tier, type TierInput, type TierResult, agree, agreeRun, sameValue } from "./agree.ts";
297
299
  export { type ConformOptions, type ConformProbe, type Finding, type Obligation, conforms } from "./conform.ts";
@@ -0,0 +1,147 @@
1
+ import { type Key, isRecord } from "./doc.ts";
2
+ import type { Machine } from "./evaluate.ts";
3
+ import { type MachinePart, type MergeConflict, type WorldPart, MergeError, mergeMachines } from "./merge.ts";
4
+
5
+ /** The code half of a language: each field of a machine part, without the world. */
6
+ export type LanguageCode = Omit<MachinePart, "world">;
7
+
8
+ /** The input of `defineLanguage`. */
9
+ export interface LanguageDefinition {
10
+ /** The name of the language. A composition holds one language of a name. */
11
+ readonly name: string;
12
+ /** The version, as `MAJOR.MINOR.PATCH`. */
13
+ readonly version: string;
14
+ /** The world of the language, with the verbs that it owns. It is JSON. */
15
+ readonly world?: WorldPart;
16
+ /** The lowest compatible version of each language that this language needs, by name (LANG.REQUIRES). */
17
+ readonly requires?: Readonly<Record<string, string>>;
18
+ /** The evaluators, the hooks and the other code. A function reads the merge of the languages before it. */
19
+ readonly code?: LanguageCode | ((prev: Machine) => LanguageCode);
20
+ }
21
+
22
+ /** One language as one frozen value, for `compose`. */
23
+ export interface Language {
24
+ readonly name: string;
25
+ readonly version: string;
26
+ readonly world: WorldPart;
27
+ readonly requires: Readonly<Record<string, string>>;
28
+ readonly code?: LanguageCode | ((prev: Machine) => LanguageCode);
29
+ /**
30
+ * The canonical JSON of the data half: `requires` and `world`, with the keys in order. Two equal data halves give
31
+ * one text, in each order of their keys (LANG.IDENTITY). The name, the version and this text identify a language.
32
+ */
33
+ readonly data: string;
34
+ }
35
+
36
+ const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
37
+
38
+ function numbers(version: string): readonly [number, number, number] {
39
+ const found = VERSION.exec(version);
40
+ if (found === null) throw new TypeError(`A language version has the form MAJOR.MINOR.PATCH. The text ${JSON.stringify(version)} does not.`);
41
+ return [Number(found[1]), Number(found[2]), Number(found[3])];
42
+ }
43
+
44
+ /**
45
+ * This function tells if a version satisfies a lowest version, by the rules of a caret range of SemVer. The first
46
+ * number that is not zero stays the same, and the version is not lower.
47
+ */
48
+ function compatible(version: string, lowest: string): boolean {
49
+ const have = numbers(version);
50
+ const want = numbers(lowest);
51
+ const stable = want.findIndex((part) => part !== 0);
52
+ const fixed = stable < 0 ? 3 : stable + 1;
53
+ for (let index = 0; index < 3; index += 1) {
54
+ const [a, b] = [have[index] as number, want[index] as number];
55
+ if (a !== b) return index >= fixed && a > b;
56
+ }
57
+ return true;
58
+ }
59
+
60
+ /** This function gives the JSON text of a value with its keys in order. A value that is not JSON is an error. */
61
+ function canonical(value: unknown, path: readonly Key[]): string {
62
+ if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
63
+ if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
64
+ if (Array.isArray(value)) return `[${value.map((item, index) => canonical(item, [...path, index])).join(",")}]`;
65
+ if (!isRecord(value)) throw new TypeError(`The data half of a language is JSON. The value at ${JSON.stringify(path)} is not.`);
66
+ const keys = Object.keys(value)
67
+ .filter((key) => value[key] !== undefined)
68
+ .sort();
69
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonical(value[key], [...path, key])}`).join(",")}}`;
70
+ }
71
+
72
+ function frozen<T>(value: T): T {
73
+ if (typeof value !== "object" || value === null) return value;
74
+ for (const item of Object.values(value)) frozen(item);
75
+ return Object.freeze(value);
76
+ }
77
+
78
+ /**
79
+ * This function makes one value of a language. The data half is JSON, thus a host can store it and compare it. The
80
+ * code half binds the evaluator names of the world.
81
+ *
82
+ * The language keeps a frozen copy of its data half, thus a later change to the input changes nothing.
83
+ *
84
+ * @param definition - The name, the version, the world, the needed languages and the code.
85
+ * @returns The language.
86
+ * @throws {TypeError} A version in the definition is not `MAJOR.MINOR.PATCH`, the name is empty, or the
87
+ * data half is not JSON.
88
+ */
89
+ export function defineLanguage(definition: LanguageDefinition): Language {
90
+ if (definition.name === "") throw new TypeError("A language has a name.");
91
+ numbers(definition.version);
92
+ const requires = definition.requires ?? {};
93
+ for (const name of Object.keys(requires)) numbers(requires[name] as string);
94
+ const data = canonical({ requires, world: definition.world ?? {} }, []);
95
+ const copy = frozen(JSON.parse(data) as { readonly requires: Readonly<Record<string, string>>; readonly world: WorldPart });
96
+ const language = { name: definition.name, version: definition.version, world: copy.world, requires: copy.requires, data };
97
+ return Object.freeze(definition.code === undefined ? language : { ...language, code: definition.code });
98
+ }
99
+
100
+ function partOf(language: Language): MachinePart | ((prev: Machine) => MachinePart) {
101
+ const { code, world } = language;
102
+ return typeof code === "function" ? (prev) => ({ ...code(prev), world }) : { ...code, world };
103
+ }
104
+
105
+ /**
106
+ * This function merges languages into one machine, in the order of the arguments, with `mergeMachines`.
107
+ *
108
+ * - A language comes after each language that it names in `requires`, at a compatible version (LANG.REQUIRES).
109
+ * - One language that comes twice counts once. Thus two languages can share a base.
110
+ * - Two versions of one name, or two data halves under one name and one version, are conflicts (LANG.COMPOSE).
111
+ *
112
+ * @param languages - The languages.
113
+ * @returns The machine.
114
+ * @throws {MergeError} The languages do not fit, or their merge has a conflict. The error lists each conflict.
115
+ */
116
+ export function compose(...languages: readonly Language[]): Machine {
117
+ const conflicts: MergeConflict[] = [];
118
+ const parts: (MachinePart | ((prev: Machine) => MachinePart))[] = [];
119
+ const seen = new Map<string, { readonly language: Language; readonly index: number }>();
120
+ languages.forEach((language, index) => {
121
+ const earlier = seen.get(language.name);
122
+ const conflict = (name: string, other: number, reason: string): void => {
123
+ conflicts.push({ path: ["languages", name], parts: [Math.min(other, index), Math.max(other, index)], reason });
124
+ };
125
+ if (earlier !== undefined) {
126
+ if (earlier.language.version !== language.version) conflict(language.name, earlier.index, "Two versions of one language.");
127
+ else if (earlier.language.data !== language.data) conflict(language.name, earlier.index, "Two languages with one name and one version differ.");
128
+ else if (earlier.language.code !== language.code) parts.push(partOf(language));
129
+ return;
130
+ }
131
+ for (const name of Object.keys(language.requires)) {
132
+ const needed = language.requires[name] as string;
133
+ const found = seen.get(name);
134
+ if (found === undefined) {
135
+ const later = languages.findIndex((other) => other.name === name);
136
+ if (later < 0) conflict(name, index, `A language requires ${name}, and the composition does not have it.`);
137
+ else conflict(name, later, `A language requires ${name}, and ${name} comes after it.`);
138
+ } else if (!compatible(found.language.version, needed)) {
139
+ conflict(name, found.index, `A language requires ${name} ${needed}, and the composition has ${found.language.version}.`);
140
+ }
141
+ }
142
+ seen.set(language.name, { language, index });
143
+ parts.push(partOf(language));
144
+ });
145
+ if (conflicts.length > 0) throw new MergeError(conflicts);
146
+ return mergeMachines(...parts);
147
+ }