@mark1russell7/coeval 0.0.0-bootstrap.0 → 0.1.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/LICENSE +21 -0
- package/build/index.d.ts +916 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +1856 -0
- package/build/index.js.map +1 -0
- package/package.json +45 -6
- package/src/classify.ts +54 -0
- package/src/context.ts +189 -0
- package/src/deref.ts +290 -0
- package/src/doc.ts +182 -0
- package/src/evaluate.ts +553 -0
- package/src/index.ts +118 -0
- package/src/ladder.ts +141 -0
- package/src/order.ts +111 -0
- package/src/parts.ts +43 -0
- package/src/stdlib.ts +283 -0
- package/src/world.ts +163 -0
- package/README.md +0 -3
package/src/ladder.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { type TypeRef, typeName } from "./doc.ts";
|
|
2
|
+
import { type Kind, type World, lookupKind } from "./world.ts";
|
|
3
|
+
|
|
4
|
+
export type { Kind, Namespace, PartsSpec, Side, World, WorldEntry } from "./world.ts";
|
|
5
|
+
|
|
6
|
+
/** The rungs of the ladder, in the order that a question visits them. */
|
|
7
|
+
export const RUNGS = ["own", "kind", "default"] as const;
|
|
8
|
+
|
|
9
|
+
/** One rung of the ladder. */
|
|
10
|
+
export type Rung = (typeof RUNGS)[number];
|
|
11
|
+
|
|
12
|
+
/** The place where the ladder found the type. */
|
|
13
|
+
export type KindSource = "document" | "container";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The outcome of one rung.
|
|
17
|
+
*
|
|
18
|
+
* - `answers`: the rung gives an evaluator.
|
|
19
|
+
* - `no-entry`: the rung has nothing for this verb, thus the question goes to the next rung.
|
|
20
|
+
* - `skipped`: the rung does not apply, because no type is known.
|
|
21
|
+
* - `unknown-kind`: the type is not a kind of the world. The question stops.
|
|
22
|
+
*/
|
|
23
|
+
export type Outcome = "answers" | "no-entry" | "skipped" | "unknown-kind";
|
|
24
|
+
|
|
25
|
+
/** The record of one rung that a question visited. */
|
|
26
|
+
export interface Step {
|
|
27
|
+
readonly rung: Rung;
|
|
28
|
+
readonly outcome: Outcome;
|
|
29
|
+
/** The evaluator, when the outcome is `answers`. */
|
|
30
|
+
readonly evaluator?: string;
|
|
31
|
+
/** The text of the type, at the kind rung. */
|
|
32
|
+
readonly kind?: string;
|
|
33
|
+
/** The place of the type, at the kind rung. */
|
|
34
|
+
readonly source?: KindSource;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The answer to a question. It keeps the record of each rung that the question visited. */
|
|
38
|
+
export interface Answer {
|
|
39
|
+
readonly verb: string;
|
|
40
|
+
/** The evaluator that answers. A question that stops with an error has no evaluator. */
|
|
41
|
+
readonly evaluator?: string;
|
|
42
|
+
/** The rung that answered. */
|
|
43
|
+
readonly rung?: Rung;
|
|
44
|
+
/** The reason when no rung answers. */
|
|
45
|
+
readonly error?: "unknown-kind" | "no-answer";
|
|
46
|
+
readonly steps: readonly Step[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The header that the ladder reads: the type and the own entries. An envelope is a header. A bare value has an
|
|
51
|
+
* empty header, and its position can declare its type.
|
|
52
|
+
*/
|
|
53
|
+
export interface Header {
|
|
54
|
+
readonly type?: TypeRef;
|
|
55
|
+
readonly own?: Readonly<Record<string, string>>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The options of a question. */
|
|
59
|
+
export interface AskOptions {
|
|
60
|
+
/** The type that the position of a bare value declares. The type of an envelope wins. */
|
|
61
|
+
readonly declaredType?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function entryOf(verbs: Readonly<Record<string, string>> | undefined, verb: string): string | undefined {
|
|
65
|
+
return verbs !== undefined && Object.hasOwn(verbs, verb) ? verbs[verb] : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* This function finds the kind that a type names, and follows each alias. It gives `undefined` for an unknown
|
|
70
|
+
* type.
|
|
71
|
+
*
|
|
72
|
+
* @param world - The world.
|
|
73
|
+
* @param type - The reference to the type.
|
|
74
|
+
* @returns The kind, or `undefined`.
|
|
75
|
+
*/
|
|
76
|
+
export function kindOf(world: World, type: TypeRef): Kind | undefined {
|
|
77
|
+
const found = lookupKind(world, type);
|
|
78
|
+
return found.ok ? found.kind : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* This function finds a kind by a text type. It is the same as `kindOf` for a one-key reference.
|
|
83
|
+
*
|
|
84
|
+
* @param world - The world.
|
|
85
|
+
* @param name - The text of the type.
|
|
86
|
+
* @returns The kind, or `undefined`.
|
|
87
|
+
*/
|
|
88
|
+
export function kindNamed(world: World, name: TypeRef): Kind | undefined {
|
|
89
|
+
return kindOf(world, name);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* This function asks a document a question, and gives the evaluator that answers it.
|
|
94
|
+
*
|
|
95
|
+
* The question visits the rungs in order: the own entry of the envelope, then its kind, then the default of the
|
|
96
|
+
* world. The type comes from the envelope, or else from the type that the position declares. The type is a
|
|
97
|
+
* reference into the world. The ladder does not guess a type from the shape of a value.
|
|
98
|
+
*
|
|
99
|
+
* @param world - The kinds and the defaults.
|
|
100
|
+
* @param doc - The header of the document: an envelope, or `{}` for a bare value.
|
|
101
|
+
* @param verb - The verb of the question, for example `eval` or `view`.
|
|
102
|
+
* @param options - The type that the position declares.
|
|
103
|
+
* @returns The answer, with the record of each rung that the question visited.
|
|
104
|
+
*/
|
|
105
|
+
export function ask(world: World, doc: Header, verb: string, options: AskOptions = {}): Answer {
|
|
106
|
+
const steps: Step[] = [];
|
|
107
|
+
|
|
108
|
+
const own = entryOf(doc.own, verb);
|
|
109
|
+
if (own !== undefined) {
|
|
110
|
+
steps.push({ rung: "own", outcome: "answers", evaluator: own });
|
|
111
|
+
return { verb, evaluator: own, rung: "own", steps };
|
|
112
|
+
}
|
|
113
|
+
steps.push({ rung: "own", outcome: "no-entry" });
|
|
114
|
+
|
|
115
|
+
const type: TypeRef | undefined = doc.type ?? options.declaredType;
|
|
116
|
+
if (type === undefined) {
|
|
117
|
+
steps.push({ rung: "kind", outcome: "skipped" });
|
|
118
|
+
} else {
|
|
119
|
+
const source: KindSource = doc.type === undefined ? "container" : "document";
|
|
120
|
+
const name = typeName(type);
|
|
121
|
+
const kind = kindOf(world, type);
|
|
122
|
+
if (kind === undefined) {
|
|
123
|
+
steps.push({ rung: "kind", outcome: "unknown-kind", kind: name, source });
|
|
124
|
+
return { verb, error: "unknown-kind", steps };
|
|
125
|
+
}
|
|
126
|
+
const entry = entryOf(kind.verbs, verb);
|
|
127
|
+
if (entry !== undefined) {
|
|
128
|
+
steps.push({ rung: "kind", outcome: "answers", evaluator: entry, kind: name, source });
|
|
129
|
+
return { verb, evaluator: entry, rung: "kind", steps };
|
|
130
|
+
}
|
|
131
|
+
steps.push({ rung: "kind", outcome: "no-entry", kind: name, source });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fallback = entryOf(world.defaults, verb);
|
|
135
|
+
if (fallback !== undefined) {
|
|
136
|
+
steps.push({ rung: "default", outcome: "answers", evaluator: fallback });
|
|
137
|
+
return { verb, evaluator: fallback, rung: "default", steps };
|
|
138
|
+
}
|
|
139
|
+
steps.push({ rung: "default", outcome: "no-entry" });
|
|
140
|
+
return { verb, error: "no-answer", steps };
|
|
141
|
+
}
|
package/src/order.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** One step of a sequence: its name, the names of the earlier steps that it reads, and if it has an effect. */
|
|
2
|
+
export interface StepSpec {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly reads: readonly string[];
|
|
5
|
+
readonly effect: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** An edge from a step that must come first to a step that must come after it. */
|
|
9
|
+
export type Edge = readonly [before: string, after: string];
|
|
10
|
+
|
|
11
|
+
/** A reference that a sequence cannot resolve. */
|
|
12
|
+
export interface OrderProblem {
|
|
13
|
+
readonly step: string;
|
|
14
|
+
readonly reads: string;
|
|
15
|
+
/** `unknown-name`: no step has this name. `read-before-defined`: the step with this name comes later. */
|
|
16
|
+
readonly problem: "unknown-name" | "read-before-defined";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This function finds the references that a sequence cannot resolve. A step can read only the steps before it.
|
|
21
|
+
*
|
|
22
|
+
* @param steps - The steps, in the written order.
|
|
23
|
+
* @returns One problem for each bad reference, in the written order.
|
|
24
|
+
*/
|
|
25
|
+
export function checkSteps(steps: readonly StepSpec[]): readonly OrderProblem[] {
|
|
26
|
+
const problems: OrderProblem[] = [];
|
|
27
|
+
steps.forEach((step, index) => {
|
|
28
|
+
for (const name of step.reads) {
|
|
29
|
+
const at = steps.findIndex((other) => other.id === name);
|
|
30
|
+
if (at === -1) problems.push({ step: step.id, reads: name, problem: "unknown-name" });
|
|
31
|
+
else if (at >= index) problems.push({ step: step.id, reads: name, problem: "read-before-defined" });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return problems;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* This function gives the data edges: a step comes after each earlier step that it reads.
|
|
39
|
+
*
|
|
40
|
+
* @param steps - The steps, in the written order.
|
|
41
|
+
* @returns The edges. A bad reference gives no edge.
|
|
42
|
+
*/
|
|
43
|
+
export function dataEdges(steps: readonly StepSpec[]): readonly Edge[] {
|
|
44
|
+
const edges: Edge[] = [];
|
|
45
|
+
steps.forEach((step, index) => {
|
|
46
|
+
for (const name of step.reads) {
|
|
47
|
+
const at = steps.findIndex((other) => other.id === name);
|
|
48
|
+
if (at !== -1 && at < index) edges.push([name, step.id]);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return edges;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* This function gives the effect edges: each step with an effect comes after the previous step with an effect.
|
|
56
|
+
* Effects keep their written order. Steps without an effect get no effect edge.
|
|
57
|
+
*
|
|
58
|
+
* @param steps - The steps, in the written order.
|
|
59
|
+
* @returns The edges, one for each pair of consecutive effect steps.
|
|
60
|
+
*/
|
|
61
|
+
export function effectEdges(steps: readonly StepSpec[]): readonly Edge[] {
|
|
62
|
+
const edges: Edge[] = [];
|
|
63
|
+
let previous: string | undefined;
|
|
64
|
+
for (const step of steps) {
|
|
65
|
+
if (!step.effect) continue;
|
|
66
|
+
if (previous !== undefined) edges.push([previous, step.id]);
|
|
67
|
+
previous = step.id;
|
|
68
|
+
}
|
|
69
|
+
return edges;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* This function tells if an order of the steps gives the same meaning as the written order.
|
|
74
|
+
* The order must contain each step one time, and it must obey the data edges and the effect edges.
|
|
75
|
+
*
|
|
76
|
+
* @param steps - The steps, in the written order.
|
|
77
|
+
* @param order - The names of the steps in the order to examine.
|
|
78
|
+
* @returns `true` when the order is permitted.
|
|
79
|
+
*/
|
|
80
|
+
export function isAllowedOrder(steps: readonly StepSpec[], order: readonly string[]): boolean {
|
|
81
|
+
if (order.length !== steps.length) return false;
|
|
82
|
+
if (new Set(order).size !== order.length) return false;
|
|
83
|
+
if (!steps.every((step) => order.includes(step.id))) return false;
|
|
84
|
+
const position = new Map(order.map((id, index) => [id, index] as const));
|
|
85
|
+
return [...dataEdges(steps), ...effectEdges(steps)].every(
|
|
86
|
+
([before, after]) => (position.get(before) ?? 0) < (position.get(after) ?? 0),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* This function puts the steps in layers. Each step goes one layer after the last step that it must follow.
|
|
92
|
+
* The steps of one layer do not depend on each other, thus they can run at the same time.
|
|
93
|
+
*
|
|
94
|
+
* @param steps - The steps, in the written order.
|
|
95
|
+
* @returns The layers, first to last. Each layer keeps the written order.
|
|
96
|
+
*/
|
|
97
|
+
export function layers(steps: readonly StepSpec[]): readonly (readonly string[])[] {
|
|
98
|
+
const edges = [...dataEdges(steps), ...effectEdges(steps)];
|
|
99
|
+
const depth = new Map<string, number>();
|
|
100
|
+
for (const step of steps) {
|
|
101
|
+
const before = edges.filter(([, after]) => after === step.id).map(([first]) => depth.get(first) ?? 0);
|
|
102
|
+
depth.set(step.id, before.length === 0 ? 0 : Math.max(...before) + 1);
|
|
103
|
+
}
|
|
104
|
+
const result: string[][] = [];
|
|
105
|
+
for (const step of steps) {
|
|
106
|
+
const at = depth.get(step.id) ?? 0;
|
|
107
|
+
while (result.length <= at) result.push([]);
|
|
108
|
+
result[at]?.push(step.id);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
package/src/parts.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Json, type JsonRecord, field, isRecord } from "./doc.ts";
|
|
2
|
+
import type { PartsSpec } from "./ladder.ts";
|
|
3
|
+
|
|
4
|
+
/** One part of a value: its key, the JSON of the part, and the type that its position declares. */
|
|
5
|
+
export interface PartSlot {
|
|
6
|
+
readonly key: string;
|
|
7
|
+
readonly item: Json;
|
|
8
|
+
readonly of?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The value of a document, split into its data and its parts. */
|
|
12
|
+
export type Split =
|
|
13
|
+
| { readonly ok: true; readonly data: Json; readonly parts: readonly PartSlot[] }
|
|
14
|
+
| { readonly ok: false; readonly reason: string };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This function splits a value into its data and its parts, by the contract of its kind. The evaluator and the
|
|
18
|
+
* view use the same split, thus a picture shows the parts that the evaluation gets.
|
|
19
|
+
*
|
|
20
|
+
* @param spec - The contract of the parts.
|
|
21
|
+
* @param value - The value of the document.
|
|
22
|
+
* @returns The data and the parts, or the reason why the value does not obey the contract.
|
|
23
|
+
*/
|
|
24
|
+
export function splitParts(spec: PartsSpec | undefined, value: Json): Split {
|
|
25
|
+
const of = (slot: { key: string; item: Json }, declared: string | undefined): PartSlot =>
|
|
26
|
+
declared === undefined ? slot : { ...slot, of: declared };
|
|
27
|
+
if (spec === undefined || spec.shape === "none") return { ok: true, data: value, parts: [] };
|
|
28
|
+
if (spec.shape === "list") {
|
|
29
|
+
if (!Array.isArray(value)) return { ok: false, reason: "The value must be a list." };
|
|
30
|
+
const items: readonly Json[] = value;
|
|
31
|
+
return { ok: true, data: null, parts: items.map((item, index) => of({ key: String(index), item }, spec.of)) };
|
|
32
|
+
}
|
|
33
|
+
if (!isRecord(value)) return { ok: false, reason: "The value must be a record." };
|
|
34
|
+
const record: JsonRecord = value;
|
|
35
|
+
const rest: Record<string, Json> = {};
|
|
36
|
+
for (const [key, item] of Object.entries(record)) if (!spec.fields.includes(key)) rest[key] = item;
|
|
37
|
+
const parts: PartSlot[] = [];
|
|
38
|
+
for (const key of spec.fields) {
|
|
39
|
+
const item = field(record, key);
|
|
40
|
+
if (item !== undefined) parts.push(of({ key, item }, spec.of));
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, data: rest, parts };
|
|
43
|
+
}
|
package/src/stdlib.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { type Json, type Key, field, isRecord } from "./doc.ts";
|
|
2
|
+
import { type Context, type Evaluator, type Machine, type Part, type Result, type RunPart, type Runner, fail, ok } from "./evaluate.ts";
|
|
3
|
+
import { type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
|
|
4
|
+
import type { World } from "./ladder.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The world of the examples: small kinds for numbers, lists, conditions, names, sequences and effects.
|
|
8
|
+
* Each kind names an evaluator for `eval` and a view for `view`. The site draws each document with its view.
|
|
9
|
+
*/
|
|
10
|
+
export const STD_WORLD: World = {
|
|
11
|
+
kinds: {
|
|
12
|
+
number: { verbs: { eval: "data.number", view: "view.value" }, summary: "A number. Its value is the result." },
|
|
13
|
+
text: { verbs: { eval: "data.text", view: "view.value" }, summary: "A text. Its value is the result." },
|
|
14
|
+
bool: { verbs: { eval: "data.bool", view: "view.value" }, summary: "A truth value. Its value is the result." },
|
|
15
|
+
add: {
|
|
16
|
+
verbs: { eval: "math.add", view: "view.op" },
|
|
17
|
+
parts: { shape: "list", of: "number" },
|
|
18
|
+
summary: "The sum of its parts. A bare number in its list gets the type number.",
|
|
19
|
+
},
|
|
20
|
+
multiply: {
|
|
21
|
+
verbs: { eval: "math.multiply", view: "view.op" },
|
|
22
|
+
parts: { shape: "list", of: "number" },
|
|
23
|
+
summary: "The product of its parts.",
|
|
24
|
+
},
|
|
25
|
+
greater: {
|
|
26
|
+
verbs: { eval: "math.greater", view: "view.op" },
|
|
27
|
+
parts: { shape: "list", of: "number" },
|
|
28
|
+
summary: "True when the first part is greater than the second part.",
|
|
29
|
+
},
|
|
30
|
+
list: { verbs: { eval: "list.all", view: "view.op" }, parts: { shape: "list" }, summary: "The results of its parts, as a list." },
|
|
31
|
+
if: {
|
|
32
|
+
verbs: { eval: "control.if", view: "view.op" },
|
|
33
|
+
parts: { shape: "record", fields: ["test", "then", "else"] },
|
|
34
|
+
summary: "It evaluates the test, then only the branch that the test selects.",
|
|
35
|
+
},
|
|
36
|
+
ref: {
|
|
37
|
+
verbs: { eval: "ref.resolve", view: "view.ref" },
|
|
38
|
+
summary: "A path from a root of the context. Each step goes up the ladder of the verb deref.",
|
|
39
|
+
},
|
|
40
|
+
nearest: {
|
|
41
|
+
verbs: { step: "walk.nearest", view: "view.card" },
|
|
42
|
+
summary: "A step of a path: it follows a key again and again, until it reaches a node of a type.",
|
|
43
|
+
},
|
|
44
|
+
panel: {
|
|
45
|
+
verbs: { eval: "tree.node", view: "view.op" },
|
|
46
|
+
parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
|
|
47
|
+
summary: "A node of a tree. Its parts see it as self, and each node links to its parent.",
|
|
48
|
+
},
|
|
49
|
+
box: {
|
|
50
|
+
verbs: { eval: "tree.node", view: "view.op" },
|
|
51
|
+
parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
|
|
52
|
+
summary: "A node of a tree, as panel. A formula in a box can read the parent of the box.",
|
|
53
|
+
},
|
|
54
|
+
math: {
|
|
55
|
+
kinds: {
|
|
56
|
+
sum: { type: "ref", value: ["world", "add"] },
|
|
57
|
+
product: { type: "ref", value: ["world", "multiply"] },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
seq: {
|
|
61
|
+
verbs: { eval: "control.seq", view: "view.seq" },
|
|
62
|
+
parts: { shape: "list" },
|
|
63
|
+
summary: "Its steps, in order. The result of a named step is a name for the later steps.",
|
|
64
|
+
},
|
|
65
|
+
pure: {
|
|
66
|
+
verbs: { eval: "control.pure", view: "view.op" },
|
|
67
|
+
parts: { shape: "list" },
|
|
68
|
+
summary: "It evaluates its part without capabilities, thus an effect in it fails.",
|
|
69
|
+
},
|
|
70
|
+
log: {
|
|
71
|
+
verbs: { eval: "effect.log", view: "view.op" },
|
|
72
|
+
parts: { shape: "list", of: "text" },
|
|
73
|
+
side: "run",
|
|
74
|
+
summary: "It writes the result of its part to the log. This is an effect.",
|
|
75
|
+
},
|
|
76
|
+
write: {
|
|
77
|
+
verbs: { eval: "effect.write", view: "view.op" },
|
|
78
|
+
parts: { shape: "record", fields: ["value"] },
|
|
79
|
+
side: "run",
|
|
80
|
+
summary: "It writes the result of its part to the store, at its key. This is an effect.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
defaults: { view: "view.card" },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function isNumber(value: Json): value is number {
|
|
87
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** This function evaluates each part, collects the errors, and gives the values when no part failed. */
|
|
91
|
+
function allValues(results: readonly Result[]): { readonly values: readonly Json[] } | { readonly error: Result } {
|
|
92
|
+
const errors = results.filter((result) => !result.ok);
|
|
93
|
+
const first = errors[0];
|
|
94
|
+
if (first !== undefined && errors.length === 1) return { error: first };
|
|
95
|
+
if (errors.length > 1) return { error: fail("#ARGS", `${errors.length} parts failed.`) };
|
|
96
|
+
return { values: results.map((result) => (result.ok ? result.value : null)) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function numbers(values: readonly Json[], kind: string): readonly number[] | Result {
|
|
100
|
+
const bad = values.findIndex((value) => !isNumber(value));
|
|
101
|
+
if (bad !== -1) return fail("#VALUE!", `Part ${bad} of ${kind} is not a number.`);
|
|
102
|
+
return values.filter(isNumber);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function dataOf(type: "number" | "string" | "boolean"): Evaluator {
|
|
106
|
+
return ({ data }) =>
|
|
107
|
+
typeof data === type && (type !== "number" || isNumber(data))
|
|
108
|
+
? ok(data)
|
|
109
|
+
: fail("#VALUE!", `The value must be a ${type === "string" ? "text" : type}.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The code of the evaluators of the standard world, by name. */
|
|
113
|
+
export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
|
|
114
|
+
"data.number": dataOf("number"),
|
|
115
|
+
"data.text": dataOf("string"),
|
|
116
|
+
"data.bool": dataOf("boolean"),
|
|
117
|
+
"math.add": ({ parts }) => {
|
|
118
|
+
const all = allValues(parts.map((part) => part.evaluate()));
|
|
119
|
+
if ("error" in all) return all.error;
|
|
120
|
+
const values = numbers(all.values, "add");
|
|
121
|
+
return Array.isArray(values) ? ok(values.reduce((sum, value) => sum + value, 0)) : (values as Result);
|
|
122
|
+
},
|
|
123
|
+
"math.multiply": ({ parts }) => {
|
|
124
|
+
const all = allValues(parts.map((part) => part.evaluate()));
|
|
125
|
+
if ("error" in all) return all.error;
|
|
126
|
+
const values = numbers(all.values, "multiply");
|
|
127
|
+
return Array.isArray(values) ? ok(values.reduce((product, value) => product * value, 1)) : (values as Result);
|
|
128
|
+
},
|
|
129
|
+
"math.greater": ({ parts }) => {
|
|
130
|
+
if (parts.length !== 2) return fail("#VALUE!", "greater needs exactly two parts.");
|
|
131
|
+
const all = allValues(parts.map((part) => part.evaluate()));
|
|
132
|
+
if ("error" in all) return all.error;
|
|
133
|
+
const values = numbers(all.values, "greater");
|
|
134
|
+
if (!Array.isArray(values)) return values as Result;
|
|
135
|
+
const [a, b] = values as readonly number[];
|
|
136
|
+
return ok((a ?? 0) > (b ?? 0));
|
|
137
|
+
},
|
|
138
|
+
"ref.resolve": ({ data, resolve }) => {
|
|
139
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
140
|
+
return fail("#VALUE!", "The value of ref is a path: a list that starts with a root, for example [\"names\", \"a\"].");
|
|
141
|
+
}
|
|
142
|
+
const path: readonly Json[] = data;
|
|
143
|
+
const found = resolve(path);
|
|
144
|
+
if (found.ok) return ok(valueOfThing(found.thing));
|
|
145
|
+
if (path[0] === "names" && found.hops.length === 1) return fail("#NAME?", `The context has no name ${String(path[1])}.`);
|
|
146
|
+
return fail(found.code, found.reason);
|
|
147
|
+
},
|
|
148
|
+
"tree.node": (input) => treeNode(input.parts, input.context, input.kind),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* This function evaluates a node of a tree. The node is a host thing. A step `parent` reaches the node that holds
|
|
153
|
+
* it. A step with the name of a part evaluates that part on demand, one time. Each part sees the node as self.
|
|
154
|
+
*/
|
|
155
|
+
function treeNode(parts: readonly Part[], context: Context, type: string): Result {
|
|
156
|
+
const memo = new Map<string, Result>();
|
|
157
|
+
const read = (key: string): Result | undefined => {
|
|
158
|
+
const known = memo.get(key);
|
|
159
|
+
if (known !== undefined) return known;
|
|
160
|
+
const part = parts.find((item) => item.key === key);
|
|
161
|
+
if (part === undefined) return undefined;
|
|
162
|
+
const result = part.evaluate({ ...context, self: me });
|
|
163
|
+
memo.set(key, result);
|
|
164
|
+
return result;
|
|
165
|
+
};
|
|
166
|
+
const me: HostThing & { readonly type: string } = {
|
|
167
|
+
[THING]: true,
|
|
168
|
+
type,
|
|
169
|
+
label: `node ${type}`,
|
|
170
|
+
deref(key: Key): Thing | undefined {
|
|
171
|
+
if (key === "parent") return context.self;
|
|
172
|
+
if (key === "type") return type;
|
|
173
|
+
const result = read(String(key));
|
|
174
|
+
return result?.ok === true ? result.value : undefined;
|
|
175
|
+
},
|
|
176
|
+
toJson(): Json {
|
|
177
|
+
const out: Record<string, Json> = {};
|
|
178
|
+
for (const part of parts) {
|
|
179
|
+
const result = read(part.key);
|
|
180
|
+
out[part.key] = result?.ok === true ? result.value : null;
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
const out: Record<string, Json> = {};
|
|
186
|
+
for (const part of parts) {
|
|
187
|
+
const result = read(part.key);
|
|
188
|
+
if (result === undefined) continue;
|
|
189
|
+
if (!result.ok) return result;
|
|
190
|
+
out[part.key] = result.value;
|
|
191
|
+
}
|
|
192
|
+
return ok(out);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The walker of `nearest`: it follows a key (the default is `parent`) until the node that it reaches has the
|
|
197
|
+
* wanted type. With `self: true`, the node where the walk starts is the first candidate. A node without that key
|
|
198
|
+
* ends the walk with an error.
|
|
199
|
+
*/
|
|
200
|
+
const nearest: Walker = ({ data, here, deref }) => {
|
|
201
|
+
const follow = isRecord(data) && typeof field(data, "follow") === "string" ? String(field(data, "follow")) : "parent";
|
|
202
|
+
const wanted = isRecord(data) ? field(data, "type") : undefined;
|
|
203
|
+
const fromSelf = isRecord(data) && field(data, "self") === true;
|
|
204
|
+
let at: Thing | undefined = here;
|
|
205
|
+
for (let hops = 0; hops < 256; hops += 1) {
|
|
206
|
+
if (hops > 0 || !fromSelf) at = at === undefined || at === null ? undefined : deref(at, follow);
|
|
207
|
+
if (at === undefined || at === null) return { ok: false, reason: `No node of the type ${String(wanted)} is on the way.` };
|
|
208
|
+
const type = isHostThing(at) ? (at as { readonly type?: unknown }).type : undefined;
|
|
209
|
+
if (type === wanted) return { ok: true, thing: at };
|
|
210
|
+
}
|
|
211
|
+
return { ok: false, reason: "The walk is too long." };
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** The walkers of the standard world, by name. */
|
|
215
|
+
export const STD_WALKERS: Readonly<Record<string, Walker>> = { "walk.nearest": nearest };
|
|
216
|
+
|
|
217
|
+
/** This function evaluates parts one after the other, and stops at the first error. */
|
|
218
|
+
function* inOrder(parts: readonly RunPart[]): Generator<Promise<Result>, { readonly values: readonly Json[] } | { readonly error: Result }, Result> {
|
|
219
|
+
const values: Json[] = [];
|
|
220
|
+
for (const part of parts) {
|
|
221
|
+
const result = yield* part.evaluate();
|
|
222
|
+
if (!result.ok) return { error: result };
|
|
223
|
+
values.push(result.value);
|
|
224
|
+
}
|
|
225
|
+
return { values };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The runners of the standard world, by name: the kinds that can hold effects. Each one evaluates its parts in
|
|
230
|
+
* order. In a run, an effect ends before the next step starts.
|
|
231
|
+
*/
|
|
232
|
+
export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
|
|
233
|
+
"control.seq": function* ({ parts, context }) {
|
|
234
|
+
let names: Record<string, Json> = { ...context.names };
|
|
235
|
+
let last: Json = null;
|
|
236
|
+
for (const part of parts) {
|
|
237
|
+
const result = yield* part.evaluate({ ...context, names });
|
|
238
|
+
if (!result.ok) return result;
|
|
239
|
+
names = { ...names, last: result.value };
|
|
240
|
+
if (part.name !== undefined) names = { ...names, [part.name]: result.value };
|
|
241
|
+
last = result.value;
|
|
242
|
+
}
|
|
243
|
+
return ok(last);
|
|
244
|
+
},
|
|
245
|
+
"control.pure": function* ({ parts, context }) {
|
|
246
|
+
const only = parts[0];
|
|
247
|
+
if (only === undefined) return fail("#VALUE!", "pure needs one part.");
|
|
248
|
+
return yield* only.evaluate({ ...context, caps: [] });
|
|
249
|
+
},
|
|
250
|
+
"control.if": function* ({ parts }) {
|
|
251
|
+
const test = parts.find((part) => part.key === "test");
|
|
252
|
+
if (test === undefined) return fail("#VALUE!", "if needs a test.");
|
|
253
|
+
const chosen = yield* test.evaluate();
|
|
254
|
+
if (!chosen.ok) return chosen;
|
|
255
|
+
if (typeof chosen.value !== "boolean") return fail("#VALUE!", "The test of if must give a truth value.");
|
|
256
|
+
const branch = parts.find((part) => part.key === (chosen.value === true ? "then" : "else"));
|
|
257
|
+
return branch === undefined ? ok(null) : yield* branch.evaluate();
|
|
258
|
+
},
|
|
259
|
+
"list.all": function* ({ parts }) {
|
|
260
|
+
const all = yield* inOrder(parts);
|
|
261
|
+
return "error" in all ? all.error : ok(all.values);
|
|
262
|
+
},
|
|
263
|
+
"effect.log": function* ({ parts, effect }) {
|
|
264
|
+
const all = yield* inOrder(parts);
|
|
265
|
+
if ("error" in all) return all.error;
|
|
266
|
+
return yield* effect("log", all.values.length === 1 ? (all.values[0] ?? null) : all.values);
|
|
267
|
+
},
|
|
268
|
+
"effect.write": function* ({ parts, data, effect }) {
|
|
269
|
+
const key = isRecord(data) ? field(data, "key") : undefined;
|
|
270
|
+
if (typeof key !== "string") return fail("#VALUE!", "write needs a text key.");
|
|
271
|
+
const value = parts.find((part) => part.key === "value");
|
|
272
|
+
if (value === undefined) return fail("#VALUE!", "write needs a value.");
|
|
273
|
+
const result = yield* value.evaluate();
|
|
274
|
+
if (!result.ok) return result;
|
|
275
|
+
return yield* effect("write", { key, value: result.value });
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/** The machine of the examples: the standard world, its evaluators, its runners and its walkers. */
|
|
280
|
+
export const STD_MACHINE: Machine = { world: STD_WORLD, evaluators: STD_EVALUATORS, runners: STD_RUNNERS, walkers: STD_WALKERS };
|
|
281
|
+
|
|
282
|
+
/** The capabilities of the examples at the root: the two effects of the standard world. */
|
|
283
|
+
export const STD_CAPS: readonly string[] = ["log", "write"];
|