@venn-lang/runtime 0.1.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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3314 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/src/check/check-calls.ts +82 -0
- package/src/check/check-deco-body.ts +77 -0
- package/src/check/check-document.ts +106 -0
- package/src/check/check-env.ts +96 -0
- package/src/check/check-fragment-call.ts +27 -0
- package/src/check/check-imports.ts +82 -0
- package/src/check/check-interpolation.ts +58 -0
- package/src/check/check-options.ts +18 -0
- package/src/check/check-uncalled.ts +42 -0
- package/src/check/check.types.ts +30 -0
- package/src/check/index.ts +4 -0
- package/src/context/action-context.ts +20 -0
- package/src/context/index.ts +1 -0
- package/src/decorators/builtin-decorators.ts +87 -0
- package/src/decorators/create-decorator-source.ts +25 -0
- package/src/decorators/index.ts +2 -0
- package/src/emit/create-emitter.ts +16 -0
- package/src/emit/emitter.types.ts +6 -0
- package/src/emit/index.ts +3 -0
- package/src/emit/run-id.ts +11 -0
- package/src/eventsink/event-sink.port.ts +13 -0
- package/src/eventsink/event-sink.types.ts +11 -0
- package/src/eventsink/index.ts +4 -0
- package/src/eventsink/memory-sink.ts +17 -0
- package/src/eventsink/ndjson-sink.ts +14 -0
- package/src/index.ts +14 -0
- package/src/ports/create-port-resolver.ts +39 -0
- package/src/ports/index.ts +2 -0
- package/src/ports/port-resolver.types.ts +12 -0
- package/src/registry/build-registry.ts +76 -0
- package/src/registry/index.ts +2 -0
- package/src/registry/registry.types.ts +24 -0
- package/src/run/create-runner.ts +103 -0
- package/src/run/index.ts +4 -0
- package/src/run/resolve-imports.ts +166 -0
- package/src/run/runner.types.ts +67 -0
- package/src/scheduler/absorb-exit.ts +19 -0
- package/src/scheduler/aliases.ts +44 -0
- package/src/scheduler/annotations.ts +48 -0
- package/src/scheduler/base-scope.ts +25 -0
- package/src/scheduler/bind-globals.ts +55 -0
- package/src/scheduler/bind-imports.ts +130 -0
- package/src/scheduler/bind-namespaces.ts +61 -0
- package/src/scheduler/bind-prelude.ts +11 -0
- package/src/scheduler/block-plan.ts +66 -0
- package/src/scheduler/branch-engine.ts +13 -0
- package/src/scheduler/call-params.ts +140 -0
- package/src/scheduler/cleanup.types.ts +18 -0
- package/src/scheduler/collect.ts +60 -0
- package/src/scheduler/concurrency.ts +16 -0
- package/src/scheduler/create-cleanup-list.ts +17 -0
- package/src/scheduler/declared-arity.ts +13 -0
- package/src/scheduler/engine.types.ts +50 -0
- package/src/scheduler/filter.ts +5 -0
- package/src/scheduler/filter.types.ts +9 -0
- package/src/scheduler/flaky.ts +32 -0
- package/src/scheduler/flaky.types.ts +7 -0
- package/src/scheduler/index.ts +16 -0
- package/src/scheduler/invocation.ts +103 -0
- package/src/scheduler/local-call.ts +28 -0
- package/src/scheduler/node-span.ts +29 -0
- package/src/scheduler/opts-text.ts +10 -0
- package/src/scheduler/opts.ts +14 -0
- package/src/scheduler/pending.types.ts +9 -0
- package/src/scheduler/run-action.ts +163 -0
- package/src/scheduler/run-around.ts +23 -0
- package/src/scheduler/run-attempts.ts +105 -0
- package/src/scheduler/run-bindings.ts +67 -0
- package/src/scheduler/run-block.ts +65 -0
- package/src/scheduler/run-document.ts +181 -0
- package/src/scheduler/run-expect.ts +48 -0
- package/src/scheduler/run-flow.ts +69 -0
- package/src/scheduler/run-foreach.ts +148 -0
- package/src/scheduler/run-group.ts +9 -0
- package/src/scheduler/run-if.ts +21 -0
- package/src/scheduler/run-lifecycle.ts +86 -0
- package/src/scheduler/run-matcher.ts +99 -0
- package/src/scheduler/run-parallel.ts +68 -0
- package/src/scheduler/run-prologue.ts +59 -0
- package/src/scheduler/run-race.ts +20 -0
- package/src/scheduler/run-repeat.ts +54 -0
- package/src/scheduler/run-run.ts +41 -0
- package/src/scheduler/run-script.ts +48 -0
- package/src/scheduler/run-statements.ts +107 -0
- package/src/scheduler/run-step.ts +76 -0
- package/src/scheduler/run-try.ts +34 -0
- package/src/scheduler/run-while.ts +50 -0
- package/src/scheduler/script-lifecycle.ts +47 -0
- package/src/scheduler/settled.ts +20 -0
- package/src/scheduler/signals.ts +33 -0
- package/src/scheduler/target.ts +30 -0
- package/src/scheduler/unknown-option.ts +87 -0
- package/src/scope/create-scope.ts +67 -0
- package/src/scope/index.ts +2 -0
- package/src/scope/scope.types.ts +12 -0
- package/src/types/create-type-catalog.ts +70 -0
- package/src/types/index.ts +1 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Document } from "@venn-lang/core";
|
|
2
|
+
import type { Scope } from "../scope/index.js";
|
|
3
|
+
import { absorbExit } from "./absorb-exit.js";
|
|
4
|
+
import { createBaseScope } from "./base-scope.js";
|
|
5
|
+
import { bindFunctions } from "./bind-globals.js";
|
|
6
|
+
import { bindImports } from "./bind-imports.js";
|
|
7
|
+
import type { Engine } from "./engine.types.js";
|
|
8
|
+
import { runPrologue } from "./run-prologue.js";
|
|
9
|
+
import { registerEnding, runSetup } from "./script-lifecycle.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run a document as a program: its top-level statements, in order, top to
|
|
13
|
+
* bottom, because the file itself is the entry point. Declarations (`flow`,
|
|
14
|
+
* `fn`, `type`, …) are definitions and run only when something calls them.
|
|
15
|
+
*
|
|
16
|
+
* `setup`, `teardown` and `defer` are the program's own lifetime: `setup` runs
|
|
17
|
+
* before the first statement and the rest are handed to the host to run on the
|
|
18
|
+
* way out, because a program that serves outlives its last line.
|
|
19
|
+
*/
|
|
20
|
+
export async function runScript(engine: Engine, doc: Document): Promise<void> {
|
|
21
|
+
const base = (): Scope => createBaseScope({ engine });
|
|
22
|
+
const root = base();
|
|
23
|
+
const graph = engine.imports;
|
|
24
|
+
if (graph) bindImports({ document: doc, uri: engine.uri, scope: root, graph, base });
|
|
25
|
+
bindFunctions(doc, root);
|
|
26
|
+
engine.emitter.emit({ kind: "run.started", data: { plan: { flows: [] } } });
|
|
27
|
+
const start = engine.clock.now();
|
|
28
|
+
await absorbExit(engine, () => runProgram(engine, doc, root));
|
|
29
|
+
const durationMs = engine.clock.now() - start;
|
|
30
|
+
engine.emitter.emit({ kind: "run.finished", data: { ...engine.result, durationMs } });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The program's lifetime: its `setup`, the ending it declared, then its
|
|
35
|
+
* statements, all inside the one absorb, because `setup` is as much the program
|
|
36
|
+
* as its first line. Outside it, an `exit` in `setup` would escape past
|
|
37
|
+
* `run.finished` and leave the stream open.
|
|
38
|
+
*
|
|
39
|
+
* The ending is registered whatever happens to `setup`: a setup that exited or
|
|
40
|
+
* failed still opened what it opened, and `teardown` is how it gives it back.
|
|
41
|
+
*/
|
|
42
|
+
async function runProgram(engine: Engine, doc: Document, scope: Scope): Promise<void> {
|
|
43
|
+
const ending = registerEnding({ engine, doc, scope });
|
|
44
|
+
await runSetup({ engine, doc, scope });
|
|
45
|
+
// Collected into the ending as they are reached, so a program interrupted
|
|
46
|
+
// halfway gives back exactly what it had managed to take.
|
|
47
|
+
await runPrologue({ engine, doc, scope, into: ending.deferred });
|
|
48
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionCall,
|
|
3
|
+
CaptureStmt,
|
|
4
|
+
ExpectStmt,
|
|
5
|
+
ForEachStmt,
|
|
6
|
+
GroupDecl,
|
|
7
|
+
IfStmt,
|
|
8
|
+
LetStmt,
|
|
9
|
+
ParallelStmt,
|
|
10
|
+
RaceStmt,
|
|
11
|
+
RepeatStmt,
|
|
12
|
+
ReturnStmt,
|
|
13
|
+
RunStmt,
|
|
14
|
+
Statement,
|
|
15
|
+
StepDecl,
|
|
16
|
+
TryStmt,
|
|
17
|
+
WhileStmt,
|
|
18
|
+
} from "@venn-lang/core";
|
|
19
|
+
import type { Scope } from "../scope/index.js";
|
|
20
|
+
import type { Engine } from "./engine.types.js";
|
|
21
|
+
import type { Pending } from "./pending.types.js";
|
|
22
|
+
import { runActionStatement } from "./run-action.js";
|
|
23
|
+
import { runCapture, runLet, runReturn } from "./run-bindings.js";
|
|
24
|
+
import { runExpect } from "./run-expect.js";
|
|
25
|
+
import { runForEach } from "./run-foreach.js";
|
|
26
|
+
import { runGroup } from "./run-group.js";
|
|
27
|
+
import { runIf } from "./run-if.js";
|
|
28
|
+
import { runParallel } from "./run-parallel.js";
|
|
29
|
+
import { runRace } from "./run-race.js";
|
|
30
|
+
import { runRepeat } from "./run-repeat.js";
|
|
31
|
+
import { runRun } from "./run-run.js";
|
|
32
|
+
import { runStep } from "./run-step.js";
|
|
33
|
+
import { runTry } from "./run-try.js";
|
|
34
|
+
import { runWhile } from "./run-while.js";
|
|
35
|
+
import { BreakSignal, CancelSignal, ContinueSignal } from "./signals.js";
|
|
36
|
+
|
|
37
|
+
/** Run a block's statements in order. */
|
|
38
|
+
export async function runStatements(
|
|
39
|
+
engine: Engine,
|
|
40
|
+
stmts: readonly Statement[],
|
|
41
|
+
scope: Scope,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
for (const stmt of stmts) {
|
|
44
|
+
const pending = runStatement(engine, stmt, scope);
|
|
45
|
+
if (pending) await pending;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Dispatch one statement to its handler. This is the single boundary where a
|
|
51
|
+
* cancelled `race` branch stops advancing.
|
|
52
|
+
*
|
|
53
|
+
* Switched on `$type`, not on the `isXxx` guards: each guard asks Langium's
|
|
54
|
+
* reflection whether one type is a subtype of another, and a chain of them costs
|
|
55
|
+
* a double-digit share of a large `forEach`. `$type` is a plain string every
|
|
56
|
+
* node carries, so one switch answers what thirteen questions would.
|
|
57
|
+
*/
|
|
58
|
+
export function runStatement(engine: Engine, stmt: Statement, scope: Scope): Pending {
|
|
59
|
+
if (engine.signal?.aborted) throw new CancelSignal();
|
|
60
|
+
switch (stmt.$type) {
|
|
61
|
+
case "LetStmt":
|
|
62
|
+
return runLet(engine, stmt as LetStmt, scope);
|
|
63
|
+
case "ActionCall":
|
|
64
|
+
return runActionStatement(engine, stmt as ActionCall, scope);
|
|
65
|
+
case "ExpectStmt":
|
|
66
|
+
return runExpect(engine, stmt as ExpectStmt, scope);
|
|
67
|
+
case "IfStmt":
|
|
68
|
+
return runIf(engine, stmt as IfStmt, scope);
|
|
69
|
+
case "StepDecl":
|
|
70
|
+
return runStep(engine, stmt as StepDecl, scope);
|
|
71
|
+
case "GroupDecl":
|
|
72
|
+
return runGroup(engine, stmt as GroupDecl, scope);
|
|
73
|
+
default:
|
|
74
|
+
return control(engine, stmt, scope);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Loops, concurrency and the control-flow signals: the rarer half. */
|
|
79
|
+
function control(engine: Engine, stmt: Statement, scope: Scope): Pending {
|
|
80
|
+
switch (stmt.$type) {
|
|
81
|
+
case "ForEachStmt":
|
|
82
|
+
return runForEach(engine, stmt as ForEachStmt, scope);
|
|
83
|
+
case "RepeatStmt":
|
|
84
|
+
return runRepeat(engine, stmt as RepeatStmt, scope);
|
|
85
|
+
case "WhileStmt":
|
|
86
|
+
return runWhile(engine, stmt as WhileStmt, scope);
|
|
87
|
+
case "ParallelStmt":
|
|
88
|
+
return runParallel(engine, stmt as ParallelStmt, scope);
|
|
89
|
+
case "RaceStmt":
|
|
90
|
+
return runRace(engine, stmt as RaceStmt, scope);
|
|
91
|
+
case "TryStmt":
|
|
92
|
+
return runTry(engine, stmt as TryStmt, scope);
|
|
93
|
+
case "RunStmt":
|
|
94
|
+
return runRun(engine, stmt as RunStmt, scope);
|
|
95
|
+
default:
|
|
96
|
+
return leaf(stmt, scope);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function leaf(stmt: Statement, scope: Scope): Pending {
|
|
101
|
+
if (stmt.$type === "CaptureStmt") return runCapture(stmt as CaptureStmt, scope);
|
|
102
|
+
if (stmt.$type === "ReturnStmt") return runReturn(stmt as ReturnStmt, scope);
|
|
103
|
+
if (stmt.$type === "BreakStmt") throw new BreakSignal();
|
|
104
|
+
if (stmt.$type === "ContinueStmt") throw new ContinueSignal();
|
|
105
|
+
// LifecycleDecl (on/defer) is handled by runBlock / runFlow, not here.
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Block, isBlock, isStepDecl, type StepDecl } from "@venn-lang/core";
|
|
2
|
+
import type { Scope } from "../scope/index.js";
|
|
3
|
+
import { hasAnnotation, readLock } from "./annotations.js";
|
|
4
|
+
import type { Engine } from "./engine.types.js";
|
|
5
|
+
import { matchesTitle } from "./filter.js";
|
|
6
|
+
import { recordFlaky } from "./flaky.js";
|
|
7
|
+
import { runAround } from "./run-around.js";
|
|
8
|
+
import { runWithAnnotations, withLock } from "./run-attempts.js";
|
|
9
|
+
import { runBlock } from "./run-block.js";
|
|
10
|
+
import { isControlSignal } from "./signals.js";
|
|
11
|
+
|
|
12
|
+
/** Run a step honouring @skip/@only, @lock, @timeout and @retry; its bindings are step-local. */
|
|
13
|
+
export async function runStep(engine: Engine, step: StepDecl, parent: Scope): Promise<void> {
|
|
14
|
+
if (!included(engine, step)) return;
|
|
15
|
+
engine.emitter.emit({ kind: "step.started", data: { title: step.title } });
|
|
16
|
+
const before = engine.result.failed;
|
|
17
|
+
const scope = parent.child();
|
|
18
|
+
try {
|
|
19
|
+
await execute(engine, step, { parent, scope });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (!isControlSignal(error)) finished(engine, step.title, "failed");
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
recordFlaky(engine, step, before);
|
|
25
|
+
finished(engine, step.title, engine.result.failed > before ? "failed" : "passed");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The gate `selectFlows` puts on a flow, put on a step: `@only` focuses, `@skip`
|
|
30
|
+
* drops, and only then the `--step` title filter.
|
|
31
|
+
*
|
|
32
|
+
* A step is focused among the steps it stands with, meaning the block it was
|
|
33
|
+
* written in, the way a flow is focused among the flows of its document.
|
|
34
|
+
*/
|
|
35
|
+
function included(engine: Engine, step: StepDecl): boolean {
|
|
36
|
+
if (hasAnnotation(step, "skip")) return false;
|
|
37
|
+
if (focusing(step.$container) && !hasAnnotation(step, "only")) return false;
|
|
38
|
+
return matchesTitle(step.title, engine.filter.step);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Whether any step in this block asked to be the only one that runs. */
|
|
42
|
+
const focused = new WeakMap<Block, boolean>();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Answered once per block: the source cannot change mid-run, and a step inside a
|
|
46
|
+
* `forEach` would otherwise re-read its siblings on every pass.
|
|
47
|
+
*/
|
|
48
|
+
function focusing(container: unknown): boolean {
|
|
49
|
+
if (!isBlock(container)) return false;
|
|
50
|
+
const known = focused.get(container);
|
|
51
|
+
if (known !== undefined) return known;
|
|
52
|
+
const found = container.stmts.some((stmt) => isStepDecl(stmt) && hasAnnotation(stmt, "only"));
|
|
53
|
+
focused.set(container, found);
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function execute(
|
|
58
|
+
engine: Engine,
|
|
59
|
+
step: StepDecl,
|
|
60
|
+
scopes: { parent: Scope; scope: Scope },
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
return withLock(engine, readLock(step), () =>
|
|
63
|
+
runWithAnnotations({
|
|
64
|
+
engine,
|
|
65
|
+
node: step,
|
|
66
|
+
scope: scopes.parent,
|
|
67
|
+
title: step.title,
|
|
68
|
+
// The caller wants a promise; a block that never suspended returns none.
|
|
69
|
+
run: () => runAround(step, () => runBlock(engine, step.body, scopes.scope)),
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function finished(engine: Engine, title: string, status: "passed" | "failed"): void {
|
|
75
|
+
engine.emitter.emit({ kind: "step.finished", data: { title, status } });
|
|
76
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TryStmt } from "@venn-lang/core";
|
|
2
|
+
import type { Scope } from "../scope/index.js";
|
|
3
|
+
import type { Engine } from "./engine.types.js";
|
|
4
|
+
import { runBlock } from "./run-block.js";
|
|
5
|
+
import { isControlSignal } from "./signals.js";
|
|
6
|
+
|
|
7
|
+
/** `try { } catch e { } finally { }`: control signals pass through, errors go to catch. */
|
|
8
|
+
export async function runTry(engine: Engine, stmt: TryStmt, scope: Scope): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
await runBlock(engine, stmt.body, scope.child());
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (isControlSignal(error)) throw error;
|
|
13
|
+
await runCatch({ engine, stmt, scope, error });
|
|
14
|
+
} finally {
|
|
15
|
+
if (stmt.finalizer) await runBlock(engine, stmt.finalizer, scope.child());
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function runCatch(args: {
|
|
20
|
+
engine: Engine;
|
|
21
|
+
stmt: TryStmt;
|
|
22
|
+
scope: Scope;
|
|
23
|
+
error: unknown;
|
|
24
|
+
}): Promise<void> {
|
|
25
|
+
if (!args.stmt.handler) return;
|
|
26
|
+
const child = args.scope.child();
|
|
27
|
+
if (args.stmt.error) child.set(args.stmt.error, toErrorValue(args.error));
|
|
28
|
+
await runBlock(args.engine, args.stmt.handler, child);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toErrorValue(error: unknown): { message: string; code: string } {
|
|
32
|
+
const e = error as { message?: string; code?: string };
|
|
33
|
+
return { message: e?.message ?? String(error), code: e?.code ?? "VN7000" };
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildProblem,
|
|
3
|
+
CODES,
|
|
4
|
+
evaluate,
|
|
5
|
+
ProblemError,
|
|
6
|
+
truthy,
|
|
7
|
+
type WhileStmt,
|
|
8
|
+
} from "@venn-lang/core";
|
|
9
|
+
import type { Scope } from "../scope/index.js";
|
|
10
|
+
import type { Engine } from "./engine.types.js";
|
|
11
|
+
import { nodeSpan } from "./node-span.js";
|
|
12
|
+
import { runBlock } from "./run-block.js";
|
|
13
|
+
import { settle } from "./settled.js";
|
|
14
|
+
import { BreakSignal, ContinueSignal } from "./signals.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Every `while` has an inherited timeout in the language; this is the hard cap
|
|
18
|
+
* that stands in for it until timeouts are wired.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_ITERATIONS = 100_000;
|
|
21
|
+
|
|
22
|
+
/** `while cond { … }`: loop while the condition is truthy. */
|
|
23
|
+
export async function runWhile(engine: Engine, stmt: WhileStmt, scope: Scope): Promise<void> {
|
|
24
|
+
let guard = 0;
|
|
25
|
+
while (truthy(await settle(evaluate(stmt.cond, scope)))) {
|
|
26
|
+
if (++guard > MAX_ITERATIONS) throw neverFinished(engine, stmt);
|
|
27
|
+
try {
|
|
28
|
+
await runBlock(engine, stmt.body, scope.child());
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof BreakSignal) break;
|
|
31
|
+
if (error instanceof ContinueSignal) continue;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reaching the cap is not the loop finishing, so it is raised rather than
|
|
39
|
+
* returned: stopping at the limit in silence would report a pass.
|
|
40
|
+
*/
|
|
41
|
+
function neverFinished(engine: Engine, stmt: WhileStmt): ProblemError {
|
|
42
|
+
return new ProblemError(
|
|
43
|
+
buildProblem({
|
|
44
|
+
spec: CODES.VN8002_LOOP_LIMIT,
|
|
45
|
+
span: nodeSpan(stmt, engine.uri),
|
|
46
|
+
title: `This while loop ran ${MAX_ITERATIONS} times and its condition was still true.`,
|
|
47
|
+
help: "Change something the condition reads inside the loop, or use `repeat n` to bound it.",
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Document } from "@venn-lang/core";
|
|
2
|
+
import type { Scope } from "../scope/index.js";
|
|
3
|
+
import { collectHooks } from "./collect.js";
|
|
4
|
+
import type { Engine } from "./engine.types.js";
|
|
5
|
+
import { runHooks } from "./run-lifecycle.js";
|
|
6
|
+
import type { Teardown } from "./run-prologue.js";
|
|
7
|
+
import { runTeardowns } from "./run-prologue.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The program's `setup`, run before its first statement.
|
|
11
|
+
*
|
|
12
|
+
* It reads what the file declared, its functions, rather than what its
|
|
13
|
+
* statements bind, because none of them has run yet. A `setup` that wants a
|
|
14
|
+
* connection opens one.
|
|
15
|
+
*/
|
|
16
|
+
export async function runSetup(args: {
|
|
17
|
+
engine: Engine;
|
|
18
|
+
doc: Document;
|
|
19
|
+
scope: Scope;
|
|
20
|
+
}): Promise<void> {
|
|
21
|
+
await runHooks({ engine: args.engine, hooks: collectHooks(args.doc).setup, scope: args.scope });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What the program will do on the way out, filled in as the program runs. */
|
|
25
|
+
export interface Ending {
|
|
26
|
+
/** Cleanups the statements deferred, in the order they were written. */
|
|
27
|
+
readonly deferred: Teardown[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Say how the program ends, before it starts.
|
|
32
|
+
*
|
|
33
|
+
* Registered up front because the ending has to be in place for an interrupt
|
|
34
|
+
* that arrives mid-statement, while what it runs is decided later, as the
|
|
35
|
+
* statements defer things. One entry rather than one per `defer`, so the order
|
|
36
|
+
* is stated here instead of falling out of a stack: `teardown` first, while the
|
|
37
|
+
* connections it needs are still open, then the deferred work in reverse.
|
|
38
|
+
*/
|
|
39
|
+
export function registerEnding(args: { engine: Engine; doc: Document; scope: Scope }): Ending {
|
|
40
|
+
const ending: Ending = { deferred: [] };
|
|
41
|
+
const teardown = collectHooks(args.doc).teardown;
|
|
42
|
+
args.engine.cleanup.add(async () => {
|
|
43
|
+
await runHooks({ engine: args.engine, hooks: teardown, scope: args.scope });
|
|
44
|
+
await runTeardowns(ending.deferred);
|
|
45
|
+
});
|
|
46
|
+
return ending;
|
|
47
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether evaluating an expression produced something still running.
|
|
3
|
+
*
|
|
4
|
+
* Expressions compile synchronously, so a plugin verb called from inside one
|
|
5
|
+
* hands back the promise it is running on rather than its result. Statement
|
|
6
|
+
* position is already asynchronous, so it can wait, and `let id = newId()` binds
|
|
7
|
+
* the id instead of `[object Promise]`.
|
|
8
|
+
*
|
|
9
|
+
* Callers branch on this rather than awaiting unconditionally: `await` on a
|
|
10
|
+
* value that is not a promise still costs a turn of the event loop, and a
|
|
11
|
+
* `forEach` over 50k items reads one expression per iteration.
|
|
12
|
+
*/
|
|
13
|
+
export function isPending(value: unknown): value is Promise<unknown> {
|
|
14
|
+
return value instanceof Promise;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Wait for a value only if there is something to wait for. */
|
|
18
|
+
export async function settle<T>(value: T | Promise<T>): Promise<T> {
|
|
19
|
+
return isPending(value) ? ((await value) as T) : (value as T);
|
|
20
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Control-flow signals thrown to unwind loops and fragments. Deliberately not
|
|
2
|
+
// Errors, so a `try/catch` in a flow re-throws them instead of treating them as
|
|
3
|
+
// failures.
|
|
4
|
+
|
|
5
|
+
export class BreakSignal {}
|
|
6
|
+
export class ContinueSignal {}
|
|
7
|
+
export class ReturnSignal {
|
|
8
|
+
constructor(readonly value: unknown) {}
|
|
9
|
+
}
|
|
10
|
+
/** Thrown at a statement boundary when the branch's race has already been won. */
|
|
11
|
+
export class CancelSignal {}
|
|
12
|
+
/**
|
|
13
|
+
* Thrown by `exit`, carrying the code the host should leave with.
|
|
14
|
+
*
|
|
15
|
+
* A signal and not an error: a run that exits has not failed. The code it
|
|
16
|
+
* carries is the whole verdict, and `exit 0` is a clean ending, which an error
|
|
17
|
+
* unwinding as a failure could not express.
|
|
18
|
+
*/
|
|
19
|
+
export class ExitSignal {
|
|
20
|
+
constructor(readonly code: number) {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ControlSignal = BreakSignal | ContinueSignal | ReturnSignal | CancelSignal | ExitSignal;
|
|
24
|
+
|
|
25
|
+
export function isControlSignal(value: unknown): value is ControlSignal {
|
|
26
|
+
return (
|
|
27
|
+
value instanceof BreakSignal ||
|
|
28
|
+
value instanceof ContinueSignal ||
|
|
29
|
+
value instanceof ReturnSignal ||
|
|
30
|
+
value instanceof CancelSignal ||
|
|
31
|
+
value instanceof ExitSignal
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prelude verbs, callable without `use` (§12). `print` writes to the console,
|
|
3
|
+
* `log` records into the event stream (what a reporter and a test see), and the
|
|
4
|
+
* rest steer the run. Pure prelude *values* (`len`, `range`, `pretty`…) are not
|
|
5
|
+
* here: they live in the root scope, so they work in any expression.
|
|
6
|
+
*/
|
|
7
|
+
export const PRELUDE: ReadonlySet<string> = new Set([
|
|
8
|
+
"print",
|
|
9
|
+
"log",
|
|
10
|
+
"wait",
|
|
11
|
+
"skip",
|
|
12
|
+
"fail",
|
|
13
|
+
"exit",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/** Split an action target `namespace.action` into parts (no dot ⇒ namespace only). */
|
|
17
|
+
export function splitTarget(target: string): { namespace: string; name: string } {
|
|
18
|
+
const dot = target.indexOf(".");
|
|
19
|
+
if (dot < 0) return { namespace: target, name: "" };
|
|
20
|
+
return { namespace: target.slice(0, dot), name: target.slice(dot + 1) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Split a target, mapping a `use "…" as h` alias back to its real namespace. */
|
|
24
|
+
export function resolveTarget(
|
|
25
|
+
target: string,
|
|
26
|
+
aliases: ReadonlyMap<string, string>,
|
|
27
|
+
): { namespace: string; name: string } {
|
|
28
|
+
const { namespace, name } = splitTarget(target);
|
|
29
|
+
return { namespace: aliases.get(namespace) ?? namespace, name };
|
|
30
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { buildProblem, CODES, type MapEntry, type MapLit, type Problem } from "@venn-lang/core";
|
|
2
|
+
import { type ParamSpec, paramSpecs } from "@venn-lang/sdk";
|
|
3
|
+
import { nodeSpan } from "./node-span.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The keys an options map spells that the schema behind it never declared,
|
|
7
|
+
* whether that schema is an action's or a matcher's.
|
|
8
|
+
*
|
|
9
|
+
* One list, two readers: the checker squiggles them before anything runs, the
|
|
10
|
+
* runtime refuses them when the line executes. Same code, same words, so a typo
|
|
11
|
+
* cannot read one way in the editor and another in the terminal.
|
|
12
|
+
*
|
|
13
|
+
* A schema that declares no keys accepts a free-form map, and nothing there is
|
|
14
|
+
* unknown: `grpc.request` takes a `z.record`, `db.seed` takes no schema at all
|
|
15
|
+
* and reads the whole map as table names.
|
|
16
|
+
*/
|
|
17
|
+
export function unknownOptions(args: {
|
|
18
|
+
opts: MapLit | undefined;
|
|
19
|
+
params: unknown;
|
|
20
|
+
uri: string;
|
|
21
|
+
}): Problem[] {
|
|
22
|
+
const specs = paramSpecs(args.params as never);
|
|
23
|
+
if (!args.opts || specs.length === 0 || welcomesMore(args.params)) return [];
|
|
24
|
+
const known = new Set(specs.map((spec) => spec.name));
|
|
25
|
+
return args.opts.entries
|
|
26
|
+
.filter((entry) => !known.has(entry.key))
|
|
27
|
+
.map((entry) => unknownOption({ entry, specs, uri: args.uri }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A schema written with a catchall (`z.looseObject`, `.catchall(…)`) names some
|
|
32
|
+
* keys and takes the rest as well. A key it never named is still a key it asked
|
|
33
|
+
* to receive, so there is nothing to report. Read structurally, past the
|
|
34
|
+
* wrappers `.optional()` and `.default()` add, the way {@link paramSpecs} reads
|
|
35
|
+
* the shape.
|
|
36
|
+
*/
|
|
37
|
+
function welcomesMore(params: unknown): boolean {
|
|
38
|
+
const def = (params as { def?: { catchall?: unknown; innerType?: unknown } } | undefined)?.def;
|
|
39
|
+
if (def?.catchall !== undefined) return true;
|
|
40
|
+
return def?.innerType !== undefined && welcomesMore(def.innerType);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function unknownOption(args: {
|
|
44
|
+
entry: MapEntry;
|
|
45
|
+
specs: readonly ParamSpec[];
|
|
46
|
+
uri: string;
|
|
47
|
+
}): Problem {
|
|
48
|
+
const { key } = args.entry;
|
|
49
|
+
const hint = nearest(key, args.specs);
|
|
50
|
+
const accepted = args.specs.map((spec) => spec.name).join(", ");
|
|
51
|
+
const title = hint
|
|
52
|
+
? `"${key}" is not an option here — did you mean "${hint}"?`
|
|
53
|
+
: `"${key}" is not an option here. Accepted: ${accepted}.`;
|
|
54
|
+
return buildProblem({
|
|
55
|
+
spec: CODES.VN3001_UNKNOWN_OPTION,
|
|
56
|
+
span: nodeSpan(args.entry, args.uri),
|
|
57
|
+
title,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The closest accepted key, when one is close enough to be worth suggesting. */
|
|
62
|
+
function nearest(key: string, specs: readonly ParamSpec[]): string | undefined {
|
|
63
|
+
const scored = specs
|
|
64
|
+
.map((spec) => ({ name: spec.name, distance: distance(key, spec.name) }))
|
|
65
|
+
.sort((left, right) => left.distance - right.distance)[0];
|
|
66
|
+
return scored && scored.distance <= Math.max(2, Math.floor(key.length / 2))
|
|
67
|
+
? scored.name
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Levenshtein, small enough to keep here and precise enough for a "did you mean". */
|
|
72
|
+
function distance(left: string, right: string): number {
|
|
73
|
+
let previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
|
|
74
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
75
|
+
const current = [i];
|
|
76
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
77
|
+
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
78
|
+
current[j] = Math.min(
|
|
79
|
+
(current[j - 1] ?? 0) + 1,
|
|
80
|
+
(previous[j] ?? 0) + 1,
|
|
81
|
+
(previous[j - 1] ?? 0) + cost,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
previous = current;
|
|
85
|
+
}
|
|
86
|
+
return previous[right.length] ?? 0;
|
|
87
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Cell } from "@venn-lang/core";
|
|
2
|
+
import type { Scope } from "./scope.types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A lexical scope.
|
|
6
|
+
*
|
|
7
|
+
* A class, not an object literal: a `forEach` over 50k items builds 50k scopes,
|
|
8
|
+
* and a literal allocates a closure per method for each of them. One prototype
|
|
9
|
+
* does for all.
|
|
10
|
+
*
|
|
11
|
+
* Bindings live in cells rather than as bare values, so a compiled function can
|
|
12
|
+
* address one once and read it by index on every call after. The cell exists as
|
|
13
|
+
* soon as anyone asks for it, which is what lets a recursive `fn` work: the
|
|
14
|
+
* closure asks for its own name while being built, before the binding fills it.
|
|
15
|
+
*/
|
|
16
|
+
class MapScope implements Scope {
|
|
17
|
+
private readonly vars = new Map<string, Cell>();
|
|
18
|
+
|
|
19
|
+
constructor(private readonly parent?: Scope) {}
|
|
20
|
+
|
|
21
|
+
lookup(name: string): unknown {
|
|
22
|
+
// A cell always exists once asked for, so its presence answers whether this
|
|
23
|
+
// scope binds the name. A binding legitimately holding `undefined` reads as
|
|
24
|
+
// its own value rather than falling through to the parent.
|
|
25
|
+
const found = this.vars.get(name);
|
|
26
|
+
return found === undefined ? this.parent?.lookup(name) : found.value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(name: string, value: unknown): void {
|
|
30
|
+
const own = this.vars.get(name);
|
|
31
|
+
if (own) {
|
|
32
|
+
own.value = value;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.vars.set(name, { value });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The cell holding `name`, taken from wherever in the chain already has one.
|
|
40
|
+
*
|
|
41
|
+
* A function reading a global must get the global's own cell, not a fresh
|
|
42
|
+
* local one that the binding would never fill. Only a name nobody has bound
|
|
43
|
+
* anywhere gets its cell here, where a later `set` will land.
|
|
44
|
+
*/
|
|
45
|
+
cell(name: string): Cell {
|
|
46
|
+
const own = this.vars.get(name);
|
|
47
|
+
if (own) return own;
|
|
48
|
+
const inherited = this.parent?.cell(name);
|
|
49
|
+
if (inherited) return inherited;
|
|
50
|
+
const made: Cell = { value: undefined };
|
|
51
|
+
this.vars.set(name, made);
|
|
52
|
+
return made;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
child(): Scope {
|
|
56
|
+
return new MapScope(this);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a scope; `lookup` falls back to the parent, `set` writes locally.
|
|
62
|
+
*
|
|
63
|
+
* @param parent The enclosing scope, or nothing for a root scope.
|
|
64
|
+
*/
|
|
65
|
+
export function createScope(parent?: Scope): Scope {
|
|
66
|
+
return new MapScope(parent);
|
|
67
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CellEnv } from "@venn-lang/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A lexical scope: the evaluator's env plus mutation and nesting.
|
|
5
|
+
*
|
|
6
|
+
* A `CellEnv`, so a closure defined here can address its free names once and
|
|
7
|
+
* read them by index afterwards instead of walking the chain per call.
|
|
8
|
+
*/
|
|
9
|
+
export interface Scope extends CellEnv {
|
|
10
|
+
set(name: string, value: unknown): void;
|
|
11
|
+
child(): Scope;
|
|
12
|
+
}
|