@mark1russell7/coeval 0.9.0 → 0.10.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/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.9.0",
4
+ "version": "0.10.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/conform.ts CHANGED
@@ -32,7 +32,7 @@ import { type Definitions, STD_DEFINITIONS } from "./reference.ts";
32
32
  import { STD_EVALUATORS } from "./stdlib.ts";
33
33
  import { sameValue } from "./agree.ts";
34
34
  import { type Random, random } from "./generate.ts";
35
- import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, isKind, isNamespace, lookupKind } from "./world.ts";
35
+ import { type Kind, type LazyRule, type Shape, type World, type WorldEntry, effectsOfVerb, isKind, isNamespace, lookupKind } from "./world.ts";
36
36
 
37
37
  /*
38
38
  * The conformance tests of one evaluator (K92, R7 P3, D36). `conforms` calls the code of one evaluator or runner
@@ -85,8 +85,9 @@ export interface Finding {
85
85
  /** The options of `conforms`. */
86
86
  export interface ConformOptions {
87
87
  /**
88
- * The verb that the evaluator serves. The default is `eval`. The facts of a contract (`effects`, `pure` and `lazy`)
89
- * describe only `eval`, thus `conforms` checks C2, C3 and C4 only for `eval` (D23).
88
+ * The verb that the evaluator serves. The default is `eval`. The facts `pure` and `lazy` of a contract describe only
89
+ * `eval`, thus `conforms` checks C3 and C4 only for `eval` (D23). `conforms` checks C2 for each verb that the
90
+ * contract declares effects for: `eval`, and each verb with an entry in `verbEffects` (D49).
90
91
  */
91
92
  readonly verb?: string;
92
93
  /**
@@ -276,6 +277,8 @@ interface Contract {
276
277
  readonly lazy: readonly LazyRule[] | undefined;
277
278
  /** `true` when the facts describe the verb: they describe only `eval`. */
278
279
  readonly describes: boolean;
280
+ /** `true` when the contract declares the effects of the verb: for `eval`, and for a verb in `verbEffects`. */
281
+ readonly declares: boolean;
279
282
  readonly caps: readonly Cap[];
280
283
  }
281
284
 
@@ -487,13 +490,16 @@ function typesNaming(world: World, name: string, verb: string): readonly TypeRef
487
490
  /** This function gives the facts of the kind of a type. The hook `facts` of the machine wins over the contract. */
488
491
  function contractOf(env: Env, kind: Kind, typePath: readonly Key[], caps: readonly Cap[] | undefined): Contract {
489
492
  const facts = env.machine.facts?.(typePath, env.name);
490
- const effects = kind.effects;
493
+ // A verb without a declaration keeps the effects of eval for the capabilities of the probes, as before D49.
494
+ const own = effectsOfVerb(kind, env.verb);
495
+ const effects = own ?? kind.effects;
491
496
  const declared: readonly Cap[] = effects === undefined ? [] : effects === "type" ? [typePath] : effects;
492
497
  return {
493
498
  declared,
494
499
  pure: (facts?.pure ?? kind.pure) === true,
495
500
  lazy: facts?.lazy ?? kind.lazy,
496
501
  describes: env.verb === "eval",
502
+ declares: own !== undefined,
497
503
  caps: caps ?? [...declared, "probe"],
498
504
  };
499
505
  }
@@ -872,13 +878,15 @@ function checkShot(env: Env, sample: Sample, contract: Contract, scenario: Scena
872
878
  report(env, "C1", `The code gives its effect ${effect.kind} ${capsText(effect.context.caps)}, and its document has ${capsText(docCaps)}.`, probe);
873
879
  }
874
880
  }
875
- if (contract.describes) {
876
- // C2: only the declared effects.
881
+ if (contract.declares) {
882
+ // C2: only the declared effects of the verb (VERB.EFFECTS).
877
883
  for (const effect of shot.caused) {
878
884
  if (!permits(contract.declared, effect.kind)) {
879
- report(env, "C2", `The code causes the effect ${effect.kind}, and the contract of ${sample.type} declares ${contract.declared.length === 0 ? "no effects" : capsText(contract.declared)}.`, probe);
885
+ report(env, "C2", `The code causes the effect ${effect.kind}, and the contract of ${sample.type} declares ${contract.declared.length === 0 ? "no effects" : capsText(contract.declared)}${env.verb === "eval" ? "" : ` for the verb ${env.verb}`}.`, probe);
880
886
  }
881
887
  }
888
+ }
889
+ if (contract.describes) {
882
890
  // C4: a pure kind causes no effect.
883
891
  const first = shot.caused[0];
884
892
  if (contract.pure && first !== undefined) report(env, "C4", `The kind ${sample.type} is pure, and the code causes the effect ${first.kind}.`, probe);
package/src/formula.ts CHANGED
@@ -4,7 +4,7 @@ import { type CompiledPath, type Context, type Resolution, compilePath, valueOfT
4
4
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
5
5
  import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled } from "./evaluate.ts";
6
6
  import { type MachinePart, mergeMachines } from "./merge.ts";
7
- import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, stepType, typeText } from "./types.ts";
7
+ import { type Domain, type ValueType, HEADER_KEYS, STD_DOMAINS, callable, fieldOf, fits, fnOf, holdsEnvelope, isPrecise, isUnknown, itemOf, join, member, stepType, typeText } from "./types.ts";
8
8
  import type { TypeInput, TypeRule } from "./typing.ts";
9
9
  import { decodeDomain, domainTypeOf } from "./codec.ts";
10
10
 
@@ -42,6 +42,8 @@ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<str
42
42
  * evaluates, and one failed part gives its own error. Without it, the first error stops the evaluation.
43
43
  * - `pure`: the pure operations, for `factsOf` (D24). `true` names each operation that is not volatile. The default
44
44
  * names no operation.
45
+ * - `pureExt`: the pure extensions of `ext`, by their name, for `factsOf` and the folds of a plan (PLAN.FOLD, D47).
46
+ * The default names no extension. A standard extension is pure without a name here.
45
47
  * - `volatile`: the operations whose results can change while their arguments stay the same, for example a clock.
46
48
  * - `sigs`: the signatures of the operations, by their name, for a check of types (TYPE.SIGNATURE). A list gives
47
49
  * overloads. An operation without a signature takes and gives `unknown` (TYPE.GRADUAL).
@@ -61,13 +63,14 @@ export interface FormulaProfile {
61
63
  readonly check?: (value: unknown, op: string) => Result | undefined;
62
64
  readonly combine?: (errors: readonly EvalError[]) => EvalError;
63
65
  readonly pure?: true | readonly string[];
66
+ readonly pureExt?: readonly string[];
64
67
  readonly volatile?: readonly string[];
65
68
  readonly sigs?: Readonly<Record<string, Signature | readonly Signature[]>> | ((name: string) => Signature | readonly Signature[] | undefined);
66
69
  }
67
70
 
68
71
  /**
69
- * The signature of an operation: the value types of its parameters and of its result (TYPE.SIGNATURE). A parameter of
70
- * the type `unknown` at the end of `params` is optional. A later version can add a key, for example the type that a
72
+ * The signature of an operation: the value types of its parameters and of its result (TYPE.SIGNATURE). A parameter at
73
+ * the end of `params` is optional when `undefined` is a member of its type, for example `unknown`. A later version can add a key, for example the type that a
71
74
  * true result proves.
72
75
  */
73
76
  export interface Signature {
@@ -209,9 +212,13 @@ export function formulaKinds(): Readonly<Record<string, WorldEntry>> {
209
212
  return kinds as Readonly<Record<string, WorldEntry>>;
210
213
  }
211
214
 
212
- /** This function gives the type of a value of a profile: the value itself as `one`. `undefined` and a function give `unknown`. */
215
+ /**
216
+ * This function gives the type of a value of a profile: the value itself as `one`. `undefined` gives the base type
217
+ * `undefined`, because JSON and thus `one` cannot hold it (D48). A function gives `unknown`.
218
+ */
213
219
  function valueType(value: unknown): ValueType {
214
- return value === undefined || typeof value === "function" ? "unknown" : { one: [value as Json] };
220
+ if (value === undefined) return "undefined";
221
+ return typeof value === "function" ? "unknown" : { one: [value as Json] };
215
222
  }
216
223
 
217
224
  /** This function tells if a value has the form of a signature: a record with `params` as a list, and `to`. */
@@ -236,14 +243,14 @@ function listText(names: readonly string[]): string {
236
243
  }
237
244
 
238
245
  /**
239
- * This function tells if a signature takes a count of arguments. A parameter of the type `unknown` at the end is
240
- * optional, because a missing argument is `undefined`.
246
+ * This function tells if a signature takes a count of arguments. A parameter at the end is optional when its type
247
+ * holds `undefined`, because a missing argument is `undefined`. `unknown` and the base type `undefined` hold it.
241
248
  *
242
249
  * @returns `undefined` when the number is correct, or else the text of the numbers that the signature takes.
243
250
  */
244
251
  function arityOf(sig: Signature, count: number, domains: Machine["domains"]): string | undefined {
245
252
  let least = sig.params.length;
246
- while (least > 0 && isUnknown(sig.params[least - 1], domains)) least -= 1;
253
+ while (least > 0 && member(undefined, sig.params[least - 1] as ValueType, domains)) least -= 1;
247
254
  const most = sig.rest === undefined ? sig.params.length : Number.POSITIVE_INFINITY;
248
255
  if (count >= least && count <= most) return undefined;
249
256
  if (most === Number.POSITIVE_INFINITY) return `at least ${argumentsText(least)}`;
@@ -487,6 +494,8 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
487
494
  const name = nameOf(input);
488
495
  const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name);
489
496
  if (own) return "unknown";
497
+ // The standard extensions give the values that JSON cannot write, thus each has a base type (D48).
498
+ if (name === "undefined") return "undefined";
490
499
  if (name === "number") return "number";
491
500
  return name === "domain" ? domainTypeOf(input.doc.value, input.machine.domains) : "unknown";
492
501
  },
@@ -534,6 +543,7 @@ const PROFILE_KEYS = /* @__PURE__ */ Object.freeze([
534
543
  "check",
535
544
  "combine",
536
545
  "pure",
546
+ "pureExt",
537
547
  "volatile",
538
548
  "sigs",
539
549
  ] as const satisfies readonly (keyof FormulaProfile)[]);
@@ -979,10 +989,13 @@ function makeKnown(profile: FormulaProfile): (path: readonly Key[], evaluator: s
979
989
  };
980
990
  }
981
991
 
982
- /** The facts of an extension of the profile. It gets the context, thus it can read outside `resolve`. */
992
+ /**
993
+ * The facts of an extension of the profile that `pureExt` does not name. It gets the context, thus it can read outside
994
+ * `resolve`.
995
+ */
983
996
  const NOT_PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: false });
984
997
 
985
- /** The facts of a standard extension. */
998
+ /** The facts of a standard extension, and of an extension that `pureExt` names. */
986
999
  const PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: true });
987
1000
 
988
1001
  /** The lazy rule of a lazy operation: it covers each argument. */
@@ -994,7 +1007,8 @@ const EACH_PART: readonly LazyRule[] = /* @__PURE__ */ Object.freeze([/* @__PURE
994
1007
  *
995
1008
  * - An operation of `lazy` has a lazy rule for each argument.
996
1009
  * - An operation is pure when the profile names it in `pure`, or when `pure` is `true`. A volatile operation is not pure.
997
- * - A standard extension is pure. An extension of the profile gets the context, thus it is not pure.
1010
+ * - A standard extension is pure. An extension of the profile is pure only when the profile names it in `pureExt` (D47).
1011
+ * Such an extension keeps C4: it reads the context only through `resolve`.
998
1012
  *
999
1013
  * Equal profiles give the same hook (D46).
1000
1014
  *
@@ -1009,10 +1023,14 @@ export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[
1009
1023
  function makeFacts(profile: FormulaProfile, known: (path: readonly Key[], evaluator: string) => true | string): (path: readonly Key[], code: string) => KindFacts | undefined {
1010
1024
  const volatile = new Set(profile.volatile ?? []);
1011
1025
  const pure = profile.pure === true ? undefined : new Set(profile.pure ?? []);
1026
+ const pureExt = new Set(profile.pureExt ?? []);
1012
1027
  return (path, code) => {
1013
1028
  if (code !== "formula.op" && code !== "formula.ext") return undefined;
1014
1029
  const name = path.slice(1).join(".");
1015
- if (code === "formula.ext") return profile.ext !== undefined && Object.hasOwn(profile.ext, name) ? NOT_PURE : Object.hasOwn(STD_EXT, name) ? PURE : undefined;
1030
+ if (code === "formula.ext") {
1031
+ if (profile.ext !== undefined && Object.hasOwn(profile.ext, name)) return pureExt.has(name) ? PURE : NOT_PURE;
1032
+ return Object.hasOwn(STD_EXT, name) ? PURE : undefined;
1033
+ }
1016
1034
  // An operation that the language does not have gives #NAME?. The kit gives it no facts.
1017
1035
  if (known(path, code) !== true) return undefined;
1018
1036
  const isVolatile = volatile.has(name);
package/src/index.ts CHANGED
@@ -49,8 +49,10 @@ export {
49
49
  type Shape as PartShape,
50
50
  type Side,
51
51
  type TreeContract,
52
+ type VerbEffects,
52
53
  type World,
53
54
  type WorldEntry,
55
+ effectsOfVerb,
54
56
  entriesAsJson,
55
57
  equationType,
56
58
  isAlias,
package/src/providers.ts CHANGED
@@ -235,6 +235,7 @@ function samplesAt(values: readonly unknown[], closed: boolean, depth: number):
235
235
  let lists = false;
236
236
  for (const value of values) {
237
237
  if (value === null) bases.add("null");
238
+ else if (value === undefined) bases.add("undefined");
238
239
  else if (typeof value === "boolean") bases.add("bool");
239
240
  else if (typeof value === "number") bases.add("number");
240
241
  else if (typeof value === "string") bases.add("text");
@@ -243,10 +244,10 @@ function samplesAt(values: readonly unknown[], closed: boolean, depth: number):
243
244
  // A hole of a list holds undefined, thus a loop by index reads it too.
244
245
  for (let index = 0; index < value.length; index += 1) items.push(value[index]);
245
246
  } else if (isRecord(value)) records.push(value);
246
- // A value without a type of the kernel, for example a function or undefined, has each type.
247
+ // A value without a type of the kernel, for example a function, has each type.
247
248
  else return "unknown";
248
249
  }
249
- for (const base of ["null", "bool", "number", "text"]) if (bases.has(base)) types.push(base);
250
+ for (const base of ["null", "undefined", "bool", "number", "text"]) if (bases.has(base)) types.push(base);
250
251
  if (lists) types.push({ list: samplesAt(items, closed, depth + 1) });
251
252
  if (records.length > 0) {
252
253
  // All records join into one record. A field that a record does not have is optional.
@@ -268,7 +269,7 @@ function samplesAt(values: readonly unknown[], closed: boolean, depth: number):
268
269
  * This function infers a value type from sample values (TYPE.SAMPLES), as a type provider of F# does. A host uses it
269
270
  * for the type of a root, for example from some events.
270
271
  *
271
- * - `null`, a truth value, a number and a text give their base types, not their values.
272
+ * - `null`, `undefined`, a truth value, a number and a text give their base types, not their values.
272
273
  * - The items of each list join into one type of items.
273
274
  * - All records join into one record with each key of the samples. A key that a record does not have is optional.
274
275
  * - Values of different forms give a union, as `join` gives. No samples give `never`.
package/src/static.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Envelope, type Json, type Key, isKey, isRecord, typeName } from "./doc.ts";
2
- import { type Kind, type Namespace, type World, type WorldEntry, equationType, isKind, isNamespace } from "./world.ts";
2
+ import { type Kind, type Namespace, type World, type WorldEntry, effectsOfVerb, equationType, isKind, isNamespace } from "./world.ts";
3
3
  import { ask } from "./ladder.ts";
4
4
  import { type PartSlot, partsOf, scopesOf } from "./parts.ts";
5
5
  import { type Cap, type Context, type Root, ROOTS, context, isRef, walkerOf, worldThing } from "./deref.ts";
@@ -364,8 +364,11 @@ export interface CheckOptions {
364
364
  readonly caps?: readonly Cap[];
365
365
  /**
366
366
  * The verb that goes up the ladder at each document, as in `prepare`. The default is `eval`. `#NOEVAL` and the hook
367
- * `known` follow this verb. The contract declares the effects and the value types of `eval` only, thus only `eval`
368
- * checks them (VERB.CHECK). The type rule of an evaluator serves each verb.
367
+ * `known` follow this verb. The type rule of an evaluator serves each verb.
368
+ *
369
+ * The check tests the effects that the contract declares for this verb (VERB.EFFECTS). A kind without a declaration
370
+ * for the verb gives no effect issue. The contract declares the value types of `eval` only, thus only `eval` checks
371
+ * them (VERB.CHECK).
369
372
  */
370
373
  readonly verb?: string;
371
374
  /**
@@ -408,16 +411,28 @@ const EACH: readonly Cap[] = /* @__PURE__ */ Object.freeze([[]]);
408
411
  const NO_EFFECTS: readonly (readonly Key[])[] = /* @__PURE__ */ Object.freeze([]);
409
412
 
410
413
  /**
411
- * This function gives the effects that the contract of the kind of a visit declares, as keys (FACTS.EFFECTS). The
412
- * text `"type"` gives the type of the document after each alias. A visit without a kind declares no effect. `check`,
413
- * `factsOf` and `effectsOf` read the declarations with this one function (D27).
414
+ * This function gives the effects that the contract of the kind of a visit declares for a verb, as keys
415
+ * (FACTS.EFFECTS, VERB.EFFECTS). The text `"type"` gives the type of the document after each alias. A visit without a
416
+ * kind declares no effect. `check`, `factsOf` and `effectsOf` read the declarations with this one function (D27).
414
417
  *
415
418
  * @param v - The visit.
419
+ * @param verb - The verb. The default is `eval`. A kind without a declaration for the verb gives no effect.
416
420
  * @returns The keys of each declared effect, in the order of the contract.
417
421
  */
418
- export function declaredEffects(v: DocVisit): readonly (readonly Key[])[] {
419
- const effects = v.kind?.effects;
420
- if (effects === undefined || effects.length === 0) return NO_EFFECTS;
422
+ export function declaredEffects(v: DocVisit, verb = "eval"): readonly (readonly Key[])[] {
423
+ return verbDeclared(v, verb) ?? NO_EFFECTS;
424
+ }
425
+
426
+ /**
427
+ * This function gives the effects that the contract of the kind of a visit declares for a verb, as keys. The result
428
+ * is `undefined` when the contract has no declaration for the verb (D49). A visit without a kind declares no effect
429
+ * for `eval` and no declaration for another verb.
430
+ */
431
+ function verbDeclared(v: DocVisit, verb: string): readonly (readonly Key[])[] | undefined {
432
+ if (v.kind === undefined) return verb === "eval" ? NO_EFFECTS : undefined;
433
+ const effects = effectsOfVerb(v.kind, verb);
434
+ if (effects === undefined) return undefined;
435
+ if (effects.length === 0) return NO_EFFECTS;
421
436
  return effects === "type" ? [v.typePath ?? []] : effects.map((kind) => kind.split("."));
422
437
  }
423
438
 
@@ -472,7 +487,9 @@ function capReason(v: DocVisit, kind: readonly Key[], allowed: (caps: readonly C
472
487
  * A run does not always fail there, thus a type issue is a static finding, not an error of a run.
473
488
  *
474
489
  * The check asks the ladder for the verb of the options, `eval` by default (VERB.CHECK). The shapes, the paths and the
475
- * names follow the contracts, thus they are the same for each verb. Only `eval` checks the declared effects.
490
+ * names follow the contracts, thus they are the same for each verb. The check tests the effects that each contract
491
+ * declares for the verb: `effects` for `eval`, and the entry of `verbEffects` for another verb (VERB.EFFECTS). A kind
492
+ * without a declaration for the verb gives no `#CAP!` and no `#CALC!` for an effect.
476
493
  *
477
494
  * @param machine - The world and the code of the evaluators.
478
495
  * @param doc - The document, as JSON.
@@ -651,9 +668,8 @@ function checkAll(machine: Machine, doc: unknown, options: CheckOptions, nodes:
651
668
  issues.push(issue);
652
669
  if (current !== undefined) current.marked = true;
653
670
  };
654
- // The effects of a contract are the effects of eval (VERB.CONTRACT). An effect of another verb meets its test when
655
- // it happens (RUN.CAP).
656
- const effects = verb === "eval";
671
+ // The field effects declares eval, and verbEffects declares the other verbs (VERB.EFFECTS). An effect of a verb
672
+ // without a declaration meets its test when it happens (RUN.CAP).
657
673
  const allowed = options.caps === undefined ? undefined : permitsOf(machine);
658
674
  // In a run, a part that can wait needs a parent that can wait (RUN.CAN-WAIT). The map tells for each document with
659
675
  // code if its code passes a wait on: false for an evaluator, true for a runner whose waits reach the driver of the
@@ -704,7 +720,8 @@ function checkAll(machine: Machine, doc: unknown, options: CheckOptions, nodes:
704
720
  if (known !== true) push({ code: "#NAME?", place: v.place, reason: known, at: v.at });
705
721
  }
706
722
  const caps = v.caps;
707
- const declared = effects ? declaredEffects(v) : NO_EFFECTS;
723
+ const stated = verbDeclared(v, verb);
724
+ const declared = stated ?? NO_EFFECTS;
708
725
  if (allowed !== undefined && caps !== undefined) {
709
726
  for (const kind of declared) {
710
727
  if (!allowed(caps, kind)) push({ code: "#CAP!", place: v.place, reason: capReason(v, kind, allowed), at: v.at });
@@ -718,16 +735,17 @@ function checkAll(machine: Machine, doc: unknown, options: CheckOptions, nodes:
718
735
  // The run drives a runner under an evaluator, and only a real wait fails (RUN.DRIVE-NESTED). Its issue goes in
719
736
  // here, in the order of the document, when an effect of its runners can wait.
720
737
  const issue: Issue = { code: "#CALC!", place: v.place, reason: waitedReason(typeName(v.doc.type)), at: v.at };
721
- chain = { issue, index: issues.length, node: current, wait: !effects };
738
+ chain = { issue, index: issues.length, node: current, wait: false };
722
739
  drives.push(chain);
723
740
  } else if (runner) {
724
741
  chain = up ?? true;
725
742
  }
726
743
  waits.set(v, chain);
727
- // A runner waits only for an effect that reaches the host (C6). A refused effect gives #CAP! without a wait.
744
+ // A runner waits only for an effect that reaches the host (C6). A refused effect gives #CAP! without a wait. A
745
+ // runner without a declaration for the verb can cause each effect, thus it can wait.
728
746
  if (typeof chain === "object" && !chain.wait) {
729
747
  const rule = allowed ?? permits;
730
- chain.wait = declared.some((kind) => caps === undefined || rule(caps, kind));
748
+ chain.wait = stated === undefined || declared.some((kind) => caps === undefined || rule(caps, kind));
731
749
  }
732
750
  // In a run, the host does each effect, thus an evaluator cannot cause one.
733
751
  const first = declared[0];
package/src/types.ts CHANGED
@@ -9,8 +9,8 @@ import { type Json, type Key, docEquals, isEnvelope, isRecord } from "./doc.ts";
9
9
  /**
10
10
  * A value type: a set of values that a result can have, as a JSON term.
11
11
  *
12
- * - A text names a base type: `unknown`, `never`, a domain of the kernel (`null`, `bool`, `number`, `text`), or a
13
- * domain of a language.
12
+ * - A text names a base type: `unknown`, `never`, a domain of the kernel (`null`, `undefined`, `bool`, `number`,
13
+ * `text`), or a domain of a language.
14
14
  * - `list`: a list whose items have the type.
15
15
  * - `record`: a plain record with the named fields. `optional` names the fields that can be absent. A `closed`
16
16
  * record has no other fields. The other fields of an open record can hold each value.
@@ -95,6 +95,8 @@ export interface TypeIssue {
95
95
  */
96
96
  export const STD_DOMAINS: Readonly<Record<string, Domain>> = {
97
97
  null: { is: (value) => value === null, summary: "The value null." },
98
+ // JSON has no undefined, thus a document holds it as ext.undefined (D48). The domain has no codec: the codec of values writes undefined first.
99
+ undefined: { is: (value) => value === undefined, summary: "The value undefined. A document holds it as the extension ext.undefined." },
98
100
  bool: { is: (value) => typeof value === "boolean", summary: "A truth value: true or false." },
99
101
  number: { is: (value) => typeof value === "number", summary: "A number of the host, also NaN, an infinity and -0." },
100
102
  text: { is: (value) => typeof value === "string", summary: "A text." },
package/src/world.ts CHANGED
@@ -172,7 +172,8 @@ export function equationType(kind: Kind): string | undefined {
172
172
  /**
173
173
  * A kind gives the name of an evaluator for each verb, and the contract of its value. The contract serves each verb.
174
174
  * For each verb, the kernel splits the parts and narrows their capabilities by it, and `check` reads its scope rules.
175
- * `side` and `effects` tell what the verb `eval` does (VERB.CONTRACT).
175
+ * `side` and `effects` tell what the verb `eval` does (VERB.CONTRACT). `verbEffects` tells the effects of the other
176
+ * verbs (VERB.EFFECTS).
176
177
  */
177
178
  export interface Kind extends KindFacts {
178
179
  /**
@@ -200,6 +201,13 @@ export interface Kind extends KindFacts {
200
201
  * (CAP.LATENT).
201
202
  */
202
203
  readonly effects?: "type" | readonly string[];
204
+ /**
205
+ * The effects that the other verbs of a document of this kind can cause, by verb, in the form of `effects`
206
+ * (VERB.EFFECTS, D49). An entry `view: []` declares no effect for `view`. A verb without an entry has no
207
+ * declaration, thus `check` does not test its effects. The field `effects` declares `eval`, thus this field has no
208
+ * entry `eval`.
209
+ */
210
+ readonly verbEffects?: VerbEffects;
203
211
  /**
204
212
  * The value type of the result of the verb `eval`, when it is fixed (TYPE.SYNTH). A check of types gives it to each
205
213
  * document of the kind, in the place of the type that the rule of the evaluator synthesizes.
@@ -214,6 +222,28 @@ export interface Kind extends KindFacts {
214
222
  readonly summary?: string;
215
223
  }
216
224
 
225
+ /**
226
+ * The effects that a contract declares for the verbs other than `eval`, by verb (D49). Each entry has the form of the
227
+ * field `effects` of a kind. The type has no entry `eval`, because the field `effects` declares `eval`.
228
+ */
229
+ export type VerbEffects = Readonly<Record<string, "type" | readonly string[]>> & { readonly eval?: never };
230
+
231
+ /**
232
+ * This function gives the effects that the contract of a kind declares for a verb (VERB.EFFECTS, D49). For `eval`,
233
+ * the field `effects` declares them, and a kind without the field declares no effect. For another verb, the entry of
234
+ * `verbEffects` declares them. `check` and `conforms` read the declarations with this one function.
235
+ *
236
+ * @param kind - The kind.
237
+ * @param verb - The verb.
238
+ * @returns The declaration in the form of `effects`, or `undefined` when the contract does not declare the effects of
239
+ * the verb.
240
+ */
241
+ export function effectsOfVerb(kind: Kind, verb: string): "type" | readonly string[] | undefined {
242
+ if (verb === "eval") return kind.effects ?? [];
243
+ const table = kind.verbEffects as Readonly<Record<string, "type" | readonly string[]>> | undefined;
244
+ return table !== undefined && Object.hasOwn(table, verb) ? table[verb] : undefined;
245
+ }
246
+
217
247
  /** A namespace: a group of entries under one key of the world. `fallback` is the kind of each other key in it. */
218
248
  export interface Namespace {
219
249
  readonly kinds: Readonly<Record<string, WorldEntry>>;