@mark1russell7/coeval 0.6.3 → 0.7.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 +32 -5
- package/build/index.d.ts +1568 -271
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4779 -492
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +15 -3
- package/src/conform.ts +1068 -0
- package/src/deref.ts +284 -47
- package/src/durable.ts +238 -0
- package/src/effects.ts +110 -0
- package/src/errors.ts +2 -1
- package/src/evaluate.ts +472 -111
- package/src/facts.ts +252 -0
- package/src/formula.ts +433 -36
- package/src/graph.ts +399 -0
- package/src/index.ts +28 -0
- package/src/ladder.ts +40 -1
- package/src/merge.ts +91 -4
- package/src/parts.ts +26 -11
- package/src/providers.ts +286 -0
- package/src/reads.ts +105 -0
- package/src/reference.ts +19 -7
- package/src/schedule.ts +436 -0
- package/src/static.ts +266 -29
- package/src/stdlib.ts +24 -7
- package/src/types.ts +741 -0
- package/src/typing.ts +390 -0
- package/src/world.ts +63 -9
package/src/static.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
|
|
2
|
-
import {
|
|
2
|
+
import type { Kind, NamesSource, ScopeRule, World } from "./world.ts";
|
|
3
3
|
import { ask } from "./ladder.ts";
|
|
4
4
|
import { type PartSlot, partsOf, pathMatches } from "./parts.ts";
|
|
5
|
-
import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
|
|
5
|
+
import { type Cap, type Context, type Root, ROOTS, context, isRef, walkerOf, worldThing } from "./deref.ts";
|
|
6
6
|
import type { ErrorCode } from "./errors.ts";
|
|
7
|
-
import { type Machine, partCaps, permits } from "./evaluate.ts";
|
|
7
|
+
import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
|
|
8
|
+
import type { ValueType } from "./types.ts";
|
|
9
|
+
import { type RootTypes, type TypeNode, type Typed, typePass } from "./typing.ts";
|
|
8
10
|
|
|
9
11
|
/** One document that a walk visits. */
|
|
10
12
|
export interface DocVisit {
|
|
@@ -97,8 +99,16 @@ function namesOf(source: NamesSource, value: unknown, covered: readonly PartSlot
|
|
|
97
99
|
return covered.map((slot) => headerName(slot.item)).filter((name): name is string => name !== undefined);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
+
/**
|
|
103
|
+
* This function gives the names that the scope rules of a kind give each part, and the names that only later parts
|
|
104
|
+
* see. `check` and `conforms` read the scope rules with it.
|
|
105
|
+
*
|
|
106
|
+
* @param rules - The scope rules of the kind.
|
|
107
|
+
* @param value - The value of the document.
|
|
108
|
+
* @param slots - The parts of the value, in order.
|
|
109
|
+
* @returns The names that each part sees, and the names that only the later parts see, by the index of the part.
|
|
110
|
+
*/
|
|
111
|
+
export function scopesOf(rules: readonly ScopeRule[] | undefined, value: unknown, slots: readonly PartSlot[]): { readonly sees: readonly (readonly string[])[]; readonly later: readonly (readonly string[])[] } {
|
|
102
112
|
const sees: string[][] = slots.map(() => []);
|
|
103
113
|
const later: string[][] = slots.map(() => []);
|
|
104
114
|
for (const rule of rules ?? []) {
|
|
@@ -162,6 +172,8 @@ function keysOf(trail: Trail | undefined): readonly Key[] {
|
|
|
162
172
|
class Visit implements DocVisit {
|
|
163
173
|
readonly #place: Trail | undefined;
|
|
164
174
|
readonly #at: Trail | undefined;
|
|
175
|
+
readonly #bound: readonly string[];
|
|
176
|
+
readonly #expect: ValueType | undefined;
|
|
165
177
|
|
|
166
178
|
constructor(
|
|
167
179
|
place: Trail | undefined,
|
|
@@ -177,9 +189,23 @@ class Visit implements DocVisit {
|
|
|
177
189
|
readonly typePath: readonly Key[] | undefined,
|
|
178
190
|
readonly address: true | undefined,
|
|
179
191
|
readonly caps: readonly Cap[] | undefined,
|
|
192
|
+
bound: readonly string[],
|
|
193
|
+
expect: ValueType | undefined,
|
|
180
194
|
) {
|
|
181
195
|
this.#place = place;
|
|
182
196
|
this.#at = at;
|
|
197
|
+
this.#bound = bound;
|
|
198
|
+
this.#expect = expect;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The names that the scope rules of the parent bind for this document. The check of types reads them. */
|
|
202
|
+
get bound(): readonly string[] {
|
|
203
|
+
return this.#bound;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The type that the shape of the parent declares for this part. The check of types reads it. */
|
|
207
|
+
get expect(): ValueType | undefined {
|
|
208
|
+
return this.#expect;
|
|
183
209
|
}
|
|
184
210
|
|
|
185
211
|
get place(): readonly Key[] {
|
|
@@ -216,6 +242,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
216
242
|
readonly slot: readonly Key[];
|
|
217
243
|
readonly parent: DocVisit | undefined;
|
|
218
244
|
readonly caps: readonly Cap[] | undefined;
|
|
245
|
+
readonly bound: readonly string[];
|
|
246
|
+
readonly expect: ValueType | undefined;
|
|
219
247
|
}
|
|
220
248
|
const stack: Item[] = [
|
|
221
249
|
{
|
|
@@ -230,6 +258,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
230
258
|
slot: [],
|
|
231
259
|
parent: undefined,
|
|
232
260
|
caps: options.caps,
|
|
261
|
+
bound: [],
|
|
262
|
+
expect: undefined,
|
|
233
263
|
},
|
|
234
264
|
];
|
|
235
265
|
while (stack.length > 0) {
|
|
@@ -254,6 +284,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
254
284
|
read.typePath,
|
|
255
285
|
item.address ? true : undefined,
|
|
256
286
|
item.caps,
|
|
287
|
+
item.bound,
|
|
288
|
+
item.expect,
|
|
257
289
|
);
|
|
258
290
|
const answer = visit(here);
|
|
259
291
|
if (answer === "skip" || kind === undefined) continue;
|
|
@@ -279,6 +311,8 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
279
311
|
slot: slot.path,
|
|
280
312
|
parent: here,
|
|
281
313
|
caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
|
|
314
|
+
bound: extra,
|
|
315
|
+
expect: slot.expect,
|
|
282
316
|
});
|
|
283
317
|
}
|
|
284
318
|
}
|
|
@@ -380,6 +414,10 @@ export interface Issue {
|
|
|
380
414
|
readonly reason: string;
|
|
381
415
|
/** The keys from the root JSON value to the value with the problem. */
|
|
382
416
|
readonly at?: readonly Key[];
|
|
417
|
+
/** The type that the position needs, for a type issue (`#TYPE!`). */
|
|
418
|
+
readonly expected?: ValueType;
|
|
419
|
+
/** The type that the check found, for a type issue (`#TYPE!`). */
|
|
420
|
+
readonly found?: ValueType;
|
|
383
421
|
}
|
|
384
422
|
|
|
385
423
|
/** The options of a check. */
|
|
@@ -393,16 +431,89 @@ export interface CheckOptions {
|
|
|
393
431
|
* each part by the capability rules of the kinds, as a run does.
|
|
394
432
|
*/
|
|
395
433
|
readonly caps?: readonly Cap[];
|
|
434
|
+
/**
|
|
435
|
+
* The verb that goes up the ladder at each document, as in `prepare`. The default is `eval`. `#NOEVAL` and the hook
|
|
436
|
+
* `known` follow this verb. The contract declares the effects and the value types of `eval` only, thus only `eval`
|
|
437
|
+
* checks them (VERB.CHECK). The type rule of an evaluator serves each verb.
|
|
438
|
+
*/
|
|
439
|
+
readonly verb?: string;
|
|
440
|
+
/**
|
|
441
|
+
* With `true`, the check also checks the value types (TYPE.CHECK). It reads the type rules of the evaluators, the
|
|
442
|
+
* signatures of the operations and the value types of the contracts. A type issue has the code `#TYPE!`. The
|
|
443
|
+
* default is `false`, thus a host opts in.
|
|
444
|
+
*/
|
|
445
|
+
readonly types?: boolean;
|
|
446
|
+
/** The type that the root document must have, for the check of the value types. */
|
|
447
|
+
readonly expect?: ValueType;
|
|
448
|
+
/**
|
|
449
|
+
* With `true`, the check also finds the problems of a run (RUN.CAN-WAIT). A runner under a parent without a runner
|
|
450
|
+
* gives `#CALC!` when an effect of its runners can reach the host. The parent cannot pass that wait through.
|
|
451
|
+
* A runner without such an effect does not wait, and the run drives it (RUN.DRIVE-NESTED). A kind that declares
|
|
452
|
+
* effects and has no runner gives `#CALC!` too, because in a run only a runner can wait for the host. The default is
|
|
453
|
+
* `false`.
|
|
454
|
+
*/
|
|
455
|
+
readonly run?: boolean;
|
|
456
|
+
/**
|
|
457
|
+
* The value types of the roots and of the names of the host, for the check of the value types (TYPE.ROOTS). A
|
|
458
|
+
* reference then gets the type that its path reaches. Only a check with `types: true` reads them.
|
|
459
|
+
*/
|
|
460
|
+
readonly rootTypes?: RootTypes;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** The result of `typeCheck`. */
|
|
464
|
+
export interface Typing {
|
|
465
|
+
/** The type of the root document. It is `unknown` when the root has an issue. */
|
|
466
|
+
readonly type: ValueType;
|
|
467
|
+
/** Each issue of the check. The type issues come after the other issues. */
|
|
468
|
+
readonly issues: readonly Issue[];
|
|
469
|
+
/** The type of each document, in the order of the document. */
|
|
470
|
+
readonly places: readonly Typed[];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** The least authority: the capability `[]` permits each effect (CAP.KEYS). */
|
|
474
|
+
const EACH: readonly Cap[] = /* @__PURE__ */ Object.freeze([[]]);
|
|
475
|
+
|
|
476
|
+
/** No declared effect. One list serves each visit without effects. */
|
|
477
|
+
const NO_EFFECTS: readonly (readonly Key[])[] = /* @__PURE__ */ Object.freeze([]);
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* This function gives the effects that the contract of the kind of a visit declares, as keys (FACTS.EFFECTS). The
|
|
481
|
+
* text `"type"` gives the type of the document after each alias. A visit without a kind declares no effect. `check`,
|
|
482
|
+
* `factsOf` and `effectsOf` read the declarations with this one function (D27).
|
|
483
|
+
*
|
|
484
|
+
* @param v - The visit.
|
|
485
|
+
* @returns The keys of each declared effect, in the order of the contract.
|
|
486
|
+
*/
|
|
487
|
+
export function declaredEffects(v: DocVisit): readonly (readonly Key[])[] {
|
|
488
|
+
const effects = v.kind?.effects;
|
|
489
|
+
if (effects === undefined || effects.length === 0) return NO_EFFECTS;
|
|
490
|
+
return effects === "type" ? [v.typePath ?? []] : effects.map((kind) => kind.split("."));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* This function gives the rule of a machine for an effect, for the static kit (CAP.PERMITS). The rule is the hook
|
|
495
|
+
* `permits` of the machine, as in a run, or else the function `permits`. A check has no values, thus the hook gets a
|
|
496
|
+
* context with the capabilities of the document and the world of the machine. Its names, its self and its event are
|
|
497
|
+
* empty.
|
|
498
|
+
*
|
|
499
|
+
* @param machine - The machine.
|
|
500
|
+
* @returns The rule. It gets the capabilities of a document and the keys of an effect.
|
|
501
|
+
*/
|
|
502
|
+
export function permitsOf(machine: Machine): (caps: readonly Cap[], kind: readonly Key[]) => boolean {
|
|
503
|
+
const hook = machine.permits;
|
|
504
|
+
if (hook === undefined) return permits;
|
|
505
|
+
let base: Context | undefined;
|
|
506
|
+
return (caps, kind) => hook(caps, kind, { ...(base ??= context({ world: worldThing(machine.world) })), caps });
|
|
396
507
|
}
|
|
397
508
|
|
|
398
509
|
/**
|
|
399
510
|
* This function gives the reason of a `#CAP!`. When a kind around the document narrowed the capabilities, the
|
|
400
511
|
* reason names the nearest such kind.
|
|
401
512
|
*/
|
|
402
|
-
function capReason(v: DocVisit, kind: readonly Key[]): string {
|
|
513
|
+
function capReason(v: DocVisit, kind: readonly Key[], allowed: (caps: readonly Cap[], kind: readonly Key[]) => boolean): string {
|
|
403
514
|
const text = kind.join(".");
|
|
404
515
|
for (let up = v.parent; up !== undefined; up = up.parent) {
|
|
405
|
-
if (up.caps !== undefined &&
|
|
516
|
+
if (up.caps !== undefined && allowed(up.caps, kind)) {
|
|
406
517
|
return `The capabilities do not permit the effect ${text} here. The kind ${typeName(up.doc.type)} around it narrows them for its parts.`;
|
|
407
518
|
}
|
|
408
519
|
}
|
|
@@ -422,14 +533,91 @@ function capReason(v: DocVisit, kind: readonly Key[]): string {
|
|
|
422
533
|
* - `#NAME?`: a name that nothing binds. With the hook `known` of the machine, also a name that the language does not
|
|
423
534
|
* have, for example an operation.
|
|
424
535
|
* - `#CAP!`: an effect that the capabilities of its document do not permit. A kind around the document can narrow them.
|
|
536
|
+
* The hook `permits` of the machine decides, as in a run (CAP.PERMITS).
|
|
537
|
+
* - `#CALC!`: with the option `run`, a runner under a parent without a runner, when an effect of its runners can reach
|
|
538
|
+
* the host. Also a kind that declares effects and has no runner (RUN.CAN-WAIT). The issue has the place and the
|
|
539
|
+
* reason of the run.
|
|
540
|
+
* - `#TYPE!`: with the option `types`, a part whose type does not fit the type that its position needs (TYPE.CHECK).
|
|
541
|
+
* A run does not always fail there, thus a type issue is a static finding, not an error of a run.
|
|
542
|
+
*
|
|
543
|
+
* The check asks the ladder for the verb of the options, `eval` by default (VERB.CHECK). The shapes, the paths and the
|
|
544
|
+
* names follow the contracts, thus they are the same for each verb. Only `eval` checks the declared effects.
|
|
425
545
|
*
|
|
426
546
|
* @param machine - The world and the code of the evaluators.
|
|
427
547
|
* @param doc - The document, as JSON.
|
|
428
|
-
* @param options - The type of a bare root value, the names of the host and the
|
|
429
|
-
* @returns The problems, in the order of the document.
|
|
548
|
+
* @param options - The type of a bare root value, the names of the host, the capabilities, the verb, the types and the run.
|
|
549
|
+
* @returns The problems, in the order of the document. The type issues come after the other issues.
|
|
430
550
|
*/
|
|
431
551
|
export function check(machine: Machine, doc: unknown, options: CheckOptions = {}): readonly Issue[] {
|
|
552
|
+
return options.types === true ? typeCheck(machine, doc, options).issues : checkAll(machine, doc, options, undefined);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* This function checks a document, and it also checks the value types (TYPE.CHECK, D25). It finds each issue of
|
|
557
|
+
* `check`. Then it synthesizes the type of each document from the types of its parts. It also checks each part
|
|
558
|
+
* against the type that its position needs. The type rule of the parent, the signature of an operation or the shape
|
|
559
|
+
* of the contract gives that type. A part with an issue gets the type `unknown`, thus one
|
|
560
|
+
* problem does not give a chain of issues.
|
|
561
|
+
*
|
|
562
|
+
* A type issue has the code `#TYPE!`, the type that the position needs (`expected`) and the type that the check found
|
|
563
|
+
* (`found`). It is a static finding: the evaluation of the document does not change.
|
|
564
|
+
*
|
|
565
|
+
* The option `rootTypes` gives the types of the roots and of the names of the host (TYPE.ROOTS). A reference then gets
|
|
566
|
+
* the type that its path reaches, and a field that a closed record does not have is a type issue (TYPE.CLOSED).
|
|
567
|
+
*
|
|
568
|
+
* @param machine - The world and the code of the evaluators, with their type rules.
|
|
569
|
+
* @param doc - The document, as JSON.
|
|
570
|
+
* @param options - The options of `check`, the type that the root must have and the types of the roots.
|
|
571
|
+
* @returns The type of the root, each issue, and the type of each document.
|
|
572
|
+
*/
|
|
573
|
+
export function typeCheck(machine: Machine, doc: unknown, options: CheckOptions = {}): Typing {
|
|
574
|
+
const nodes: TypeNode[] = [];
|
|
575
|
+
const issues = checkAll(machine, doc, options, nodes);
|
|
576
|
+
const pass = typePass(machine, nodes, options.verb ?? "eval", options.expect, options.rootTypes);
|
|
577
|
+
return { type: pass.type, issues: pass.issues.length === 0 ? issues : [...issues, ...pass.issues], places: pass.places };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* A runner under an evaluator in a check of a run. The run drives its steps without a wait, thus only a wait for an
|
|
582
|
+
* effect of its runners fails (RUN.DRIVE-NESTED).
|
|
583
|
+
*/
|
|
584
|
+
interface Drive {
|
|
585
|
+
/** The issue of the run when a step waits. */
|
|
586
|
+
readonly issue: Issue;
|
|
587
|
+
/** The index of the issue in the order of the document. */
|
|
588
|
+
readonly index: number;
|
|
589
|
+
/** The node of the runner for the pass of types. */
|
|
590
|
+
readonly node: TypeNode | undefined;
|
|
591
|
+
/** `true` when an effect of its runners can reach the host, and thus wait. */
|
|
592
|
+
wait: boolean;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** What the code of a document does with a wait: an evaluator stops it, and a runner passes it on. */
|
|
596
|
+
type Waits = boolean | Drive;
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* This function finds the issues of `check`. With a list of nodes, it also records each document that it visits for
|
|
600
|
+
* the pass of types. It marks each document with an issue.
|
|
601
|
+
*/
|
|
602
|
+
function checkAll(machine: Machine, doc: unknown, options: CheckOptions, nodes: TypeNode[] | undefined): readonly Issue[] {
|
|
432
603
|
const issues: Issue[] = [];
|
|
604
|
+
const verb = options.verb ?? "eval";
|
|
605
|
+
const indices = nodes === undefined ? undefined : new Map<DocVisit, number>();
|
|
606
|
+
let current: TypeNode | undefined;
|
|
607
|
+
const push = (issue: Issue): void => {
|
|
608
|
+
issues.push(issue);
|
|
609
|
+
if (current !== undefined) current.marked = true;
|
|
610
|
+
};
|
|
611
|
+
// The effects of a contract are the effects of eval (VERB.CONTRACT). An effect of another verb meets its test when
|
|
612
|
+
// it happens (RUN.CAP).
|
|
613
|
+
const effects = verb === "eval";
|
|
614
|
+
const allowed = options.caps === undefined ? undefined : permitsOf(machine);
|
|
615
|
+
// In a run, a part that can wait needs a parent that can wait (RUN.CAN-WAIT). The map tells for each document with
|
|
616
|
+
// code if its code passes a wait on: false for an evaluator, true for a runner whose waits reach the driver of the
|
|
617
|
+
// run, and a drive for a runner under an evaluator. A document without code fails before its parts, thus it is not
|
|
618
|
+
// in the map.
|
|
619
|
+
const waits = options.run === true ? new Map<DocVisit, Waits>() : undefined;
|
|
620
|
+
const drives: Drive[] = [];
|
|
433
621
|
const maxDepth = machine.maxDepth ?? 1000;
|
|
434
622
|
const anyName = options.names === "any";
|
|
435
623
|
const roots: readonly string[] = machine.roots ?? ROOTS;
|
|
@@ -438,34 +626,70 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
438
626
|
machine.world,
|
|
439
627
|
doc,
|
|
440
628
|
(v) => {
|
|
629
|
+
if (nodes !== undefined && indices !== undefined) {
|
|
630
|
+
current = { visit: v, bound: v instanceof Visit ? v.bound : [], expect: v instanceof Visit ? v.expect : undefined, children: [], evaluator: undefined, marked: false };
|
|
631
|
+
const parent = v.parent === undefined ? undefined : indices.get(v.parent);
|
|
632
|
+
if (parent !== undefined) nodes[parent]?.children.push(nodes.length);
|
|
633
|
+
indices.set(v, nodes.length);
|
|
634
|
+
nodes.push(current);
|
|
635
|
+
}
|
|
441
636
|
if (v.depth > maxDepth) {
|
|
442
|
-
|
|
637
|
+
push({ code: "#DEPTH!", place: v.place, reason: `The nesting is deeper than ${maxDepth} documents.`, at: v.at });
|
|
443
638
|
return "skip";
|
|
444
639
|
}
|
|
445
640
|
const header = v.declared ? {} : v.doc;
|
|
446
|
-
const answer = ask(machine.world, header,
|
|
641
|
+
const answer = ask(machine.world, header, verb, v.declared ? { declaredType: typeName(v.doc.type) } : {});
|
|
447
642
|
if (answer.error === "unknown-kind") {
|
|
448
|
-
|
|
643
|
+
push({ code: "#KIND?", place: v.place, reason: `The world has no kind ${typeName(v.doc.type)}.`, at: v.at });
|
|
449
644
|
return "skip";
|
|
450
645
|
}
|
|
451
646
|
const name = answer.evaluator;
|
|
452
|
-
if (
|
|
453
|
-
|
|
647
|
+
if (current !== undefined) current.evaluator = name;
|
|
648
|
+
// A runner hides the evaluator of its name (MACHINE.RUNNER), as in the compilation of a plan.
|
|
649
|
+
const runner = name !== undefined && has(machine.runners, name);
|
|
650
|
+
const coded = name !== undefined && (runner || has(machine.evaluators, name));
|
|
651
|
+
if (name === undefined || !coded) {
|
|
652
|
+
push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
|
|
454
653
|
} else if (machine.known !== undefined) {
|
|
455
654
|
// The evaluator gives #NAME? in a run for a name that the language does not have, for example an operation.
|
|
456
655
|
const known = machine.known(v.typePath ?? [], name);
|
|
457
|
-
if (known !== true)
|
|
656
|
+
if (known !== true) push({ code: "#NAME?", place: v.place, reason: known, at: v.at });
|
|
458
657
|
}
|
|
459
658
|
const caps = v.caps;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
for (const kind of
|
|
463
|
-
if (!
|
|
659
|
+
const declared = effects ? declaredEffects(v) : NO_EFFECTS;
|
|
660
|
+
if (allowed !== undefined && caps !== undefined) {
|
|
661
|
+
for (const kind of declared) {
|
|
662
|
+
if (!allowed(caps, kind)) push({ code: "#CAP!", place: v.place, reason: capReason(v, kind, allowed), at: v.at });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (waits !== undefined && coded) {
|
|
666
|
+
// The parent of the root is the driver of the run, and it can wait. An evaluator gives its parts no wait.
|
|
667
|
+
const up = v.parent === undefined ? true : waits.get(v.parent);
|
|
668
|
+
let chain: Waits = false;
|
|
669
|
+
if (runner && up === false) {
|
|
670
|
+
// The run drives a runner under an evaluator, and only a real wait fails (RUN.DRIVE-NESTED). Its issue goes in
|
|
671
|
+
// here, in the order of the document, when an effect of its runners can wait.
|
|
672
|
+
const issue: Issue = { code: "#CALC!", place: v.place, reason: waitedReason(typeName(v.doc.type)), at: v.at };
|
|
673
|
+
chain = { issue, index: issues.length, node: current, wait: !effects };
|
|
674
|
+
drives.push(chain);
|
|
675
|
+
} else if (runner) {
|
|
676
|
+
chain = up ?? true;
|
|
677
|
+
}
|
|
678
|
+
waits.set(v, chain);
|
|
679
|
+
// A runner waits only for an effect that reaches the host (C6). A refused effect gives #CAP! without a wait.
|
|
680
|
+
if (typeof chain === "object" && !chain.wait) {
|
|
681
|
+
const rule = allowed ?? permits;
|
|
682
|
+
chain.wait = declared.some((kind) => caps === undefined || rule(caps, kind));
|
|
683
|
+
}
|
|
684
|
+
// In a run, the host does each effect, thus an evaluator cannot cause one.
|
|
685
|
+
const first = declared[0];
|
|
686
|
+
if (!runner && first !== undefined) {
|
|
687
|
+
push({ code: "#CALC!", place: v.place, reason: `The effect ${first.join(".")} needs a runner: in a run, the host does each effect.`, at: v.at });
|
|
464
688
|
}
|
|
465
689
|
}
|
|
466
690
|
const isPath = isRef(v.doc);
|
|
467
691
|
if (!isPath && v.typePath?.length === 1 && v.typePath[0] === "ref") {
|
|
468
|
-
|
|
692
|
+
push({ code: "#VALUE!", place: v.place, reason: "The value of ref is a path: a list that starts with a root.", at: [...v.at, ...(v.declared ? [] : ["value"])] });
|
|
469
693
|
}
|
|
470
694
|
if (!isPath) return undefined;
|
|
471
695
|
const path = v.doc.value as readonly Json[];
|
|
@@ -473,17 +697,17 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
473
697
|
// The JSON path of a problem of a path goes to its step.
|
|
474
698
|
const stepAt = (index: number): readonly Key[] => [...v.at, "value", ...(index < path.length ? [index] : [])];
|
|
475
699
|
if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
|
|
476
|
-
|
|
700
|
+
push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.`, at: stepAt(0) });
|
|
477
701
|
// The step documents of the path are parts, and they evaluate before the path, thus the walk visits them.
|
|
478
702
|
return undefined;
|
|
479
703
|
}
|
|
480
704
|
if (!roots.includes(root)) {
|
|
481
|
-
|
|
705
|
+
push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.`, at: stepAt(0) });
|
|
482
706
|
}
|
|
483
707
|
const first = path[1];
|
|
484
708
|
if (root === ("names" as Root) && !anyName && isKey(first) && !v.names.has(String(first))) {
|
|
485
709
|
const later = v.later.has(String(first));
|
|
486
|
-
|
|
710
|
+
push({
|
|
487
711
|
code: "#NAME?",
|
|
488
712
|
place: v.place,
|
|
489
713
|
reason: later ? `The name ${String(first)} is bound by a later step.` : `Nothing binds the name ${String(first)}.`,
|
|
@@ -492,10 +716,9 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
492
716
|
}
|
|
493
717
|
path.forEach((step, index) => {
|
|
494
718
|
if (index === 0 || isKey(step)) return;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
issues.push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step.", at: stepAt(index) });
|
|
719
|
+
// The resolution finds the walker with the same function, thus the check and a run agree (VERB.LADDER).
|
|
720
|
+
if (walkerOf(machine, step) === undefined) {
|
|
721
|
+
push({ code: "#STEP?", place: v.place, reason: "A step is a key, or a document of a kind with the verb step.", at: stepAt(index) });
|
|
499
722
|
}
|
|
500
723
|
});
|
|
501
724
|
// The step documents of the path are parts when the kind ref has a deep shape. The walk checks them too.
|
|
@@ -504,9 +727,23 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
504
727
|
{
|
|
505
728
|
...(options.declaredType === undefined ? {} : { declaredType: options.declaredType }),
|
|
506
729
|
...(Array.isArray(options.names) ? { names: options.names } : {}),
|
|
507
|
-
|
|
508
|
-
|
|
730
|
+
// Without capabilities, a check of the run narrows the least authority, as effectsOf does. Thus a mask of a kind
|
|
731
|
+
// removes the effects that cannot wait. The check of the capabilities stays off.
|
|
732
|
+
...(options.caps === undefined ? (waits === undefined ? {} : { caps: EACH }) : { caps: options.caps }),
|
|
733
|
+
problem: (problem) => {
|
|
734
|
+
issues.push({ code: problem.code, place: problem.place, reason: problem.reason, at: problem.at });
|
|
735
|
+
// A value without the shape of its kind is a problem of the document that the walk visited last. A part that is
|
|
736
|
+
// not a document has no visit, thus it marks nothing.
|
|
737
|
+
if (problem.code === "#VALUE!" && current !== undefined) current.marked = true;
|
|
738
|
+
},
|
|
509
739
|
},
|
|
510
740
|
);
|
|
741
|
+
// The issue of each drive that can wait goes in at its place. The last drive goes in first, thus each index stays.
|
|
742
|
+
for (let index = drives.length - 1; index >= 0; index -= 1) {
|
|
743
|
+
const drive = drives[index] as Drive;
|
|
744
|
+
if (!drive.wait) continue;
|
|
745
|
+
issues.splice(drive.index, 0, drive.issue);
|
|
746
|
+
if (drive.node !== undefined) drive.node.marked = true;
|
|
747
|
+
}
|
|
511
748
|
return issues;
|
|
512
749
|
}
|
package/src/stdlib.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type Context, type EvalInput, type Evaluator, type Machine, type Part,
|
|
|
3
3
|
import { Failure } from "./errors.ts";
|
|
4
4
|
import { type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
|
|
5
5
|
import type { World } from "./ladder.ts";
|
|
6
|
+
import { STD_DOMAINS } from "./types.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The world of the examples: small kinds for numbers, lists, conditions, names, sequences and effects.
|
|
@@ -10,36 +11,43 @@ import type { World } from "./ladder.ts";
|
|
|
10
11
|
*/
|
|
11
12
|
export const STD_WORLD: World = {
|
|
12
13
|
kinds: {
|
|
13
|
-
number: { verbs: { eval: "data.number", view: "view.value" }, summary: "A number. Its value is the result." },
|
|
14
|
-
text: { verbs: { eval: "data.text", view: "view.value" }, summary: "A text. Its value is the result." },
|
|
15
|
-
bool: { verbs: { eval: "data.bool", view: "view.value" }, summary: "A truth value. Its value is the result." },
|
|
14
|
+
number: { verbs: { eval: "data.number", view: "view.value" }, pure: true, summary: "A number. Its value is the result." },
|
|
15
|
+
text: { verbs: { eval: "data.text", view: "view.value" }, pure: true, summary: "A text. Its value is the result." },
|
|
16
|
+
bool: { verbs: { eval: "data.bool", view: "view.value" }, pure: true, summary: "A truth value. Its value is the result." },
|
|
16
17
|
add: {
|
|
17
18
|
verbs: { eval: "math.add", view: "view.op" },
|
|
18
19
|
parts: { shape: "list", of: "number" },
|
|
20
|
+
pure: true,
|
|
19
21
|
summary: "The sum of its parts. A bare number in its list gets the type number.",
|
|
20
22
|
},
|
|
21
23
|
multiply: {
|
|
22
24
|
verbs: { eval: "math.multiply", view: "view.op" },
|
|
23
25
|
parts: { shape: "list", of: "number" },
|
|
26
|
+
pure: true,
|
|
24
27
|
summary: "The product of its parts.",
|
|
25
28
|
},
|
|
26
29
|
greater: {
|
|
27
30
|
verbs: { eval: "math.greater", view: "view.op" },
|
|
28
31
|
parts: { shape: "list", of: "number" },
|
|
32
|
+
pure: true,
|
|
29
33
|
summary: "True when the first part is greater than the second part.",
|
|
30
34
|
},
|
|
31
|
-
list: { verbs: { eval: "list.all", view: "view.op" }, parts: { shape: "list" }, summary: "The results of its parts, as a list." },
|
|
35
|
+
list: { verbs: { eval: "list.all", view: "view.op" }, parts: { shape: "list" }, pure: true, summary: "The results of its parts, as a list." },
|
|
32
36
|
if: {
|
|
33
37
|
verbs: { eval: "control.if", view: "view.op" },
|
|
34
38
|
parts: { shape: "record", fields: ["test", "then", "else"] },
|
|
39
|
+
pure: true,
|
|
40
|
+
lazy: [{ in: [["then"], ["else"]] }],
|
|
35
41
|
summary: "It evaluates the test, then only the branch that the test selects.",
|
|
36
42
|
},
|
|
37
43
|
ref: {
|
|
38
44
|
verbs: { eval: "ref.resolve", view: "view.ref" },
|
|
45
|
+
pure: true,
|
|
39
46
|
summary: "A path from a root of the context. Each step goes up the ladder of the verb deref.",
|
|
40
47
|
},
|
|
41
48
|
nearest: {
|
|
42
49
|
verbs: { step: "walk.nearest", view: "view.card" },
|
|
50
|
+
pure: true,
|
|
43
51
|
summary: "A step of a path: it follows a key again and again, until it reaches a node of a type.",
|
|
44
52
|
},
|
|
45
53
|
panel: {
|
|
@@ -47,12 +55,17 @@ export const STD_WORLD: World = {
|
|
|
47
55
|
parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
|
|
48
56
|
// An attribute evaluates on demand, thus in an order that is not the order of the document. It causes no effect.
|
|
49
57
|
caps: [{ rule: "mask" }],
|
|
58
|
+
// A node that nothing reads keeps its attributes without an evaluation (TREE.LAZY).
|
|
59
|
+
pure: true,
|
|
60
|
+
lazy: [{}],
|
|
50
61
|
summary: "A node of a tree. Each part is an attribute that evaluates on demand, one time, with the node as self.",
|
|
51
62
|
},
|
|
52
63
|
box: {
|
|
53
64
|
verbs: { eval: "tree.node", view: "view.op" },
|
|
54
65
|
parts: { shape: "record", fields: ["width", "height", "children"], of: "number" },
|
|
55
66
|
caps: [{ rule: "mask" }],
|
|
67
|
+
pure: true,
|
|
68
|
+
lazy: [{}],
|
|
56
69
|
summary: "A node of a tree, as panel. A formula in a box can read its parent and its siblings.",
|
|
57
70
|
},
|
|
58
71
|
math: {
|
|
@@ -68,12 +81,16 @@ export const STD_WORLD: World = {
|
|
|
68
81
|
{ names: { headers: true }, in: [["*"]], sequential: true },
|
|
69
82
|
{ names: { fixed: ["last"] }, in: [["*"]], sequential: true },
|
|
70
83
|
],
|
|
84
|
+
pure: true,
|
|
71
85
|
summary: "Its steps, in order. The result of a named step is a name for the later steps.",
|
|
72
86
|
},
|
|
73
87
|
pure: {
|
|
74
88
|
verbs: { eval: "control.pure", view: "view.op" },
|
|
75
89
|
parts: { shape: "list" },
|
|
76
90
|
caps: [{ rule: "mask" }],
|
|
91
|
+
// The runner evaluates only the first part.
|
|
92
|
+
pure: true,
|
|
93
|
+
lazy: [{ after: 1 }],
|
|
77
94
|
summary: "It evaluates its part without capabilities, thus an effect in it fails.",
|
|
78
95
|
},
|
|
79
96
|
log: {
|
|
@@ -432,7 +449,7 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
|
|
|
432
449
|
// The contract of pure masks the capabilities of its part, and the kernel applies it. Thus the runner does not.
|
|
433
450
|
"control.pure": function* ({ parts }) {
|
|
434
451
|
const only = parts[0];
|
|
435
|
-
if (only === undefined) return fail("#VALUE!", "pure needs one part.");
|
|
452
|
+
if (only === undefined || parts.length > 1) return fail("#VALUE!", "pure needs exactly one part.");
|
|
436
453
|
return yield* only.evaluate();
|
|
437
454
|
},
|
|
438
455
|
"control.if": function* ({ parts }) {
|
|
@@ -464,8 +481,8 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
|
|
|
464
481
|
},
|
|
465
482
|
};
|
|
466
483
|
|
|
467
|
-
/** The machine of the examples: the standard world, its evaluators, its runners and
|
|
468
|
-
export const STD_MACHINE: Machine = { world: STD_WORLD, evaluators: STD_EVALUATORS, runners: STD_RUNNERS, walkers: STD_WALKERS };
|
|
484
|
+
/** The machine of the examples: the standard world, its evaluators, its runners, its walkers and the domains of the kernel. */
|
|
485
|
+
export const STD_MACHINE: Machine = { world: STD_WORLD, evaluators: STD_EVALUATORS, runners: STD_RUNNERS, walkers: STD_WALKERS, domains: STD_DOMAINS };
|
|
469
486
|
|
|
470
487
|
/** The capabilities of the examples at the root: the two effects of the standard world. */
|
|
471
488
|
export const STD_CAPS: readonly string[] = ["log", "write"];
|