@mark1russell7/coeval 0.5.0 → 0.6.1
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/README.md +2 -1
- package/build/index.d.ts +241 -128
- package/build/index.d.ts.map +1 -1
- package/build/index.js +456 -27
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/formula.ts +13 -19
- package/src/index.ts +11 -0
- package/src/merge.ts +486 -0
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.
|
|
4
|
+
"version": "0.6.1",
|
|
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/formula.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Kind, WorldEntry } from "./world.ts";
|
|
|
3
3
|
import { type Context, type Resolution, valueOfThing } from "./deref.ts";
|
|
4
4
|
import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
|
|
5
5
|
import type { Code, CompileInput, CompiledEvaluator, CompiledPart, Evaluator, Machine } from "./evaluate.ts";
|
|
6
|
+
import { type MachinePart, mergeMachines } from "./merge.ts";
|
|
6
7
|
|
|
7
8
|
/** An operation of a formula language: a function of the values of its arguments. */
|
|
8
9
|
export type OpFn = (...args: never[]) => unknown;
|
|
@@ -507,27 +508,20 @@ export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[
|
|
|
507
508
|
}
|
|
508
509
|
|
|
509
510
|
/**
|
|
510
|
-
* This function makes a machine for a formula language.
|
|
511
|
-
*
|
|
512
|
-
*
|
|
511
|
+
* This function makes a machine for a formula language. It merges the kit and the parts that the language adds with
|
|
512
|
+
* `mergeMachines`. Thus a kind `ops.*` or a namespace `ops` of the language extends the namespace of the kit, and
|
|
513
|
+
* the fallback of the kit stays. The hook `known` of the kit comes from the profile (`formulaKnown`). A hook `known`
|
|
514
|
+
* of the language answers after it.
|
|
515
|
+
*
|
|
516
|
+
* A kind or an evaluator of the language that differs from the entry of the kit with its name is a conflict. The
|
|
517
|
+
* language names the path in `replaces` to replace the entry, for example `["world", "kinds", "ref"]` or
|
|
518
|
+
* `["evaluators", "formula.op"]`. Then the hook of the kit does not answer for a replaced evaluator.
|
|
513
519
|
*
|
|
514
520
|
* @param profile - The operations and the rules of the language.
|
|
515
|
-
* @param more - The parts of a machine that the language adds
|
|
521
|
+
* @param more - The parts of a machine that the language adds, with the paths of the entries that it replaces.
|
|
516
522
|
* @returns The machine.
|
|
523
|
+
* @throws {MergeError} The language and the kit have a conflict.
|
|
517
524
|
*/
|
|
518
|
-
export function formulaMachine(profile: FormulaProfile = {}, more:
|
|
519
|
-
|
|
520
|
-
// An evaluator or a runner of the language with the name of an evaluator of the kit replaces it, and the kit does not answer for it.
|
|
521
|
-
const replaced = (name: string): boolean => (more.evaluators !== undefined && Object.hasOwn(more.evaluators, name)) || (more.runners !== undefined && Object.hasOwn(more.runners, name));
|
|
522
|
-
const theirs = more.known;
|
|
523
|
-
const known = (path: readonly Key[], evaluator: string): true | string => {
|
|
524
|
-
const answer = replaced(evaluator) ? true : kit(path, evaluator);
|
|
525
|
-
return answer === true && theirs !== undefined ? theirs(path, evaluator) : answer;
|
|
526
|
-
};
|
|
527
|
-
return {
|
|
528
|
-
...more,
|
|
529
|
-
world: { kinds: { ...formulaKinds(), ...more.world?.kinds }, defaults: { ...more.world?.defaults } },
|
|
530
|
-
evaluators: { ...formulaEvaluators(profile), ...more.evaluators },
|
|
531
|
-
known,
|
|
532
|
-
};
|
|
525
|
+
export function formulaMachine(profile: FormulaProfile = {}, more: MachinePart = {}): Machine {
|
|
526
|
+
return mergeMachines({ world: { kinds: formulaKinds() }, evaluators: formulaEvaluators(profile), known: formulaKnown(profile) }, more);
|
|
533
527
|
}
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,17 @@ export {
|
|
|
55
55
|
looksLikeKind,
|
|
56
56
|
lookupKind,
|
|
57
57
|
} from "./world.ts";
|
|
58
|
+
export {
|
|
59
|
+
type MachinePart,
|
|
60
|
+
type MergeConflict,
|
|
61
|
+
type MergePath,
|
|
62
|
+
type WorldPart,
|
|
63
|
+
MergeError,
|
|
64
|
+
mergeMachines,
|
|
65
|
+
mergeWorlds,
|
|
66
|
+
restrictMachine,
|
|
67
|
+
restrictWorld,
|
|
68
|
+
} from "./merge.ts";
|
|
58
69
|
export {
|
|
59
70
|
type Answer,
|
|
60
71
|
type AskOptions,
|
package/src/merge.ts
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { type Key, docEquals, isRecord } from "./doc.ts";
|
|
2
|
+
import type { DerefCode, Walker } from "./deref.ts";
|
|
3
|
+
import type { Evaluator, Machine, Runner } from "./evaluate.ts";
|
|
4
|
+
import { type Kind, type World, type WorldEntry, isAlias, isKind, isNamespace } from "./world.ts";
|
|
5
|
+
|
|
6
|
+
/** The keys from a part of a merge to one of its entries, for example `["kinds", "ref"]`. */
|
|
7
|
+
export type MergePath = readonly Key[];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A world as a part of a merge. Each field is optional. `replaces` names the paths of the entries that this part
|
|
11
|
+
* replaces, for example `["kinds", "ref"]` or `["defaults", "view"]`. The result of a merge has no `replaces`.
|
|
12
|
+
*/
|
|
13
|
+
export interface WorldPart {
|
|
14
|
+
readonly kinds?: Readonly<Record<string, WorldEntry>>;
|
|
15
|
+
readonly defaults?: Readonly<Record<string, string>>;
|
|
16
|
+
readonly fallback?: Kind;
|
|
17
|
+
readonly replaces?: readonly MergePath[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A machine as a part of a merge. Each field is optional. `replaces` names paths in the machine, for example
|
|
22
|
+
* `["evaluators", "formula.op"]` or `["world", "kinds", "ref"]`. The paths in `world.replaces` start at the world.
|
|
23
|
+
*/
|
|
24
|
+
export interface MachinePart extends Omit<Partial<Machine>, "world"> {
|
|
25
|
+
readonly world?: WorldPart;
|
|
26
|
+
readonly replaces?: readonly MergePath[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One conflict of a merge. `path` is the path of the entry, as `replaces` names it. Thus the same path in the
|
|
31
|
+
* `replaces` of the later part resolves the conflict. `parts` gives the indices of the two parts, the earlier first.
|
|
32
|
+
*/
|
|
33
|
+
export interface MergeConflict {
|
|
34
|
+
readonly path: MergePath;
|
|
35
|
+
readonly parts: readonly [number, number];
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The exception of a merge with conflicts. It lists each conflict of the merge, not only the first. */
|
|
40
|
+
export class MergeError extends Error {
|
|
41
|
+
readonly conflicts: readonly MergeConflict[];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This constructor makes the exception.
|
|
45
|
+
*
|
|
46
|
+
* @param conflicts - The conflicts, in the order that the merge found them.
|
|
47
|
+
*/
|
|
48
|
+
constructor(conflicts: readonly MergeConflict[]) {
|
|
49
|
+
const lines = conflicts.map((item) => `At ${JSON.stringify(item.path)}, the parts ${item.parts[0]} and ${item.parts[1]}: ${item.reason}`);
|
|
50
|
+
super(`The merge has ${conflicts.length === 1 ? "1 conflict" : `${conflicts.length} conflicts`}. ${lines.join(" ")}`);
|
|
51
|
+
this.name = "MergeError";
|
|
52
|
+
this.conflicts = conflicts;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type Known = NonNullable<Machine["known"]>;
|
|
57
|
+
|
|
58
|
+
const NONE: ReadonlySet<string> = new Set();
|
|
59
|
+
|
|
60
|
+
function keyOf(path: MergePath): string {
|
|
61
|
+
return JSON.stringify(path.map(String));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** This function sets an own data property. An assignment to the key `__proto__` changes the prototype. */
|
|
65
|
+
function setOwn<T>(target: Record<string, T>, key: string, value: T): void {
|
|
66
|
+
if (key === "__proto__") Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
67
|
+
else target[key] = value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function own<T>(table: Readonly<Record<string, T>> | undefined, key: string): T | undefined {
|
|
71
|
+
return table !== undefined && Object.hasOwn(table, key) ? table[key] : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** This function gives a copy of a JSON value without the fields whose value is `undefined`, as JSON writes it. */
|
|
75
|
+
function defined(value: unknown): unknown {
|
|
76
|
+
if (Array.isArray(value)) return value.map(defined);
|
|
77
|
+
if (!isRecord(value)) return value;
|
|
78
|
+
const out: Record<string, unknown> = {};
|
|
79
|
+
for (const key of Object.keys(value)) if (value[key] !== undefined) setOwn(out, key, defined(value[key]));
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Two values are the same when they are equal by `docEquals`. A function and a host value are equal only to themselves. */
|
|
84
|
+
function same(a: unknown, b: unknown): boolean {
|
|
85
|
+
return Object.is(a, b) || docEquals(defined(a), defined(b));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Two lists of roots are the same when they hold the same roots, in any order. */
|
|
89
|
+
function sameRoots(a: unknown, b: unknown): boolean {
|
|
90
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return same(a, b);
|
|
91
|
+
return same([...new Set(a.map(String))].sort(), [...new Set(b.map(String))].sort());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The record of a merge: the conflicts, and the part that gave each entry. */
|
|
95
|
+
class Merger {
|
|
96
|
+
readonly conflicts: MergeConflict[] = [];
|
|
97
|
+
private readonly sources = new Map<string, number>();
|
|
98
|
+
|
|
99
|
+
/** This method gives the part that gave the entry at a path, or the entry nearest above it. */
|
|
100
|
+
sourceOf(path: MergePath): number {
|
|
101
|
+
for (let length = path.length; length >= 0; length -= 1) {
|
|
102
|
+
const found = this.sources.get(keyOf(path.slice(0, length)));
|
|
103
|
+
if (found !== undefined) return found;
|
|
104
|
+
}
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** This method records the part that gives a new entry. */
|
|
109
|
+
give(path: MergePath, part: number): void {
|
|
110
|
+
this.sources.set(keyOf(path), part);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** This method records the part that replaces an entry, and forgets the parts of each entry below it. */
|
|
114
|
+
replace(path: MergePath, part: number): void {
|
|
115
|
+
const below = `${keyOf(path).slice(0, -1)},`;
|
|
116
|
+
for (const key of [...this.sources.keys()]) if (key.startsWith(below)) this.sources.delete(key);
|
|
117
|
+
this.give(path, part);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
clash(path: MergePath, part: number, reason: string): void {
|
|
121
|
+
this.conflicts.push({ path, parts: [this.sourceOf(path), part], reason });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* This function merges one entry of a table into the table. A new entry joins the table. A path in `replaces` gives
|
|
127
|
+
* the later entry. Two entries that are the same give one entry. Each other pair is a conflict.
|
|
128
|
+
*/
|
|
129
|
+
function mergeLeaf<T>(
|
|
130
|
+
m: Merger,
|
|
131
|
+
table: Record<string, T>,
|
|
132
|
+
key: string,
|
|
133
|
+
later: T | undefined,
|
|
134
|
+
path: MergePath,
|
|
135
|
+
part: number,
|
|
136
|
+
replaces: ReadonlySet<string>,
|
|
137
|
+
equal: (a: T, b: T) => boolean,
|
|
138
|
+
reason: string,
|
|
139
|
+
): void {
|
|
140
|
+
if (later === undefined) return;
|
|
141
|
+
const earlier = own(table, key);
|
|
142
|
+
if (earlier === undefined) {
|
|
143
|
+
setOwn(table, key, later);
|
|
144
|
+
m.give(path, part);
|
|
145
|
+
} else if (replaces.has(keyOf(path))) {
|
|
146
|
+
setOwn(table, key, later);
|
|
147
|
+
m.replace(path, part);
|
|
148
|
+
} else if (!equal(earlier, later)) {
|
|
149
|
+
m.clash(path, part, reason);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function what(entry: WorldEntry): string {
|
|
154
|
+
if (isKind(entry)) return "a kind";
|
|
155
|
+
if (isAlias(entry)) return "an alias";
|
|
156
|
+
return isNamespace(entry) ? "a namespace" : "an entry";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function mergeFallback(m: Merger, earlier: Kind | undefined, later: Kind | undefined, path: MergePath, part: number, replaces: ReadonlySet<string>): Kind | undefined {
|
|
160
|
+
const table: Record<string, Kind> = earlier === undefined ? {} : { fallback: earlier };
|
|
161
|
+
mergeLeaf(m, table, "fallback", later, path, part, replaces, same, "Two parts give two different fallback kinds.");
|
|
162
|
+
return table["fallback"];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function mergeEntries(m: Merger, into: Record<string, WorldEntry>, from: Readonly<Record<string, WorldEntry>>, path: MergePath, part: number, replaces: ReadonlySet<string>): void {
|
|
166
|
+
for (const key of Object.keys(from)) {
|
|
167
|
+
const later = from[key];
|
|
168
|
+
if (later === undefined) continue;
|
|
169
|
+
const at = [...path, key];
|
|
170
|
+
const earlier = own(into, key);
|
|
171
|
+
if (earlier === undefined) {
|
|
172
|
+
setOwn(into, key, later);
|
|
173
|
+
m.give(at, part);
|
|
174
|
+
} else {
|
|
175
|
+
setOwn(into, key, mergeEntry(m, earlier, later, at, part, replaces));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** This function merges two entries at one path. Two namespaces merge key by key, and their fallbacks merge too. */
|
|
181
|
+
function mergeEntry(m: Merger, earlier: WorldEntry, later: WorldEntry, path: MergePath, part: number, replaces: ReadonlySet<string>): WorldEntry {
|
|
182
|
+
if (replaces.has(keyOf(path))) {
|
|
183
|
+
m.replace(path, part);
|
|
184
|
+
return later;
|
|
185
|
+
}
|
|
186
|
+
if (same(earlier, later)) return earlier;
|
|
187
|
+
if (isNamespace(earlier) && isNamespace(later)) {
|
|
188
|
+
const kinds: Record<string, WorldEntry> = { ...earlier.kinds };
|
|
189
|
+
mergeEntries(m, kinds, later.kinds, [...path, "kinds"], part, replaces);
|
|
190
|
+
const fallback = mergeFallback(m, earlier.fallback, later.fallback, [...path, "fallback"], part, replaces);
|
|
191
|
+
return fallback === undefined ? { kinds } : { kinds, fallback };
|
|
192
|
+
}
|
|
193
|
+
const a = what(earlier);
|
|
194
|
+
const b = what(later);
|
|
195
|
+
m.clash(path, part, a === b ? `Two parts give two different ${a === "an alias" ? "aliases" : "kinds"}.` : `One part gives ${a}, and the other part gives ${b}.`);
|
|
196
|
+
return earlier;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface WorldDraft {
|
|
200
|
+
readonly kinds: Record<string, WorldEntry>;
|
|
201
|
+
readonly defaults: Record<string, string>;
|
|
202
|
+
fallback?: Kind;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function worldOf(draft: WorldDraft): World {
|
|
206
|
+
const world = { kinds: { ...draft.kinds }, defaults: { ...draft.defaults } };
|
|
207
|
+
return draft.fallback === undefined ? world : { ...world, fallback: draft.fallback };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function mergeWorldInto(m: Merger, draft: WorldDraft, part: WorldPart | undefined, index: number, replaces: ReadonlySet<string>, base: MergePath): void {
|
|
211
|
+
if (part === undefined) return;
|
|
212
|
+
if (part.kinds !== undefined) mergeEntries(m, draft.kinds, part.kinds, [...base, "kinds"], index, replaces);
|
|
213
|
+
const defaults = part.defaults ?? {};
|
|
214
|
+
for (const verb of Object.keys(defaults)) {
|
|
215
|
+
mergeLeaf(m, draft.defaults, verb, defaults[verb], [...base, "defaults", verb], index, replaces, Object.is, "Two parts give two different defaults for this verb.");
|
|
216
|
+
}
|
|
217
|
+
const fallback = mergeFallback(m, draft.fallback, part.fallback, [...base, "fallback"], index, replaces);
|
|
218
|
+
if (fallback !== undefined) draft.fallback = fallback;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* This function merges worlds into one world, in the order of the parts. It does not change a part.
|
|
223
|
+
*
|
|
224
|
+
* - Two entries that are equal by `docEquals` give one entry.
|
|
225
|
+
* - Two namespaces merge key by key, at each depth. Their fallbacks merge as each other entry.
|
|
226
|
+
* - The defaults merge verb by verb, and the world keeps one fallback.
|
|
227
|
+
* - Each other pair of entries at one path is a conflict, unless the later part names the path in `replaces`.
|
|
228
|
+
* Then the later entry wins, with no merge below it.
|
|
229
|
+
*
|
|
230
|
+
* A part can be a function of the merge of the parts before it (`prev`, as an overlay of Nix). Thus a part can read
|
|
231
|
+
* the entry that it replaces.
|
|
232
|
+
*
|
|
233
|
+
* @param parts - The worlds, or functions that give a world from the merge before them.
|
|
234
|
+
* @returns The world. It has no `replaces`.
|
|
235
|
+
* @throws {MergeError} The merge has a conflict. The error lists each conflict.
|
|
236
|
+
*/
|
|
237
|
+
export function mergeWorlds(...parts: readonly (WorldPart | ((prev: World) => WorldPart))[]): World {
|
|
238
|
+
const m = new Merger();
|
|
239
|
+
const draft: WorldDraft = { kinds: {}, defaults: {} };
|
|
240
|
+
parts.forEach((input, index) => {
|
|
241
|
+
const part = typeof input === "function" ? input(worldOf(draft)) : input;
|
|
242
|
+
mergeWorldInto(m, draft, part, index, new Set((part.replaces ?? []).map(keyOf)), []);
|
|
243
|
+
});
|
|
244
|
+
if (m.conflicts.length > 0) throw new MergeError(m.conflicts);
|
|
245
|
+
return worldOf(draft);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The fields of a machine that merge by their own rules. Each other field is one value, or a conflict. */
|
|
249
|
+
const TABLES = new Set(["world", "evaluators", "runners", "derefs", "walkers", "known", "replaces"]);
|
|
250
|
+
|
|
251
|
+
interface MachineDraft {
|
|
252
|
+
readonly world: WorldDraft;
|
|
253
|
+
readonly evaluators: Record<string, Evaluator>;
|
|
254
|
+
runners?: Record<string, Runner>;
|
|
255
|
+
derefs?: Record<string, DerefCode>;
|
|
256
|
+
walkers?: Record<string, Walker>;
|
|
257
|
+
readonly hooks: { readonly part: number; readonly known: Known }[];
|
|
258
|
+
/** The last part that replaced the code of each evaluator name. */
|
|
259
|
+
readonly replaced: Map<string, number>;
|
|
260
|
+
readonly rest: Record<string, unknown>;
|
|
261
|
+
readonly parts: MachinePart[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* This function composes the hooks `known` in the order of their parts. The first answer that is not `true` wins. A
|
|
266
|
+
* hook does not answer for an evaluator name whose code a later part replaces.
|
|
267
|
+
*/
|
|
268
|
+
function composeKnown(hooks: readonly { readonly part: number; readonly known: Known }[], replaced: ReadonlyMap<string, number>): Known | undefined {
|
|
269
|
+
const first = hooks[0];
|
|
270
|
+
if (first === undefined) return undefined;
|
|
271
|
+
if (hooks.length === 1 && replaced.size === 0) return first.known;
|
|
272
|
+
const list = [...hooks];
|
|
273
|
+
const last = new Map(replaced);
|
|
274
|
+
return (path, evaluator) => {
|
|
275
|
+
const after = last.get(evaluator);
|
|
276
|
+
for (const hook of list) {
|
|
277
|
+
if (after !== undefined && hook.part < after) continue;
|
|
278
|
+
const answer = hook.known(path, evaluator);
|
|
279
|
+
if (answer !== true) return answer;
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function machineOf(draft: MachineDraft): Machine {
|
|
286
|
+
const known = composeKnown(draft.hooks, draft.replaced);
|
|
287
|
+
return {
|
|
288
|
+
...draft.rest,
|
|
289
|
+
world: worldOf(draft.world),
|
|
290
|
+
evaluators: { ...draft.evaluators },
|
|
291
|
+
...(draft.runners === undefined ? {} : { runners: { ...draft.runners } }),
|
|
292
|
+
...(draft.derefs === undefined ? {} : { derefs: { ...draft.derefs } }),
|
|
293
|
+
...(draft.walkers === undefined ? {} : { walkers: { ...draft.walkers } }),
|
|
294
|
+
...(known === undefined ? {} : { known }),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** This function merges the evaluators and the runners of a part. The evaluator and the runner of a name are its code. */
|
|
299
|
+
function mergeCode(m: Merger, draft: MachineDraft, part: MachinePart, index: number, replaces: ReadonlySet<string>): void {
|
|
300
|
+
if (part.runners !== undefined) draft.runners ??= {};
|
|
301
|
+
const names = new Set([...Object.keys(part.evaluators ?? {}), ...Object.keys(part.runners ?? {})]);
|
|
302
|
+
for (const name of names) {
|
|
303
|
+
const evaluator = own(part.evaluators, name);
|
|
304
|
+
const runner = own(part.runners, name);
|
|
305
|
+
if (evaluator === undefined && runner === undefined) continue;
|
|
306
|
+
const evaluatorPath = ["evaluators", name];
|
|
307
|
+
const runnerPath = ["runners", name];
|
|
308
|
+
if (replaces.has(keyOf(evaluatorPath)) || replaces.has(keyOf(runnerPath))) {
|
|
309
|
+
// The part replaces the code of the name: the evaluator and the runner of the earlier parts go.
|
|
310
|
+
if (evaluator === undefined) delete draft.evaluators[name];
|
|
311
|
+
else setOwn(draft.evaluators, name, evaluator);
|
|
312
|
+
if (runner !== undefined) setOwn((draft.runners ??= {}), name, runner);
|
|
313
|
+
else if (draft.runners !== undefined) delete draft.runners[name];
|
|
314
|
+
m.replace(evaluatorPath, index);
|
|
315
|
+
m.replace(runnerPath, index);
|
|
316
|
+
draft.replaced.set(name, index);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
mergeLeaf(m, draft.evaluators, name, evaluator, evaluatorPath, index, NONE, Object.is, "Two parts give two different evaluators for this name.");
|
|
320
|
+
if (runner !== undefined) mergeLeaf(m, (draft.runners ??= {}), name, runner, runnerPath, index, NONE, Object.is, "Two parts give two different runners for this name.");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** A runner hides the evaluator of its name. Thus a name with a runner and an evaluator needs one part that gives both. */
|
|
325
|
+
function checkHidden(m: Merger, draft: MachineDraft): void {
|
|
326
|
+
const runners = draft.runners ?? {};
|
|
327
|
+
for (const name of Object.keys(runners)) {
|
|
328
|
+
const runner = runners[name];
|
|
329
|
+
const evaluator = own(draft.evaluators, name);
|
|
330
|
+
if (runner === undefined || evaluator === undefined) continue;
|
|
331
|
+
if (draft.parts.some((part) => own(part.evaluators, name) === evaluator && own(part.runners, name) === runner)) continue;
|
|
332
|
+
const a = m.sourceOf(["evaluators", name]);
|
|
333
|
+
const b = m.sourceOf(["runners", name]);
|
|
334
|
+
m.conflicts.push({
|
|
335
|
+
path: ["runners", name],
|
|
336
|
+
parts: [Math.min(a, b), Math.max(a, b)],
|
|
337
|
+
reason: "The runner of one part hides the evaluator of the other part, because a runner wins over an evaluator of its name.",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* This function merges machines into one machine, in the order of the parts. It does not change a part.
|
|
344
|
+
*
|
|
345
|
+
* - The worlds merge with the rules of `mergeWorlds`. The paths of their conflicts start with `"world"`.
|
|
346
|
+
* - The tables `evaluators`, `runners`, `derefs` and `walkers` merge name by name. A function is equal only to itself.
|
|
347
|
+
* - The evaluator and the runner of a name are its code. A replacement of the name replaces both.
|
|
348
|
+
* - A runner hides the evaluator of its name. Thus one part must give both, or the merge gives a conflict.
|
|
349
|
+
* - The hooks `known` compose in the order of the parts. A hook does not answer for a name that a later part replaces.
|
|
350
|
+
* - Each other field, for example `permits`, `adopt`, `roots`, `plain` or `maxDepth`, is one value. Two different
|
|
351
|
+
* values are a conflict. An absent field has no value.
|
|
352
|
+
*
|
|
353
|
+
* A conflict stops the merge, unless the later part names its path in `replaces`. A part can be a function of the
|
|
354
|
+
* merge of the parts before it (`prev`, as an overlay of Nix). Thus a part can wrap the code that it replaces.
|
|
355
|
+
*
|
|
356
|
+
* @param parts - The machines, or functions that give a machine from the merge before them.
|
|
357
|
+
* @returns The machine.
|
|
358
|
+
* @throws {MergeError} The merge has a conflict. The error lists each conflict.
|
|
359
|
+
*/
|
|
360
|
+
export function mergeMachines(...parts: readonly (MachinePart | ((prev: Machine) => MachinePart))[]): Machine {
|
|
361
|
+
const m = new Merger();
|
|
362
|
+
const draft: MachineDraft = { world: { kinds: {}, defaults: {} }, evaluators: {}, hooks: [], replaced: new Map(), rest: {}, parts: [] };
|
|
363
|
+
parts.forEach((input, index) => {
|
|
364
|
+
const part = typeof input === "function" ? input(machineOf(draft)) : input;
|
|
365
|
+
draft.parts.push(part);
|
|
366
|
+
const replaces = new Set([...(part.replaces ?? []), ...(part.world?.replaces ?? []).map((path) => ["world", ...path])].map(keyOf));
|
|
367
|
+
mergeWorldInto(m, draft.world, part.world, index, replaces, ["world"]);
|
|
368
|
+
mergeCode(m, draft, part, index, replaces);
|
|
369
|
+
if (part.derefs !== undefined) {
|
|
370
|
+
const derefs = (draft.derefs ??= {});
|
|
371
|
+
for (const name of Object.keys(part.derefs)) mergeLeaf(m, derefs, name, part.derefs[name], ["derefs", name], index, replaces, Object.is, "Two parts give two different deref codes for this name.");
|
|
372
|
+
}
|
|
373
|
+
if (part.walkers !== undefined) {
|
|
374
|
+
const walkers = (draft.walkers ??= {});
|
|
375
|
+
for (const name of Object.keys(part.walkers)) mergeLeaf(m, walkers, name, part.walkers[name], ["walkers", name], index, replaces, Object.is, "Two parts give two different walkers for this name.");
|
|
376
|
+
}
|
|
377
|
+
if (part.known !== undefined && !draft.hooks.some((hook) => hook.known === part.known)) draft.hooks.push({ part: index, known: part.known });
|
|
378
|
+
const fields = part as Readonly<Record<string, unknown>>;
|
|
379
|
+
for (const key of Object.keys(fields)) {
|
|
380
|
+
if (TABLES.has(key)) continue;
|
|
381
|
+
mergeLeaf(m, draft.rest, key, fields[key], [key], index, replaces, key === "roots" ? sameRoots : same, `Two parts give two different values for ${key}.`);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
checkHidden(m, draft);
|
|
385
|
+
if (m.conflicts.length > 0) throw new MergeError(m.conflicts);
|
|
386
|
+
return machineOf(draft);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** This function removes the entry at a path from a table of entries. It gives the same table when no entry is there. */
|
|
390
|
+
function removeEntry(entries: Readonly<Record<string, WorldEntry>>, path: readonly Key[]): Readonly<Record<string, WorldEntry>> {
|
|
391
|
+
const [first, ...rest] = path;
|
|
392
|
+
if (first === undefined) return entries;
|
|
393
|
+
const key = String(first);
|
|
394
|
+
const entry = own(entries, key);
|
|
395
|
+
if (entry === undefined) return entries;
|
|
396
|
+
let next: WorldEntry | undefined;
|
|
397
|
+
if (rest.length > 0) {
|
|
398
|
+
if (!isNamespace(entry)) return entries;
|
|
399
|
+
if (rest.length === 1 && String(rest[0]) === "fallback") {
|
|
400
|
+
if (entry.fallback === undefined) return entries;
|
|
401
|
+
next = { kinds: entry.kinds };
|
|
402
|
+
} else {
|
|
403
|
+
if (String(rest[0]) !== "kinds") return entries;
|
|
404
|
+
const kinds = removeEntry(entry.kinds, rest.slice(1));
|
|
405
|
+
if (kinds === entry.kinds) return entries;
|
|
406
|
+
next = entry.fallback === undefined ? { kinds } : { kinds, fallback: entry.fallback };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const out: Record<string, WorldEntry> = { ...entries };
|
|
410
|
+
if (next === undefined) delete out[key];
|
|
411
|
+
else setOwn(out, key, next);
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* This function removes entries from a world, for a language that restricts another language. A path names an entry
|
|
417
|
+
* as `replaces` names it, for example `["kinds", "fn"]`, `["kinds", "ops", "kinds", "mul"]`,
|
|
418
|
+
* `["kinds", "ops", "fallback"]`, `["defaults", "view"]` or `["fallback"]`. A path that names no entry changes
|
|
419
|
+
* nothing. The function does not change the world.
|
|
420
|
+
*
|
|
421
|
+
* A removed kind gives `#KIND?`, also through an alias to it. A namespace without its fallback knows only its own keys.
|
|
422
|
+
* A world without its fallback is closed.
|
|
423
|
+
*
|
|
424
|
+
* @param world - The world.
|
|
425
|
+
* @param paths - The paths of the entries to remove.
|
|
426
|
+
* @returns The world without the entries.
|
|
427
|
+
*/
|
|
428
|
+
export function restrictWorld(world: World, paths: readonly MergePath[]): World {
|
|
429
|
+
let kinds = world.kinds;
|
|
430
|
+
const defaults: Record<string, string> = { ...world.defaults };
|
|
431
|
+
let fallback = world.fallback;
|
|
432
|
+
for (const path of paths) {
|
|
433
|
+
const [first, ...rest] = path.map(String);
|
|
434
|
+
if (first === "kinds") kinds = removeEntry(kinds, rest);
|
|
435
|
+
else if (first === "defaults" && rest.length === 1) delete defaults[rest[0] as string];
|
|
436
|
+
else if (first === "fallback" && rest.length === 0) fallback = undefined;
|
|
437
|
+
}
|
|
438
|
+
const out = { kinds: { ...kinds }, defaults };
|
|
439
|
+
return fallback === undefined ? out : { ...out, fallback };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** The tables of the code of a machine, as the paths of a restriction name them. */
|
|
443
|
+
const CODE_TABLES = ["evaluators", "runners", "derefs", "walkers"] as const;
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* This function removes entries from a machine, for a language that restricts another language. A path names an
|
|
447
|
+
* entry as `replaces` names it:
|
|
448
|
+
*
|
|
449
|
+
* - A path that starts with `"world"` removes an entry of the world, with the rules of `restrictWorld`.
|
|
450
|
+
* - `["evaluators", name]` or `["runners", name]` removes the code of the name: its evaluator and its runner.
|
|
451
|
+
* - `["derefs", name]` and `["walkers", name]` remove one entry of their tables.
|
|
452
|
+
*
|
|
453
|
+
* A path that names no entry changes nothing. The function does not change the machine. An own entry names an
|
|
454
|
+
* evaluator directly, thus only a machine without the code of a name refuses each document that names it.
|
|
455
|
+
*
|
|
456
|
+
* @param machine - The machine.
|
|
457
|
+
* @param paths - The paths of the entries to remove.
|
|
458
|
+
* @returns The machine without the entries.
|
|
459
|
+
*/
|
|
460
|
+
export function restrictMachine(machine: Machine, paths: readonly MergePath[]): Machine {
|
|
461
|
+
const world = paths.filter((path) => String(path[0]) === "world").map((path) => path.slice(1));
|
|
462
|
+
const tables: Record<(typeof CODE_TABLES)[number], Record<string, unknown> | undefined> = {
|
|
463
|
+
evaluators: { ...machine.evaluators },
|
|
464
|
+
runners: machine.runners === undefined ? undefined : { ...machine.runners },
|
|
465
|
+
derefs: machine.derefs === undefined ? undefined : { ...machine.derefs },
|
|
466
|
+
walkers: machine.walkers === undefined ? undefined : { ...machine.walkers },
|
|
467
|
+
};
|
|
468
|
+
for (const path of paths) {
|
|
469
|
+
const [table, name, ...rest] = path.map(String);
|
|
470
|
+
if (name === undefined || rest.length > 0 || !(CODE_TABLES as readonly (string | undefined)[]).includes(table)) continue;
|
|
471
|
+
// The evaluator and the runner of a name are its code, as for a replacement (MACHINE.RUNNER).
|
|
472
|
+
const from = table === "evaluators" || table === "runners" ? ["evaluators", "runners"] : [table as string];
|
|
473
|
+
for (const key of from) {
|
|
474
|
+
const entries = tables[key as (typeof CODE_TABLES)[number]];
|
|
475
|
+
if (entries !== undefined) delete entries[name];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
...machine,
|
|
480
|
+
world: world.length === 0 ? machine.world : restrictWorld(machine.world, world),
|
|
481
|
+
evaluators: tables.evaluators as Machine["evaluators"],
|
|
482
|
+
...(tables.runners === undefined ? {} : { runners: tables.runners as NonNullable<Machine["runners"]> }),
|
|
483
|
+
...(tables.derefs === undefined ? {} : { derefs: tables.derefs as NonNullable<Machine["derefs"]> }),
|
|
484
|
+
...(tables.walkers === undefined ? {} : { walkers: tables.walkers as NonNullable<Machine["walkers"]> }),
|
|
485
|
+
};
|
|
486
|
+
}
|