@mark1russell7/coeval 0.6.3 → 0.7.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/README.md +32 -5
- package/build/index.d.ts +1568 -271
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4779 -492
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +15 -3
- package/src/conform.ts +1068 -0
- package/src/deref.ts +284 -47
- package/src/durable.ts +238 -0
- package/src/effects.ts +110 -0
- package/src/errors.ts +2 -1
- package/src/evaluate.ts +472 -111
- package/src/facts.ts +252 -0
- package/src/formula.ts +433 -36
- package/src/graph.ts +399 -0
- package/src/index.ts +28 -0
- package/src/ladder.ts +40 -1
- package/src/merge.ts +91 -4
- package/src/parts.ts +26 -11
- package/src/providers.ts +286 -0
- package/src/reads.ts +105 -0
- package/src/reference.ts +19 -7
- package/src/schedule.ts +436 -0
- package/src/static.ts +266 -29
- package/src/stdlib.ts +24 -7
- package/src/types.ts +741 -0
- package/src/typing.ts +390 -0
- package/src/world.ts +63 -9
package/src/durable.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { Context } from "./deref.ts";
|
|
2
|
+
import type { Result } from "./errors.ts";
|
|
3
|
+
import { type Machine, type Plan, type PrepareOptions, type Read, prepare } from "./evaluate.ts";
|
|
4
|
+
import { type DocFacts, type FactsOptions, factsOf } from "./facts.ts";
|
|
5
|
+
import { type VerifyOptions, verifyReads } from "./reads.ts";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Two levels of durability (R4 P7, D30). The plan and the facts of a document depend only on the structure: the
|
|
9
|
+
* machine and the documents. A result also depends on the data: the values that a context gives. A host keeps one
|
|
10
|
+
* epoch for each level. Thus a change of data keeps each plan and each fact, and it only verifies or evaluates.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The two epochs of a host (DURABLE.EPOCHS). Each epoch counts the changes of its level. */
|
|
14
|
+
export interface Epochs {
|
|
15
|
+
/** The count of the changes of the structure: a new machine, or an edit of a document. */
|
|
16
|
+
readonly structure: number;
|
|
17
|
+
/** The count of the changes of the data. A change of the structure is also a change of the data. */
|
|
18
|
+
readonly data: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The work of a durable cache since its start. A schedule reads it as a measure, not as a rule (R4). */
|
|
22
|
+
export interface DurableCounts {
|
|
23
|
+
/** The compilations: the calls of `prepare`. */
|
|
24
|
+
readonly compiles: number;
|
|
25
|
+
/** The calls of `factsOf`. */
|
|
26
|
+
readonly facts: number;
|
|
27
|
+
/** The evaluations of a plan. */
|
|
28
|
+
readonly evaluations: number;
|
|
29
|
+
/** The calls of `verifyReads`. */
|
|
30
|
+
readonly verifications: number;
|
|
31
|
+
/** The results that stayed without an evaluation. */
|
|
32
|
+
readonly reuses: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The options of `durable`. */
|
|
36
|
+
export interface DurableOptions {
|
|
37
|
+
/** The type of a bare root value, for each plan and each fact. */
|
|
38
|
+
readonly declaredType?: string;
|
|
39
|
+
/** The verb of each plan and each fact. The default is `eval` (VERB.PREPARE). */
|
|
40
|
+
readonly verb?: string;
|
|
41
|
+
/** The volatile reads of the host, as in `factsOf`. A document with such a read is not pure, thus it evaluates again after each change of data. */
|
|
42
|
+
readonly volatile?: FactsOptions["volatile"];
|
|
43
|
+
/** The rule of the host for two things, as in `verifyReads`. */
|
|
44
|
+
readonly equal?: VerifyOptions["equal"];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A cache of plans, facts and results with two levels of durability (D30). A host or a schedule tells the cache each
|
|
49
|
+
* change of the structure and each change of the data.
|
|
50
|
+
*/
|
|
51
|
+
export interface Durable {
|
|
52
|
+
/** The machine of the current structure epoch. */
|
|
53
|
+
readonly machine: Machine;
|
|
54
|
+
/** The current epochs. A schedule builds again what depends on the whole structure when `structure` changes, for example a graph of the nodes. */
|
|
55
|
+
readonly epochs: Epochs;
|
|
56
|
+
/** The work since the start. */
|
|
57
|
+
readonly counts: DurableCounts;
|
|
58
|
+
/**
|
|
59
|
+
* This function gives the plan of a document. A document object compiles one time for a machine (DURABLE.STRUCTURE).
|
|
60
|
+
* A change of data compiles nothing (DURABLE.DATA).
|
|
61
|
+
*
|
|
62
|
+
* @param doc - The document, as JSON.
|
|
63
|
+
* @returns The plan.
|
|
64
|
+
*/
|
|
65
|
+
plan(doc: unknown): Plan;
|
|
66
|
+
/**
|
|
67
|
+
* This function gives the static facts of a document. A document object computes its facts one time for a machine.
|
|
68
|
+
*
|
|
69
|
+
* @param doc - The document, as JSON.
|
|
70
|
+
* @returns The facts.
|
|
71
|
+
*/
|
|
72
|
+
facts(doc: unknown): DocFacts;
|
|
73
|
+
/**
|
|
74
|
+
* This function gives the result of the document of a node (DURABLE.REUSE). The key names the node, and in one data
|
|
75
|
+
* epoch the context of a key gives the same data.
|
|
76
|
+
*
|
|
77
|
+
* - In the data epoch of its result, a key with the same plan gives that result again.
|
|
78
|
+
* - After a change of data, a pure document verifies its reads, and it keeps its result when each read is the same.
|
|
79
|
+
* - Each other document evaluates again.
|
|
80
|
+
*
|
|
81
|
+
* @param key - The name of the node in the host, for example the name of a cell.
|
|
82
|
+
* @param doc - The document of the node.
|
|
83
|
+
* @param context - The context of the node.
|
|
84
|
+
* @returns The result.
|
|
85
|
+
*/
|
|
86
|
+
evaluate(key: unknown, doc: unknown, context: Context): Result;
|
|
87
|
+
/** This function tells the cache that the data changed: a context gives a new value (DURABLE.EPOCHS). */
|
|
88
|
+
changeData(): void;
|
|
89
|
+
/**
|
|
90
|
+
* This function tells the cache that the structure changed (DURABLE.EPOCHS). An edit of a document gives a new
|
|
91
|
+
* document object, and only that object compiles. A new machine drops each plan, each fact and each result.
|
|
92
|
+
*
|
|
93
|
+
* @param machine - The new machine, or `undefined` for an edit of a document.
|
|
94
|
+
*/
|
|
95
|
+
changeStructure(machine?: Machine): void;
|
|
96
|
+
/**
|
|
97
|
+
* This function drops the result of a node, for example a node that the host removed.
|
|
98
|
+
*
|
|
99
|
+
* @param key - The name of the node.
|
|
100
|
+
*/
|
|
101
|
+
forget(key: unknown): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The result of one node, with what it depends on. */
|
|
105
|
+
interface Entry {
|
|
106
|
+
/** The plan of the evaluation. A new document or a new machine gives another plan. */
|
|
107
|
+
readonly plan: Plan;
|
|
108
|
+
readonly result: Result;
|
|
109
|
+
/** The reads of the evaluation, only for a pure document. */
|
|
110
|
+
readonly reads: readonly Read[] | undefined;
|
|
111
|
+
/** The data epoch in which the result was last valid. */
|
|
112
|
+
data: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The most values that a cache of value documents keeps, as in `planner`. */
|
|
116
|
+
const MAX_VALUES = 1000;
|
|
117
|
+
|
|
118
|
+
/** No reads. One frozen list serves each result of a pure document without reads. */
|
|
119
|
+
const NO_READS: readonly Read[] = /* @__PURE__ */ Object.freeze([]);
|
|
120
|
+
|
|
121
|
+
/** The key of the document `-0` in a cache of value documents. A `Map` finds the entry of `0` for `-0`. */
|
|
122
|
+
const NEGATIVE_ZERO = /* @__PURE__ */ Symbol("coeval.negative-zero");
|
|
123
|
+
|
|
124
|
+
/** This function makes a cache by document. A document object gives its value one time, as PLAN.CACHE says. */
|
|
125
|
+
function byDocument<T>(make: (doc: unknown) => T): (doc: unknown) => T {
|
|
126
|
+
const objects = new WeakMap<object, T>();
|
|
127
|
+
const values = new Map<unknown, T>();
|
|
128
|
+
return (doc) => {
|
|
129
|
+
if (typeof doc === "object" && doc !== null) {
|
|
130
|
+
if (objects.has(doc)) return objects.get(doc) as T;
|
|
131
|
+
const made = make(doc);
|
|
132
|
+
objects.set(doc, made);
|
|
133
|
+
return made;
|
|
134
|
+
}
|
|
135
|
+
const key = Object.is(doc, -0) ? NEGATIVE_ZERO : doc;
|
|
136
|
+
if (values.has(key)) return values.get(key) as T;
|
|
137
|
+
const made = make(doc);
|
|
138
|
+
if (values.size >= MAX_VALUES) values.clear();
|
|
139
|
+
values.set(key, made);
|
|
140
|
+
return made;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* This function makes a cache with two levels of durability for a host or a schedule (R4 P7, D30). The structure is
|
|
146
|
+
* the machine and the documents. The data is each value that a context gives: the names, `self`, `event`, the
|
|
147
|
+
* capabilities and the world of the host.
|
|
148
|
+
*
|
|
149
|
+
* - A plan and the facts depend only on the structure. A document object compiles one time for a machine, thus a
|
|
150
|
+
* change of data compiles nothing (DURABLE.DATA).
|
|
151
|
+
* - A result depends on the data too. It stays in its data epoch. After a change of data, a pure document verifies
|
|
152
|
+
* its reads with `verifyReads`, and each other document evaluates again.
|
|
153
|
+
* - An edit gives a new document object, thus only that document compiles. A new machine drops each plan, each fact
|
|
154
|
+
* and each result (DURABLE.STRUCTURE).
|
|
155
|
+
*
|
|
156
|
+
* 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.
|
|
158
|
+
*
|
|
159
|
+
* @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.
|
|
161
|
+
* @returns The cache.
|
|
162
|
+
*/
|
|
163
|
+
export function durable(machine: Machine, options: DurableOptions = {}): Durable {
|
|
164
|
+
const prepareOptions: PrepareOptions = {
|
|
165
|
+
...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
|
|
166
|
+
...(options.verb === undefined ? {} : { verb: options.verb }),
|
|
167
|
+
};
|
|
168
|
+
const factsOptions: FactsOptions = { ...prepareOptions, ...(options.volatile === undefined ? {} : { volatile: options.volatile }) };
|
|
169
|
+
const verifyOptions: VerifyOptions = options.equal === undefined ? {} : { equal: options.equal };
|
|
170
|
+
const counts = { compiles: 0, facts: 0, evaluations: 0, verifications: 0, reuses: 0 };
|
|
171
|
+
const results = new Map<unknown, Entry>();
|
|
172
|
+
let current = machine;
|
|
173
|
+
let epochs: Epochs = { structure: 0, data: 0 };
|
|
174
|
+
const plansOf = (m: Machine): ((doc: unknown) => Plan) =>
|
|
175
|
+
byDocument((doc) => {
|
|
176
|
+
counts.compiles += 1;
|
|
177
|
+
return prepare(m, doc, prepareOptions);
|
|
178
|
+
});
|
|
179
|
+
const factsFor = (m: Machine): ((doc: unknown) => DocFacts) =>
|
|
180
|
+
byDocument((doc) => {
|
|
181
|
+
counts.facts += 1;
|
|
182
|
+
return factsOf(m, doc, factsOptions);
|
|
183
|
+
});
|
|
184
|
+
let plans = plansOf(machine);
|
|
185
|
+
let facts = factsFor(machine);
|
|
186
|
+
return {
|
|
187
|
+
get machine() {
|
|
188
|
+
return current;
|
|
189
|
+
},
|
|
190
|
+
get epochs() {
|
|
191
|
+
return epochs;
|
|
192
|
+
},
|
|
193
|
+
get counts() {
|
|
194
|
+
return { ...counts };
|
|
195
|
+
},
|
|
196
|
+
plan: (doc) => plans(doc),
|
|
197
|
+
facts: (doc) => facts(doc),
|
|
198
|
+
evaluate(key, doc, context) {
|
|
199
|
+
const plan = plans(doc);
|
|
200
|
+
const entry = results.get(key);
|
|
201
|
+
if (entry !== undefined && entry.plan === plan) {
|
|
202
|
+
if (entry.data === epochs.data) {
|
|
203
|
+
counts.reuses += 1;
|
|
204
|
+
return entry.result;
|
|
205
|
+
}
|
|
206
|
+
if (entry.reads !== undefined) {
|
|
207
|
+
counts.verifications += 1;
|
|
208
|
+
if (verifyReads(plan, entry.reads, context, verifyOptions).same) {
|
|
209
|
+
entry.data = epochs.data;
|
|
210
|
+
counts.reuses += 1;
|
|
211
|
+
return entry.result;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Only a pure document is a function of its reads (FACTS.PURE), thus only its reads can keep its result.
|
|
216
|
+
const reads: Read[] | undefined = facts(doc).pure ? [] : undefined;
|
|
217
|
+
counts.evaluations += 1;
|
|
218
|
+
const result = plan.evaluate(context, reads === undefined ? undefined : { reads });
|
|
219
|
+
// 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 });
|
|
221
|
+
return result;
|
|
222
|
+
},
|
|
223
|
+
changeData() {
|
|
224
|
+
epochs = { structure: epochs.structure, data: epochs.data + 1 };
|
|
225
|
+
},
|
|
226
|
+
changeStructure(next) {
|
|
227
|
+
epochs = { structure: epochs.structure + 1, data: epochs.data + 1 };
|
|
228
|
+
if (next === undefined || next === current) return;
|
|
229
|
+
current = next;
|
|
230
|
+
plans = plansOf(next);
|
|
231
|
+
facts = factsFor(next);
|
|
232
|
+
results.clear();
|
|
233
|
+
},
|
|
234
|
+
forget(key) {
|
|
235
|
+
results.delete(key);
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
package/src/effects.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Key } from "./doc.ts";
|
|
2
|
+
import type { Cap } from "./deref.ts";
|
|
3
|
+
import { type Machine, capKeys, permits } from "./evaluate.ts";
|
|
4
|
+
import { type DocVisit, type Issue, declaredEffects, permitsOf, walk } from "./static.ts";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The effects that reach the host (R2 P3, D27). The contract of each kind declares the effects of its documents, and
|
|
8
|
+
* its capability rules narrow the capabilities of its parts (D14). One walk narrows the capabilities as a run does. An
|
|
9
|
+
* effect that the capabilities of its document do not permit fails in the run, thus it does not reach the host.
|
|
10
|
+
*
|
|
11
|
+
* A procedure value keeps the capabilities of the document that made it, as the closure of `fn` does. Thus its latent
|
|
12
|
+
* effects are the effects of its body, under the capabilities of the body, and the fold needs no effect variables.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The options of `effectsOf`. */
|
|
16
|
+
export interface EffectsOptions {
|
|
17
|
+
/** The type of a bare root value. */
|
|
18
|
+
readonly declaredType?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The capabilities at the root. With them, the result keeps only the effects that the rule of the machine permits at
|
|
21
|
+
* their documents, as a run does (CAP.PERMITS). Without them, the root has each capability, and only the capability
|
|
22
|
+
* rules of the kinds remove an effect.
|
|
23
|
+
*/
|
|
24
|
+
readonly caps?: readonly Cap[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Each capability: one capability with no keys. It permits each effect, because each kind starts with no keys (CAP.KEYS). */
|
|
28
|
+
const EACH: readonly Cap[] = /* @__PURE__ */ Object.freeze([[]]);
|
|
29
|
+
|
|
30
|
+
/** One declared effect that can reach the host: its keys and the visit of its document. */
|
|
31
|
+
interface Reaching {
|
|
32
|
+
readonly kind: readonly Key[];
|
|
33
|
+
readonly visit: DocVisit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* This function finds each declared effect that can reach the host, in the order of the document. It is the one
|
|
38
|
+
* inference of `effectsOf` and `checkLatent`. `check` and `factsOf` read the same declarations (`declaredEffects`).
|
|
39
|
+
*/
|
|
40
|
+
function reaching(machine: Machine, doc: unknown, options: EffectsOptions): readonly Reaching[] {
|
|
41
|
+
const out: Reaching[] = [];
|
|
42
|
+
// Without capabilities, the host has no rule to ask. The capability rules of the kinds still narrow each capability.
|
|
43
|
+
const rule = options.caps === undefined ? permits : permitsOf(machine);
|
|
44
|
+
walk(
|
|
45
|
+
machine.world,
|
|
46
|
+
doc,
|
|
47
|
+
(v) => {
|
|
48
|
+
const caps = v.caps ?? EACH;
|
|
49
|
+
for (const kind of declaredEffects(v)) if (rule(caps, kind)) out.push({ kind, visit: v });
|
|
50
|
+
return undefined;
|
|
51
|
+
},
|
|
52
|
+
{ caps: options.caps ?? EACH, ...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }) },
|
|
53
|
+
);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* This function gives the effects of a document that can reach the host (CAP.EFFECTS-OF, R2 P3). The result is the
|
|
59
|
+
* effects that the contract of each kind declares, without the effects that the capability rules around them remove.
|
|
60
|
+
* A rule `mask` removes each effect of its parts, and a rule `only` removes each effect outside its list.
|
|
61
|
+
*
|
|
62
|
+
* Without capabilities in the options, the result is the least authority of the document. A host that grants these
|
|
63
|
+
* kinds as capabilities grants each effect that a run of the document can give it. With capabilities, the result keeps
|
|
64
|
+
* only the effects that the rule of the machine permits at their documents.
|
|
65
|
+
*
|
|
66
|
+
* The result covers each effect that an evaluation or a run gives the host (CAP.EFFECTS-SOUND). A procedure value keeps
|
|
67
|
+
* the capabilities of the document that made it, thus the effects of its body count at the body (CAP.LATENT). The
|
|
68
|
+
* contract describes the code of its kind. The code of an own entry for another evaluator is outside the result.
|
|
69
|
+
*
|
|
70
|
+
* @param machine - The world and its contracts, and the hook `permits`.
|
|
71
|
+
* @param doc - The document, as JSON.
|
|
72
|
+
* @param options - The type of a bare root value and the capabilities at the root.
|
|
73
|
+
* @returns The kinds of the effects, as texts with dots, each one time, in the order of the document.
|
|
74
|
+
*/
|
|
75
|
+
export function effectsOf(machine: Machine, doc: unknown, options: EffectsOptions = {}): readonly string[] {
|
|
76
|
+
const out: string[] = [];
|
|
77
|
+
const seen = new Set<string>();
|
|
78
|
+
for (const { kind } of reaching(machine, doc, options)) {
|
|
79
|
+
const text = kind.join(".");
|
|
80
|
+
if (seen.has(text)) continue;
|
|
81
|
+
seen.add(text);
|
|
82
|
+
out.push(text);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* This function compares the latent effects that a procedure declares with the body of the procedure (CAP.LATENT). A
|
|
89
|
+
* procedure entry is a kind whose code evaluates the document of a procedure. Its field `effects` declares the latent
|
|
90
|
+
* effects: the effects that a call of the procedure can cause. `check` tests each call against this declaration.
|
|
91
|
+
*
|
|
92
|
+
* The function gives a `#CAP!` for each effect of the body that can reach the host and that the declaration does not
|
|
93
|
+
* permit. An effect that a kind in the body masks fails in the body, thus it is not a latent effect.
|
|
94
|
+
*
|
|
95
|
+
* @param machine - The world and its contracts.
|
|
96
|
+
* @param declared - The latent effects that the procedure declares, for example the field `effects` of its kind.
|
|
97
|
+
* @param body - The document of the procedure, as JSON.
|
|
98
|
+
* @param options - The type of a bare root value.
|
|
99
|
+
* @returns The issues, in the order of the body.
|
|
100
|
+
*/
|
|
101
|
+
export function checkLatent(machine: Machine, declared: readonly Cap[], body: unknown, options: Omit<EffectsOptions, "caps"> = {}): readonly Issue[] {
|
|
102
|
+
const issues: Issue[] = [];
|
|
103
|
+
const list = declared.map((cap) => capKeys(cap).join(".")).join(", ");
|
|
104
|
+
const head = declared.length === 0 ? "The procedure declares no latent effect" : `The procedure declares the latent effects ${list}`;
|
|
105
|
+
for (const { kind, visit } of reaching(machine, body, options.declaredType === undefined ? {} : { declaredType: options.declaredType })) {
|
|
106
|
+
if (permits(declared, kind)) continue;
|
|
107
|
+
issues.push({ code: "#CAP!", place: visit.place, reason: `${head}, and its body can cause the effect ${kind.join(".")} here.`, at: visit.at });
|
|
108
|
+
}
|
|
109
|
+
return issues;
|
|
110
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -17,7 +17,8 @@ export type KernelCode =
|
|
|
17
17
|
| "#CALC!"
|
|
18
18
|
| "#DEPTH!"
|
|
19
19
|
| "#ABORT"
|
|
20
|
-
| "#REPLAY!"
|
|
20
|
+
| "#REPLAY!"
|
|
21
|
+
| "#TYPE!";
|
|
21
22
|
|
|
22
23
|
/** The code of an error value. A language can add its own codes. Each code starts with `#`. */
|
|
23
24
|
export type ErrorCode = KernelCode | `#${string}`;
|