@mark1russell7/coeval 0.23.0 → 0.24.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/src/schedule.ts CHANGED
@@ -294,6 +294,8 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
294
294
  // The nodes that finished in a kept read, and if that read met a node in evaluation below it (D143).
295
295
  const keptNodes: string[] = [];
296
296
  let keptMet = false;
297
+ // The count of the visits at the start of the kept read. A node with a lower number started below the read.
298
+ let keptFrom = 0;
297
299
  let count = 0;
298
300
  let ahead = 0;
299
301
  // The node that stopped the evaluation of a walk, because the walk has no result for it.
@@ -370,8 +372,12 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
370
372
  const reader = stack[stack.length - 1];
371
373
  const known = epoch.values.get(id);
372
374
  if (known !== undefined) {
373
- // A finished node of an open component is on a cycle with its reader too.
374
- if (reader !== undefined && onOpen.has(id)) lower(reader, order.get(id) as number);
375
+ if (reader !== undefined && onOpen.has(id)) {
376
+ // The path from that component to a kept read has a read that no evaluation asked for (SCHED.KEPT-OPEN).
377
+ if (keptAt >= 0 && (order.get(id) as number) < keptFrom) keptMet = true;
378
+ // A finished node of an open component is on a cycle with its reader too.
379
+ else lower(reader, order.get(id) as number);
380
+ }
375
381
  return known;
376
382
  }
377
383
  if (busy.has(id)) {
@@ -592,9 +598,11 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
592
598
  keeping(read) {
593
599
  const outer = keptAt;
594
600
  const outerMet = keptMet;
601
+ const outerFrom = keptFrom;
595
602
  const from = keptNodes.length;
596
603
  keptAt = stack.length;
597
604
  keptMet = false;
605
+ keptFrom = count;
598
606
  try {
599
607
  return read();
600
608
  } finally {
@@ -609,6 +617,7 @@ export function schedule(machine: Machine, options: ScheduleOptions = {}): Sched
609
617
  }
610
618
  keptAt = outer;
611
619
  keptMet = outerMet;
620
+ keptFrom = outerFrom;
612
621
  }
613
622
  },
614
623
  });
package/src/static.ts CHANGED
@@ -4,7 +4,7 @@ import { answerOf, ask } from "./ladder.ts";
4
4
  import { type PartSlot, partsOf, rulesCover, scopesOf } from "./parts.ts";
5
5
  import { type Cap, type Context, type Root, ROOTS, context, isRef, walkerOf, worldThing } from "./deref.ts";
6
6
  import type { ErrorCode } from "./errors.ts";
7
- import { type Machine, partCaps, permits, waitedReason } from "./evaluate.ts";
7
+ import { type Machine, partCaps, partGrants, permits, waitedReason } from "./evaluate.ts";
8
8
  import type { ValueType } from "./types.ts";
9
9
  import { type RootTypes, type TypeNode, type Typed, machineTypeIssues, typePass } from "./typing.ts";
10
10
  import { cycleIssues } from "./instances.ts";
@@ -41,6 +41,8 @@ export interface DocVisit {
41
41
  * around it (`Kind.caps`). A walk without capabilities gives none here.
42
42
  */
43
43
  readonly caps?: readonly Cap[] | undefined;
44
+ /** The kinds of effects that a handler around the document answers (CAP.GRANT, D146). Such an effect does not reach the host. */
45
+ readonly handled?: readonly Cap[] | undefined;
44
46
  }
45
47
 
46
48
  /** A position that a walk cannot read as a document. */
@@ -114,6 +116,7 @@ class Visit implements DocVisit {
114
116
  readonly typePath: readonly Key[] | undefined;
115
117
  readonly address: true | undefined;
116
118
  readonly caps: readonly Cap[] | undefined;
119
+ readonly handled: readonly Cap[] | undefined;
117
120
  readonly #place: Trail | undefined;
118
121
  readonly #at: Trail | undefined;
119
122
  readonly #bound: readonly string[];
@@ -135,6 +138,7 @@ class Visit implements DocVisit {
135
138
  caps: readonly Cap[] | undefined,
136
139
  bound: readonly string[],
137
140
  expect: ValueType | undefined,
141
+ handled: readonly Cap[] | undefined,
138
142
  ) {
139
143
  this.doc = doc;
140
144
  this.declared = declared;
@@ -147,6 +151,7 @@ class Visit implements DocVisit {
147
151
  this.typePath = typePath;
148
152
  this.address = address;
149
153
  this.caps = caps;
154
+ this.handled = handled;
150
155
  this.#place = place;
151
156
  this.#at = at;
152
157
  this.#bound = bound;
@@ -199,6 +204,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
199
204
  readonly caps: readonly Cap[] | undefined;
200
205
  readonly bound: readonly string[];
201
206
  readonly expect: ValueType | undefined;
207
+ readonly handled: readonly Cap[] | undefined;
202
208
  }
203
209
  const stack: Item[] = [
204
210
  {
@@ -213,6 +219,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
213
219
  slot: [],
214
220
  parent: undefined,
215
221
  caps: options.caps,
222
+ handled: undefined,
216
223
  bound: [],
217
224
  expect: undefined,
218
225
  },
@@ -241,6 +248,7 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
241
248
  item.caps,
242
249
  item.bound,
243
250
  item.expect,
251
+ item.handled,
244
252
  );
245
253
  const answer = visit(here);
246
254
  if (answer === "skip" || kind === undefined) continue;
@@ -250,10 +258,14 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
250
258
  continue;
251
259
  }
252
260
  const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
261
+ // The rules of a grant are read one time for each kind, not for each part. A kind without such a rule gives no list (K238).
262
+ const grantRules = kind.caps !== undefined && kind.caps.some((rule) => rule.rule === "grant") ? kind.caps : undefined;
253
263
  for (let index = split.parts.length - 1; index >= 0; index -= 1) {
254
264
  const slot = split.parts[index] as PartSlot;
255
265
  const extra = sees[index] ?? [];
256
266
  const hidden = later[index] ?? [];
267
+ const grants = grantRules === undefined ? undefined : partGrants(grantRules, slot.path, doc.value);
268
+ const granted = grants !== undefined && grants.length > 0 ? grants : undefined;
257
269
  stack.push({
258
270
  value: slot.item,
259
271
  place: slot.path.length === 0 ? item.place : { up: item.place, keys: slot.path },
@@ -265,7 +277,14 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
265
277
  address: slot.address === true,
266
278
  slot: slot.path,
267
279
  parent: here,
268
- caps: item.caps === undefined || kind.caps === undefined ? item.caps : partCaps(kind.caps, slot.path, item.caps),
280
+ // A part with no grant keeps the list of partCaps, with no copy (K238).
281
+ caps:
282
+ item.caps === undefined || kind.caps === undefined
283
+ ? item.caps
284
+ : granted === undefined
285
+ ? partCaps(kind.caps, slot.path, item.caps)
286
+ : [...partCaps(kind.caps, slot.path, item.caps), ...granted],
287
+ handled: granted === undefined ? item.handled : [...(item.handled ?? []), ...granted],
269
288
  bound: extra,
270
289
  expect: slot.expect,
271
290
  });
@@ -361,6 +380,45 @@ export function mapRefs(world: World, doc: unknown, change: (use: RefUse) => unk
361
380
  return out;
362
381
  }
363
382
 
383
+ /**
384
+ * The answer of the function of `mapDocs` for one document.
385
+ *
386
+ * - `undefined`: keep the document and go into its parts.
387
+ * - `"skip"`: keep the document and stop there.
388
+ * - `{ to }`: put `to` in the place of the document and stop there.
389
+ * - `{ to, into: true }`: put `to` in the place of the document, then go into the parts of the old document. Thus
390
+ * `to` must keep the path of each such part.
391
+ */
392
+ export type DocChange = undefined | "skip" | { readonly to: unknown; readonly into?: boolean };
393
+
394
+ /**
395
+ * This function changes the documents of a document, from the root to the parts (STATIC.MAPDOCS). The function
396
+ * `change` reads only the original documents. It decides at each node, and it can stop at a node. The kinds of a
397
+ * position come from the world, thus this function names no kind. The document is not changed: the function gives a
398
+ * new document that shares each part without a change.
399
+ *
400
+ * @param world - The world of the kinds.
401
+ * @param doc - The document, as JSON.
402
+ * @param change - The function that gives the answer for each visit. See `DocChange`.
403
+ * @param options - The type of a bare root value, and the names that the host binds at the root.
404
+ * @returns The new document, or `doc` itself when no document changes.
405
+ */
406
+ export function mapDocs(world: World, doc: unknown, change: (visit: DocVisit) => DocChange, options: WalkOptions = {}): unknown {
407
+ let out = doc;
408
+ walk(
409
+ world,
410
+ doc,
411
+ (visit) => {
412
+ const next = change(visit);
413
+ if (next === undefined || next === "skip") return next;
414
+ out = setAt(out, visit.at, next.to);
415
+ return next.into === true ? undefined : "skip";
416
+ },
417
+ options,
418
+ );
419
+ return out;
420
+ }
421
+
364
422
  /** A problem that a check finds before a document runs. */
365
423
  export interface Issue {
366
424
  readonly code: ErrorCode;
@@ -770,6 +828,7 @@ const KEEPS_SELF: ReadonlySet<string> = /* @__PURE__ */ new Set([
770
828
  "control.seq",
771
829
  "control.pure",
772
830
  "control.if",
831
+ "control.handle",
773
832
  "list.all",
774
833
  "effect.log",
775
834
  "effect.write",
package/src/stdlib.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { type Json, type Key, docEquals, field, isRecord } from "./doc.ts";
1
+ import { type Json, type Key, docEquals, field, isKey, isRecord } from "./doc.ts";
2
2
  import {
3
3
  type EvalInput,
4
4
  type Evaluator,
5
+ type Clause,
5
6
  type InputWithRuntime,
6
7
  type Machine,
7
8
  type Plan,
@@ -144,6 +145,27 @@ export const STD_WORLD: World = {
144
145
  lazy: [{ after: 1 }],
145
146
  summary: "It evaluates its part without capabilities, thus an effect in it fails.",
146
147
  },
148
+ handle: {
149
+ verbs: { eval: "control.handle", view: "view.op" },
150
+ parts: {
151
+ shape: "record",
152
+ fields: {
153
+ with: { shape: "list", item: { shape: "record", fields: { op: { shape: "none" }, do: { shape: "term" }, abort: { shape: "term" } }, required: ["op"], closed: true } },
154
+ return: { shape: "term" },
155
+ body: { shape: "term" },
156
+ },
157
+ required: ["with", "body"],
158
+ closed: true,
159
+ },
160
+ scope: [
161
+ { names: { fixed: ["payload"] }, in: [["with", "*", "do"], ["with", "*", "abort"]] },
162
+ { names: { fixed: ["result"] }, in: [["return"]] },
163
+ ],
164
+ caps: [{ rule: "grant", from: ["with", "*", "op"], in: [["body"]] }],
165
+ // A clause runs at an effect of the body, and the clause return runs only after a body without a failure.
166
+ lazy: [{ in: [["with", "*", "do"], ["with", "*", "abort"], ["return"]] }],
167
+ summary: "It answers the effects of its body with its clauses, in place, thus a mock, a retry or a log is a document.",
168
+ },
147
169
  log: {
148
170
  verbs: { eval: "effect.log", view: "view.op" },
149
171
  parts: { shape: "list", of: "text" },
@@ -170,7 +192,8 @@ export const STD_WORLD: World = {
170
192
  * A literal in a place that takes a document gets its data envelope (K159, D65). Such a place is a part of `if`,
171
193
  * `list`, `seq` or `pure`, the value of `write`, or the whole text. The document after a header name is such a place
172
194
  * too. Thus `list(1, "a")` evaluates to `[1, "a"]`. The forms `add`, `multiply` and `greater` write an operator document that its operator
173
- * does not write, for example `add(5)` (K163).
195
+ * does not write, for example `add(5)` (K163). The field `with` of `handle` is a field of data, thus each clause is a
196
+ * record between braces in a list between brackets (D177).
174
197
  *
175
198
  * A host with more kinds spreads this profile and adds its forms to `calls.forms`. `STD_SYNTAX` stays the profile of
176
199
  * the formula kit.
@@ -201,6 +224,8 @@ export const WORLD_SYNTAX: SyntaxProfile = /* @__PURE__ */ Object.freeze({
201
224
  { name: "tree.named", kind: "tree.named", args: "named", fields: ["name", "in"] },
202
225
  { name: "tree.at", kind: "tree.at", args: "named", fields: ["attr", "args"] },
203
226
  { name: "tree.quote", kind: "tree.quote", args: "literal" },
227
+ // The clauses are data with documents in it, for example handle(with: [{op: "log", do: payload + 1}], body: log(2)) (D177).
228
+ { name: "handle", kind: "handle", args: "named", fields: ["with", "return", "body"], data: ["with"], wrap: ["return", "body"] },
204
229
  // An operator document with fewer terms than its operator takes, for example add(5) (K163, D65).
205
230
  { name: "add", kind: "add" },
206
231
  { name: "multiply", kind: "multiply" },
@@ -813,6 +838,7 @@ function leaderOnHost(n: LoopNode, key: string, rule: CircularRule & { readonly
813
838
  /**
814
839
  * This function gives an attribute of a node with circular attributes, or an attribute in an open fixed point (D113).
815
840
  * The memo and the marks of the node do not hold the attribute. An open fixed point does not use the cells of the host.
841
+ * With the cells of a host, only a circular attribute comes here outside a fixed point. A plain attribute takes its cell.
816
842
  */
817
843
  function circularRead(n: LoopNode, key: string): Result | undefined {
818
844
  const rule = circularRule(n.tree, key);
@@ -1199,7 +1225,8 @@ function treeThing(input: EvalInput, place: readonly Key[], build: TreeBuild, ou
1199
1225
  const text = attributeText(key, type, place);
1200
1226
  if (marks.has(key)) return busyAt(state, key);
1201
1227
  if (tree === undefined && up?.tree === undefined && !parts.some((item) => item.key === key)) return undefined;
1202
- if (circular || openPoint !== undefined) return circularRead(state, key);
1228
+ // Outside a fixed point, a plain attribute of a node with a circular attribute also takes the cell of the host.
1229
+ if (openPoint !== undefined || (circular && circularRule(tree, key) !== undefined)) return circularRead(state, key);
1203
1230
  marks.set(key, BUSY);
1204
1231
  demands.push({ cells: marks, key, text });
1205
1232
  let thrown: unknown = NOT_THROWN;
@@ -1493,6 +1520,39 @@ function* inOrder(parts: readonly RunPart[]): Generator<Promise<unknown>, { read
1493
1520
  return { values };
1494
1521
  }
1495
1522
 
1523
+ /** The value of a `handle` document, with a part in the place of each part. */
1524
+ interface HandleValue {
1525
+ readonly with: readonly { readonly op: Cap; readonly do?: RunPart; readonly abort?: RunPart }[];
1526
+ readonly return?: RunPart;
1527
+ readonly body?: RunPart;
1528
+ }
1529
+
1530
+ /** This function tells if a value names a kind of effects: a text, or a list of keys. */
1531
+ function isOp(value: unknown): value is Cap {
1532
+ return typeof value === "string" || (Array.isArray(value) && value.every(isKey));
1533
+ }
1534
+
1535
+ /**
1536
+ * This function gives the runner of an `if` with its own truth (RUN.IF-TRUTH). A branch can wait for the host, thus a
1537
+ * language with the function `truthy` of a profile does not need a copy of `if`. A missing test and a test without a
1538
+ * truth give `#VALUE!`, a failed test gives its failure, and a missing branch gives `null`.
1539
+ *
1540
+ * @param truth - The function that gives the truth of a value, or `undefined` for a value without a truth.
1541
+ * @returns The runner.
1542
+ */
1543
+ export function ifRunner(truth: (value: unknown) => boolean | undefined): Runner {
1544
+ return function* ({ parts }) {
1545
+ const test = parts.find((part) => part.key === "test");
1546
+ if (test === undefined) return fail("#VALUE!", "if needs a test.");
1547
+ const chosen = yield* test.evaluate();
1548
+ if (!chosen.ok) return chosen;
1549
+ const held = truth(chosen.value);
1550
+ if (held === undefined) return fail("#VALUE!", "The test of if must give a truth value.");
1551
+ const branch = parts.find((part) => part.key === (held ? "then" : "else"));
1552
+ return branch === undefined ? ok(null) : yield* branch.evaluate();
1553
+ };
1554
+ }
1555
+
1496
1556
  /**
1497
1557
  * The runners of the standard world, by name: the kinds that can hold effects. Each one evaluates its parts in
1498
1558
  * order. In a run, an effect ends before the next step starts.
@@ -1516,15 +1576,33 @@ export const STD_RUNNERS: Readonly<Record<string, Runner>> = {
1516
1576
  if (only === undefined || parts.length > 1) return fail("#VALUE!", "pure needs exactly one part.");
1517
1577
  return yield* only.evaluate();
1518
1578
  },
1519
- "control.if": function* ({ parts }) {
1520
- const test = parts.find((part) => part.key === "test");
1521
- if (test === undefined) return fail("#VALUE!", "if needs a test.");
1522
- const chosen = yield* test.evaluate();
1523
- if (!chosen.ok) return chosen;
1524
- if (typeof chosen.value !== "boolean") return fail("#VALUE!", "The test of if must give a truth value.");
1525
- const branch = parts.find((part) => part.key === (chosen.value === true ? "then" : "else"));
1526
- return branch === undefined ? ok(null) : yield* branch.evaluate();
1579
+ // A clause gets the context of the handle document, thus it has no more authority than its parent (HANDLE.CLAUSE).
1580
+ "control.handle": function* ({ value, context }) {
1581
+ const { with: list, return: after, body } = value as HandleValue;
1582
+ if (body?.handled === undefined) return fail("#VALUE!", "handle needs a body.");
1583
+ let stopped: Result | undefined;
1584
+ const clauses: Clause[] = [];
1585
+ for (const item of list) {
1586
+ const part = item.do ?? item.abort;
1587
+ if (!isOp(item.op)) return fail("#VALUE!", "The op of a clause of handle must be a text or a list of keys.");
1588
+ if (part === undefined || (item.do !== undefined && item.abort !== undefined)) return fail("#VALUE!", "Each clause of handle needs exactly one of do and abort.");
1589
+ const stops = item.do === undefined;
1590
+ clauses.push({
1591
+ op: item.op,
1592
+ answer: function* (payload) {
1593
+ const result = yield* part.evaluate({ ...context, names: { ...context.names, payload } });
1594
+ if (!stops) return result;
1595
+ stopped ??= result;
1596
+ return fail("#CALC!", "A clause abort of handle stopped the body.");
1597
+ },
1598
+ });
1599
+ }
1600
+ const result = yield* body.handled(clauses);
1601
+ if (stopped !== undefined) return stopped;
1602
+ if (!result.ok || after === undefined) return result;
1603
+ return yield* after.evaluate({ ...context, names: { ...context.names, result: result.value } });
1527
1604
  },
1605
+ "control.if": /* @__PURE__ */ ifRunner((value) => (typeof value === "boolean" ? value : undefined)),
1528
1606
  "list.all": function* ({ parts }) {
1529
1607
  const all = yield* inOrder(parts);
1530
1608
  return "error" in all ? all.error : ok(all.values);