@mark1russell7/coeval 0.4.2 → 0.5.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 +6 -4
- package/build/index.d.ts +94 -10
- package/build/index.d.ts.map +1 -1
- package/build/index.js +495 -118
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +8 -2
- package/src/doc.ts +26 -8
- package/src/errors.ts +4 -2
- package/src/evaluate.ts +27 -20
- package/src/formula.ts +45 -3
- package/src/generate.ts +37 -8
- package/src/index.ts +3 -2
- package/src/parts.ts +59 -3
- package/src/reference.ts +157 -18
- package/src/static.ts +17 -16
- package/src/stdlib.ts +180 -27
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.5.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/agree.ts
CHANGED
|
@@ -74,8 +74,14 @@ export type Agreement =
|
|
|
74
74
|
/**
|
|
75
75
|
* This function tells if two values are the same for `agree`. Lists and plain records compare item by item and field
|
|
76
76
|
* by field, and the order of the fields does not matter. Each other leaf compares with `Object.is`, thus `-0` is not
|
|
77
|
-
* `0`, and `NaN` is `NaN`.
|
|
78
|
-
*
|
|
77
|
+
* `0`, and `NaN` is `NaN`. The identity of a list or of a plain record is not compared.
|
|
78
|
+
*
|
|
79
|
+
* Two closures of `fn` are the same when they have the same parameters. Two `Held` values are the same when their
|
|
80
|
+
* errors have the same code and place. Each other object is the same only as itself, unless the rule of the caller
|
|
81
|
+
* tells otherwise.
|
|
82
|
+
*
|
|
83
|
+
* `docEquals` uses the same rules for lists, plain records and the other objects. It has no rules for closures, for
|
|
84
|
+
* `Held` values and for a rule of the caller. Thus two closures are equal there only when they are the same function.
|
|
79
85
|
*
|
|
80
86
|
* @param a - The first value.
|
|
81
87
|
* @param b - The second value.
|
package/src/doc.ts
CHANGED
|
@@ -221,9 +221,17 @@ export function fromJson(value: unknown): Envelope | undefined {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
* This function tells if two
|
|
225
|
-
* the same items in the same order, and the same fields. The order of the fields
|
|
226
|
-
*
|
|
224
|
+
* This function tells if two values are equal. Plain JSON compares by its structure. Equal values have the same
|
|
225
|
+
* texts, numbers and truth values, the same items in the same order, and the same fields. The order of the fields
|
|
226
|
+
* does not matter. Each leaf compares with `Object.is`, thus `-0` is not `0`, and `NaN` is `NaN`.
|
|
227
|
+
*
|
|
228
|
+
* Only lists and plain records (`isRecord`) compare by their items and fields. Each other object compares by
|
|
229
|
+
* identity: a date, a map, a function and an instance of a class are equal only to themselves. Thus an early cutoff
|
|
230
|
+
* with this function does not keep a stale host value.
|
|
231
|
+
*
|
|
232
|
+
* The function does not use the stack for the depth, thus a deep document cannot overflow it. A cycle of lists or
|
|
233
|
+
* records does not make it loop. `sameValue` of `agree` uses the same rules, and it adds rules for closures and for
|
|
234
|
+
* a rule of the caller.
|
|
227
235
|
*
|
|
228
236
|
* @param a - The first value.
|
|
229
237
|
* @param b - The second value.
|
|
@@ -231,15 +239,25 @@ export function fromJson(value: unknown): Envelope | undefined {
|
|
|
231
239
|
*/
|
|
232
240
|
export function docEquals(a: unknown, b: unknown): boolean {
|
|
233
241
|
const stack: [unknown, unknown][] = [[a, b]];
|
|
242
|
+
// The pairs of containers that the comparison already visits. It is made at the first pair of containers.
|
|
243
|
+
let seen: Map<object, Set<object>> | undefined;
|
|
234
244
|
while (stack.length > 0) {
|
|
235
245
|
const [x, y] = stack.pop() as [unknown, unknown];
|
|
236
246
|
if (Object.is(x, y)) continue;
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
247
|
+
const list = Array.isArray(x);
|
|
248
|
+
if (list !== Array.isArray(y)) return false;
|
|
249
|
+
// An object that is not a list and not a plain record is a host value: Object.is above compared it.
|
|
250
|
+
if (!list && (!isRecord(x) || !isRecord(y))) return false;
|
|
251
|
+
seen ??= new Map();
|
|
252
|
+
const pairs = seen.get(x as object);
|
|
253
|
+
if (pairs?.has(y as object) === true) continue;
|
|
254
|
+
if (pairs === undefined) seen.set(x as object, new Set([y as object]));
|
|
255
|
+
else pairs.add(y as object);
|
|
256
|
+
if (list) {
|
|
257
|
+
const xs = x as readonly unknown[];
|
|
240
258
|
const ys = y as readonly unknown[];
|
|
241
|
-
if (
|
|
242
|
-
for (let index = 0; index <
|
|
259
|
+
if (xs.length !== ys.length) return false;
|
|
260
|
+
for (let index = 0; index < xs.length; index += 1) stack.push([xs[index], ys[index]]);
|
|
243
261
|
continue;
|
|
244
262
|
}
|
|
245
263
|
const xr = x as Readonly<Record<string, unknown>>;
|
package/src/errors.ts
CHANGED
|
@@ -105,9 +105,11 @@ export class Failure extends Error {
|
|
|
105
105
|
*/
|
|
106
106
|
export function isStackOverflow(error: unknown): boolean {
|
|
107
107
|
try {
|
|
108
|
-
|
|
108
|
+
// A regular expression that compiles near the limit of the stack throws a SyntaxError with the same message.
|
|
109
|
+
// Thus the test reads the message with includes, and it accepts a SyntaxError.
|
|
110
|
+
return (error instanceof RangeError || error instanceof SyntaxError) && error.message.includes("call stack");
|
|
109
111
|
} catch {
|
|
110
|
-
// Near the limit of the stack, the test itself
|
|
112
|
+
// Near the limit of the stack, the test itself can overflow.
|
|
111
113
|
return true;
|
|
112
114
|
}
|
|
113
115
|
}
|
package/src/evaluate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, type TypeRef, isEnvelope, typeName } from "./doc.ts";
|
|
2
|
-
import { type Split,
|
|
2
|
+
import { type Split, partsOf, pathMatches } from "./parts.ts";
|
|
3
3
|
import { ask, type Answer } from "./ladder.ts";
|
|
4
|
-
import {
|
|
4
|
+
import type { CapsRule, Kind, Side, World } from "./world.ts";
|
|
5
5
|
import { type ErrorCode, type EvalError, type Result, fail, ok, placed, thrown, describe } from "./errors.ts";
|
|
6
6
|
import {
|
|
7
7
|
type Cap,
|
|
@@ -428,6 +428,7 @@ export type Runner = RunFn | CompiledRunner;
|
|
|
428
428
|
*
|
|
429
429
|
* - `maxDepth`: the deepest nesting of documents. The default is 1000.
|
|
430
430
|
* - `permits`: the rule of the host for an effect. The default is the function `permits` on the capabilities.
|
|
431
|
+
* - `known`: the names that the evaluators know, for `check`. Without it, `check` does not check such names.
|
|
431
432
|
*/
|
|
432
433
|
export interface Machine extends Navigator {
|
|
433
434
|
readonly evaluators: Readonly<Record<string, Evaluator>>;
|
|
@@ -435,6 +436,16 @@ export interface Machine extends Navigator {
|
|
|
435
436
|
readonly runners?: Readonly<Record<string, Runner>>;
|
|
436
437
|
readonly maxDepth?: number;
|
|
437
438
|
readonly permits?: (caps: readonly Cap[], kind: readonly Key[], context: Context) => boolean;
|
|
439
|
+
/**
|
|
440
|
+
* This function tells if an evaluator knows the name in the path of a kind. An example is the operation
|
|
441
|
+
* `["ops", "add"]` of a formula language: a fallback kind names it, and the evaluator finds it in a table.
|
|
442
|
+
* `check` asks it for each document with an evaluator. The function must be pure.
|
|
443
|
+
*
|
|
444
|
+
* @param path - The keys of the kind of the document, after each alias.
|
|
445
|
+
* @param evaluator - The name of the evaluator that the ladder gives the document.
|
|
446
|
+
* @returns `true` when the evaluator knows the name, or else the reason of the `#NAME?` that a run gives.
|
|
447
|
+
*/
|
|
448
|
+
readonly known?: (path: readonly Key[], evaluator: string) => true | string;
|
|
438
449
|
}
|
|
439
450
|
|
|
440
451
|
/** One evaluation in a trace. The children are the evaluations of its parts, in the order that they started. */
|
|
@@ -894,12 +905,12 @@ function finish(c: Compiler, meta: Meta, code: Code, run: RunCode): { readonly c
|
|
|
894
905
|
* This function finds the evaluator of the verb `eval` without the record of the rungs, for a compilation without a
|
|
895
906
|
* trace. It gives the same evaluator as `ask`: the own entry, then the kind, then the default of the world.
|
|
896
907
|
*/
|
|
897
|
-
function evaluatorOf(world: World, envelope: Envelope | undefined,
|
|
908
|
+
function evaluatorOf(world: World, envelope: Envelope | undefined, kind: Kind | undefined): { readonly name?: string; readonly unknown: boolean } {
|
|
898
909
|
const own = envelope?.own;
|
|
899
910
|
const ownName = own !== undefined && Object.hasOwn(own, "eval") ? own["eval"] : undefined;
|
|
900
911
|
if (ownName !== undefined) return { name: ownName, unknown: false };
|
|
901
|
-
if (
|
|
902
|
-
const verbs =
|
|
912
|
+
if (kind === undefined) return { unknown: true };
|
|
913
|
+
const verbs = kind.verbs;
|
|
903
914
|
const name = Object.hasOwn(verbs, "eval") ? verbs["eval"] : Object.hasOwn(world.defaults, "eval") ? world.defaults["eval"] : undefined;
|
|
904
915
|
return name === undefined ? { unknown: false } : { name, unknown: false };
|
|
905
916
|
}
|
|
@@ -908,17 +919,17 @@ function evaluatorOf(world: World, envelope: Envelope | undefined, found: Lookup
|
|
|
908
919
|
const NO_META: Meta = Object.freeze({ path: [], doc: { type: "?", value: null }, declared: false, answer: NO_ANSWER, side: "derive" });
|
|
909
920
|
|
|
910
921
|
function compileNode(c: Compiler, value: unknown, place: readonly Key[], declared: string | undefined, depth: number, address = false): { readonly code: Code; readonly run: RunCode } {
|
|
911
|
-
const
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
return failing(c,
|
|
922
|
+
const world = c.machine.world;
|
|
923
|
+
const read = partsOf(world, value, declared);
|
|
924
|
+
if (!read.ok) {
|
|
925
|
+
const meta: Meta = c.traced ? { path: place.map(String), doc: { type: "?", value: value as Json }, declared: true, answer: NO_ANSWER, side: "derive" } : NO_META;
|
|
926
|
+
return failing(c, meta, place, read.code, read.reason);
|
|
916
927
|
}
|
|
928
|
+
const { doc, kind, split } = read;
|
|
929
|
+
const envelope = read.declared ? undefined : doc;
|
|
930
|
+
const meta0: Meta = c.traced ? { path: place.map(String), doc, declared: read.declared, answer: NO_ANSWER, side: "derive" } : NO_META;
|
|
917
931
|
if (depth > c.maxDepth) return failing(c, meta0, place, "#DEPTH!", `The nesting is deeper than ${c.maxDepth} documents.`);
|
|
918
932
|
|
|
919
|
-
const world = c.machine.world;
|
|
920
|
-
const found = lookupKind(world, doc.type);
|
|
921
|
-
const kind = found.ok ? found.kind : undefined;
|
|
922
933
|
let meta = meta0;
|
|
923
934
|
let name: string | undefined;
|
|
924
935
|
let unknown: boolean;
|
|
@@ -928,7 +939,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
928
939
|
name = answer.evaluator;
|
|
929
940
|
unknown = answer.error === "unknown-kind";
|
|
930
941
|
} else {
|
|
931
|
-
const quick = evaluatorOf(world, envelope,
|
|
942
|
+
const quick = evaluatorOf(world, envelope, kind);
|
|
932
943
|
name = quick.name;
|
|
933
944
|
unknown = quick.unknown;
|
|
934
945
|
}
|
|
@@ -941,11 +952,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
941
952
|
return failing(c, meta, place, "#NOEVAL", `No rung gives an evaluator for ${kindText}.`);
|
|
942
953
|
}
|
|
943
954
|
|
|
944
|
-
|
|
945
|
-
if (!split.ok) {
|
|
946
|
-
const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
|
|
947
|
-
return failing(c, meta, place, "#VALUE!", `${split.reason}${at} The kind is ${kindText}.`);
|
|
948
|
-
}
|
|
955
|
+
if (!split.ok) return failing(c, meta, place, "#VALUE!", split.reason);
|
|
949
956
|
const parts: CompiledPart[] = split.parts.map((slot) => {
|
|
950
957
|
const item = slot.item;
|
|
951
958
|
// The capability rules of the contract narrow the context of the part, thus no evaluator can forget them.
|
|
@@ -958,7 +965,7 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
|
|
|
958
965
|
? { key: slot.key, path: slot.path, code: compiled.code, run: compiled.run }
|
|
959
966
|
: { key: slot.key, path: slot.path, name: header, code: compiled.code, run: compiled.run };
|
|
960
967
|
});
|
|
961
|
-
const typePath =
|
|
968
|
+
const typePath = read.typePath ?? [];
|
|
962
969
|
const input: CompileInput = {
|
|
963
970
|
kind: kindText,
|
|
964
971
|
typePath,
|
package/src/formula.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Json, isRecord } from "./doc.ts";
|
|
1
|
+
import { type Json, type Key, isRecord } from "./doc.ts";
|
|
2
2
|
import type { Kind, WorldEntry } from "./world.ts";
|
|
3
3
|
import { type Context, type Resolution, valueOfThing } from "./deref.ts";
|
|
4
4
|
import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
|
|
@@ -142,7 +142,7 @@ export function formulaKinds(): Readonly<Record<string, WorldEntry>> {
|
|
|
142
142
|
},
|
|
143
143
|
fn: {
|
|
144
144
|
verbs: { eval: "formula.fn" },
|
|
145
|
-
parts: { shape: "record", fields: { body: term }, required: ["params", "body"] },
|
|
145
|
+
parts: { shape: "record", fields: { params: { shape: "list", item: { shape: "text" }, distinct: true }, body: term }, required: ["params", "body"] },
|
|
146
146
|
scope: [{ names: { listAt: ["params"] }, in: [["body"]] }],
|
|
147
147
|
summary: "A function: its parameters are names for its body.",
|
|
148
148
|
},
|
|
@@ -474,18 +474,60 @@ export function formulaEvaluators(profile: FormulaProfile = {}): Readonly<Record
|
|
|
474
474
|
};
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
+
/**
|
|
478
|
+
* This function makes the hook `known` of the formula kit for one profile (STATIC.KNOWN). The evaluator
|
|
479
|
+
* `formula.op` knows an operation of `lazy` or of `ops`. A profile whose `ops` is a function answers through a call
|
|
480
|
+
* of that function. A function that throws counts as an answer, because the run then gives `#CALC!`. The evaluator
|
|
481
|
+
* `formula.ext` knows an extension of `ext` and each standard extension. The hook knows each name of the other
|
|
482
|
+
* evaluators.
|
|
483
|
+
*
|
|
484
|
+
* @param profile - The operations and the extensions of the language.
|
|
485
|
+
* @returns The hook: `true`, or the reason of the `#NAME?` that the evaluator gives in a run.
|
|
486
|
+
*/
|
|
487
|
+
export function formulaKnown(profile: FormulaProfile = {}): (path: readonly Key[], evaluator: string) => true | string {
|
|
488
|
+
const hasOp = (name: string): boolean => {
|
|
489
|
+
if (profile.lazy !== undefined && Object.hasOwn(profile.lazy, name)) return true;
|
|
490
|
+
const ops = profile.ops;
|
|
491
|
+
if (ops === undefined) return false;
|
|
492
|
+
if (typeof ops !== "function") return Object.hasOwn(ops, name);
|
|
493
|
+
try {
|
|
494
|
+
return ops(name) !== undefined;
|
|
495
|
+
} catch {
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const hasExt = (name: string): boolean => (profile.ext !== undefined && Object.hasOwn(profile.ext, name)) || Object.hasOwn(STD_EXT, name);
|
|
500
|
+
return (path, evaluator) => {
|
|
501
|
+
if (evaluator !== "formula.op" && evaluator !== "formula.ext") return true;
|
|
502
|
+
// The evaluators give the same reasons when they do not find the name.
|
|
503
|
+
const name = path.slice(1).join(".");
|
|
504
|
+
if (evaluator === "formula.op") return hasOp(name) ? true : `The language has no operation ${name}.`;
|
|
505
|
+
return hasExt(name) ? true : `The language has no extension ${name}.`;
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
477
509
|
/**
|
|
478
510
|
* This function makes a machine for a formula language. The machine has the kinds and the evaluators of the
|
|
479
|
-
* formula kit. It also has the kinds, the evaluators and the walkers that the language adds.
|
|
511
|
+
* formula kit. It also has the kinds, the evaluators and the walkers that the language adds. Its hook `known` comes
|
|
512
|
+
* from the profile (`formulaKnown`). A hook `known` of the language answers each name that the kit knows.
|
|
480
513
|
*
|
|
481
514
|
* @param profile - The operations and the rules of the language.
|
|
482
515
|
* @param more - The parts of a machine that the language adds. Its kinds win over the kinds of the kit.
|
|
483
516
|
* @returns The machine.
|
|
484
517
|
*/
|
|
485
518
|
export function formulaMachine(profile: FormulaProfile = {}, more: Partial<Machine> = {}): Machine {
|
|
519
|
+
const kit = formulaKnown(profile);
|
|
520
|
+
// An evaluator or a runner of the language with the name of an evaluator of the kit replaces it, and the kit does not answer for it.
|
|
521
|
+
const replaced = (name: string): boolean => (more.evaluators !== undefined && Object.hasOwn(more.evaluators, name)) || (more.runners !== undefined && Object.hasOwn(more.runners, name));
|
|
522
|
+
const theirs = more.known;
|
|
523
|
+
const known = (path: readonly Key[], evaluator: string): true | string => {
|
|
524
|
+
const answer = replaced(evaluator) ? true : kit(path, evaluator);
|
|
525
|
+
return answer === true && theirs !== undefined ? theirs(path, evaluator) : answer;
|
|
526
|
+
};
|
|
486
527
|
return {
|
|
487
528
|
...more,
|
|
488
529
|
world: { kinds: { ...formulaKinds(), ...more.world?.kinds }, defaults: { ...more.world?.defaults } },
|
|
489
530
|
evaluators: { ...formulaEvaluators(profile), ...more.evaluators },
|
|
531
|
+
known,
|
|
490
532
|
};
|
|
491
533
|
}
|
package/src/generate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type Json, type Key, docEquals, isEnvelope } from "./doc.ts";
|
|
1
|
+
import { type Json, type Key, docEquals, isEnvelope, typePath } from "./doc.ts";
|
|
2
2
|
import { type Kind, type ScopeRule, type Shape, type World, type WorldEntry, isAlias, isKind, isNamespace, lookupKind } from "./world.ts";
|
|
3
3
|
import { pathMatches } from "./parts.ts";
|
|
4
|
-
import { ROOTS } from "./deref.ts";
|
|
4
|
+
import { ROOTS, type Root } from "./deref.ts";
|
|
5
5
|
|
|
6
6
|
/** A source of random numbers with a seed. The same seed gives the same numbers. */
|
|
7
7
|
export interface Random {
|
|
@@ -70,6 +70,10 @@ export interface GenerateOptions {
|
|
|
70
70
|
readonly own?: readonly string[];
|
|
71
71
|
/** The keys for the steps of a path after its root and its name. */
|
|
72
72
|
readonly keys?: readonly Key[];
|
|
73
|
+
/** The roots that a path can start at. The default is each root. */
|
|
74
|
+
readonly roots?: readonly Root[];
|
|
75
|
+
/** The most steps of a path after its root and its name. The default is 2. */
|
|
76
|
+
readonly steps?: number;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/**
|
|
@@ -125,8 +129,9 @@ interface Scope {
|
|
|
125
129
|
/**
|
|
126
130
|
* This function builds a random document for a world from the contracts of its kinds. It reads the shapes of the
|
|
127
131
|
* parts, the declared types, the permitted kinds and the names that the scope rules bind. Some choices are malformed
|
|
128
|
-
* on purpose
|
|
129
|
-
* nothing binds. The depth and the size have
|
|
132
|
+
* on purpose, for example an unknown kind or an unknown name under a namespace with a fallback. Other examples are a
|
|
133
|
+
* value without its shape, a bare value without a type and a name that nothing binds. The depth and the size have
|
|
134
|
+
* limits.
|
|
130
135
|
*
|
|
131
136
|
* @param world - The world.
|
|
132
137
|
* @param options - The seed, the types, the limits and the data of the leaves.
|
|
@@ -139,10 +144,19 @@ export function generate(world: World, options: GenerateOptions): unknown {
|
|
|
139
144
|
const maxSize = options.maxSize ?? 24;
|
|
140
145
|
const malformed = options.malformed ?? 0.05;
|
|
141
146
|
const keys = options.keys ?? KEYS;
|
|
147
|
+
const steps = options.steps ?? 2;
|
|
148
|
+
const roots: readonly Root[] = options.roots ?? ROOTS;
|
|
142
149
|
const stepTypes = types.filter((type) => {
|
|
143
150
|
const found = lookupKind(world, type);
|
|
144
151
|
return found.ok && Object.hasOwn(found.kind.verbs, "step");
|
|
145
152
|
});
|
|
153
|
+
// A name under each namespace with a fallback that no evaluator knows, for example ["ops", "nope"].
|
|
154
|
+
const unknownNames: readonly (readonly Key[])[] = Object.keys(world.kinds)
|
|
155
|
+
.filter((key) => {
|
|
156
|
+
const entry = world.kinds[key];
|
|
157
|
+
return isNamespace(entry) && entry.fallback !== undefined;
|
|
158
|
+
})
|
|
159
|
+
.map((key) => [key, "nope"]);
|
|
146
160
|
let size = 0;
|
|
147
161
|
|
|
148
162
|
const kindOf = (type: readonly Key[]): Kind | undefined => {
|
|
@@ -194,11 +208,13 @@ export function generate(world: World, options: GenerateOptions): unknown {
|
|
|
194
208
|
/** A path from a root (D10). It uses the names in scope, and sometimes a step document. */
|
|
195
209
|
const pathFor = (pos: Position, refKind: Kind | undefined): Json => {
|
|
196
210
|
if (rng.chance(malformed)) return rng.pick<Json>(["names", [], { names: "a" }, ["nowhere", "a"]]);
|
|
197
|
-
const
|
|
211
|
+
const others = roots.filter((name) => name !== "names");
|
|
212
|
+
const named = roots.includes("names") && pos.names.length > 0 && (others.length === 0 || rng.chance(0.75));
|
|
213
|
+
const root = named || others.length === 0 ? "names" : rng.pick(others);
|
|
198
214
|
const path: Json[] = [root];
|
|
199
215
|
if (root === "names") path.push(pickName(pos.names));
|
|
200
216
|
if (root === "world") path.push(String(rng.pick(types)[0]));
|
|
201
|
-
for (let index = rng.int(
|
|
217
|
+
for (let index = rng.int(steps + 1); index > 0; index -= 1) {
|
|
202
218
|
if (stepTypes.length > 0 && rng.chance(0.2)) {
|
|
203
219
|
const type = rng.pick(stepTypes);
|
|
204
220
|
const kind = kindOf(type);
|
|
@@ -332,9 +348,18 @@ export function generate(world: World, options: GenerateOptions): unknown {
|
|
|
332
348
|
}
|
|
333
349
|
};
|
|
334
350
|
|
|
335
|
-
/**
|
|
351
|
+
/**
|
|
352
|
+
* A malformed document: an unknown kind, or an unknown name under a namespace with a fallback. It can also be a
|
|
353
|
+
* bad shape, a bare value without a type, or a bad reference.
|
|
354
|
+
*/
|
|
336
355
|
const brokenAt = (pos: Position): Json => {
|
|
337
356
|
const roll = rng.int(5);
|
|
357
|
+
if (roll === 0 && unknownNames.length > 0 && rng.chance(0.5)) {
|
|
358
|
+
// The fallback kind answers the name, and its evaluator does not know it, for example an unknown operation.
|
|
359
|
+
const type = rng.pick(unknownNames);
|
|
360
|
+
const kind = kindOf(type);
|
|
361
|
+
return envelope(type, kind?.parts === undefined ? dataOf(type) : valueOf(kind.parts, [], pos, scopeOf(kind), type));
|
|
362
|
+
}
|
|
338
363
|
if (roll === 0) return { type: rng.pick<Json>(["nothing", ["no", "such"]]), value: 1 };
|
|
339
364
|
if (roll === 1) return pos.declared === undefined ? rng.pick<Json>([7, "bare", [1]]) : { type: "ref", value: ["names", "unbound"] };
|
|
340
365
|
if (roll === 2) {
|
|
@@ -349,7 +374,11 @@ export function generate(world: World, options: GenerateOptions): unknown {
|
|
|
349
374
|
size += 1;
|
|
350
375
|
const leaf = pos.depth >= maxDepth || size >= maxSize;
|
|
351
376
|
if (rng.chance(malformed)) return brokenAt(pos);
|
|
352
|
-
if (pos.declared !== undefined && rng.chance(leaf ? 0.7 : 0.3))
|
|
377
|
+
if (pos.declared !== undefined && rng.chance(leaf ? 0.7 : 0.3)) {
|
|
378
|
+
const bare = dataOf([pos.declared]);
|
|
379
|
+
// A bare value with the form of an envelope is a document of its own type, and the position must permit that type.
|
|
380
|
+
if (!isEnvelope(bare) || permitted(typePath(bare.type) ?? [], pos.kinds)) return bare;
|
|
381
|
+
}
|
|
353
382
|
let candidates = types.filter((type) => permitted(type, pos.kinds) && (!leaf || leafKind(kindOf(type))));
|
|
354
383
|
if (pos.hint !== undefined && rng.chance(0.6)) {
|
|
355
384
|
const hinted = candidates.filter((type) => pos.hint?.some((hint) => hint.length === type.length && hint.every((key, index) => key === type[index])));
|
package/src/index.ts
CHANGED
|
@@ -95,7 +95,7 @@ export {
|
|
|
95
95
|
worldThing,
|
|
96
96
|
} from "./deref.ts";
|
|
97
97
|
export { type Classification, type Shape, classify, tag } from "./classify.ts";
|
|
98
|
-
export { type PartSlot, type Split, pathMatches, splitParts } from "./parts.ts";
|
|
98
|
+
export { type DocParts, type PartSlot, type Split, partsOf, pathMatches, splitParts } from "./parts.ts";
|
|
99
99
|
export {
|
|
100
100
|
type AllOptions,
|
|
101
101
|
type Code,
|
|
@@ -181,10 +181,11 @@ export {
|
|
|
181
181
|
STD_EXT,
|
|
182
182
|
formulaEvaluators,
|
|
183
183
|
formulaKinds,
|
|
184
|
+
formulaKnown,
|
|
184
185
|
formulaMachine,
|
|
185
186
|
isClosure,
|
|
186
187
|
jsTruthy,
|
|
187
188
|
} from "./formula.ts";
|
|
188
|
-
export { type Definitions, type ReferenceOptions, formulaDefinitions, reference } from "./reference.ts";
|
|
189
|
+
export { type Definitions, type ReferenceOptions, STD_DEFINITIONS, formulaDefinitions, reference } from "./reference.ts";
|
|
189
190
|
export { type AgreeOptions, type Agreement, type Tier, type TierResult, agree, sameValue } from "./agree.ts";
|
|
190
191
|
export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
|
package/src/parts.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Json, type Key, docEquals, isEnvelope, isRecord, typeName, typePath } from "./doc.ts";
|
|
2
|
-
import type
|
|
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";
|
|
3
3
|
|
|
4
4
|
/** One part of a value: its place in the value, the JSON of the part, and the type that its position declares. */
|
|
5
5
|
export interface PartSlot {
|
|
@@ -56,6 +56,11 @@ function hasSlots(template: Template): boolean {
|
|
|
56
56
|
return template.t !== "data";
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** This function tells if a shape holds only data: no value of the shape can hold a part. */
|
|
60
|
+
function isData(shape: Shape): boolean {
|
|
61
|
+
return shape.shape === "none" || shape.shape === "text" || shape.shape === "number" || shape.shape === "one";
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
/** This function gives the reason why a value is not data of a shape, or `undefined` when it is. */
|
|
60
65
|
function dataFault(spec: Extract<Shape, { readonly shape: "text" | "number" | "one" }>, item: Json): string | undefined {
|
|
61
66
|
if (spec.shape === "text") return typeof item === "string" ? undefined : "The value must be a text.";
|
|
@@ -127,7 +132,9 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
|
|
|
127
132
|
if (twice !== -1) throw new Bad("The items of the list must be different.", [...path, twice]);
|
|
128
133
|
}
|
|
129
134
|
const each: Shape = spec.item ?? { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) };
|
|
130
|
-
|
|
135
|
+
const items = list.map((child, index) => walk(each, child, [...path, index]));
|
|
136
|
+
// A list of data is data, also when it is empty. Only a list that can hold parts gives null in the data.
|
|
137
|
+
return isData(each) ? { t: "data", value: item } : { t: "list", items };
|
|
131
138
|
}
|
|
132
139
|
if (!isRecord(item)) throw new Bad("The value must be a record.", path);
|
|
133
140
|
if (spec.shape === "map") {
|
|
@@ -181,6 +188,55 @@ export function splitParts(shape: Shape | undefined, value: Json): Split {
|
|
|
181
188
|
return { ok: true, parts, data: strip(template) ?? null, fill: (handles) => fillWith(template, handles) };
|
|
182
189
|
}
|
|
183
190
|
|
|
191
|
+
/**
|
|
192
|
+
* The kind and the parts of one document: what `partsOf` gives. `ok` is `false` only when the value is not a
|
|
193
|
+
* document: it has no envelope, and its position declares no type. A value without the shape of its kind gives a
|
|
194
|
+
* failed `split`.
|
|
195
|
+
*/
|
|
196
|
+
export type DocParts =
|
|
197
|
+
| {
|
|
198
|
+
readonly ok: true;
|
|
199
|
+
/** The document. A bare value gets the type that its position declares. */
|
|
200
|
+
readonly doc: Envelope;
|
|
201
|
+
/** `true` when the type came from the position and not from an envelope. */
|
|
202
|
+
readonly declared: boolean;
|
|
203
|
+
/** The kind, or `undefined` when the world has no kind for the type. Then the value is data. */
|
|
204
|
+
readonly kind: Kind | undefined;
|
|
205
|
+
/** The keys of the kind after each alias, or `undefined` without a kind. */
|
|
206
|
+
readonly typePath: readonly Key[] | undefined;
|
|
207
|
+
/**
|
|
208
|
+
* The split of the value by the shape of the kind. The reason of a failed split is the reason of the `#VALUE!`
|
|
209
|
+
* of an evaluation: it names the kind. Its `at` gives the keys from the value to the problem.
|
|
210
|
+
*/
|
|
211
|
+
readonly split: Split;
|
|
212
|
+
}
|
|
213
|
+
| { readonly ok: false; readonly code: "#PART?"; readonly reason: string };
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* This function reads one document by the contracts of the world. It finds the kind of the type. A bare value gets
|
|
217
|
+
* the type that its position declares. Then the function splits the value by the shape of the kind. The plan, the
|
|
218
|
+
* reference interpreter and the static kit read each document with this function. Thus they see the same parts.
|
|
219
|
+
*
|
|
220
|
+
* @param world - The world of the kinds.
|
|
221
|
+
* @param value - The document, or a bare value.
|
|
222
|
+
* @param declaredType - The type that the position of a bare value declares.
|
|
223
|
+
* @returns The document, its kind and its parts, or the reason why the value is not a document.
|
|
224
|
+
*/
|
|
225
|
+
export function partsOf(world: World, value: unknown, declaredType?: string): DocParts {
|
|
226
|
+
const envelope = isEnvelope(value) ? (value as Envelope) : undefined;
|
|
227
|
+
if (envelope === undefined && declaredType === undefined) return { ok: false, code: "#PART?", reason: "The value has no envelope, and its position declares no type." };
|
|
228
|
+
const doc: Envelope = envelope ?? { type: declaredType as string, value: value as Json };
|
|
229
|
+
const found = lookupKind(world, doc.type);
|
|
230
|
+
const kind = found.ok ? found.kind : undefined;
|
|
231
|
+
const split = splitParts(kind?.parts, doc.value);
|
|
232
|
+
const typePath = found.ok ? found.path : undefined;
|
|
233
|
+
if (split.ok) return { ok: true, doc, declared: envelope === undefined, kind, typePath, split };
|
|
234
|
+
// The reason names the place in the value and the kind, as each reader of the document gives it.
|
|
235
|
+
const where = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
|
|
236
|
+
const reason = `${split.reason}${where} The kind is ${typeName(doc.type)}.`;
|
|
237
|
+
return { ok: true, doc, declared: envelope === undefined, kind, typePath, split: { ok: false, reason, at: split.at } };
|
|
238
|
+
}
|
|
239
|
+
|
|
184
240
|
/**
|
|
185
241
|
* This function tells if the path of a part matches a pattern of a scope rule. The key `"*"` of a pattern matches
|
|
186
242
|
* each key.
|