@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/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.2",
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`. Two closures of `fn` are the same when they have the same parameters. Two `Held` values
78
- * are the same when their errors have the same code and place. Identity is not compared.
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 JSON values are equal. Equal values have the same texts, numbers and truth values,
225
- * the same items in the same order, and the same fields. The order of the fields does not matter. It does not use the stack for the
226
- * depth, thus a deep document cannot overflow it.
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
- if (typeof x !== "object" || typeof y !== "object" || x === null || y === null) return false;
238
- if (Array.isArray(x) !== Array.isArray(y)) return false;
239
- if (Array.isArray(x)) {
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 (x.length !== ys.length) return false;
242
- for (let index = 0; index < x.length; index += 1) stack.push([x[index], ys[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, pathMatches, splitParts } from "./parts.ts";
2
+ import { type Split, partsOf, pathMatches } from "./parts.ts";
3
3
  import { ask, type Answer } from "./ladder.ts";
4
- import { type CapsRule, type Lookup, type Side, type World, lookupKind } from "./world.ts";
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, found: Lookup): { readonly name?: string; readonly unknown: boolean } {
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 (!found.ok) return { unknown: true };
902
- const verbs = found.kind.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 envelope = isEnvelope(value) ? (value as Envelope) : undefined;
912
- const doc: Envelope = envelope ?? { type: declared ?? "?", value: value as Json };
913
- const meta0: Meta = c.traced ? { path: place.map(String), doc, declared: envelope === undefined, answer: NO_ANSWER, side: "derive" } : NO_META;
914
- if (envelope === undefined && declared === undefined) {
915
- return failing(c, meta0, place, "#PART?", "The value has no envelope, and its position declares no type.");
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, found);
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
- const split = splitParts(kind?.parts, doc.value);
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 = found.ok ? found.path : [];
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. Examples are an unknown kind, a value without its shape, a bare value without a type and a name that
129
- * nothing binds. The depth and the size have limits.
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
- /** A malformed document: an unknown kind, a bad shape, a bare value without a type, or a bad reference. */
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)) return dataOf([pos.declared]);
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 { Shape } from "./world.ts";
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
- return { t: "list", items: list.map((child, index) => walk(each, child, [...path, index])) };
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 { type CapsRule, type Kind, lookupKind } from "./world.ts";
4
- import { type PartSlot, type Split, pathMatches, splitParts } from "./parts.ts";
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 envelope = isEnvelope(at.value) ? (at.value as Envelope) : undefined;
108
- if (envelope === undefined && declared === undefined) return fail("#PART?", "The value has no envelope, and its position declares no type.", { place });
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 doc: Envelope = envelope ?? { type: declared as string, value: at.value as Json };
112
+ const { doc, kind, split } = read;
111
113
  const kindText = typeName(doc.type);
112
- const world = env.machine.world;
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
- const found = lookupKind(world, doc.type);
119
- const kind = found.ok ? found.kind : undefined;
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, pathMatches, splitParts } from "./parts.ts";
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 envelope = isEnvelope(item.value) ? (item.value as Envelope) : undefined;
238
- if (envelope === undefined && item.declared === undefined) {
239
- options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code: "#PART?", reason: "The value has no envelope, and its position declares no type." });
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 doc: Envelope = envelope ?? { type: item.declared ?? "?", value: item.value as Json };
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
- envelope === undefined,
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
- found.ok ? found.path : undefined,
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 split = splitParts(kind.parts, doc.value);
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
- const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
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
- return "skip";
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) });