@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,40 @@
1
+ import type { AstNode } from "langium";
2
+ import { nativeFn } from "../../expr/index.js";
3
+ import { writeMeta } from "../node-meta.js";
4
+ import { swapNode } from "../swap-node.js";
5
+ import type { VerbTable } from "./handle.types.js";
6
+
7
+ /**
8
+ * What every kind answers to: what it is called, what it carries, and leaving.
9
+ *
10
+ * `meta` is how a decorator says something the grammar has no word for, the same
11
+ * channel `@retry(2)` uses, so a `deco` written in the language reaches exactly
12
+ * what a plugin's TypeScript decorator reaches.
13
+ */
14
+ export const COMMON_VERBS: VerbTable = {
15
+ props: { name: (node) => nameOf(node) },
16
+ calls: {
17
+ meta: (node) =>
18
+ nativeFn((args) => {
19
+ writeMeta(node, String(args[0]), args[1]);
20
+ return null;
21
+ }),
22
+ remove: (node) =>
23
+ nativeFn(() => {
24
+ swapNode(node, undefined);
25
+ return null;
26
+ }),
27
+ },
28
+ };
29
+
30
+ /**
31
+ * A flow or a step has a title rather than a name, and answers with it.
32
+ *
33
+ * Empty for the things that have neither. `$type` would answer, but with a word
34
+ * out of the compiler's own tree: the one vocabulary this whole surface exists
35
+ * to keep out of the language.
36
+ */
37
+ function nameOf(node: AstNode): string {
38
+ const named = node as { name?: string; title?: string };
39
+ return named.name ?? named.title ?? "";
40
+ }
@@ -0,0 +1,58 @@
1
+ import { nativeFn } from "../../expr/index.js";
2
+ import type { FnDecl, Param, ParamList } from "../../generated/ast.js";
3
+ import type { VerbTable } from "./handle.types.js";
4
+
5
+ /**
6
+ * A function's shape: the parameters it takes and the name it answers to.
7
+ *
8
+ * Every one of these edits the declaration itself, before it is compiled, so a
9
+ * parameter added here is in scope for the body that was written without it.
10
+ * That is what `@inject("who")` depends on.
11
+ */
12
+ export const FN_VERBS: VerbTable = {
13
+ props: { params: (node) => names(node as FnDecl) },
14
+ calls: {
15
+ addParam: (node) => nativeFn((args) => addParam(node as FnDecl, String(args[0]))),
16
+ removeParam: (node) => nativeFn((args) => removeParam(node as FnDecl, String(args[0]))),
17
+ rename: (node) => nativeFn((args) => rename(node as FnDecl, String(args[0]))),
18
+ },
19
+ };
20
+
21
+ function names(decl: FnDecl): string[] {
22
+ return (decl.params?.params ?? []).map((param) => param.name);
23
+ }
24
+
25
+ function addParam(decl: FnDecl, name: string): null {
26
+ const list = paramList(decl);
27
+ if (!list.params.some((param) => param.name === name)) list.params.push(param(list, name));
28
+ return null;
29
+ }
30
+
31
+ function removeParam(decl: FnDecl, name: string): null {
32
+ const list = decl.params;
33
+ if (!list) return null;
34
+ list.params = list.params.filter((param) => param.name !== name);
35
+ return null;
36
+ }
37
+
38
+ function rename(decl: FnDecl, name: string): null {
39
+ decl.name = name;
40
+ return null;
41
+ }
42
+
43
+ /** A function written `fn f()` has no list at all until something adds to it. */
44
+ function paramList(decl: FnDecl): ParamList {
45
+ if (decl.params) return decl.params;
46
+ const list = node<ParamList>({ $type: "ParamList", params: [] }, decl, "params");
47
+ decl.params = list;
48
+ return list;
49
+ }
50
+
51
+ function param(list: ParamList, name: string): Param {
52
+ return node<Param>({ $type: "Param", annotations: [], name }, list, "params");
53
+ }
54
+
55
+ /** A synthesized node, containered the way the parser would have containered it. */
56
+ function node<T>(shape: object, container: object, property: string): T {
57
+ return { ...shape, $container: container, $containerProperty: property } as T;
58
+ }
@@ -0,0 +1,33 @@
1
+ import type { AstNode } from "langium";
2
+
3
+ /**
4
+ * What a `deco` decorates, written as the type of its first parameter.
5
+ *
6
+ * These are the words the language uses about itself. A decorator author says
7
+ * `target: Fn` and never learns that the compiler calls that node a `FnDecl`,
8
+ * which is the whole reason this vocabulary exists.
9
+ */
10
+ export type TargetKind = "Fn" | "Flow" | "Step" | "Binding" | "Type" | "Node";
11
+
12
+ /**
13
+ * The value a decorator body holds for its target: named members over the real
14
+ * declaration. Reading one may refuse, so the members are defined as getters.
15
+ */
16
+ export type TargetHandle = Record<string, unknown>;
17
+
18
+ /** What a handle is, for a caller holding one: its kind and every name it answers to. */
19
+ export interface HandleSurface {
20
+ readonly kind: TargetKind;
21
+ readonly offered: readonly string[];
22
+ }
23
+
24
+ /** How one member is built, given the node the handle stands for. */
25
+ export type Verb = (node: AstNode) => unknown;
26
+
27
+ /** A slice of the handle surface: what to read, and what to call. */
28
+ export interface VerbTable {
29
+ /** Members read as values: `.name`, `.params`, `.title`, `.value`, `.fields`. */
30
+ readonly props: Readonly<Record<string, Verb>>;
31
+ /** Members that do something: `.meta`, `.remove`, `.addParam`, `.wrap`. */
32
+ readonly calls: Readonly<Record<string, Verb>>;
33
+ }
@@ -0,0 +1,4 @@
1
+ export type { HandleSurface, TargetHandle, TargetKind, Verb, VerbTable } from "./handle.types.js";
2
+ export { isTargetKind, kindOf, TARGET_KINDS } from "./kind-of.js";
3
+ export { handleSurface, makeHandle } from "./make-handle.js";
4
+ export { missingVerb, verbRefusal } from "./missing-verb.js";
@@ -0,0 +1,36 @@
1
+ import type { AstNode } from "langium";
2
+ import type { TargetKind } from "./handle.types.js";
3
+
4
+ /** Every kind a `deco` may name, in the order a diagnostic should list them. */
5
+ export const TARGET_KINDS: readonly TargetKind[] = [
6
+ "Fn",
7
+ "Flow",
8
+ "Step",
9
+ "Binding",
10
+ "Type",
11
+ "Node",
12
+ ];
13
+
14
+ /**
15
+ * The only place the two vocabularies meet.
16
+ *
17
+ * `Node` is absent on purpose: it is not one node type, it is the absence of a
18
+ * restriction, so it maps to no `$type` and to every one.
19
+ */
20
+ const BY_TYPE: Readonly<Record<string, TargetKind>> = {
21
+ FnDecl: "Fn",
22
+ FlowDecl: "Flow",
23
+ StepDecl: "Step",
24
+ LetStmt: "Binding",
25
+ TypeDecl: "Type",
26
+ };
27
+
28
+ /** What kind of thing a node is, in the language's own words. */
29
+ export function kindOf(node: AstNode): TargetKind {
30
+ return BY_TYPE[node.$type] ?? "Node";
31
+ }
32
+
33
+ /** Whether a written type name is one of the kinds. */
34
+ export function isTargetKind(name: string): name is TargetKind {
35
+ return (TARGET_KINDS as readonly string[]).includes(name);
36
+ }
@@ -0,0 +1,100 @@
1
+ import type { AstNode } from "langium";
2
+ import { BODY_AROUND_VERBS, CALL_AROUND_VERBS } from "./around-verbs.js";
3
+ import { BINDING_VERBS } from "./binding-verbs.js";
4
+ import { COMMON_VERBS } from "./common-verbs.js";
5
+ import { FN_VERBS } from "./fn-verbs.js";
6
+ import type { HandleSurface, TargetHandle, TargetKind, VerbTable } from "./handle.types.js";
7
+ import { missingVerb } from "./missing-verb.js";
8
+ import { RUNNABLE_VERBS } from "./runnable-verbs.js";
9
+ import { TYPE_VERBS } from "./type-verbs.js";
10
+
11
+ /** What each kind offers, assembled from the slices it shares with the others. */
12
+ const SURFACE: Readonly<Record<TargetKind, readonly VerbTable[]>> = {
13
+ Fn: [COMMON_VERBS, FN_VERBS, CALL_AROUND_VERBS],
14
+ Flow: [COMMON_VERBS, RUNNABLE_VERBS, BODY_AROUND_VERBS],
15
+ Step: [COMMON_VERBS, RUNNABLE_VERBS, BODY_AROUND_VERBS],
16
+ Binding: [COMMON_VERBS, BINDING_VERBS],
17
+ Type: [COMMON_VERBS, TYPE_VERBS],
18
+ Node: [COMMON_VERBS],
19
+ };
20
+
21
+ /** Every verb any kind has, so a kind without one can say so instead of vanishing. */
22
+ const ALL_VERBS: readonly string[] = [
23
+ ...new Set(Object.values(SURFACE).flatMap((tables) => tables.flatMap(namesOf))),
24
+ ];
25
+
26
+ /**
27
+ * The typed mutable handle a decorator holds: verbs over the real declaration.
28
+ *
29
+ * Built like the native members of a list or a string, as a table of names and a
30
+ * value produced on read, because that is what it is: the built-in members of
31
+ * one more kind of value, whose receiver happens to be a piece of the program
32
+ * rather than a piece of its data.
33
+ *
34
+ * Every verb any kind has is installed. The ones this kind lacks throw a
35
+ * `ProblemError` naming the surface it does offer, so a reach for the wrong verb
36
+ * is refused rather than read as nothing.
37
+ */
38
+ export function makeHandle(args: { node: AstNode; kind: TargetKind }): TargetHandle {
39
+ const handle: TargetHandle = {};
40
+ const offered = install(handle, args);
41
+ for (const verb of ALL_VERBS) {
42
+ if (!offered.has(verb)) refuse(handle, { verb, kind: args.kind, offered });
43
+ }
44
+ brand(handle, { kind: args.kind, offered: [...offered] });
45
+ return handle;
46
+ }
47
+
48
+ const HANDLE = Symbol("venn.handle");
49
+
50
+ /**
51
+ * What this handle answers to, or nothing when the value is not one.
52
+ *
53
+ * A getter can only refuse a name some kind has; a name no kind has would still
54
+ * read as nothing. The caller that reaches for one asks here instead, so
55
+ * `target.wobble` is refused in the same sentence as `target.addParam`.
56
+ */
57
+ export function handleSurface(value: unknown): HandleSurface | undefined {
58
+ if (typeof value !== "object" || value === null) return undefined;
59
+ return (value as { [HANDLE]?: HandleSurface })[HANDLE];
60
+ }
61
+
62
+ function brand(handle: TargetHandle, surface: HandleSurface): void {
63
+ Object.defineProperty(handle, HANDLE, { value: surface });
64
+ }
65
+
66
+ function install(handle: TargetHandle, args: { node: AstNode; kind: TargetKind }): Set<string> {
67
+ const offered = new Set<string>();
68
+ for (const table of SURFACE[args.kind]) {
69
+ for (const [verb, make] of entriesOf(table)) {
70
+ offered.add(verb);
71
+ define(handle, verb, () => make(args.node));
72
+ }
73
+ }
74
+ return offered;
75
+ }
76
+
77
+ function refuse(
78
+ handle: TargetHandle,
79
+ args: { verb: string; kind: TargetKind; offered: Set<string> },
80
+ ): void {
81
+ define(handle, args.verb, () => {
82
+ throw missingVerb(args);
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Non-enumerable on purpose: a handle is verbs, not data, so serialising one
88
+ * must not run every getter, including the ones whose whole job is to refuse.
89
+ */
90
+ function define(handle: TargetHandle, verb: string, get: () => unknown): void {
91
+ Object.defineProperty(handle, verb, { get, configurable: true });
92
+ }
93
+
94
+ function entriesOf(table: VerbTable): [string, (node: AstNode) => unknown][] {
95
+ return [...Object.entries(table.props), ...Object.entries(table.calls)];
96
+ }
97
+
98
+ function namesOf(table: VerbTable): string[] {
99
+ return [...Object.keys(table.props), ...Object.keys(table.calls)];
100
+ }
@@ -0,0 +1,27 @@
1
+ import { buildProblem, CODES } from "../../codes/index.js";
2
+ import { ProblemError } from "../../problem/index.js";
3
+ import type { TargetKind } from "./handle.types.js";
4
+
5
+ /** Filled in by the body runner, which knows which statement asked. */
6
+ const NO_SPAN = { uri: "", offset: 0, length: 0, line: 1, column: 1 };
7
+
8
+ /**
9
+ * The verb this kind does not have, answered with the ones it does.
10
+ *
11
+ * Never `undefined`, never a `TypeError`: a decorator that reaches for
12
+ * `.addParam` on a flow has made a mistake the language can name, and naming it
13
+ * beside the whole surface of that kind is the shortest path to the fix.
14
+ */
15
+ export function missingVerb(args: {
16
+ verb: string;
17
+ kind: TargetKind;
18
+ offered: Iterable<string>;
19
+ }): ProblemError {
20
+ const has = [...args.offered].join(", ");
21
+ return verbRefusal(`A ${args.kind} has no \`${args.verb}\` — it has ${has}.`);
22
+ }
23
+
24
+ /** A verb this target cannot honour, for a reason of its own. */
25
+ export function verbRefusal(title: string): ProblemError {
26
+ return new ProblemError(buildProblem({ spec: CODES.VN2017_DECO_VERB, span: NO_SPAN, title }));
27
+ }
@@ -0,0 +1,13 @@
1
+ import type { FlowDecl, StepDecl } from "../../generated/ast.js";
2
+ import type { VerbTable } from "./handle.types.js";
3
+
4
+ /**
5
+ * What a flow and a step have beyond the common surface: the sentence they are
6
+ * known by. Read-only, because a title is what the reporter, the `--flow` filter
7
+ * and the trace all key off, and renaming it mid-expansion would leave every one
8
+ * of them pointing at a run nobody asked for.
9
+ */
10
+ export const RUNNABLE_VERBS: VerbTable = {
11
+ props: { title: (node) => (node as FlowDecl | StepDecl).title },
12
+ calls: {},
13
+ };
@@ -0,0 +1,56 @@
1
+ import { nativeFn } from "../../expr/index.js";
2
+ import type { FieldDecl, TypeBody, TypeDecl, TypeRef } from "../../generated/ast.js";
3
+ import type { VerbTable } from "./handle.types.js";
4
+ import { verbRefusal } from "./missing-verb.js";
5
+
6
+ /** The fields of a `type { … }`, and the two ways to change the set. */
7
+ export const TYPE_VERBS: VerbTable = {
8
+ props: { fields: (node) => (bodyOf(node as TypeDecl)?.fields ?? []).map((one) => one.name) },
9
+ calls: {
10
+ addField: (node) =>
11
+ nativeFn((args) => addField(node as TypeDecl, String(args[0]), String(args[1] ?? "string"))),
12
+ removeField: (node) => nativeFn((args) => removeField(node as TypeDecl, String(args[0]))),
13
+ },
14
+ };
15
+
16
+ function bodyOf(decl: TypeDecl): TypeBody | undefined {
17
+ return decl.body;
18
+ }
19
+
20
+ /** `type Id = string` has no fields to change, and says so rather than growing one. */
21
+ function requireBody(decl: TypeDecl, verb: string): TypeBody {
22
+ const body = bodyOf(decl);
23
+ if (body) return body;
24
+ throw verbRefusal(`\`${decl.name}\` is an alias, so \`${verb}\` has no fields to change.`);
25
+ }
26
+
27
+ function addField(decl: TypeDecl, name: string, type: string): null {
28
+ const body = requireBody(decl, "addField");
29
+ if (body.fields.some((field) => field.name === name)) return null;
30
+ body.fields.push(field({ body, name, type }));
31
+ return null;
32
+ }
33
+
34
+ function removeField(decl: TypeDecl, name: string): null {
35
+ const body = requireBody(decl, "removeField");
36
+ body.fields = body.fields.filter((one) => one.name !== name);
37
+ return null;
38
+ }
39
+
40
+ function field(args: { body: TypeBody; name: string; type: string }): FieldDecl {
41
+ return {
42
+ $type: "FieldDecl",
43
+ $container: args.body,
44
+ $containerProperty: "fields",
45
+ annotations: [],
46
+ name: args.name,
47
+ optional: false,
48
+ fieldType: typeRef(args.type),
49
+ } as unknown as FieldDecl;
50
+ }
51
+
52
+ function typeRef(name: string): TypeRef {
53
+ const ref = { $type: "TypeRef", members: [] } as unknown as TypeRef;
54
+ ref.members.push({ $type: "NamedType", $container: ref, name, args: [] } as unknown as never);
55
+ return ref;
56
+ }
@@ -0,0 +1,40 @@
1
+ export { acceptedKinds, decoTarget } from "./accepted-kinds.js";
2
+ export type {
3
+ DecoBodyArgs,
4
+ DecoSignature,
5
+ DocumentDecoArgs,
6
+ ImportedDeco,
7
+ SignatureResult,
8
+ } from "./deco/index.js";
9
+ export { decoCannotCall, decoDecorator, readSignature, withDocumentDecos } from "./deco/index.js";
10
+ export { decorateCallable } from "./decorate-callable.js";
11
+ export type { Decorations } from "./decorations.js";
12
+ export { AROUND_KEYS, addDecoration, readDecorations } from "./decorations.js";
13
+ export { expand } from "./expand.js";
14
+ export type {
15
+ DecoratedNode,
16
+ DecoratorDefinition,
17
+ DecoratorSource,
18
+ ExpandContext,
19
+ ExpandResult,
20
+ NodeMeta,
21
+ } from "./expand.types.js";
22
+ export type { HandleSurface, TargetHandle, TargetKind } from "./handles/index.js";
23
+ export {
24
+ handleSurface,
25
+ isTargetKind,
26
+ kindOf,
27
+ makeHandle,
28
+ TARGET_KINDS,
29
+ } from "./handles/index.js";
30
+ export { metaOf, readMeta, writeMeta } from "./node-meta.js";
31
+ export { spanOf } from "./node-span.js";
32
+ export { swapNode } from "./swap-node.js";
33
+ export {
34
+ everyKindWritten,
35
+ kindWords,
36
+ nodeWord,
37
+ wrongKind,
38
+ wrongKindTitle,
39
+ wrongTargetTitle,
40
+ } from "./wrong-kind.js";
@@ -0,0 +1,69 @@
1
+ import type { AstNode } from "langium";
2
+ import { buildProblem } from "../codes/index.js";
3
+ import { evaluate } from "../expr/index.js";
4
+ import type { Annotation } from "../generated/ast.js";
5
+ import { isRef } from "../generated/ast.js";
6
+ import type { Problem } from "../problem/index.js";
7
+ import type { ExpandContext } from "./expand.types.js";
8
+ import { writeMeta } from "./node-meta.js";
9
+ import { spanOf } from "./node-span.js";
10
+ import { swapNode } from "./swap-node.js";
11
+
12
+ /**
13
+ * The handle one decorator gets on one node.
14
+ *
15
+ * `replace` and `remove` write straight into the parent's own array or field.
16
+ * That is the tree the checker and the runtime read, not a copy of it, so a
17
+ * decorator's rewrite is indistinguishable downstream from source the author
18
+ * wrote by hand.
19
+ */
20
+ export function makeContext(args: {
21
+ node: AstNode;
22
+ annotation: Annotation;
23
+ uri?: string;
24
+ problems: Problem[];
25
+ }): ExpandContext {
26
+ const { node, annotation } = args;
27
+ return {
28
+ node,
29
+ parent: node.$container,
30
+ args: evaluatedArgs(annotation),
31
+ // The expressions as written, for a decorator that needs the tree and not
32
+ // the value: a macro reads its argument, it does not compute it.
33
+ written: (annotation.args?.args ?? []).map((arg) => arg.value),
34
+ replace: (next) => swapNode(node, next),
35
+ remove: () => swapNode(node, undefined),
36
+ meta: (key, value) => writeMeta(node, key, value),
37
+ reject: (rejection) => args.problems.push(problemOf({ ...args, ...rejection })),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * A decorator's arguments are evaluated once, here, against nothing.
43
+ *
44
+ * They are written before the program runs and cannot depend on it: `@retry(2)`
45
+ * is part of the shape of the program, not of its execution. That is also why a
46
+ * bare name inside one is a *word* and not a variable. `@tags(smoke)` and
47
+ * `@scope(worker)` name a tag and a lifetime, and nothing exists yet for them to
48
+ * refer to.
49
+ */
50
+ function evaluatedArgs(annotation: Annotation): unknown[] {
51
+ const nothing = { lookup: () => undefined };
52
+ return (annotation.args?.args ?? []).map((arg) =>
53
+ isRef(arg.value) ? arg.value.name : evaluate(arg.value, nothing),
54
+ );
55
+ }
56
+
57
+ /** A decorator names its own code, so a plugin can refuse a program in its own terms. */
58
+ function problemOf(args: {
59
+ annotation: Annotation;
60
+ uri?: string;
61
+ code: string;
62
+ title: string;
63
+ }): Problem {
64
+ return buildProblem({
65
+ spec: { code: args.code, severity: "error" },
66
+ span: spanOf(args.annotation, args.uri ?? "memory://inline.vn"),
67
+ title: args.title,
68
+ });
69
+ }
@@ -0,0 +1,30 @@
1
+ import type { NodeMeta } from "./expand.types.js";
2
+
3
+ /** Where a decorator's leavings live on the node. */
4
+ const META = "$meta";
5
+
6
+ /**
7
+ * What the decorators left on this node, or nothing.
8
+ *
9
+ * Metadata rather than a rewrite is how a decorator says something the grammar
10
+ * has no word for. `@retry(2)` cannot be expressed as a tree of existing
11
+ * statements, so it is expressed as a fact about one.
12
+ */
13
+ export function metaOf(node: object): NodeMeta | undefined {
14
+ return (node as { $meta?: NodeMeta }).$meta;
15
+ }
16
+
17
+ /** Read one key, typed by the caller who knows what it wrote. */
18
+ export function readMeta<T>(node: object, key: string): T | undefined {
19
+ return metaOf(node)?.[key] as T | undefined;
20
+ }
21
+
22
+ /** Attach a fact to a node. Non-enumerable, so it never lands in a serialised AST. */
23
+ export function writeMeta(node: object, key: string, value: unknown): void {
24
+ const existing = metaOf(node);
25
+ if (existing) {
26
+ existing[key] = value;
27
+ return;
28
+ }
29
+ Object.defineProperty(node, META, { value: { [key]: value }, configurable: true });
30
+ }
@@ -0,0 +1,17 @@
1
+ import type { AstNode } from "langium";
2
+ import type { Span } from "../problem/index.js";
3
+
4
+ /** Where a node sits in its file, for a Problem that has to point at it. */
5
+ export function spanOf(node: AstNode, uri: string): Span {
6
+ const cst = node.$cstNode as
7
+ | { offset: number; length: number; range?: { start: { line: number; character: number } } }
8
+ | undefined;
9
+ const start = cst?.range?.start;
10
+ return {
11
+ uri,
12
+ offset: cst?.offset ?? 0,
13
+ length: cst?.length ?? 0,
14
+ line: (start?.line ?? 0) + 1,
15
+ column: (start?.character ?? 0) + 1,
16
+ };
17
+ }
@@ -0,0 +1,28 @@
1
+ import type { AstNode } from "langium";
2
+
3
+ /**
4
+ * Put `next` where `node` is, in its parent's list or in the field holding it.
5
+ * Passing nothing takes the node out of the program entirely.
6
+ *
7
+ * The write goes straight into the parent's own array or field. That is the tree
8
+ * the checker and the runtime read, not a copy of it, so a decorator's rewrite
9
+ * is indistinguishable downstream from source the author wrote.
10
+ */
11
+ export function swapNode(node: AstNode, next: AstNode | undefined): void {
12
+ const parent = node.$container as Record<string, unknown> | undefined;
13
+ const property = node.$containerProperty;
14
+ if (!parent || !property) return;
15
+ const held = parent[property];
16
+ if (Array.isArray(held)) {
17
+ swapInList(held, node, next);
18
+ return;
19
+ }
20
+ parent[property] = next;
21
+ }
22
+
23
+ function swapInList(list: unknown[], node: AstNode, next: AstNode | undefined): void {
24
+ const at = list.indexOf(node);
25
+ if (at < 0) return;
26
+ if (next) list[at] = next;
27
+ else list.splice(at, 1);
28
+ }
@@ -0,0 +1,102 @@
1
+ import type { AstNode } from "langium";
2
+ import { kindOf, TARGET_KINDS, type TargetKind } from "./handles/index.js";
3
+
4
+ /** What each kind is called in a sentence someone reads. */
5
+ const KIND_WORDS: Readonly<Record<TargetKind, string>> = {
6
+ Fn: "a function",
7
+ Flow: "a flow",
8
+ Step: "a step",
9
+ Binding: "a binding",
10
+ Type: "a type",
11
+ Node: "anything",
12
+ };
13
+
14
+ /**
15
+ * What a node is called in that same sentence.
16
+ *
17
+ * Wider than {@link KIND_WORDS} on purpose: a fragment and a group have no kind
18
+ * of their own, but a message that cannot name what the author actually wrote
19
+ * is a message that sends them looking for it.
20
+ */
21
+ const NODE_WORDS: Readonly<Record<string, string>> = {
22
+ FnDecl: "a function",
23
+ FlowDecl: "a flow",
24
+ StepDecl: "a step",
25
+ GroupDecl: "a group",
26
+ LetStmt: "a binding",
27
+ TypeDecl: "a type",
28
+ FragmentDecl: "a fragment",
29
+ DecoDecl: "a decorator",
30
+ FieldDecl: "a field",
31
+ Param: "a parameter",
32
+ ActionCall: "a call",
33
+ };
34
+
35
+ /** A list of alternatives, read the way it is spoken: "a flow, a step or a group". */
36
+ function oneOf(words: readonly string[]): string {
37
+ const all = [...words];
38
+ const last = all.pop();
39
+ return all.length === 0 ? (last ?? "") : `${all.join(", ")} or ${last}`;
40
+ }
41
+
42
+ /** The kinds a signature allows, read as prose: "a function or a flow". */
43
+ export function kindWords(kinds: readonly TargetKind[]): string {
44
+ return oneOf(kinds.map((kind) => KIND_WORDS[kind]));
45
+ }
46
+
47
+ /** Every kind, listed for an author who has to pick one. */
48
+ export function everyKindWritten(): string {
49
+ return oneOf(TARGET_KINDS);
50
+ }
51
+
52
+ /**
53
+ * What a node is called for someone reading a message.
54
+ *
55
+ * A plugin's decorator may name any node it is able to read, so an unlisted one
56
+ * has its word derived rather than printed: `FlowDecl` is the compiler talking
57
+ * to itself, and a word nobody ever typed has no business on a user's screen.
58
+ */
59
+ export function nodeWord(type: string): string {
60
+ return NODE_WORDS[type] ?? `a ${type.replace(/(Decl|Stmt|Expr|Lit)$/, "").toLowerCase()}`;
61
+ }
62
+
63
+ /** "@memoize decorates a function, and this is a flow." */
64
+ export function wrongKindTitle(args: {
65
+ name: string;
66
+ kinds: readonly TargetKind[];
67
+ node: AstNode;
68
+ }): string {
69
+ return `@${args.name} decorates ${kindWords(args.kinds)}, and this is ${nodeWord(args.node.$type)}.`;
70
+ }
71
+
72
+ /**
73
+ * The same sentence for a plugin's decorator, whose reach is a list of node
74
+ * types. It still reads as prose: what the author wrote is what they are told
75
+ * about, and the list they never wrote stays out of it.
76
+ */
77
+ export function wrongTargetTitle(args: {
78
+ name: string;
79
+ targets: readonly string[];
80
+ node: AstNode;
81
+ }): string {
82
+ const allowed = oneOf([...new Set(args.targets.map(nodeWord))]);
83
+ return `@${args.name} decorates ${allowed}, and this is ${nodeWord(args.node.$type)}.`;
84
+ }
85
+
86
+ /**
87
+ * Why a `@name` does not belong on this node, if it does not.
88
+ *
89
+ * Nothing when the signature never said which kinds it takes: that is a fault
90
+ * committed once, where the `deco` is written, and repeating it at every use
91
+ * site would bury the one message that can be acted on.
92
+ */
93
+ export function wrongKind(args: {
94
+ name: string;
95
+ kinds: readonly TargetKind[];
96
+ node: AstNode;
97
+ }): string | undefined {
98
+ const { kinds, node } = args;
99
+ if (kinds.length === 0 || kinds.includes("Node") || kinds.includes(kindOf(node)))
100
+ return undefined;
101
+ return wrongKindTitle(args);
102
+ }