@mark1russell7/coeval 0.9.0 → 0.11.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.11.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/evaluate.ts CHANGED
@@ -785,17 +785,13 @@ export interface StopSignal {
785
785
  addEventListener(type: "abort", listener: () => void, options?: { readonly once?: boolean }): void;
786
786
  }
787
787
 
788
- /** The options of a run. */
789
- export interface RunOptions {
788
+ /**
789
+ * The options of `plan.run`. A plan keeps the verb and the declared type of its `prepare`, thus these options do not
790
+ * have them (PLAN.RUN-OPTIONS).
791
+ */
792
+ export interface PlanRunOptions {
790
793
  /** The host code that does each effect. Without it, an effect gives its payload, as in an evaluation. */
791
794
  readonly perform?: Perform;
792
- /** The type of a bare root value. */
793
- readonly declaredType?: string;
794
- /**
795
- * The verb of `run`, for example `format`. The default is `eval` (VERB.PREPARE). A plan keeps the verb of its
796
- * `prepare`, thus `plan.run` does not read this option.
797
- */
798
- readonly verb?: string;
799
795
  /** A signal of the host. When it aborts, the run stops with `#ABORT`. */
800
796
  readonly signal?: StopSignal;
801
797
  /**
@@ -805,6 +801,14 @@ export interface RunOptions {
805
801
  readonly reads?: Read[];
806
802
  }
807
803
 
804
+ /** The options of `run`: the options of `plan.run`, and the type of a bare root value and the verb for `prepare`. */
805
+ export interface RunOptions extends PlanRunOptions {
806
+ /** The type of a bare root value. */
807
+ readonly declaredType?: string;
808
+ /** The verb of `run`, for example `format`. The default is `eval` (VERB.PREPARE). */
809
+ readonly verb?: string;
810
+ }
811
+
808
812
  /** The options of an evaluation of a plan. */
809
813
  export interface EvalOptions {
810
814
  /** A list that gets each effect, in its order. */
@@ -1303,7 +1307,7 @@ export interface Plan {
1303
1307
  * @param options - The host code that does the effects, a signal to stop the run and a list that gets the reads.
1304
1308
  * @returns The result, when the run ends.
1305
1309
  */
1306
- run(context?: Context, options?: RunOptions): Promise<Result>;
1310
+ run(context?: Context, options?: PlanRunOptions): Promise<Result>;
1307
1311
  }
1308
1312
 
1309
1313
  interface Compiler {
@@ -1471,7 +1475,8 @@ function compileNode(c: Compiler, value: unknown, place: readonly Key[], declare
1471
1475
  } catch (error) {
1472
1476
  return failingWith(c, meta, placed(thrown(error, `the evaluator ${name}`), place));
1473
1477
  }
1474
- const done = finish(c, meta, placedCode(inner, place, name), undefined);
1478
+ // Code that cannot fail needs no place and no catch, and it does not read the context of its grant (PLAN.TOTAL).
1479
+ const done = finish(c, meta, TOTAL.has(inner) ? inner : placedCode(inner, place, name), undefined);
1475
1480
  if (frees === undefined || kind === undefined || !pureCode(c, envelope, kind, typePath, name, quick)) return done;
1476
1481
  const free = freeOf(doc, kind, split.parts, frees);
1477
1482
  if (free === undefined) return done;
@@ -1721,6 +1726,24 @@ function copyPlain(value: unknown, memo: Map<object, unknown>): unknown {
1721
1726
  * the split of the value. Thus a plan with these helpers keeps only its code.
1722
1727
  */
1723
1728
 
1729
+ /**
1730
+ * The compiled code that cannot fail (PLAN.TOTAL). Such code gives a value and throws nothing. It does not read its
1731
+ * context or its runtime. Only the kits of the kernel mark code, thus the code of a host always gets its grant.
1732
+ */
1733
+ const TOTAL: WeakSet<Code> = /* @__PURE__ */ new WeakSet();
1734
+
1735
+ /**
1736
+ * This function marks compiled code that cannot fail (PLAN.TOTAL). Then `compileNode` does not wrap the code in
1737
+ * `placedCode`, thus a literal costs one call. The kits of the kernel use it. The package does not export it.
1738
+ *
1739
+ * @param code - The code. It gives a value and throws nothing. It does not read its context or its runtime.
1740
+ * @returns The same code.
1741
+ */
1742
+ export function totalCode(code: Code): Code {
1743
+ TOTAL.add(code);
1744
+ return code;
1745
+ }
1746
+
1724
1747
  /**
1725
1748
  * This function gives the code of a document: the code of its evaluator, with the place of each error and of each
1726
1749
  * exception. The code gets the context that its grant permits, thus a compiled parent cannot widen it (CAP.NARROW-CODE).
@@ -1952,7 +1975,7 @@ class CompiledPlan implements Plan {
1952
1975
  return this.code(ctx, s);
1953
1976
  }
1954
1977
 
1955
- run(context: Context = makeContext(), runOptions: RunOptions = {}): Promise<Result> {
1978
+ run(context: Context = makeContext(), runOptions: PlanRunOptions = {}): Promise<Result> {
1956
1979
  const machine = this.machine;
1957
1980
  const ctx = rootContext(machine, context);
1958
1981
  const s = new Session(machine, true, this.traced, runOptions.perform, runOptions.signal, undefined, readingOf(runOptions.reads, ctx));
package/src/formula.ts CHANGED
@@ -2,9 +2,9 @@ import { type Json, type Key, docEquals, isKey, isRecord } from "./doc.ts";
2
2
  import type { Kind, KindFacts, LazyRule, WorldEntry } from "./world.ts";
3
3
  import { type CompiledPath, type Context, type Resolution, compilePath, valueOfThing } from "./deref.ts";
4
4
  import { type EvalError, type Failed, type Result, Failure, fail, ok, thrown } from "./errors.ts";
5
- import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled } from "./evaluate.ts";
5
+ import { type Code, type CompileInput, type CompiledEvaluator, type CompiledPart, type Evaluator, type Machine, type Runtime, resolveCompiled, totalCode } from "./evaluate.ts";
6
6
  import { type MachinePart, mergeMachines } from "./merge.ts";
7
- 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)[]);
@@ -687,8 +697,9 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
687
697
 
688
698
  return {
689
699
  "formula.lit": compiled(({ doc }) => {
700
+ // A literal cannot fail, thus its code needs no place (PLAN.TOTAL).
690
701
  const result = ok(doc.value);
691
- return () => result;
702
+ return totalCode(() => result);
692
703
  }),
693
704
 
694
705
  "formula.ref": compiled(({ doc, parts, fill, address, machine }) => {
@@ -708,7 +719,7 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
708
719
  // A step document without parts is its own value. A step document with parts gets the result of each part.
709
720
  if (parts.length === 0) {
710
721
  const same = ok(doc);
711
- return () => same;
722
+ return totalCode(() => same);
712
723
  }
713
724
  return (ctx, rt) => {
714
725
  const values = valuesOf(parts, ctx, rt);
@@ -979,10 +990,13 @@ function makeKnown(profile: FormulaProfile): (path: readonly Key[], evaluator: s
979
990
  };
980
991
  }
981
992
 
982
- /** The facts of an extension of the profile. It gets the context, thus it can read outside `resolve`. */
993
+ /**
994
+ * The facts of an extension of the profile that `pureExt` does not name. It gets the context, thus it can read outside
995
+ * `resolve`.
996
+ */
983
997
  const NOT_PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: false });
984
998
 
985
- /** The facts of a standard extension. */
999
+ /** The facts of a standard extension, and of an extension that `pureExt` names. */
986
1000
  const PURE: KindFacts = /* @__PURE__ */ Object.freeze({ pure: true });
987
1001
 
988
1002
  /** The lazy rule of a lazy operation: it covers each argument. */
@@ -994,7 +1008,8 @@ const EACH_PART: readonly LazyRule[] = /* @__PURE__ */ Object.freeze([/* @__PURE
994
1008
  *
995
1009
  * - An operation of `lazy` has a lazy rule for each argument.
996
1010
  * - 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.
1011
+ * - A standard extension is pure. An extension of the profile is pure only when the profile names it in `pureExt` (D47).
1012
+ * Such an extension keeps C4: it reads the context only through `resolve`.
998
1013
  *
999
1014
  * Equal profiles give the same hook (D46).
1000
1015
  *
@@ -1009,10 +1024,14 @@ export function formulaFacts(profile: FormulaProfile = {}): (path: readonly Key[
1009
1024
  function makeFacts(profile: FormulaProfile, known: (path: readonly Key[], evaluator: string) => true | string): (path: readonly Key[], code: string) => KindFacts | undefined {
1010
1025
  const volatile = new Set(profile.volatile ?? []);
1011
1026
  const pure = profile.pure === true ? undefined : new Set(profile.pure ?? []);
1027
+ const pureExt = new Set(profile.pureExt ?? []);
1012
1028
  return (path, code) => {
1013
1029
  if (code !== "formula.op" && code !== "formula.ext") return undefined;
1014
1030
  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;
1031
+ if (code === "formula.ext") {
1032
+ if (profile.ext !== undefined && Object.hasOwn(profile.ext, name)) return pureExt.has(name) ? PURE : NOT_PURE;
1033
+ return Object.hasOwn(STD_EXT, name) ? PURE : undefined;
1034
+ }
1016
1035
  // An operation that the language does not have gives #NAME?. The kit gives it no facts.
1017
1036
  if (known(path, code) !== true) return undefined;
1018
1037
  const isVolatile = volatile.has(name);
package/src/generate.ts CHANGED
@@ -146,9 +146,11 @@ export function generate(world: World, options: GenerateOptions): unknown {
146
146
  const keys = options.keys ?? KEYS;
147
147
  const steps = options.steps ?? 2;
148
148
  const roots: readonly Root[] = options.roots ?? ROOTS;
149
+ // A kind with the verb step walks a step document. With a default of step, each other kind walks one too (GEN.STEPS).
150
+ const defaultStep = Object.hasOwn(world.defaults, "step");
149
151
  const stepTypes = types.filter((type) => {
150
152
  const found = lookupKind(world, type);
151
- return found.ok && Object.hasOwn(found.kind.verbs, "step");
153
+ return found.ok && (defaultStep || Object.hasOwn(found.kind.verbs, "step"));
152
154
  });
153
155
  // A name under each namespace with a fallback that no evaluator knows, for example ["ops", "nope"].
154
156
  const unknownNames: readonly (readonly Key[])[] = Object.keys(world.kinds)
@@ -205,7 +207,10 @@ export function generate(world: World, options: GenerateOptions): unknown {
205
207
  return out;
206
208
  };
207
209
 
208
- /** A path from a root (D10). It uses the names in scope, and sometimes a step document. */
210
+ /**
211
+ * A path from a root (D10). It uses the names in scope, and sometimes a step document. The kind of a step document
212
+ * has the verb `step`, or the default of the world for `step` serves it.
213
+ */
209
214
  const pathFor = (pos: Position, refKind: Kind | undefined): Json => {
210
215
  if (rng.chance(malformed)) return rng.pick<Json>(["names", [], { names: "a" }, ["nowhere", "a"]]);
211
216
  const others = roots.filter((name) => name !== "names");
package/src/index.ts CHANGED
@@ -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,
@@ -132,6 +134,7 @@ export {
132
134
  type Part,
133
135
  type Perform,
134
136
  type Plan,
137
+ type PlanRunOptions,
135
138
  type PrepareOptions,
136
139
  type Read,
137
140
  type Run,
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>>;