@mark1russell7/coeval 0.3.0 → 0.3.2
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 +49 -12
- package/build/index.d.ts.map +1 -1
- package/build/index.js +139 -47
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/doc.ts +7 -3
- package/src/evaluate.ts +113 -28
- package/src/formula.ts +12 -5
- package/src/index.ts +1 -0
- package/src/parts.ts +7 -1
- package/src/static.ts +27 -14
- package/src/world.ts +8 -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.3.
|
|
4
|
+
"version": "0.3.2",
|
|
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,
|
|
1
|
+
import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName } from "./doc.ts";
|
|
2
2
|
import { 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,
|
|
@@ -145,10 +145,22 @@ export interface Runtime {
|
|
|
145
145
|
* This function drives several steps at the same time. When one waits, the others go on.
|
|
146
146
|
*
|
|
147
147
|
* @param list - One function for each item. Each function starts the steps of its item.
|
|
148
|
-
* @param options - The most items that run at the same time
|
|
148
|
+
* @param options - The most items that run at the same time, and the stop at the first failure.
|
|
149
149
|
* @returns The steps that give each result, in the order of the list.
|
|
150
150
|
*/
|
|
151
|
-
all(list: readonly (() => Steps)[], options?:
|
|
151
|
+
all(list: readonly (() => Steps)[], options?: AllOptions): Generator<Promise<unknown>, readonly Result[], unknown>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The options of a drive of parts at the same time.
|
|
156
|
+
*
|
|
157
|
+
* - `concurrency`: the most parts that run at the same time. The default is each part.
|
|
158
|
+
* - `failFast`: `true` stops at the first part that fails. The parts that did not start do not start, the parts that
|
|
159
|
+
* run stop at their wait, and the results come at once. Each part without a result gives `#ABORT`.
|
|
160
|
+
*/
|
|
161
|
+
export interface AllOptions {
|
|
162
|
+
readonly concurrency?: number;
|
|
163
|
+
readonly failFast?: boolean;
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
/** Compiled code: the evaluation of a document in a context. */
|
|
@@ -187,6 +199,14 @@ export interface CompileInput {
|
|
|
187
199
|
/** The keys from the root document to this document. */
|
|
188
200
|
readonly place: readonly Key[];
|
|
189
201
|
readonly machine: Machine;
|
|
202
|
+
/**
|
|
203
|
+
* This function gives the value of the document with an item in the place of each part. The containers on the way
|
|
204
|
+
* to a part are new. The data around the parts stays the same objects.
|
|
205
|
+
*
|
|
206
|
+
* @param items - One item for each part, in the order of `parts`, for example the results of the parts.
|
|
207
|
+
* @returns The value with the items.
|
|
208
|
+
*/
|
|
209
|
+
fill(items: readonly unknown[]): unknown;
|
|
190
210
|
}
|
|
191
211
|
|
|
192
212
|
/**
|
|
@@ -243,6 +263,14 @@ export interface EvalInput {
|
|
|
243
263
|
* @returns The payload, or the error `#CAP!`.
|
|
244
264
|
*/
|
|
245
265
|
effect(kind: Cap, payload: unknown): Result;
|
|
266
|
+
/**
|
|
267
|
+
* This function gives the value of the document with an item in the place of each part, for example the result of
|
|
268
|
+
* each part. The containers on the way to a part are new. The data around the parts stays the same objects.
|
|
269
|
+
*
|
|
270
|
+
* @param items - One item for each part, in the order of `parts`.
|
|
271
|
+
* @returns The value with the items.
|
|
272
|
+
*/
|
|
273
|
+
fill(items: readonly unknown[]): unknown;
|
|
246
274
|
}
|
|
247
275
|
|
|
248
276
|
/** A part for a runner. Its evaluation can wait for the effects in it. */
|
|
@@ -284,13 +312,22 @@ export interface RunInput {
|
|
|
284
312
|
*/
|
|
285
313
|
effect(kind: Cap, payload: unknown): Steps;
|
|
286
314
|
/**
|
|
287
|
-
* This function evaluates parts at the same time. Take the results with `yield*`.
|
|
315
|
+
* This function evaluates parts at the same time. Take the results with `yield*`. A runner can give its own parts,
|
|
316
|
+
* for example a part with a different context: each part needs only the function `evaluate`.
|
|
288
317
|
*
|
|
289
318
|
* @param parts - The parts.
|
|
290
|
-
* @param options - The most parts that run at the same time.
|
|
319
|
+
* @param options - The most parts that run at the same time, and the stop at the first failure.
|
|
291
320
|
* @returns The steps that give each result, in the order of the parts.
|
|
292
321
|
*/
|
|
293
|
-
all(parts: readonly RunPart[], options?:
|
|
322
|
+
all(parts: readonly RunPart[], options?: AllOptions): Generator<Promise<unknown>, readonly Result[], unknown>;
|
|
323
|
+
/**
|
|
324
|
+
* This function gives the value of the document with an item in the place of each part, for example the result of
|
|
325
|
+
* each part. The containers on the way to a part are new. The data around the parts stays the same objects.
|
|
326
|
+
*
|
|
327
|
+
* @param items - One item for each part, in the order of `parts`.
|
|
328
|
+
* @returns The value with the items.
|
|
329
|
+
*/
|
|
330
|
+
fill(items: readonly unknown[]): unknown;
|
|
294
331
|
}
|
|
295
332
|
|
|
296
333
|
/** An evaluator that is a function of its input. The kernel calls it for each evaluation. */
|
|
@@ -520,9 +557,11 @@ class Session implements Runtime {
|
|
|
520
557
|
return (yield done.then((value: unknown) => ok(value), failed)) as Result;
|
|
521
558
|
}
|
|
522
559
|
|
|
523
|
-
*all(list: readonly (() => Steps)[], options:
|
|
560
|
+
*all(list: readonly (() => Steps)[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
|
|
524
561
|
const results: Result[] = new Array<Result>(list.length);
|
|
525
562
|
const limit = Math.max(1, options.concurrency ?? list.length);
|
|
563
|
+
const failFast = options.failFast === true;
|
|
564
|
+
let failed = false;
|
|
526
565
|
interface Active {
|
|
527
566
|
readonly index: number;
|
|
528
567
|
readonly steps: Steps;
|
|
@@ -530,16 +569,20 @@ class Session implements Runtime {
|
|
|
530
569
|
}
|
|
531
570
|
const active: Active[] = [];
|
|
532
571
|
let next = 0;
|
|
572
|
+
const settle = (index: number, result: Result): void => {
|
|
573
|
+
results[index] = result;
|
|
574
|
+
if (failFast && !result.ok) failed = true;
|
|
575
|
+
};
|
|
533
576
|
const advance = (index: number, steps: Steps, input: { readonly value?: unknown; readonly error?: unknown; readonly threw: boolean }): void => {
|
|
534
577
|
let step: IteratorResult<Promise<unknown>, Result>;
|
|
535
578
|
try {
|
|
536
579
|
step = input.threw ? steps.throw(input.error) : steps.next(input.value);
|
|
537
580
|
} catch (error) {
|
|
538
|
-
|
|
581
|
+
settle(index, thrown(error, "a part"));
|
|
539
582
|
return;
|
|
540
583
|
}
|
|
541
584
|
if (step.done === true) {
|
|
542
|
-
|
|
585
|
+
settle(index, step.value);
|
|
543
586
|
return;
|
|
544
587
|
}
|
|
545
588
|
const item: Active = { index, steps, pending: Promise.resolve() };
|
|
@@ -550,7 +593,7 @@ class Session implements Runtime {
|
|
|
550
593
|
active.push(item);
|
|
551
594
|
};
|
|
552
595
|
const start = (): void => {
|
|
553
|
-
while (active.length < limit && next < list.length) {
|
|
596
|
+
while (!failed && active.length < limit && next < list.length) {
|
|
554
597
|
const index = next;
|
|
555
598
|
next += 1;
|
|
556
599
|
const begin = list[index] as () => Steps;
|
|
@@ -558,14 +601,14 @@ class Session implements Runtime {
|
|
|
558
601
|
try {
|
|
559
602
|
steps = begin();
|
|
560
603
|
} catch (error) {
|
|
561
|
-
|
|
604
|
+
settle(index, thrown(error, "a part"));
|
|
562
605
|
continue;
|
|
563
606
|
}
|
|
564
607
|
advance(index, steps, { threw: false });
|
|
565
608
|
}
|
|
566
609
|
};
|
|
567
610
|
start();
|
|
568
|
-
while (active.length > 0) {
|
|
611
|
+
while (active.length > 0 && !failed) {
|
|
569
612
|
const settled = (yield Promise.race(active.map((item) => item.pending))) as {
|
|
570
613
|
readonly item: Active;
|
|
571
614
|
readonly value?: unknown;
|
|
@@ -576,6 +619,16 @@ class Session implements Runtime {
|
|
|
576
619
|
advance(settled.item.index, settled.item.steps, settled);
|
|
577
620
|
start();
|
|
578
621
|
}
|
|
622
|
+
if (!failed) return results;
|
|
623
|
+
const stopped = fail("#ABORT", "The part stopped: a part before it failed, and the parts run with failFast.");
|
|
624
|
+
for (const item of active) {
|
|
625
|
+
try {
|
|
626
|
+
item.steps.return(stopped);
|
|
627
|
+
} catch {
|
|
628
|
+
// The finally code of a part threw. The part stops with the abort.
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
for (let index = 0; index < list.length; index += 1) if (results[index] === undefined) results[index] = stopped;
|
|
579
632
|
return results;
|
|
580
633
|
}
|
|
581
634
|
|
|
@@ -777,23 +830,50 @@ function finish(c: Compiler, meta: Meta, code: Code, run: RunCode): { readonly c
|
|
|
777
830
|
return { code: tracedCode, run: tracedRun };
|
|
778
831
|
}
|
|
779
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
|
+
|
|
780
850
|
function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number): { readonly code: Code; readonly run: RunCode } {
|
|
781
851
|
const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
|
|
782
852
|
const doc: Envelope = envelope ?? { type: declared ?? "?", value: value as Json };
|
|
783
|
-
const
|
|
784
|
-
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;
|
|
785
854
|
if (envelope === undefined && declared === undefined) {
|
|
786
855
|
return failing(c, meta0, place, "#PART?", "The value has no envelope, and its position declares no type.");
|
|
787
856
|
}
|
|
788
857
|
if (depth > c.maxDepth) return failing(c, meta0, place, "#DEPTH!", `The nesting is deeper than ${c.maxDepth} documents.`);
|
|
789
858
|
|
|
790
|
-
const
|
|
791
|
-
const found = lookupKind(
|
|
859
|
+
const world = c.machine.world;
|
|
860
|
+
const found = lookupKind(world, doc.type);
|
|
792
861
|
const kind = found.ok ? found.kind : undefined;
|
|
793
|
-
|
|
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
|
+
}
|
|
794
875
|
const kindText = typeName(doc.type);
|
|
795
|
-
if (
|
|
796
|
-
const name = answer.evaluator;
|
|
876
|
+
if (unknown) return failing(c, meta, place, "#KIND?", `The world has no kind ${kindText}.`);
|
|
797
877
|
const runners = c.machine.runners ?? {};
|
|
798
878
|
const runner = name !== undefined && Object.hasOwn(runners, name) ? runners[name] : undefined;
|
|
799
879
|
const evaluator = name !== undefined && Object.hasOwn(c.machine.evaluators, name) ? c.machine.evaluators[name] : undefined;
|
|
@@ -807,9 +887,13 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
807
887
|
return failing(c, meta, place, "#VALUE!", `${split.reason}${at} The kind is ${kindText}.`);
|
|
808
888
|
}
|
|
809
889
|
const parts: CompiledPart[] = split.parts.map((slot) => {
|
|
810
|
-
const
|
|
811
|
-
const
|
|
812
|
-
|
|
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 };
|
|
813
897
|
});
|
|
814
898
|
const typePath = found.ok ? found.path : [];
|
|
815
899
|
const input: CompileInput = {
|
|
@@ -821,9 +905,8 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
821
905
|
parts,
|
|
822
906
|
place,
|
|
823
907
|
machine: c.machine,
|
|
908
|
+
fill: (items) => (split.parts.length === 0 ? doc.value : split.fill(items)),
|
|
824
909
|
};
|
|
825
|
-
const who = `the evaluator ${name}`;
|
|
826
|
-
|
|
827
910
|
if (runner !== undefined) {
|
|
828
911
|
const inner: RunCode = isCompiledRunner(runner) ? runner.compileRun(input) : interpretRun(runner, input, split.fill);
|
|
829
912
|
const run: RunCode = function* (ctx, rt) {
|
|
@@ -847,7 +930,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
847
930
|
try {
|
|
848
931
|
return placed(inner(ctx, rt), place);
|
|
849
932
|
} catch (error) {
|
|
850
|
-
return placed(thrown(error,
|
|
933
|
+
return placed(thrown(error, `the evaluator ${name}`), place);
|
|
851
934
|
}
|
|
852
935
|
};
|
|
853
936
|
return finish(c, meta, code, function* (ctx, rt) {
|
|
@@ -877,6 +960,7 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
|
|
|
877
960
|
place,
|
|
878
961
|
resolve: (path, options) => rt.resolve(ctx, path, options),
|
|
879
962
|
effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
|
|
963
|
+
fill: input.fill,
|
|
880
964
|
});
|
|
881
965
|
};
|
|
882
966
|
}
|
|
@@ -904,6 +988,7 @@ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonl
|
|
|
904
988
|
resolve: (path, options) => rt.resolve(ctx, path, options),
|
|
905
989
|
effect: (effectKind, payload) => rt.runEffect(ctx, effectKind, payload, place),
|
|
906
990
|
all: (list, options) => rt.all(list.map((part) => () => part.evaluate()), options),
|
|
991
|
+
fill: input.fill,
|
|
907
992
|
});
|
|
908
993
|
};
|
|
909
994
|
}
|
|
@@ -940,8 +1025,8 @@ export function prepare(machine: Machine, doc: unknown, options: PrepareOptions
|
|
|
940
1025
|
machine,
|
|
941
1026
|
traced: c.traced,
|
|
942
1027
|
root: compiled,
|
|
943
|
-
evaluate(context: Context = makeContext(), evalOptions
|
|
944
|
-
const s = new Session(machine, false, c.traced, undefined, undefined, evalOptions
|
|
1028
|
+
evaluate(context: Context = makeContext(), evalOptions?: EvalOptions): Result {
|
|
1029
|
+
const s = new Session(machine, false, c.traced, undefined, undefined, evalOptions?.effects);
|
|
945
1030
|
return root.code(rootContext(machine, context), s);
|
|
946
1031
|
},
|
|
947
1032
|
run(context: Context = makeContext(), runOptions: RunOptions = {}): Promise<Result> {
|
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/index.ts
CHANGED
|
@@ -96,6 +96,7 @@ export {
|
|
|
96
96
|
export { type Classification, type Shape, classify, tag } from "./classify.ts";
|
|
97
97
|
export { type PartSlot, type Split, pathMatches, splitParts } from "./parts.ts";
|
|
98
98
|
export {
|
|
99
|
+
type AllOptions,
|
|
99
100
|
type Code,
|
|
100
101
|
type CompileInput,
|
|
101
102
|
type CompiledEvaluator,
|
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,
|
package/src/world.ts
CHANGED
|
@@ -90,10 +90,14 @@ export type Alias = { readonly type: "ref"; readonly value: readonly Json[] };
|
|
|
90
90
|
/** One entry of the world: a kind, a namespace, or an alias. */
|
|
91
91
|
export type WorldEntry = Kind | Namespace | Alias;
|
|
92
92
|
|
|
93
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* A world holds the kinds and the defaults that answer the questions. `fallback` is the kind of each first key that
|
|
95
|
+
* the world does not have. A world with a fallback is open: each path that starts with an unknown key names a kind.
|
|
96
|
+
*/
|
|
94
97
|
export interface World {
|
|
95
98
|
readonly kinds: Readonly<Record<string, WorldEntry>>;
|
|
96
99
|
readonly defaults: Readonly<Record<string, string>>;
|
|
100
|
+
readonly fallback?: Kind;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
/** The result of a lookup of a type in the world. */
|
|
@@ -139,7 +143,8 @@ function entryAt(entries: Readonly<Record<string, WorldEntry>>, key: Key): World
|
|
|
139
143
|
/**
|
|
140
144
|
* This function finds the kind that a type names. It walks the path from the root of the world, one key at a
|
|
141
145
|
* time. An alias on the way is followed until a kind answers (collapse). A cycle of aliases stops the lookup. In a
|
|
142
|
-
* namespace with a fallback, a key that the namespace does not have gets the fallback kind.
|
|
146
|
+
* namespace with a fallback, a key that the namespace does not have gets the fallback kind. In an open world, a first
|
|
147
|
+
* key that the world does not have gets the fallback kind of the world.
|
|
143
148
|
*
|
|
144
149
|
* @param world - The world.
|
|
145
150
|
* @param type - The reference to the type.
|
|
@@ -152,7 +157,7 @@ export function lookupKind(world: World, type: TypeRef): Lookup {
|
|
|
152
157
|
let aliases = 0;
|
|
153
158
|
for (;;) {
|
|
154
159
|
let entries = world.kinds;
|
|
155
|
-
let fallback: Kind | undefined;
|
|
160
|
+
let fallback: Kind | undefined = world.fallback;
|
|
156
161
|
let entry: WorldEntry | undefined;
|
|
157
162
|
let followed = false;
|
|
158
163
|
for (let index = 0; index < path.length; index += 1) {
|