@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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/index.d.ts +453 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +3314 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +56 -0
  8. package/src/check/check-calls.ts +82 -0
  9. package/src/check/check-deco-body.ts +77 -0
  10. package/src/check/check-document.ts +106 -0
  11. package/src/check/check-env.ts +96 -0
  12. package/src/check/check-fragment-call.ts +27 -0
  13. package/src/check/check-imports.ts +82 -0
  14. package/src/check/check-interpolation.ts +58 -0
  15. package/src/check/check-options.ts +18 -0
  16. package/src/check/check-uncalled.ts +42 -0
  17. package/src/check/check.types.ts +30 -0
  18. package/src/check/index.ts +4 -0
  19. package/src/context/action-context.ts +20 -0
  20. package/src/context/index.ts +1 -0
  21. package/src/decorators/builtin-decorators.ts +87 -0
  22. package/src/decorators/create-decorator-source.ts +25 -0
  23. package/src/decorators/index.ts +2 -0
  24. package/src/emit/create-emitter.ts +16 -0
  25. package/src/emit/emitter.types.ts +6 -0
  26. package/src/emit/index.ts +3 -0
  27. package/src/emit/run-id.ts +11 -0
  28. package/src/eventsink/event-sink.port.ts +13 -0
  29. package/src/eventsink/event-sink.types.ts +11 -0
  30. package/src/eventsink/index.ts +4 -0
  31. package/src/eventsink/memory-sink.ts +17 -0
  32. package/src/eventsink/ndjson-sink.ts +14 -0
  33. package/src/index.ts +14 -0
  34. package/src/ports/create-port-resolver.ts +39 -0
  35. package/src/ports/index.ts +2 -0
  36. package/src/ports/port-resolver.types.ts +12 -0
  37. package/src/registry/build-registry.ts +76 -0
  38. package/src/registry/index.ts +2 -0
  39. package/src/registry/registry.types.ts +24 -0
  40. package/src/run/create-runner.ts +103 -0
  41. package/src/run/index.ts +4 -0
  42. package/src/run/resolve-imports.ts +166 -0
  43. package/src/run/runner.types.ts +67 -0
  44. package/src/scheduler/absorb-exit.ts +19 -0
  45. package/src/scheduler/aliases.ts +44 -0
  46. package/src/scheduler/annotations.ts +48 -0
  47. package/src/scheduler/base-scope.ts +25 -0
  48. package/src/scheduler/bind-globals.ts +55 -0
  49. package/src/scheduler/bind-imports.ts +130 -0
  50. package/src/scheduler/bind-namespaces.ts +61 -0
  51. package/src/scheduler/bind-prelude.ts +11 -0
  52. package/src/scheduler/block-plan.ts +66 -0
  53. package/src/scheduler/branch-engine.ts +13 -0
  54. package/src/scheduler/call-params.ts +140 -0
  55. package/src/scheduler/cleanup.types.ts +18 -0
  56. package/src/scheduler/collect.ts +60 -0
  57. package/src/scheduler/concurrency.ts +16 -0
  58. package/src/scheduler/create-cleanup-list.ts +17 -0
  59. package/src/scheduler/declared-arity.ts +13 -0
  60. package/src/scheduler/engine.types.ts +50 -0
  61. package/src/scheduler/filter.ts +5 -0
  62. package/src/scheduler/filter.types.ts +9 -0
  63. package/src/scheduler/flaky.ts +32 -0
  64. package/src/scheduler/flaky.types.ts +7 -0
  65. package/src/scheduler/index.ts +16 -0
  66. package/src/scheduler/invocation.ts +103 -0
  67. package/src/scheduler/local-call.ts +28 -0
  68. package/src/scheduler/node-span.ts +29 -0
  69. package/src/scheduler/opts-text.ts +10 -0
  70. package/src/scheduler/opts.ts +14 -0
  71. package/src/scheduler/pending.types.ts +9 -0
  72. package/src/scheduler/run-action.ts +163 -0
  73. package/src/scheduler/run-around.ts +23 -0
  74. package/src/scheduler/run-attempts.ts +105 -0
  75. package/src/scheduler/run-bindings.ts +67 -0
  76. package/src/scheduler/run-block.ts +65 -0
  77. package/src/scheduler/run-document.ts +181 -0
  78. package/src/scheduler/run-expect.ts +48 -0
  79. package/src/scheduler/run-flow.ts +69 -0
  80. package/src/scheduler/run-foreach.ts +148 -0
  81. package/src/scheduler/run-group.ts +9 -0
  82. package/src/scheduler/run-if.ts +21 -0
  83. package/src/scheduler/run-lifecycle.ts +86 -0
  84. package/src/scheduler/run-matcher.ts +99 -0
  85. package/src/scheduler/run-parallel.ts +68 -0
  86. package/src/scheduler/run-prologue.ts +59 -0
  87. package/src/scheduler/run-race.ts +20 -0
  88. package/src/scheduler/run-repeat.ts +54 -0
  89. package/src/scheduler/run-run.ts +41 -0
  90. package/src/scheduler/run-script.ts +48 -0
  91. package/src/scheduler/run-statements.ts +107 -0
  92. package/src/scheduler/run-step.ts +76 -0
  93. package/src/scheduler/run-try.ts +34 -0
  94. package/src/scheduler/run-while.ts +50 -0
  95. package/src/scheduler/script-lifecycle.ts +47 -0
  96. package/src/scheduler/settled.ts +20 -0
  97. package/src/scheduler/signals.ts +33 -0
  98. package/src/scheduler/target.ts +30 -0
  99. package/src/scheduler/unknown-option.ts +87 -0
  100. package/src/scope/create-scope.ts +67 -0
  101. package/src/scope/index.ts +2 -0
  102. package/src/scope/scope.types.ts +12 -0
  103. package/src/types/create-type-catalog.ts +70 -0
  104. 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
+ }