@venn-lang/core 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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +303 -0
  3. package/dist/index.d.ts +3654 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +13137 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +61 -0
  8. package/src/ast/call-args.ts +15 -0
  9. package/src/ast/dotted-path.ts +22 -0
  10. package/src/ast/index.ts +7 -0
  11. package/src/ast/is-statement.ts +40 -0
  12. package/src/ast/split-call.ts +28 -0
  13. package/src/ast/walk.ts +6 -0
  14. package/src/codes/build-problem.ts +24 -0
  15. package/src/codes/catalog.ts +35 -0
  16. package/src/codes/code.types.ts +7 -0
  17. package/src/codes/index.ts +3 -0
  18. package/src/compile/compile.ts +169 -0
  19. package/src/compile/compile.types.ts +40 -0
  20. package/src/compile/index.ts +3 -0
  21. package/src/compile/lex-scope.ts +78 -0
  22. package/src/compile/nodes/binary.ts +56 -0
  23. package/src/compile/nodes/call.ts +50 -0
  24. package/src/compile/nodes/collection.ts +83 -0
  25. package/src/compile/nodes/const-lit.ts +30 -0
  26. package/src/compile/nodes/fast-binary.ts +71 -0
  27. package/src/compile/nodes/fn.ts +87 -0
  28. package/src/compile/nodes/index.ts +8 -0
  29. package/src/compile/nodes/literal.ts +86 -0
  30. package/src/compile/nodes/map-fields.ts +79 -0
  31. package/src/compile/nodes/member.ts +32 -0
  32. package/src/events/envelope.types.ts +17 -0
  33. package/src/events/event-data.types.ts +25 -0
  34. package/src/events/ids.types.ts +10 -0
  35. package/src/events/index.ts +5 -0
  36. package/src/events/run-plan.types.ts +18 -0
  37. package/src/events/status.types.ts +2 -0
  38. package/src/expand/accepted-kinds.ts +23 -0
  39. package/src/expand/deco/deco-decorator.ts +77 -0
  40. package/src/expand/deco/deco-env.ts +24 -0
  41. package/src/expand/deco/deco.types.ts +43 -0
  42. package/src/expand/deco/document-decos.ts +64 -0
  43. package/src/expand/deco/index.ts +7 -0
  44. package/src/expand/deco/read-signature.ts +57 -0
  45. package/src/expand/deco/run-body.ts +119 -0
  46. package/src/expand/decorate-callable.ts +49 -0
  47. package/src/expand/decorations.ts +41 -0
  48. package/src/expand/expand.ts +108 -0
  49. package/src/expand/expand.types.ts +78 -0
  50. package/src/expand/handles/around-verbs.ts +35 -0
  51. package/src/expand/handles/binding-verbs.ts +27 -0
  52. package/src/expand/handles/common-verbs.ts +40 -0
  53. package/src/expand/handles/fn-verbs.ts +58 -0
  54. package/src/expand/handles/handle.types.ts +33 -0
  55. package/src/expand/handles/index.ts +4 -0
  56. package/src/expand/handles/kind-of.ts +36 -0
  57. package/src/expand/handles/make-handle.ts +100 -0
  58. package/src/expand/handles/missing-verb.ts +27 -0
  59. package/src/expand/handles/runnable-verbs.ts +13 -0
  60. package/src/expand/handles/type-verbs.ts +56 -0
  61. package/src/expand/index.ts +40 -0
  62. package/src/expand/make-context.ts +69 -0
  63. package/src/expand/node-meta.ts +30 -0
  64. package/src/expand/node-span.ts +17 -0
  65. package/src/expand/swap-node.ts +28 -0
  66. package/src/expand/wrong-kind.ts +102 -0
  67. package/src/expr/cell.types.ts +25 -0
  68. package/src/expr/closure.ts +53 -0
  69. package/src/expr/closure.types.ts +31 -0
  70. package/src/expr/eval-env.types.ts +9 -0
  71. package/src/expr/evaluate.ts +18 -0
  72. package/src/expr/frame.ts +60 -0
  73. package/src/expr/index.ts +14 -0
  74. package/src/expr/invoke.ts +140 -0
  75. package/src/expr/member-value.ts +91 -0
  76. package/src/expr/methods/index.ts +65 -0
  77. package/src/expr/methods/list-grouping.ts +107 -0
  78. package/src/expr/methods/list-methods.ts +57 -0
  79. package/src/expr/methods/list-selection.ts +142 -0
  80. package/src/expr/methods/map-extras.ts +87 -0
  81. package/src/expr/methods/map-methods.ts +17 -0
  82. package/src/expr/methods/number-methods.ts +29 -0
  83. package/src/expr/methods/string-extras.ts +66 -0
  84. package/src/expr/methods/string-methods.ts +32 -0
  85. package/src/expr/methods/unit-methods.ts +54 -0
  86. package/src/expr/namespace.ts +20 -0
  87. package/src/expr/native.types.ts +40 -0
  88. package/src/expr/operators.ts +91 -0
  89. package/src/expr/pending.ts +47 -0
  90. package/src/expr/prelude.ts +75 -0
  91. package/src/expr/task.ts +71 -0
  92. package/src/format/format-text.ts +22 -0
  93. package/src/format/format.types.ts +18 -0
  94. package/src/format/from-settings.ts +40 -0
  95. package/src/format/index.ts +5 -0
  96. package/src/format/organize-header.ts +66 -0
  97. package/src/format/reindent.ts +35 -0
  98. package/src/generated/ast.ts +2383 -0
  99. package/src/generated/grammar.ts +5236 -0
  100. package/src/generated/module.ts +25 -0
  101. package/src/grammar/venn.langium +291 -0
  102. package/src/graph/graph.types.ts +20 -0
  103. package/src/graph/index.ts +2 -0
  104. package/src/graph/to-graph.ts +87 -0
  105. package/src/index.ts +93 -0
  106. package/src/interpolation/compile-template.ts +40 -0
  107. package/src/interpolation/index.ts +4 -0
  108. package/src/interpolation/interpolation.types.ts +18 -0
  109. package/src/interpolation/scan-interpolations.ts +47 -0
  110. package/src/interpolation/template.types.ts +19 -0
  111. package/src/lang/default-services.ts +14 -0
  112. package/src/lang/index.ts +3 -0
  113. package/src/lang/venn-lexer.ts +34 -0
  114. package/src/lang/venn-module.ts +51 -0
  115. package/src/module/index.ts +5 -0
  116. package/src/module/specifier.ts +21 -0
  117. package/src/parse/error-to-problem.ts +43 -0
  118. package/src/parse/index.ts +3 -0
  119. package/src/parse/parse-expression.ts +27 -0
  120. package/src/parse/parse-output.types.ts +8 -0
  121. package/src/parse/parse.ts +26 -0
  122. package/src/problem/build-diff.ts +116 -0
  123. package/src/problem/diff.types.ts +20 -0
  124. package/src/problem/format-value.ts +40 -0
  125. package/src/problem/index.ts +8 -0
  126. package/src/problem/problem-error.ts +15 -0
  127. package/src/problem/problem.types.ts +21 -0
  128. package/src/problem/related.types.ts +7 -0
  129. package/src/problem/severity.types.ts +2 -0
  130. package/src/problem/span.types.ts +8 -0
  131. package/src/typecheck/action-signature.ts +53 -0
  132. package/src/typecheck/builtin-types.ts +63 -0
  133. package/src/typecheck/builtins.ts +267 -0
  134. package/src/typecheck/catalog.types.ts +16 -0
  135. package/src/typecheck/check-deco.ts +174 -0
  136. package/src/typecheck/check-stmts.ts +151 -0
  137. package/src/typecheck/check-types.ts +232 -0
  138. package/src/typecheck/context.ts +31 -0
  139. package/src/typecheck/imported-types.ts +127 -0
  140. package/src/typecheck/index.ts +26 -0
  141. package/src/typecheck/infer.ts +416 -0
  142. package/src/typecheck/kind-types.ts +79 -0
  143. package/src/typecheck/member-docs.ts +211 -0
  144. package/src/typecheck/named-types.ts +57 -0
  145. package/src/typecheck/prelude-types.ts +152 -0
  146. package/src/typecheck/reshaped-fns.ts +58 -0
  147. package/src/typecheck/scheme.ts +75 -0
  148. package/src/typecheck/seed-params.ts +72 -0
  149. package/src/typecheck/seed-values.ts +64 -0
  150. package/src/typecheck/show.ts +64 -0
  151. package/src/typecheck/solidify.ts +60 -0
  152. package/src/typecheck/spec-to-type.ts +88 -0
  153. package/src/typecheck/type-env.ts +46 -0
  154. package/src/typecheck/type-ref.ts +62 -0
  155. package/src/typecheck/type.types.ts +168 -0
  156. package/src/typecheck/unify.ts +192 -0
  157. package/src/typecheck/unit-types.ts +49 -0
  158. package/src/units/combine.ts +97 -0
  159. package/src/units/index.ts +6 -0
  160. package/src/units/parse-instant.ts +12 -0
  161. package/src/units/parse-number.ts +48 -0
  162. package/src/units/unit-guards.ts +16 -0
  163. package/src/units/unit.types.ts +38 -0
  164. package/src/value/equals.ts +4 -0
  165. package/src/value/index.ts +4 -0
  166. package/src/value/is-numeric.ts +6 -0
  167. package/src/value/truthiness.ts +9 -0
  168. package/src/value/value.types.ts +12 -0
@@ -0,0 +1,83 @@
1
+ import { isWaiting } from "../../expr/pending.js";
2
+ import type { ListLit, MapLit, Ternary } from "../../generated/ast.js";
3
+ import { truthy } from "../../value/index.js";
4
+ import type { Compile, Thunk } from "../compile.types.js";
5
+ import { UNROLLED_MAP } from "./map-fields.js";
6
+
7
+ /**
8
+ * A list literal, built in one pass.
9
+ *
10
+ * The values are collected and watched for at the same time: walking the list
11
+ * again to ask whether anything is waiting would cost a second pass on every
12
+ * literal, which a program shaping many records feels.
13
+ */
14
+ export function compileList(expr: ListLit, compile: Compile): Thunk {
15
+ const items = expr.items.map(compile);
16
+ return (env) => {
17
+ const out: unknown[] = [];
18
+ let waiting = false;
19
+ for (let at = 0; at < items.length; at += 1) {
20
+ const value = (items[at] as Thunk)(env);
21
+ waiting = waiting || isWaiting(value);
22
+ out.push(value);
23
+ }
24
+ return waiting ? Promise.all(out) : out;
25
+ };
26
+ }
27
+
28
+ /**
29
+ * A map literal, filled field by field into an empty object.
30
+ *
31
+ * Writing the whole literal at once with computed keys builds faster and is a
32
+ * trap: the object it makes shares no shape with the next one, so every later
33
+ * read of a field is slow. Records are built once and read many times, so the
34
+ * assignment order must stay.
35
+ */
36
+ export function compileMap(expr: MapLit, compile: Compile): Thunk {
37
+ // Keys are fixed by the source; only the values are computed.
38
+ const keys = expr.entries.map((entry) => entry.key);
39
+ const values = expr.entries.map((entry) => compile(entry.value));
40
+ const build = UNROLLED_MAP[keys.length];
41
+ if (build) return build(keys, values, settleFields);
42
+ return (env) => {
43
+ const out: Record<string, unknown> = {};
44
+ let waiting = false;
45
+ for (let at = 0; at < keys.length; at += 1) {
46
+ const value = (values[at] as Thunk)(env);
47
+ waiting = waiting || isWaiting(value);
48
+ out[keys[at] as string] = value;
49
+ }
50
+ return waiting ? settleFields(out, keys) : out;
51
+ };
52
+ }
53
+
54
+ /** Replace each field with what it was waiting for, keeping the same shape. */
55
+ async function settleFields(
56
+ out: Record<string, unknown>,
57
+ keys: readonly string[],
58
+ ): Promise<Record<string, unknown>> {
59
+ const ready = await Promise.all(keys.map((key) => out[key]));
60
+ keys.forEach((key, at) => {
61
+ out[key] = ready[at];
62
+ });
63
+ return out;
64
+ }
65
+
66
+ /**
67
+ * `cond ? a : b`, branching without allocating to do it.
68
+ *
69
+ * Handing the continuation to `whenReady` reads better, but the continuation
70
+ * closes over `env`, so it costs a fresh function object per evaluation. The
71
+ * waiting branch pays that willingly: there it is a real suspension, and one
72
+ * allocation is the least of it.
73
+ */
74
+ export function compileTernary(expr: Ternary, compile: Compile): Thunk {
75
+ const condition = compile(expr.condition);
76
+ const then = compile(expr.then);
77
+ const otherwise = compile(expr.otherwise);
78
+ return (env) => {
79
+ const cond = condition(env);
80
+ if (!isWaiting(cond)) return truthy(cond) ? then(env) : otherwise(env);
81
+ return cond.then((ready) => (truthy(ready) ? then(env) : otherwise(env)));
82
+ };
83
+ }
@@ -0,0 +1,30 @@
1
+ import type { Expr } from "../../generated/ast.js";
2
+ import type { Thunk } from "../compile.types.js";
3
+ import { constant } from "./literal.js";
4
+
5
+ /** A value standing where an expression was. Only expansion ever writes one. */
6
+ interface ConstLit {
7
+ $type: "ConstLit";
8
+ value: unknown;
9
+ }
10
+
11
+ const CONST_LIT = "ConstLit";
12
+
13
+ /**
14
+ * Wrap a value as an expression node.
15
+ *
16
+ * A decorator's `.setValue` binds a value, and the grammar has no node that
17
+ * holds one: every literal it produces is text still to be read. Expansion
18
+ * writes this instead, so a changed binding is a change to the tree itself
19
+ * rather than to a lookup table kept beside it.
20
+ */
21
+ export function constLit(value: unknown): Expr {
22
+ return { $type: CONST_LIT, value } as unknown as Expr;
23
+ }
24
+
25
+ /** The thunk for a node expansion put there, or nothing for anything else. */
26
+ export function constThunk(expr: unknown): Thunk | undefined {
27
+ const written = expr as ConstLit | undefined;
28
+ if (written?.$type !== CONST_LIT) return undefined;
29
+ return constant(written.value);
30
+ }
@@ -0,0 +1,71 @@
1
+ import { applyBinary } from "../../expr/operators.js";
2
+ import type { Thunk } from "../compile.types.js";
3
+
4
+ /**
5
+ * One thunk builder per arithmetic operator, with the operation written out.
6
+ *
7
+ * The repetition is the point. Sharing `(a, b) => a + b` and calling it from
8
+ * the compiled thunk gives one call site eleven different callees, which is
9
+ * megamorphic and stops V8 inlining it. Writing `a + b` into each thunk gives
10
+ * every operator its own site with one callee.
11
+ *
12
+ * Two plain numbers take the short way; anything else (units, strings, values
13
+ * still arriving) falls through to `applyBinary`.
14
+ */
15
+ export const FAST_BINARY: Readonly<Record<string, (left: Thunk, right: Thunk) => Thunk>> = {
16
+ "+": (l, r) => (env) => {
17
+ const a = l(env);
18
+ const b = r(env);
19
+ return typeof a === "number" && typeof b === "number" ? a + b : applyBinary("+", a, b);
20
+ },
21
+ "-": (l, r) => (env) => {
22
+ const a = l(env);
23
+ const b = r(env);
24
+ return typeof a === "number" && typeof b === "number" ? a - b : applyBinary("-", a, b);
25
+ },
26
+ "*": (l, r) => (env) => {
27
+ const a = l(env);
28
+ const b = r(env);
29
+ return typeof a === "number" && typeof b === "number" ? a * b : applyBinary("*", a, b);
30
+ },
31
+ "/": (l, r) => (env) => {
32
+ const a = l(env);
33
+ const b = r(env);
34
+ return typeof a === "number" && typeof b === "number" ? a / b : applyBinary("/", a, b);
35
+ },
36
+ "%": (l, r) => (env) => {
37
+ const a = l(env);
38
+ const b = r(env);
39
+ return typeof a === "number" && typeof b === "number" ? a % b : applyBinary("%", a, b);
40
+ },
41
+ "<": (l, r) => (env) => {
42
+ const a = l(env);
43
+ const b = r(env);
44
+ return typeof a === "number" && typeof b === "number" ? a < b : applyBinary("<", a, b);
45
+ },
46
+ "<=": (l, r) => (env) => {
47
+ const a = l(env);
48
+ const b = r(env);
49
+ return typeof a === "number" && typeof b === "number" ? a <= b : applyBinary("<=", a, b);
50
+ },
51
+ ">": (l, r) => (env) => {
52
+ const a = l(env);
53
+ const b = r(env);
54
+ return typeof a === "number" && typeof b === "number" ? a > b : applyBinary(">", a, b);
55
+ },
56
+ ">=": (l, r) => (env) => {
57
+ const a = l(env);
58
+ const b = r(env);
59
+ return typeof a === "number" && typeof b === "number" ? a >= b : applyBinary(">=", a, b);
60
+ },
61
+ "==": (l, r) => (env) => {
62
+ const a = l(env);
63
+ const b = r(env);
64
+ return typeof a === "number" && typeof b === "number" ? a === b : applyBinary("==", a, b);
65
+ },
66
+ "!=": (l, r) => (env) => {
67
+ const a = l(env);
68
+ const b = r(env);
69
+ return typeof a === "number" && typeof b === "number" ? a !== b : applyBinary("!=", a, b);
70
+ },
71
+ };
@@ -0,0 +1,87 @@
1
+ import type { Cell } from "../../expr/cell.types.js";
2
+ import { hasCells } from "../../expr/cell.types.js";
3
+ import { makeClosure } from "../../expr/closure.js";
4
+ import type { Closure } from "../../expr/closure.types.js";
5
+ import type { EvalEnv } from "../../expr/eval-env.types.js";
6
+ import { INLINE_SLOTS } from "../../expr/frame.js";
7
+ import type { FnBody, FnDecl, FnExpr, ParamList } from "../../generated/ast.js";
8
+ import type { CompiledBody, Thunk } from "../compile.types.js";
9
+ import { type LexScope, scopeOf, stayedBare } from "../lex-scope.js";
10
+
11
+ /** How the dispatcher compiles a sub-expression in a given scope. */
12
+ export type CompileIn = (expr: FnBody["result"], scope: LexScope) => Thunk;
13
+
14
+ /** `fn (…) => …` as a value: the body is compiled here, not on every evaluation. */
15
+ export function compileFnExpr(expr: FnExpr, compileIn: CompileIn): Thunk {
16
+ const params = paramNames(expr.params);
17
+ // No `cellOf`: this body is compiled once and shared by every closure the
18
+ // expression makes, so it cannot hold cells belonging to one of them.
19
+ const body = compileBody({ params: expr.params, body: expr.body, compileIn });
20
+ return (env) => makeClosure(params, body, env);
21
+ }
22
+
23
+ /**
24
+ * A top-level `fn name(…)`, bound into a scope.
25
+ *
26
+ * This body is compiled per closure, and there is exactly one closure per
27
+ * declaration, so the cells its free names resolve to can be captured in the
28
+ * thunks that read them. That is what lets a recursive function keep its own
29
+ * name without needing an environment to find it in, which in turn is what lets
30
+ * it run without a frame.
31
+ */
32
+ export function closureIn(decl: FnDecl, env: EvalEnv, compileIn: CompileIn): Closure {
33
+ const cellOf = hasCells(env) ? (name: string) => env.cell(name) : undefined;
34
+ const body = compileBody({ params: decl.params, body: decl.body, compileIn, cellOf });
35
+ return makeClosure(paramNames(decl.params), body, env);
36
+ }
37
+
38
+ interface BodyArgs {
39
+ params: ParamList | undefined;
40
+ body: FnBody;
41
+ compileIn: CompileIn;
42
+ /** Absent when the body is shared by more than one closure. */
43
+ cellOf?: (name: string) => Cell;
44
+ }
45
+
46
+ /**
47
+ * Compile a body, first assuming it needs no frame.
48
+ *
49
+ * Whether it does is known only once it has been compiled: a free name needs
50
+ * cells to reach, and a `"${…}"` asks for names as text. So the optimistic pass
51
+ * runs and is thrown away when it was wrong, which costs one extra compile of
52
+ * one body against an allocation saved on every call it will ever receive.
53
+ */
54
+ function compileBody(args: BodyArgs): CompiledBody {
55
+ const first = compileInScope(scoped(args), args);
56
+ if (first.bare || !first.wasBare) return first.compiled;
57
+ const plain = scoped(args);
58
+ plain.bare = false;
59
+ return compileInScope(plain, args).compiled;
60
+ }
61
+
62
+ function scoped(args: BodyArgs): LexScope {
63
+ const scope = scopeOf(args.params, args.body);
64
+ scope.cellOf = args.cellOf;
65
+ return scope;
66
+ }
67
+
68
+ function compileInScope(
69
+ scope: LexScope,
70
+ args: BodyArgs,
71
+ ): { compiled: CompiledBody; bare: boolean; wasBare: boolean } {
72
+ const { body, compileIn } = args;
73
+ const wasBare = Boolean(scope.bare);
74
+ const locals = body.locals.map((local) => ({
75
+ slot: scope.names.indexOf(local.name),
76
+ value: compileIn(local.value, scope),
77
+ }));
78
+ const result = compileIn(body.result, scope);
79
+ const bare = stayedBare(scope);
80
+ const extra = Math.max(0, scope.names.length - INLINE_SLOTS);
81
+ const free = scope.free ?? [];
82
+ return { compiled: { names: scope.names, extra, free, locals, result, bare }, bare, wasBare };
83
+ }
84
+
85
+ function paramNames(params: ParamList | undefined): readonly string[] {
86
+ return (params?.params ?? []).map((param) => param.name);
87
+ }
@@ -0,0 +1,8 @@
1
+ export { compileBinary, compileUnary } from "./binary.js";
2
+ export { compileCall } from "./call.js";
3
+ export { compileList, compileMap, compileTernary } from "./collection.js";
4
+ export { constLit, constThunk } from "./const-lit.js";
5
+ export type { CompileIn } from "./fn.js";
6
+ export { closureIn, compileFnExpr } from "./fn.js";
7
+ export { compileInstant, compileNumber, compileString, constant } from "./literal.js";
8
+ export { compileIndex, compileMember } from "./member.js";
@@ -0,0 +1,86 @@
1
+ import { buildProblem, CODES } from "../../codes/index.js";
2
+ import { isWaiting } from "../../expr/pending.js";
3
+ import type { NumberLit, StringLit } from "../../generated/ast.js";
4
+ import { compileTemplate, type TemplateHole } from "../../interpolation/index.js";
5
+ import { ProblemError } from "../../problem/index.js";
6
+ import { isUnitValue, parseInstant, parseNumber, type UnitValue } from "../../units/index.js";
7
+ import type { Compile, Thunk } from "../compile.types.js";
8
+
9
+ const NO_SPAN = { uri: "", offset: 0, length: 0, line: 1, column: 1 };
10
+
11
+ /** A literal is a constant: read the lexeme once, then hand back the value. */
12
+ export function constant(value: unknown): Thunk {
13
+ return () => value;
14
+ }
15
+
16
+ export function compileNumber(expr: NumberLit): Thunk {
17
+ return constant(parseNumber(expr.raw));
18
+ }
19
+
20
+ export function compileInstant(expr: { value: string }): Thunk {
21
+ return constant(parseInstant(expr.value));
22
+ }
23
+
24
+ /**
25
+ * A string with no `${…}` is a constant. One with placeholders becomes the
26
+ * literal chunks plus a compiled thunk per hole: the scanning, the parsing and
27
+ * the splitting all happen here, never again.
28
+ */
29
+ export function compileString(expr: StringLit, compile: Compile): Thunk {
30
+ const { chunks, holes } = compileTemplate(expr.value);
31
+ if (holes.length === 0) return constant(expr.value);
32
+ const fills = holes.map((hole) => compileHole(hole, compile));
33
+ // Filled in one pass, watching as it goes for a placeholder still arriving.
34
+ // `map` with a continuation reads better but builds two function objects per
35
+ // evaluation, both closing over `env`. The values still land in an array
36
+ // because a hole cannot be evaluated twice: one of them may be an action.
37
+ return (env) => {
38
+ const values = new Array<unknown>(fills.length);
39
+ let waiting = false;
40
+ for (let at = 0; at < fills.length; at += 1) {
41
+ const value = (fills[at] as Thunk)(env);
42
+ waiting = waiting || isWaiting(value);
43
+ values[at] = value;
44
+ }
45
+ return waiting ? settleJoin(chunks, values) : join(chunks, values);
46
+ };
47
+ }
48
+
49
+ /** The waiting path: every hole settled, then the same join. */
50
+ async function settleJoin(chunks: readonly string[], values: unknown[]): Promise<string> {
51
+ return join(chunks, await Promise.all(values));
52
+ }
53
+
54
+ function join(chunks: readonly string[], values: readonly unknown[]): string {
55
+ let out = chunks[0] ?? "";
56
+ for (let at = 0; at < values.length; at += 1) {
57
+ out += stringify(values[at]) + (chunks[at + 1] ?? "");
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /** A placeholder that does not parse is a mistake worth reporting, not an empty string. */
63
+ function compileHole(hole: TemplateHole, compile: Compile): Thunk {
64
+ if (hole.expr) return compile(hole.expr);
65
+ return () => {
66
+ throw new ProblemError(
67
+ buildProblem({
68
+ spec: CODES.VN1002_PARSE,
69
+ span: NO_SPAN,
70
+ title: `Cannot read \`\${${hole.source}}\` — that is not an expression.`,
71
+ }),
72
+ );
73
+ };
74
+ }
75
+
76
+ function stringify(value: unknown): string {
77
+ if (value === null || value === undefined) return "";
78
+ if (isUnitValue(value)) return stringifyUnit(value);
79
+ return String(value);
80
+ }
81
+
82
+ function stringifyUnit(value: UnitValue): string {
83
+ if (value.kind === "duration") return `${value.ms}ms`;
84
+ if (value.kind === "size") return `${value.bytes}b`;
85
+ return `${value.ratio * 100}%`;
86
+ }
@@ -0,0 +1,79 @@
1
+ import { isWaiting } from "../../expr/pending.js";
2
+ import type { Thunk } from "../compile.types.js";
3
+
4
+ /** Replace each field with what it was waiting for, keeping the same shape. */
5
+ type Settle = (out: Record<string, unknown>, keys: readonly string[]) => Promise<unknown>;
6
+
7
+ /** How a map of a known size is built, given its keys and its value thunks. */
8
+ type Build = (keys: readonly string[], values: readonly Thunk[], settle: Settle) => Thunk;
9
+
10
+ /**
11
+ * Map literals of the everyday sizes, with the loop written out.
12
+ *
13
+ * The object is still made empty and filled by assignment, which is what gives
14
+ * every map from one site the same shape and so makes reading a field fast.
15
+ * What goes is the loop: reading `keys[at]` and `values[at]` per field, and
16
+ * carrying a flag for whether anything was waiting.
17
+ *
18
+ * Writing the literal in one go (`{ [k0]: a, [k1]: b }`) is faster to build and
19
+ * much slower to read, because those objects share no shape with each other.
20
+ * Records are built once and read many times, so it is the wrong trade.
21
+ */
22
+ export const UNROLLED_MAP: Readonly<Record<number, Build>> = {
23
+ 1: (keys, values, settle) => {
24
+ const k0 = keys[0] as string;
25
+ const v0 = values[0] as Thunk;
26
+ return (env) => {
27
+ const a = v0(env);
28
+ const out: Record<string, unknown> = {};
29
+ out[k0] = a;
30
+ return isWaiting(a) ? settle(out, keys) : out;
31
+ };
32
+ },
33
+ 2: (keys, values, settle) => {
34
+ const [k0, k1] = keys as [string, string];
35
+ const [v0, v1] = values as [Thunk, Thunk];
36
+ return (env) => {
37
+ const a = v0(env);
38
+ const b = v1(env);
39
+ const out: Record<string, unknown> = {};
40
+ out[k0] = a;
41
+ out[k1] = b;
42
+ return isWaiting(a) || isWaiting(b) ? settle(out, keys) : out;
43
+ };
44
+ },
45
+ 3: (keys, values, settle) => {
46
+ const [k0, k1, k2] = keys as [string, string, string];
47
+ const [v0, v1, v2] = values as [Thunk, Thunk, Thunk];
48
+ return (env) => {
49
+ const a = v0(env);
50
+ const b = v1(env);
51
+ const c = v2(env);
52
+ const out: Record<string, unknown> = {};
53
+ out[k0] = a;
54
+ out[k1] = b;
55
+ out[k2] = c;
56
+ return isWaiting(a) || isWaiting(b) || isWaiting(c) ? settle(out, keys) : out;
57
+ };
58
+ },
59
+ 4: (keys, values, settle) => {
60
+ const [k0, k1, k2, k3] = keys as [string, string, string, string];
61
+ const [v0, v1, v2, v3] = values as [Thunk, Thunk, Thunk, Thunk];
62
+ return (env) => {
63
+ const a = v0(env);
64
+ const b = v1(env);
65
+ const c = v2(env);
66
+ const d = v3(env);
67
+ const out: Record<string, unknown> = {};
68
+ out[k0] = a;
69
+ out[k1] = b;
70
+ out[k2] = c;
71
+ out[k3] = d;
72
+ return waiting4(a, b, c, d) ? settle(out, keys) : out;
73
+ };
74
+ },
75
+ };
76
+
77
+ function waiting4(a: unknown, b: unknown, c: unknown, d: unknown): boolean {
78
+ return isWaiting(a) || isWaiting(b) || isWaiting(c) || isWaiting(d);
79
+ }
@@ -0,0 +1,32 @@
1
+ import { memberValue } from "../../expr/member-value.js";
2
+ import { isWaiting, whenBothReady } from "../../expr/pending.js";
3
+ import type { Index, Member } from "../../generated/ast.js";
4
+ import type { Compile, Thunk } from "../compile.types.js";
5
+
6
+ /** `x.member`, with the member name fixed by the source. */
7
+ export function compileMember(expr: Member, compile: Compile): Thunk {
8
+ const receiver = compile(expr.receiver);
9
+ const member = expr.member;
10
+ return (env) => memberValue(receiver(env), member);
11
+ }
12
+
13
+ /**
14
+ * `xs[i]`, reading straight through when neither side is still arriving.
15
+ *
16
+ * Indexing `null` gives undefined rather than throwing, so a missing path along
17
+ * a chain reads as absent instead of stopping the flow.
18
+ */
19
+ export function compileIndex(expr: Index, compile: Compile): Thunk {
20
+ const receiver = compile(expr.receiver);
21
+ const key = compile(expr.index);
22
+ return (env) => {
23
+ const target = receiver(env);
24
+ const at = key(env);
25
+ if (!isWaiting(target) && !isWaiting(at)) return elementAt(target, at);
26
+ return whenBothReady(target, at, elementAt);
27
+ };
28
+ }
29
+
30
+ function elementAt(target: unknown, at: unknown): unknown {
31
+ return target == null ? undefined : (target as Record<string, unknown>)[at as string];
32
+ }
@@ -0,0 +1,17 @@
1
+ import type { EventData, EventKind } from "./event-data.types.js";
2
+ import type { NodePath, RunId } from "./ids.types.js";
3
+
4
+ /** The single contract between runner, host, and UI. Everything else derives from it. */
5
+ export interface Envelope<K extends EventKind = EventKind> {
6
+ /** Monotonic per run; the UI detects a gap and asks for a resync. */
7
+ seq: number;
8
+ /** ISO-8601 with milliseconds, from the runner's clock. */
9
+ ts: string;
10
+ run: RunId;
11
+ kind: K;
12
+ node?: NodePath;
13
+ parent?: NodePath;
14
+ /** Which parallel worker emitted this, so interleaved output can be split. */
15
+ worker?: number;
16
+ data: EventData[K];
17
+ }
@@ -0,0 +1,25 @@
1
+ import type { Problem } from "../problem/index.js";
2
+ import type { RunPlan } from "./run-plan.types.js";
3
+ import type { Status } from "./status.types.js";
4
+
5
+ /**
6
+ * The payload per event kind. EventKind is DERIVED from these keys, so adding an
7
+ * event is a single edit here. This is the M1 subset of the §15 taxonomy.
8
+ */
9
+ export interface EventData {
10
+ "run.started": { plan: RunPlan };
11
+ "run.finished": { passed: number; failed: number; durationMs: number };
12
+ "flow.started": { title: string };
13
+ "flow.finished": { title: string; status: Status };
14
+ "flow.retrying": { title: string; attempt: number; reason: string };
15
+ "step.started": { title: string };
16
+ "step.finished": { title: string; status: Status };
17
+ "action.started": { namespace: string; action: string };
18
+ "action.finished": { namespace: string; action: string; status: Status; durationMs: number };
19
+ "expect.passed": { source: string };
20
+ "expect.failed": { problem: Problem };
21
+ log: { level: string; message: string };
22
+ }
23
+
24
+ /** Every event name, derived from {@link EventData}. */
25
+ export type EventKind = keyof EventData;
@@ -0,0 +1,10 @@
1
+ /** A run identifier (ULID). Branded so it is not confused with a plain string. */
2
+ export type RunId = string & { readonly __brand: "RunId" };
3
+
4
+ /**
5
+ * A node path derived from `@id`, e.g. "checkout/flow-checkout/step-cart".
6
+ *
7
+ * The join key across the node graph, the editor margin, and the execution
8
+ * tree. Never a numeric index: an edit would silently desync history.
9
+ */
10
+ export type NodePath = string & { readonly __brand: "NodePath" };
@@ -0,0 +1,5 @@
1
+ export type { Envelope } from "./envelope.types.js";
2
+ export type { EventData, EventKind } from "./event-data.types.js";
3
+ export type { NodePath, RunId } from "./ids.types.js";
4
+ export type { PlannedFlow, PlannedStep, RunPlan } from "./run-plan.types.js";
5
+ export type { Status } from "./status.types.js";
@@ -0,0 +1,18 @@
1
+ /** One step of a {@link RunPlan}. */
2
+ export interface PlannedStep {
3
+ title: string;
4
+ }
5
+
6
+ /** One flow of a {@link RunPlan}, with the steps it will take. */
7
+ export interface PlannedFlow {
8
+ title: string;
9
+ steps: PlannedStep[];
10
+ }
11
+
12
+ /**
13
+ * What the run intends to do, carried on `run.started`. The UI draws it in
14
+ * grey before anything executes, so the shape of the run is visible up front.
15
+ */
16
+ export interface RunPlan {
17
+ flows: PlannedFlow[];
18
+ }
@@ -0,0 +1,2 @@
1
+ /** Where a flow, step or action stands when an event reports it. */
2
+ export type Status = "passed" | "failed" | "skipped" | "running";
@@ -0,0 +1,23 @@
1
+ import type { DecoDecl, Param } from "../generated/ast.js";
2
+ import { isTargetKind, type TargetKind } from "./handles/index.js";
3
+
4
+ /** The parameter that *is* the target: the first one, always. */
5
+ export function decoTarget(decl: DecoDecl): Param | undefined {
6
+ return decl.params?.params[0];
7
+ }
8
+
9
+ /**
10
+ * The kinds a `deco` decorates, read off the type on its first parameter.
11
+ *
12
+ * The signature is the only statement of what a decorator is for; a separate
13
+ * list of node names would drift from it. Empty means the signature never said
14
+ * (no parameter, or one typed with something that is not a kind). That fault is
15
+ * reported where the `deco` is declared, so a use site stays quiet about a
16
+ * mistake it did not make.
17
+ *
18
+ * @returns the kinds the first parameter's type names, in written order.
19
+ */
20
+ export function acceptedKinds(decl: DecoDecl): readonly TargetKind[] {
21
+ const written = decoTarget(decl)?.paramType?.members ?? [];
22
+ return written.map((member) => (member as { name?: string }).name ?? "").filter(isTargetKind);
23
+ }
@@ -0,0 +1,77 @@
1
+ import { buildProblem, CODES } from "../../codes/index.js";
2
+ import type { DecoDecl } from "../../generated/ast.js";
3
+ import type { Problem } from "../../problem/index.js";
4
+ import type { DecoratorDefinition, ExpandContext } from "../expand.types.js";
5
+ import { kindOf, makeHandle } from "../handles/index.js";
6
+ import { spanOf } from "../node-span.js";
7
+ import type { DecoSignature } from "./deco.types.js";
8
+ import { DecoEnv } from "./deco-env.js";
9
+ import { readSignature } from "./read-signature.js";
10
+ import { runDecoBody } from "./run-body.js";
11
+
12
+ /** A `deco` and everything reporting about it needs. */
13
+ interface DecoArgs {
14
+ decl: DecoDecl;
15
+ uri: string;
16
+ problems: Problem[];
17
+ }
18
+
19
+ /**
20
+ * A `deco` written in the language, as a decorator the expansion phase applies.
21
+ *
22
+ * This is the very {@link DecoratorDefinition} a plugin's TypeScript
23
+ * `defineDecorator` produces: one mechanism, two doors. What the signature says
24
+ * it decorates becomes `accepts`, so the type error for `@memoize` on a flow
25
+ * comes from how the author wrote the parameters rather than from a list kept by
26
+ * hand beside them.
27
+ *
28
+ * @returns a definition that does nothing when the signature does not read. The
29
+ * reason is pushed onto `args.problems` once, here, rather than at every use.
30
+ */
31
+ export function decoDecorator(args: DecoArgs): DecoratorDefinition {
32
+ const read = readSignature(args.decl);
33
+ if (read.ok) return definition({ ...args, sig: read.signature });
34
+ args.problems.push(signatureProblem(args, read.title));
35
+ // Still resolvable, doing nothing: a fault committed once, where the `deco` is
36
+ // written, must not be repeated at every use of it.
37
+ return { name: args.decl.name, expand: () => {} };
38
+ }
39
+
40
+ function definition(args: DecoArgs & { sig: DecoSignature }): DecoratorDefinition {
41
+ return {
42
+ name: args.decl.name,
43
+ accepts: args.sig.kinds,
44
+ expand: (ctx) => apply({ ...args, ctx }),
45
+ };
46
+ }
47
+
48
+ function apply(args: DecoArgs & { sig: DecoSignature; ctx: ExpandContext }): void {
49
+ const handle = makeHandle({ node: args.ctx.node, kind: kindOf(args.ctx.node) });
50
+ runDecoBody({
51
+ body: args.decl.body,
52
+ env: new DecoEnv(bindings(args.sig, handle, args.ctx.args)),
53
+ uri: args.uri,
54
+ reject: (problem) => args.problems.push(problem),
55
+ });
56
+ }
57
+
58
+ /** The target under the name the body calls it by, then `@name(…)` in order. */
59
+ function bindings(
60
+ sig: DecoSignature,
61
+ handle: unknown,
62
+ given: readonly unknown[],
63
+ ): Record<string, unknown> {
64
+ const bound: Record<string, unknown> = { [sig.target]: handle };
65
+ sig.args.forEach((name, at) => {
66
+ bound[name] = given[at];
67
+ });
68
+ return bound;
69
+ }
70
+
71
+ function signatureProblem(args: DecoArgs, title: string): Problem {
72
+ return buildProblem({
73
+ spec: CODES.VN2015_DECO_SIGNATURE,
74
+ span: spanOf(args.decl, args.uri),
75
+ title,
76
+ });
77
+ }