@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,267 @@
1
+ import type { TypeContext } from "./context.js";
2
+ import {
3
+ BOOL,
4
+ baseOf,
5
+ callback,
6
+ DYNAMIC,
7
+ type FnType,
8
+ fn,
9
+ list,
10
+ NUMBER,
11
+ prim,
12
+ type RecordType,
13
+ STRING,
14
+ type Type,
15
+ type TypeVar,
16
+ type UnionType,
17
+ union,
18
+ } from "./type.types.js";
19
+ import { prune } from "./unify.js";
20
+
21
+ /**
22
+ * The type of a built-in member: a property such as `length`, or a generic
23
+ * method such as `map`.
24
+ *
25
+ * These are where generics earn their keep. `list<T>.map` is
26
+ * `fn(fn(T, number) -> U) -> list<U>`, with a fresh `U` per use.
27
+ *
28
+ * @returns undefined when the receiver's type has no such member, so the caller
29
+ * can fall back to a record field or to `dynamic`.
30
+ */
31
+ export function memberType(receiver: Type, name: string, ctx: TypeContext): Type | undefined {
32
+ const t = prune(receiver);
33
+ if (t.kind === "list") return listMember(t.element, name, ctx);
34
+ if (t.kind === "literal") return memberType(prim(baseOf(t.value)), name, ctx);
35
+ if (t.kind === "union") return unionMember(t, name, ctx);
36
+ if (t.kind === "prim" && t.name === "string") return stringMember(name);
37
+ if (t.kind === "prim" && t.name === "number") return numberMember(name);
38
+ if (t.kind === "record") return recordMember(t, name);
39
+ if (t.kind === "prim") return unitMember(t.name, name);
40
+ // A handle answers to what it published, and to nothing else: its inside is
41
+ // none of the reader's business, which is what makes it opaque.
42
+ if (t.kind === "opaque") return t.members?.get(name);
43
+ return undefined;
44
+ }
45
+
46
+ /**
47
+ * A member of a union is a member of every branch, or of none.
48
+ *
49
+ * Offering what only one branch has would be a lie the moment the value is the
50
+ * other one.
51
+ */
52
+ function unionMember(type: UnionType, name: string, ctx: TypeContext): Type | undefined {
53
+ const found = type.members.map((member) => memberType(member, name, ctx));
54
+ if (found.some((member) => member === undefined)) return undefined;
55
+ return union(found as Type[]);
56
+ }
57
+
58
+ const DURATION: Type = { kind: "prim", name: "duration" };
59
+ const SIZE: Type = { kind: "prim", name: "size" };
60
+ const PERCENT: Type = { kind: "prim", name: "percent" };
61
+
62
+ /**
63
+ * Reading a unit back as a plain number, in whichever unit you want it. The unit
64
+ * exists to keep `300ms + 2mb` from type-checking; once you are printing or
65
+ * comparing against raw data, these are the way across.
66
+ */
67
+ const UNIT_MEMBERS: Record<string, Record<string, Type>> = {
68
+ duration: { ms: NUMBER, seconds: NUMBER, minutes: NUMBER, hours: NUMBER },
69
+ size: { bytes: NUMBER, kb: NUMBER, mb: NUMBER, gb: NUMBER },
70
+ percent: { ratio: NUMBER, percent: NUMBER, of: fn([NUMBER], NUMBER) },
71
+ };
72
+
73
+ function unitMember(unit: string, name: string): Type | undefined {
74
+ return UNIT_MEMBERS[unit]?.[name];
75
+ }
76
+
77
+ function listMember(element: Type, name: string, ctx: TypeContext): Type | undefined {
78
+ const u = (): TypeVar => ctx.fresh();
79
+ const self = list(element);
80
+ /** Every list callback is handed the index too; `p => p.age` may ignore it. */
81
+ const over = (result: Type): FnType => callback([element, NUMBER], result, 1);
82
+ const predicate = over(BOOL);
83
+ const table: Record<string, () => Type> = {
84
+ len: () => NUMBER,
85
+ first: () => element,
86
+ last: () => element,
87
+ reverse: () => self,
88
+ flatten: () => list(DYNAMIC),
89
+ isEmpty: () => BOOL,
90
+ sum: () => NUMBER,
91
+ average: () => NUMBER,
92
+ min: () => NUMBER,
93
+ max: () => NUMBER,
94
+ toMap: () => DYNAMIC,
95
+ // Selecting keeps the element type; only the shape of the result changes.
96
+ take: () => fn([NUMBER], self),
97
+ drop: () => fn([NUMBER], self),
98
+ takeLast: () => fn([NUMBER], self),
99
+ dropLast: () => fn([NUMBER], self),
100
+ takeWhile: () => fn([predicate], self),
101
+ dropWhile: () => fn([predicate], self),
102
+ distinct: () => self,
103
+ distinctBy: () => fn([over(u())], self),
104
+ sortBy: () => fn([over(u())], self),
105
+ minBy: () => fn([over(NUMBER)], element),
106
+ maxBy: () => fn([over(NUMBER)], element),
107
+ sumBy: () => fn([over(NUMBER)], NUMBER),
108
+ flatMap: () => flatMapType(over, u()),
109
+ // Grouping changes the shape: a list becomes a map, or a list of lists.
110
+ groupBy: () => fn([over(DYNAMIC)], DYNAMIC),
111
+ countBy: () => fn([over(DYNAMIC)], DYNAMIC),
112
+ keyBy: () => fn([over(DYNAMIC)], DYNAMIC),
113
+ partition: () => fn([predicate], list(self)),
114
+ chunk: () => fn([NUMBER], list(self)),
115
+ windows: () => fn([NUMBER], list(self)),
116
+ pairwise: () => list(self),
117
+ zip: () => fn([list(DYNAMIC)], list(list(DYNAMIC))),
118
+ unzip: () => list(list(DYNAMIC)),
119
+ map: () => mapType(over, u()),
120
+ filter: () => fn([predicate], list(element)),
121
+ find: () => fn([predicate], element),
122
+ some: () => fn([predicate], BOOL),
123
+ every: () => fn([predicate], BOOL),
124
+ forEach: () => fn([over(DYNAMIC)], NULL_VOID),
125
+ reduce: () => reduceType(element, u(), NUMBER),
126
+ contains: () => fn([element], BOOL),
127
+ indexOf: () => fn([element], NUMBER),
128
+ join: () => fn([STRING], STRING),
129
+ sort: () => fn([fn([element, element], NUMBER)], list(element)),
130
+ slice: () => fn([NUMBER, NUMBER], list(element)),
131
+ concat: () => fn([list(element)], list(element)),
132
+ push: () => fn([element], list(element)),
133
+ };
134
+ return table[name]?.();
135
+ }
136
+
137
+ const NULL_VOID = { kind: "prim", name: "null" } as const;
138
+
139
+ /** The callback shape a list method hands its element to. */
140
+ type Over = (result: Type) => FnType;
141
+
142
+ function mapType(over: Over, into: Type): Type {
143
+ return fn([over(into)], list(into));
144
+ }
145
+
146
+ function flatMapType(over: Over, into: Type): Type {
147
+ return fn([over(list(into))], list(into));
148
+ }
149
+
150
+ /** `reduce` hands over the running total, the item, and the index. */
151
+ function reduceType(element: Type, acc: Type, index: Type): Type {
152
+ return fn([callback([acc, element, index], acc, 2), acc], acc);
153
+ }
154
+
155
+ function stringMember(name: string): Type | undefined {
156
+ const table: Record<string, Type> = {
157
+ len: NUMBER,
158
+ upper: STRING,
159
+ lower: STRING,
160
+ trim: STRING,
161
+ reverse: STRING,
162
+ toNumber: NUMBER,
163
+ split: fn([STRING], list(STRING)),
164
+ replace: fn([STRING, STRING], STRING),
165
+ contains: fn([STRING], BOOL),
166
+ startsWith: fn([STRING], BOOL),
167
+ endsWith: fn([STRING], BOOL),
168
+ slice: fn([NUMBER, NUMBER], STRING),
169
+ repeat: fn([NUMBER], STRING),
170
+ padStart: fn([NUMBER, STRING], STRING),
171
+ padEnd: fn([NUMBER, STRING], STRING),
172
+ indexOf: fn([STRING], NUMBER),
173
+ words: list(STRING),
174
+ lines: list(STRING),
175
+ chars: list(STRING),
176
+ capitalize: STRING,
177
+ title: STRING,
178
+ slugify: STRING,
179
+ isEmpty: BOOL,
180
+ isBlank: BOOL,
181
+ trimStart: STRING,
182
+ trimEnd: STRING,
183
+ count: fn([STRING], NUMBER),
184
+ matches: fn([STRING], list(STRING)),
185
+ test: fn([STRING], BOOL),
186
+ before: fn([STRING], STRING),
187
+ after: fn([STRING], STRING),
188
+ ensureStart: fn([STRING], STRING),
189
+ ensureEnd: fn([STRING], STRING),
190
+ };
191
+ return table[name];
192
+ }
193
+
194
+ function numberMember(name: string): Type | undefined {
195
+ const table: Record<string, Type> = {
196
+ abs: NUMBER,
197
+ floor: NUMBER,
198
+ ceil: NUMBER,
199
+ sign: NUMBER,
200
+ sqrt: NUMBER,
201
+ isEven: BOOL,
202
+ isOdd: BOOL,
203
+ round: fn([NUMBER], NUMBER),
204
+ toFixed: fn([NUMBER], STRING),
205
+ clamp: fn([NUMBER, NUMBER], NUMBER),
206
+ pow: fn([NUMBER], NUMBER),
207
+ times: list(NUMBER),
208
+ toString: STRING,
209
+ };
210
+ return table[name] ?? TO_UNIT[name];
211
+ }
212
+
213
+ /**
214
+ * Reading a plain number as a unit, the way back from {@link UNIT_MEMBERS}. For
215
+ * every `X` a unit answers to, a number answers to `toX`.
216
+ */
217
+ const TO_UNIT: Record<string, Type> = {
218
+ toMs: DURATION,
219
+ toSeconds: DURATION,
220
+ toMinutes: DURATION,
221
+ toHours: DURATION,
222
+ toBytes: SIZE,
223
+ toKb: SIZE,
224
+ toMb: SIZE,
225
+ toGb: SIZE,
226
+ toRatio: PERCENT,
227
+ toPercent: PERCENT,
228
+ };
229
+
230
+ function recordMember(receiver: RecordType, name: string): Type | undefined {
231
+ const table: Record<string, Type> = {
232
+ keys: list(STRING),
233
+ values: list(DYNAMIC),
234
+ entries: list(DYNAMIC),
235
+ len: NUMBER,
236
+ has: fn([STRING], BOOL),
237
+ get: fn([STRING], DYNAMIC),
238
+ merge: fn([DYNAMIC], DYNAMIC),
239
+ mergeDeep: fn([DYNAMIC], DYNAMIC),
240
+ // A map's callbacks are handed the key alongside the value, or the other
241
+ // way round for `mapKeys`. Taking the second one is optional.
242
+ mapValues: fn([callback([DYNAMIC, STRING], DYNAMIC, 1)], DYNAMIC),
243
+ mapKeys: fn([callback([STRING, DYNAMIC], STRING, 1)], DYNAMIC),
244
+ filterValues: fn([callback([DYNAMIC, STRING], BOOL, 1)], DYNAMIC),
245
+ pick: fn([STRING], DYNAMIC),
246
+ omit: fn([STRING], DYNAMIC),
247
+ invert: DYNAMIC,
248
+ isEmpty: BOOL,
249
+ getPath: fn([STRING], DYNAMIC),
250
+ hasPath: fn([STRING], BOOL),
251
+ };
252
+ return receiver.fields.has(name) ? undefined : table[name];
253
+ }
254
+
255
+ /**
256
+ * A member as the language reads it: the built-in when there is one, otherwise
257
+ * the field the map carries.
258
+ *
259
+ * {@link memberType} alone stops at built-ins so inference can report an unknown
260
+ * field. Tooling wants the answer, not the distinction.
261
+ */
262
+ export function resolveMember(receiver: Type, name: string, ctx: TypeContext): Type | undefined {
263
+ const built = memberType(receiver, name, ctx);
264
+ if (built) return built;
265
+ const t = prune(receiver);
266
+ return t.kind === "record" ? t.fields.get(name) : undefined;
267
+ }
@@ -0,0 +1,16 @@
1
+ import type { FnType, Type } from "./type.types.js";
2
+
3
+ /**
4
+ * What the checker knows that the file itself does not: the types the loaded
5
+ * plugins contribute.
6
+ *
7
+ * The core has no idea what a plugin is, and must not: it asks this interface
8
+ * two questions and takes the answers. Whoever owns the registry, the runtime or
9
+ * the language server, is the one that can answer them.
10
+ */
11
+ export interface TypeCatalog {
12
+ /** A named type: `http.Request`. Undefined when nothing declares it. */
13
+ typeOf(name: string): Type | undefined;
14
+ /** A verb's signature: `http.serve`, `http.on`. */
15
+ signatureOf(target: string): FnType | undefined;
16
+ }
@@ -0,0 +1,174 @@
1
+ import { walkAst } from "../ast/index.js";
2
+ import { buildProblem, CODES } from "../codes/index.js";
3
+ import { acceptedKinds, readSignature, spanOf, wrongKind } from "../expand/index.js";
4
+ import type { Annotation, DecoDecl, Document, Expr, Param } from "../generated/ast.js";
5
+ import { isAnnotation, isDecoDecl } from "../generated/ast.js";
6
+ import type { Problem } from "../problem/index.js";
7
+ import { checkBlock } from "./check-stmts.js";
8
+ import { expect, type Infer, inferExpr } from "./infer.js";
9
+ import { mono } from "./scheme.js";
10
+ import { DYNAMIC, type Type } from "./type.types.js";
11
+ import { emptyEnv, type TypeEnv } from "./type-env.js";
12
+ import { typeRefToType } from "./type-ref.js";
13
+
14
+ /**
15
+ * A `deco`'s body, checked the way a `fn`'s is.
16
+ *
17
+ * The kind on the first parameter is a real type, so the body holds a typed
18
+ * handle: hovering `target` says `Fn`, completing `target.` offers what an `Fn`
19
+ * offers, and the checker knows what `target.params` is without anyone teaching
20
+ * it a second vocabulary.
21
+ */
22
+ export function checkDecoBody(decl: DecoDecl, env: TypeEnv, infer: Infer): void {
23
+ let scope = env;
24
+ for (const param of decl.params?.params ?? []) {
25
+ const type = decoParamType(param, infer);
26
+ infer.types?.set(param, type);
27
+ scope = scope.with(param.name, mono(type));
28
+ }
29
+ checkBlock(decl.body, scope, infer);
30
+ }
31
+
32
+ /**
33
+ * What a `deco`'s parameter is.
34
+ *
35
+ * An unwritten one stays `dynamic` rather than an open question: a decorator's
36
+ * arguments arrive from every `@name(…)` in the file, and letting the first use
37
+ * decide for all of them would turn the second into an error nobody made.
38
+ */
39
+ function decoParamType(param: Param, infer: Infer): Type {
40
+ if (!param.paramType) return DYNAMIC;
41
+ const { ctx, named, catalog } = infer;
42
+ return typeRefToType({ ref: param.paramType, ctx, named, catalog });
43
+ }
44
+
45
+ /**
46
+ * What a document's `deco`s answer for statically: a signature that reads, and
47
+ * arguments that match it.
48
+ *
49
+ * The signature is read with the very reader expansion uses, so the sentence
50
+ * `venn check` prints is the sentence a run prints. Expansion does not happen
51
+ * here (the checker looks at the program as written), which is why the signature
52
+ * is read twice, and read the same.
53
+ */
54
+ export function checkDecos(args: { document: Document; infer: Infer; uri: string }): Problem[] {
55
+ const problems: Problem[] = [];
56
+ for (const decl of args.document.decls.filter(isDecoDecl)) {
57
+ const read = readSignature(decl);
58
+ if (!read.ok) problems.push(signatureProblem(decl, read.title, args.uri));
59
+ }
60
+ return [...problems, ...checkUses({ ...args, declared: args.infer.decos ?? new Map() })];
61
+ }
62
+
63
+ /**
64
+ * Every `deco` a use site in this file could name, local or imported, keyed by
65
+ * the name an `@` writes. Only the ones whose signature reads, since a use of a
66
+ * broken one has nothing to be checked against.
67
+ *
68
+ * An imported one contributes no VN2015 of its own: that is the other file's
69
+ * fault, reported when the other file is checked, and a span into a document
70
+ * this run never opened would point nowhere.
71
+ */
72
+ export function decosInReach(args: {
73
+ document: Document;
74
+ imported?: ReadonlyMap<string, { readonly decl: DecoDecl }>;
75
+ }): Map<string, DecoDecl> {
76
+ const found = new Map<string, DecoDecl>();
77
+ for (const [name, from] of args.imported ?? []) {
78
+ if (readSignature(from.decl).ok) found.set(name, from.decl);
79
+ }
80
+ for (const decl of args.document.decls.filter(isDecoDecl)) {
81
+ if (readSignature(decl).ok) found.set(decl.name, decl);
82
+ }
83
+ return found;
84
+ }
85
+
86
+ function signatureProblem(decl: DecoDecl, title: string, uri: string): Problem {
87
+ return buildProblem({ spec: CODES.VN2015_DECO_SIGNATURE, span: spanOf(decl, uri), title });
88
+ }
89
+
90
+ /** Everything a use site needs to be checked against the `deco` it names. */
91
+ interface Uses {
92
+ document: Document;
93
+ infer: Infer;
94
+ uri: string;
95
+ declared: ReadonlyMap<string, DecoDecl>;
96
+ }
97
+
98
+ function checkUses(args: Uses): Problem[] {
99
+ if (args.declared.size === 0) return [];
100
+ const problems: Problem[] = [];
101
+ for (const node of walkAst(args.document)) {
102
+ if (isAnnotation(node)) problems.push(...checkUse(node, args));
103
+ }
104
+ return problems;
105
+ }
106
+
107
+ /**
108
+ * `@retry(3)` against `deco retry(target: Flow, times: number)`: is it on
109
+ * something it decorates, and were it given what it asks for.
110
+ */
111
+ function checkUse(annotation: Annotation, args: Uses): Problem[] {
112
+ const decl = args.declared.get(annotation.name);
113
+ if (!decl) return [];
114
+ const wrong = misplaced(annotation, decl, args.uri);
115
+ return wrong ? [wrong] : checkArgs(annotation, decl, args);
116
+ }
117
+
118
+ /**
119
+ * The very refusal expansion would make, made while the file is still open.
120
+ *
121
+ * A run reaches it too, and with the same sentence, because both ask
122
+ * {@link wrongKind}. `venn check` never expands, so this is the only place the
123
+ * type error a signature exists to produce reaches an editor.
124
+ */
125
+ function misplaced(annotation: Annotation, decl: DecoDecl, uri: string): Problem | undefined {
126
+ const node = annotation.$container;
127
+ const title = node && wrongKind({ name: annotation.name, kinds: acceptedKinds(decl), node });
128
+ if (!title) return undefined;
129
+ const spec = CODES.VN2014_DECORATOR_TARGET;
130
+ return buildProblem({ spec, span: spanOf(annotation, uri), title });
131
+ }
132
+
133
+ /**
134
+ * The arguments are read against an empty scope on purpose: they are part of the
135
+ * shape of the program and not of its execution, so nothing exists yet for a
136
+ * name inside one to refer to. That is the same reason a bare word there is a
137
+ * word and not a variable.
138
+ */
139
+ function checkArgs(annotation: Annotation, decl: DecoDecl, args: Uses): Problem[] {
140
+ const params = (decl.params?.params ?? []).slice(1);
141
+ const given = annotation.args?.args ?? [];
142
+ if (given.length !== params.length) {
143
+ return [arityProblem({ annotation, params, given, uri: args.uri })];
144
+ }
145
+ for (const [at, arg] of given.entries()) checkArg(arg.value, params[at], args.infer);
146
+ return [];
147
+ }
148
+
149
+ function checkArg(value: Expr, param: Param | undefined, infer: Infer): void {
150
+ if (!param) return;
151
+ expect(infer, value, inferExpr(value, emptyEnv(), infer), decoParamType(param, infer));
152
+ }
153
+
154
+ function arityProblem(args: {
155
+ annotation: Annotation;
156
+ params: readonly Param[];
157
+ given: readonly unknown[];
158
+ uri: string;
159
+ }): Problem {
160
+ const title = arityTitle(args.annotation.name, args.params.length, args.given.length);
161
+ return buildProblem({
162
+ spec: CODES.VN3017_DECO_ARGUMENTS,
163
+ span: spanOf(args.annotation, args.uri),
164
+ title,
165
+ });
166
+ }
167
+
168
+ function arityTitle(name: string, want: number, got: number): string {
169
+ return `@${name} takes ${wanted(want)}, and was given ${got === 0 ? "none" : got}.`;
170
+ }
171
+
172
+ function wanted(count: number): string {
173
+ return count === 0 ? "no arguments" : `${count} argument${count === 1 ? "" : "s"}`;
174
+ }
@@ -0,0 +1,151 @@
1
+ import { callArgs } from "../ast/index.js";
2
+ import type { Block, Expr, FragmentDecl, Statement } from "../generated/ast.js";
3
+ import * as ast from "../generated/ast.js";
4
+ import { callType } from "./action-signature.js";
5
+ import { expect, type Infer, inferExpr } from "./infer.js";
6
+ import { mono } from "./scheme.js";
7
+ import { DYNAMIC, type Type } from "./type.types.js";
8
+ import type { TypeEnv } from "./type-env.js";
9
+ import { typeRefToType } from "./type-ref.js";
10
+ import { prune } from "./unify.js";
11
+
12
+ /** Walk a statement, inferring the expressions it contains under `env`. */
13
+ export function checkStatement(node: Statement, env: TypeEnv, infer: Infer): TypeEnv {
14
+ if (ast.isLetStmt(node)) return bindLet(node, env, infer);
15
+ if (ast.isExpectStmt(node)) return expectStmt(node, env, infer);
16
+ if (ast.isActionCall(node)) return actionArgs(node, env, infer);
17
+ if (ast.isRunStmt(node)) return runArgs(node, env, infer);
18
+ if (ast.isIfStmt(node)) return ifStmt(node, env, infer);
19
+ if (ast.isForEachStmt(node)) return forEach(node, env, infer);
20
+ if (ast.isWhileStmt(node)) return blockAfterCond(node.cond, node.body, env, infer);
21
+ if (ast.isRepeatStmt(node)) return repeat(node, env, infer);
22
+ if (ast.isTryStmt(node)) return tryStmt(node, env, infer);
23
+ if (ast.isReturnStmt(node)) return maybeInfer(node.value, env, infer);
24
+ if (ast.isStepDecl(node) || ast.isGroupDecl(node)) return nested(node.body, env, infer);
25
+ if (ast.isParallelStmt(node) || ast.isRaceStmt(node)) return nested(node.body, env, infer);
26
+ if (ast.isLifecycleDecl(node)) return nested(node.body, env, infer);
27
+ return env;
28
+ }
29
+
30
+ /** Check a nested block, then keep the outer scope: its bindings do not escape. */
31
+ function nested(block: Block, env: TypeEnv, infer: Infer): TypeEnv {
32
+ checkBlock(block, env, infer);
33
+ return env;
34
+ }
35
+
36
+ /** A `let`/`const` binds its inferred type; the binding is visible from here on. */
37
+ function bindLet(node: ast.LetStmt, env: TypeEnv, infer: Infer): TypeEnv {
38
+ const type = isCall(node) ? boundCall(node, env, infer) : inferExpr(node.value, env, infer);
39
+ const declared = declaredTypeOf(node, infer);
40
+ // An annotation the checker parses and never reads is worse than no
41
+ // annotation: the author believes something is being enforced.
42
+ if (declared) expect(infer, node.value, type, declared);
43
+ // Record it on the declaration too, so a hover on the name knows the type.
44
+ infer.types?.set(node, declared ?? type);
45
+ return env.with(node.name, mono(declared ?? type));
46
+ }
47
+
48
+ function declaredTypeOf(node: ast.LetStmt, infer: Infer): Type | undefined {
49
+ if (!node.declaredType) return undefined;
50
+ const { ctx, named, catalog } = infer;
51
+ return typeRefToType({ ref: node.declaredType, ctx, named, catalog });
52
+ }
53
+
54
+ /** Trailing arguments or an options map are what make a binding a verb call. */
55
+ function isCall(node: ast.LetStmt): boolean {
56
+ return node.args.length > 0 || node.opts !== undefined;
57
+ }
58
+
59
+ function boundCall(node: ast.LetStmt, env: TypeEnv, infer: Infer): Type {
60
+ return callType({ target: targetOf(node.value), args: node.args, opts: node.opts }, env, infer);
61
+ }
62
+
63
+ /** The dotted name a call was written with, such as `http.serve`. */
64
+ function targetOf(value: Expr): string {
65
+ return (value as { $cstNode?: { text?: string } }).$cstNode?.text?.trim() ?? "";
66
+ }
67
+
68
+ function expectStmt(node: ast.ExpectStmt, env: TypeEnv, infer: Infer): TypeEnv {
69
+ if (node.subject) inferExpr(node.subject, env, infer);
70
+ for (const check of node.checks) inferExpr(check, env, infer);
71
+ return env;
72
+ }
73
+
74
+ /**
75
+ * A verb written as a statement, in either spelling.
76
+ *
77
+ * `callArgs` because the arguments live in different places on the node: bare
78
+ * ones hang off the statement, bracketed ones sit in the call. Reading only the
79
+ * first makes `http.on(api, route)` look like a call with no arguments, and then
80
+ * nothing tells `route` what it is handed.
81
+ */
82
+ function actionArgs(node: ast.ActionCall, env: TypeEnv, infer: Infer): TypeEnv {
83
+ callType({ target: node.target, args: callArgs(node), opts: node.opts }, env, infer);
84
+ return env;
85
+ }
86
+
87
+ function runArgs(node: ast.RunStmt, env: TypeEnv, infer: Infer): TypeEnv {
88
+ for (const arg of node.args?.args ?? []) inferExpr(arg.value, env, infer);
89
+ return node.bind ? env.with(node.bind, mono(DYNAMIC)) : env;
90
+ }
91
+
92
+ function ifStmt(node: ast.IfStmt, env: TypeEnv, infer: Infer): TypeEnv {
93
+ inferExpr(node.cond, env, infer);
94
+ checkBlockOrIf(node.then, env, infer);
95
+ if (node.otherwise) checkBlockOrIf(node.otherwise, env, infer);
96
+ return env;
97
+ }
98
+
99
+ function checkBlockOrIf(node: Block | ast.IfStmt, env: TypeEnv, infer: Infer): void {
100
+ if (ast.isIfStmt(node)) ifStmt(node, env, infer);
101
+ else checkBlock(node, env, infer);
102
+ }
103
+
104
+ /** `forEach item in source`: the item's type is the source's element type. */
105
+ function forEach(node: ast.ForEachStmt, env: TypeEnv, infer: Infer): TypeEnv {
106
+ const source = prune(inferExpr(node.source, env, infer));
107
+ const item: Type = source.kind === "list" ? source.element : DYNAMIC;
108
+ checkBlock(node.body, env.with(node.item, mono(item)), infer);
109
+ return env;
110
+ }
111
+
112
+ function repeat(node: ast.RepeatStmt, env: TypeEnv, infer: Infer): TypeEnv {
113
+ inferExpr(node.count, env, infer);
114
+ const inner = node.index ? env.with(node.index, mono({ kind: "prim", name: "number" })) : env;
115
+ checkBlock(node.body, inner, infer);
116
+ return env;
117
+ }
118
+
119
+ function tryStmt(node: ast.TryStmt, env: TypeEnv, infer: Infer): TypeEnv {
120
+ checkBlock(node.body, env, infer);
121
+ if (node.handler) {
122
+ const scope = node.error ? env.with(node.error, mono(DYNAMIC)) : env;
123
+ checkBlock(node.handler, scope, infer);
124
+ }
125
+ if (node.finalizer) checkBlock(node.finalizer, env, infer);
126
+ return env;
127
+ }
128
+
129
+ function blockAfterCond(cond: Expr, body: Block, env: TypeEnv, infer: Infer): TypeEnv {
130
+ inferExpr(cond, env, infer);
131
+ checkBlock(body, env, infer);
132
+ return env;
133
+ }
134
+
135
+ function maybeInfer(expr: Expr | undefined, env: TypeEnv, infer: Infer): TypeEnv {
136
+ if (expr) inferExpr(expr, env, infer);
137
+ return env;
138
+ }
139
+
140
+ /** A block opens its own scope; bindings inside it do not escape. */
141
+ export function checkBlock(block: Block, env: TypeEnv, infer: Infer): void {
142
+ let scope = env;
143
+ for (const stmt of block.stmts) scope = checkStatement(stmt, scope, infer);
144
+ }
145
+
146
+ /** A fragment's params are in scope throughout its body. */
147
+ export function checkFragment(decl: FragmentDecl, env: TypeEnv, infer: Infer): void {
148
+ let scope = env;
149
+ for (const param of decl.params?.params ?? []) scope = scope.with(param.name, mono(DYNAMIC));
150
+ checkBlock(decl.body, scope, infer);
151
+ }