@mark1russell7/coeval 0.24.0 → 0.25.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 +3 -2
- package/build/index.d.ts +201 -7
- package/build/index.d.ts.map +1 -1
- package/build/index.js +720 -110
- package/build/index.js.map +1 -1
- package/package.json +3 -3
- package/src/agree.ts +105 -2
- package/src/evaluate.ts +49 -5
- package/src/formula.ts +62 -6
- package/src/index.ts +7 -3
- package/src/merge.ts +130 -10
- package/src/reference.ts +13 -2
- package/src/schedule.ts +11 -2
- package/src/static.ts +53 -4
- package/src/stdlib.ts +44 -18
- package/src/syntax.ts +114 -19
- package/src/types.ts +65 -0
- package/src/typing.ts +147 -12
package/src/merge.ts
CHANGED
|
@@ -7,14 +7,30 @@ import { type Kind, type World, type WorldEntry, isAlias, isKind, isNamespace }
|
|
|
7
7
|
/** The keys from a part of a merge to one of its entries, for example `["kinds", "ref"]`. */
|
|
8
8
|
export type MergePath = readonly Key[];
|
|
9
9
|
|
|
10
|
+
/** The entries of one kind of another part (COMPOSE.EXTEND-VERB). */
|
|
11
|
+
export interface VerbEntries {
|
|
12
|
+
/**
|
|
13
|
+
* The path of the kind, as `replaces` names it, for example `["kinds", "ref"]`, `["kinds", "ops", "kinds", "mul"]`,
|
|
14
|
+
* `["kinds", "ops", "fallback"]` or `["fallback"]`.
|
|
15
|
+
*/
|
|
16
|
+
readonly kind: MergePath;
|
|
17
|
+
/** The evaluator names, by verb, as in `verbs` of a kind. */
|
|
18
|
+
readonly verbs: Readonly<Record<string, string>>;
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
/**
|
|
11
22
|
* A world as a part of a merge. Each field is optional. `replaces` names the paths of the entries that this part
|
|
12
|
-
* replaces, for example `["kinds", "ref"]` or `["defaults", "view"]`. The result of a merge has no `replaces
|
|
23
|
+
* replaces, for example `["kinds", "ref"]` or `["defaults", "view"]`. The result of a merge has no `replaces`, no
|
|
24
|
+
* `verbs` and no `extends`.
|
|
13
25
|
*/
|
|
14
26
|
export interface WorldPart {
|
|
15
27
|
readonly kinds?: Readonly<Record<string, WorldEntry>>;
|
|
16
28
|
readonly defaults?: Readonly<Record<string, string>>;
|
|
17
29
|
readonly fallback?: Kind;
|
|
30
|
+
/** The verbs that this part owns. A part can add an entry of such a verb to a kind of another part. */
|
|
31
|
+
readonly verbs?: readonly string[];
|
|
32
|
+
/** The entries that this part adds to kinds. The merge adds them after each part (COMPOSE.EXTEND-VERB). */
|
|
33
|
+
readonly extends?: readonly VerbEntries[];
|
|
18
34
|
readonly replaces?: readonly MergePath[];
|
|
19
35
|
}
|
|
20
36
|
|
|
@@ -58,6 +74,8 @@ type Known = NonNullable<Machine["known"]>;
|
|
|
58
74
|
type Facts = NonNullable<Machine["facts"]>;
|
|
59
75
|
|
|
60
76
|
const NONE: ReadonlySet<string> = /* @__PURE__ */ new Set();
|
|
77
|
+
/** The verbs that the kernel reads. Thus no language owns them (COMPOSE.PIRACY). */
|
|
78
|
+
const KERNEL_VERBS: ReadonlySet<string> = /* @__PURE__ */ new Set(["eval", "deref", "step"]);
|
|
61
79
|
|
|
62
80
|
function keyOf(path: MergePath): string {
|
|
63
81
|
return JSON.stringify(path.map(String));
|
|
@@ -283,15 +301,112 @@ function mergeEntry(m: Merger, earlier: WorldEntry, later: WorldEntry, path: Mer
|
|
|
283
301
|
return earlier;
|
|
284
302
|
}
|
|
285
303
|
|
|
304
|
+
/** One entry of `extends`, with what the merge knows from its part. */
|
|
305
|
+
interface VerbEntry {
|
|
306
|
+
readonly kind: MergePath;
|
|
307
|
+
readonly verb: string;
|
|
308
|
+
readonly code: string;
|
|
309
|
+
readonly path: MergePath;
|
|
310
|
+
readonly part: number;
|
|
311
|
+
/** The part owns the verb, or it gives the kind. Thus the entry is not piracy. */
|
|
312
|
+
readonly allowed: boolean;
|
|
313
|
+
readonly replaced: boolean;
|
|
314
|
+
}
|
|
315
|
+
|
|
286
316
|
interface WorldDraft {
|
|
287
317
|
readonly kinds: Record<string, WorldEntry>;
|
|
288
318
|
readonly defaults: Record<string, string>;
|
|
289
319
|
fallback?: Kind;
|
|
320
|
+
/** The part that owns each verb. */
|
|
321
|
+
readonly owners: Map<string, WorldPart>;
|
|
322
|
+
readonly entries: VerbEntry[];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function worldDraft(): WorldDraft {
|
|
326
|
+
return { kinds: {}, defaults: {}, owners: new Map(), entries: [] };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface KindTable {
|
|
330
|
+
readonly kinds: Readonly<Record<string, WorldEntry>>;
|
|
331
|
+
readonly fallback?: Kind;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* This function gives a copy of a table with a change to the kind at a path, as `replaces` names the path. It gives
|
|
336
|
+
* `undefined` when no kind is at the path. An alias is not a kind.
|
|
337
|
+
*/
|
|
338
|
+
function changeKind<T extends KindTable>(table: T, path: MergePath, change: (kind: Kind) => Kind): T | undefined {
|
|
339
|
+
const [first, key, ...rest] = path;
|
|
340
|
+
if (String(first) === "fallback" && key === undefined) return table.fallback === undefined ? undefined : { ...table, fallback: change(table.fallback) };
|
|
341
|
+
if (String(first) !== "kinds" || key === undefined) return undefined;
|
|
342
|
+
const entry = own(table.kinds, String(key));
|
|
343
|
+
const next = rest.length === 0 ? (isKind(entry) ? change(entry) : undefined) : isNamespace(entry) ? changeKind(entry, rest, change) : undefined;
|
|
344
|
+
if (next === undefined) return undefined;
|
|
345
|
+
const kinds = { ...table.kinds };
|
|
346
|
+
setOwn(kinds, String(key), next);
|
|
347
|
+
return { ...table, kinds };
|
|
290
348
|
}
|
|
291
349
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
350
|
+
/**
|
|
351
|
+
* This function adds the entries of `extends` to the kinds of a draft. A later part can give the kind, thus this step
|
|
352
|
+
* reads the whole draft. Without `m`, it reports no conflict: a function part sees the entries of the parts before it.
|
|
353
|
+
*/
|
|
354
|
+
function addVerbs(draft: WorldDraft, m?: Merger): KindTable {
|
|
355
|
+
let table: KindTable = draft.fallback === undefined ? { kinds: draft.kinds } : { kinds: draft.kinds, fallback: draft.fallback };
|
|
356
|
+
for (const entry of draft.entries) {
|
|
357
|
+
if (!entry.allowed && !entry.replaced) {
|
|
358
|
+
m?.clash(entry.path, entry.part, "A part adds a verb that it does not own to a kind that it does not give.");
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const next = changeKind(table, entry.kind, (kind) => {
|
|
362
|
+
const verbs: Record<string, string> = { ...kind.verbs };
|
|
363
|
+
const earlier = own(verbs, entry.verb);
|
|
364
|
+
if (earlier === undefined) m?.give(entry.path, entry.part);
|
|
365
|
+
else if (entry.replaced) m?.replace(entry.path, entry.part);
|
|
366
|
+
else if (earlier !== entry.code) m?.clash(entry.path, entry.part, "Two parts give two different evaluators for this verb of this kind.");
|
|
367
|
+
if (earlier === undefined || entry.replaced) setOwn(verbs, entry.verb, entry.code);
|
|
368
|
+
return { ...kind, verbs };
|
|
369
|
+
});
|
|
370
|
+
if (next !== undefined) table = next;
|
|
371
|
+
else if (!entry.replaced) m?.clash(entry.path, entry.part, "A part adds a verb to a path without a kind.");
|
|
372
|
+
}
|
|
373
|
+
return table;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function worldOf(draft: WorldDraft, m?: Merger): World {
|
|
377
|
+
const table = addVerbs(draft, m);
|
|
378
|
+
const world = { kinds: { ...table.kinds }, defaults: { ...draft.defaults } };
|
|
379
|
+
return table.fallback === undefined ? world : { ...world, fallback: table.fallback };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** This function records the verbs that a part owns, and the entries that it adds to kinds. */
|
|
383
|
+
function mergeVerbs(m: Merger, draft: WorldDraft, part: WorldPart, index: number, replaces: ReadonlySet<string>, base: MergePath): void {
|
|
384
|
+
const owned = part.verbs ?? [];
|
|
385
|
+
for (const verb of owned) {
|
|
386
|
+
const path = [...base, "verbs", verb];
|
|
387
|
+
const owner = draft.owners.get(verb);
|
|
388
|
+
if (replaces.has(keyOf(path))) {
|
|
389
|
+
draft.owners.set(verb, part);
|
|
390
|
+
m.replace(path, index);
|
|
391
|
+
} else if (KERNEL_VERBS.has(verb)) {
|
|
392
|
+
m.clash(path, index, "The kernel owns this verb.");
|
|
393
|
+
} else if (owner === undefined) {
|
|
394
|
+
draft.owners.set(verb, part);
|
|
395
|
+
m.give(path, index);
|
|
396
|
+
} else if (!same(owner, part)) {
|
|
397
|
+
m.clash(path, index, "Two parts own this verb.");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const table: KindTable = part.fallback === undefined ? { kinds: part.kinds ?? {} } : { kinds: part.kinds ?? {}, fallback: part.fallback };
|
|
401
|
+
for (const { kind, verbs } of part.extends ?? []) {
|
|
402
|
+
const gives = changeKind(table, kind, (found) => found) !== undefined;
|
|
403
|
+
for (const verb of Object.keys(verbs)) {
|
|
404
|
+
const code = verbs[verb];
|
|
405
|
+
if (code === undefined) continue;
|
|
406
|
+
const path = [...base, ...kind, "verbs", verb];
|
|
407
|
+
draft.entries.push({ kind, verb, code, path, part: index, allowed: gives || owned.includes(verb), replaced: replaces.has(keyOf(path)) });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
295
410
|
}
|
|
296
411
|
|
|
297
412
|
function mergeWorldInto(m: Merger, draft: WorldDraft, part: WorldPart | undefined, index: number, replaces: ReadonlySet<string>, base: MergePath): void {
|
|
@@ -303,6 +418,7 @@ function mergeWorldInto(m: Merger, draft: WorldDraft, part: WorldPart | undefine
|
|
|
303
418
|
}
|
|
304
419
|
const fallback = mergeFallback(m, draft.fallback, part.fallback, [...base, "fallback"], index, replaces);
|
|
305
420
|
if (fallback !== undefined) draft.fallback = fallback;
|
|
421
|
+
mergeVerbs(m, draft, part, index, replaces, base);
|
|
306
422
|
}
|
|
307
423
|
|
|
308
424
|
/**
|
|
@@ -313,6 +429,8 @@ function mergeWorldInto(m: Merger, draft: WorldDraft, part: WorldPart | undefine
|
|
|
313
429
|
* - The defaults merge verb by verb, and the world keeps one fallback.
|
|
314
430
|
* - Each other pair of entries at one path is a conflict, unless the later part names the path in `replaces`.
|
|
315
431
|
* Then the later entry wins, with no merge below it.
|
|
432
|
+
* - The entries of `extends` join their kinds after each part, thus the order of the parts does not change them. A
|
|
433
|
+
* verb that a part does not own on a kind that it does not give is a conflict (COMPOSE.PIRACY).
|
|
316
434
|
*
|
|
317
435
|
* A part can be a function of the merge of the parts before it (`prev`, as an overlay of Nix). Thus a part can read
|
|
318
436
|
* the entry that it replaces.
|
|
@@ -323,13 +441,14 @@ function mergeWorldInto(m: Merger, draft: WorldDraft, part: WorldPart | undefine
|
|
|
323
441
|
*/
|
|
324
442
|
export function mergeWorlds(...parts: readonly (WorldPart | ((prev: World) => WorldPart))[]): World {
|
|
325
443
|
const m = new Merger();
|
|
326
|
-
const draft
|
|
444
|
+
const draft = worldDraft();
|
|
327
445
|
parts.forEach((input, index) => {
|
|
328
446
|
const part = typeof input === "function" ? input(worldOf(draft)) : input;
|
|
329
447
|
mergeWorldInto(m, draft, part, index, new Set((part.replaces ?? []).map(keyOf)), []);
|
|
330
448
|
});
|
|
449
|
+
const world = worldOf(draft, m);
|
|
331
450
|
if (m.conflicts.length > 0) throw new MergeError(m.conflicts);
|
|
332
|
-
return
|
|
451
|
+
return world;
|
|
333
452
|
}
|
|
334
453
|
|
|
335
454
|
/** The fields of a machine that merge by their own rules. Each other field is one value, or a conflict. */
|
|
@@ -392,12 +511,12 @@ function composeFacts(hooks: readonly { readonly part: number; readonly facts: F
|
|
|
392
511
|
};
|
|
393
512
|
}
|
|
394
513
|
|
|
395
|
-
function machineOf(draft: MachineDraft): Machine {
|
|
514
|
+
function machineOf(draft: MachineDraft, m?: Merger): Machine {
|
|
396
515
|
const known = composeKnown(draft.hooks, draft.replaced);
|
|
397
516
|
const facts = composeFacts(draft.factHooks, draft.replaced);
|
|
398
517
|
return {
|
|
399
518
|
...draft.rest,
|
|
400
|
-
world: worldOf(draft.world),
|
|
519
|
+
world: worldOf(draft.world, m),
|
|
401
520
|
evaluators: { ...draft.evaluators },
|
|
402
521
|
...(draft.runners === undefined ? {} : { runners: { ...draft.runners } }),
|
|
403
522
|
...(draft.derefs === undefined ? {} : { derefs: { ...draft.derefs } }),
|
|
@@ -474,7 +593,7 @@ function checkHidden(m: Merger, draft: MachineDraft): void {
|
|
|
474
593
|
*/
|
|
475
594
|
export function mergeMachines(...parts: readonly (MachinePart | ((prev: Machine) => MachinePart))[]): Machine {
|
|
476
595
|
const m = new Merger();
|
|
477
|
-
const draft: MachineDraft = { world:
|
|
596
|
+
const draft: MachineDraft = { world: worldDraft(), evaluators: {}, hooks: [], factHooks: [], replaced: new Map(), rest: {}, parts: [] };
|
|
478
597
|
parts.forEach((input, index) => {
|
|
479
598
|
const part = typeof input === "function" ? input(machineOf(draft)) : input;
|
|
480
599
|
draft.parts.push(part);
|
|
@@ -502,8 +621,9 @@ export function mergeMachines(...parts: readonly (MachinePart | ((prev: Machine)
|
|
|
502
621
|
}
|
|
503
622
|
});
|
|
504
623
|
checkHidden(m, draft);
|
|
624
|
+
const machine = machineOf(draft, m);
|
|
505
625
|
if (m.conflicts.length > 0) throw new MergeError(m.conflicts);
|
|
506
|
-
return
|
|
626
|
+
return machine;
|
|
507
627
|
}
|
|
508
628
|
|
|
509
629
|
/** This function removes the entry at a path from a table of entries. It gives the same table when no entry is there. */
|
package/src/reference.ts
CHANGED
|
@@ -1336,11 +1336,22 @@ const treeDefinition: EvalFn = ({ parts, context, kind, place, machine, typePath
|
|
|
1336
1336
|
}
|
|
1337
1337
|
};
|
|
1338
1338
|
|
|
1339
|
+
/**
|
|
1340
|
+
* The definition of the standard evaluator `ref.resolve` (REF.RESOLVE, D133). It resolves the steps of the path at each
|
|
1341
|
+
* evaluation. The plan compiles the same path one time.
|
|
1342
|
+
*/
|
|
1343
|
+
const refDefinition: EvalFn = ({ data, resolve: walk }) => {
|
|
1344
|
+
if (!Array.isArray(data) || data.length === 0) return fail("#VALUE!", 'The value of ref is a path: a list that starts with a root, for example ["names", "a"].');
|
|
1345
|
+
const found = walk(data as readonly Json[]);
|
|
1346
|
+
return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1339
1349
|
/**
|
|
1340
1350
|
* The definitions of the reference for the evaluators of the standard machine that need their own reading. The
|
|
1341
|
-
* reference uses them for the standard evaluators, also without the option `definitions`. It holds `tree.node
|
|
1351
|
+
* reference uses them for the standard evaluators, also without the option `definitions`. It holds `tree.node` and
|
|
1352
|
+
* `ref.resolve`. A host that wraps `ref.resolve` wraps this function, because the standard evaluator is compiled.
|
|
1342
1353
|
*/
|
|
1343
|
-
export const STD_DEFINITIONS: Definitions = { evaluators: { "tree.node": treeDefinition } };
|
|
1354
|
+
export const STD_DEFINITIONS: Definitions = { evaluators: { "tree.node": treeDefinition, "ref.resolve": refDefinition } };
|
|
1344
1355
|
|
|
1345
1356
|
/* The definitions of the formula kit: one function evaluator for each construct, written from docs/SPEC.md. */
|
|
1346
1357
|
|
package/src/schedule.ts
CHANGED
|
@@ -294,6 +294,8 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
294
294
|
// The nodes that finished in a kept read, and if that read met a node in evaluation below it (D143).
|
|
295
295
|
const keptNodes: string[] = [];
|
|
296
296
|
let keptMet = false;
|
|
297
|
+
// The count of the visits at the start of the kept read. A node with a lower number started below the read.
|
|
298
|
+
let keptFrom = 0;
|
|
297
299
|
let count = 0;
|
|
298
300
|
let ahead = 0;
|
|
299
301
|
// The node that stopped the evaluation of a walk, because the walk has no result for it.
|
|
@@ -370,8 +372,12 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
370
372
|
const reader = stack[stack.length - 1];
|
|
371
373
|
const known = epoch.values.get(id);
|
|
372
374
|
if (known !== undefined) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
+
if (reader !== undefined && onOpen.has(id)) {
|
|
376
|
+
// The path from that component to a kept read has a read that no evaluation asked for (SCHED.KEPT-OPEN).
|
|
377
|
+
if (keptAt >= 0 && (order.get(id) as number) < keptFrom) keptMet = true;
|
|
378
|
+
// A finished node of an open component is on a cycle with its reader too.
|
|
379
|
+
else lower(reader, order.get(id) as number);
|
|
380
|
+
}
|
|
375
381
|
return known;
|
|
376
382
|
}
|
|
377
383
|
if (busy.has(id)) {
|
|
@@ -592,9 +598,11 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
592
598
|
keeping(read) {
|
|
593
599
|
const outer = keptAt;
|
|
594
600
|
const outerMet = keptMet;
|
|
601
|
+
const outerFrom = keptFrom;
|
|
595
602
|
const from = keptNodes.length;
|
|
596
603
|
keptAt = stack.length;
|
|
597
604
|
keptMet = false;
|
|
605
|
+
keptFrom = count;
|
|
598
606
|
try {
|
|
599
607
|
return read();
|
|
600
608
|
} finally {
|
|
@@ -609,6 +617,7 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
|
|
|
609
617
|
}
|
|
610
618
|
keptAt = outer;
|
|
611
619
|
keptMet = outerMet;
|
|
620
|
+
keptFrom = outerFrom;
|
|
612
621
|
}
|
|
613
622
|
},
|
|
614
623
|
});
|
package/src/static.ts
CHANGED
|
@@ -258,11 +258,14 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
258
258
|
continue;
|
|
259
259
|
}
|
|
260
260
|
const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
|
|
261
|
+
// The rules of a grant are read one time for each kind, not for each part. A kind without such a rule gives no list (K238).
|
|
262
|
+
const grantRules = kind.caps !== undefined && kind.caps.some((rule) => rule.rule === "grant") ? kind.caps : undefined;
|
|
261
263
|
for (let index = split.parts.length - 1; index >= 0; index -= 1) {
|
|
262
264
|
const slot = split.parts[index] as PartSlot;
|
|
263
265
|
const extra = sees[index] ?? [];
|
|
264
266
|
const hidden = later[index] ?? [];
|
|
265
|
-
const grants = partGrants(
|
|
267
|
+
const grants = grantRules === undefined ? undefined : partGrants(grantRules, slot.path, doc.value);
|
|
268
|
+
const granted = grants !== undefined && grants.length > 0 ? grants : undefined;
|
|
266
269
|
stack.push({
|
|
267
270
|
value: slot.item,
|
|
268
271
|
place: slot.path.length === 0 ? item.place : { up: item.place, keys: slot.path },
|
|
@@ -274,8 +277,14 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
274
277
|
address: slot.address === true,
|
|
275
278
|
slot: slot.path,
|
|
276
279
|
parent: here,
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
// A part with no grant keeps the list of partCaps, with no copy (K238).
|
|
281
|
+
caps:
|
|
282
|
+
item.caps === undefined || kind.caps === undefined
|
|
283
|
+
? item.caps
|
|
284
|
+
: granted === undefined
|
|
285
|
+
? partCaps(kind.caps, slot.path, item.caps)
|
|
286
|
+
: [...partCaps(kind.caps, slot.path, item.caps), ...granted],
|
|
287
|
+
handled: granted === undefined ? item.handled : [...(item.handled ?? []), ...granted],
|
|
279
288
|
bound: extra,
|
|
280
289
|
expect: slot.expect,
|
|
281
290
|
});
|
|
@@ -371,6 +380,45 @@ export function mapRefs(world: World, doc: unknown, change: (use: RefUse) => unk
|
|
|
371
380
|
return out;
|
|
372
381
|
}
|
|
373
382
|
|
|
383
|
+
/**
|
|
384
|
+
* The answer of the function of `mapDocs` for one document.
|
|
385
|
+
*
|
|
386
|
+
* - `undefined`: keep the document and go into its parts.
|
|
387
|
+
* - `"skip"`: keep the document and stop there.
|
|
388
|
+
* - `{ to }`: put `to` in the place of the document and stop there.
|
|
389
|
+
* - `{ to, into: true }`: put `to` in the place of the document, then go into the parts of the old document. Thus
|
|
390
|
+
* `to` must keep the path of each such part.
|
|
391
|
+
*/
|
|
392
|
+
export type DocChange = undefined | "skip" | { readonly to: unknown; readonly into?: boolean };
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* This function changes the documents of a document, from the root to the parts (STATIC.MAPDOCS). The function
|
|
396
|
+
* `change` reads only the original documents. It decides at each node, and it can stop at a node. The kinds of a
|
|
397
|
+
* position come from the world, thus this function names no kind. The document is not changed: the function gives a
|
|
398
|
+
* new document that shares each part without a change.
|
|
399
|
+
*
|
|
400
|
+
* @param world - The world of the kinds.
|
|
401
|
+
* @param doc - The document, as JSON.
|
|
402
|
+
* @param change - The function that gives the answer for each visit. See `DocChange`.
|
|
403
|
+
* @param options - The type of a bare root value, and the names that the host binds at the root.
|
|
404
|
+
* @returns The new document, or `doc` itself when no document changes.
|
|
405
|
+
*/
|
|
406
|
+
export function mapDocs(world: World, doc: unknown, change: (visit: DocVisit) => DocChange, options: WalkOptions = {}): unknown {
|
|
407
|
+
let out = doc;
|
|
408
|
+
walk(
|
|
409
|
+
world,
|
|
410
|
+
doc,
|
|
411
|
+
(visit) => {
|
|
412
|
+
const next = change(visit);
|
|
413
|
+
if (next === undefined || next === "skip") return next;
|
|
414
|
+
out = setAt(out, visit.at, next.to);
|
|
415
|
+
return next.into === true ? undefined : "skip";
|
|
416
|
+
},
|
|
417
|
+
options,
|
|
418
|
+
);
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
|
|
374
422
|
/** A problem that a check finds before a document runs. */
|
|
375
423
|
export interface Issue {
|
|
376
424
|
readonly code: ErrorCode;
|
|
@@ -593,7 +641,8 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
593
641
|
export function typeCheck(machine: Machine, doc: unknown, options: CheckOptions = {}): Typing {
|
|
594
642
|
const nodes: TypeNode[] = [];
|
|
595
643
|
const issues = checkAll(machine, doc, options, nodes);
|
|
596
|
-
const
|
|
644
|
+
const verb = options.verb ?? "eval";
|
|
645
|
+
const pass = typePass(machine, nodes, verb, options.expect, options.rootTypes, (node) => declaredEffects(node.visit, verb, machine, node.evaluator).length > 0);
|
|
597
646
|
return { type: pass.type, issues: pass.issues.length === 0 ? issues : [...issues, ...pass.issues], places: pass.places };
|
|
598
647
|
}
|
|
599
648
|
|
package/src/stdlib.ts
CHANGED
|
@@ -15,9 +15,10 @@ import {
|
|
|
15
15
|
fail,
|
|
16
16
|
ok,
|
|
17
17
|
planner,
|
|
18
|
+
resolveCompiled,
|
|
18
19
|
} from "./evaluate.ts";
|
|
19
20
|
import { Failure } from "./errors.ts";
|
|
20
|
-
import { type Cap, type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
|
|
21
|
+
import { type Cap, type HostThing, type Thing, type Walker, THING, compilePath, isHostThing, valueOfThing } from "./deref.ts";
|
|
21
22
|
import type { World } from "./ladder.ts";
|
|
22
23
|
import {
|
|
23
24
|
type AttributeDeclaration,
|
|
@@ -192,7 +193,8 @@ export const STD_WORLD: World = {
|
|
|
192
193
|
* A literal in a place that takes a document gets its data envelope (K159, D65). Such a place is a part of `if`,
|
|
193
194
|
* `list`, `seq` or `pure`, the value of `write`, or the whole text. The document after a header name is such a place
|
|
194
195
|
* too. Thus `list(1, "a")` evaluates to `[1, "a"]`. The forms `add`, `multiply` and `greater` write an operator document that its operator
|
|
195
|
-
* does not write, for example `add(5)` (K163).
|
|
196
|
+
* does not write, for example `add(5)` (K163). The field `with` of `handle` is a field of data, thus each clause is a
|
|
197
|
+
* record between braces in a list between brackets (D177).
|
|
196
198
|
*
|
|
197
199
|
* A host with more kinds spreads this profile and adds its forms to `calls.forms`. `STD_SYNTAX` stays the profile of
|
|
198
200
|
* the formula kit.
|
|
@@ -223,6 +225,8 @@ export const WORLD_SYNTAX: SyntaxProfile = /* @__PURE__ */ Object.freeze({
|
|
|
223
225
|
{ name: "tree.named", kind: "tree.named", args: "named", fields: ["name", "in"] },
|
|
224
226
|
{ name: "tree.at", kind: "tree.at", args: "named", fields: ["attr", "args"] },
|
|
225
227
|
{ name: "tree.quote", kind: "tree.quote", args: "literal" },
|
|
228
|
+
// The clauses are data with documents in it, for example handle(with: [{op: "log", do: payload + 1}], body: log(2)) (D177).
|
|
229
|
+
{ name: "handle", kind: "handle", args: "named", fields: ["with", "return", "body"], data: ["with"], wrap: ["return", "body"] },
|
|
226
230
|
// An operator document with fewer terms than its operator takes, for example add(5) (K163, D65).
|
|
227
231
|
{ name: "add", kind: "add" },
|
|
228
232
|
{ name: "multiply", kind: "multiply" },
|
|
@@ -286,12 +290,19 @@ export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
|
|
|
286
290
|
const [a, b] = values as readonly number[];
|
|
287
291
|
return ok((a ?? 0) > (b ?? 0));
|
|
288
292
|
},
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
// The path is data of the document, thus it compiles one time for the machine of the plan (PLAN.PATH-STD, D133).
|
|
294
|
+
"ref.resolve": {
|
|
295
|
+
compile: ({ data, machine }) => {
|
|
296
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
297
|
+
const bad = fail("#VALUE!", "The value of ref is a path: a list that starts with a root, for example [\"names\", \"a\"].");
|
|
298
|
+
return () => bad;
|
|
299
|
+
}
|
|
300
|
+
const path = compilePath(machine, data);
|
|
301
|
+
return (ctx, rt) => {
|
|
302
|
+
const found = resolveCompiled(rt, ctx, path);
|
|
303
|
+
return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
|
|
304
|
+
};
|
|
305
|
+
},
|
|
295
306
|
},
|
|
296
307
|
"tree.node": (input) => treeNode(input, undefined),
|
|
297
308
|
};
|
|
@@ -835,6 +846,7 @@ function leaderOnHost(n: LoopNode, key: string, rule: CircularRule & { readonly
|
|
|
835
846
|
/**
|
|
836
847
|
* This function gives an attribute of a node with circular attributes, or an attribute in an open fixed point (D113).
|
|
837
848
|
* The memo and the marks of the node do not hold the attribute. An open fixed point does not use the cells of the host.
|
|
849
|
+
* With the cells of a host, only a circular attribute comes here outside a fixed point. A plain attribute takes its cell.
|
|
838
850
|
*/
|
|
839
851
|
function circularRead(n: LoopNode, key: string): Result | undefined {
|
|
840
852
|
const rule = circularRule(n.tree, key);
|
|
@@ -1221,7 +1233,8 @@ function treeThing(input: EvalInput, place: readonly Key[], build: TreeBuild, ou
|
|
|
1221
1233
|
const text = attributeText(key, type, place);
|
|
1222
1234
|
if (marks.has(key)) return busyAt(state, key);
|
|
1223
1235
|
if (tree === undefined && up?.tree === undefined && !parts.some((item) => item.key === key)) return undefined;
|
|
1224
|
-
|
|
1236
|
+
// Outside a fixed point, a plain attribute of a node with a circular attribute also takes the cell of the host.
|
|
1237
|
+
if (openPoint !== undefined || (circular && circularRule(tree, key) !== undefined)) return circularRead(state, key);
|
|
1225
1238
|
marks.set(key, BUSY);
|
|
1226
1239
|
demands.push({ cells: marks, key, text });
|
|
1227
1240
|
let thrown: unknown = NOT_THROWN;
|
|
@@ -1527,6 +1540,27 @@ function isOp(value: unknown): value is Cap {
|
|
|
1527
1540
|
return typeof value === "string" || (Array.isArray(value) && value.every(isKey));
|
|
1528
1541
|
}
|
|
1529
1542
|
|
|
1543
|
+
/**
|
|
1544
|
+
* This function gives the runner of an `if` with its own truth (RUN.IF-TRUTH). A branch can wait for the host, thus a
|
|
1545
|
+
* language with the function `truthy` of a profile does not need a copy of `if`. A missing test and a test without a
|
|
1546
|
+
* truth give `#VALUE!`, a failed test gives its failure, and a missing branch gives `null`.
|
|
1547
|
+
*
|
|
1548
|
+
* @param truth - The function that gives the truth of a value, or `undefined` for a value without a truth.
|
|
1549
|
+
* @returns The runner.
|
|
1550
|
+
*/
|
|
1551
|
+
export function ifRunner(truth: (value: unknown) => boolean | undefined): Runner {
|
|
1552
|
+
return function* ({ parts }) {
|
|
1553
|
+
const test = parts.find((part) => part.key === "test");
|
|
1554
|
+
if (test === undefined) return fail("#VALUE!", "if needs a test.");
|
|
1555
|
+
const chosen = yield* test.evaluate();
|
|
1556
|
+
if (!chosen.ok) return chosen;
|
|
1557
|
+
const held = truth(chosen.value);
|
|
1558
|
+
if (held === undefined) return fail("#VALUE!", "The test of if must give a truth value.");
|
|
1559
|
+
const branch = parts.find((part) => part.key === (held ? "then" : "else"));
|
|
1560
|
+
return branch === undefined ? ok(null) : yield* branch.evaluate();
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1530
1564
|
/**
|
|
1531
1565
|
* The runners of the standard world, by name: the kinds that can hold effects. Each one evaluates its parts in
|
|
1532
1566
|
* order. In a run, an effect ends before the next step starts.
|
|
@@ -1576,15 +1610,7 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
|
|
|
1576
1610
|
if (!result.ok || after === undefined) return result;
|
|
1577
1611
|
return yield* after.evaluate({ ...context, names: { ...context.names, result: result.value } });
|
|
1578
1612
|
},
|
|
1579
|
-
"control.if":
|
|
1580
|
-
const test = parts.find((part) => part.key === "test");
|
|
1581
|
-
if (test === undefined) return fail("#VALUE!", "if needs a test.");
|
|
1582
|
-
const chosen = yield* test.evaluate();
|
|
1583
|
-
if (!chosen.ok) return chosen;
|
|
1584
|
-
if (typeof chosen.value !== "boolean") return fail("#VALUE!", "The test of if must give a truth value.");
|
|
1585
|
-
const branch = parts.find((part) => part.key === (chosen.value === true ? "then" : "else"));
|
|
1586
|
-
return branch === undefined ? ok(null) : yield* branch.evaluate();
|
|
1587
|
-
},
|
|
1613
|
+
"control.if": /* @__PURE__ */ ifRunner((value) => (typeof value === "boolean" ? value : undefined)),
|
|
1588
1614
|
"list.all": function* ({ parts }) {
|
|
1589
1615
|
const all = yield* inOrder(parts);
|
|
1590
1616
|
return "error" in all ? all.error : ok(all.values);
|