@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,24 @@
1
+ import type { EvalEnv } from "../../expr/index.js";
2
+ import { PRELUDE_VALUES } from "../../expr/index.js";
3
+
4
+ /**
5
+ * What a decorator body can see: its own parameters, its own `let`s, and the
6
+ * prelude.
7
+ *
8
+ * Nothing else, and deliberately so. The body runs before the program exists: no
9
+ * flow has started and no plugin has been asked for anything, so a name it did
10
+ * not bind itself cannot mean anything yet. The runner says exactly that instead
11
+ * of quietly evaluating to nothing.
12
+ */
13
+ export class DecoEnv implements EvalEnv {
14
+ constructor(private readonly bindings: Record<string, unknown>) {}
15
+
16
+ lookup(name: string): unknown {
17
+ if (Object.hasOwn(this.bindings, name)) return this.bindings[name];
18
+ return PRELUDE_VALUES[name];
19
+ }
20
+
21
+ bind(name: string, value: unknown): void {
22
+ this.bindings[name] = value;
23
+ }
24
+ }
@@ -0,0 +1,43 @@
1
+ import type { Block, DecoDecl } from "../../generated/ast.js";
2
+ import type { Problem } from "../../problem/index.js";
3
+ import type { TargetKind } from "../handles/index.js";
4
+ import type { DecoEnv } from "./deco-env.js";
5
+
6
+ /**
7
+ * A `deco`'s parameter list, read as what it means: the first parameter is the
8
+ * thing decorated and its type is what may be decorated; the rest are the
9
+ * decorator's own arguments, bound from `@name(…)`.
10
+ */
11
+ export interface DecoSignature {
12
+ /** The name the body calls its target by. */
13
+ readonly target: string;
14
+ /** The kinds the declared type allows. Several when it is a union. */
15
+ readonly kinds: readonly TargetKind[];
16
+ /** The remaining parameter names, in the order `@name(…)` fills them. */
17
+ readonly args: readonly string[];
18
+ }
19
+
20
+ /** A signature that reads, or the one-line reason it does not. */
21
+ export type SignatureResult =
22
+ | { readonly ok: true; readonly signature: DecoSignature }
23
+ | { readonly ok: false; readonly title: string };
24
+
25
+ /**
26
+ * A `pub deco` another file exported, with the file it was written in.
27
+ *
28
+ * The uri travels with the declaration because a fault in it (a signature that
29
+ * does not read, a verb its kind has not got) belongs to the line that wrote it,
30
+ * not to the line that imported it.
31
+ */
32
+ export interface ImportedDeco {
33
+ readonly decl: DecoDecl;
34
+ readonly uri: string;
35
+ }
36
+
37
+ /** Everything running one decorator's body needs. */
38
+ export interface DecoBodyArgs {
39
+ readonly body: Block;
40
+ readonly env: DecoEnv;
41
+ readonly uri: string;
42
+ readonly reject: (problem: Problem) => void;
43
+ }
@@ -0,0 +1,64 @@
1
+ import type { Document } from "../../generated/ast.js";
2
+ import { isDecoDecl } from "../../generated/ast.js";
3
+ import type { Problem } from "../../problem/index.js";
4
+ import type { DecoratorDefinition, DecoratorSource } from "../expand.types.js";
5
+ import type { ImportedDeco } from "./deco.types.js";
6
+ import { decoDecorator } from "./deco-decorator.js";
7
+
8
+ /** Every `deco` one document can reach, and the file it is being expanded for. */
9
+ export interface DocumentDecoArgs {
10
+ document: Document;
11
+ decorators: DecoratorSource;
12
+ uri: string;
13
+ problems: Problem[];
14
+ /** The `pub deco`s this file's imports reach, by name. */
15
+ imported?: ReadonlyMap<string, ImportedDeco>;
16
+ }
17
+
18
+ /**
19
+ * The document's own `deco`s, then the ones it imported, layered over whatever
20
+ * the host contributed.
21
+ *
22
+ * A local declaration wins against a plugin's and against a built-in of the same
23
+ * name: the more local one is what the author is looking at. All of them arrive
24
+ * as ordinary members of one {@link DecoratorSource}, so `@memoize` resolves
25
+ * without expansion ever asking where a decorator came from.
26
+ */
27
+ export function withDocumentDecos(args: DocumentDecoArgs): DecoratorSource {
28
+ const own = ownDecos(args);
29
+ const shared = sharedDecos(args);
30
+ if (own.size === 0 && !shared) return args.decorators;
31
+ return { get: (name) => own.get(name) ?? shared?.(name) ?? args.decorators.get(name) };
32
+ }
33
+
34
+ /** Built up front: a signature that does not read is reported where it is written. */
35
+ function ownDecos(args: DocumentDecoArgs): Map<string, DecoratorDefinition> {
36
+ const own = new Map<string, DecoratorDefinition>();
37
+ for (const decl of args.document.decls) {
38
+ if (isDecoDecl(decl)) own.set(decl.name, decoDecorator({ ...args, decl }));
39
+ }
40
+ return own;
41
+ }
42
+
43
+ type Found = DecoratorDefinition | undefined;
44
+
45
+ /**
46
+ * Built on first use, and remembered.
47
+ *
48
+ * An imported file exports every `pub deco` it has, and this one may write none
49
+ * of them: reading a signature nobody asked for would charge another file's
50
+ * fault to this one, for a decorator that was never applied.
51
+ */
52
+ function sharedDecos(args: DocumentDecoArgs): ((name: string) => Found) | undefined {
53
+ const imported = args.imported;
54
+ if (!imported || imported.size === 0) return undefined;
55
+ const built = new Map<string, Found>();
56
+ return (name) => {
57
+ if (built.has(name)) return built.get(name);
58
+ const from = imported.get(name);
59
+ // Its own uri, so a fault in it points at the line that wrote it.
60
+ const made = from && decoDecorator({ ...args, decl: from.decl, uri: from.uri });
61
+ built.set(name, made);
62
+ return made;
63
+ };
64
+ }
@@ -0,0 +1,7 @@
1
+ export type { DecoBodyArgs, DecoSignature, ImportedDeco, SignatureResult } from "./deco.types.js";
2
+ export { decoDecorator } from "./deco-decorator.js";
3
+ export { DecoEnv } from "./deco-env.js";
4
+ export type { DocumentDecoArgs } from "./document-decos.js";
5
+ export { withDocumentDecos } from "./document-decos.js";
6
+ export { readSignature } from "./read-signature.js";
7
+ export { impure as decoCannotCall, runDecoBody } from "./run-body.js";
@@ -0,0 +1,57 @@
1
+ import type { DecoDecl, Param, TypeRef } from "../../generated/ast.js";
2
+ import { acceptedKinds, decoTarget } from "../accepted-kinds.js";
3
+ import type { TargetKind } from "../handles/index.js";
4
+ import { everyKindWritten } from "../wrong-kind.js";
5
+ import type { SignatureResult } from "./deco.types.js";
6
+
7
+ /**
8
+ * Read a `deco`'s parameters as what they mean.
9
+ *
10
+ * The first parameter *is* the target and its type is what may carry the
11
+ * decorator; the rest are the decorator's own arguments, filled by `@name(…)` in
12
+ * order. Nobody writing a decorator ever names a node of the compiler's tree,
13
+ * and what it decorates follows from the signature rather than from a list kept
14
+ * beside it.
15
+ *
16
+ * @returns the signature, or the one-line reason it does not read.
17
+ */
18
+ export function readSignature(decl: DecoDecl): SignatureResult {
19
+ const target = decoTarget(decl);
20
+ if (!target) return { ok: false, title: needsTarget(decl.name) };
21
+ if (!target.paramType) return { ok: false, title: needsType(decl.name, target.name) };
22
+ const kinds = acceptedKinds(decl);
23
+ // A union may name several kinds; one word that is not a kind spoils it all.
24
+ if (kinds.length !== target.paramType.members.length) {
25
+ return { ok: false, title: notAKind(decl.name, target.paramType) };
26
+ }
27
+ return signature(target, kinds, decl.params?.params ?? []);
28
+ }
29
+
30
+ function signature(
31
+ target: Param,
32
+ kinds: readonly TargetKind[],
33
+ params: readonly Param[],
34
+ ): SignatureResult {
35
+ return {
36
+ ok: true,
37
+ signature: { target: target.name, kinds, args: params.slice(1).map((one) => one.name) },
38
+ };
39
+ }
40
+
41
+ function written(type: TypeRef): string {
42
+ return type.members.map((member) => (member as { name?: string }).name ?? "?").join(" | ");
43
+ }
44
+
45
+ function needsTarget(name: string): string {
46
+ return `\`deco ${name}\` needs a first parameter — the thing it decorates.`;
47
+ }
48
+
49
+ function needsType(name: string, target: string): string {
50
+ const kinds = everyKindWritten();
51
+ return `\`deco ${name}\` must say what it decorates: give \`${target}\` a type — ${kinds}.`;
52
+ }
53
+
54
+ function notAKind(name: string, type: TypeRef): string {
55
+ const kinds = everyKindWritten();
56
+ return `\`deco ${name}\` decorates \`${written(type)}\`, which is not a kind — say ${kinds}.`;
57
+ }
@@ -0,0 +1,119 @@
1
+ import type { AstNode } from "langium";
2
+ import { callArgs } from "../../ast/index.js";
3
+ import { buildProblem, CODES } from "../../codes/index.js";
4
+ import { evaluate, invoke, memberValue } from "../../expr/index.js";
5
+ import type { ActionCall, Block, IfStmt, LetStmt, Statement } from "../../generated/ast.js";
6
+ import { type Problem, ProblemError } from "../../problem/index.js";
7
+ import { truthy } from "../../value/index.js";
8
+ import { handleSurface, missingVerb } from "../handles/index.js";
9
+ import { spanOf } from "../node-span.js";
10
+ import type { DecoBodyArgs } from "./deco.types.js";
11
+
12
+ /**
13
+ * Run a decorator's body, at expansion time, over the handle it was given.
14
+ *
15
+ * It understands `let`/`const`, `if`, and a call on something it can already
16
+ * see. Nothing else, because nothing else exists yet: a call whose head it
17
+ * cannot find is a plugin's verb, and saying so is more use than a lookup that
18
+ * quietly yields nothing and a `TypeError` two lines later.
19
+ *
20
+ * Nothing is thrown. Every refusal reaches `args.reject` as a `Problem` pointing
21
+ * at the statement that caused it.
22
+ */
23
+ export function runDecoBody(args: DecoBodyArgs): void {
24
+ runStmts(args.body.stmts, args);
25
+ }
26
+
27
+ function runStmts(stmts: readonly Statement[], args: DecoBodyArgs): void {
28
+ for (const stmt of stmts) {
29
+ try {
30
+ dispatch(stmt, args);
31
+ } catch (error) {
32
+ args.reject(located(error, stmt, args.uri));
33
+ }
34
+ }
35
+ }
36
+
37
+ function dispatch(stmt: Statement, args: DecoBodyArgs): void {
38
+ if (stmt.$type === "LetStmt") {
39
+ bind(stmt as LetStmt, args);
40
+ } else if (stmt.$type === "ActionCall") {
41
+ callVerb(stmt as ActionCall, args);
42
+ } else if (stmt.$type === "IfStmt") {
43
+ branch(stmt as IfStmt, args);
44
+ } else {
45
+ throw refuse(UNSUPPORTED);
46
+ }
47
+ }
48
+
49
+ /** `const cache = {}`. Trailing arguments would make it an action, which it cannot be. */
50
+ function bind(stmt: LetStmt, args: DecoBodyArgs): void {
51
+ if (stmt.args.length > 0 || stmt.opts) throw refuse(IMPURE_LET);
52
+ args.env.bind(stmt.name, evaluate(stmt.value, args.env));
53
+ }
54
+
55
+ /** `target.wrap(f)`, `target.meta "retry" 3`: a verb on something already in hand. */
56
+ function callVerb(stmt: ActionCall, args: DecoBodyArgs): void {
57
+ const path = stmt.target.split(".");
58
+ const root = args.env.lookup(path[0] as string);
59
+ if (root === undefined) throw refuse(impure(stmt.target));
60
+ const callee = path.slice(1).reduce<unknown>(reach, root);
61
+ invoke(callee, values(stmt, args));
62
+ }
63
+
64
+ /** A handle answers only to its own verbs; anything else is refused by name. */
65
+ function reach(held: unknown, name: string): unknown {
66
+ const surface = handleSurface(held);
67
+ if (surface && !surface.offered.includes(name)) {
68
+ throw missingVerb({ verb: name, kind: surface.kind, offered: surface.offered });
69
+ }
70
+ return memberValue(held, name);
71
+ }
72
+
73
+ function values(stmt: ActionCall, args: DecoBodyArgs): unknown[] {
74
+ const given = callArgs(stmt).map((arg) => evaluate(arg, args.env));
75
+ if (stmt.opts) given.push(evaluate(stmt.opts, args.env));
76
+ return given;
77
+ }
78
+
79
+ function branch(stmt: IfStmt, args: DecoBodyArgs): void {
80
+ if (truthy(evaluate(stmt.cond, args.env))) {
81
+ runStmts(stmt.then.stmts, args);
82
+ return;
83
+ }
84
+ const other = stmt.otherwise;
85
+ if (!other) return;
86
+ // `else if` is another statement; a plain `else` is a block of them.
87
+ if (other.$type === "Block") runStmts((other as Block).stmts, args);
88
+ else dispatch(other as Statement, args);
89
+ }
90
+
91
+ const IMPURE_LET =
92
+ "A decorator runs before the program exists, so this binding cannot call an action.";
93
+
94
+ const UNSUPPORTED =
95
+ "A decorator body understands `let`, `const`, `if` and verbs on what it was given — this is none of them.";
96
+
97
+ /**
98
+ * The one sentence for a verb a decorator cannot reach.
99
+ *
100
+ * Exported because the static pass notices the same thing without ever running
101
+ * the body, and two wordings for one fact is how they start to disagree.
102
+ */
103
+ export function impure(target: string): string {
104
+ return `A decorator runs before the program exists, so it cannot call \`${target}\`.`;
105
+ }
106
+
107
+ /** Located later, by the statement that asked. */
108
+ const NO_SPAN = { uri: "", offset: 0, length: 0, line: 1, column: 1 };
109
+
110
+ function refuse(title: string): ProblemError {
111
+ return new ProblemError(buildProblem({ spec: CODES.VN2016_DECO_IMPURE, span: NO_SPAN, title }));
112
+ }
113
+
114
+ /** Whatever went wrong, pointed at the line of the body that asked for it. */
115
+ function located(error: unknown, stmt: AstNode, uri: string): Problem {
116
+ if (error instanceof ProblemError) return { ...error.problem, span: spanOf(stmt, uri) };
117
+ const title = (error as { message?: string })?.message ?? String(error);
118
+ return buildProblem({ spec: CODES.VN2016_DECO_IMPURE, span: spanOf(stmt, uri), title });
119
+ }
@@ -0,0 +1,49 @@
1
+ import { invoke, nativeFn } from "../expr/index.js";
2
+ import { type Decorations, readDecorations } from "./decorations.js";
3
+
4
+ /** A call, reduced to the one thing a middleware needs: values in, value out. */
5
+ type Call = (values: readonly unknown[]) => unknown;
6
+
7
+ /**
8
+ * The callable a decorated `fn` binds to: its own body with every `.wrap`,
9
+ * `.before` and `.after` a `deco` asked for already around it.
10
+ *
11
+ * An untouched function is handed straight back, so nothing pays for a feature
12
+ * it did not use.
13
+ */
14
+ export function decorateCallable(node: object, base: unknown): unknown {
15
+ const around = readDecorations(node);
16
+ if (!around) return base;
17
+ const chain = wrapChain(around.wrap, base);
18
+ return nativeFn((values) => aroundCall({ chain, around, values }));
19
+ }
20
+
21
+ function aroundCall(args: {
22
+ chain: Call;
23
+ around: Decorations;
24
+ values: readonly unknown[];
25
+ }): unknown {
26
+ for (const fn of args.around.before) invoke(fn, [args.values]);
27
+ const result = args.chain(args.values);
28
+ for (const fn of args.around.after) invoke(fn, [args.values, result]);
29
+ return result;
30
+ }
31
+
32
+ /** The first-written wrap ends up outermost: it decides whether the rest runs. */
33
+ function wrapChain(wraps: readonly unknown[], base: unknown): Call {
34
+ let next: Call = (values) => invoke(base, values);
35
+ for (let at = wraps.length - 1; at >= 0; at -= 1) next = oneWrap(wraps[at], next);
36
+ return next;
37
+ }
38
+
39
+ function oneWrap(wrap: unknown, inner: Call): Call {
40
+ const through = nativeFn((given) => inner(passedOn(given)));
41
+ return (values) => invoke(wrap, [through, values]);
42
+ }
43
+
44
+ /** `call(args)` passes the list straight along; `call(a, b)` passes both. */
45
+ function passedOn(given: readonly unknown[]): readonly unknown[] {
46
+ const first = given[0];
47
+ if (given.length === 1 && Array.isArray(first)) return first;
48
+ return given;
49
+ }
@@ -0,0 +1,41 @@
1
+ import { readMeta, writeMeta } from "./node-meta.js";
2
+
3
+ /**
4
+ * Where `.wrap`, `.before` and `.after` leave their closures.
5
+ *
6
+ * Prefixed, because `meta` is open to any key a decorator invents and a program
7
+ * that writes `target.meta "before" x` must not be mistaken for one that wrote
8
+ * `target.before(f)`.
9
+ */
10
+ export const AROUND_KEYS = {
11
+ wrap: "deco.wrap",
12
+ before: "deco.before",
13
+ after: "deco.after",
14
+ } as const;
15
+
16
+ /** The closures a `deco` asked to run around this declaration. */
17
+ export interface Decorations {
18
+ readonly wrap: readonly unknown[];
19
+ readonly before: readonly unknown[];
20
+ readonly after: readonly unknown[];
21
+ }
22
+
23
+ /**
24
+ * What the behavioural verbs left on this node, or nothing at all.
25
+ *
26
+ * The language has no syntax for "around this body", so `.wrap` cannot rewrite
27
+ * the tree the way `.addParam` does. It records a fact about the declaration
28
+ * and the scheduler honours it.
29
+ */
30
+ export function readDecorations(node: object): Decorations | undefined {
31
+ const wrap = readMeta<unknown[]>(node, AROUND_KEYS.wrap);
32
+ const before = readMeta<unknown[]>(node, AROUND_KEYS.before);
33
+ const after = readMeta<unknown[]>(node, AROUND_KEYS.after);
34
+ if (!wrap && !before && !after) return undefined;
35
+ return { wrap: wrap ?? [], before: before ?? [], after: after ?? [] };
36
+ }
37
+
38
+ /** Append one closure to a behavioural list, keeping the order they were written. */
39
+ export function addDecoration(node: object, key: string, fn: unknown): void {
40
+ writeMeta(node, key, [...(readMeta<unknown[]>(node, key) ?? []), fn]);
41
+ }
@@ -0,0 +1,108 @@
1
+ import type { AstNode } from "langium";
2
+ import { walkAst } from "../ast/index.js";
3
+ import { buildProblem, CODES } from "../codes/index.js";
4
+ import type { Annotation, Document } from "../generated/ast.js";
5
+ import type { Problem } from "../problem/index.js";
6
+ import type { ImportedDeco } from "./deco/index.js";
7
+ import { withDocumentDecos } from "./deco/index.js";
8
+ import type { DecoratorDefinition, DecoratorSource, ExpandResult } from "./expand.types.js";
9
+ import { makeContext } from "./make-context.js";
10
+ import { spanOf } from "./node-span.js";
11
+ import { wrongKind, wrongTargetTitle } from "./wrong-kind.js";
12
+
13
+ /** What one decorated node needs for its decorators to run. */
14
+ interface Site {
15
+ node: AstNode & { annotations: Annotation[] };
16
+ decorators: DecoratorSource;
17
+ uri: string;
18
+ problems: Problem[];
19
+ }
20
+
21
+ /**
22
+ * Run every decorator the program wrote, before anything else reads the program.
23
+ *
24
+ * This is the one place a `@name` means anything. The kernel does not know what
25
+ * `@retry` is; it knows that a name written with an `@` is looked up, handed the
26
+ * node it sits on, and allowed to rewrite it. Built-in decorators come through
27
+ * here too, so there is no shorter path for them.
28
+ *
29
+ * Applied innermost first, so a decorator that rewrites a body finds a body its
30
+ * own decorators have already finished with.
31
+ *
32
+ * A `deco` the document declares, or one imported from a file that wrote `pub`,
33
+ * joins the same source the built-ins live in before any of them run. Nothing
34
+ * below here can tell whether `@memoize` was written in TypeScript, in this
35
+ * file, or in the one next to it.
36
+ *
37
+ * @returns the problems raised. The document itself is rewritten in place.
38
+ */
39
+ export function expand(args: {
40
+ document: Document;
41
+ decorators: DecoratorSource;
42
+ uri?: string;
43
+ /** The `pub deco`s this file's imports reach, by name. */
44
+ imported?: ReadonlyMap<string, ImportedDeco>;
45
+ }): ExpandResult {
46
+ const problems: Problem[] = [];
47
+ const uri = args.uri ?? "memory://inline.vn";
48
+ const decorators = withDocumentDecos({ ...args, uri, problems });
49
+ const sites = walkAst(args.document).filter(decorated);
50
+ for (const node of sites.reverse()) {
51
+ applyAll({ node, decorators, uri, problems });
52
+ }
53
+ return { problems };
54
+ }
55
+
56
+ function decorated(node: AstNode): node is AstNode & { annotations: Annotation[] } {
57
+ return ((node as { annotations?: Annotation[] }).annotations ?? []).length > 0;
58
+ }
59
+
60
+ function applyAll(site: Site): void {
61
+ // A copy: a decorator is allowed to rewrite the list it is standing in.
62
+ for (const annotation of [...site.node.annotations]) applyOne(site, annotation);
63
+ }
64
+
65
+ function applyOne(site: Site, annotation: Annotation): void {
66
+ const found = site.decorators.get(annotation.name);
67
+ if (!found) {
68
+ refuse(site, annotation, unknownTitle(annotation), CODES.VN2013_UNKNOWN_DECORATOR.code);
69
+ return;
70
+ }
71
+ const misplaced = wrongPlace(found, site.node, annotation);
72
+ if (misplaced) {
73
+ refuse(site, annotation, misplaced, CODES.VN2014_DECORATOR_TARGET.code);
74
+ return;
75
+ }
76
+ found.expand(
77
+ makeContext({ node: site.node, annotation, uri: site.uri, problems: site.problems }),
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Why a decorator does not belong here, if it does not.
83
+ *
84
+ * A `deco` says what it decorates by typing its first parameter, so the answer
85
+ * is phrased in the words the author used. A plugin's decorator still names node
86
+ * types, because it is handed the raw node and nothing shorter describes what it
87
+ * is able to read.
88
+ */
89
+ function wrongPlace(
90
+ found: DecoratorDefinition,
91
+ node: AstNode,
92
+ annotation: Annotation,
93
+ ): string | undefined {
94
+ const name = annotation.name;
95
+ if (found.accepts) return wrongKind({ name, kinds: found.accepts, node });
96
+ if (!found.targets || found.targets.includes(node.$type)) return undefined;
97
+ return wrongTargetTitle({ name, targets: found.targets, node });
98
+ }
99
+
100
+ function unknownTitle(annotation: Annotation): string {
101
+ return `No decorator is named "@${annotation.name}".`;
102
+ }
103
+
104
+ function refuse(site: Site, annotation: Annotation, title: string, code: string): void {
105
+ site.problems.push(
106
+ buildProblem({ spec: { code, severity: "error" }, span: spanOf(annotation, site.uri), title }),
107
+ );
108
+ }
@@ -0,0 +1,78 @@
1
+ import type { AstNode } from "langium";
2
+ import type { Problem } from "../problem/index.js";
3
+ import type { TargetKind } from "./handles/index.js";
4
+
5
+ /**
6
+ * A node a decorator may be attached to, as the decorator sees it.
7
+ *
8
+ * Deliberately structural: a decorator receives the real tree, not a copy and
9
+ * not a facade, so it can read anything the grammar produced and rewrite it.
10
+ * `$type` is how it knows what it is holding.
11
+ */
12
+ export type DecoratedNode = AstNode;
13
+
14
+ /** What a decorator writes for the runtime to read later. */
15
+ export type NodeMeta = Record<string, unknown>;
16
+
17
+ /**
18
+ * Everything a decorator is handed when it runs.
19
+ *
20
+ * Expansion happens once, between parsing and everything else, so a decorator
21
+ * sees the whole program before a single statement has run. What it leaves
22
+ * behind is what the checker checks and the runtime runs.
23
+ */
24
+ export interface ExpandContext {
25
+ /** The node carrying this decorator. */
26
+ readonly node: DecoratedNode;
27
+ /** The arguments, evaluated: `@retry(2, { backoff: 500ms })` gives `[2, {…}]`. */
28
+ readonly args: readonly unknown[];
29
+ /** The same arguments as syntax, for a decorator that transforms rather than reads. */
30
+ readonly written: readonly DecoratedNode[];
31
+ /** Where the node sits, for a decorator that needs to look around or reattach. */
32
+ readonly parent: DecoratedNode | undefined;
33
+ /** Put this node in place of the decorated one. */
34
+ replace(node: DecoratedNode): void;
35
+ /** Take the decorated node out of the tree entirely. */
36
+ remove(): void;
37
+ /** Leave something for the runtime, keyed by name. */
38
+ meta(key: string, value: unknown): void;
39
+ /** Refuse the program, in the language's own error vocabulary. */
40
+ reject(args: { code: string; title: string }): void;
41
+ }
42
+
43
+ /**
44
+ * A decorator: a named transformation of the tree, contributed by the kernel or
45
+ * by a plugin, and applied wherever a program writes `@name`.
46
+ */
47
+ export interface DecoratorDefinition {
48
+ readonly name: string;
49
+ readonly doc?: string;
50
+ /**
51
+ * The `$type`s this may decorate, such as `["FlowDecl", "StepDecl"]`. Omitted
52
+ * means anywhere: a decorator that reads nothing about its target constrains
53
+ * nothing about it.
54
+ *
55
+ * Node names are the plugin author's spelling, and they stay. A TypeScript
56
+ * decorator is handed the raw node, so nothing shorter describes what it is
57
+ * able to read.
58
+ */
59
+ readonly targets?: readonly string[];
60
+ /**
61
+ * The kinds this decorates, as a `deco` declared them, read off the type of
62
+ * its first parameter with `acceptedKinds`. Present only for a decorator the
63
+ * language itself declared, and it wins over `targets`: a signature says in
64
+ * the user's words what a list of node names only approximates.
65
+ */
66
+ readonly accepts?: readonly TargetKind[];
67
+ expand(ctx: ExpandContext): void;
68
+ }
69
+
70
+ /** Where expansion looks a decorator up. The kernel does not know what plugins are. */
71
+ export interface DecoratorSource {
72
+ get(name: string): DecoratorDefinition | undefined;
73
+ }
74
+
75
+ /** What expansion reports. The tree itself is rewritten in place. */
76
+ export interface ExpandResult {
77
+ problems: Problem[];
78
+ }
@@ -0,0 +1,35 @@
1
+ import { nativeFn } from "../../expr/index.js";
2
+ import { AROUND_KEYS, addDecoration } from "../decorations.js";
3
+ import type { Verb, VerbTable } from "./handle.types.js";
4
+
5
+ /** Everything a function's call can be surrounded with. */
6
+ export const CALL_AROUND_VERBS: VerbTable = {
7
+ props: {},
8
+ calls: { wrap: around(AROUND_KEYS.wrap), before: before(), after: after() },
9
+ };
10
+
11
+ /**
12
+ * A flow and a step have no `wrap`: there is no call to intercept and no return
13
+ * to replace, only a body that runs. Offering it would be a verb that does
14
+ * nothing, which is worse than not having it.
15
+ */
16
+ export const BODY_AROUND_VERBS: VerbTable = {
17
+ props: {},
18
+ calls: { before: before(), after: after() },
19
+ };
20
+
21
+ function before(): Verb {
22
+ return around(AROUND_KEYS.before);
23
+ }
24
+
25
+ function after(): Verb {
26
+ return around(AROUND_KEYS.after);
27
+ }
28
+
29
+ function around(key: string): Verb {
30
+ return (node) =>
31
+ nativeFn((args) => {
32
+ addDecoration(node, key, args[0]);
33
+ return null;
34
+ });
35
+ }
@@ -0,0 +1,27 @@
1
+ import { constLit } from "../../compile/index.js";
2
+ import { evaluate, nativeFn } from "../../expr/index.js";
3
+ import type { LetStmt } from "../../generated/ast.js";
4
+ import type { VerbTable } from "./handle.types.js";
5
+
6
+ /** Nothing is bound yet, so what a binding's value refers to is nothing either. */
7
+ const NOTHING = { lookup: () => undefined };
8
+
9
+ /**
10
+ * A `let` or `const`: what it was bound to, and binding it to something else.
11
+ *
12
+ * `.value` reads the expression as written, against an empty scope. Expansion
13
+ * happens before any of the program's own names exist, so a binding whose value
14
+ * depends on one answers with nothing rather than with a guess.
15
+ */
16
+ export const BINDING_VERBS: VerbTable = {
17
+ props: { value: (node) => evaluate((node as LetStmt).value, NOTHING) },
18
+ calls: { setValue: (node) => nativeFn((args) => setValue(node as LetStmt, args[0])) },
19
+ };
20
+
21
+ function setValue(stmt: LetStmt, value: unknown): null {
22
+ const written = constLit(value) as { $container?: object; $containerProperty?: string };
23
+ written.$container = stmt;
24
+ written.$containerProperty = "value";
25
+ stmt.value = written as LetStmt["value"];
26
+ return null;
27
+ }