@mark1russell7/coeval 0.7.1 → 0.9.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 +1 -1
- package/build/index.d.ts +226 -99
- package/build/index.d.ts.map +1 -1
- package/build/index.js +977 -146
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agree.ts +26 -5
- package/src/conform.ts +1 -2
- package/src/doc.ts +2 -2
- package/src/evaluate.ts +275 -7
- package/src/facts.ts +17 -3
- package/src/formula.ts +121 -7
- package/src/index.ts +19 -1
- package/src/parts.ts +70 -1
- package/src/reference.ts +145 -19
- package/src/static.ts +122 -74
- package/src/stdlib.ts +235 -20
- package/src/world.ts +54 -1
package/src/formula.ts
CHANGED
|
@@ -498,19 +498,118 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
|
|
|
498
498
|
* This function makes the evaluators of the formula kit for one language. Each evaluator compiles its document
|
|
499
499
|
* one time into code. Each evaluator also holds its type rule, for a check of types (TYPE.SYNTH).
|
|
500
500
|
*
|
|
501
|
+
* Equal profiles give the same evaluator objects (COMPOSE.UNIFY, D46). Thus two machines of one profile merge without
|
|
502
|
+
* a conflict. Each call gives a new record. Each evaluator object is frozen, because many machines can share it.
|
|
503
|
+
*
|
|
501
504
|
* @param profile - The operations and the rules of the language.
|
|
502
505
|
* @returns The evaluators, by their name.
|
|
503
506
|
*/
|
|
504
507
|
export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record<string, Evaluator>> {
|
|
508
|
+
return { ...kitOf(profile).evaluators };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** This function makes new evaluators of the formula kit for the fields of one profile. */
|
|
512
|
+
function makeEvaluators(profile: FormulaProfile): Readonly<Record<string, Evaluator>> {
|
|
505
513
|
const evaluators = formulaCode(profile);
|
|
506
514
|
const types = formulaTypes(profile);
|
|
507
515
|
const out: Record<string, Evaluator> = {};
|
|
508
516
|
for (const name of Object.keys(evaluators)) {
|
|
509
517
|
const evaluator = evaluators[name] as CompiledEvaluator;
|
|
510
518
|
const type = Object.hasOwn(types, name) ? types[name] : undefined;
|
|
511
|
-
out[name] = type === undefined ? evaluator : { compile: evaluator.compile, type };
|
|
519
|
+
out[name] = Object.freeze(type === undefined ? evaluator : { compile: evaluator.compile, type });
|
|
520
|
+
}
|
|
521
|
+
return Object.freeze(out);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** The keys of a profile that the kit reads. The kit ignores each other key. */
|
|
525
|
+
const PROFILE_KEYS = /* @__PURE__ */ Object.freeze([
|
|
526
|
+
"ops",
|
|
527
|
+
"lazy",
|
|
528
|
+
"ext",
|
|
529
|
+
"truthy",
|
|
530
|
+
"otherwise",
|
|
531
|
+
"logic",
|
|
532
|
+
"omit",
|
|
533
|
+
"missing",
|
|
534
|
+
"check",
|
|
535
|
+
"combine",
|
|
536
|
+
"pure",
|
|
537
|
+
"volatile",
|
|
538
|
+
"sigs",
|
|
539
|
+
] as const satisfies readonly (keyof FormulaProfile)[]);
|
|
540
|
+
|
|
541
|
+
/** The evaluators and the hooks of the formula kit for one profile, with the fields that made them. */
|
|
542
|
+
interface Kit {
|
|
543
|
+
readonly fields: FormulaProfile;
|
|
544
|
+
readonly evaluators: Readonly<Record<string, Evaluator>>;
|
|
545
|
+
readonly known: (path: readonly Key[], evaluator: string) => true | string;
|
|
546
|
+
readonly facts: (path: readonly Key[], code: string) => KindFacts | undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** The number of recent kits that a profile in a new object can find by its fields. */
|
|
550
|
+
const RECENT_KITS = 16;
|
|
551
|
+
|
|
552
|
+
/** The kit of each profile object. A kit goes when its profile object goes. */
|
|
553
|
+
const kitsByProfile = /* @__PURE__ */ new WeakMap<object, Kit>();
|
|
554
|
+
|
|
555
|
+
/** The recent kits, the most recent first. The list holds `RECENT_KITS` kits or fewer. */
|
|
556
|
+
const recentKits: Kit[] = [];
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* This function gives the canonical form of a profile: a frozen record of the fields that the kit reads. A field
|
|
560
|
+
* with the value `undefined` is absent, but `otherwise` is present when the profile has it as an own key. A list
|
|
561
|
+
* is a frozen copy. Each other value stays the same object.
|
|
562
|
+
*/
|
|
563
|
+
function fieldsOf(profile: FormulaProfile): FormulaProfile {
|
|
564
|
+
const out: Record<string, unknown> = {};
|
|
565
|
+
for (const key of PROFILE_KEYS) {
|
|
566
|
+
if (key === "otherwise" ? !Object.hasOwn(profile, key) : profile[key] === undefined) continue;
|
|
567
|
+
const value: unknown = profile[key];
|
|
568
|
+
out[key] = Array.isArray(value) ? Object.freeze([...(value as unknown[])]) : value;
|
|
512
569
|
}
|
|
513
|
-
return out;
|
|
570
|
+
return Object.freeze(out);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* This function tells if two canonical forms are equal. They must have the same keys. A function, a record and a host
|
|
575
|
+
* value are equal only to themselves. Two lists are equal when their items are the same, in order.
|
|
576
|
+
*/
|
|
577
|
+
function sameFields(a: FormulaProfile, b: FormulaProfile): boolean {
|
|
578
|
+
const left = a as Readonly<Record<string, unknown>>;
|
|
579
|
+
const right = b as Readonly<Record<string, unknown>>;
|
|
580
|
+
const keys = Object.keys(left);
|
|
581
|
+
if (keys.length !== Object.keys(right).length) return false;
|
|
582
|
+
return keys.every((key) => {
|
|
583
|
+
if (!Object.hasOwn(right, key)) return false;
|
|
584
|
+
const x = left[key];
|
|
585
|
+
const y = right[key];
|
|
586
|
+
if (Object.is(x, y)) return true;
|
|
587
|
+
return Array.isArray(x) && Array.isArray(y) && x.length === y.length && x.every((item, index) => Object.is(item, y[index]));
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* This function gives the kit of a profile (D46). A profile object that the kit knows, with the same fields, gives its
|
|
593
|
+
* kit. Then a recent kit with equal fields serves a new profile object. Else the function makes a new kit from the
|
|
594
|
+
* canonical form. Thus a later change of a field of the profile object gives a new kit, and an old kit does not change.
|
|
595
|
+
*/
|
|
596
|
+
function kitOf(profile: FormulaProfile): Kit {
|
|
597
|
+
const fields = fieldsOf(profile);
|
|
598
|
+
const known = kitsByProfile.get(profile);
|
|
599
|
+
if (known !== undefined && sameFields(known.fields, fields)) return known;
|
|
600
|
+
const index = recentKits.findIndex((kit) => sameFields(kit.fields, fields));
|
|
601
|
+
let kit: Kit;
|
|
602
|
+
if (index >= 0) {
|
|
603
|
+
kit = recentKits[index] as Kit;
|
|
604
|
+
recentKits.splice(index, 1);
|
|
605
|
+
} else {
|
|
606
|
+
const hook = makeKnown(fields);
|
|
607
|
+
kit = { fields, evaluators: makeEvaluators(fields), known: hook, facts: makeFacts(fields, hook) };
|
|
608
|
+
}
|
|
609
|
+
recentKits.unshift(kit);
|
|
610
|
+
if (recentKits.length > RECENT_KITS) recentKits.length = RECENT_KITS;
|
|
611
|
+
kitsByProfile.set(profile, kit);
|
|
612
|
+
return kit;
|
|
514
613
|
}
|
|
515
614
|
|
|
516
615
|
/** This function makes the code of the evaluators of the formula kit, without their type rules. */
|
|
@@ -848,10 +947,17 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
|
|
|
848
947
|
* `formula.ext` knows an extension of `ext` and each standard extension. The hook knows each name of the other
|
|
849
948
|
* evaluators.
|
|
850
949
|
*
|
|
950
|
+
* Equal profiles give the same hook (D46).
|
|
951
|
+
*
|
|
851
952
|
* @param profile - The operations and the extensions of the language.
|
|
852
953
|
* @returns The hook: `true`, or the reason of the `#NAME?` that the evaluator gives in a run.
|
|
853
954
|
*/
|
|
854
955
|
export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[], evaluator: string) => true | string {
|
|
956
|
+
return kitOf(profile).known;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** This function makes a new hook `known` for the fields of one profile. */
|
|
960
|
+
function makeKnown(profile: FormulaProfile): (path: readonly Key[], evaluator: string) => true | string {
|
|
855
961
|
const hasOp = (name: string): boolean => {
|
|
856
962
|
if (profile.lazy !== undefined && Object.hasOwn(profile.lazy, name)) return true;
|
|
857
963
|
const ops = profile.ops;
|
|
@@ -890,13 +996,19 @@ const EACH_PART: readonly LazyRule[] = /* @__PURE__ */ Object.freeze([/* @__PURE
|
|
|
890
996
|
* - An operation is pure when the profile names it in `pure`, or when `pure` is `true`. A volatile operation is not pure.
|
|
891
997
|
* - A standard extension is pure. An extension of the profile gets the context, thus it is not pure.
|
|
892
998
|
*
|
|
999
|
+
* Equal profiles give the same hook (D46).
|
|
1000
|
+
*
|
|
893
1001
|
* @param profile - The operations, the extensions and their facts.
|
|
894
1002
|
* @returns The hook: the facts of a name, or `undefined` for the facts of the contract.
|
|
895
1003
|
*/
|
|
896
1004
|
export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[], code: string) => KindFacts | undefined {
|
|
1005
|
+
return kitOf(profile).facts;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/** This function makes a new hook `facts` for the fields of one profile, with the hook `known` of the same fields. */
|
|
1009
|
+
function makeFacts(profile: FormulaProfile, known: (path: readonly Key[], evaluator: string) => true | string): (path: readonly Key[], code: string) => KindFacts | undefined {
|
|
897
1010
|
const volatile = new Set(profile.volatile ?? []);
|
|
898
1011
|
const pure = profile.pure === true ? undefined : new Set(profile.pure ?? []);
|
|
899
|
-
const known = formulaKnown(profile);
|
|
900
1012
|
return (path, code) => {
|
|
901
1013
|
if (code !== "formula.op" && code !== "formula.ext") return undefined;
|
|
902
1014
|
const name = path.slice(1).join(".");
|
|
@@ -922,14 +1034,16 @@ export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[
|
|
|
922
1034
|
* The kit gives the domains of the kernel (`STD_DOMAINS`). Thus a domain of the language with the name `number` is a
|
|
923
1035
|
* conflict. The language names `["domains", "number"]` in `replaces` to replace it.
|
|
924
1036
|
*
|
|
1037
|
+
* Equal profiles give the same evaluators and the same hooks (COMPOSE.UNIFY, D46). Thus two machines of one profile
|
|
1038
|
+
* merge, also when each adds its own language. Two profiles with different fields give different evaluators, and
|
|
1039
|
+
* their machines conflict at each evaluator name of the kit.
|
|
1040
|
+
*
|
|
925
1041
|
* @param profile - The operations and the rules of the language.
|
|
926
1042
|
* @param more - The parts of a machine that the language adds, with the paths of the entries that it replaces.
|
|
927
1043
|
* @returns The machine.
|
|
928
1044
|
* @throws {MergeError} The language and the kit have a conflict.
|
|
929
1045
|
*/
|
|
930
1046
|
export function formulaMachine(profile: FormulaProfile = {}, more: MachinePart = {}): Machine {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
more,
|
|
934
|
-
);
|
|
1047
|
+
const kit = kitOf(profile);
|
|
1048
|
+
return mergeMachines({ world: { kinds: formulaKinds() }, evaluators: { ...kit.evaluators }, domains: STD_DOMAINS, known: kit.known, facts: kit.facts }, more);
|
|
935
1049
|
}
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export {
|
|
|
36
36
|
} from "./errors.ts";
|
|
37
37
|
export {
|
|
38
38
|
type Alias,
|
|
39
|
+
type AttributeDeclaration,
|
|
39
40
|
type CapsRule,
|
|
40
41
|
type Kind,
|
|
41
42
|
type KindFacts,
|
|
@@ -47,9 +48,11 @@ export {
|
|
|
47
48
|
type ScopeRule,
|
|
48
49
|
type Shape as PartShape,
|
|
49
50
|
type Side,
|
|
51
|
+
type TreeContract,
|
|
50
52
|
type World,
|
|
51
53
|
type WorldEntry,
|
|
52
54
|
entriesAsJson,
|
|
55
|
+
equationType,
|
|
53
56
|
isAlias,
|
|
54
57
|
isKind,
|
|
55
58
|
isNamespace,
|
|
@@ -157,7 +160,20 @@ export {
|
|
|
157
160
|
prepare,
|
|
158
161
|
run,
|
|
159
162
|
} from "./evaluate.ts";
|
|
160
|
-
export {
|
|
163
|
+
export {
|
|
164
|
+
type AttributeRung,
|
|
165
|
+
type AttributeTrace,
|
|
166
|
+
type NearestOptions,
|
|
167
|
+
ATTRIBUTE_RUNGS,
|
|
168
|
+
STD_CAPS,
|
|
169
|
+
STD_EVALUATORS,
|
|
170
|
+
STD_MACHINE,
|
|
171
|
+
STD_RUNNERS,
|
|
172
|
+
STD_WALKERS,
|
|
173
|
+
STD_WORLD,
|
|
174
|
+
attributeTrace,
|
|
175
|
+
nearestWalker,
|
|
176
|
+
} from "./stdlib.ts";
|
|
161
177
|
export {
|
|
162
178
|
type Edge,
|
|
163
179
|
type OrderProblem,
|
|
@@ -186,7 +202,9 @@ export {
|
|
|
186
202
|
type Typing,
|
|
187
203
|
type WalkOptions,
|
|
188
204
|
type WalkProblem,
|
|
205
|
+
type WorldIssue,
|
|
189
206
|
check,
|
|
207
|
+
checkWorld,
|
|
190
208
|
mapRefs,
|
|
191
209
|
refsOf,
|
|
192
210
|
typeCheck,
|
package/src/parts.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, docEquals, isEnvelope, isRecord, typeName, typePath } from "./doc.ts";
|
|
2
|
-
import { type Kind, type Shape, type World, lookupKind } from "./world.ts";
|
|
2
|
+
import { type Kind, type NamesSource, type ScopeRule, type Shape, type World, lookupKind } from "./world.ts";
|
|
3
3
|
import type { ValueType } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
/** One part of a value: its place in the value, the JSON of the part, and the type that its position declares. */
|
|
@@ -268,3 +268,72 @@ export function pathMatches(path: readonly Key[], pattern: readonly Key[]): bool
|
|
|
268
268
|
}
|
|
269
269
|
return true;
|
|
270
270
|
}
|
|
271
|
+
|
|
272
|
+
function valueAt(value: unknown, path: readonly Key[]): unknown {
|
|
273
|
+
let here: unknown = value;
|
|
274
|
+
for (const key of path) {
|
|
275
|
+
if (Array.isArray(here)) here = here[Number(key)];
|
|
276
|
+
else if (isRecord(here) && Object.hasOwn(here, String(key))) here = here[String(key)];
|
|
277
|
+
else return undefined;
|
|
278
|
+
}
|
|
279
|
+
return here;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function headerName(item: Json): string | undefined {
|
|
283
|
+
return isEnvelope(item) && typeof (item as { readonly name?: unknown }).name === "string" ? (item as { readonly name: string }).name : undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function namesOf(source: NamesSource, value: unknown, covered: readonly PartSlot[]): readonly string[] {
|
|
287
|
+
if ("fixed" in source) return source.fixed;
|
|
288
|
+
if ("keysAt" in source) {
|
|
289
|
+
const at = valueAt(value, source.keysAt);
|
|
290
|
+
return isRecord(at) ? Object.keys(at) : [];
|
|
291
|
+
}
|
|
292
|
+
if ("listAt" in source) {
|
|
293
|
+
const at = valueAt(value, source.listAt);
|
|
294
|
+
return Array.isArray(at) ? (at as readonly unknown[]).filter((item): item is string => typeof item === "string") : [];
|
|
295
|
+
}
|
|
296
|
+
if ("textAt" in source) {
|
|
297
|
+
const at = valueAt(value, source.textAt);
|
|
298
|
+
if (typeof at === "string") return [at];
|
|
299
|
+
return source.otherwise === undefined ? [] : [source.otherwise];
|
|
300
|
+
}
|
|
301
|
+
return covered.map((slot) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* This function gives the names that the scope rules of a kind give each part, and the names that only later parts
|
|
306
|
+
* see. `check`, `conforms` and the plan read the scope rules with it.
|
|
307
|
+
*
|
|
308
|
+
* @param rules - The scope rules of the kind.
|
|
309
|
+
* @param value - The value of the document.
|
|
310
|
+
* @param slots - The parts of the value, in order.
|
|
311
|
+
* @returns The names that each part sees, and the names that only the later parts see, by the index of the part.
|
|
312
|
+
*/
|
|
313
|
+
export function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots: readonly PartSlot[]): { readonly sees: readonly (readonly string[])[]; readonly later: readonly (readonly string[])[] } {
|
|
314
|
+
const sees: string[][] = slots.map(() => []);
|
|
315
|
+
const later: string[][] = slots.map(() => []);
|
|
316
|
+
for (const rule of rules ?? []) {
|
|
317
|
+
const covered = slots.map((slot, index) => ({ slot, index })).filter(({ slot }) => rule.in.some((pattern) => pathMatches(slot.path, pattern)));
|
|
318
|
+
if (rule.sequential !== true) {
|
|
319
|
+
const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
|
|
320
|
+
for (const { index } of covered) sees[index]?.push(...names);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if ("headers" in rule.names) {
|
|
324
|
+
covered.forEach(({ index }, position) => {
|
|
325
|
+
const before = covered.slice(0, position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
326
|
+
const after = covered.slice(position).map(({ slot }) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
327
|
+
sees[index]?.push(...before);
|
|
328
|
+
later[index]?.push(...after);
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const names = namesOf(rule.names, value, covered.map(({ slot }) => slot));
|
|
333
|
+
covered.forEach(({ index }, position) => {
|
|
334
|
+
if (position > 0) sees[index]?.push(...names);
|
|
335
|
+
else later[index]?.push(...names);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return { sees, later };
|
|
339
|
+
}
|
package/src/reference.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, isEnvelope, isRecord, typeName } from "./doc.ts";
|
|
2
2
|
import { ask } from "./ladder.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type AttributeDeclaration, type CapsRule, type Kind, type TreeContract, equationType, lookupKind } from "./world.ts";
|
|
4
4
|
import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
|
|
5
5
|
import { type Cap, type Context, type HostThing, type Resolution, type ResolveOptions, type Thing, THING, isHostThing, resolve, valueOfThing, worldThing } from "./deref.ts";
|
|
6
|
-
import { STD_EVALUATORS } from "./stdlib.ts";
|
|
6
|
+
import { type AttributeRung, STD_EVALUATORS } from "./stdlib.ts";
|
|
7
7
|
import { type EvalError, type Failed, type Result, Failure, describe, fail, ok, placed, thrown } from "./errors.ts";
|
|
8
8
|
import {
|
|
9
9
|
type AllOptions,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
type RunInput,
|
|
18
18
|
type RunPart,
|
|
19
19
|
type Steps,
|
|
20
|
-
intersectCaps,
|
|
21
20
|
makeContext,
|
|
22
21
|
permits,
|
|
23
22
|
placeText,
|
|
@@ -150,13 +149,23 @@ function examine(env: Env, at: At): Seen | Failed {
|
|
|
150
149
|
return { doc, kindText, typePath: read.typePath ?? [], kind, name, chosen, split };
|
|
151
150
|
}
|
|
152
151
|
|
|
152
|
+
/**
|
|
153
|
+
* This function gives the effects that two lists of capabilities both permit (CAP.ONLY). The reference reads the rule
|
|
154
|
+
* from SPEC directly, and not from the intersection of the plan. Thus `agree` finds a bug in either one (D15).
|
|
155
|
+
*/
|
|
156
|
+
function bothPermit(a: readonly Cap[], b: readonly Cap[]): readonly Cap[] {
|
|
157
|
+
const out: Cap[] = [];
|
|
158
|
+
for (const cap of [...a, ...b]) if (permits(a, cap) && permits(b, cap) && !permits(out, cap)) out.push(cap);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
153
162
|
/** This function gives the capabilities that the rules of a kind give one part (D14, CAP.CONTRACT, CAP.ONLY). */
|
|
154
163
|
function capsOfPart(rules: readonly CapsRule[] | undefined, path: readonly Key[], caps: readonly Cap[]): readonly Cap[] {
|
|
155
164
|
let out = caps;
|
|
156
165
|
for (const rule of rules ?? []) {
|
|
157
166
|
if (rule.in !== undefined && !rule.in.some((pattern) => pathMatches(path, pattern))) continue;
|
|
158
167
|
if (rule.rule === "mask") return [];
|
|
159
|
-
if (rule.rule === "only") out =
|
|
168
|
+
if (rule.rule === "only") out = bothPermit(out, rule.caps);
|
|
160
169
|
}
|
|
161
170
|
return out;
|
|
162
171
|
}
|
|
@@ -420,13 +429,20 @@ export function reference(machine: Machine, doc: unknown, context: Context = mak
|
|
|
420
429
|
scope: { prefix: "", counts: new Map(), alls: 0 },
|
|
421
430
|
};
|
|
422
431
|
const ctx = context.world === null ? { ...context, world: worldThing(machine.world) } : context;
|
|
432
|
+
// A node of a tree reads the definitions of this evaluation for its equations (TREE.LADDER).
|
|
433
|
+
activeEnvs.push(env);
|
|
423
434
|
try {
|
|
424
435
|
return evaluateAt(env, { value: doc, place: [], declared: options.declaredType, depth: 0, address: false, ctx });
|
|
425
436
|
} catch (error) {
|
|
426
437
|
return placed(thrown(error, "the reference"), []);
|
|
438
|
+
} finally {
|
|
439
|
+
activeEnvs.pop();
|
|
427
440
|
}
|
|
428
441
|
}
|
|
429
442
|
|
|
443
|
+
/** The evaluations of the reference that run at this time, the innermost last. */
|
|
444
|
+
const activeEnvs: Env[] = [];
|
|
445
|
+
|
|
430
446
|
/*
|
|
431
447
|
* The definition of the decorated tree (TREE.*), written from docs/SPEC.md. It shares no code with `tree.node` of
|
|
432
448
|
* stdlib.ts: its nodes, its memo, its marks for attributes in evaluation and its JSON are its own.
|
|
@@ -444,6 +460,16 @@ interface RefNode {
|
|
|
444
460
|
readonly place: readonly Key[];
|
|
445
461
|
readonly parts: readonly Part[];
|
|
446
462
|
readonly ctx: Context;
|
|
463
|
+
/** The tree contract of the kind of the node (D42). */
|
|
464
|
+
readonly contract: TreeContract | undefined;
|
|
465
|
+
/** The type of a bare equation: the type `of` of the parts of the kind. */
|
|
466
|
+
readonly of: string | undefined;
|
|
467
|
+
/** The parent node in the same tree, for the rungs `parent` and `copy`. */
|
|
468
|
+
readonly up: RefNode | undefined;
|
|
469
|
+
/** The evaluation of the reference that made the node. Its equations evaluate with its definitions. */
|
|
470
|
+
readonly env: Env | undefined;
|
|
471
|
+
/** The host thing of the node. */
|
|
472
|
+
thing: HostThing;
|
|
447
473
|
/** The results of the attributes that ended. */
|
|
448
474
|
readonly values: Map<string, Result>;
|
|
449
475
|
/** The attributes in evaluation. */
|
|
@@ -463,8 +489,86 @@ function refNodeOf(thing: unknown): RefNode | undefined {
|
|
|
463
489
|
return typeof thing === "object" && thing !== null ? refNodes.get(thing) : undefined;
|
|
464
490
|
}
|
|
465
491
|
|
|
466
|
-
/**
|
|
467
|
-
|
|
492
|
+
/**
|
|
493
|
+
* TREE.LADDER: an equation of the kind of a node evaluates in the reference. It gets the node as `self`, the names of
|
|
494
|
+
* its rung and no capabilities. A bare equation gets the type `of` of the parts of the kind.
|
|
495
|
+
*/
|
|
496
|
+
function equationOf(node: RefNode, term: Json, names: Readonly<Record<string, unknown>>): Result {
|
|
497
|
+
const env = node.env;
|
|
498
|
+
if (env === undefined) return fail("#NOEVAL", "The evaluation of an equation needs the machine.");
|
|
499
|
+
const ctx: Context = { ...node.ctx, names, caps: [], self: node.thing };
|
|
500
|
+
return reference(env.machine, term, ctx, { definitions: env.definitions, ...(node.of === undefined ? {} : { declaredType: node.of }) });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function termIn<T>(table: Readonly<Record<string, T>> | undefined, name: string): T | undefined {
|
|
504
|
+
return table === undefined || !Object.hasOwn(table, name) ? undefined : table[name];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** TREE.DOWN: the names of a child for an equation of `down` of its parent: `child`, `index` and `prev`. */
|
|
508
|
+
function namesOfChild(up: RefNode, child: RefNode): Result {
|
|
509
|
+
const children = attributeOf(up, "children");
|
|
510
|
+
if (children !== undefined && !children.ok) return children;
|
|
511
|
+
const list: readonly unknown[] = children !== undefined && Array.isArray(children.value) ? children.value : [];
|
|
512
|
+
const index = list.indexOf(child.thing);
|
|
513
|
+
return ok({ child: child.thing, index: index < 0 ? null : index, prev: index > 0 ? list[index - 1] : null });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** TREE.FOLD: the unit, then `combine` with the attribute of each node of this tree in the list `children`. */
|
|
517
|
+
function foldOf(node: RefNode, name: string, fold: NonNullable<AttributeDeclaration["fold"]>): Result {
|
|
518
|
+
let acc = equationOf(node, fold.unit, {});
|
|
519
|
+
if (!acc.ok) return acc;
|
|
520
|
+
const children = attributeOf(node, "children");
|
|
521
|
+
if (children === undefined) return acc;
|
|
522
|
+
if (!children.ok) return children;
|
|
523
|
+
if (!Array.isArray(children.value)) return fail("#VALUE!", `The fold of ${name} needs a list in children.`);
|
|
524
|
+
for (const item of children.value) {
|
|
525
|
+
const child = refNodeOf(item);
|
|
526
|
+
if (child === undefined || child.tree !== node.tree) continue;
|
|
527
|
+
const value = attributeOf(child, name);
|
|
528
|
+
if (value === undefined) return fail("#N/A", `No rung gives ${nameOfAttribute(child, name)}.`);
|
|
529
|
+
if (!value.ok) return value;
|
|
530
|
+
acc = equationOf(node, fold.combine, { acc: acc.value, item: value.value });
|
|
531
|
+
if (!acc.ok) return acc;
|
|
532
|
+
}
|
|
533
|
+
return acc;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* TREE.LADDER: the rungs of the ladder of an attribute, in their order. A rung gives `undefined` when it does not
|
|
538
|
+
* answer, and the next rung then tries.
|
|
539
|
+
*/
|
|
540
|
+
const REF_RUNGS: readonly (readonly [AttributeRung, (node: RefNode, name: string) => Result | undefined])[] = [
|
|
541
|
+
["own", (node, name) => node.parts.find((item) => item.key === name)?.evaluate({ ...node.ctx, self: node.thing })],
|
|
542
|
+
[
|
|
543
|
+
"kind",
|
|
544
|
+
(node, name) => {
|
|
545
|
+
const term = termIn(node.contract?.equations, name);
|
|
546
|
+
return term === undefined ? undefined : equationOf(node, term, {});
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
[
|
|
550
|
+
"parent",
|
|
551
|
+
(node, name) => {
|
|
552
|
+
const up = node.up;
|
|
553
|
+
const term = up === undefined ? undefined : termIn(up.contract?.down, name);
|
|
554
|
+
if (up === undefined || term === undefined) return undefined;
|
|
555
|
+
const names = namesOfChild(up, node);
|
|
556
|
+
return names.ok ? equationOf(up, term, names.value as Readonly<Record<string, unknown>>) : names;
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
["copy", (node, name) => (node.up !== undefined && termIn(node.contract?.attributes, name)?.copy === true ? attributeOf(node.up, name) : undefined)],
|
|
560
|
+
[
|
|
561
|
+
"default",
|
|
562
|
+
(node, name) => {
|
|
563
|
+
const declared = termIn(node.contract?.attributes, name);
|
|
564
|
+
if (declared?.fold !== undefined) return foldOf(node, name, declared.fold);
|
|
565
|
+
return declared?.default === undefined ? undefined : equationOf(node, declared.default, {});
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
/** TREE.ONCE, TREE.MEMO and TREE.CYCLE: the result of one attribute, or `undefined` when no rung of its ladder answers. */
|
|
571
|
+
function attributeOf(node: RefNode, name: string): Result | undefined {
|
|
468
572
|
const done = node.values.get(name);
|
|
469
573
|
if (done !== undefined) return done;
|
|
470
574
|
if (node.pending.has(name)) {
|
|
@@ -472,14 +576,16 @@ function attributeOf(node: RefNode, self: HostThing, name: string): Result | und
|
|
|
472
576
|
const steps = from < 0 ? [nameOfAttribute(node, name)] : inEvaluation.slice(from).map((entry) => nameOfAttribute(entry.node, entry.name));
|
|
473
577
|
return fail("#CYCLE!", `The attributes form a cycle: ${[...steps, nameOfAttribute(node, name)].join(" -> ")}.`);
|
|
474
578
|
}
|
|
475
|
-
const part = node.parts.find((item) => item.key === name);
|
|
476
|
-
if (part === undefined) return undefined;
|
|
477
579
|
node.pending.add(name);
|
|
478
580
|
inEvaluation.push({ node, name });
|
|
479
581
|
try {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
582
|
+
for (const [, rung] of REF_RUNGS) {
|
|
583
|
+
const result = rung(node, name);
|
|
584
|
+
if (result === undefined) continue;
|
|
585
|
+
node.values.set(name, result);
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
return undefined;
|
|
483
589
|
} finally {
|
|
484
590
|
node.pending.delete(name);
|
|
485
591
|
inEvaluation.pop();
|
|
@@ -490,7 +596,7 @@ function attributeOf(node: RefNode, self: HostThing, name: string): Result | und
|
|
|
490
596
|
function jsonOfNode(node: RefNode, self: HostThing, open: readonly object[]): Result {
|
|
491
597
|
const record: Record<string, unknown> = {};
|
|
492
598
|
for (const part of node.parts) {
|
|
493
|
-
const result = attributeOf(node,
|
|
599
|
+
const result = attributeOf(node, part.key) as Result;
|
|
494
600
|
if (!result.ok) return result;
|
|
495
601
|
const value = plainValue(result.value, [...open, self]);
|
|
496
602
|
if (!value.ok) return value;
|
|
@@ -528,30 +634,50 @@ function plainValue(value: unknown, open: readonly object[]): Result {
|
|
|
528
634
|
|
|
529
635
|
/**
|
|
530
636
|
* TREE.LAZY, TREE.PARENT, TREE.SIBLING and TREE.ERROR: a node of an open tree gives its thing, and the root of a new
|
|
531
|
-
* tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error.
|
|
637
|
+
* tree gives its JSON. A step on the thing reads an attribute, and a failed attribute throws its own error. The node
|
|
638
|
+
* reads `self` only through `resolve` (C4, TREE.SELF).
|
|
532
639
|
*/
|
|
533
|
-
const treeDefinition: EvalFn = ({ parts, context, kind, place }) => {
|
|
534
|
-
const
|
|
640
|
+
const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath, resolve: walk }) => {
|
|
641
|
+
const reached = walk(["self"]);
|
|
642
|
+
const parent: Thing | undefined = reached.ok ? reached.thing : undefined;
|
|
643
|
+
const outer = refNodeOf(parent);
|
|
535
644
|
const nested = outer !== undefined && outer.tree.open;
|
|
536
|
-
const
|
|
645
|
+
const found = machine === undefined || typePath.length === 0 ? undefined : lookupKind(machine.world, typePath);
|
|
646
|
+
const contract = found?.ok === true ? found.kind.tree : undefined;
|
|
647
|
+
const of = contract === undefined || found?.ok !== true ? undefined : equationType(found.kind);
|
|
648
|
+
const node: RefNode = {
|
|
649
|
+
tree: nested ? outer.tree : { open: true },
|
|
650
|
+
kind,
|
|
651
|
+
place,
|
|
652
|
+
parts,
|
|
653
|
+
ctx: context,
|
|
654
|
+
contract,
|
|
655
|
+
of,
|
|
656
|
+
up: nested ? outer : undefined,
|
|
657
|
+
env: activeEnvs[activeEnvs.length - 1],
|
|
658
|
+
thing: null as unknown as HostThing,
|
|
659
|
+
values: new Map(),
|
|
660
|
+
pending: new Set(),
|
|
661
|
+
};
|
|
537
662
|
const self: HostThing & { readonly type: string } = {
|
|
538
663
|
[THING]: true,
|
|
539
664
|
type: kind,
|
|
540
665
|
label: `node ${kind}`,
|
|
541
666
|
deref: (key): Thing | undefined => {
|
|
542
|
-
if (key === "parent") return
|
|
667
|
+
if (key === "parent") return parent;
|
|
543
668
|
if (key === "type") return kind;
|
|
544
|
-
const result = attributeOf(node,
|
|
669
|
+
const result = attributeOf(node, String(key));
|
|
545
670
|
if (result === undefined || result.ok) return result?.ok === true ? result.value : undefined;
|
|
546
671
|
throw new Failure(result.error);
|
|
547
672
|
},
|
|
548
|
-
has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)),
|
|
673
|
+
has: (key) => key === "parent" || key === "type" || parts.some((part) => part.key === String(key)) || node.values.has(String(key)),
|
|
549
674
|
toJson: () => {
|
|
550
675
|
const result = jsonOfNode(node, self, []);
|
|
551
676
|
if (result.ok) return result.value;
|
|
552
677
|
throw new Failure(result.error);
|
|
553
678
|
},
|
|
554
679
|
};
|
|
680
|
+
node.thing = self;
|
|
555
681
|
refNodes.set(self, node);
|
|
556
682
|
if (nested) return ok(self);
|
|
557
683
|
try {
|