@mark1russell7/coeval 0.7.0 → 0.8.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 +31 -0
- package/build/index.d.ts +399 -100
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1818 -172
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agree.ts +26 -5
- package/src/codec.ts +185 -0
- package/src/conform.ts +2 -3
- package/src/doc.ts +2 -2
- package/src/evaluate.ts +281 -9
- package/src/formula.ts +19 -8
- package/src/index.ts +36 -2
- package/src/parts.ts +70 -1
- package/src/reference.ts +143 -18
- package/src/static.ts +54 -74
- package/src/stdlib.ts +220 -18
- package/src/syntax.ts +846 -0
- package/src/types.ts +28 -1
- package/src/world.ts +50 -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.8.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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"json"
|
|
35
35
|
],
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@mark1russell7/cue": "
|
|
37
|
+
"@mark1russell7/cue": "^1.0.2",
|
|
38
38
|
"@types/node": "^26.6.4",
|
|
39
39
|
"esbuild": "^0.28.2",
|
|
40
40
|
"typescript": "^7.0.2",
|
package/src/agree.ts
CHANGED
|
@@ -11,9 +11,11 @@ import { type Definitions, reference } from "./reference.ts";
|
|
|
11
11
|
* - `reference`: the reference interpreter, which compiles nothing.
|
|
12
12
|
* - `traced`: the traced evaluation, `evaluate`.
|
|
13
13
|
* - `again`: a second evaluation of the same plan.
|
|
14
|
+
* - `folds`: a second plan with the other choice of folds (PLAN.FOLD). It has no folds when the plan has them, and
|
|
15
|
+
* it has folds when the option `observe` stops the folds of the plan.
|
|
14
16
|
* - `other`: the second tier of a language, from the option `other`.
|
|
15
17
|
*/
|
|
16
|
-
export type Tier = "reference" | "traced" | "again" | "other";
|
|
18
|
+
export type Tier = "reference" | "traced" | "again" | "folds" | "other";
|
|
17
19
|
|
|
18
20
|
/** What a second tier gives: its result, and its effects when it records them. */
|
|
19
21
|
export interface TierResult {
|
|
@@ -41,7 +43,10 @@ export interface AgreeOptions {
|
|
|
41
43
|
readonly verb?: string;
|
|
42
44
|
/** The definitional code of the compiled evaluators of the machine, for the reference. */
|
|
43
45
|
readonly definitions?: Definitions;
|
|
44
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* The tiers to compare with the plan. The default is the reference, the traced evaluation, a second evaluation and
|
|
48
|
+
* the plan with the other choice of folds.
|
|
49
|
+
*/
|
|
45
50
|
readonly tiers?: readonly Exclude<Tier, "other">[];
|
|
46
51
|
/**
|
|
47
52
|
* This function gives the result of the second tier of a language, for example a compiler to loops.
|
|
@@ -70,13 +75,17 @@ export interface AgreeOptions {
|
|
|
70
75
|
/**
|
|
71
76
|
* This function gives what the host saw since its last call, for example the calls of an operation. `agree` uses
|
|
72
77
|
* it before the plan and after each tier, and compares the results. Thus a tier that evaluates a part that the plan
|
|
73
|
-
* does not evaluate fails.
|
|
78
|
+
* does not evaluate fails. A fold moves the calls of a part to the compilation, thus with this option the plan has
|
|
79
|
+
* no folds. The tier `folds` then has folds, and `agree` does not compare its calls.
|
|
74
80
|
*
|
|
75
81
|
* @returns What the host saw.
|
|
76
82
|
*/
|
|
77
83
|
readonly observe?: () => unknown;
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
/** The tiers that `agree` compares when its options name none. */
|
|
87
|
+
const DEFAULT_TIERS: readonly Exclude<Tier, "other">[] = /* @__PURE__ */ Object.freeze(["reference", "traced", "again", "folds"]);
|
|
88
|
+
|
|
80
89
|
/** The outcome of `agree`: the result of the plan, or the first tier that disagrees with it. */
|
|
81
90
|
export type Agreement =
|
|
82
91
|
| { readonly ok: true; readonly result: Result; readonly effects: readonly Effect[] }
|
|
@@ -195,7 +204,9 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
195
204
|
const tierInput: TierInput = { ...mock, ...(options.verb === undefined ? {} : { verb: options.verb }) };
|
|
196
205
|
const observe = options.observe;
|
|
197
206
|
observe?.();
|
|
198
|
-
|
|
207
|
+
// A fold moves host calls to the compilation, thus a plan whose calls the host compares has no folds (PLAN.FOLD).
|
|
208
|
+
const folds = observe === undefined;
|
|
209
|
+
const plan = prepare(machine, doc, { ...declared, fold: folds });
|
|
199
210
|
const effects: Effect[] = [];
|
|
200
211
|
const result = plan.evaluate(context, { effects, ...mock });
|
|
201
212
|
const saw = observe?.();
|
|
@@ -207,7 +218,7 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
207
218
|
return sameValue(saw, observe(), options.equal) ? undefined : "The host saw other calls in the tier.";
|
|
208
219
|
};
|
|
209
220
|
|
|
210
|
-
for (const tier of options.tiers ??
|
|
221
|
+
for (const tier of options.tiers ?? DEFAULT_TIERS) {
|
|
211
222
|
let other: Result;
|
|
212
223
|
let otherEffects: Effect[] = [];
|
|
213
224
|
if (tier === "reference") {
|
|
@@ -216,6 +227,16 @@ export function agree(machine: Machine, doc: unknown, context: Context = makeCon
|
|
|
216
227
|
const traced = evaluate(machine, doc, context, { ...declared, ...mock });
|
|
217
228
|
other = traced.result;
|
|
218
229
|
otherEffects = [...traced.effects];
|
|
230
|
+
} else if (tier === "folds") {
|
|
231
|
+
other = prepare(machine, doc, { ...declared, fold: !folds }).evaluate(context, { effects: otherEffects, ...mock });
|
|
232
|
+
if (observe !== undefined) {
|
|
233
|
+
// The folds of this tier call the host when it compiles. Thus only the result and the effects compare.
|
|
234
|
+
observe();
|
|
235
|
+
if (overflowed(result) || overflowed(other)) continue;
|
|
236
|
+
const why = resultDiffers(result, other, options) ?? effectsDiffer(effects, otherEffects, options);
|
|
237
|
+
if (why !== undefined) return { ok: false, tier, plan: result, other, reason: why };
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
219
240
|
} else {
|
|
220
241
|
other = plan.evaluate(context, { effects: otherEffects, ...mock });
|
|
221
242
|
}
|
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
|
+
}
|
package/src/conform.ts
CHANGED
|
@@ -27,12 +27,11 @@ import {
|
|
|
27
27
|
permits,
|
|
28
28
|
rootContext,
|
|
29
29
|
} from "./evaluate.ts";
|
|
30
|
-
import { type PartSlot, type Split, pathMatches, splitParts } from "./parts.ts";
|
|
30
|
+
import { type PartSlot, type Split, pathMatches, scopesOf, splitParts } from "./parts.ts";
|
|
31
31
|
import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
|
|
32
32
|
import { STD_EVALUATORS } from "./stdlib.ts";
|
|
33
33
|
import { sameValue } from "./agree.ts";
|
|
34
34
|
import { type Random, random } from "./generate.ts";
|
|
35
|
-
import { scopesOf } from "./static.ts";
|
|
36
35
|
import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, isKind, isNamespace, lookupKind } from "./world.ts";
|
|
37
36
|
|
|
38
37
|
/*
|
|
@@ -692,7 +691,7 @@ function finish(shot: Shot, start: () => Result, who: string): Result {
|
|
|
692
691
|
*/
|
|
693
692
|
function instanceOf(env: Env, under: Under, doc: Envelope, split: SplitOk, sample: Sample): Instance {
|
|
694
693
|
const fill = (items: readonly unknown[]): unknown => (split.parts.length === 0 ? doc.value : split.fill(items));
|
|
695
|
-
const base = { kind: sample.type, typePath: sample.typePath, doc, data: split.data, place: PLACE, fill };
|
|
694
|
+
const base = { kind: sample.type, typePath: sample.typePath, doc, data: split.data, place: PLACE, machine: env.machine, fill };
|
|
696
695
|
const who = `the ${env.runner ? "runner" : "evaluator"} ${env.name}`;
|
|
697
696
|
if (under.form === "eval" || under.form === "run") {
|
|
698
697
|
if (under.form === "eval") {
|
package/src/doc.ts
CHANGED
|
@@ -20,8 +20,8 @@ export type Key = string | number;
|
|
|
20
20
|
export type TypeRef = string | readonly Key[] | { readonly type: "ref"; readonly value: readonly Json[] };
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* A typed document: an envelope with a header and a value. The header names the type of the value. The
|
|
24
|
-
* not
|
|
23
|
+
* A typed document: an envelope with a header and a value. The header names the type of the value. The envelope
|
|
24
|
+
* does not change the value at any time. Thus any JSON value can have a type: a number, a list, a text or a record.
|
|
25
25
|
*/
|
|
26
26
|
export type Envelope = {
|
|
27
27
|
/** The type: a reference to a kind of the world. */
|