@mark1russell7/coeval 0.3.2 → 0.4.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 +4 -4
- package/build/index.d.ts +56 -9
- package/build/index.d.ts.map +1 -1
- package/build/index.js +306 -98
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/deref.ts +10 -6
- package/src/evaluate.ts +66 -23
- package/src/formula.ts +91 -19
- package/src/parts.ts +42 -5
- package/src/static.ts +144 -42
- package/src/world.ts +15 -3
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.4.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",
|
package/src/deref.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Envelope, type Json, type JsonRecord, type Key, type TypeRef, field, isEnvelope, isKey, isRecord, typePath } from "./doc.ts";
|
|
2
2
|
import { type World, entriesAsJson, lookupKind } from "./world.ts";
|
|
3
|
+
import type { ErrorCode } from "./errors.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The brand of a host thing: an object of the host that answers the steps of a path itself. The brand is a
|
|
@@ -83,8 +84,11 @@ export interface Context {
|
|
|
83
84
|
/** The code of a `deref` evaluator: it answers one step of a path on a document of its kind. */
|
|
84
85
|
export type DerefCode = (doc: Envelope, key: Key) => Thing | undefined;
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The result of a step document: what it reached, or the reason of a failure. A failure can give its code, for
|
|
89
|
+
* example `#REF!` for a position outside a space. The default code is `#N/A`.
|
|
90
|
+
*/
|
|
91
|
+
export type WalkResult = { readonly ok: true; readonly thing: Thing } | { readonly ok: false; readonly reason: string; readonly code?: ErrorCode };
|
|
88
92
|
|
|
89
93
|
/** The input of a walker: the code of a step document, for example `nearest`. */
|
|
90
94
|
export interface WalkInput {
|
|
@@ -134,13 +138,13 @@ export interface Hop {
|
|
|
134
138
|
readonly by: HopBy;
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
/** The codes
|
|
141
|
+
/** The codes that a resolution gives itself. A walker can give another code for its failure. */
|
|
138
142
|
export type ResolveCode = "#REF!" | "#NAME?" | "#N/A" | "#CYCLE!" | "#STEP?";
|
|
139
143
|
|
|
140
144
|
/** The result of a resolution: what the path reached, with each hop, or an error with the hops before it. */
|
|
141
145
|
export type Resolution =
|
|
142
146
|
| { readonly ok: true; readonly thing: Thing; readonly hops: readonly Hop[] }
|
|
143
|
-
| { readonly ok: false; readonly code: ResolveCode; readonly reason: string; readonly hops: readonly Hop[] };
|
|
147
|
+
| { readonly ok: false; readonly code: ResolveCode | ErrorCode; readonly reason: string; readonly hops: readonly Hop[] };
|
|
144
148
|
|
|
145
149
|
/** The options of a resolution. */
|
|
146
150
|
export interface ResolveOptions {
|
|
@@ -333,7 +337,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
|
|
|
333
337
|
}
|
|
334
338
|
const hops: Hop[] | undefined = options.hops === false ? undefined : [];
|
|
335
339
|
const done = hops ?? NO_HOPS;
|
|
336
|
-
const failed = (code: ResolveCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
|
|
340
|
+
const failed = (code: ResolveCode | ErrorCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
|
|
337
341
|
let collapses = 0;
|
|
338
342
|
const adopt = nav.adopt ?? ((value: unknown): Thing => value);
|
|
339
343
|
|
|
@@ -399,7 +403,7 @@ export function resolve(nav: Navigator, ctx: Context, path: readonly unknown[],
|
|
|
399
403
|
return reached === undefined ? undefined : adopt(reached.thing);
|
|
400
404
|
},
|
|
401
405
|
});
|
|
402
|
-
if (!result.ok) return failed("#N/A", result.reason);
|
|
406
|
+
if (!result.ok) return failed(result.code ?? "#N/A", result.reason);
|
|
403
407
|
here = adopt(result.thing);
|
|
404
408
|
hops?.push({ step: envelope as Json, thing: here, by: "step" });
|
|
405
409
|
}
|
package/src/evaluate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName } from "./doc.ts";
|
|
2
|
-
import { splitParts } from "./parts.ts";
|
|
2
|
+
import { type Split, splitParts } from "./parts.ts";
|
|
3
3
|
import { ask, type Answer } from "./ladder.ts";
|
|
4
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";
|
|
@@ -207,6 +207,8 @@ export interface CompileInput {
|
|
|
207
207
|
* @returns The value with the items.
|
|
208
208
|
*/
|
|
209
209
|
fill(items: readonly unknown[]): unknown;
|
|
210
|
+
/** `true` when the position of the document declares an address. A reference there gives the thing, not its value. */
|
|
211
|
+
readonly address?: boolean;
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
/**
|
|
@@ -246,6 +248,8 @@ export interface EvalInput {
|
|
|
246
248
|
readonly context: Context;
|
|
247
249
|
/** The keys from the root document to this document. */
|
|
248
250
|
readonly place: readonly Key[];
|
|
251
|
+
/** `true` when the position of the document declares an address. */
|
|
252
|
+
readonly address?: boolean;
|
|
249
253
|
/**
|
|
250
254
|
* This function resolves a path from the context of the document.
|
|
251
255
|
*
|
|
@@ -847,7 +851,7 @@ function evaluatorOf(world: World, envelope: Envelope | undefined, found: Lookup
|
|
|
847
851
|
/** The meta of a node of a compilation without a trace. Only a trace reads the meta of a node. */
|
|
848
852
|
const NO_META: Meta = Object.freeze({ path: [], doc: { type: "?", value: null }, declared: false, answer: NO_ANSWER, side: "derive" });
|
|
849
853
|
|
|
850
|
-
function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number): { readonly code: Code; readonly run: RunCode } {
|
|
854
|
+
function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number, address = false): { readonly code: Code; readonly run: RunCode } {
|
|
851
855
|
const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
|
|
852
856
|
const doc: Envelope = envelope ?? { type: declared ?? "?", value: value as Json };
|
|
853
857
|
const meta0: Meta = c.traced ? { path: place.map(String), doc, declared: envelope === undefined, answer: NO_ANSWER, side: "derive" } : NO_META;
|
|
@@ -888,7 +892,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
888
892
|
}
|
|
889
893
|
const parts: CompiledPart[] = split.parts.map((slot) => {
|
|
890
894
|
const item = slot.item;
|
|
891
|
-
const compiled = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1);
|
|
895
|
+
const compiled = compileNode(c, item, place.concat(slot.path), slot.of, depth + 1, slot.address === true);
|
|
892
896
|
// The binding name is in the header of an envelope. Most parts have no name, thus the cheap test comes first.
|
|
893
897
|
const header = typeof (item as { readonly name?: unknown } | null)?.name === "string" && isEnvelope(item) ? (item as { readonly name: string }).name : undefined;
|
|
894
898
|
return header === undefined
|
|
@@ -905,41 +909,74 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
905
909
|
parts,
|
|
906
910
|
place,
|
|
907
911
|
machine: c.machine,
|
|
908
|
-
fill: (
|
|
912
|
+
fill: fillOf(split, doc),
|
|
913
|
+
...(address ? { address: true } : {}),
|
|
909
914
|
};
|
|
910
915
|
if (runner !== undefined) {
|
|
911
916
|
const inner: RunCode = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
|
|
912
|
-
const run
|
|
913
|
-
|
|
914
|
-
return placed(yield* inner(ctx, rt), place);
|
|
915
|
-
} catch (error) {
|
|
916
|
-
return placed(thrown(error, `the runner ${name}`), place);
|
|
917
|
-
}
|
|
918
|
-
};
|
|
919
|
-
const code: Code = (ctx, rt) =>
|
|
920
|
-
rt.running
|
|
921
|
-
? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
|
|
922
|
-
: drive(run(ctx, rt));
|
|
923
|
-
return finish(c, meta, code, run);
|
|
917
|
+
const run = placedRun(inner, place, name);
|
|
918
|
+
return finish(c, meta, runnerCode(run, place, kindText), run);
|
|
924
919
|
}
|
|
925
920
|
|
|
926
921
|
const inner: Code = isCompiledEvaluator(evaluator as Evaluator)
|
|
927
922
|
? (evaluator as CompiledEvaluator).compile(input)
|
|
928
923
|
: interpret(evaluator as EvalFn, input, split.fill);
|
|
929
|
-
const code
|
|
924
|
+
const code = placedCode(inner, place, name);
|
|
925
|
+
return finish(c, meta, code, runOf(code));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/*
|
|
929
|
+
* The helpers below make the closures that a plan keeps. Each one has its own small scope. A closure that a
|
|
930
|
+
* compilation made in its own scope keeps each variable of that scope that any closure there reads, for example
|
|
931
|
+
* the split of the value. Thus a plan with these helpers keeps only its code.
|
|
932
|
+
*/
|
|
933
|
+
|
|
934
|
+
/** This function gives the code of a document: the code of its evaluator, with the place of each error and of each exception. */
|
|
935
|
+
function placedCode(inner: Code, place: readonly Key[], name: string): Code {
|
|
936
|
+
return (ctx, rt) => {
|
|
930
937
|
try {
|
|
931
938
|
return placed(inner(ctx, rt), place);
|
|
932
939
|
} catch (error) {
|
|
933
940
|
return placed(thrown(error, `the evaluator ${name}`), place);
|
|
934
941
|
}
|
|
935
942
|
};
|
|
936
|
-
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** This function gives the steps of a runner, with the place of each error and of each exception. */
|
|
946
|
+
function placedRun(inner: RunCode, place: readonly Key[], name: string): RunCode {
|
|
947
|
+
return function* (ctx, rt) {
|
|
948
|
+
try {
|
|
949
|
+
return placed(yield* inner(ctx, rt), place);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
return placed(thrown(error, `the runner ${name}`), place);
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** This function gives the code of a runner in an evaluation: its steps, which cannot wait. */
|
|
957
|
+
function runnerCode(run: RunCode, place: readonly Key[], kindText: string): Code {
|
|
958
|
+
return (ctx, rt) =>
|
|
959
|
+
rt.running
|
|
960
|
+
? fail("#CALC!", `${kindText} waits for effects, and its parent cannot wait. Give the parent a runner.`, { place })
|
|
961
|
+
: drive(run(ctx, rt));
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** This function gives the steps of a run of an evaluator. The code runs at the first step, and it does not wait. */
|
|
965
|
+
function runOf(code: Code): RunCode {
|
|
966
|
+
return function* (ctx, rt) {
|
|
937
967
|
return code(ctx, rt);
|
|
938
|
-
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** This function gives the fill of a compile input: the value with an item in the place of each part. */
|
|
972
|
+
function fillOf(split: Extract<Split, { readonly ok: true }>, doc: Envelope): <P>(items: readonly P[]) => unknown {
|
|
973
|
+
return (items) => (split.parts.length === 0 ? doc.value : split.fill(items));
|
|
939
974
|
}
|
|
940
975
|
|
|
976
|
+
|
|
941
977
|
function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
|
|
942
978
|
const { kind, typePath, doc, data, place } = input;
|
|
979
|
+
const address = input.address === true ? { address: true } : {};
|
|
943
980
|
const compiled = input.parts;
|
|
944
981
|
const hasParts = compiled.length > 0;
|
|
945
982
|
return (ctx, rt) => {
|
|
@@ -958,6 +995,7 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
|
|
|
958
995
|
parts,
|
|
959
996
|
context: ctx,
|
|
960
997
|
place,
|
|
998
|
+
...address,
|
|
961
999
|
resolve: (path, options) => rt.resolve(ctx, path, options),
|
|
962
1000
|
effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
|
|
963
1001
|
fill: input.fill,
|
|
@@ -1021,16 +1059,21 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
|
|
|
1021
1059
|
};
|
|
1022
1060
|
}
|
|
1023
1061
|
const compiled: CompiledPart = { key: "", path: [], code: root.code, run: root.run };
|
|
1062
|
+
return planOf(machine, c.traced, compiled);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/** This function makes the object of a plan. Its closures keep only the machine and the compiled root. */
|
|
1066
|
+
function planOf(machine: Machine, traced: boolean, root: CompiledPart): Plan {
|
|
1024
1067
|
return {
|
|
1025
1068
|
machine,
|
|
1026
|
-
traced
|
|
1027
|
-
root
|
|
1069
|
+
traced,
|
|
1070
|
+
root,
|
|
1028
1071
|
evaluate(context: Context = makeContext(), evalOptions?: EvalOptions): Result {
|
|
1029
|
-
const s = new Session(machine, false,
|
|
1072
|
+
const s = new Session(machine, false, traced, undefined, undefined, evalOptions?.effects);
|
|
1030
1073
|
return root.code(rootContext(machine, context), s);
|
|
1031
1074
|
},
|
|
1032
1075
|
run(context: Context = makeContext(), runOptions: RunOptions = {}): Promise<Result> {
|
|
1033
|
-
const s = new Session(machine, true,
|
|
1076
|
+
const s = new Session(machine, true, traced, runOptions.perform, runOptions.signal, undefined);
|
|
1034
1077
|
return driveWaiting(root.run(rootContext(machine, context), s), runOptions.signal);
|
|
1035
1078
|
},
|
|
1036
1079
|
};
|
package/src/formula.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Json, isRecord } from "./doc.ts";
|
|
2
2
|
import type { Kind, WorldEntry } from "./world.ts";
|
|
3
|
-
import { valueOfThing } from "./deref.ts";
|
|
3
|
+
import { type Context, type Resolution, valueOfThing } from "./deref.ts";
|
|
4
4
|
import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
|
|
5
5
|
import type { Code, CompileInput, CompiledEvaluator, CompiledPart, Evaluator, Machine } from "./evaluate.ts";
|
|
6
6
|
|
|
@@ -13,8 +13,8 @@ export type OpFn = (...args: never[]) => unknown;
|
|
|
13
13
|
*/
|
|
14
14
|
export type LazyOp = (args: readonly (() => Result)[]) => Result;
|
|
15
15
|
|
|
16
|
-
/** The code of an extension: it gives the value of the data of an extension document. */
|
|
17
|
-
export type ExtFn = (data: Json) => Result;
|
|
16
|
+
/** The code of an extension: it gives the value of the data of an extension document, in the context of the document. */
|
|
17
|
+
export type ExtFn = (data: Json, context: Context) => Result;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The rules of one formula language: its operations, and the rules that differ between languages.
|
|
@@ -30,6 +30,8 @@ export type ExtFn = (data: Json) => Result;
|
|
|
30
30
|
* - `omit`: the errors that leave a field out of a `record`, as an absent value.
|
|
31
31
|
* - `missing`: the values that a reference cannot give. Such a value gives `#N/A`.
|
|
32
32
|
* - `check`: a check of the value of an operation, for example "a number must be finite".
|
|
33
|
+
* - `combine`: the error of two or more failed parts of an operation, a `record` or a `list`. With it, each part
|
|
34
|
+
* evaluates, and one failed part gives its own error. Without it, the first error stops the evaluation.
|
|
33
35
|
*/
|
|
34
36
|
export interface FormulaProfile {
|
|
35
37
|
readonly ops?: Readonly<Record<string, OpFn>> | ((name: string) => OpFn | undefined);
|
|
@@ -41,8 +43,12 @@ export interface FormulaProfile {
|
|
|
41
43
|
readonly omit?: (error: EvalError) => boolean;
|
|
42
44
|
readonly missing?: (value: unknown) => boolean;
|
|
43
45
|
readonly check?: (value: unknown, op: string) => Result | undefined;
|
|
46
|
+
readonly combine?: (errors: readonly EvalError[]) => EvalError;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/** The mark of a field that the profile leaves out of a record. */
|
|
50
|
+
const SKIPPED: unique symbol = Symbol("skipped");
|
|
51
|
+
|
|
46
52
|
/** The value of a name whose binding failed. A reference to the name gives the error. */
|
|
47
53
|
export class Held {
|
|
48
54
|
constructor(readonly error: EvalError) {}
|
|
@@ -97,6 +103,17 @@ function compiled(compile: (input: CompileInput) => Code): CompiledEvaluator {
|
|
|
97
103
|
return { compile };
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
/** This function evaluates parts in order. It gives the value of each part, or the first error. */
|
|
107
|
+
function valuesOf(parts: readonly CompiledPart[], ctx: Context, rt: Parameters<Code>[1]): unknown[] | Failed {
|
|
108
|
+
const values = new Array<unknown>(parts.length);
|
|
109
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
110
|
+
const result = (parts[index] as CompiledPart).code(ctx, rt);
|
|
111
|
+
if (!result.ok) return result;
|
|
112
|
+
values[index] = result.value;
|
|
113
|
+
}
|
|
114
|
+
return values;
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
function partsOf(value: unknown): Readonly<Record<string, CompiledPart>> {
|
|
101
118
|
return isRecord(value) ? (value as unknown as Readonly<Record<string, CompiledPart>>) : {};
|
|
102
119
|
}
|
|
@@ -168,6 +185,22 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
168
185
|
return Object.hasOwn(ops, name) ? ops[name] : undefined;
|
|
169
186
|
};
|
|
170
187
|
const notTruth = (what: string): Failed => fail("#VALUE!", `The ${what} must give a truth value.`);
|
|
188
|
+
const combine = profile.combine;
|
|
189
|
+
/** This function evaluates each part. Without `combine`, the first error stops it. A field that `omit` skips is absent. */
|
|
190
|
+
const collect = (parts: readonly CompiledPart[], ctx: Context, rt: Parameters<Code>[1], skip?: (error: EvalError) => boolean): (readonly unknown[]) | Failed => {
|
|
191
|
+
const values = new Array<unknown>(parts.length);
|
|
192
|
+
let errors: EvalError[] | undefined;
|
|
193
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
194
|
+
const result = (parts[index] as CompiledPart).code(ctx, rt);
|
|
195
|
+
if (result.ok) values[index] = result.value;
|
|
196
|
+
else if (skip !== undefined && skip(result.error)) values[index] = SKIPPED;
|
|
197
|
+
else if (combine === undefined) return result;
|
|
198
|
+
else (errors ??= []).push(result.error);
|
|
199
|
+
}
|
|
200
|
+
// The errors exist only with combine, so the second test only narrows the type.
|
|
201
|
+
if (errors === undefined || combine === undefined) return values;
|
|
202
|
+
return { ok: false, error: errors.length === 1 ? (errors[0] as EvalError) : combine(errors) };
|
|
203
|
+
};
|
|
171
204
|
|
|
172
205
|
return {
|
|
173
206
|
"formula.lit": compiled(({ doc }) => {
|
|
@@ -175,7 +208,7 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
175
208
|
return () => result;
|
|
176
209
|
}),
|
|
177
210
|
|
|
178
|
-
"formula.ref": compiled(({ doc }) => {
|
|
211
|
+
"formula.ref": compiled(({ doc, parts, fill, address }) => {
|
|
179
212
|
const path = doc.value;
|
|
180
213
|
if (!Array.isArray(path) || path.length === 0) {
|
|
181
214
|
const bad = fail("#VALUE!", 'The value of ref is a path: a list that starts with a root, for example ["names", "a"].');
|
|
@@ -183,17 +216,37 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
183
216
|
}
|
|
184
217
|
// A path through a name whose binding failed gives the error of the binding.
|
|
185
218
|
const head = path[0] === "names" && typeof path[1] === "string" ? path[1] : undefined;
|
|
219
|
+
const give = (found: Resolution): Result => {
|
|
220
|
+
if (!found.ok) return fail(found.code, found.reason);
|
|
221
|
+
// An address gives what the path reaches: a host thing stays a thing.
|
|
222
|
+
if (address === true) return ok(found.thing);
|
|
223
|
+
const reached = valueOfThing(found.thing);
|
|
224
|
+
if (reached instanceof Held) return { ok: false, error: reached.error };
|
|
225
|
+
if (missing !== undefined && missing(reached)) return fail("#N/A", "The path reaches no value.");
|
|
226
|
+
return ok(reached);
|
|
227
|
+
};
|
|
228
|
+
// The step documents of a path are its parts when the kind ref has a deep shape. They evaluate first, in order.
|
|
229
|
+
const computed = parts.length > 0;
|
|
186
230
|
return (ctx, rt) => {
|
|
187
231
|
if (head !== undefined && Object.hasOwn(ctx.names, head)) {
|
|
188
232
|
const bound = ctx.names[head];
|
|
189
233
|
if (bound instanceof Held) return { ok: false, error: bound.error };
|
|
190
234
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
235
|
+
if (!computed) return give(rt.resolve(ctx, path));
|
|
236
|
+
const steps = valuesOf(parts, ctx, rt);
|
|
237
|
+
return Array.isArray(steps) ? give(rt.resolve(ctx, fill(steps) as readonly unknown[])) : steps;
|
|
238
|
+
};
|
|
239
|
+
}),
|
|
240
|
+
|
|
241
|
+
"formula.step": compiled(({ doc, parts, fill }) => {
|
|
242
|
+
// A step document without parts is its own value. A step document with parts gets the result of each part.
|
|
243
|
+
if (parts.length === 0) {
|
|
244
|
+
const same = ok(doc);
|
|
245
|
+
return () => same;
|
|
246
|
+
}
|
|
247
|
+
return (ctx, rt) => {
|
|
248
|
+
const values = valuesOf(parts, ctx, rt);
|
|
249
|
+
return Array.isArray(values) ? ok({ ...doc, value: fill(values) as Json }) : values;
|
|
197
250
|
};
|
|
198
251
|
}),
|
|
199
252
|
|
|
@@ -201,6 +254,19 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
201
254
|
const fields = partsOf(value);
|
|
202
255
|
const keys = Object.keys(fields);
|
|
203
256
|
const codes = keys.map((key) => (fields[key] as CompiledPart).code);
|
|
257
|
+
if (combine !== undefined) {
|
|
258
|
+
// With combine, each field evaluates, and two failed fields give one error.
|
|
259
|
+
const parts = keys.map((key) => fields[key] as CompiledPart);
|
|
260
|
+
return (ctx, rt) => {
|
|
261
|
+
const values = collect(parts, ctx, rt, omit);
|
|
262
|
+
if (!Array.isArray(values)) return values as Failed;
|
|
263
|
+
const out: Record<string, unknown> = {};
|
|
264
|
+
keys.forEach((key, index) => {
|
|
265
|
+
if (values[index] !== SKIPPED) setOwn(out, key, values[index]);
|
|
266
|
+
});
|
|
267
|
+
return ok(out);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
204
270
|
return (ctx, rt) => {
|
|
205
271
|
const out: Record<string, unknown> = {};
|
|
206
272
|
for (let index = 0; index < keys.length; index += 1) {
|
|
@@ -216,13 +282,8 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
216
282
|
}),
|
|
217
283
|
|
|
218
284
|
"formula.list": compiled(({ parts }) => (ctx, rt) => {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
const result = (parts[index] as CompiledPart).code(ctx, rt);
|
|
222
|
-
if (!result.ok) return result;
|
|
223
|
-
out[index] = result.value;
|
|
224
|
-
}
|
|
225
|
-
return ok(out);
|
|
285
|
+
const values = collect(parts, ctx, rt);
|
|
286
|
+
return Array.isArray(values) ? ok(values) : (values as Failed);
|
|
226
287
|
}),
|
|
227
288
|
|
|
228
289
|
"formula.let": compiled(({ value }) => {
|
|
@@ -340,6 +401,17 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
340
401
|
}
|
|
341
402
|
const done = (out: unknown): Result => (check === undefined ? ok(out) : (check(out, name) ?? ok(out)));
|
|
342
403
|
const failed = (error: unknown): Result => thrown(error, `the operation ${name}`);
|
|
404
|
+
if (combine !== undefined) {
|
|
405
|
+
return (ctx, rt) => {
|
|
406
|
+
const values = collect(parts, ctx, rt);
|
|
407
|
+
if (!Array.isArray(values)) return values as Failed;
|
|
408
|
+
try {
|
|
409
|
+
return done(fn(...values));
|
|
410
|
+
} catch (error) {
|
|
411
|
+
return failed(error);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
343
415
|
if (parts.length === 1) {
|
|
344
416
|
const [a] = parts as readonly [CompiledPart];
|
|
345
417
|
return (ctx, rt) => {
|
|
@@ -389,9 +461,9 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
389
461
|
const unknown = fail("#NAME?", `The language has no extension ${name}.`);
|
|
390
462
|
return () => unknown;
|
|
391
463
|
}
|
|
392
|
-
return () => {
|
|
464
|
+
return (ctx) => {
|
|
393
465
|
try {
|
|
394
|
-
return handler(doc.value);
|
|
466
|
+
return handler(doc.value, ctx);
|
|
395
467
|
} catch (error) {
|
|
396
468
|
return thrown(error, `the extension ${name}`);
|
|
397
469
|
}
|
package/src/parts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Json, type Key, isEnvelope, isRecord } from "./doc.ts";
|
|
1
|
+
import { type Json, type Key, docEquals, isEnvelope, isRecord, typeName, typePath } from "./doc.ts";
|
|
2
2
|
import type { Shape } from "./world.ts";
|
|
3
3
|
|
|
4
4
|
/** One part of a value: its place in the value, the JSON of the part, and the type that its position declares. */
|
|
@@ -9,6 +9,8 @@ export interface PartSlot {
|
|
|
9
9
|
readonly key: string;
|
|
10
10
|
readonly item: Json;
|
|
11
11
|
readonly of?: string;
|
|
12
|
+
/** `true` when the position of the part declares an address: a reference there gives what its path reaches. */
|
|
13
|
+
readonly address?: true;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
type Template =
|
|
@@ -54,6 +56,27 @@ function hasSlots(template: Template): boolean {
|
|
|
54
56
|
return template.t !== "data";
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/** This function gives the reason why a value is not data of a shape, or `undefined` when it is. */
|
|
60
|
+
function dataFault(spec: Extract<Shape, { readonly shape: "text" | "number" | "one" }>, item: Json): string | undefined {
|
|
61
|
+
if (spec.shape === "text") return typeof item === "string" ? undefined : "The value must be a text.";
|
|
62
|
+
if (spec.shape === "one") return spec.values.some((value) => docEquals(value, item)) ? undefined : `The value must be one of ${JSON.stringify(spec.values)}.`;
|
|
63
|
+
if (typeof item !== "number") return "The value must be a number.";
|
|
64
|
+
if (spec.whole === true && !Number.isInteger(item)) return "The value must be a whole number.";
|
|
65
|
+
if (spec.min !== undefined && !(item >= spec.min)) return `The value must be ${String(spec.min)} or more.`;
|
|
66
|
+
if (spec.max !== undefined && !(item <= spec.max)) return `The value must be ${String(spec.max)} or less.`;
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** This function stops a split when a part has a type that its position does not permit. */
|
|
71
|
+
function kindFault(kinds: readonly (readonly Key[])[], item: Json, of: string | undefined, path: readonly Key[]): void {
|
|
72
|
+
const type = isEnvelope(item) ? typePath(item.type) : of === undefined ? undefined : [of];
|
|
73
|
+
const fits = type !== undefined && kinds.some((kind) => kind.length <= type.length && kind.every((key, index) => String(key) === String(type[index])));
|
|
74
|
+
if (fits) return;
|
|
75
|
+
const names = kinds.map((kind) => kind.join(".")).join(", ");
|
|
76
|
+
const what = isEnvelope(item) ? `The kind ${typeName(item.type)} is not permitted here.` : "A value without an envelope is not permitted here.";
|
|
77
|
+
throw new Bad(`${what} The permitted kinds are ${names}.`, path);
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
/**
|
|
58
81
|
* This function splits a value into its parts, by the shape that its kind declares. The evaluator, the view and
|
|
59
82
|
* the static kit use the same split, thus each one sees the same parts.
|
|
@@ -66,8 +89,8 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
|
|
|
66
89
|
// A value without parts is data: the split needs no template.
|
|
67
90
|
if (shape === undefined || shape.shape === "none") return { ok: true, parts: NO_SLOTS, data: value, fill: () => value };
|
|
68
91
|
const parts: PartSlot[] = [];
|
|
69
|
-
const slot = (item: Json, path: readonly Key[], of: string | undefined): Template => {
|
|
70
|
-
parts.push({ path, key: path.join("."), item, ...(of === undefined ? {} : { of }) });
|
|
92
|
+
const slot = (item: Json, path: readonly Key[], of: string | undefined, address = false): Template => {
|
|
93
|
+
parts.push({ path, key: path.join("."), item, ...(of === undefined ? {} : { of }), ...(address ? { address: true as const } : {}) });
|
|
71
94
|
return { t: "slot", index: parts.length - 1 };
|
|
72
95
|
};
|
|
73
96
|
|
|
@@ -86,12 +109,25 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
|
|
|
86
109
|
|
|
87
110
|
const walk = (spec: Shape | undefined, item: Json, path: readonly Key[]): Template => {
|
|
88
111
|
if (spec === undefined || spec.shape === "none") return { t: "data", value: item };
|
|
89
|
-
if (spec.shape === "
|
|
112
|
+
if (spec.shape === "text" || spec.shape === "number" || spec.shape === "one") {
|
|
113
|
+
const reason = dataFault(spec, item);
|
|
114
|
+
if (reason !== undefined) throw new Bad(reason, path);
|
|
115
|
+
return { t: "data", value: item };
|
|
116
|
+
}
|
|
117
|
+
if (spec.shape === "term") {
|
|
118
|
+
if (spec.kinds !== undefined) kindFault(spec.kinds, item, spec.of, path);
|
|
119
|
+
return slot(item, path, spec.of, spec.address === true);
|
|
120
|
+
}
|
|
90
121
|
if (spec.shape === "deep") return deep(item, path, spec.of);
|
|
91
122
|
if (spec.shape === "list") {
|
|
92
123
|
if (!Array.isArray(item)) throw new Bad("The value must be a list.", path);
|
|
124
|
+
const list = item as readonly Json[];
|
|
125
|
+
if (spec.distinct === true) {
|
|
126
|
+
const twice = list.findIndex((child, index) => list.slice(0, index).some((before) => docEquals(before, child)));
|
|
127
|
+
if (twice !== -1) throw new Bad("The items of the list must be different.", [...path, twice]);
|
|
128
|
+
}
|
|
93
129
|
const each: Shape = spec.item ?? { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) };
|
|
94
|
-
return { t: "list", items:
|
|
130
|
+
return { t: "list", items: list.map((child, index) => walk(each, child, [...path, index])) };
|
|
95
131
|
}
|
|
96
132
|
if (!isRecord(item)) throw new Bad("The value must be a record.", path);
|
|
97
133
|
if (spec.shape === "map") {
|
|
@@ -106,6 +142,7 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
|
|
|
106
142
|
: (spec.fields as Readonly<Record<string, Shape>>);
|
|
107
143
|
const fields: (readonly [string, Template])[] = [];
|
|
108
144
|
for (const key of Object.keys(item)) {
|
|
145
|
+
if (spec.closed === true && !Object.hasOwn(named, key)) throw new Bad(`The field ${key} is not a field of this kind.`, [...path, key]);
|
|
109
146
|
const fieldShape = Object.hasOwn(named, key) ? named[key] : spec.rest;
|
|
110
147
|
fields.push([key, walk(fieldShape, item[key] as Json, [...path, key])]);
|
|
111
148
|
}
|