@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,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Document,
|
|
3
|
+
type FragmentDecl,
|
|
4
|
+
type ImportedDeco,
|
|
5
|
+
isDecoDecl,
|
|
6
|
+
isFragmentDecl,
|
|
7
|
+
isPackageSpecifier,
|
|
8
|
+
isUseDecl,
|
|
9
|
+
isValueImport,
|
|
10
|
+
parse,
|
|
11
|
+
} from "@venn-lang/core";
|
|
12
|
+
|
|
13
|
+
/** Read source by URI and resolve a specifier relative to a base URI. */
|
|
14
|
+
export interface ModuleIo {
|
|
15
|
+
read: (uri: string) => Promise<string>;
|
|
16
|
+
resolve: (base: string, spec: string) => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Loading a package that was installed rather than written here.
|
|
21
|
+
*
|
|
22
|
+
* Kept apart from {@link ModuleIo} because the two answer different questions:
|
|
23
|
+
* one reads `.vn` source to be parsed, the other hands back a module that is
|
|
24
|
+
* already a value. A host without one simply has no packages, which is the state
|
|
25
|
+
* of every run in a Web Worker and not an error.
|
|
26
|
+
*/
|
|
27
|
+
export interface NpmModules {
|
|
28
|
+
load(spec: string): Promise<Record<string, unknown> | undefined>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a run needs from the files it reaches: their fragments, decos and plugins. */
|
|
32
|
+
export interface ResolvedImports {
|
|
33
|
+
fragments: Map<string, FragmentDecl>;
|
|
34
|
+
/** The `pub deco`s reachable through the import graph, with the file each came from. */
|
|
35
|
+
decos: Map<string, ImportedDeco>;
|
|
36
|
+
/** Every package `use`d anywhere in the graph, so only those need loading. */
|
|
37
|
+
packages: Set<string>;
|
|
38
|
+
/**
|
|
39
|
+
* What each npm specifier loaded to, by the name that was written.
|
|
40
|
+
*
|
|
41
|
+
* Loaded here, before anything runs, because binding a scope cannot wait: by
|
|
42
|
+
* the time a name is looked up the module has to already be a value.
|
|
43
|
+
*/
|
|
44
|
+
npm: Map<string, Record<string, unknown>>;
|
|
45
|
+
/**
|
|
46
|
+
* Every module reached, parsed, keyed by its resolved URI.
|
|
47
|
+
*
|
|
48
|
+
* Carried whole rather than reduced to a list of exported names, because a
|
|
49
|
+
* `pub fn` is a closure over the file it was written in: it calls that file's
|
|
50
|
+
* private helpers and reads that file's globals. Handing over the function
|
|
51
|
+
* without the place it came from produces something that resolves and then
|
|
52
|
+
* fails on its first line.
|
|
53
|
+
*/
|
|
54
|
+
modules: Map<string, Document>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Walk the import graph from one document, parsing every `.vn` file it reaches
|
|
59
|
+
* and loading every package it names. A file already seen is skipped, so a cycle
|
|
60
|
+
* ends rather than loops. A file that cannot be read is skipped in silence:
|
|
61
|
+
* whoever tried to read it reports the failure.
|
|
62
|
+
*
|
|
63
|
+
* @param args The entry document, its URI, the source reader and the optional
|
|
64
|
+
* package loader.
|
|
65
|
+
* @returns The exported fragments and decos, the packages, the loaded npm
|
|
66
|
+
* modules and every parsed document, keyed by resolved URI.
|
|
67
|
+
*/
|
|
68
|
+
export async function resolveImports(args: {
|
|
69
|
+
document: Document;
|
|
70
|
+
uri: string;
|
|
71
|
+
io: ModuleIo;
|
|
72
|
+
/** Absent when the host has no way to load one: a Worker, most tests. */
|
|
73
|
+
npm?: NpmModules;
|
|
74
|
+
}): Promise<ResolvedImports> {
|
|
75
|
+
const found: Exports = { fragments: new Map(), decos: new Map() };
|
|
76
|
+
const packages = new Set<string>();
|
|
77
|
+
const modules = new Map<string, Document>();
|
|
78
|
+
const npm = new Map<string, Record<string, unknown>>();
|
|
79
|
+
collectUses(args.document, packages);
|
|
80
|
+
await loadInto({
|
|
81
|
+
document: args.document,
|
|
82
|
+
uri: args.uri,
|
|
83
|
+
io: args.io,
|
|
84
|
+
npmLoader: args.npm,
|
|
85
|
+
found,
|
|
86
|
+
packages,
|
|
87
|
+
modules,
|
|
88
|
+
npm,
|
|
89
|
+
seen: new Set([args.uri]),
|
|
90
|
+
});
|
|
91
|
+
return { ...found, packages, modules, npm };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Add the packages a file declares with `use` to `into`. Collected across the
|
|
96
|
+
* whole graph: a fragment imported from elsewhere calls the verbs *its* file
|
|
97
|
+
* asked for.
|
|
98
|
+
*
|
|
99
|
+
* @param document The file to read `use` declarations from.
|
|
100
|
+
* @param into The accumulating set of package specifiers, mutated in place.
|
|
101
|
+
*/
|
|
102
|
+
export function collectUses(document: Document, into: Set<string>): void {
|
|
103
|
+
for (const decl of document.imports) {
|
|
104
|
+
if (isUseDecl(decl)) into.add(decl.pkg);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What `pub` has handed over so far, filled as the graph is walked. */
|
|
109
|
+
interface Exports {
|
|
110
|
+
fragments: Map<string, FragmentDecl>;
|
|
111
|
+
decos: Map<string, ImportedDeco>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface LoadState {
|
|
115
|
+
document: Document;
|
|
116
|
+
uri: string;
|
|
117
|
+
io: ModuleIo;
|
|
118
|
+
npmLoader?: NpmModules;
|
|
119
|
+
found: Exports;
|
|
120
|
+
packages: Set<string>;
|
|
121
|
+
modules: Map<string, Document>;
|
|
122
|
+
npm: Map<string, Record<string, unknown>>;
|
|
123
|
+
seen: Set<string>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function loadInto(state: LoadState): Promise<void> {
|
|
127
|
+
for (const decl of state.document.imports) {
|
|
128
|
+
if (!isValueImport(decl)) continue;
|
|
129
|
+
if (isPackageSpecifier(decl.path)) await loadPackage(state, decl.path);
|
|
130
|
+
else await loadModule(state, decl.path);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A package that was installed, loaded once however many files name it.
|
|
136
|
+
*
|
|
137
|
+
* A host with no loader has no packages, which is the state of every run in a
|
|
138
|
+
* Web Worker. The import is left unbound and the name reports itself as unknown,
|
|
139
|
+
* rather than the load failing somewhere nobody can see.
|
|
140
|
+
*/
|
|
141
|
+
async function loadPackage(state: LoadState, spec: string): Promise<void> {
|
|
142
|
+
if (state.npm.has(spec) || !state.npmLoader) return;
|
|
143
|
+
const found = await state.npmLoader.load(spec).catch(() => undefined);
|
|
144
|
+
if (found) state.npm.set(spec, found);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function loadModule(state: LoadState, spec: string): Promise<void> {
|
|
148
|
+
const target = state.io.resolve(state.uri, spec);
|
|
149
|
+
if (state.seen.has(target)) return;
|
|
150
|
+
state.seen.add(target);
|
|
151
|
+
const source = await state.io.read(target).catch(() => undefined);
|
|
152
|
+
if (source === undefined) return;
|
|
153
|
+
const { ast } = parse(source, { uri: target });
|
|
154
|
+
state.modules.set(target, ast);
|
|
155
|
+
collectExports({ document: ast, uri: target, found: state.found });
|
|
156
|
+
collectUses(ast, state.packages);
|
|
157
|
+
await loadInto({ ...state, document: ast, uri: target });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Everything a file marked `pub`. A `deco` keeps its file: its faults are its own. */
|
|
161
|
+
function collectExports(args: { document: Document; uri: string; found: Exports }): void {
|
|
162
|
+
for (const decl of args.document.decls) {
|
|
163
|
+
if (isFragmentDecl(decl) && decl.export) args.found.fragments.set(decl.name, decl);
|
|
164
|
+
if (isDecoDecl(decl) && decl.export) args.found.decos.set(decl.name, { decl, uri: args.uri });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Host } from "@venn-lang/contracts";
|
|
2
|
+
import type { Document, FragmentDecl, ImportedDeco, Problem, RunId } from "@venn-lang/core";
|
|
3
|
+
import type { PluginDefinition } from "@venn-lang/sdk";
|
|
4
|
+
import type { EventSink } from "../eventsink/index.js";
|
|
5
|
+
import type { PortBinding, PortResolver } from "../ports/index.js";
|
|
6
|
+
import type { Registry } from "../registry/index.js";
|
|
7
|
+
import type { CleanupSink, ImportGraph, RunFilter } from "../scheduler/index.js";
|
|
8
|
+
|
|
9
|
+
/** What one run reports back to the host that asked for it. */
|
|
10
|
+
export interface RunResult {
|
|
11
|
+
run: RunId;
|
|
12
|
+
/** What the decorators refused before a statement ran. */
|
|
13
|
+
problems?: Problem[];
|
|
14
|
+
passed: number;
|
|
15
|
+
failed: number;
|
|
16
|
+
/**
|
|
17
|
+
* What `exit` asked the host to end with, absent if nothing called it. It
|
|
18
|
+
* overrides the tally on purpose: `exit 0` is a clean ending, `exit 3` is a
|
|
19
|
+
* failure the program named itself.
|
|
20
|
+
*/
|
|
21
|
+
exitCode?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The level-2 embedding API (§18): a host owns the process and drives runs. */
|
|
25
|
+
export interface Runner {
|
|
26
|
+
/** Test mode: collect and run every `flow`, reporting each. */
|
|
27
|
+
run(document: Document): Promise<RunResult>;
|
|
28
|
+
/** Script mode: execute the file's top-level statements, top to bottom. */
|
|
29
|
+
script(document: Document): Promise<RunResult>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Everything {@link Runner} needs, settled once and reused for every run. */
|
|
33
|
+
export interface RunnerArgs {
|
|
34
|
+
host: Host;
|
|
35
|
+
plugins: readonly PluginDefinition[];
|
|
36
|
+
sink: EventSink;
|
|
37
|
+
ports?: readonly PortBinding[];
|
|
38
|
+
uri?: string;
|
|
39
|
+
/** Which flows and steps to include (`--tags`, `--flow`, `--step`). */
|
|
40
|
+
filter?: RunFilter;
|
|
41
|
+
/** Stop after the first flow that fails. */
|
|
42
|
+
bail?: boolean;
|
|
43
|
+
env?: Record<string, unknown>;
|
|
44
|
+
moduleFragments?: Map<string, FragmentDecl>;
|
|
45
|
+
/**
|
|
46
|
+
* The files the import graph reached, so a `pub fn` can be called with the
|
|
47
|
+
* module it was written in around it. Without this the name resolves and the
|
|
48
|
+
* call fails on its first line.
|
|
49
|
+
*/
|
|
50
|
+
modules?: ImportGraph;
|
|
51
|
+
/** The `pub deco`s the imported files exported, so `@name` resolves across a file. */
|
|
52
|
+
moduleDecos?: Map<string, ImportedDeco>;
|
|
53
|
+
/**
|
|
54
|
+
* Where a script-mode program registers what it opened. The host owns when a
|
|
55
|
+
* program ends, so the host owns this list; without one the run keeps its own
|
|
56
|
+
* and nothing closes until the process does.
|
|
57
|
+
*/
|
|
58
|
+
cleanup?: CleanupSink;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What one `run(document)` needs: the runner's args plus the once-built services. */
|
|
62
|
+
export interface RunOnceInput {
|
|
63
|
+
args: RunnerArgs;
|
|
64
|
+
registry: Registry;
|
|
65
|
+
resolver: PortResolver;
|
|
66
|
+
document: Document;
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Engine } from "./engine.types.js";
|
|
2
|
+
import { ExitSignal } from "./signals.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run `body`, letting an `exit` end it.
|
|
6
|
+
*
|
|
7
|
+
* The exit code travels on the engine rather than on the throw, so whatever
|
|
8
|
+
* wraps this still finishes the way it always does: teardowns run, `run.finished`
|
|
9
|
+
* is emitted, and the host reads one number off the result. Any other error keeps
|
|
10
|
+
* unwinding untouched.
|
|
11
|
+
*/
|
|
12
|
+
export async function absorbExit(engine: Engine, body: () => Promise<void>): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
await body();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (!(error instanceof ExitSignal)) throw error;
|
|
17
|
+
engine.exit = error.code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Document, isLetStmt, isUseDecl } from "@venn-lang/core";
|
|
2
|
+
import type { Registry } from "../registry/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The namespaces this file actually brought in with `use`: the alias when one
|
|
6
|
+
* was given, otherwise the namespace the package contributes. A name outside
|
|
7
|
+
* this set was never imported, however well the registry knows it.
|
|
8
|
+
*/
|
|
9
|
+
export function collectNamespaces(document: Document, registry: Registry): Set<string> {
|
|
10
|
+
const names = new Set<string>();
|
|
11
|
+
for (const decl of document.imports) {
|
|
12
|
+
if (!isUseDecl(decl)) continue;
|
|
13
|
+
const bound = decl.alias ?? registry.namespaceOf(decl.pkg);
|
|
14
|
+
if (bound) names.add(bound);
|
|
15
|
+
}
|
|
16
|
+
return names;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The names this file binds at the top level.
|
|
21
|
+
*
|
|
22
|
+
* `page.click()` reads like a namespace and a verb, and is neither: it is a
|
|
23
|
+
* method on something the file holds. The registry has no schema for it, so it
|
|
24
|
+
* is known here and verified nowhere, which beats reporting it as an unknown
|
|
25
|
+
* action it plainly is not.
|
|
26
|
+
*/
|
|
27
|
+
export function collectBoundNames(document: Document): Set<string> {
|
|
28
|
+
const names = new Set<string>();
|
|
29
|
+
for (const decl of document.decls) {
|
|
30
|
+
if (isLetStmt(decl)) names.add(decl.name);
|
|
31
|
+
}
|
|
32
|
+
return names;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `use "venn/http" as h` → `{ h → "http" }`, resolved through the registry. */
|
|
36
|
+
export function collectAliases(document: Document, registry: Registry): Map<string, string> {
|
|
37
|
+
const aliases = new Map<string, string>();
|
|
38
|
+
for (const decl of document.imports) {
|
|
39
|
+
if (!isUseDecl(decl) || !decl.alias) continue;
|
|
40
|
+
const namespace = registry.namespaceOf(decl.pkg);
|
|
41
|
+
if (namespace) aliases.set(decl.alias, namespace);
|
|
42
|
+
}
|
|
43
|
+
return aliases;
|
|
44
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Annotation, readMeta } from "@venn-lang/core";
|
|
2
|
+
|
|
3
|
+
/** Any AST node that carries decorators (`@tags`, `@timeout`, `@retry`…). */
|
|
4
|
+
export interface Annotated {
|
|
5
|
+
annotations: Annotation[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** A parsed `@retry(n, { backoff, factor })` specification. */
|
|
9
|
+
export interface RetrySpec {
|
|
10
|
+
attempts: number;
|
|
11
|
+
backoffMs: number;
|
|
12
|
+
factor: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether a decorator set this flag on the node.
|
|
17
|
+
*
|
|
18
|
+
* The scheduler reads what expansion concluded, never the annotation list
|
|
19
|
+
* itself, so a decorator is understood in exactly one place.
|
|
20
|
+
*/
|
|
21
|
+
export function hasAnnotation(node: Annotated, name: string): boolean {
|
|
22
|
+
return readMeta<boolean>(node, name) === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `@timeout(90s)` → milliseconds, or undefined. */
|
|
26
|
+
export function readTimeout(node: Annotated): number | undefined {
|
|
27
|
+
return readMeta<number>(node, "timeout");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `@retry(2, { backoff: 500ms, factor: 2 })` → a retry spec, or undefined. */
|
|
31
|
+
export function readRetry(node: Annotated): RetrySpec | undefined {
|
|
32
|
+
return readMeta<RetrySpec>(node, "retry");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `@lock("orders")` → the lock name, or undefined. */
|
|
36
|
+
export function readLock(node: Annotated): string | undefined {
|
|
37
|
+
return readMeta<string>(node, "lock");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `@flaky(0.05)` → the tolerated failure ratio; bare `@flaky` tolerates all. */
|
|
41
|
+
export function readFlaky(node: Annotated): number | undefined {
|
|
42
|
+
return readMeta<number>(node, "flaky");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `@tags(smoke, critical)` → the tag names. */
|
|
46
|
+
export function readTags(node: Annotated): string[] {
|
|
47
|
+
return readMeta<string[]>(node, "tags") ?? [];
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createScope, type Scope } from "../scope/index.js";
|
|
2
|
+
import { bindNamespaces } from "./bind-namespaces.js";
|
|
3
|
+
import { bindPrelude } from "./bind-prelude.js";
|
|
4
|
+
import type { Engine } from "./engine.types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What every scope in a run starts as: the prelude, the plugin namespaces and
|
|
8
|
+
* `env`, plus the matrix variant when there is one.
|
|
9
|
+
*
|
|
10
|
+
* One place, shared by test runs and program runs, so `venn test` and `venn run`
|
|
11
|
+
* cannot drift apart. Every scope a module is read in starts here too, which is
|
|
12
|
+
* what makes an imported function see the same world its own file did.
|
|
13
|
+
*/
|
|
14
|
+
export function createBaseScope(args: {
|
|
15
|
+
engine: Engine;
|
|
16
|
+
/** The `matrix` variant, for a test run. A program has none. */
|
|
17
|
+
variant?: Record<string, unknown>;
|
|
18
|
+
}): Scope {
|
|
19
|
+
const scope = createScope();
|
|
20
|
+
bindPrelude(scope);
|
|
21
|
+
bindNamespaces({ registry: args.engine.registry, ctx: args.engine.ctx, scope });
|
|
22
|
+
scope.set("env", args.engine.env);
|
|
23
|
+
if (args.variant) scope.set("matrix", args.variant);
|
|
24
|
+
return scope;
|
|
25
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closureOfDecl,
|
|
3
|
+
type Document,
|
|
4
|
+
decorateCallable,
|
|
5
|
+
evaluate,
|
|
6
|
+
isFnDecl,
|
|
7
|
+
isLetStmt,
|
|
8
|
+
type LetStmt,
|
|
9
|
+
} from "@venn-lang/core";
|
|
10
|
+
import type { Scope } from "../scope/index.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Bind every top-level `fn` as a callable closure. Hoisted so a function can be
|
|
14
|
+
* called before its textual position, and so functions may reference each other
|
|
15
|
+
* regardless of order.
|
|
16
|
+
*
|
|
17
|
+
* A `deco` that asked to `wrap` one is honoured here, at the only moment the
|
|
18
|
+
* function becomes a value: the name a caller reaches is already the decorated
|
|
19
|
+
* one, so a middleware cannot be stepped around by calling from somewhere else.
|
|
20
|
+
*/
|
|
21
|
+
export function bindFunctions(doc: Document, scope: Scope): void {
|
|
22
|
+
for (const decl of doc.decls) {
|
|
23
|
+
if (isFnDecl(decl)) scope.set(decl.name, decorateCallable(decl, closureOfDecl(decl, scope)));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Bind top-level functions and the plain `const`/`let` globals every flow can
|
|
29
|
+
* read without anything having run yet.
|
|
30
|
+
*
|
|
31
|
+
* Only pure values. One that calls a verb is a statement, and statements run in
|
|
32
|
+
* the prologue, in order and once, before the flows: opening a database is
|
|
33
|
+
* something a program *does*, not something a name quietly is.
|
|
34
|
+
*/
|
|
35
|
+
export function bindGlobals(doc: Document, scope: Scope): void {
|
|
36
|
+
bindFunctions(doc, scope);
|
|
37
|
+
bindPlainValues(doc, scope);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The `const`/`let` globals, evaluated where they stand.
|
|
42
|
+
*
|
|
43
|
+
* Separate from the functions because the order matters across files: a function
|
|
44
|
+
* is hoisted and reads its scope when it is *called*, while a value is read now,
|
|
45
|
+
* so anything a value might depend on has to be in place first.
|
|
46
|
+
*/
|
|
47
|
+
export function bindPlainValues(doc: Document, scope: Scope): void {
|
|
48
|
+
for (const decl of doc.decls) {
|
|
49
|
+
if (isLetStmt(decl) && isPlainValue(decl)) scope.set(decl.name, evaluate(decl.value, scope));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isPlainValue(decl: LetStmt): boolean {
|
|
54
|
+
return decl.args.length === 0 && !decl.opts;
|
|
55
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Document,
|
|
3
|
+
isFnDecl,
|
|
4
|
+
isPackageSpecifier,
|
|
5
|
+
isValueImport,
|
|
6
|
+
type ValueImport,
|
|
7
|
+
} from "@venn-lang/core";
|
|
8
|
+
import type { Scope } from "../scope/index.js";
|
|
9
|
+
import { bindFunctions, bindPlainValues } from "./bind-globals.js";
|
|
10
|
+
|
|
11
|
+
/** The files an import graph reached, and how one file names another. */
|
|
12
|
+
export interface ImportGraph {
|
|
13
|
+
modules: ReadonlyMap<string, Document>;
|
|
14
|
+
resolve: (from: string, spec: string) => string;
|
|
15
|
+
/** What each npm specifier loaded to, already a value. */
|
|
16
|
+
npm?: ReadonlyMap<string, Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Bind the names a document imported, each to the value its own module made.
|
|
21
|
+
*
|
|
22
|
+
* A `pub fn` is a closure over the file it was written in (it calls that file's
|
|
23
|
+
* private helpers and reads that file's globals) so it cannot simply be lifted
|
|
24
|
+
* into the importer's scope. Each module gets a scope of its own, built the same
|
|
25
|
+
* way the entry document's is, and the importer takes only the names it asked
|
|
26
|
+
* for out of it.
|
|
27
|
+
*
|
|
28
|
+
* Three passes over every module at once, not one walk per import: two files
|
|
29
|
+
* that call each other are ordinary, and no single order resolves them. Filling
|
|
30
|
+
* every scope first and wiring afterwards has no order to get wrong, because a
|
|
31
|
+
* function captures the scope object and wiring mutates that same object.
|
|
32
|
+
*
|
|
33
|
+
* Call this before the document's own globals are bound, so a local name of the
|
|
34
|
+
* same spelling wins. That is the rule fragments already follow.
|
|
35
|
+
*/
|
|
36
|
+
export function bindImports(args: {
|
|
37
|
+
document: Document;
|
|
38
|
+
uri: string;
|
|
39
|
+
scope: Scope;
|
|
40
|
+
graph: ImportGraph;
|
|
41
|
+
/** How the scope a module is read in gets made: the same one the entry gets. */
|
|
42
|
+
base: () => Scope;
|
|
43
|
+
}): void {
|
|
44
|
+
const built = new Map<string, Scope>();
|
|
45
|
+
for (const [uri, module] of args.graph.modules) {
|
|
46
|
+
const scope = args.base();
|
|
47
|
+
built.set(uri, scope);
|
|
48
|
+
bindFunctions(module, scope);
|
|
49
|
+
}
|
|
50
|
+
for (const [uri, module] of args.graph.modules) {
|
|
51
|
+
wire({ document: module, uri, into: scopeAt(built, uri), graph: args.graph, built });
|
|
52
|
+
}
|
|
53
|
+
// Last, because a value is read where it stands: whatever it names, local
|
|
54
|
+
// function or imported one, has to be in place by now.
|
|
55
|
+
for (const [uri, module] of args.graph.modules) bindPlainValues(module, scopeAt(built, uri));
|
|
56
|
+
wire({ document: args.document, uri: args.uri, into: args.scope, graph: args.graph, built });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scopeAt(built: ReadonlyMap<string, Scope>, uri: string): Scope {
|
|
60
|
+
return built.get(uri) as Scope;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface Wiring {
|
|
64
|
+
document: Document;
|
|
65
|
+
uri: string;
|
|
66
|
+
into: Scope;
|
|
67
|
+
graph: ImportGraph;
|
|
68
|
+
built: ReadonlyMap<string, Scope>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function wire(args: Wiring): void {
|
|
72
|
+
for (const decl of args.document.imports) {
|
|
73
|
+
if (!isValueImport(decl)) continue;
|
|
74
|
+
if (isPackageSpecifier(decl.path)) {
|
|
75
|
+
takePackage({ decl, graph: args.graph, into: args.into });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const from = args.graph.resolve(args.uri, decl.path);
|
|
79
|
+
const module = args.graph.modules.get(from);
|
|
80
|
+
const source = args.built.get(from);
|
|
81
|
+
if (module && source) take({ decl, module, source, into: args.into });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The names an installed package published.
|
|
87
|
+
*
|
|
88
|
+
* Everything it exports is fair game: a package has no `pub`, its exports *are*
|
|
89
|
+
* what it made public, so unlike a `.vn` module there is nothing to filter
|
|
90
|
+
* against. `default` is bound too, under whichever name the import gave it,
|
|
91
|
+
* because that is the only name it has here.
|
|
92
|
+
*/
|
|
93
|
+
function takePackage(args: { decl: ValueImport; graph: ImportGraph; into: Scope }): void {
|
|
94
|
+
const module = args.graph.npm?.get(args.decl.path);
|
|
95
|
+
if (!module) return;
|
|
96
|
+
if (args.decl.wildcard) return void args.into.set(args.decl.wildcard, { ...module });
|
|
97
|
+
if (args.decl.default) return void args.into.set(args.decl.default, module.default);
|
|
98
|
+
for (const name of args.decl.names) {
|
|
99
|
+
if (name in module) args.into.set(name, module[name]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function take(args: { decl: ValueImport; module: Document; source: Scope; into: Scope }): void {
|
|
104
|
+
const exported = exportedFunctions(args.module);
|
|
105
|
+
if (args.decl.wildcard) {
|
|
106
|
+
args.into.set(args.decl.wildcard, gathered(exported, args.source));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Only what the module offered. Reaching a private name would work here and
|
|
110
|
+
// then stop working the day that file rearranges its own insides.
|
|
111
|
+
for (const name of args.decl.names) {
|
|
112
|
+
if (exported.has(name)) args.into.set(name, args.source.lookup(name));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `import * as u`: one value holding everything the module published. */
|
|
117
|
+
function gathered(names: ReadonlySet<string>, source: Scope): Record<string, unknown> {
|
|
118
|
+
const out: Record<string, unknown> = {};
|
|
119
|
+
for (const name of names) out[name] = source.lookup(name);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The names a module made `pub` that are values a caller can hold. */
|
|
124
|
+
function exportedFunctions(document: Document): Set<string> {
|
|
125
|
+
const names = new Set<string>();
|
|
126
|
+
for (const decl of document.decls) {
|
|
127
|
+
if (isFnDecl(decl) && decl.export) names.add(decl.name);
|
|
128
|
+
}
|
|
129
|
+
return names;
|
|
130
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { namespaceValue, nativeFn } from "@venn-lang/core";
|
|
2
|
+
import type { ActionContext, ActionDefinition } from "@venn-lang/sdk";
|
|
3
|
+
import type { Registry } from "../registry/index.js";
|
|
4
|
+
import type { Scope } from "../scope/index.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Put every plugin namespace in the root scope as a value, so its verbs work
|
|
8
|
+
* inside any expression (`print(fmt.table(rows))`, `"${crypto.hash(x)}"`) and
|
|
9
|
+
* not only as a bare statement.
|
|
10
|
+
*
|
|
11
|
+
* A verb reached this way runs synchronously: it is the pure corner of the
|
|
12
|
+
* stdlib (`fmt`, `data`, `crypto`). An action that does I/O returns its promise,
|
|
13
|
+
* so those stay written as statements, where the runtime awaits them.
|
|
14
|
+
*/
|
|
15
|
+
export function bindNamespaces(args: {
|
|
16
|
+
registry: Registry;
|
|
17
|
+
ctx: ActionContext;
|
|
18
|
+
scope: Scope;
|
|
19
|
+
}): void {
|
|
20
|
+
const byNamespace = new Map<string, Record<string, unknown>>();
|
|
21
|
+
for (const entry of args.registry.actions()) {
|
|
22
|
+
const members = byNamespace.get(entry.namespace) ?? {};
|
|
23
|
+
place({ members, path: entry.name, action: entry.action, ctx: args.ctx });
|
|
24
|
+
byNamespace.set(entry.namespace, members);
|
|
25
|
+
}
|
|
26
|
+
for (const [namespace, members] of byNamespace) {
|
|
27
|
+
args.scope.set(namespace, namespaceValue(members));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `jwt.decode` nests: `{ jwt: { decode: fn } }`, so the dotted call reads naturally. */
|
|
32
|
+
function place(args: {
|
|
33
|
+
members: Record<string, unknown>;
|
|
34
|
+
path: string;
|
|
35
|
+
action: ActionDefinition;
|
|
36
|
+
ctx: ActionContext;
|
|
37
|
+
}): void {
|
|
38
|
+
const parts = args.path.split(".");
|
|
39
|
+
const leaf = parts.pop();
|
|
40
|
+
if (!leaf) return;
|
|
41
|
+
let level = args.members;
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
level[part] = (level[part] as Record<string, unknown>) ?? {};
|
|
44
|
+
level = level[part] as Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
level[leaf] = verb(args.action, args.ctx);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function verb(action: ActionDefinition, ctx: ActionContext): unknown {
|
|
50
|
+
return nativeFn((values) => action.run(ctx, { args: values, params: params(action) }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** No options map in expression position, so let the schema supply its defaults. */
|
|
54
|
+
function params(action: ActionDefinition): unknown {
|
|
55
|
+
if (!action.params) return {};
|
|
56
|
+
try {
|
|
57
|
+
return action.params.parse({});
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PRELUDE_VALUES } from "@venn-lang/core";
|
|
2
|
+
import type { Scope } from "../scope/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Put the pure prelude values (`len`, `range`, `pretty`, `str`, `min`…) in the
|
|
6
|
+
* root scope. Being values rather than verbs is what lets them appear in any
|
|
7
|
+
* expression: `xs.take(len(ys))`, `"${pretty(user)}"`.
|
|
8
|
+
*/
|
|
9
|
+
export function bindPrelude(scope: Scope): void {
|
|
10
|
+
for (const [name, value] of Object.entries(PRELUDE_VALUES)) scope.set(name, value);
|
|
11
|
+
}
|