@mark1russell7/coeval 0.4.2 → 0.4.3
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 +4 -3
- package/build/index.d.ts +85 -10
- package/build/index.d.ts.map +1 -1
- package/build/index.js +219 -85
- 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/evaluate.ts +27 -20
- package/src/formula.ts +45 -3
- package/src/generate.ts +26 -5
- package/src/index.ts +2 -1
- package/src/parts.ts +59 -3
- package/src/reference.ts +10 -15
- package/src/static.ts +17 -16
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.
|
|
4
|
+
"version": "0.4.3",
|
|
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/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,4 +1,4 @@
|
|
|
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
4
|
import { ROOTS } from "./deref.ts";
|
|
@@ -125,8 +125,9 @@ interface Scope {
|
|
|
125
125
|
/**
|
|
126
126
|
* This function builds a random document for a world from the contracts of its kinds. It reads the shapes of the
|
|
127
127
|
* 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
|
|
128
|
+
* on purpose, for example an unknown kind or an unknown name under a namespace with a fallback. Other examples are a
|
|
129
|
+
* value without its shape, a bare value without a type and a name that nothing binds. The depth and the size have
|
|
130
|
+
* limits.
|
|
130
131
|
*
|
|
131
132
|
* @param world - The world.
|
|
132
133
|
* @param options - The seed, the types, the limits and the data of the leaves.
|
|
@@ -143,6 +144,13 @@ export function generate(world: World, options: GenerateOptions): unknown {
|
|
|
143
144
|
const found = lookupKind(world, type);
|
|
144
145
|
return found.ok && Object.hasOwn(found.kind.verbs, "step");
|
|
145
146
|
});
|
|
147
|
+
// A name under each namespace with a fallback that no evaluator knows, for example ["ops", "nope"].
|
|
148
|
+
const unknownNames: readonly (readonly Key[])[] = Object.keys(world.kinds)
|
|
149
|
+
.filter((key) => {
|
|
150
|
+
const entry = world.kinds[key];
|
|
151
|
+
return isNamespace(entry) && entry.fallback !== undefined;
|
|
152
|
+
})
|
|
153
|
+
.map((key) => [key, "nope"]);
|
|
146
154
|
let size = 0;
|
|
147
155
|
|
|
148
156
|
const kindOf = (type: readonly Key[]): Kind | undefined => {
|
|
@@ -332,9 +340,18 @@ export function generate(world: World, options: GenerateOptions): unknown {
|
|
|
332
340
|
}
|
|
333
341
|
};
|
|
334
342
|
|
|
335
|
-
/**
|
|
343
|
+
/**
|
|
344
|
+
* A malformed document: an unknown kind, or an unknown name under a namespace with a fallback. It can also be a
|
|
345
|
+
* bad shape, a bare value without a type, or a bad reference.
|
|
346
|
+
*/
|
|
336
347
|
const brokenAt = (pos: Position): Json => {
|
|
337
348
|
const roll = rng.int(5);
|
|
349
|
+
if (roll === 0 && unknownNames.length > 0 && rng.chance(0.5)) {
|
|
350
|
+
// The fallback kind answers the name, and its evaluator does not know it, for example an unknown operation.
|
|
351
|
+
const type = rng.pick(unknownNames);
|
|
352
|
+
const kind = kindOf(type);
|
|
353
|
+
return envelope(type, kind?.parts === undefined ? dataOf(type) : valueOf(kind.parts, [], pos, scopeOf(kind), type));
|
|
354
|
+
}
|
|
338
355
|
if (roll === 0) return { type: rng.pick<Json>(["nothing", ["no", "such"]]), value: 1 };
|
|
339
356
|
if (roll === 1) return pos.declared === undefined ? rng.pick<Json>([7, "bare", [1]]) : { type: "ref", value: ["names", "unbound"] };
|
|
340
357
|
if (roll === 2) {
|
|
@@ -349,7 +366,11 @@ export function generate(world: World, options: GenerateOptions): unknown {
|
|
|
349
366
|
size += 1;
|
|
350
367
|
const leaf = pos.depth >= maxDepth || size >= maxSize;
|
|
351
368
|
if (rng.chance(malformed)) return brokenAt(pos);
|
|
352
|
-
if (pos.declared !== undefined && rng.chance(leaf ? 0.7 : 0.3))
|
|
369
|
+
if (pos.declared !== undefined && rng.chance(leaf ? 0.7 : 0.3)) {
|
|
370
|
+
const bare = dataOf([pos.declared]);
|
|
371
|
+
// A bare value with the form of an envelope is a document of its own type, and the position must permit that type.
|
|
372
|
+
if (!isEnvelope(bare) || permitted(typePath(bare.type) ?? [], pos.kinds)) return bare;
|
|
373
|
+
}
|
|
353
374
|
let candidates = types.filter((type) => permitted(type, pos.kinds) && (!leaf || leafKind(kindOf(type))));
|
|
354
375
|
if (pos.hint !== undefined && rng.chance(0.6)) {
|
|
355
376
|
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,6 +181,7 @@ export {
|
|
|
181
181
|
STD_EXT,
|
|
182
182
|
formulaEvaluators,
|
|
183
183
|
formulaKinds,
|
|
184
|
+
formulaKnown,
|
|
184
185
|
formulaMachine,
|
|
185
186
|
isClosure,
|
|
186
187
|
jsTruthy,
|
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.
|
package/src/reference.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, isEnvelope, isRecord, typeName } from "./doc.ts";
|
|
2
2
|
import { ask } from "./ladder.ts";
|
|
3
|
-
import {
|
|
4
|
-
import { type PartSlot, type Split,
|
|
3
|
+
import type { CapsRule, Kind } from "./world.ts";
|
|
4
|
+
import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
|
|
5
5
|
import { type Cap, type Context, type Resolution, type ResolveOptions, resolve, valueOfThing, worldThing } from "./deref.ts";
|
|
6
6
|
import { type EvalError, type Failed, type Result, Failure, fail, ok, placed, thrown } from "./errors.ts";
|
|
7
7
|
import {
|
|
@@ -104,25 +104,20 @@ function choose(env: Env, name: string): Chosen | undefined {
|
|
|
104
104
|
*/
|
|
105
105
|
function examine(env: Env, at: At): Seen | Failed {
|
|
106
106
|
const { place, declared } = at;
|
|
107
|
-
const
|
|
108
|
-
|
|
107
|
+
const world = env.machine.world;
|
|
108
|
+
// The lookup of the kind and the split of the value are definitions of the model (D15): partsOf gives both.
|
|
109
|
+
const read = partsOf(world, at.value, declared);
|
|
110
|
+
if (!read.ok) return fail(read.code, read.reason, { place });
|
|
109
111
|
if (at.depth > env.maxDepth) return fail("#DEPTH!", `The nesting is deeper than ${env.maxDepth} documents.`, { place });
|
|
110
|
-
const
|
|
112
|
+
const { doc, kind, split } = read;
|
|
111
113
|
const kindText = typeName(doc.type);
|
|
112
|
-
const
|
|
113
|
-
const answer = ask(world, envelope ?? {}, "eval", declared === undefined ? {} : { declaredType: declared });
|
|
114
|
+
const answer = ask(world, read.declared ? {} : doc, "eval", declared === undefined ? {} : { declaredType: declared });
|
|
114
115
|
if (answer.error === "unknown-kind") return fail("#KIND?", `The world has no kind ${kindText}.`, { place });
|
|
115
116
|
const name = answer.evaluator;
|
|
116
117
|
const chosen = name === undefined ? undefined : choose(env, name);
|
|
117
118
|
if (name === undefined || chosen === undefined) return fail("#NOEVAL", `No rung gives an evaluator for ${kindText}.`, { place });
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const split = splitParts(kind?.parts, doc.value);
|
|
121
|
-
if (!split.ok) {
|
|
122
|
-
const where = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
|
|
123
|
-
return fail("#VALUE!", `${split.reason}${where} The kind is ${kindText}.`, { place });
|
|
124
|
-
}
|
|
125
|
-
return { doc, kindText, typePath: found.ok ? found.path : [], kind, name, chosen, split };
|
|
119
|
+
if (!split.ok) return fail("#VALUE!", split.reason, { place });
|
|
120
|
+
return { doc, kindText, typePath: read.typePath ?? [], kind, name, chosen, split };
|
|
126
121
|
}
|
|
127
122
|
|
|
128
123
|
/** This function gives the capabilities that the rules of a kind give one part (D14, CAP.CONTRACT, CAP.ONLY). */
|
package/src/static.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
|
|
2
2
|
import { type Kind, type NamesSource, type ScopeRule, type World, lookupKind } from "./world.ts";
|
|
3
3
|
import { ask } from "./ladder.ts";
|
|
4
|
-
import { type PartSlot,
|
|
4
|
+
import { type PartSlot, partsOf, pathMatches } from "./parts.ts";
|
|
5
5
|
import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
|
|
6
6
|
import type { ErrorCode } from "./errors.ts";
|
|
7
7
|
import { type Machine, partCaps, permits } from "./evaluate.ts";
|
|
@@ -234,37 +234,32 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
|
|
|
234
234
|
];
|
|
235
235
|
while (stack.length > 0) {
|
|
236
236
|
const item = stack.pop() as Item;
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code:
|
|
237
|
+
const read = partsOf(world, item.value, item.declared);
|
|
238
|
+
if (!read.ok) {
|
|
239
|
+
options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code: read.code, reason: read.reason });
|
|
240
240
|
continue;
|
|
241
241
|
}
|
|
242
|
-
const
|
|
243
|
-
const found = lookupKind(world, doc.type);
|
|
244
|
-
const kind = found.ok ? found.kind : undefined;
|
|
242
|
+
const { doc, kind, split } = read;
|
|
245
243
|
const here = new Visit(
|
|
246
244
|
item.place,
|
|
247
245
|
item.at,
|
|
248
246
|
doc,
|
|
249
|
-
|
|
247
|
+
read.declared,
|
|
250
248
|
item.depth,
|
|
251
249
|
item.names,
|
|
252
250
|
item.later,
|
|
253
251
|
item.slot,
|
|
254
252
|
item.parent,
|
|
255
253
|
kind,
|
|
256
|
-
|
|
254
|
+
read.typePath,
|
|
257
255
|
item.address ? true : undefined,
|
|
258
256
|
item.caps,
|
|
259
257
|
);
|
|
260
258
|
const answer = visit(here);
|
|
261
259
|
if (answer === "skip" || kind === undefined) continue;
|
|
262
|
-
const
|
|
263
|
-
const base: Trail | undefined = envelope === undefined ? item.at : { up: item.at, keys: ["value"] };
|
|
260
|
+
const base: Trail | undefined = read.declared ? item.at : { up: item.at, keys: ["value"] };
|
|
264
261
|
if (!split.ok) {
|
|
265
|
-
|
|
266
|
-
const where = keysOf({ up: base, keys: split.at });
|
|
267
|
-
options.problem?.({ place: keysOf(item.place), at: where, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
|
|
262
|
+
options.problem?.({ place: keysOf(item.place), at: keysOf({ up: base, keys: split.at }), code: "#VALUE!", reason: split.reason });
|
|
268
263
|
continue;
|
|
269
264
|
}
|
|
270
265
|
const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
|
|
@@ -424,7 +419,8 @@ function capReason(v: DocVisit, kind: readonly Key[]): string {
|
|
|
424
419
|
* - `#PART?`: a value without a type.
|
|
425
420
|
* - `#DEPTH!`: too much nesting.
|
|
426
421
|
* - `#REF!` and `#STEP?`: a bad path.
|
|
427
|
-
* - `#NAME?`: a name that nothing binds.
|
|
422
|
+
* - `#NAME?`: a name that nothing binds. With the hook `known` of the machine, also a name that the language does not
|
|
423
|
+
* have, for example an operation.
|
|
428
424
|
* - `#CAP!`: an effect that the capabilities of its document do not permit. A kind around the document can narrow them.
|
|
429
425
|
*
|
|
430
426
|
* @param machine - The world and the code of the evaluators.
|
|
@@ -455,6 +451,10 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
455
451
|
const name = answer.evaluator;
|
|
456
452
|
if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
|
|
457
453
|
issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
|
|
454
|
+
} else if (machine.known !== undefined) {
|
|
455
|
+
// The evaluator gives #NAME? in a run for a name that the language does not have, for example an operation.
|
|
456
|
+
const known = machine.known(v.typePath ?? [], name);
|
|
457
|
+
if (known !== true) issues.push({ code: "#NAME?", place: v.place, reason: known, at: v.at });
|
|
458
458
|
}
|
|
459
459
|
const caps = v.caps;
|
|
460
460
|
if (caps !== undefined && v.kind?.effects !== undefined) {
|
|
@@ -474,7 +474,8 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
|
|
|
474
474
|
const stepAt = (index: number): readonly Key[] => [...v.at, "value", ...(index < path.length ? [index] : [])];
|
|
475
475
|
if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
|
|
476
476
|
issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.`, at: stepAt(0) });
|
|
477
|
-
|
|
477
|
+
// The step documents of the path are parts, and they evaluate before the path, thus the walk visits them.
|
|
478
|
+
return undefined;
|
|
478
479
|
}
|
|
479
480
|
if (!roots.includes(root)) {
|
|
480
481
|
issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.`, at: stepAt(0) });
|