@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,82 @@
1
+ import {
2
+ type ActionCall,
3
+ type AstNode,
4
+ buildProblem,
5
+ type CaptureStmt,
6
+ CODES,
7
+ type LetStmt,
8
+ type MapLit,
9
+ type Problem,
10
+ } from "@venn-lang/core";
11
+ import { actionTarget, nodeSpan, PRELUDE, resolveTarget, splitTarget } from "../scheduler/index.js";
12
+ import type { CheckContext } from "./check.types.js";
13
+ import { checkOptions } from "./check-options.js";
14
+
15
+ /** `http.get "…"` written as a statement. */
16
+ export function checkAction(call: ActionCall, ctx: CheckContext): Problem[] {
17
+ return checkTarget({ node: call, target: call.target, opts: call.opts, ctx });
18
+ }
19
+
20
+ /**
21
+ * `let auth = http.post url { … }`. Only the unmistakable call is checked: with
22
+ * no arguments the path could be a field of a variable this pass cannot see, and
23
+ * a false "unknown action" is worse than a missed one.
24
+ */
25
+ export function checkLet(stmt: LetStmt, ctx: CheckContext): Problem[] {
26
+ if (stmt.args.length === 0 && !stmt.opts) return [];
27
+ const target = actionTarget(stmt.value);
28
+ if (target === undefined) {
29
+ return [problem(stmt, ctx, CODES.VN2003_UNKNOWN_ACTION, "This is not an action to call.")];
30
+ }
31
+ return checkTarget({ node: stmt, target, opts: stmt.opts, ctx });
32
+ }
33
+
34
+ /** `capture` is folded into `let`; say so where it is written. */
35
+ export function checkCapture(stmt: CaptureStmt, ctx: CheckContext): Problem {
36
+ const title =
37
+ "`capture` was removed — use `let` for a value that changes, `const` for one that does not.";
38
+ return problem(stmt, ctx, CODES.VN5001_REMOVED_KEYWORD, title);
39
+ }
40
+
41
+ /**
42
+ * A namespace is only usable when this file brought it in with `use` (or it is
43
+ * a resource). Loading the whole stdlib must not make `use` optional.
44
+ */
45
+ function checkTarget(args: {
46
+ node: AstNode;
47
+ target: string;
48
+ opts: MapLit | undefined;
49
+ ctx: CheckContext;
50
+ }): Problem[] {
51
+ const { node, target, ctx } = args;
52
+ if (PRELUDE.has(target)) return [];
53
+ const written = splitTarget(target).namespace;
54
+ if (ctx.bound.has(written)) return [];
55
+ if (!ctx.imported.has(written)) return [missingImport(args, written)];
56
+ const resolved = ctx.registry.action(resolveTarget(target, ctx.aliases));
57
+ if (!resolved) {
58
+ return [problem(node, ctx, CODES.VN2003_UNKNOWN_ACTION, `Unknown action "${target}".`)];
59
+ }
60
+ return checkOptions({ opts: args.opts, params: resolved.action.params, ctx });
61
+ }
62
+
63
+ function missingImport(
64
+ args: { node: AstNode; target: string; ctx: CheckContext },
65
+ namespace: string,
66
+ ): Problem {
67
+ if (!args.ctx.registry.hasNamespace(namespace)) {
68
+ const title = `Unknown action "${args.target}" — no loaded plugin provides it.`;
69
+ return problem(args.node, args.ctx, CODES.VN2003_UNKNOWN_ACTION, title);
70
+ }
71
+ const title = `"${namespace}" is not imported in this file — add a \`use\` for its package.`;
72
+ return problem(args.node, args.ctx, CODES.VN2007_NAMESPACE_NOT_IMPORTED, title);
73
+ }
74
+
75
+ function problem(
76
+ node: AstNode,
77
+ ctx: CheckContext,
78
+ spec: (typeof CODES)[keyof typeof CODES],
79
+ title: string,
80
+ ): Problem {
81
+ return buildProblem({ spec, span: nodeSpan(node, ctx.uri), title });
82
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ type AstNode,
3
+ buildProblem,
4
+ CODES,
5
+ type DecoDecl,
6
+ decoCannotCall,
7
+ isActionCall,
8
+ isDecoDecl,
9
+ isLetStmt,
10
+ type Problem,
11
+ } from "@venn-lang/core";
12
+ import { actionTarget, nodeSpan, PRELUDE, splitTarget } from "../scheduler/index.js";
13
+ import type { CheckContext } from "./check.types.js";
14
+
15
+ /**
16
+ * Check a node written inside a `deco` body, which is not the program and so is
17
+ * not resolved against the registry: `target.wrap(f)` is a verb on the handle
18
+ * the body was handed, and expansion settles what the rest means. The one
19
+ * refusal this pass can make is a verb from a plugin, which cannot work in a
20
+ * `deco` because a decorator runs before the program exists.
21
+ *
22
+ * @returns The node's problems, or `undefined` when the node is not inside a
23
+ * `deco`, which tells the caller to apply the ordinary document checks instead.
24
+ */
25
+ export function checkInsideDeco(node: AstNode, ctx: CheckContext): Problem[] | undefined {
26
+ const deco = enclosingDeco(node);
27
+ if (!deco) return undefined;
28
+ const target = calledTarget(node);
29
+ const refused = target === undefined ? undefined : pluginVerb({ deco, target, ctx });
30
+ return refused ? [problem(node, ctx, refused)] : [];
31
+ }
32
+
33
+ /** The `deco` this node is written inside, if any. */
34
+ function enclosingDeco(node: AstNode): DecoDecl | undefined {
35
+ for (let at: AstNode | undefined = node; at; at = at.$container) {
36
+ if (isDecoDecl(at)) return at;
37
+ }
38
+ return undefined;
39
+ }
40
+
41
+ /** The dotted name this statement calls, if calling is what it does. */
42
+ function calledTarget(node: AstNode): string | undefined {
43
+ if (isActionCall(node)) return node.target;
44
+ if (!isLetStmt(node) || (node.args.length === 0 && !node.opts)) return undefined;
45
+ return actionTarget(node.value);
46
+ }
47
+
48
+ /**
49
+ * Why this call cannot be made from here, if it cannot. Only a head the file can
50
+ * already account for is refused: an imported namespace, a resource, a namespace
51
+ * some loaded plugin owns. Anything else is a name only expansion resolves, and
52
+ * guessing at it would put an error on every well-written decorator.
53
+ */
54
+ function pluginVerb(args: {
55
+ deco: DecoDecl;
56
+ target: string;
57
+ ctx: CheckContext;
58
+ }): string | undefined {
59
+ const { namespace } = splitTarget(args.target);
60
+ if (PRELUDE.has(args.target) || paramNames(args.deco).has(namespace)) return undefined;
61
+ if (!reachesTheWorld(namespace, args.ctx)) return undefined;
62
+ return decoCannotCall(args.target);
63
+ }
64
+
65
+ function reachesTheWorld(namespace: string, ctx: CheckContext): boolean {
66
+ return (
67
+ ctx.imported.has(namespace) || ctx.bound.has(namespace) || ctx.registry.hasNamespace(namespace)
68
+ );
69
+ }
70
+
71
+ function paramNames(deco: DecoDecl): Set<string> {
72
+ return new Set((deco.params?.params ?? []).map((param) => param.name));
73
+ }
74
+
75
+ function problem(node: AstNode, ctx: CheckContext, title: string): Problem {
76
+ return buildProblem({ spec: CODES.VN2016_DECO_IMPURE, span: nodeSpan(node, ctx.uri), title });
77
+ }
@@ -0,0 +1,106 @@
1
+ import {
2
+ type AstNode,
3
+ buildProblem,
4
+ CODES,
5
+ isActionCall,
6
+ isCall,
7
+ isCaptureStmt,
8
+ isLetStmt,
9
+ isMatcherClause,
10
+ isRunStmt,
11
+ type MatcherClause,
12
+ type Problem,
13
+ type RunStmt,
14
+ walkAst,
15
+ } from "@venn-lang/core";
16
+ import {
17
+ collectAliases,
18
+ collectBoundNames,
19
+ collectNamespaces,
20
+ nodeSpan,
21
+ } from "../scheduler/index.js";
22
+ import type { CheckArgs, CheckContext } from "./check.types.js";
23
+ import { checkAction, checkCapture, checkLet } from "./check-calls.js";
24
+ import { checkInsideDeco } from "./check-deco-body.js";
25
+ import { checkEnv } from "./check-env.js";
26
+ import { checkFragmentCall } from "./check-fragment-call.js";
27
+ import { checkInterpolation } from "./check-interpolation.js";
28
+ import { checkUncalledAction } from "./check-uncalled.js";
29
+
30
+ /**
31
+ * Statically resolve every action, matcher and fragment reference in a parsed
32
+ * document. The errors the runner would otherwise raise mid-run are surfaced all
33
+ * at once, each with its source span.
34
+ *
35
+ * @param args Document, registry, known fragments and the declared `env` names.
36
+ * @returns One `Problem` per unresolved reference; empty when the document is clean.
37
+ */
38
+ export function checkDocument(args: CheckArgs): Problem[] {
39
+ const ctx: CheckContext = {
40
+ registry: args.registry,
41
+ fragments: args.fragments,
42
+ aliases: collectAliases(args.document, args.registry),
43
+ imported: collectNamespaces(args.document, args.registry),
44
+ bound: collectBoundNames(args.document),
45
+ env: args.env ? new Set(args.env) : undefined,
46
+ uri: args.uri ?? "memory://inline.vn",
47
+ };
48
+ const problems: Problem[] = [];
49
+ for (const node of walkAst(args.document)) {
50
+ const inDeco = checkInsideDeco(node, ctx);
51
+ if (inDeco) problems.push(...inDeco);
52
+ else problems.push(...everyCheck(node, ctx));
53
+ }
54
+ return problems;
55
+ }
56
+
57
+ function everyCheck(node: AstNode, ctx: CheckContext): Problem[] {
58
+ return [
59
+ ...checkNode(node, ctx),
60
+ ...checkEnv(node, ctx),
61
+ ...checkInterpolation(node, ctx),
62
+ ...one(checkUncalledAction(node, ctx)),
63
+ ];
64
+ }
65
+
66
+ function checkNode(node: AstNode, ctx: CheckContext): Problem[] {
67
+ if (isActionCall(node)) return checkAction(node, ctx);
68
+ if (isLetStmt(node)) return checkLet(node, ctx);
69
+ if (isCaptureStmt(node)) return [checkCapture(node, ctx)];
70
+ if (isMatcherClause(node)) return one(checkMatcher(node, ctx));
71
+ if (isRunStmt(node)) return one(checkFragment(node, ctx));
72
+ if (isCall(node)) return one(checkFragmentCall(node, ctx));
73
+ return [];
74
+ }
75
+
76
+ function one(problem: Problem | undefined): Problem[] {
77
+ return problem ? [problem] : [];
78
+ }
79
+
80
+ /**
81
+ * A matcher comes from a plugin like any action, so the file has to bring that
82
+ * plugin in. Resolving against the whole loaded stdlib would make `use` decorative.
83
+ */
84
+ function checkMatcher(clause: MatcherClause, ctx: CheckContext): Problem | undefined {
85
+ const owner = ctx.registry.matcher(clause.name);
86
+ if (!owner) {
87
+ return problem(clause, ctx, CODES.VN2004_UNKNOWN_MATCHER, `Unknown matcher "${clause.name}".`);
88
+ }
89
+ if (ctx.imported.has(owner.plugin.namespace)) return undefined;
90
+ const title = `"${clause.name}" comes from "${owner.plugin.namespace}", which is not imported in this file.`;
91
+ return problem(clause, ctx, CODES.VN2007_NAMESPACE_NOT_IMPORTED, title);
92
+ }
93
+
94
+ function checkFragment(stmt: RunStmt, ctx: CheckContext): Problem | undefined {
95
+ if (ctx.fragments.has(stmt.target)) return undefined;
96
+ return problem(stmt, ctx, CODES.VN2005_UNKNOWN_FRAGMENT, `Unknown fragment "${stmt.target}".`);
97
+ }
98
+
99
+ function problem(
100
+ node: AstNode,
101
+ ctx: CheckContext,
102
+ spec: (typeof CODES)[keyof typeof CODES],
103
+ title: string,
104
+ ): Problem {
105
+ return buildProblem({ spec, span: nodeSpan(node, ctx.uri), title });
106
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ type AstNode,
3
+ buildProblem,
4
+ CODES,
5
+ isMember,
6
+ type Problem,
7
+ type Span,
8
+ } from "@venn-lang/core";
9
+ import { actionTarget, nodeSpan } from "../scheduler/index.js";
10
+ import type { CheckContext } from "./check.types.js";
11
+
12
+ /** `env.NAME` as it appears inside a `${…}` placeholder. */
13
+ const ENV_READ = /\benv\.([A-Za-z_]\w*)/g;
14
+
15
+ /** Always present: the runner sets it to the `--env` that was selected. */
16
+ const BUILT_IN = new Set(["name"]);
17
+
18
+ /**
19
+ * Match every `env.*` read against what `venn.toml` declares, so a typo is an
20
+ * error rather than an empty string and a puzzling 404.
21
+ *
22
+ * Nothing is reported when the manifest could not be read: a wrong error about a
23
+ * variable that does exist is worse than no error at all.
24
+ */
25
+ export function checkEnv(node: AstNode, ctx: CheckContext): Problem[] {
26
+ if (!isMember(node) || isMember(node.$container)) return [];
27
+ const path = actionTarget(node);
28
+ const name = path?.startsWith("env.") ? path.slice(4) : undefined;
29
+ if (name === undefined || name.includes(".")) return [];
30
+ const span = nodeSpan(node, ctx.uri);
31
+ if (!ctx.imported.has("env")) return [notImported(span)];
32
+ if (!ctx.env || declared(name, ctx.env)) return [];
33
+ return [envProblem(name, span, ctx)];
34
+ }
35
+
36
+ /**
37
+ * Configuration is brought in like anything else. Reading `env.*` without saying
38
+ * where it comes from is the same hole `use` closes for actions and matchers:
39
+ * the reader should not have to know which names are magic.
40
+ */
41
+ function notImported(span: Span): Problem {
42
+ return buildProblem({
43
+ spec: CODES.VN2007_NAMESPACE_NOT_IMPORTED,
44
+ span,
45
+ title: '"env" is not imported in this file — add `use "venn/env"`.',
46
+ });
47
+ }
48
+
49
+ /** The same checks for text the parser never turned into nodes: `"${env.X}"`. */
50
+ export function envProblemsIn(source: string, span: Span, ctx: CheckContext): Problem[] {
51
+ const names = [...source.matchAll(ENV_READ)].map((match) => match[1] ?? "").filter(Boolean);
52
+ if (names.length === 0) return [];
53
+ if (!ctx.imported.has("env")) return [notImported(span)];
54
+ const env = ctx.env;
55
+ if (!env) return [];
56
+ return names.filter((name) => !declared(name, env)).map((name) => envProblem(name, span, ctx));
57
+ }
58
+
59
+ function declared(name: string, env: ReadonlySet<string>): boolean {
60
+ return env.has(name) || BUILT_IN.has(name);
61
+ }
62
+
63
+ /** The undeclared-variable problem, with the nearest declared name as a hint. */
64
+ export function envProblem(name: string, span: Span, ctx: CheckContext): Problem {
65
+ const hint = nearest(name, [...(ctx.env ?? [])]);
66
+ const title = hint
67
+ ? `"env.${name}" is not declared in venn.toml — did you mean "env.${hint}"?`
68
+ : `"env.${name}" is not declared in venn.toml.`;
69
+ return buildProblem({ spec: CODES.VN2006_UNKNOWN_ENV, span, title });
70
+ }
71
+
72
+ function nearest(name: string, known: readonly string[]): string | undefined {
73
+ const best = known
74
+ .map((candidate) => ({ candidate, distance: distance(name, candidate) }))
75
+ .sort((left, right) => left.distance - right.distance)[0];
76
+ const tolerance = Math.max(2, Math.floor(name.length / 3));
77
+ return best && best.distance <= tolerance ? best.candidate : undefined;
78
+ }
79
+
80
+ /** Levenshtein edit distance, kept to two rows because only the score is needed. */
81
+ function distance(left: string, right: string): number {
82
+ let previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
83
+ for (let i = 1; i <= left.length; i += 1) {
84
+ const current = [i];
85
+ for (let j = 1; j <= right.length; j += 1) {
86
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
87
+ current[j] = Math.min(
88
+ (current[j - 1] ?? 0) + 1,
89
+ (previous[j] ?? 0) + 1,
90
+ (previous[j - 1] ?? 0) + cost,
91
+ );
92
+ }
93
+ previous = current;
94
+ }
95
+ return previous[right.length] ?? 0;
96
+ }
@@ -0,0 +1,27 @@
1
+ import { buildProblem, type Call, CODES, isRef, type Problem } from "@venn-lang/core";
2
+ import { nodeSpan } from "../scheduler/index.js";
3
+ import type { CheckContext } from "./check.types.js";
4
+
5
+ /**
6
+ * Refuse a fragment called as though it were a function.
7
+ *
8
+ * The two look alike where they are written but are different kinds of thing: a
9
+ * `fn` gives back a value, a `fragment` gives back steps, and steps are recorded
10
+ * in the report, can fail and belong to a flow. So a fragment is invoked with
11
+ * `run`. `run entrar(…)` never reaches here: its target is a name, not an
12
+ * expression.
13
+ *
14
+ * @param call The call expression to inspect.
15
+ * @param ctx The document's resolved check context.
16
+ * @returns A `VN3013` problem, or `undefined` when the callee is not a fragment.
17
+ */
18
+ export function checkFragmentCall(call: Call, ctx: CheckContext): Problem | undefined {
19
+ const callee = call.callee;
20
+ if (!isRef(callee) || !ctx.fragments.has(callee.name)) return undefined;
21
+ return buildProblem({
22
+ spec: CODES.VN3013_NOT_CALLABLE,
23
+ span: nodeSpan(call, ctx.uri),
24
+ title: `${callee.name} is a fragment, so it cannot be called for a value.`,
25
+ note: `Invoke it with \`run ${callee.name}(…)\`, which records its steps in the report.`,
26
+ });
27
+ }
@@ -0,0 +1,82 @@
1
+ import {
2
+ buildProblem,
3
+ CODES,
4
+ type Document,
5
+ isDecoDecl,
6
+ isFnDecl,
7
+ isFragmentDecl,
8
+ isValueImport,
9
+ type Problem,
10
+ type ValueImport,
11
+ } from "@venn-lang/core";
12
+ import type { ImportGraph } from "../scheduler/index.js";
13
+ import { nodeSpan } from "../scheduler/index.js";
14
+
15
+ /**
16
+ * Check every name a file imports against what the file it names publishes.
17
+ * Otherwise a misspelt import stays quietly `undefined` until something calls
18
+ * it, and the run blames the call site rather than the import.
19
+ *
20
+ * @param args The importing document, its URI, and the resolved import graph.
21
+ * @returns One `VN2009` problem per name the target does not publish.
22
+ */
23
+ export function checkImports(args: {
24
+ document: Document;
25
+ uri: string;
26
+ graph: ImportGraph;
27
+ }): Problem[] {
28
+ const problems: Problem[] = [];
29
+ for (const decl of args.document.imports) {
30
+ if (!isValueImport(decl)) continue;
31
+ const target = args.graph.resolve(args.uri, decl.path);
32
+ const module = args.graph.modules.get(target);
33
+ // A path that reads nothing is already reported by whoever tried to read it.
34
+ if (module) problems.push(...missing({ decl, module, uri: args.uri, path: decl.path }));
35
+ }
36
+ return problems;
37
+ }
38
+
39
+ function missing(args: {
40
+ decl: ValueImport;
41
+ module: Document;
42
+ uri: string;
43
+ path: string;
44
+ }): Problem[] {
45
+ const published = exported(args.module);
46
+ return args.decl.names
47
+ .filter((name) => !published.has(name))
48
+ .map((name) =>
49
+ buildProblem({
50
+ spec: CODES.VN2009_NOT_EXPORTED,
51
+ span: nodeSpan(args.decl, args.uri),
52
+ title: `"${args.path}" does not publish ${name}.`,
53
+ note: hint(name, args.module),
54
+ }),
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Why it is not there: written but kept private, or not written at all. The two
60
+ * are different mistakes with different fixes, and a reader told only "not
61
+ * published" goes looking for a typo they did not make.
62
+ */
63
+ function hint(name: string, module: Document): string {
64
+ const declared = module.decls.some(
65
+ (decl) =>
66
+ (isFnDecl(decl) || isFragmentDecl(decl) || isDecoDecl(decl)) &&
67
+ (decl as { name?: string }).name === name,
68
+ );
69
+ return declared
70
+ ? `It is declared there, but not marked \`pub\`.`
71
+ : `Nothing of that name is declared there.`;
72
+ }
73
+
74
+ /** Everything a file marked `pub`: functions, fragments and decorators alike. */
75
+ function exported(document: Document): Set<string> {
76
+ const names = new Set<string>();
77
+ for (const decl of document.decls) {
78
+ if (!isFnDecl(decl) && !isFragmentDecl(decl) && !isDecoDecl(decl)) continue;
79
+ if (decl.export) names.add(decl.name);
80
+ }
81
+ return names;
82
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ type AstNode,
3
+ buildProblem,
4
+ CODES,
5
+ type InterpolationSlot,
6
+ isStringLit,
7
+ type Problem,
8
+ parseExpression,
9
+ type Span,
10
+ scanInterpolations,
11
+ } from "@venn-lang/core";
12
+ import type { CheckContext } from "./check.types.js";
13
+ import { envProblemsIn } from "./check-env.js";
14
+
15
+ /**
16
+ * Check each `${…}` placeholder in a string literal and point at the placeholder
17
+ * itself, not at the whole string. A slot that does not parse would otherwise
18
+ * evaluate to an empty string, so a typo inside a URL fails as a puzzling 404.
19
+ */
20
+ export function checkInterpolation(node: AstNode, ctx: CheckContext): Problem[] {
21
+ const cst = node.$cstNode;
22
+ if (!isStringLit(node) || !cst) return [];
23
+ return scanInterpolations(cst.text).flatMap((slot) =>
24
+ inSlot(slot, spanOf(slot, { cst, uri: ctx.uri }), ctx),
25
+ );
26
+ }
27
+
28
+ /** A URL is where `env` reads live, so they are checked here as well as in the AST. */
29
+ function inSlot(slot: InterpolationSlot, span: Span, ctx: CheckContext): Problem[] {
30
+ if (!parseExpression(slot.source)) return [unreadable(slot, span)];
31
+ return envProblemsIn(slot.source, span, ctx);
32
+ }
33
+
34
+ function unreadable(slot: InterpolationSlot, span: Span): Problem {
35
+ return buildProblem({
36
+ spec: CODES.VN1002_PARSE,
37
+ span,
38
+ title: `Cannot read \`\${${slot.source}}\` — that is not an expression.`,
39
+ });
40
+ }
41
+
42
+ /** The span of the placeholder itself: line and column follow the string's own start. */
43
+ function spanOf(
44
+ slot: InterpolationSlot,
45
+ target: {
46
+ cst: { offset: number; range?: { start: { line: number; character: number } } };
47
+ uri: string;
48
+ },
49
+ ): Span {
50
+ const start = target.cst.range?.start;
51
+ return {
52
+ uri: target.uri,
53
+ offset: target.cst.offset + slot.start,
54
+ length: slot.end - slot.start,
55
+ line: (start?.line ?? 0) + 1,
56
+ column: (start?.character ?? 0) + 1 + slot.start,
57
+ };
58
+ }
@@ -0,0 +1,18 @@
1
+ import type { MapLit, Problem } from "@venn-lang/core";
2
+ import { unknownOptions } from "../scheduler/index.js";
3
+ import type { CheckContext } from "./check.types.js";
4
+
5
+ /**
6
+ * Check the keys of an options map against the action's own schema.
7
+ *
8
+ * The runtime refuses an unknown key too, but only when the flow reaches that
9
+ * line. Both read the same list and say the same sentence, so the editor can
10
+ * mark the word before anything runs.
11
+ */
12
+ export function checkOptions(args: {
13
+ opts: MapLit | undefined;
14
+ params: unknown;
15
+ ctx: CheckContext;
16
+ }): Problem[] {
17
+ return unknownOptions({ opts: args.opts, params: args.params, uri: args.ctx.uri });
18
+ }
@@ -0,0 +1,42 @@
1
+ import {
2
+ type AstNode,
3
+ buildProblem,
4
+ CODES,
5
+ isCall,
6
+ isLetStmt,
7
+ isMember,
8
+ type Member,
9
+ type Problem,
10
+ } from "@venn-lang/core";
11
+ import { actionTarget, nodeSpan, resolveTarget } from "../scheduler/index.js";
12
+ import type { CheckContext } from "./check.types.js";
13
+
14
+ /**
15
+ * A plugin verb named but never called.
16
+ *
17
+ * `let id = data.faker.uuid` runs the verb, because the runtime recognises a
18
+ * bare path that names an action. The same words inside an expression evaluate
19
+ * to the verb itself, so a program meaning to read a value gets a function.
20
+ * Rather than guess which was meant, this asks for the parentheses.
21
+ */
22
+ export function checkUncalledAction(node: AstNode, ctx: CheckContext): Problem | undefined {
23
+ if (!isMember(node) || !readsAsValue(node)) return undefined;
24
+ const target = actionTarget(node);
25
+ if (target === undefined) return undefined;
26
+ if (!ctx.registry.action(resolveTarget(target, ctx.aliases))) return undefined;
27
+ return buildProblem({
28
+ spec: CODES.VN2008_UNCALLED_ACTION,
29
+ span: nodeSpan(node, ctx.uri),
30
+ title: `\`${target}\` is a verb, not a value — write \`${target}()\` to call it.`,
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Where naming the verb produces the verb. Skipped when it is the head of a
36
+ * longer path or the thing being called, and when it stands as a statement's
37
+ * value, which the runtime calls anyway.
38
+ */
39
+ function readsAsValue(node: Member): boolean {
40
+ const parent = node.$container;
41
+ return !isMember(parent) && !isCall(parent) && !isLetStmt(parent);
42
+ }
@@ -0,0 +1,30 @@
1
+ import type { Document } from "@venn-lang/core";
2
+ import type { Registry } from "../registry/index.js";
3
+
4
+ /** Inputs to a static name-resolution pass over a parsed document. */
5
+ export interface CheckArgs {
6
+ document: Document;
7
+ registry: Registry;
8
+ fragments: ReadonlySet<string>;
9
+ uri?: string;
10
+ /**
11
+ * The variables `venn.toml` declares. Omit it when they are unknown: an
12
+ * undeclared-variable error is only worth raising when the declaration list is
13
+ * trustworthy.
14
+ */
15
+ env?: readonly string[];
16
+ }
17
+
18
+ /** Everything a per-node check needs, resolved once per document. */
19
+ export interface CheckContext {
20
+ registry: Registry;
21
+ fragments: ReadonlySet<string>;
22
+ aliases: ReadonlyMap<string, string>;
23
+ /** Namespaces this file actually brought in with `use`. */
24
+ imported: ReadonlySet<string>;
25
+ /** Names this file binds: callable, but their methods cannot be verified. */
26
+ bound: ReadonlySet<string>;
27
+ /** Declared `env` variables, or undefined when the manifest could not be read. */
28
+ env?: ReadonlySet<string>;
29
+ uri: string;
30
+ }
@@ -0,0 +1,4 @@
1
+ export type { CheckArgs } from "./check.types.js";
2
+ export { checkDocument } from "./check-document.js";
3
+ export { checkFragmentCall } from "./check-fragment-call.js";
4
+ export { checkImports } from "./check-imports.js";
@@ -0,0 +1,20 @@
1
+ import type { Host, Port } from "@venn-lang/contracts";
2
+ import { invoke } from "@venn-lang/core";
3
+ import type { ActionContext } from "@venn-lang/sdk";
4
+ import type { PortResolver } from "../ports/index.js";
5
+
6
+ /** Build the context an action's `run` receives from the host and port resolver. */
7
+ export function createActionContext(args: {
8
+ host: Host;
9
+ ports: PortResolver;
10
+ config?: Record<string, unknown>;
11
+ }): ActionContext {
12
+ return {
13
+ port: <T>(port: Port<T>): T => args.ports.resolve(port),
14
+ secrets: args.host.secrets,
15
+ config: args.config ?? {},
16
+ log: (message) => args.host.log.log({ level: "info", message }),
17
+ redact: () => {},
18
+ invoke: (fn, values) => invoke(fn, values),
19
+ };
20
+ }
@@ -0,0 +1 @@
1
+ export { createActionContext } from "./action-context.js";