@mark1russell7/coeval 0.1.0 → 0.2.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/src/stdlib.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Json, type Key, field, isRecord } from "./doc.ts";
2
- import { type Context, type Evaluator, type Machine, type Part, type Result, type RunPart, type Runner, fail, ok } from "./evaluate.ts";
2
+ import { type Context, type Evaluator, type Machine, type Part, type Result, type RunPart, type Runner, type Steps, fail, ok } from "./evaluate.ts";
3
3
  import { type HostThing, type Thing, type Walker, THING, isHostThing, valueOfThing } from "./deref.ts";
4
4
  import type { World } from "./ladder.ts";
5
5
 
@@ -60,6 +60,10 @@ export const STD_WORLD: World = {
60
60
  seq: {
61
61
  verbs: { eval: "control.seq", view: "view.seq" },
62
62
  parts: { shape: "list" },
63
+ scope: [
64
+ { names: { headers: true }, in: [["*"]], sequential: true },
65
+ { names: { fixed: ["last"] }, in: [["*"]], sequential: true },
66
+ ],
63
67
  summary: "Its steps, in order. The result of a named step is a name for the later steps.",
64
68
  },
65
69
  pure: {
@@ -83,12 +87,12 @@ export const STD_WORLD: World = {
83
87
  defaults: { view: "view.card" },
84
88
  };
85
89
 
86
- function isNumber(value: Json): value is number {
90
+ function isNumber(value: unknown): value is number {
87
91
  return typeof value === "number" && Number.isFinite(value);
88
92
  }
89
93
 
90
94
  /** This function evaluates each part, collects the errors, and gives the values when no part failed. */
91
- function allValues(results: readonly Result[]): { readonly values: readonly Json[] } | { readonly error: Result } {
95
+ function allValues(results: readonly Result[]): { readonly values: readonly unknown[] } | { readonly error: Result } {
92
96
  const errors = results.filter((result) => !result.ok);
93
97
  const first = errors[0];
94
98
  if (first !== undefined && errors.length === 1) return { error: first };
@@ -96,7 +100,7 @@ function allValues(results: readonly Result[]): { readonly values: readonly Json
96
100
  return { values: results.map((result) => (result.ok ? result.value : null)) };
97
101
  }
98
102
 
99
- function numbers(values: readonly Json[], kind: string): readonly number[] | Result {
103
+ function numbers(values: readonly unknown[], kind: string): readonly number[] | Result {
100
104
  const bad = values.findIndex((value) => !isNumber(value));
101
105
  if (bad !== -1) return fail("#VALUE!", `Part ${bad} of ${kind} is not a number.`);
102
106
  return values.filter(isNumber);
@@ -139,11 +143,8 @@ export const STD_EVALUATORS: Readonly<Record<string, Evaluator>> = {
139
143
  if (!Array.isArray(data) || data.length === 0) {
140
144
  return fail("#VALUE!", "The value of ref is a path: a list that starts with a root, for example [\"names\", \"a\"].");
141
145
  }
142
- const path: readonly Json[] = data;
143
- const found = resolve(path);
144
- if (found.ok) return ok(valueOfThing(found.thing));
145
- if (path[0] === "names" && found.hops.length === 1) return fail("#NAME?", `The context has no name ${String(path[1])}.`);
146
- return fail(found.code, found.reason);
146
+ const found = resolve(data as readonly Json[]);
147
+ return found.ok ? ok(valueOfThing(found.thing)) : fail(found.code, found.reason);
147
148
  },
148
149
  "tree.node": (input) => treeNode(input.parts, input.context, input.kind),
149
150
  };
@@ -173,8 +174,8 @@ function treeNode(parts: readonly Part[], context: Context, type: string): Resul
173
174
  const result = read(String(key));
174
175
  return result?.ok === true ? result.value : undefined;
175
176
  },
176
- toJson(): Json {
177
- const out: Record<string, Json> = {};
177
+ toJson(): unknown {
178
+ const out: Record<string, unknown> = {};
178
179
  for (const part of parts) {
179
180
  const result = read(part.key);
180
181
  out[part.key] = result?.ok === true ? result.value : null;
@@ -182,7 +183,7 @@ function treeNode(parts: readonly Part[], context: Context, type: string): Resul
182
183
  return out;
183
184
  },
184
185
  };
185
- const out: Record<string, Json> = {};
186
+ const out: Record<string, unknown> = {};
186
187
  for (const part of parts) {
187
188
  const result = read(part.key);
188
189
  if (result === undefined) continue;
@@ -192,31 +193,57 @@ function treeNode(parts: readonly Part[], context: Context, type: string): Resul
192
193
  return ok(out);
193
194
  }
194
195
 
196
+ /** The options of a walker `nearest`. */
197
+ export interface NearestOptions {
198
+ /**
199
+ * This function tells if a node has the wanted type. The default compares the field `type` of a host thing.
200
+ *
201
+ * @param node - The node that the walk reached.
202
+ * @param wanted - The type that the step document asks for.
203
+ * @returns `true` when the node has the type.
204
+ */
205
+ readonly matches?: (node: Thing, wanted: Json | undefined) => boolean;
206
+ /** The most hops of one walk. The default is 256. */
207
+ readonly maxHops?: number;
208
+ }
209
+
210
+ function hostType(node: Thing, wanted: Json | undefined): boolean {
211
+ return isHostThing(node) && (node as { readonly type?: unknown }).type === wanted;
212
+ }
213
+
195
214
  /**
196
- * The walker of `nearest`: it follows a key (the default is `parent`) until the node that it reaches has the
197
- * wanted type. With `self: true`, the node where the walk starts is the first candidate. A node without that key
198
- * ends the walk with an error.
215
+ * This function makes a walker `nearest`. The walker follows a key (the default is `parent`) until the node that it
216
+ * reaches has the wanted type. With `self: true`, the node where the walk starts is the first candidate. A node
217
+ * without that key ends the walk with an error.
218
+ *
219
+ * @param options - The test of a type and the most hops.
220
+ * @returns The walker.
199
221
  */
200
- const nearest: Walker = ({ data, here, deref }) => {
201
- const follow = isRecord(data) && typeof field(data, "follow") === "string" ? String(field(data, "follow")) : "parent";
202
- const wanted = isRecord(data) ? field(data, "type") : undefined;
203
- const fromSelf = isRecord(data) && field(data, "self") === true;
204
- let at: Thing | undefined = here;
205
- for (let hops = 0; hops < 256; hops += 1) {
206
- if (hops > 0 || !fromSelf) at = at === undefined || at === null ? undefined : deref(at, follow);
207
- if (at === undefined || at === null) return { ok: false, reason: `No node of the type ${String(wanted)} is on the way.` };
208
- const type = isHostThing(at) ? (at as { readonly type?: unknown }).type : undefined;
209
- if (type === wanted) return { ok: true, thing: at };
210
- }
211
- return { ok: false, reason: "The walk is too long." };
212
- };
222
+ export function nearestWalker(options: NearestOptions = {}): Walker {
223
+ const matches = options.matches ?? hostType;
224
+ const maxHops = options.maxHops ?? 256;
225
+ return ({ data, here, deref }) => {
226
+ const follow = isRecord(data) && typeof field(data, "follow") === "string" ? String(field(data, "follow")) : "parent";
227
+ const wanted = isRecord(data) ? field(data, "type") : undefined;
228
+ const fromSelf = isRecord(data) && field(data, "self") === true;
229
+ let at: Thing | undefined = here;
230
+ for (let hops = 0; hops < maxHops; hops += 1) {
231
+ if (hops > 0 || !fromSelf) at = at === undefined || at === null ? undefined : deref(at, follow);
232
+ if (at === undefined || at === null) return { ok: false, reason: `No node of the type ${String(wanted)} is on the way.` };
233
+ if (matches(at, wanted)) return { ok: true, thing: at };
234
+ }
235
+ return { ok: false, reason: "The walk is too long." };
236
+ };
237
+ }
238
+
239
+ const nearest: Walker = nearestWalker();
213
240
 
214
241
  /** The walkers of the standard world, by name. */
215
242
  export const STD_WALKERS: Readonly<Record<string, Walker>> = { "walk.nearest": nearest };
216
243
 
217
244
  /** This function evaluates parts one after the other, and stops at the first error. */
218
- function* inOrder(parts: readonly RunPart[]): Generator<Promise<Result>, { readonly values: readonly Json[] } | { readonly error: Result }, Result> {
219
- const values: Json[] = [];
245
+ function* inOrder(parts: readonly RunPart[]): Generator<Promise<unknown>, { readonly values: readonly unknown[] } | { readonly error: Result }, unknown> {
246
+ const values: unknown[] = [];
220
247
  for (const part of parts) {
221
248
  const result = yield* part.evaluate();
222
249
  if (!result.ok) return { error: result };
@@ -230,9 +257,9 @@ function* inOrder(parts: readonly RunPart[]): Generator<Promise<Result>, { reado
230
257
  * order. In a run, an effect ends before the next step starts.
231
258
  */
232
259
  export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
233
- "control.seq": function* ({ parts, context }) {
234
- let names: Record<string, Json> = { ...context.names };
235
- let last: Json = null;
260
+ "control.seq": function* ({ parts, context }): Steps {
261
+ let names: Record<string, unknown> = { ...context.names };
262
+ let last: unknown = null;
236
263
  for (const part of parts) {
237
264
  const result = yield* part.evaluate({ ...context, names });
238
265
  if (!result.ok) return result;
package/src/world.ts CHANGED
@@ -4,33 +4,84 @@ import { type Envelope, type Json, type Key, type TypeRef, field, isRecord, type
4
4
  export type Side = "derive" | "run";
5
5
 
6
6
  /**
7
- * Where the parts of a value are. A part is a typed document that the evaluator gets sealed.
7
+ * Where the parts of a value are: the shape of the value. A part is a typed document that the evaluator gets
8
+ * sealed. `of` is the type of a bare part: a part without an envelope gets this type from its position.
8
9
  *
9
10
  * - `none`: the value is data. It has no parts.
10
- * - `list`: the value is a list, and each item is a part.
11
- * - `record`: the value is a record. The named fields are parts, and the other fields are data.
12
- *
13
- * `of` is the type of a bare item. An item without an envelope gets this type from its container.
11
+ * - `term`: the value itself is one part.
12
+ * - `list`: the value is a list. Each item has the shape `item` (the default is a part).
13
+ * - `map`: the value is a record. Each field has the shape `item` (the default is a part).
14
+ * - `record`: the value is a record. Each named field has its shape. A list of names makes each named field a
15
+ * part. The other fields have the shape `rest` (the default is data). `required` names the fields that must be.
16
+ * - `deep`: each envelope at any depth of the value is a part, and the rest is data.
14
17
  */
15
- export type PartsSpec =
18
+ export type Shape =
16
19
  | { readonly shape: "none" }
17
- | { readonly shape: "list"; readonly of?: string }
18
- | { readonly shape: "record"; readonly fields: readonly string[]; readonly of?: string };
20
+ | { readonly shape: "term"; readonly of?: string }
21
+ | { readonly shape: "list"; readonly of?: string; readonly item?: Shape }
22
+ | { readonly shape: "map"; readonly of?: string; readonly item?: Shape }
23
+ | {
24
+ readonly shape: "record";
25
+ readonly fields: readonly string[] | Readonly<Record<string, Shape>>;
26
+ readonly of?: string;
27
+ readonly required?: readonly string[];
28
+ readonly rest?: Shape;
29
+ }
30
+ | { readonly shape: "deep"; readonly of?: string };
31
+
32
+ /** The former name of `Shape`. */
33
+ export type PartsSpec = Shape;
34
+
35
+ /**
36
+ * Where the names of a scope rule come from.
37
+ *
38
+ * - `fixed`: the names in the list.
39
+ * - `keysAt`: the keys of the record at this path of the value, for example the bindings of a `let`.
40
+ * - `listAt`: the texts of the list at this path of the value, for example the parameters of a `fn`.
41
+ * - `textAt`: the text at this path of the value, or `otherwise` when the value has no text there.
42
+ * - `headers`: the binding name (`name`) of each part that the rule covers.
43
+ */
44
+ export type NamesSource =
45
+ | { readonly fixed: readonly string[] }
46
+ | { readonly keysAt: readonly Key[] }
47
+ | { readonly listAt: readonly Key[] }
48
+ | { readonly textAt: readonly Key[]; readonly otherwise?: string }
49
+ | { readonly headers: true };
50
+
51
+ /**
52
+ * A scope rule of a kind: names that the kind binds, and the parts that see them. The static kit reads the rules,
53
+ * and the evaluator of the kind binds the same names when it evaluates the parts.
54
+ */
55
+ export interface ScopeRule {
56
+ readonly names: NamesSource;
57
+ /** The parts that see the names, as patterns of their paths in the value. The key `"*"` matches each key. */
58
+ readonly in: readonly (readonly Key[])[];
59
+ /** `true`: a part sees only the names of the parts before it, as in a sequence. */
60
+ readonly sequential?: boolean;
61
+ }
19
62
 
20
63
  /** A kind gives the name of an evaluator for each verb, and the contract of its value. */
21
64
  export interface Kind {
22
65
  readonly verbs: Readonly<Record<string, string>>;
23
66
  /** Where the parts of the value are. The default is `none`. */
24
- readonly parts?: PartsSpec;
67
+ readonly parts?: Shape;
68
+ /** The names that the kind binds for its parts. */
69
+ readonly scope?: readonly ScopeRule[];
25
70
  /** The side of the kind. A kind of the run side causes effects. The default is `derive`. */
26
71
  readonly side?: Side;
72
+ /**
73
+ * The effects that a document of this kind can cause. It is a list of the kinds of the effects. The text
74
+ * `"type"` means that the kind of the effect is the type of the document itself, as for a call.
75
+ */
76
+ readonly effects?: "type" | readonly string[];
27
77
  /** One sentence that tells what the kind is. */
28
78
  readonly summary?: string;
29
79
  }
30
80
 
31
- /** A namespace: a group of entries under one key of the world. */
81
+ /** A namespace: a group of entries under one key of the world. `fallback` is the kind of each other key in it. */
32
82
  export interface Namespace {
33
83
  readonly kinds: Readonly<Record<string, WorldEntry>>;
84
+ readonly fallback?: Kind;
34
85
  }
35
86
 
36
87
  /** An alias: a reference to another entry of the world. The lookup follows it (collapse). */
@@ -70,6 +121,16 @@ export function isAlias(entry: WorldEntry | undefined): entry is Alias {
70
121
  return entry !== undefined && (entry as { type?: unknown }).type === "ref" && Array.isArray((entry as { value?: unknown }).value);
71
122
  }
72
123
 
124
+ /**
125
+ * This function tells if an entry of the world is a namespace.
126
+ *
127
+ * @param entry - The entry.
128
+ * @returns `true` for a namespace.
129
+ */
130
+ export function isNamespace(entry: WorldEntry | undefined): entry is Namespace {
131
+ return entry !== undefined && !isKind(entry) && !isAlias(entry) && Object.hasOwn(entry, "kinds");
132
+ }
133
+
73
134
  function entryAt(entries: Readonly<Record<string, WorldEntry>>, key: Key): WorldEntry | undefined {
74
135
  const name = String(key);
75
136
  return Object.hasOwn(entries, name) ? entries[name] : undefined;
@@ -77,7 +138,8 @@ function entryAt(entries: Readonly<Record<string, WorldEntry>>, key: Key): World
77
138
 
78
139
  /**
79
140
  * This function finds the kind that a type names. It walks the path from the root of the world, one key at a
80
- * time. An alias on the way is followed until a kind answers (collapse). A cycle of aliases stops the lookup.
141
+ * time. An alias on the way is followed until a kind answers (collapse). A cycle of aliases stops the lookup. In a
142
+ * namespace with a fallback, a key that the namespace does not have gets the fallback kind.
81
143
  *
82
144
  * @param world - The world.
83
145
  * @param type - The reference to the type.
@@ -90,12 +152,16 @@ export function lookupKind(world: World, type: TypeRef): Lookup {
90
152
  let aliases = 0;
91
153
  for (;;) {
92
154
  let entries = world.kinds;
155
+ let fallback: Kind | undefined;
93
156
  let entry: WorldEntry | undefined;
94
157
  let followed = false;
95
158
  for (let index = 0; index < path.length; index += 1) {
96
159
  const key = path[index] as Key;
97
160
  entry = entryAt(entries, key);
98
- if (entry === undefined) return { ok: false, reason: "unknown", path };
161
+ if (entry === undefined) {
162
+ if (fallback !== undefined) return { ok: true, kind: fallback, path, aliases };
163
+ return { ok: false, reason: "unknown", path };
164
+ }
99
165
  if (isAlias(entry)) {
100
166
  aliases += 1;
101
167
  if (aliases > 32) return { ok: false, reason: "cycle", path };
@@ -108,8 +174,9 @@ export function lookupKind(world: World, type: TypeRef): Lookup {
108
174
  break;
109
175
  }
110
176
  if (index < path.length - 1) {
111
- if (isKind(entry)) return { ok: false, reason: "not-a-kind", path };
112
- entries = (entry as Namespace).kinds;
177
+ if (!isNamespace(entry)) return { ok: false, reason: "not-a-kind", path };
178
+ entries = entry.kinds;
179
+ fallback = entry.fallback;
113
180
  }
114
181
  }
115
182
  if (followed) continue;
@@ -135,6 +202,16 @@ export function entriesAsJson(entries: Readonly<Record<string, WorldEntry>>): Js
135
202
  return out;
136
203
  }
137
204
 
205
+ function describeShape(shape: Shape | undefined): Json {
206
+ if (shape === undefined || shape.shape === "none") return "none";
207
+ if (shape.shape === "term") return `a part${shape.of === undefined ? "" : ` of ${shape.of}`}`;
208
+ if (shape.shape === "deep") return "each document in the value";
209
+ if (shape.shape === "list") return `a list${shape.of === undefined ? "" : ` of ${shape.of}`}`;
210
+ if (shape.shape === "map") return `a record of parts${shape.of === undefined ? "" : ` of ${shape.of}`}`;
211
+ const names = Array.isArray(shape.fields) ? shape.fields : Object.keys(shape.fields);
212
+ return `the fields ${names.join(", ")}`;
213
+ }
214
+
138
215
  /**
139
216
  * This function makes the document of a kind, so that a view can draw it as each other document.
140
217
  *
@@ -143,13 +220,10 @@ export function entriesAsJson(entries: Readonly<Record<string, WorldEntry>>): Js
143
220
  * @returns The document of type `kind`.
144
221
  */
145
222
  export function kindDocument(name: string, kind: Kind): Envelope {
146
- const parts: Json =
147
- kind.parts === undefined || kind.parts.shape === "none"
148
- ? "none"
149
- : kind.parts.shape === "list"
150
- ? `a list${kind.parts.of === undefined ? "" : ` of ${kind.parts.of}`}`
151
- : `the fields ${kind.parts.fields.join(", ")}`;
152
- return { type: "kind", value: { name, verbs: { ...kind.verbs }, parts, side: kind.side ?? "derive", summary: kind.summary ?? "" } };
223
+ return {
224
+ type: "kind",
225
+ value: { name, verbs: { ...kind.verbs }, parts: describeShape(kind.parts), side: kind.side ?? "derive", summary: kind.summary ?? "" },
226
+ };
153
227
  }
154
228
 
155
229
  /**
@@ -158,6 +232,6 @@ export function kindDocument(name: string, kind: Kind): Envelope {
158
232
  * @param value - The value.
159
233
  * @returns `true` when the value has verbs.
160
234
  */
161
- export function looksLikeKind(value: Json | undefined): boolean {
235
+ export function looksLikeKind(value: unknown): boolean {
162
236
  return isRecord(value) && isRecord(field(value, "verbs"));
163
237
  }