@mark1russell7/coeval 0.6.3 → 0.7.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 +63 -5
- package/build/index.d.ts +1771 -254
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5809 -511
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +15 -3
- package/src/codec.ts +185 -0
- 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 +480 -113
- package/src/facts.ts +252 -0
- package/src/formula.ts +450 -42
- package/src/graph.ts +399 -0
- package/src/index.ts +46 -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 +23 -9
- package/src/schedule.ts +436 -0
- package/src/static.ts +266 -29
- package/src/stdlib.ts +24 -7
- package/src/syntax.ts +846 -0
- package/src/types.ts +768 -0
- package/src/typing.ts +390 -0
- package/src/world.ts +63 -9
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.7.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/agree.ts
CHANGED
|
@@ -25,12 +25,20 @@ export interface TierResult {
|
|
|
25
25
|
export interface TierInput {
|
|
26
26
|
/** The mock of the host, when `agree` has one (AGREE.MOCK). The tier answers each effect with it. */
|
|
27
27
|
readonly perform?: Perform;
|
|
28
|
+
/** The verb of `agree`, when its options name one (VERB.AGREE). A tier that does not serve the verb fails. */
|
|
29
|
+
readonly verb?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** The options of `agree`. */
|
|
31
33
|
export interface AgreeOptions {
|
|
32
34
|
/** The type of a bare root value. */
|
|
33
35
|
readonly declaredType?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The verb of each tier, as in `prepare`. The default is `eval`. The plan, the reference, the traced evaluation and a
|
|
38
|
+
* second evaluation each ask the ladder for this verb (VERB.AGREE). The second tier of a language gets it in its
|
|
39
|
+
* input.
|
|
40
|
+
*/
|
|
41
|
+
readonly verb?: string;
|
|
34
42
|
/** The definitional code of the compiled evaluators of the machine, for the reference. */
|
|
35
43
|
readonly definitions?: Definitions;
|
|
36
44
|
/** The tiers to compare with the plan. The default is the reference, the traced evaluation and a second evaluation. */
|
|
@@ -40,7 +48,7 @@ export interface AgreeOptions {
|
|
|
40
48
|
*
|
|
41
49
|
* @param doc - The document.
|
|
42
50
|
* @param context - The context.
|
|
43
|
-
* @param input - The mock of the host, when `agree` has
|
|
51
|
+
* @param input - The mock of the host and the verb, when `agree` has them.
|
|
44
52
|
* @returns The result, or the result with the effects.
|
|
45
53
|
*/
|
|
46
54
|
readonly other?: (doc: unknown, context: Context, input: TierInput) => Result | TierResult;
|
|
@@ -179,8 +187,12 @@ function effectsDiffer(a: readonly Effect[], b: readonly Effect[], options: Agre
|
|
|
179
187
|
* @returns The result of the plan, or the first tier that disagrees and the reason.
|
|
180
188
|
*/
|
|
181
189
|
export function agree(machine: Machine, doc: unknown, context: Context = makeContext(), options: AgreeOptions = {}): Agreement {
|
|
182
|
-
const declared =
|
|
190
|
+
const declared = {
|
|
191
|
+
...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
|
|
192
|
+
...(options.verb === undefined ? {} : { verb: options.verb }),
|
|
193
|
+
};
|
|
183
194
|
const mock = options.perform === undefined ? {} : { perform: options.perform };
|
|
195
|
+
const tierInput: TierInput = { ...mock, ...(options.verb === undefined ? {} : { verb: options.verb }) };
|
|
184
196
|
const observe = options.observe;
|
|
185
197
|
observe?.();
|
|
186
198
|
const plan = prepare(machine, doc, declared);
|
|
@@ -214,7 +226,7 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
214
226
|
if (options.other !== undefined) {
|
|
215
227
|
let given: Result | TierResult;
|
|
216
228
|
try {
|
|
217
|
-
given = options.other(doc, context,
|
|
229
|
+
given = options.other(doc, context, tierInput);
|
|
218
230
|
} catch (error) {
|
|
219
231
|
given = thrown(error, "the tier");
|
|
220
232
|
}
|
package/src/codec.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { type Json, isEnvelope, isRecord } from "./doc.ts";
|
|
2
|
+
import { type Result, describe, fail, ok } from "./errors.ts";
|
|
3
|
+
import { type Machine, makeContext, prepare } from "./evaluate.ts";
|
|
4
|
+
import { type Domain, type DomainCodec, domainOf, member } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The codec of the values that JSON cannot write (K140, D40). A document holds such a value as a standard extension:
|
|
8
|
+
* `ext.number` for `NaN`, an infinity and `-0`, `ext.undefined` for `undefined`, and `ext.domain` for a member of a
|
|
9
|
+
* domain with a codec. Thus the same value has the same document in each language (D12).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The domains of a machine, by their names. */
|
|
13
|
+
type Domains = Readonly<Record<string, Domain>> | undefined;
|
|
14
|
+
|
|
15
|
+
/** This function gives the codec of a domain, or `undefined` when the domain has no codec. */
|
|
16
|
+
function codecOf(domain: Domain): DomainCodec | undefined {
|
|
17
|
+
const codec: unknown = domain.codec;
|
|
18
|
+
if (typeof codec !== "object" || codec === null) return undefined;
|
|
19
|
+
const { encode, decode } = codec as Partial<DomainCodec>;
|
|
20
|
+
return typeof encode === "function" && typeof decode === "function" ? (codec as DomainCodec) : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This function reads the value of an extension `ext.domain` (EXT.DOMAIN). The value is a record with only the text
|
|
25
|
+
* `domain` and the JSON `data`. The codec of the named domain reads the data, and the result must be a member of the
|
|
26
|
+
* domain.
|
|
27
|
+
*
|
|
28
|
+
* @param data - The value of the extension document.
|
|
29
|
+
* @param domains - The domains of the machine.
|
|
30
|
+
* @returns The member, `#NAME?` for a domain without a codec, or `#VALUE!` for data that the codec cannot read.
|
|
31
|
+
*/
|
|
32
|
+
export function decodeDomain(data: Json, domains: Domains): Result {
|
|
33
|
+
if (!isRecord(data) || typeof data["domain"] !== "string" || !Object.hasOwn(data, "data") || Object.keys(data).length !== 2) {
|
|
34
|
+
return fail("#VALUE!", "The value of ext.domain is a record with only the text domain and the data.");
|
|
35
|
+
}
|
|
36
|
+
const name = data["domain"];
|
|
37
|
+
const domain = domainOf(name, domains);
|
|
38
|
+
if (domain === undefined) return fail("#NAME?", `The machine has no domain ${name}.`);
|
|
39
|
+
const codec = codecOf(domain);
|
|
40
|
+
if (codec === undefined) return fail("#NAME?", `The domain ${name} has no codec.`);
|
|
41
|
+
let value: unknown;
|
|
42
|
+
try {
|
|
43
|
+
value = codec.decode(data["data"] as Json);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return fail("#VALUE!", `The codec of the domain ${name} cannot read the data: ${describe(error)}`, { cause: error });
|
|
46
|
+
}
|
|
47
|
+
if (!member(value, name, domains)) return fail("#VALUE!", `The codec of the domain ${name} gives a value that is not a member of the domain.`);
|
|
48
|
+
return ok(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* This function gives the static type of an extension `ext.domain`: the name of its domain when that domain has a
|
|
53
|
+
* codec. A run then gives a member of the domain or an error, because `decodeDomain` tests the membership.
|
|
54
|
+
*
|
|
55
|
+
* @param data - The value of the extension document.
|
|
56
|
+
* @param domains - The domains of the machine.
|
|
57
|
+
* @returns The name of the domain, or `unknown`.
|
|
58
|
+
*/
|
|
59
|
+
export function domainTypeOf(data: unknown, domains: Domains): string {
|
|
60
|
+
if (!isRecord(data) || typeof data["domain"] !== "string") return "unknown";
|
|
61
|
+
const domain = domainOf(data["domain"], domains);
|
|
62
|
+
return domain !== undefined && codecOf(domain) !== undefined ? data["domain"] : "unknown";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** This function sets an own data property. Only the key `__proto__` needs `defineProperty`. */
|
|
66
|
+
function setOwn(target: Record<string, Json>, key: string, value: Json): void {
|
|
67
|
+
if (key === "__proto__") Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
68
|
+
else target[key] = value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The state of one call of `encodeValue`. */
|
|
72
|
+
interface Encoder {
|
|
73
|
+
readonly domains: Domains;
|
|
74
|
+
/** The domains with a codec, in the order of their names. */
|
|
75
|
+
readonly codecs: readonly (readonly [string, DomainCodec])[];
|
|
76
|
+
/** The answer of `isJson` for each container that it examined. */
|
|
77
|
+
readonly json: WeakMap<object, boolean>;
|
|
78
|
+
/** The containers on the way from the root to the value. A container on the way again is a cycle. */
|
|
79
|
+
readonly open: Set<object>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* This function tells if JSON writes a value exactly. Such a value is a finite number other than `-0`, a text, a truth
|
|
84
|
+
* value, `null`, or a container of such values.
|
|
85
|
+
*/
|
|
86
|
+
function isJson(value: unknown, encoder: Encoder): boolean {
|
|
87
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
88
|
+
if (typeof value === "number") return Number.isFinite(value) && !Object.is(value, -0);
|
|
89
|
+
const list = Array.isArray(value);
|
|
90
|
+
if (!list && !isRecord(value)) return false;
|
|
91
|
+
const container = value as object;
|
|
92
|
+
const known = encoder.json.get(container);
|
|
93
|
+
if (known !== undefined) return known;
|
|
94
|
+
// A cycle is not JSON. The mark stays false until each item is JSON.
|
|
95
|
+
encoder.json.set(container, false);
|
|
96
|
+
let all = true;
|
|
97
|
+
if (list) {
|
|
98
|
+
const items = value as readonly unknown[];
|
|
99
|
+
for (let index = 0; index < items.length && all; index += 1) all = isJson(items[index], encoder);
|
|
100
|
+
} else {
|
|
101
|
+
const record = value as Readonly<Record<string, unknown>>;
|
|
102
|
+
for (const key of Object.keys(record)) {
|
|
103
|
+
if (!isJson(record[key], encoder)) {
|
|
104
|
+
all = false;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
encoder.json.set(container, all);
|
|
110
|
+
return all;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** This function gives the text of a number that JSON cannot write, for `ext.number`. */
|
|
114
|
+
function numberText(value: number): string {
|
|
115
|
+
if (Number.isNaN(value)) return "NaN";
|
|
116
|
+
if (value === Number.POSITIVE_INFINITY) return "Infinity";
|
|
117
|
+
if (value === Number.NEGATIVE_INFINITY) return "-Infinity";
|
|
118
|
+
return "-0";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** This function gives the term document of a value in one call of `encodeValue`. */
|
|
122
|
+
function encodeTerm(value: unknown, encoder: Encoder): Json {
|
|
123
|
+
if (isJson(value, encoder)) return isEnvelope(value) ? { type: "lit", value: value as Json } : (value as Json);
|
|
124
|
+
if (value === undefined) return { type: ["ext", "undefined"], value: null };
|
|
125
|
+
if (typeof value === "number") return { type: ["ext", "number"], value: numberText(value) };
|
|
126
|
+
for (const [name, codec] of encoder.codecs) {
|
|
127
|
+
if (member(value, name, encoder.domains)) return { type: ["ext", "domain"], value: { domain: name, data: codec.encode(value) } };
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(value) && !isRecord(value)) {
|
|
130
|
+
throw new TypeError(`encodeValue cannot write the value ${describe(value)}: no domain of the machine with a codec holds it.`);
|
|
131
|
+
}
|
|
132
|
+
if (encoder.open.has(value)) throw new TypeError("encodeValue cannot write a value with a cycle.");
|
|
133
|
+
encoder.open.add(value);
|
|
134
|
+
let out: Json;
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
// `Array.from` visits each hole of a sparse list as `undefined`.
|
|
137
|
+
out = { type: "list", value: Array.from(value as readonly unknown[], (item) => encodeTerm(item, encoder)) };
|
|
138
|
+
} else {
|
|
139
|
+
const fields: Record<string, Json> = {};
|
|
140
|
+
const record = value as Readonly<Record<string, unknown>>;
|
|
141
|
+
for (const key of Object.keys(record)) setOwn(fields, key, encodeTerm(record[key], encoder));
|
|
142
|
+
out = { type: "record", value: fields };
|
|
143
|
+
}
|
|
144
|
+
encoder.open.delete(value);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* This function writes a host value as a term document of the formula kit (EXT.ENCODE, D40). Thus the same value has
|
|
150
|
+
* the same document in each language (D12).
|
|
151
|
+
*
|
|
152
|
+
* - A value that JSON writes exactly stays as it is. A literal with the shape of an envelope goes into `lit`.
|
|
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.
|
|
155
|
+
* - A list or a plain record with such a value becomes the construct `list` or `record`.
|
|
156
|
+
*
|
|
157
|
+
* @param machine - The machine, or only its domains.
|
|
158
|
+
* @param value - The host value.
|
|
159
|
+
* @returns The document. `decodeValue` gives the value again (EXT.ROUNDTRIP).
|
|
160
|
+
* @throws {TypeError} The value has a cycle, or it is not JSON and no domain with a codec holds it.
|
|
161
|
+
*/
|
|
162
|
+
export function encodeValue(machine: Pick<Machine, "domains">, value: unknown): Json {
|
|
163
|
+
const domains = machine.domains;
|
|
164
|
+
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
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return encodeTerm(value, { domains, codecs, json: new WeakMap(), open: new Set() });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* This function reads a document of `encodeValue` again: it evaluates the document as a term, in an empty context.
|
|
177
|
+
* A bare root value is a literal.
|
|
178
|
+
*
|
|
179
|
+
* @param machine - A machine of the formula kit, with the domains of the document.
|
|
180
|
+
* @param doc - The document.
|
|
181
|
+
* @returns The value, or the error of the evaluation.
|
|
182
|
+
*/
|
|
183
|
+
export function decodeValue(machine: Machine, doc: unknown): Result {
|
|
184
|
+
return prepare(machine, doc, { declaredType: "lit" }).evaluate(makeContext());
|
|
185
|
+
}
|