@mark1russell7/coeval 0.4.1 → 0.4.3

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.
@@ -0,0 +1,566 @@
1
+ import { type Envelope, type Json, type Key, isEnvelope, isRecord, typeName } from "./doc.ts";
2
+ import { ask } from "./ladder.ts";
3
+ import type { CapsRule, Kind } from "./world.ts";
4
+ import { type PartSlot, type Split, partsOf, pathMatches } from "./parts.ts";
5
+ import { type Cap, type Context, type Resolution, type ResolveOptions, resolve, valueOfThing, worldThing } from "./deref.ts";
6
+ import { type EvalError, type Failed, type Result, Failure, fail, ok, placed, thrown } from "./errors.ts";
7
+ import {
8
+ type AllOptions,
9
+ type Effect,
10
+ type EvalFn,
11
+ type EvalInput,
12
+ type Machine,
13
+ type Part,
14
+ type RunFn,
15
+ type RunInput,
16
+ type RunPart,
17
+ type Steps,
18
+ intersectCaps,
19
+ makeContext,
20
+ permits,
21
+ } from "./evaluate.ts";
22
+ import { type FormulaProfile, type OpFn, Held, STD_EXT } from "./formula.ts";
23
+
24
+ /*
25
+ * The reference interpreter: a direct reading of the rules of docs/SPEC.md. It compiles nothing and caches nothing.
26
+ * At each evaluation of a document, it asks the ladder, finds the kind, splits the value and calls the evaluator. A
27
+ * part evaluates only when its evaluator asks. Thus it shares no compiler with a plan, and a test can compare them.
28
+ */
29
+
30
+ /**
31
+ * The definitional code of the reference, by evaluator name. The reference cannot read compiled code, thus each
32
+ * compiled evaluator or runner of a machine needs a definition here. A function evaluator needs none.
33
+ */
34
+ export interface Definitions {
35
+ readonly evaluators?: Readonly<Record<string, EvalFn>>;
36
+ readonly runners?: Readonly<Record<string, RunFn>>;
37
+ }
38
+
39
+ /** The options of the reference interpreter. */
40
+ export interface ReferenceOptions {
41
+ /** The type of a bare root value. */
42
+ readonly declaredType?: string;
43
+ /** The definitional code of the compiled evaluators and runners of the machine. */
44
+ readonly definitions?: Definitions;
45
+ /** A list that gets each effect, in its order. */
46
+ readonly effects?: Effect[];
47
+ }
48
+
49
+ /** The state of one evaluation of the reference. */
50
+ interface Env {
51
+ readonly machine: Machine;
52
+ readonly definitions: Definitions;
53
+ readonly effects: Effect[];
54
+ readonly maxDepth: number;
55
+ }
56
+
57
+ /** The code that answers a document: a function evaluator, a runner, or a gap of the reference. */
58
+ type Chosen = { readonly evaluator: EvalFn } | { readonly runner: RunFn } | { readonly gap: string };
59
+
60
+ /** The answers of the ladder and of the contract of the kind for one document. */
61
+ interface Seen {
62
+ readonly doc: Envelope;
63
+ readonly kindText: string;
64
+ readonly typePath: readonly Key[];
65
+ readonly kind: Kind | undefined;
66
+ readonly name: string;
67
+ readonly chosen: Chosen;
68
+ readonly split: Extract<Split, { readonly ok: true }>;
69
+ }
70
+
71
+ /** One position of a document: its value, its place, the type that it declares, its depth and its context. */
72
+ interface At {
73
+ readonly value: unknown;
74
+ readonly place: readonly Key[];
75
+ readonly declared: string | undefined;
76
+ readonly depth: number;
77
+ readonly address: boolean;
78
+ readonly ctx: Context;
79
+ }
80
+
81
+ function own<T>(table: Readonly<Record<string, T>> | undefined, name: string): T | undefined {
82
+ return table !== undefined && Object.hasOwn(table, name) ? table[name] : undefined;
83
+ }
84
+
85
+ /** This function finds the code of an evaluator name. A runner wins over an evaluator of the same name. */
86
+ function choose(env: Env, name: string): Chosen | undefined {
87
+ const runner = own(env.machine.runners, name);
88
+ if (runner !== undefined) {
89
+ const defined = own(env.definitions.runners, name);
90
+ if (defined !== undefined) return { runner: defined };
91
+ return typeof runner === "function" ? { runner } : { gap: `The reference has no definition of the compiled runner ${name}.` };
92
+ }
93
+ const evaluator = own(env.machine.evaluators, name);
94
+ if (evaluator === undefined) return undefined;
95
+ const defined = own(env.definitions.evaluators, name);
96
+ if (defined !== undefined) return { evaluator: defined };
97
+ return typeof evaluator === "function" ? { evaluator } : { gap: `The reference has no definition of the compiled evaluator ${name}.` };
98
+ }
99
+
100
+ /**
101
+ * This function reads one document by the rules, in their order. First come an envelope or a declared type
102
+ * (EVAL.ERRORS) and the limit of the depth (PLAN.DEPTH). Then come the ladder (LADDER.*), an evaluator and the shape
103
+ * of the parts (SHAPE.*).
104
+ */
105
+ function examine(env: Env, at: At): Seen | Failed {
106
+ const { place, declared } = at;
107
+ const world = env.machine.world;
108
+ // The lookup of the kind and the split of the value are definitions of the model (D15): partsOf gives both.
109
+ const read = partsOf(world, at.value, declared);
110
+ if (!read.ok) return fail(read.code, read.reason, { place });
111
+ if (at.depth > env.maxDepth) return fail("#DEPTH!", `The nesting is deeper than ${env.maxDepth} documents.`, { place });
112
+ const { doc, kind, split } = read;
113
+ const kindText = typeName(doc.type);
114
+ const answer = ask(world, read.declared ? {} : doc, "eval", declared === undefined ? {} : { declaredType: declared });
115
+ if (answer.error === "unknown-kind") return fail("#KIND?", `The world has no kind ${kindText}.`, { place });
116
+ const name = answer.evaluator;
117
+ const chosen = name === undefined ? undefined : choose(env, name);
118
+ if (name === undefined || chosen === undefined) return fail("#NOEVAL", `No rung gives an evaluator for ${kindText}.`, { place });
119
+ if (!split.ok) return fail("#VALUE!", split.reason, { place });
120
+ return { doc, kindText, typePath: read.typePath ?? [], kind, name, chosen, split };
121
+ }
122
+
123
+ /** This function gives the capabilities that the rules of a kind give one part (D14, CAP.CONTRACT, CAP.ONLY). */
124
+ function capsOfPart(rules: readonly CapsRule[] | undefined, path: readonly Key[], caps: readonly Cap[]): readonly Cap[] {
125
+ let out = caps;
126
+ for (const rule of rules ?? []) {
127
+ if (rule.in !== undefined && !rule.in.some((pattern) => pathMatches(path, pattern))) continue;
128
+ if (rule.rule === "mask") return [];
129
+ if (rule.rule === "only") out = intersectCaps(out, rule.caps);
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /**
135
+ * This function gives the context of a part. A context that the evaluator gives keeps only the capabilities of the
136
+ * parent (CAP.NARROW). Then the rules of the kind narrow it (CAP.CONTRACT).
137
+ */
138
+ function contextOfPart(seen: Seen, slot: PartSlot, parent: Context, given: Context | undefined): Context {
139
+ const base = given === undefined || given === parent ? parent : { ...given, caps: given.caps.filter((cap) => permits(parent.caps, cap)) };
140
+ const caps = capsOfPart(seen.kind?.caps, slot.path, base.caps);
141
+ return caps === base.caps ? base : { ...base, caps };
142
+ }
143
+
144
+ function headerOf(item: Json): string | undefined {
145
+ return isEnvelope(item) && typeof (item as { readonly name?: unknown }).name === "string" ? (item as { readonly name: string }).name : undefined;
146
+ }
147
+
148
+ function partAt(seen: Seen, at: At, slot: PartSlot, given: Context | undefined): At {
149
+ return {
150
+ value: slot.item,
151
+ place: at.place.concat(slot.path),
152
+ declared: slot.of,
153
+ depth: at.depth + 1,
154
+ address: slot.address === true,
155
+ ctx: contextOfPart(seen, slot, at.ctx, given),
156
+ };
157
+ }
158
+
159
+ /** This function resolves a path with the full algorithm of `resolve`, not with its short path (DEREF.QUICK). */
160
+ function resolveFully(machine: Machine, ctx: Context, path: readonly unknown[], options?: ResolveOptions): Resolution {
161
+ const found = resolve(machine, ctx, path, { hops: true });
162
+ return options?.hops === true ? found : { ...found, hops: [] };
163
+ }
164
+
165
+ /** This function records an effect when the capabilities permit it (RUN.CAP, CAP.HOOK). */
166
+ function record(env: Env, ctx: Context, kind: Cap, payload: unknown, place: readonly Key[]): Failed | undefined {
167
+ const keys = typeof kind === "string" ? kind.split(".") : kind;
168
+ const text = keys.join(".");
169
+ const machine = env.machine;
170
+ const allowed = machine.permits === undefined ? permits(ctx.caps, keys) : machine.permits(ctx.caps, keys, ctx);
171
+ if (!allowed) return fail("#CAP!", `The context has no capability for the effect ${text}.`, { place });
172
+ env.effects.push({ index: env.effects.length, kind: text, path: keys, payload, node: -1, at: 0, place, context: ctx });
173
+ return undefined;
174
+ }
175
+
176
+ /** This function evaluates a document. Each error gets the place of the document where it started (K5). */
177
+ function evaluateAt(env: Env, at: At): Result {
178
+ const seen = examine(env, at);
179
+ if (!("doc" in seen)) return seen;
180
+ return evaluateSeen(env, seen, at);
181
+ }
182
+
183
+ function evaluateSeen(env: Env, seen: Seen, at: At): Result {
184
+ const { chosen } = seen;
185
+ const { place } = at;
186
+ if ("gap" in chosen) return fail("#NOEVAL", chosen.gap, { place });
187
+ if ("runner" in chosen) return drive(runSeen(env, seen, at, chosen.runner), place);
188
+ const input = evalInput(env, seen, at);
189
+ try {
190
+ return placed(chosen.evaluator(input), place);
191
+ } catch (error) {
192
+ return placed(thrown(error, `the evaluator ${seen.name}`), place);
193
+ }
194
+ }
195
+
196
+ /** This function gives the steps of a document for a runner. Nothing happens before the first step. */
197
+ function* stepsAt(env: Env, at: At): Steps {
198
+ const seen = examine(env, at);
199
+ if (!("doc" in seen)) return seen;
200
+ if ("runner" in seen.chosen) return yield* runSeen(env, seen, at, seen.chosen.runner);
201
+ return evaluateSeen(env, seen, at);
202
+ }
203
+
204
+ function* runSeen(env: Env, seen: Seen, at: At, runner: RunFn): Steps {
205
+ try {
206
+ return placed(yield* runner(runInput(env, seen, at)), at.place);
207
+ } catch (error) {
208
+ return placed(thrown(error, `the runner ${seen.name}`), at.place);
209
+ }
210
+ }
211
+
212
+ /** This function drives steps that cannot wait. A step that waits gives `#CALC!` at the document (RUN.WAIT). */
213
+ function drive(steps: Steps, place: readonly Key[]): Result {
214
+ const step = steps.next();
215
+ if (step.done === true) return step.value;
216
+ void step.value.catch(() => undefined);
217
+ const waited = fail("#CALC!", "A part waited, and its parent cannot wait.", { place });
218
+ steps.return(waited);
219
+ return waited;
220
+ }
221
+
222
+ function evalInput(env: Env, seen: Seen, at: At): EvalInput {
223
+ const { split, doc } = seen;
224
+ const parts: Part[] = split.parts.map((slot) => {
225
+ const name = headerOf(slot.item);
226
+ return {
227
+ key: slot.key,
228
+ path: slot.path,
229
+ ...(name === undefined ? {} : { name }),
230
+ evaluate: (given?: Context) => evaluateAt(env, partAt(seen, at, slot, given)),
231
+ };
232
+ });
233
+ const fill = (items: readonly unknown[]): unknown => (split.parts.length === 0 ? doc.value : split.fill(items));
234
+ return {
235
+ kind: seen.kindText,
236
+ typePath: seen.typePath,
237
+ doc,
238
+ value: fill(parts),
239
+ data: split.data,
240
+ parts,
241
+ context: at.ctx,
242
+ place: at.place,
243
+ ...(at.address ? { address: true } : {}),
244
+ resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
245
+ effect: (kind, payload) => record(env, at.ctx, kind, payload, at.place) ?? ok(payload),
246
+ fill,
247
+ };
248
+ }
249
+
250
+ function runInput(env: Env, seen: Seen, at: At): RunInput {
251
+ const { split, doc } = seen;
252
+ const parts: RunPart[] = split.parts.map((slot) => {
253
+ const name = headerOf(slot.item);
254
+ return {
255
+ key: slot.key,
256
+ path: slot.path,
257
+ ...(name === undefined ? {} : { name }),
258
+ evaluate: (given?: Context) => stepsAt(env, partAt(seen, at, slot, given)),
259
+ };
260
+ });
261
+ const fill = (items: readonly unknown[]): unknown => (split.parts.length === 0 ? doc.value : split.fill(items));
262
+ return {
263
+ kind: seen.kindText,
264
+ typePath: seen.typePath,
265
+ doc,
266
+ value: fill(parts),
267
+ data: split.data,
268
+ parts,
269
+ context: at.ctx,
270
+ place: at.place,
271
+ resolve: (path, options) => resolveFully(env.machine, at.ctx, path, options),
272
+ effect: function* (kind, payload) {
273
+ return record(env, at.ctx, kind, payload, at.place) ?? ok(payload);
274
+ },
275
+ all: (list, options) => together(list, options),
276
+ fill,
277
+ };
278
+ }
279
+
280
+ /**
281
+ * This function evaluates parts for `all` (RUN.ALL). The parts start in their order, up to the limit, and each part
282
+ * goes on until it ends or waits. In an evaluation, a part that waits does not end, thus it keeps its place in the
283
+ * limit. With `failFast`, the first failure stops the parts that wait, and the later parts do not start
284
+ * (RUN.FAIL-FAST). When no part failed and a part waits, the steps wait, and the drive gives `#CALC!`.
285
+ */
286
+ function* together(list: readonly RunPart[], options: AllOptions = {}): Generator<Promise<unknown>, readonly Result[], unknown> {
287
+ const limit = Math.max(1, options.concurrency ?? list.length);
288
+ const results: (Result | undefined)[] = list.map(() => undefined);
289
+ const waiting: { readonly steps: Steps; readonly promise: Promise<unknown> }[] = [];
290
+ let failed = false;
291
+ for (let index = 0; index < list.length && !failed && waiting.length < limit; index += 1) {
292
+ let result: Result | undefined;
293
+ try {
294
+ const steps = (list[index] as RunPart).evaluate();
295
+ const step = steps.next();
296
+ if (step.done === true) result = step.value;
297
+ else waiting.push({ steps, promise: step.value });
298
+ } catch (error) {
299
+ result = thrown(error, "a part");
300
+ }
301
+ results[index] = result;
302
+ if (result !== undefined && options.failFast === true && !result.ok) failed = true;
303
+ }
304
+ if (failed) {
305
+ const stopped = fail("#ABORT", "The part stopped: a part before it failed, and the parts run with failFast.");
306
+ for (const item of waiting) {
307
+ try {
308
+ item.steps.return(stopped);
309
+ } catch {
310
+ // The finally code of a part threw. The part stops with the abort.
311
+ }
312
+ }
313
+ return results.map((result) => result ?? stopped);
314
+ }
315
+ if (waiting.length > 0) yield Promise.race(waiting.map((item) => item.promise));
316
+ // An evaluation stops at the first wait, thus only parts that ended reach this line.
317
+ return results as readonly Result[];
318
+ }
319
+
320
+ /**
321
+ * This function evaluates a document by the rules, without a compilation. It is the reference of the plans: a plan,
322
+ * a traced plan and the second tier of a language must give its results. It covers evaluations, not runs: an effect
323
+ * gives its payload, and a part that waits gives `#CALC!`.
324
+ *
325
+ * @param machine - The world and the code of the evaluators.
326
+ * @param doc - The document.
327
+ * @param context - The names and the capabilities at the root.
328
+ * @param options - The type of a bare root value, the definitions of compiled code, and a list for the effects.
329
+ * @returns The result.
330
+ */
331
+ export function reference(machine: Machine, doc: unknown, context: Context = makeContext(), options: ReferenceOptions = {}): Result {
332
+ const env: Env = { machine, definitions: options.definitions ?? {}, effects: options.effects ?? [], maxDepth: machine.maxDepth ?? 1000 };
333
+ const ctx = context.world === null ? { ...context, world: worldThing(machine.world) } : context;
334
+ try {
335
+ return evaluateAt(env, { value: doc, place: [], declared: options.declaredType, depth: 0, address: false, ctx });
336
+ } catch (error) {
337
+ return placed(thrown(error, "the reference"), []);
338
+ }
339
+ }
340
+
341
+ /* The definitions of the formula kit: one function evaluator for each construct, written from docs/SPEC.md. */
342
+
343
+ const CLOSURE: unique symbol = Symbol.for("coeval.closure") as never;
344
+
345
+ /** This function sets an own field, also the field `__proto__` (SHAPE.PROTO). */
346
+ function setField(target: Record<string, unknown>, key: string, value: unknown): void {
347
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
348
+ }
349
+
350
+ function partNamed(parts: readonly Part[], key: string): Part | undefined {
351
+ return parts.find((part) => part.path.length === 1 && part.path[0] === key);
352
+ }
353
+
354
+ function partsUnder(parts: readonly Part[], key: string): readonly Part[] {
355
+ return parts.filter((part) => part.path.length === 2 && part.path[0] === key);
356
+ }
357
+
358
+ /**
359
+ * This function gives the definitions of the formula kit for one profile. Each construct is a function evaluator in
360
+ * the most direct style. It shares no code with the compilers of `formulaEvaluators`, thus the two readings of the
361
+ * rules can disagree, and a test finds it.
362
+ *
363
+ * @param profile - The operations and the rules of the language.
364
+ * @returns The definitions, by evaluator name.
365
+ */
366
+ export function formulaDefinitions(profile: FormulaProfile = {}): Definitions {
367
+ const truthy = profile.truthy ?? ((value: unknown): boolean | undefined => (typeof value === "boolean" ? value : undefined));
368
+ const opOf = (name: string): OpFn | undefined => (typeof profile.ops === "function" ? profile.ops(name) : own(profile.ops, name));
369
+ const noTruth = (what: string): Failed => fail("#VALUE!", `The ${what} must give a truth value.`);
370
+
371
+ /** FORMULA.LIST, FORMULA.OPS and FORMULA.COMBINE: the values of parts in order, or the error. */
372
+ const valuesOf = (parts: readonly Part[], skip?: (error: EvalError) => boolean): { readonly values: readonly (unknown | typeof SKIP)[] } | Failed => {
373
+ const values: unknown[] = [];
374
+ const errors: EvalError[] = [];
375
+ for (const part of parts) {
376
+ const result = part.evaluate();
377
+ if (result.ok) values.push(result.value);
378
+ else if (skip?.(result.error) === true) values.push(SKIP);
379
+ else if (profile.combine === undefined) return result;
380
+ else errors.push(result.error);
381
+ }
382
+ if (errors.length === 0 || profile.combine === undefined) return { values };
383
+ return { ok: false, error: errors.length === 1 ? (errors[0] as EvalError) : profile.combine(errors) };
384
+ };
385
+
386
+ const lit: EvalFn = ({ doc }) => ok(doc.value);
387
+
388
+ const ref: EvalFn = ({ doc, parts, fill, address, context, resolve: walk }) => {
389
+ const path = doc.value;
390
+ if (!Array.isArray(path) || path.length === 0) return fail("#VALUE!", 'The value of ref is a path: a list that starts with a root, for example ["names", "a"].');
391
+ // FORMULA.STEPS: the step documents of the path are parts. They evaluate first, in order.
392
+ let steps: readonly unknown[] = path;
393
+ if (parts.length > 0) {
394
+ const all = valuesOfStrict(parts);
395
+ if (!("values" in all)) return all;
396
+ steps = fill(all.values) as readonly unknown[];
397
+ }
398
+ // FORMULA.LET: a path through a name whose binding failed gives the error of the binding.
399
+ const head = path[0] === "names" && typeof path[1] === "string" ? path[1] : undefined;
400
+ const bound = head === undefined ? undefined : own(context.names, head);
401
+ if (bound instanceof Held) return { ok: false, error: bound.error };
402
+ const found = walk(steps);
403
+ if (!found.ok) return fail(found.code, found.reason);
404
+ // FORMULA.ADDRESS: an address gives what the path reaches.
405
+ if (address === true) return ok(found.thing);
406
+ const reached = valueOfThing(found.thing);
407
+ if (reached instanceof Held) return { ok: false, error: reached.error };
408
+ if (profile.missing?.(reached) === true) return fail("#N/A", "The path reaches no value.");
409
+ return ok(reached);
410
+ };
411
+
412
+ const step: EvalFn = ({ doc, parts, fill }) => {
413
+ if (parts.length === 0) return ok(doc);
414
+ const all = valuesOfStrict(parts);
415
+ return "values" in all ? ok({ ...doc, value: fill(all.values) as Json }) : all;
416
+ };
417
+
418
+ const record: EvalFn = ({ parts }) => {
419
+ const all = valuesOf(parts, profile.omit);
420
+ if (!("values" in all)) return all;
421
+ const out: Record<string, unknown> = {};
422
+ parts.forEach((part, index) => {
423
+ const value = all.values[index];
424
+ if (value !== SKIP) setField(out, String(part.path[0]), value);
425
+ });
426
+ return ok(out);
427
+ };
428
+
429
+ const list: EvalFn = ({ parts }) => {
430
+ const all = valuesOf(parts);
431
+ return "values" in all ? ok([...all.values]) : all;
432
+ };
433
+
434
+ const letIn: EvalFn = ({ parts, context }) => {
435
+ const names: Record<string, unknown> = {};
436
+ for (const key of Object.keys(context.names)) setField(names, key, context.names[key]);
437
+ for (const binding of partsUnder(parts, "bind")) {
438
+ const result = binding.evaluate();
439
+ setField(names, String(binding.path[1]), result.ok ? result.value : new Held(result.error));
440
+ }
441
+ return (partNamed(parts, "body") as Part).evaluate({ ...context, names });
442
+ };
443
+
444
+ const fn: EvalFn = ({ data, parts, context }) => {
445
+ const params = isRecord(data) ? data["params"] : undefined;
446
+ if (!Array.isArray(params) || !params.every((param) => typeof param === "string") || new Set(params).size !== params.length) {
447
+ return fail("#VALUE!", "The params of fn are a list of different texts.");
448
+ }
449
+ const names = params as readonly string[];
450
+ const body = partNamed(parts, "body") as Part;
451
+ const closure = (...args: readonly unknown[]): unknown => {
452
+ const scope: Record<string, unknown> = {};
453
+ for (const key of Object.keys(context.names)) setField(scope, key, context.names[key]);
454
+ names.forEach((name, index) => setField(scope, name, args[index]));
455
+ const result = body.evaluate({ ...context, names: scope });
456
+ if (!result.ok) throw new Failure(result.error);
457
+ return result.value;
458
+ };
459
+ Object.defineProperty(closure, CLOSURE, { value: { params: names } });
460
+ return ok(closure);
461
+ };
462
+
463
+ const call: EvalFn = ({ parts }) => {
464
+ const target = (partNamed(parts, "fn") as Part).evaluate();
465
+ if (!target.ok) return target;
466
+ if (typeof target.value !== "function") return fail("#VALUE!", "The fn of call must give a function.");
467
+ const all = valuesOfStrict(partsUnder(parts, "args"));
468
+ if (!("values" in all)) return all;
469
+ try {
470
+ return ok((target.value as (...items: unknown[]) => unknown)(...all.values));
471
+ } catch (error) {
472
+ return thrown(error, "the function of call");
473
+ }
474
+ };
475
+
476
+ const choice: EvalFn = ({ parts }) => {
477
+ const test = (partNamed(parts, "test") as Part).evaluate();
478
+ if (!test.ok) return test;
479
+ const truth = truthy(test.value);
480
+ if (truth === undefined) return noTruth("test of if");
481
+ if (truth) return (partNamed(parts, "then") as Part).evaluate();
482
+ const other = partNamed(parts, "else");
483
+ if (other !== undefined) return other.evaluate();
484
+ return ok(Object.hasOwn(profile, "otherwise") ? profile.otherwise : null);
485
+ };
486
+
487
+ /** FORMULA.LOGIC: `and` stops at the first false term, and `or` at the first true term. */
488
+ const logic =
489
+ (stopAt: boolean, what: string): EvalFn =>
490
+ ({ parts }) => {
491
+ let last: Result = ok(!stopAt);
492
+ for (const part of parts) {
493
+ last = part.evaluate();
494
+ if (!last.ok) return last;
495
+ const truth = truthy(last.value);
496
+ if (truth === undefined) return noTruth(what);
497
+ if (truth === stopAt) return profile.logic === "value" ? last : ok(stopAt);
498
+ }
499
+ return profile.logic === "value" ? last : ok(!stopAt);
500
+ };
501
+
502
+ const op: EvalFn = ({ typePath, parts }) => {
503
+ const name = typePath.slice(1).join(".");
504
+ const lazy = own(profile.lazy, name);
505
+ if (lazy !== undefined) {
506
+ try {
507
+ return lazy(parts.map((part) => () => part.evaluate()));
508
+ } catch (error) {
509
+ return thrown(error, `the operation ${name}`);
510
+ }
511
+ }
512
+ const apply = opOf(name) as ((...args: unknown[]) => unknown) | undefined;
513
+ if (apply === undefined) return fail("#NAME?", `The language has no operation ${name}.`);
514
+ const all = valuesOf(parts);
515
+ if (!("values" in all)) return all;
516
+ try {
517
+ const out = apply(...all.values);
518
+ return profile.check?.(out, name) ?? ok(out);
519
+ } catch (error) {
520
+ return thrown(error, `the operation ${name}`);
521
+ }
522
+ };
523
+
524
+ const ext: EvalFn = ({ typePath, doc, context }) => {
525
+ const name = typePath.slice(1).join(".");
526
+ const handler = own(profile.ext, name) ?? own(STD_EXT, name);
527
+ if (handler === undefined) return fail("#NAME?", `The language has no extension ${name}.`);
528
+ try {
529
+ return handler(doc.value, context);
530
+ } catch (error) {
531
+ return thrown(error, `the extension ${name}`);
532
+ }
533
+ };
534
+
535
+ return {
536
+ evaluators: {
537
+ "formula.lit": lit,
538
+ "formula.ref": ref,
539
+ "formula.step": step,
540
+ "formula.record": record,
541
+ "formula.list": list,
542
+ "formula.let": letIn,
543
+ "formula.fn": fn,
544
+ "formula.call": call,
545
+ "formula.if": choice,
546
+ "formula.and": logic(false, "term of and"),
547
+ "formula.or": logic(true, "term of or"),
548
+ "formula.op": op,
549
+ "formula.ext": ext,
550
+ },
551
+ };
552
+ }
553
+
554
+ /** The mark of a field that the profile leaves out of a record (FORMULA.RECORD). */
555
+ const SKIP: unique symbol = Symbol("skip");
556
+
557
+ /** The values of parts in order. The first error stops them, also with `combine` (FORMULA.STEPS, FORMULA.FN). */
558
+ function valuesOfStrict(parts: readonly Part[]): { readonly values: readonly unknown[] } | Failed {
559
+ const values: unknown[] = [];
560
+ for (const part of parts) {
561
+ const result = part.evaluate();
562
+ if (!result.ok) return result;
563
+ values.push(result.value);
564
+ }
565
+ return { values };
566
+ }
package/src/static.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Envelope, type Json, type Key, isEnvelope, isKey, isRecord, typeName } from "./doc.ts";
2
2
  import { type Kind, type NamesSource, type ScopeRule, type World, lookupKind } from "./world.ts";
3
3
  import { ask } from "./ladder.ts";
4
- import { type PartSlot, pathMatches, splitParts } from "./parts.ts";
4
+ import { type PartSlot, partsOf, pathMatches } from "./parts.ts";
5
5
  import { type Cap, type Root, ROOTS, isRef } from "./deref.ts";
6
6
  import type { ErrorCode } from "./errors.ts";
7
7
  import { type Machine, partCaps, permits } from "./evaluate.ts";
@@ -234,37 +234,32 @@ export function walk(world: World, doc: unknown, visit: (visit: DocVisit) => voi
234
234
  ];
235
235
  while (stack.length > 0) {
236
236
  const item = stack.pop() as Item;
237
- const envelope = isEnvelope(item.value) ? (item.value as Envelope) : undefined;
238
- if (envelope === undefined && item.declared === undefined) {
239
- options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code: "#PART?", reason: "The value has no envelope, and its position declares no type." });
237
+ const read = partsOf(world, item.value, item.declared);
238
+ if (!read.ok) {
239
+ options.problem?.({ place: keysOf(item.place), at: keysOf(item.at), code: read.code, reason: read.reason });
240
240
  continue;
241
241
  }
242
- const doc: Envelope = envelope ?? { type: item.declared ?? "?", value: item.value as Json };
243
- const found = lookupKind(world, doc.type);
244
- const kind = found.ok ? found.kind : undefined;
242
+ const { doc, kind, split } = read;
245
243
  const here = new Visit(
246
244
  item.place,
247
245
  item.at,
248
246
  doc,
249
- envelope === undefined,
247
+ read.declared,
250
248
  item.depth,
251
249
  item.names,
252
250
  item.later,
253
251
  item.slot,
254
252
  item.parent,
255
253
  kind,
256
- found.ok ? found.path : undefined,
254
+ read.typePath,
257
255
  item.address ? true : undefined,
258
256
  item.caps,
259
257
  );
260
258
  const answer = visit(here);
261
259
  if (answer === "skip" || kind === undefined) continue;
262
- const split = splitParts(kind.parts, doc.value);
263
- const base: Trail | undefined = envelope === undefined ? item.at : { up: item.at, keys: ["value"] };
260
+ const base: Trail | undefined = read.declared ? item.at : { up: item.at, keys: ["value"] };
264
261
  if (!split.ok) {
265
- const at = split.at.length === 0 ? "" : ` (at ${split.at.join(".")})`;
266
- const where = keysOf({ up: base, keys: split.at });
267
- options.problem?.({ place: keysOf(item.place), at: where, code: "#VALUE!", reason: `${split.reason}${at} The kind is ${typeName(doc.type)}.` });
262
+ options.problem?.({ place: keysOf(item.place), at: keysOf({ up: base, keys: split.at }), code: "#VALUE!", reason: split.reason });
268
263
  continue;
269
264
  }
270
265
  const { sees, later } = scopesOf(kind.scope, doc.value, split.parts);
@@ -424,7 +419,8 @@ function capReason(v: DocVisit, kind: readonly Key[]): string {
424
419
  * - `#PART?`: a value without a type.
425
420
  * - `#DEPTH!`: too much nesting.
426
421
  * - `#REF!` and `#STEP?`: a bad path.
427
- * - `#NAME?`: a name that nothing binds.
422
+ * - `#NAME?`: a name that nothing binds. With the hook `known` of the machine, also a name that the language does not
423
+ * have, for example an operation.
428
424
  * - `#CAP!`: an effect that the capabilities of its document do not permit. A kind around the document can narrow them.
429
425
  *
430
426
  * @param machine - The world and the code of the evaluators.
@@ -455,6 +451,10 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
455
451
  const name = answer.evaluator;
456
452
  if (name === undefined || (!has(machine.evaluators, name) && !has(machine.runners, name))) {
457
453
  issues.push({ code: "#NOEVAL", place: v.place, reason: `No rung gives an evaluator for ${typeName(v.doc.type)}.`, at: v.at });
454
+ } else if (machine.known !== undefined) {
455
+ // The evaluator gives #NAME? in a run for a name that the language does not have, for example an operation.
456
+ const known = machine.known(v.typePath ?? [], name);
457
+ if (known !== true) issues.push({ code: "#NAME?", place: v.place, reason: known, at: v.at });
458
458
  }
459
459
  const caps = v.caps;
460
460
  if (caps !== undefined && v.kind?.effects !== undefined) {
@@ -474,7 +474,8 @@ export function check(machine: Machine, doc: unknown, options: CheckOptions = {}
474
474
  const stepAt = (index: number): readonly Key[] => [...v.at, "value", ...(index < path.length ? [index] : [])];
475
475
  if (typeof root !== "string" || !(ROOTS as readonly string[]).includes(root)) {
476
476
  issues.push({ code: "#REF!", place: v.place, reason: `A path starts with a root: ${ROOTS.join(", ")}.`, at: stepAt(0) });
477
- return "skip";
477
+ // The step documents of the path are parts, and they evaluate before the path, thus the walk visits them.
478
+ return undefined;
478
479
  }
479
480
  if (!roots.includes(root)) {
480
481
  issues.push({ code: "#REF!", place: v.place, reason: `The root ${root} is not open here. The open roots are ${roots.join(", ")}.`, at: stepAt(0) });