@mark1russell7/coeval 0.7.0 → 0.7.1

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.7.0",
4
+ "version": "0.7.1",
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/codec.ts ADDED
@@ -0,0 +1,185 @@
1
+ import { type Json, isEnvelope, isRecord } from "./doc.ts";
2
+ import { type Result, describe, fail, ok } from "./errors.ts";
3
+ import { type Machine, makeContext, prepare } from "./evaluate.ts";
4
+ import { type Domain, type DomainCodec, domainOf, member } from "./types.ts";
5
+
6
+ /*
7
+ * The codec of the values that JSON cannot write (K140, D40). A document holds such a value as a standard extension:
8
+ * `ext.number` for `NaN`, an infinity and `-0`, `ext.undefined` for `undefined`, and `ext.domain` for a member of a
9
+ * domain with a codec. Thus the same value has the same document in each language (D12).
10
+ */
11
+
12
+ /** The domains of a machine, by their names. */
13
+ type Domains = Readonly<Record<string, Domain>> | undefined;
14
+
15
+ /** This function gives the codec of a domain, or `undefined` when the domain has no codec. */
16
+ function codecOf(domain: Domain): DomainCodec | undefined {
17
+ const codec: unknown = domain.codec;
18
+ if (typeof codec !== "object" || codec === null) return undefined;
19
+ const { encode, decode } = codec as Partial<DomainCodec>;
20
+ return typeof encode === "function" && typeof decode === "function" ? (codec as DomainCodec) : undefined;
21
+ }
22
+
23
+ /**
24
+ * This function reads the value of an extension `ext.domain` (EXT.DOMAIN). The value is a record with only the text
25
+ * `domain` and the JSON `data`. The codec of the named domain reads the data, and the result must be a member of the
26
+ * domain.
27
+ *
28
+ * @param data - The value of the extension document.
29
+ * @param domains - The domains of the machine.
30
+ * @returns The member, `#NAME?` for a domain without a codec, or `#VALUE!` for data that the codec cannot read.
31
+ */
32
+ export function decodeDomain(data: Json, domains: Domains): Result {
33
+ if (!isRecord(data) || typeof data["domain"] !== "string" || !Object.hasOwn(data, "data") || Object.keys(data).length !== 2) {
34
+ return fail("#VALUE!", "The value of ext.domain is a record with only the text domain and the data.");
35
+ }
36
+ const name = data["domain"];
37
+ const domain = domainOf(name, domains);
38
+ if (domain === undefined) return fail("#NAME?", `The machine has no domain ${name}.`);
39
+ const codec = codecOf(domain);
40
+ if (codec === undefined) return fail("#NAME?", `The domain ${name} has no codec.`);
41
+ let value: unknown;
42
+ try {
43
+ value = codec.decode(data["data"] as Json);
44
+ } catch (error) {
45
+ return fail("#VALUE!", `The codec of the domain ${name} cannot read the data: ${describe(error)}`, { cause: error });
46
+ }
47
+ if (!member(value, name, domains)) return fail("#VALUE!", `The codec of the domain ${name} gives a value that is not a member of the domain.`);
48
+ return ok(value);
49
+ }
50
+
51
+ /**
52
+ * This function gives the static type of an extension `ext.domain`: the name of its domain when that domain has a
53
+ * codec. A run then gives a member of the domain or an error, because `decodeDomain` tests the membership.
54
+ *
55
+ * @param data - The value of the extension document.
56
+ * @param domains - The domains of the machine.
57
+ * @returns The name of the domain, or `unknown`.
58
+ */
59
+ export function domainTypeOf(data: unknown, domains: Domains): string {
60
+ if (!isRecord(data) || typeof data["domain"] !== "string") return "unknown";
61
+ const domain = domainOf(data["domain"], domains);
62
+ return domain !== undefined && codecOf(domain) !== undefined ? data["domain"] : "unknown";
63
+ }
64
+
65
+ /** This function sets an own data property. Only the key `__proto__` needs `defineProperty`. */
66
+ function setOwn(target: Record<string, Json>, key: string, value: Json): void {
67
+ if (key === "__proto__") Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
68
+ else target[key] = value;
69
+ }
70
+
71
+ /** The state of one call of `encodeValue`. */
72
+ interface Encoder {
73
+ readonly domains: Domains;
74
+ /** The domains with a codec, in the order of their names. */
75
+ readonly codecs: readonly (readonly [string, DomainCodec])[];
76
+ /** The answer of `isJson` for each container that it examined. */
77
+ readonly json: WeakMap<object, boolean>;
78
+ /** The containers on the way from the root to the value. A container on the way again is a cycle. */
79
+ readonly open: Set<object>;
80
+ }
81
+
82
+ /**
83
+ * This function tells if JSON writes a value exactly. Such a value is a finite number other than `-0`, a text, a truth
84
+ * value, `null`, or a container of such values.
85
+ */
86
+ function isJson(value: unknown, encoder: Encoder): boolean {
87
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
88
+ if (typeof value === "number") return Number.isFinite(value) && !Object.is(value, -0);
89
+ const list = Array.isArray(value);
90
+ if (!list && !isRecord(value)) return false;
91
+ const container = value as object;
92
+ const known = encoder.json.get(container);
93
+ if (known !== undefined) return known;
94
+ // A cycle is not JSON. The mark stays false until each item is JSON.
95
+ encoder.json.set(container, false);
96
+ let all = true;
97
+ if (list) {
98
+ const items = value as readonly unknown[];
99
+ for (let index = 0; index < items.length && all; index += 1) all = isJson(items[index], encoder);
100
+ } else {
101
+ const record = value as Readonly<Record<string, unknown>>;
102
+ for (const key of Object.keys(record)) {
103
+ if (!isJson(record[key], encoder)) {
104
+ all = false;
105
+ break;
106
+ }
107
+ }
108
+ }
109
+ encoder.json.set(container, all);
110
+ return all;
111
+ }
112
+
113
+ /** This function gives the text of a number that JSON cannot write, for `ext.number`. */
114
+ function numberText(value: number): string {
115
+ if (Number.isNaN(value)) return "NaN";
116
+ if (value === Number.POSITIVE_INFINITY) return "Infinity";
117
+ if (value === Number.NEGATIVE_INFINITY) return "-Infinity";
118
+ return "-0";
119
+ }
120
+
121
+ /** This function gives the term document of a value in one call of `encodeValue`. */
122
+ function encodeTerm(value: unknown, encoder: Encoder): Json {
123
+ if (isJson(value, encoder)) return isEnvelope(value) ? { type: "lit", value: value as Json } : (value as Json);
124
+ if (value === undefined) return { type: ["ext", "undefined"], value: null };
125
+ if (typeof value === "number") return { type: ["ext", "number"], value: numberText(value) };
126
+ for (const [name, codec] of encoder.codecs) {
127
+ if (member(value, name, encoder.domains)) return { type: ["ext", "domain"], value: { domain: name, data: codec.encode(value) } };
128
+ }
129
+ if (!Array.isArray(value) && !isRecord(value)) {
130
+ throw new TypeError(`encodeValue cannot write the value ${describe(value)}: no domain of the machine with a codec holds it.`);
131
+ }
132
+ if (encoder.open.has(value)) throw new TypeError("encodeValue cannot write a value with a cycle.");
133
+ encoder.open.add(value);
134
+ let out: Json;
135
+ if (Array.isArray(value)) {
136
+ // `Array.from` visits each hole of a sparse list as `undefined`.
137
+ out = { type: "list", value: Array.from(value as readonly unknown[], (item) => encodeTerm(item, encoder)) };
138
+ } else {
139
+ const fields: Record<string, Json> = {};
140
+ const record = value as Readonly<Record<string, unknown>>;
141
+ for (const key of Object.keys(record)) setOwn(fields, key, encodeTerm(record[key], encoder));
142
+ out = { type: "record", value: fields };
143
+ }
144
+ encoder.open.delete(value);
145
+ return out;
146
+ }
147
+
148
+ /**
149
+ * This function writes a host value as a term document of the formula kit (EXT.ENCODE, D40). Thus the same value has
150
+ * the same document in each language (D12).
151
+ *
152
+ * - A value that JSON writes exactly stays as it is. A literal with the shape of an envelope goes into `lit`.
153
+ * - `NaN`, an infinity and `-0` become `ext.number`, and `undefined` becomes `ext.undefined`.
154
+ * - A member of a domain with a codec becomes `ext.domain`. The domains go in the order of their names.
155
+ * - A list or a plain record with such a value becomes the construct `list` or `record`.
156
+ *
157
+ * @param machine - The machine, or only its domains.
158
+ * @param value - The host value.
159
+ * @returns The document. `decodeValue` gives the value again (EXT.ROUNDTRIP).
160
+ * @throws {TypeError} The value has a cycle, or it is not JSON and no domain with a codec holds it.
161
+ */
162
+ export function encodeValue(machine: Pick<Machine, "domains">, value: unknown): Json {
163
+ const domains = machine.domains;
164
+ const codecs: (readonly [string, DomainCodec])[] = [];
165
+ if (domains !== undefined) {
166
+ for (const name of Object.keys(domains).sort()) {
167
+ const domain = domainOf(name, domains);
168
+ const codec = domain === undefined ? undefined : codecOf(domain);
169
+ if (codec !== undefined) codecs.push([name, codec]);
170
+ }
171
+ }
172
+ return encodeTerm(value, { domains, codecs, json: new WeakMap(), open: new Set() });
173
+ }
174
+
175
+ /**
176
+ * This function reads a document of `encodeValue` again: it evaluates the document as a term, in an empty context.
177
+ * A bare root value is a literal.
178
+ *
179
+ * @param machine - A machine of the formula kit, with the domains of the document.
180
+ * @param doc - The document.
181
+ * @returns The value, or the error of the evaluation.
182
+ */
183
+ export function decodeValue(machine: Machine, doc: unknown): Result {
184
+ return prepare(machine, doc, { declaredType: "lit" }).evaluate(makeContext());
185
+ }
package/src/conform.ts CHANGED
@@ -692,7 +692,7 @@ function finish(shot: Shot, start: () => Result, who: string): Result {
692
692
  */
693
693
  function instanceOf(env: Env, under: Under, doc: Envelope, split: SplitOk, sample: Sample): Instance {
694
694
  const fill = (items: readonly unknown[]): unknown => (split.parts.length === 0 ? doc.value : split.fill(items));
695
- const base = { kind: sample.type, typePath: sample.typePath, doc, data: split.data, place: PLACE, fill };
695
+ const base = { kind: sample.type, typePath: sample.typePath, doc, data: split.data, place: PLACE, machine: env.machine, fill };
696
696
  const who = `the ${env.runner ? "runner" : "evaluator"} ${env.name}`;
697
697
  if (under.form === "eval" || under.form === "run") {
698
698
  if (under.form === "eval") {
package/src/evaluate.ts CHANGED
@@ -523,6 +523,8 @@ export interface EvalInput {
523
523
  readonly context: Context;
524
524
  /** The keys from the root document to this document. */
525
525
  readonly place: readonly Key[];
526
+ /** The machine of the evaluation, for example for its domains (D40). The kernel sets it. */
527
+ readonly machine?: Machine;
526
528
  /** `true` when the position of the document declares an address. */
527
529
  readonly address?: boolean;
528
530
  /**
@@ -580,6 +582,8 @@ export interface RunInput {
580
582
  readonly parts: readonly RunPart[];
581
583
  readonly context: Context;
582
584
  readonly place: readonly Key[];
585
+ /** The machine of the evaluation, for example for its domains (D40). The kernel sets it. */
586
+ readonly machine?: Machine;
583
587
  resolve(path: readonly unknown[], options?: ResolveOptions): Resolution;
584
588
  /**
585
589
  * This function causes an effect. In a run, the host does the effect, and the steps wait when the host waits. In
@@ -1534,7 +1538,7 @@ function fillOf(split: Extract<Split, { readonly ok: true }>, doc: Envelope): <P
1534
1538
 
1535
1539
 
1536
1540
  function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): Code {
1537
- const { kind, typePath, doc, data, place } = input;
1541
+ const { kind, typePath, doc, data, place, machine } = input;
1538
1542
  const address = input.address === true ? { address: true } : {};
1539
1543
  // The code keeps the fill and not the whole input, thus it does not keep the compiled value of the document (PLAN.LEAN).
1540
1544
  const refill = input.fill;
@@ -1556,6 +1560,7 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
1556
1560
  parts,
1557
1561
  context: ctx,
1558
1562
  place,
1563
+ machine,
1559
1564
  ...address,
1560
1565
  resolve: (path, options) => rt.resolve(ctx, path, options),
1561
1566
  effect: (effectKind, payload) => rt.effect(ctx, effectKind, payload, place),
@@ -1565,7 +1570,7 @@ function interpret(fn: EvalFn, input: CompileInput, fill: <P>(handles: readonly
1565
1570
  }
1566
1571
 
1567
1572
  function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonly P[]) => unknown): RunCode {
1568
- const { kind, typePath, doc, data, place } = input;
1573
+ const { kind, typePath, doc, data, place, machine } = input;
1569
1574
  const refill = input.fill;
1570
1575
  const compiled = input.parts;
1571
1576
  const hasParts = compiled.length > 0;
@@ -1585,6 +1590,7 @@ function interpretRun(fn: RunFn, input: CompileInput, fill: <P>(handles: readonl
1585
1590
  parts,
1586
1591
  context: ctx,
1587
1592
  place,
1593
+ machine,
1588
1594
  resolve: (path, options) => rt.resolve(ctx, path, options),
1589
1595
  effect: (effectKind, payload) => rt.runEffect(ctx, effectKind, payload, place),
1590
1596
  all: (list, options) => rt.all(list.map((part) => () => part.evaluate()), options),
package/src/formula.ts CHANGED
@@ -4,8 +4,9 @@ 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 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, stepType, typeText } from "./types.ts";
8
8
  import type { TypeInput, TypeRule } from "./typing.ts";
9
+ import { decodeDomain, domainTypeOf } from "./codec.ts";
9
10
 
10
11
  /** An operation of a formula language: a function of the values of its arguments. */
11
12
  export type OpFn = (...args: never[]) => unknown;
@@ -16,15 +17,19 @@ export type OpFn = (...args: never[]) => unknown;
16
17
  */
17
18
  export type LazyOp = (args: readonly (() => Result)[]) => Result;
18
19
 
19
- /** The code of an extension: it gives the value of the data of an extension document, in the context of the document. */
20
- export type ExtFn = (data: Json, context: Context) => Result;
20
+ /**
21
+ * The code of an extension: it gives the value of the data of an extension document, in the context of the document.
22
+ * It also gets the domains of the machine, for the codecs of `ext.domain` (D40).
23
+ */
24
+ export type ExtFn = (data: Json, context: Context, domains?: Readonly<Record<string, Domain>>) => Result;
21
25
 
22
26
  /**
23
27
  * The rules of one formula language: its operations, and the rules that differ between languages.
24
28
  *
25
29
  * - `ops`: the operations of the namespace `ops`, by their name (the keys after `ops`, with dots between them).
26
30
  * - `lazy`: the operations that evaluate their arguments themselves.
27
- * - `ext`: the extensions of the namespace `ext`, by their name. The kit has `ext.undefined` and `ext.number`.
31
+ * - `ext`: the extensions of the namespace `ext`, by their name. The kit has `ext.undefined`, `ext.number` and
32
+ * `ext.domain`.
28
33
  * - `truthy`: the truth of a value for `if`, `and` and `or`. `undefined` is an error `#VALUE!`. The default
29
34
  * accepts only `true` and `false`.
30
35
  * - `otherwise`: the value of an `if` without `else`. The default is `null`. A profile with this key gives its value,
@@ -97,7 +102,10 @@ export function isClosure(value: unknown): value is Closure {
97
102
  return typeof value === "function" && (value as Partial<Closure>)[CLOSURE] !== undefined;
98
103
  }
99
104
 
100
- /** The standard extensions: the values that JSON cannot write. */
105
+ /**
106
+ * The standard extensions: the values that JSON cannot write. `ext.domain` reads a member of a domain through the
107
+ * codec of the domain (EXT.DOMAIN, D40).
108
+ */
101
109
  export const STD_EXT: Readonly<Record<string, ExtFn>> = {
102
110
  undefined: () => ok(undefined),
103
111
  number: (data) => {
@@ -107,6 +115,7 @@ export const STD_EXT: Readonly<Record<string, ExtFn>> = {
107
115
  if (data === "-0") return ok(-0);
108
116
  return fail("#VALUE!", "The value of ext.number is NaN, Infinity, -Infinity or -0.");
109
117
  },
118
+ domain: (data, _context, domains) => decodeDomain(data, domains),
110
119
  };
111
120
 
112
121
  /** The truth of a value in the rules of JavaScript: `0`, `""`, `null`, `undefined` and `NaN` are false. */
@@ -477,7 +486,9 @@ function formulaTypes(profile: FormulaProfile): Readonly<Record<string, TypeRule
477
486
  synth: (input) => {
478
487
  const name = nameOf(input);
479
488
  const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name);
480
- return name === "number" && !own ? "number" : "unknown";
489
+ if (own) return "unknown";
490
+ if (name === "number") return "number";
491
+ return name === "domain" ? domainTypeOf(input.doc.value, input.machine.domains) : "unknown";
481
492
  },
482
493
  },
483
494
  };
@@ -811,7 +822,7 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
811
822
  };
812
823
  }),
813
824
 
814
- "formula.ext": compiled(({ typePath, doc }) => {
825
+ "formula.ext": compiled(({ typePath, doc, machine }) => {
815
826
  const name = typePath.slice(1).join(".");
816
827
  const own = profile.ext !== undefined && Object.hasOwn(profile.ext, name) ? profile.ext[name] : undefined;
817
828
  const handler = own ?? (Object.hasOwn(STD_EXT, name) ? STD_EXT[name] : undefined);
@@ -821,7 +832,7 @@ function formulaCode(profile: FormulaProfile): Readonly<Record<string, CompiledE
821
832
  }
822
833
  return (ctx) => {
823
834
  try {
824
- return handler(doc.value, ctx);
835
+ return handler(doc.value, ctx, machine.domains);
825
836
  } catch (error) {
826
837
  return thrown(error, `the extension ${name}`);
827
838
  }
package/src/index.ts CHANGED
@@ -211,6 +211,24 @@ export {
211
211
  isClosure,
212
212
  jsTruthy,
213
213
  } from "./formula.ts";
214
+ export { decodeDomain, decodeValue, encodeValue } from "./codec.ts";
215
+ export {
216
+ type ParseResult,
217
+ type SyntaxAssoc,
218
+ type SyntaxCalls,
219
+ type SyntaxFixity,
220
+ type SyntaxForm,
221
+ type SyntaxIssue,
222
+ type SyntaxLiterals,
223
+ type SyntaxOperator,
224
+ type SyntaxProfile,
225
+ type SyntaxRefPattern,
226
+ type SyntaxRefs,
227
+ STD_SYNTAX,
228
+ parse,
229
+ print,
230
+ syntaxFormat,
231
+ } from "./syntax.ts";
214
232
  export { type Definitions, type ReferenceOptions, STD_DEFINITIONS, formulaDefinitions, reference } from "./reference.ts";
215
233
  export { type AgreeOptions, type Agreement, type Tier, type TierInput, type TierResult, agree, sameValue } from "./agree.ts";
216
234
  export { type ConformOptions, type ConformProbe, type Finding, type Obligation, conforms } from "./conform.ts";
@@ -230,6 +248,6 @@ export {
230
248
  export { type GraphCycle, type GraphEdge, type GraphNode, type GraphOptions, type NodeGraph, components, cycles, heights, necessary, nodeLayers } from "./graph.ts";
231
249
  export { type Recorder, type ReplayEntry, type ReplayLog, type ReplayOptions, type ReplaySettle, recorder, replayer } from "./replay.ts";
232
250
  export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
233
- export { type Domain, type TypeIssue, type ValueType, STD_DOMAINS, fits, join, member, typeIssues, typeText } from "./types.ts";
251
+ export { type Domain, type DomainCodec, type TypeIssue, type ValueType, STD_DOMAINS, fits, join, member, typeIssues, typeText } from "./types.ts";
234
252
  export { type PartType, type RootTypes, type TypeInput, type TypeProblem, type TypeRule, type Typed, CAPS_TYPE } from "./typing.ts";
235
253
  export { type SampleOptions, type TypeReading, typeFromSamples, typeFromSchema } from "./providers.ts";
package/src/reference.ts CHANGED
@@ -307,6 +307,7 @@ function evalInput(env: Env, seen: Seen, at: At): EvalInput {
307
307
  parts,
308
308
  context: at.ctx,
309
309
  place: at.place,
310
+ machine: env.machine,
310
311
  ...(at.address ? { address: true } : {}),
311
312
  resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
312
313
  effect: (kind, payload) => cause(env, at.ctx, kind, payload, at.place),
@@ -335,6 +336,7 @@ function runInput(env: Env, seen: Seen, at: At): RunInput {
335
336
  parts,
336
337
  context: at.ctx,
337
338
  place: at.place,
339
+ machine: env.machine,
338
340
  resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
339
341
  effect: function* (kind, payload) {
340
342
  return cause(env, at.ctx, kind, payload, at.place);
@@ -748,12 +750,12 @@ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
748
750
  }
749
751
  };
750
752
 
751
- const ext: EvalFn = ({ typePath, doc, context }) => {
753
+ const ext: EvalFn = ({ typePath, doc, context, machine }) => {
752
754
  const name = typePath.slice(1).join(".");
753
755
  const handler = own(profile.ext, name) ?? own(STD_EXT, name);
754
756
  if (handler === undefined) return fail("#NAME?", `The language has no extension ${name}.`);
755
757
  try {
756
- return handler(doc.value, context);
758
+ return handler(doc.value, context, machine?.domains);
757
759
  } catch (error) {
758
760
  return thrown(error, `the extension ${name}`);
759
761
  }