@mark1russell7/coeval 0.10.0 → 0.12.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 +28 -15
- package/build/index.d.ts.map +1 -1
- package/build/index.js +80 -13
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/deref.ts +11 -2
- package/src/evaluate.ts +57 -15
- package/src/formula.ts +4 -3
- package/src/generate.ts +7 -2
- package/src/index.ts +1 -0
- package/src/world.ts +29 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/coeval",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"description": "coeval: the kernel of declarative languages. Documents in envelopes, a ladder that finds the evaluator, kind contracts, compiled plans, a static kit, a formula kit and a run side with capabilities.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./build/index.js",
|
package/src/deref.ts
CHANGED
|
@@ -363,6 +363,8 @@ export function context(parts: Partial<Context> = {}): Context {
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
const MAX_COLLAPSE = 64;
|
|
366
|
+
/** The reason of a resolution that reaches the bound of collapses without a cycle that it found (DEREF.CHAIN-REASON). It is a plain string, thus a bundle drops it (PKG.PURE). */
|
|
367
|
+
const CHAIN_REASON = "The path follows more than 64 references. One resolution follows at most 64.";
|
|
366
368
|
const NO_HOPS: readonly Hop[] = /* @__PURE__ */ Object.freeze([]);
|
|
367
369
|
|
|
368
370
|
/** A value that can be a reference document: a record with the type `ref`. The full check comes after. */
|
|
@@ -560,7 +562,8 @@ function walkersOf(nav: Navigator, path: readonly unknown[]): KnownWalkers | und
|
|
|
560
562
|
* - `#NAME?`: the root `names` has no such name.
|
|
561
563
|
* - `#N/A`: a step reaches nothing.
|
|
562
564
|
* - `#STEP?`: a step is not a key and not a step document.
|
|
563
|
-
* - `#CYCLE!`: the references form a cycle.
|
|
565
|
+
* - `#CYCLE!`: the references form a cycle, or the resolution follows more than 64 references. The reason tells
|
|
566
|
+
* which (DEREF.CHAIN-REASON).
|
|
564
567
|
*
|
|
565
568
|
* @param nav - The world and the code of the evaluators.
|
|
566
569
|
* @param ctx - The context.
|
|
@@ -590,15 +593,21 @@ function resolveFully(nav: Navigator, ctx: Context, path: readonly unknown[], ho
|
|
|
590
593
|
const done = hops ?? NO_HOPS;
|
|
591
594
|
const failed = (code: ResolveCode | ErrorCode, reason: string): Resolution => ({ ok: false, code, reason, hops: done });
|
|
592
595
|
let collapses = 0;
|
|
596
|
+
// The references whose paths are in evaluation. A reference that is in this list again closes a cycle.
|
|
597
|
+
let open: object[] | undefined;
|
|
593
598
|
const adopt = nav.adopt ?? ((value: unknown): Thing => value);
|
|
594
599
|
|
|
595
600
|
const collapse = (thing: Thing): Resolution => {
|
|
596
601
|
let here = thing;
|
|
597
602
|
if (nav.plain === true) return { ok: true, thing: here, hops: done };
|
|
598
603
|
while (isRef(here)) {
|
|
604
|
+
if (open?.includes(here) === true) return failed("#CYCLE!", "The references form a cycle.");
|
|
599
605
|
collapses += 1;
|
|
600
|
-
if (collapses > MAX_COLLAPSE) return failed("#CYCLE!",
|
|
606
|
+
if (collapses > MAX_COLLAPSE) return failed("#CYCLE!", CHAIN_REASON);
|
|
607
|
+
open ??= [];
|
|
608
|
+
open.push(here);
|
|
601
609
|
const inner = walk(here.value, undefined);
|
|
610
|
+
open.pop();
|
|
602
611
|
if (!inner.ok) return inner;
|
|
603
612
|
here = adopt(inner.thing);
|
|
604
613
|
hops?.push({ step: "collapse", thing: here, by: "collapse" });
|
package/src/evaluate.ts
CHANGED
|
@@ -156,6 +156,22 @@ function granted(ctx: Context): Context {
|
|
|
156
156
|
return out;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* This function gives the context in which the runtime resolves a path of compiled code (CAP.CAPS-GRANT). A path from
|
|
161
|
+
* the root `caps` reads the capabilities that both the context and its grant permit, and a context without a grant
|
|
162
|
+
* reads none. Thus a context that compiled code widened gives the capabilities of its document, and a context that it
|
|
163
|
+
* narrowed keeps its own list. Each other path, and each context whose grant is its own list, keeps the same context.
|
|
164
|
+
*/
|
|
165
|
+
function readable(ctx: Context, path: readonly unknown[]): Context {
|
|
166
|
+
if (path[0] !== "caps") return ctx;
|
|
167
|
+
const grant = (ctx as Granted)[GRANT];
|
|
168
|
+
if (grant === ctx.caps) return ctx;
|
|
169
|
+
const caps = grant === undefined ? NO_CAPS : intersectCaps(ctx.caps, grant);
|
|
170
|
+
if (caps === ctx.caps) return ctx;
|
|
171
|
+
const out: Granted = { ...ctx, caps, [GRANT]: caps };
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
159
175
|
/**
|
|
160
176
|
* This function gives the context of a part of a function evaluator. It is the context that the evaluator gives,
|
|
161
177
|
* narrowed by the context of the document (CAP.NARROW), with the grant of its capabilities.
|
|
@@ -785,17 +801,13 @@ export interface StopSignal {
|
|
|
785
801
|
addEventListener(type: "abort", listener: () => void, options?: { readonly once?: boolean }): void;
|
|
786
802
|
}
|
|
787
803
|
|
|
788
|
-
/**
|
|
789
|
-
|
|
804
|
+
/**
|
|
805
|
+
* The options of `plan.run`. A plan keeps the verb and the declared type of its `prepare`, thus these options do not
|
|
806
|
+
* have them (PLAN.RUN-OPTIONS).
|
|
807
|
+
*/
|
|
808
|
+
export interface PlanRunOptions {
|
|
790
809
|
/** The host code that does each effect. Without it, an effect gives its payload, as in an evaluation. */
|
|
791
810
|
readonly perform?: Perform;
|
|
792
|
-
/** The type of a bare root value. */
|
|
793
|
-
readonly declaredType?: string;
|
|
794
|
-
/**
|
|
795
|
-
* The verb of `run`, for example `format`. The default is `eval` (VERB.PREPARE). A plan keeps the verb of its
|
|
796
|
-
* `prepare`, thus `plan.run` does not read this option.
|
|
797
|
-
*/
|
|
798
|
-
readonly verb?: string;
|
|
799
811
|
/** A signal of the host. When it aborts, the run stops with `#ABORT`. */
|
|
800
812
|
readonly signal?: StopSignal;
|
|
801
813
|
/**
|
|
@@ -805,6 +817,14 @@ export interface RunOptions {
|
|
|
805
817
|
readonly reads?: Read[];
|
|
806
818
|
}
|
|
807
819
|
|
|
820
|
+
/** The options of `run`: the options of `plan.run`, and the type of a bare root value and the verb for `prepare`. */
|
|
821
|
+
export interface RunOptions extends PlanRunOptions {
|
|
822
|
+
/** The type of a bare root value. */
|
|
823
|
+
readonly declaredType?: string;
|
|
824
|
+
/** The verb of `run`, for example `format`. The default is `eval` (VERB.PREPARE). */
|
|
825
|
+
readonly verb?: string;
|
|
826
|
+
}
|
|
827
|
+
|
|
808
828
|
/** The options of an evaluation of a plan. */
|
|
809
829
|
export interface EvalOptions {
|
|
810
830
|
/** A list that gets each effect, in its order. */
|
|
@@ -892,15 +912,18 @@ class Session implements Runtime {
|
|
|
892
912
|
return this.reading !== undefined;
|
|
893
913
|
}
|
|
894
914
|
|
|
915
|
+
/** This function resolves a path. A path from the root `caps` reads the capabilities through the grant (CAP.CAPS-GRANT). */
|
|
895
916
|
resolve(ctx: Context, path: readonly unknown[], options?: ResolveOptions): Resolution {
|
|
896
|
-
return this.read(ctx, path, undefined, options);
|
|
917
|
+
return this.read(readable(ctx, path), path, undefined, options);
|
|
897
918
|
}
|
|
898
919
|
|
|
899
920
|
/**
|
|
900
921
|
* This function resolves a compiled path (PLAN.PATH). Without a list of reads and without a trace, the compiled
|
|
901
|
-
* path answers at once. Each other resolution records its read, as `resolve` does.
|
|
922
|
+
* path answers at once. Each other resolution records its read, as `resolve` does. A path from the root `caps` reads
|
|
923
|
+
* the capabilities through the grant (CAP.CAPS-GRANT).
|
|
902
924
|
*/
|
|
903
|
-
resolvePath(
|
|
925
|
+
resolvePath(given: Context, compiled: CompiledPath): Resolution {
|
|
926
|
+
const ctx = readable(given, compiled.path);
|
|
904
927
|
if (this.reading === undefined && !this.traced && compiled.nav === this.machine) return compiled.resolve(ctx);
|
|
905
928
|
return this.read(ctx, compiled.path, compiled, undefined);
|
|
906
929
|
}
|
|
@@ -1303,7 +1326,7 @@ export interface Plan {
|
|
|
1303
1326
|
* @param options - The host code that does the effects, a signal to stop the run and a list that gets the reads.
|
|
1304
1327
|
* @returns The result, when the run ends.
|
|
1305
1328
|
*/
|
|
1306
|
-
run(context?: Context, options?:
|
|
1329
|
+
run(context?: Context, options?: PlanRunOptions): Promise<Result>;
|
|
1307
1330
|
}
|
|
1308
1331
|
|
|
1309
1332
|
interface Compiler {
|
|
@@ -1471,7 +1494,8 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
1471
1494
|
} catch (error) {
|
|
1472
1495
|
return failingWith(c, meta, placed(thrown(error, `the evaluator ${name}`), place));
|
|
1473
1496
|
}
|
|
1474
|
-
|
|
1497
|
+
// Code that cannot fail needs no place and no catch, and it does not read the context of its grant (PLAN.TOTAL).
|
|
1498
|
+
const done = finish(c, meta, TOTAL.has(inner) ? inner : placedCode(inner, place, name), undefined);
|
|
1475
1499
|
if (frees === undefined || kind === undefined || !pureCode(c, envelope, kind, typePath, name, quick)) return done;
|
|
1476
1500
|
const free = freeOf(doc, kind, split.parts, frees);
|
|
1477
1501
|
if (free === undefined) return done;
|
|
@@ -1721,6 +1745,24 @@ function copyPlain(value: unknown, memo: Map<object, unknown>): unknown {
|
|
|
1721
1745
|
* the split of the value. Thus a plan with these helpers keeps only its code.
|
|
1722
1746
|
*/
|
|
1723
1747
|
|
|
1748
|
+
/**
|
|
1749
|
+
* The compiled code that cannot fail (PLAN.TOTAL). Such code gives a value and throws nothing. It does not read its
|
|
1750
|
+
* context or its runtime. Only the kits of the kernel mark code, thus the code of a host always gets its grant.
|
|
1751
|
+
*/
|
|
1752
|
+
const TOTAL: WeakSet<Code> = /* @__PURE__ */ new WeakSet();
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* This function marks compiled code that cannot fail (PLAN.TOTAL). Then `compileNode` does not wrap the code in
|
|
1756
|
+
* `placedCode`, thus a literal costs one call. The kits of the kernel use it. The package does not export it.
|
|
1757
|
+
*
|
|
1758
|
+
* @param code - The code. It gives a value and throws nothing. It does not read its context or its runtime.
|
|
1759
|
+
* @returns The same code.
|
|
1760
|
+
*/
|
|
1761
|
+
export function totalCode(code: Code): Code {
|
|
1762
|
+
TOTAL.add(code);
|
|
1763
|
+
return code;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1724
1766
|
/**
|
|
1725
1767
|
* This function gives the code of a document: the code of its evaluator, with the place of each error and of each
|
|
1726
1768
|
* exception. The code gets the context that its grant permits, thus a compiled parent cannot widen it (CAP.NARROW-CODE).
|
|
@@ -1952,7 +1994,7 @@ class CompiledPlan implements Plan {
|
|
|
1952
1994
|
return this.code(ctx, s);
|
|
1953
1995
|
}
|
|
1954
1996
|
|
|
1955
|
-
run(context: Context = makeContext(), runOptions:
|
|
1997
|
+
run(context: Context = makeContext(), runOptions: PlanRunOptions = {}): Promise<Result> {
|
|
1956
1998
|
const machine = this.machine;
|
|
1957
1999
|
const ctx = rootContext(machine, context);
|
|
1958
2000
|
const s = new Session(machine, true, this.traced, runOptions.perform, runOptions.signal, undefined, readingOf(runOptions.reads, ctx));
|
package/src/formula.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Json, type Key, docEquals, isKey, isRecord } from "./doc.ts";
|
|
|
2
2
|
import type { Kind, KindFacts, LazyRule, WorldEntry } from "./world.ts";
|
|
3
3
|
import { type CompiledPath, type Context, type Resolution, compilePath, valueOfThing } from "./deref.ts";
|
|
4
4
|
import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
|
|
5
|
-
import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled } from "./evaluate.ts";
|
|
5
|
+
import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled, totalCode } from "./evaluate.ts";
|
|
6
6
|
import { type MachinePart, mergeMachines } from "./merge.ts";
|
|
7
7
|
import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeText } from "./types.ts";
|
|
8
8
|
import type { TypeInput, TypeRule } from "./typing.ts";
|
|
@@ -697,8 +697,9 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
|
|
|
697
697
|
|
|
698
698
|
return {
|
|
699
699
|
"formula.lit": compiled(({ doc }) => {
|
|
700
|
+
// A literal cannot fail, thus its code needs no place (PLAN.TOTAL).
|
|
700
701
|
const result = ok(doc.value);
|
|
701
|
-
return () => result;
|
|
702
|
+
return totalCode(() => result);
|
|
702
703
|
}),
|
|
703
704
|
|
|
704
705
|
"formula.ref": compiled(({ doc, parts, fill, address, machine }) => {
|
|
@@ -718,7 +719,7 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
|
|
|
718
719
|
// A step document without parts is its own value. A step document with parts gets the result of each part.
|
|
719
720
|
if (parts.length === 0) {
|
|
720
721
|
const same = ok(doc);
|
|
721
|
-
return () => same;
|
|
722
|
+
return totalCode(() => same);
|
|
722
723
|
}
|
|
723
724
|
return (ctx, rt) => {
|
|
724
725
|
const values = valuesOf(parts, ctx, rt);
|
package/src/generate.ts
CHANGED
|
@@ -146,9 +146,11 @@ export function generate(world: World, options: GenerateOptions): unknown {
|
|
|
146
146
|
const keys = options.keys ?? KEYS;
|
|
147
147
|
const steps = options.steps ?? 2;
|
|
148
148
|
const roots: readonly Root[] = options.roots ?? ROOTS;
|
|
149
|
+
// A kind with the verb step walks a step document. With a default of step, each other kind walks one too (GEN.STEPS).
|
|
150
|
+
const defaultStep = Object.hasOwn(world.defaults, "step");
|
|
149
151
|
const stepTypes = types.filter((type) => {
|
|
150
152
|
const found = lookupKind(world, type);
|
|
151
|
-
return found.ok && Object.hasOwn(found.kind.verbs, "step");
|
|
153
|
+
return found.ok && (defaultStep || Object.hasOwn(found.kind.verbs, "step"));
|
|
152
154
|
});
|
|
153
155
|
// A name under each namespace with a fallback that no evaluator knows, for example ["ops", "nope"].
|
|
154
156
|
const unknownNames: readonly (readonly Key[])[] = Object.keys(world.kinds)
|
|
@@ -205,7 +207,10 @@ export function generate(world: World, options: GenerateOptions): unknown {
|
|
|
205
207
|
return out;
|
|
206
208
|
};
|
|
207
209
|
|
|
208
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* A path from a root (D10). It uses the names in scope, and sometimes a step document. The kind of a step document
|
|
212
|
+
* has the verb `step`, or the default of the world for `step` serves it.
|
|
213
|
+
*/
|
|
209
214
|
const pathFor = (pos: Position, refKind: Kind | undefined): Json => {
|
|
210
215
|
if (rng.chance(malformed)) return rng.pick<Json>(["names", [], { names: "a" }, ["nowhere", "a"]]);
|
|
211
216
|
const others = roots.filter((name) => name !== "names");
|
package/src/index.ts
CHANGED
package/src/world.ts
CHANGED
|
@@ -270,10 +270,28 @@ export interface World {
|
|
|
270
270
|
readonly fallback?: Kind;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* The result of a lookup of a type in the world. The reasons of a failure:
|
|
275
|
+
*
|
|
276
|
+
* - `unknown`: the world has no entry for a key, and no fallback kind.
|
|
277
|
+
* - `cycle`: the aliases form a cycle. The lookup reached a path a second time.
|
|
278
|
+
* - `too-long`: the lookup followed more than 32 aliases without a path a second time (DEREF.CHAIN-REASON).
|
|
279
|
+
* - `not-a-kind`: the path reaches an entry of the wrong sort. An example is an alias whose target is not in the world.
|
|
280
|
+
* - `computed`: the type is a computed type, thus it has no keys.
|
|
281
|
+
*/
|
|
274
282
|
export type Lookup =
|
|
275
283
|
| { readonly ok: true; readonly kind: Kind; readonly path: readonly Key[]; readonly aliases: number }
|
|
276
|
-
| { readonly ok: false; readonly reason: "unknown" | "cycle" | "not-a-kind" | "computed"; readonly path: readonly Key[] };
|
|
284
|
+
| { readonly ok: false; readonly reason: "unknown" | "cycle" | "too-long" | "not-a-kind" | "computed"; readonly path: readonly Key[] };
|
|
285
|
+
|
|
286
|
+
/** The most aliases that one lookup follows (WORLD.ALIAS-LIMIT). */
|
|
287
|
+
const MAX_ALIASES = 32;
|
|
288
|
+
|
|
289
|
+
/** This function tells if two paths of a lookup have the same keys. */
|
|
290
|
+
function samePath(a: readonly Key[], b: readonly Key[]): boolean {
|
|
291
|
+
if (a.length !== b.length) return false;
|
|
292
|
+
for (let index = 0; index < a.length; index += 1) if (a[index] !== b[index]) return false;
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
277
295
|
|
|
278
296
|
/**
|
|
279
297
|
* This function tells if an entry of the world is a kind.
|
|
@@ -312,7 +330,8 @@ function entryAt(entries: Readonly<Record<string, WorldEntry>>, key: Key): World
|
|
|
312
330
|
|
|
313
331
|
/**
|
|
314
332
|
* This function finds the kind that a type names. It walks the path from the root of the world, one key at a
|
|
315
|
-
* time. An alias on the way is followed until a kind answers (collapse). A cycle of aliases stops the lookup
|
|
333
|
+
* time. An alias on the way is followed until a kind answers (collapse). A cycle of aliases stops the lookup, and so
|
|
334
|
+
* does a chain of more than 32 aliases. In a
|
|
316
335
|
* namespace with a fallback, a key that the namespace does not have gets the fallback kind. In an open world, a first
|
|
317
336
|
* key that the world does not have gets the fallback kind of the world.
|
|
318
337
|
*
|
|
@@ -325,6 +344,8 @@ export function lookupKind(world: World, type: TypeRef): Lookup {
|
|
|
325
344
|
if (start === undefined) return { ok: false, reason: "computed", path: [] };
|
|
326
345
|
let path: readonly Key[] = start;
|
|
327
346
|
let aliases = 0;
|
|
347
|
+
// The paths before each alias. The same path again closes a cycle, because the lookup of a path is always the same.
|
|
348
|
+
let seen: (readonly Key[])[] | undefined;
|
|
328
349
|
for (;;) {
|
|
329
350
|
let entries = world.kinds;
|
|
330
351
|
let fallback: Kind | undefined = world.fallback;
|
|
@@ -339,12 +360,16 @@ export function lookupKind(world: World, type: TypeRef): Lookup {
|
|
|
339
360
|
}
|
|
340
361
|
if (isAlias(entry)) {
|
|
341
362
|
aliases += 1;
|
|
342
|
-
if (aliases >
|
|
363
|
+
if (aliases > MAX_ALIASES) return { ok: false, reason: "too-long", path };
|
|
343
364
|
const target = entry.value.slice(1);
|
|
344
365
|
if (entry.value[0] !== "world" || !target.every((item) => typeof item === "string" || typeof item === "number")) {
|
|
345
366
|
return { ok: false, reason: "not-a-kind", path };
|
|
346
367
|
}
|
|
368
|
+
seen ??= [];
|
|
369
|
+
seen.push(path);
|
|
347
370
|
path = [...(target as readonly Key[]), ...path.slice(index + 1)];
|
|
371
|
+
const next = path;
|
|
372
|
+
if (seen.some((before) => samePath(before, next))) return { ok: false, reason: "cycle", path };
|
|
348
373
|
followed = true;
|
|
349
374
|
break;
|
|
350
375
|
}
|