@mark1russell7/coeval 0.3.1 → 0.3.3
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/build/index.d.ts.map +1 -1
- package/build/index.js +152 -55
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/doc.ts +7 -3
- package/src/evaluate.ts +102 -37
- package/src/formula.ts +12 -5
- package/src/parts.ts +7 -1
- package/src/static.ts +27 -14
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.3.
|
|
4
|
+
"version": "0.3.3",
|
|
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/doc.ts
CHANGED
|
@@ -106,6 +106,7 @@ export function typePath(type: TypeRef): readonly Key[] | undefined {
|
|
|
106
106
|
* @returns The text, for example `git.push`.
|
|
107
107
|
*/
|
|
108
108
|
export function typeName(type: TypeRef): string {
|
|
109
|
+
if (typeof type === "string") return type;
|
|
109
110
|
return typePath(type)?.join(".") ?? "(computed)";
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -126,9 +127,12 @@ function headerFits(value: JsonRecord): boolean {
|
|
|
126
127
|
* @returns `true` when the value has the form of an envelope.
|
|
127
128
|
*/
|
|
128
129
|
export function isEnvelope(value: unknown): value is JsonRecord & Envelope {
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
131
|
+
// The keys come first: most values that are not envelopes have another key, thus the check stops early.
|
|
132
|
+
for (const key in value) {
|
|
133
|
+
if (key !== "type" && key !== "value" && key !== "name" && key !== "own" && Object.hasOwn(value, key)) return false;
|
|
134
|
+
}
|
|
135
|
+
return isRecord(value) && headerFits(value);
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
/** A stored document: an envelope, and the keys of its store beside it, for example `_id`. */
|
package/src/evaluate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type Envelope, type Json, type Key, type TypeRef, isEnvelope,
|
|
2
|
-
import { splitParts } from "./parts.ts";
|
|
1
|
+
import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName } from "./doc.ts";
|
|
2
|
+
import { type Split, splitParts } from "./parts.ts";
|
|
3
3
|
import { ask, type Answer } from "./ladder.ts";
|
|
4
|
-
import { type Side, lookupKind } from "./world.ts";
|
|
4
|
+
import { type Lookup, type Side, type World, lookupKind } from "./world.ts";
|
|
5
5
|
import { type ErrorCode, type EvalError, type Result, fail, ok, placed, thrown, describe } from "./errors.ts";
|
|
6
6
|
import {
|
|
7
7
|
type Cap,
|
|
@@ -830,23 +830,50 @@ function finish(c: Compiler, meta: Meta, code: Code, run: RunCode): { readonly c
|
|
|
830
830
|
return { code: tracedCode, run: tracedRun };
|
|
831
831
|
}
|
|
832
832
|
|
|
833
|
+
/**
|
|
834
|
+
* This function finds the evaluator of the verb `eval` without the record of the rungs, for a compilation without a
|
|
835
|
+
* trace. It gives the same evaluator as `ask`: the own entry, then the kind, then the default of the world.
|
|
836
|
+
*/
|
|
837
|
+
function evaluatorOf(world: World, envelope: Envelope | undefined, found: Lookup): { readonly name?: string; readonly unknown: boolean } {
|
|
838
|
+
const own = envelope?.own;
|
|
839
|
+
const ownName = own !== undefined && Object.hasOwn(own, "eval") ? own["eval"] : undefined;
|
|
840
|
+
if (ownName !== undefined) return { name: ownName, unknown: false };
|
|
841
|
+
if (!found.ok) return { unknown: true };
|
|
842
|
+
const verbs = found.kind.verbs;
|
|
843
|
+
const name = Object.hasOwn(verbs, "eval") ? verbs["eval"] : Object.hasOwn(world.defaults, "eval") ? world.defaults["eval"] : undefined;
|
|
844
|
+
return name === undefined ? { unknown: false } : { name, unknown: false };
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** The meta of a node of a compilation without a trace. Only a trace reads the meta of a node. */
|
|
848
|
+
const NO_META: Meta = Object.freeze({ path: [], doc: { type: "?", value: null }, declared: false, answer: NO_ANSWER, side: "derive" });
|
|
849
|
+
|
|
833
850
|
function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number): { readonly code: Code; readonly run: RunCode } {
|
|
834
851
|
const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
|
|
835
852
|
const doc: Envelope = envelope ?? { type: declared ?? "?", value: value as Json };
|
|
836
|
-
const
|
|
837
|
-
const meta0: Meta = { path, doc, declared: envelope === undefined, answer: NO_ANSWER, side: "derive" };
|
|
853
|
+
const meta0: Meta = c.traced ? { path: place.map(String), doc, declared: envelope === undefined, answer: NO_ANSWER, side: "derive" } : NO_META;
|
|
838
854
|
if (envelope === undefined && declared === undefined) {
|
|
839
855
|
return failing(c, meta0, place, "#PART?", "The value has no envelope, and its position declares no type.");
|
|
840
856
|
}
|
|
841
857
|
if (depth > c.maxDepth) return failing(c, meta0, place, "#DEPTH!", `The nesting is deeper than ${c.maxDepth} documents.`);
|
|
842
858
|
|
|
843
|
-
const
|
|
844
|
-
const found = lookupKind(
|
|
859
|
+
const world = c.machine.world;
|
|
860
|
+
const found = lookupKind(world, doc.type);
|
|
845
861
|
const kind = found.ok ? found.kind : undefined;
|
|
846
|
-
|
|
862
|
+
let meta = meta0;
|
|
863
|
+
let name: string | undefined;
|
|
864
|
+
let unknown: boolean;
|
|
865
|
+
if (c.traced) {
|
|
866
|
+
const answer = ask(world, envelope ?? {}, "eval", declared === undefined ? {} : { declaredType: declared });
|
|
867
|
+
meta = { ...meta0, answer, side: kind?.side ?? "derive" };
|
|
868
|
+
name = answer.evaluator;
|
|
869
|
+
unknown = answer.error === "unknown-kind";
|
|
870
|
+
} else {
|
|
871
|
+
const quick = evaluatorOf(world, envelope, found);
|
|
872
|
+
name = quick.name;
|
|
873
|
+
unknown = quick.unknown;
|
|
874
|
+
}
|
|
847
875
|
const kindText = typeName(doc.type);
|
|
848
|
-
if (
|
|
849
|
-
const name = answer.evaluator;
|
|
876
|
+
if (unknown) return failing(c, meta, place, "#KIND?", `The world has no kind ${kindText}.`);
|
|
850
877
|
const runners = c.machine.runners ?? {};
|
|
851
878
|
const runner = name !== undefined && Object.hasOwn(runners, name) ? runners[name] : undefined;
|
|
852
879
|
const evaluator = name !== undefined && Object.hasOwn(c.machine.evaluators, name) ? c.machine.evaluators[name] : undefined;
|
|
@@ -860,9 +887,13 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
860
887
|
return failing(c, meta, place, "#VALUE!", `${split.reason}${at} The kind is ${kindText}.`);
|
|
861
888
|
}
|
|
862
889
|
const parts: CompiledPart[] = split.parts.map((slot) => {
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
890
|
+
const item = slot.item;
|
|
891
|
+
const compiled = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1);
|
|
892
|
+
// The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
|
|
893
|
+
const header = typeof (item as { readonly name?: unknown } | null)?.name === "string" && isEnvelope(item) ? (item as { readonly name: string }).name : undefined;
|
|
894
|
+
return header === undefined
|
|
895
|
+
? { key: slot.key, path: slot.path, code: compiled.code, run: compiled.run }
|
|
896
|
+
: { key: slot.key, path: slot.path, name: header, code: compiled.code, run: compiled.run };
|
|
866
897
|
});
|
|
867
898
|
const typePath = found.ok ? found.path : [];
|
|
868
899
|
const input: CompileInput = {
|
|
@@ -874,41 +905,70 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
874
905
|
parts,
|
|
875
906
|
place,
|
|
876
907
|
machine: c.machine,
|
|
877
|
-
fill: (
|
|
908
|
+
fill: fillOf(split, doc),
|
|
878
909
|
};
|
|
879
|
-
const who = `the evaluator ${name}`;
|
|
880
|
-
|
|
881
910
|
if (runner !== undefined) {
|
|
882
911
|
const inner: RunCode = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
|
|
883
|
-
const run
|
|
884
|
-
|
|
885
|
-
return placed(yield* inner(ctx, rt), place);
|
|
886
|
-
} catch (error) {
|
|
887
|
-
return placed(thrown(error, `the runner ${name}`), place);
|
|
888
|
-
}
|
|
889
|
-
};
|
|
890
|
-
const code: Code = (ctx, rt) =>
|
|
891
|
-
rt.running
|
|
892
|
-
? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
|
|
893
|
-
: drive(run(ctx, rt));
|
|
894
|
-
return finish(c, meta, code, run);
|
|
912
|
+
const run = placedRun(inner, place, name);
|
|
913
|
+
return finish(c, meta, runnerCode(run, place, kindText), run);
|
|
895
914
|
}
|
|
896
915
|
|
|
897
916
|
const inner: Code = isCompiledEvaluator(evaluator as Evaluator)
|
|
898
917
|
? (evaluator as CompiledEvaluator).compile(input)
|
|
899
918
|
: interpret(evaluator as EvalFn, input, split.fill);
|
|
900
|
-
const code
|
|
919
|
+
const code = placedCode(inner, place, name);
|
|
920
|
+
return finish(c, meta, code, runOf(code));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/*
|
|
924
|
+
* The helpers below make the closures that a plan keeps. Each one has its own small scope. A closure that a
|
|
925
|
+
* compilation made in its own scope keeps each variable of that scope that any closure there reads, for example
|
|
926
|
+
* the split of the value. Thus a plan with these helpers keeps only its code.
|
|
927
|
+
*/
|
|
928
|
+
|
|
929
|
+
/** This function gives the code of a document: the code of its evaluator, with the place of each error and of each exception. */
|
|
930
|
+
function placedCode(inner: Code, place: readonly Key[], name: string): Code {
|
|
931
|
+
return (ctx, rt) => {
|
|
901
932
|
try {
|
|
902
933
|
return placed(inner(ctx, rt), place);
|
|
903
934
|
} catch (error) {
|
|
904
|
-
return placed(thrown(error,
|
|
935
|
+
return placed(thrown(error, `the evaluator ${name}`), place);
|
|
905
936
|
}
|
|
906
937
|
};
|
|
907
|
-
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** This function gives the steps of a runner, with the place of each error and of each exception. */
|
|
941
|
+
function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode {
|
|
942
|
+
return function* (ctx, rt) {
|
|
943
|
+
try {
|
|
944
|
+
return placed(yield* inner(ctx, rt), place);
|
|
945
|
+
} catch (error) {
|
|
946
|
+
return placed(thrown(error, `the runner ${name}`), place);
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/** This function gives the code of a runner in an evaluation: its steps, which cannot wait. */
|
|
952
|
+
function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code {
|
|
953
|
+
return (ctx, rt) =>
|
|
954
|
+
rt.running
|
|
955
|
+
? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
|
|
956
|
+
: drive(run(ctx, rt));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** This function gives the steps of a run of an evaluator. The code runs at the first step, and it does not wait. */
|
|
960
|
+
function runOf(code: Code): RunCode {
|
|
961
|
+
return function* (ctx, rt) {
|
|
908
962
|
return code(ctx, rt);
|
|
909
|
-
}
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** This function gives the fill of a compile input: the value with an item in the place of each part. */
|
|
967
|
+
function fillOf(split: Extract<Split, { readonly ok: true }>, doc: Envelope): <P>(items: readonly P[]) => unknown {
|
|
968
|
+
return (items) => (split.parts.length === 0 ? doc.value : split.fill(items));
|
|
910
969
|
}
|
|
911
970
|
|
|
971
|
+
|
|
912
972
|
function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
|
|
913
973
|
const { kind, typePath, doc, data, place } = input;
|
|
914
974
|
const compiled = input.parts;
|
|
@@ -992,16 +1052,21 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
|
|
|
992
1052
|
};
|
|
993
1053
|
}
|
|
994
1054
|
const compiled: CompiledPart = { key: "", path: [], code: root.code, run: root.run };
|
|
1055
|
+
return planOf(machine, c.traced, compiled);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** This function makes the object of a plan. Its closures keep only the machine and the compiled root. */
|
|
1059
|
+
function planOf(machine: Machine, traced: boolean, root: CompiledPart): Plan {
|
|
995
1060
|
return {
|
|
996
1061
|
machine,
|
|
997
|
-
traced
|
|
998
|
-
root
|
|
999
|
-
evaluate(context: Context = makeContext(), evalOptions
|
|
1000
|
-
const s = new Session(machine, false,
|
|
1062
|
+
traced,
|
|
1063
|
+
root,
|
|
1064
|
+
evaluate(context: Context = makeContext(), evalOptions?: EvalOptions): Result {
|
|
1065
|
+
const s = new Session(machine, false, traced, undefined, undefined, evalOptions?.effects);
|
|
1001
1066
|
return root.code(rootContext(machine, context), s);
|
|
1002
1067
|
},
|
|
1003
1068
|
run(context: Context = makeContext(), runOptions: RunOptions = {}): Promise<Result> {
|
|
1004
|
-
const s = new Session(machine, true,
|
|
1069
|
+
const s = new Session(machine, true, traced, runOptions.perform, runOptions.signal, undefined);
|
|
1005
1070
|
return driveWaiting(root.run(rootContext(machine, context), s), runOptions.signal);
|
|
1006
1071
|
},
|
|
1007
1072
|
};
|
package/src/formula.ts
CHANGED
|
@@ -84,8 +84,13 @@ function strictTruthy(value: unknown): boolean | undefined {
|
|
|
84
84
|
return typeof value === "boolean" ? value : undefined;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* This function sets an own data property. Only the key `__proto__` needs `defineProperty`: an assignment to it
|
|
89
|
+
* changes the prototype. An assignment is faster for each other key of a new plain object.
|
|
90
|
+
*/
|
|
87
91
|
function setOwn(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
88
|
-
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
92
|
+
if (key === "__proto__") Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
93
|
+
else target[key] = value;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
function compiled(compile: (input: CompileInput) => Code): CompiledEvaluator {
|
|
@@ -193,16 +198,18 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
193
198
|
}),
|
|
194
199
|
|
|
195
200
|
"formula.record": compiled(({ value }) => {
|
|
196
|
-
const fields =
|
|
201
|
+
const fields = partsOf(value);
|
|
202
|
+
const keys = Object.keys(fields);
|
|
203
|
+
const codes = keys.map((key) => (fields[key] as CompiledPart).code);
|
|
197
204
|
return (ctx, rt) => {
|
|
198
205
|
const out: Record<string, unknown> = {};
|
|
199
|
-
for (
|
|
200
|
-
const result =
|
|
206
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
207
|
+
const result = (codes[index] as Code)(ctx, rt);
|
|
201
208
|
if (!result.ok) {
|
|
202
209
|
if (omit !== undefined && omit(result.error)) continue;
|
|
203
210
|
return result;
|
|
204
211
|
}
|
|
205
|
-
setOwn(out,
|
|
212
|
+
setOwn(out, keys[index] as string, result.value);
|
|
206
213
|
}
|
|
207
214
|
return ok(out);
|
|
208
215
|
};
|
package/src/parts.ts
CHANGED
|
@@ -42,10 +42,14 @@ class Bad {
|
|
|
42
42
|
) {}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** This function sets an own data property. Only the key `__proto__` needs `defineProperty`. */
|
|
45
46
|
function setOwn(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
46
|
-
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
47
|
+
if (key === "__proto__") Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
48
|
+
else target[key] = value;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
const NO_SLOTS: readonly PartSlot[] = Object.freeze([]);
|
|
52
|
+
|
|
49
53
|
function hasSlots(template: Template): boolean {
|
|
50
54
|
return template.t !== "data";
|
|
51
55
|
}
|
|
@@ -59,6 +63,8 @@ function hasSlots(template: Template): boolean {
|
|
|
59
63
|
* @returns The parts and a way to fill their places, or the reason why the value does not have the shape.
|
|
60
64
|
*/
|
|
61
65
|
export function splitParts(shape: Shape | undefined, value: Json): Split {
|
|
66
|
+
// A value without parts is data: the split needs no template.
|
|
67
|
+
if (shape === undefined || shape.shape === "none") return { ok: true, parts: NO_SLOTS, data: value, fill: () => value };
|
|
62
68
|
const parts: PartSlot[] = [];
|
|
63
69
|
const slot = (item: Json, path: readonly Key[], of: string | undefined): Template => {
|
|
64
70
|
parts.push({ path, key: path.join("."), item, ...(of === undefined ? {} : { of }) });
|
package/src/static.ts
CHANGED
|
@@ -111,6 +111,15 @@ function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots
|
|
|
111
111
|
return { sees, later };
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** The names of a walk without names. A walk does not change a set at any time: it makes a new set to add names. */
|
|
115
|
+
const NO_NAMES: ReadonlySet<string> = new Set();
|
|
116
|
+
|
|
117
|
+
/** This function gives the options of a walk without the names of the host. */
|
|
118
|
+
function withoutNames(options: WalkOptions): WalkOptions {
|
|
119
|
+
const { names: _host, ...rest } = options;
|
|
120
|
+
return rest;
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
/**
|
|
115
124
|
* This function visits each document of a document: the root and each part at each depth, by the contracts of the
|
|
116
125
|
* kinds. A part of a kind that the world does not have is not visited. The walk uses no stack of the host for the
|
|
@@ -131,9 +140,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
131
140
|
readonly names: ReadonlySet<string>;
|
|
132
141
|
readonly later: ReadonlySet<string>;
|
|
133
142
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
];
|
|
143
|
+
const names: ReadonlySet<string> = options.names === undefined || options.names.length === 0 ? NO_NAMES : new Set(options.names);
|
|
144
|
+
const stack: Item[] = [{ value: doc, place: [], at: [], declared: options.declaredType, depth: 0, names, later: NO_NAMES }];
|
|
137
145
|
while (stack.length > 0) {
|
|
138
146
|
const item = stack.pop() as Item;
|
|
139
147
|
const envelope = isEnvelope(item.value) ? (item.value as Envelope) : undefined;
|
|
@@ -144,16 +152,21 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
144
152
|
const doc: Envelope = envelope ?? { type: item.declared ?? "?", value: item.value as Json };
|
|
145
153
|
const found = lookupKind(world, doc.type);
|
|
146
154
|
const kind = found.ok ? found.kind : undefined;
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
const seen: DocVisit =
|
|
156
|
+
kind === undefined
|
|
157
|
+
? { place: item.place, at: item.at, doc, declared: envelope === undefined, depth: item.depth, names: item.names, later: item.later }
|
|
158
|
+
: {
|
|
159
|
+
place: item.place,
|
|
160
|
+
at: item.at,
|
|
161
|
+
doc,
|
|
162
|
+
declared: envelope === undefined,
|
|
163
|
+
kind,
|
|
164
|
+
typePath: found.ok ? found.path : [],
|
|
165
|
+
depth: item.depth,
|
|
166
|
+
names: item.names,
|
|
167
|
+
later: item.later,
|
|
168
|
+
};
|
|
169
|
+
const answer = visit(seen);
|
|
157
170
|
if (answer === "skip" || kind === undefined) continue;
|
|
158
171
|
const split = splitParts(kind.parts, doc.value);
|
|
159
172
|
if (!split.ok) {
|
|
@@ -206,7 +219,7 @@ export interface RefUse {
|
|
|
206
219
|
export function refsOf(world: World, doc: unknown, options: WalkOptions = {}): readonly RefUse[] {
|
|
207
220
|
const out: RefUse[] = [];
|
|
208
221
|
// The walk gets no names of the host: a name that the host binds is a dependency, thus its reference is free.
|
|
209
|
-
const
|
|
222
|
+
const rest: WalkOptions = options.names === undefined ? options : withoutNames(options);
|
|
210
223
|
walk(
|
|
211
224
|
world,
|
|
212
225
|
doc,
|