@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,211 @@
1
+ import type { Type } from "./type.types.js";
2
+ import { prune } from "./unify.js";
3
+
4
+ /** What a built-in member does, and how it reads. */
5
+ export interface MemberDoc {
6
+ doc: string;
7
+ example?: string;
8
+ }
9
+
10
+ const LIST: Record<string, MemberDoc> = {
11
+ len: { doc: "How many items it holds.", example: "[1, 2, 3].len # 3" },
12
+ first: { doc: "The first item, or null when empty." },
13
+ last: { doc: "The last item, or null when empty." },
14
+ isEmpty: { doc: "True when there is nothing in it." },
15
+ map: {
16
+ doc: "A new list, each item passed through the function.",
17
+ example: "[1, 2].map(fn (x) => x * 2) # [2, 4]",
18
+ },
19
+ filter: { doc: "Only the items the function keeps.", example: "xs.filter(fn (x) => x > 2)" },
20
+ reduce: {
21
+ doc: "Fold the list into one value, from a starting one.",
22
+ example: "xs.reduce(fn (total, x) => total + x, 0)",
23
+ },
24
+ forEach: { doc: "Run the function once per item, for its effect." },
25
+ find: { doc: "The first item the function accepts, or null." },
26
+ some: { doc: "True when the function accepts any item." },
27
+ every: { doc: "True when the function accepts all of them." },
28
+ contains: { doc: "True when the item is in the list." },
29
+ indexOf: { doc: "Where the item sits, or -1." },
30
+ join: { doc: "The items as one string, separated.", example: "['a', 'b'].join('-') # \"a-b\"" },
31
+ reverse: { doc: "The same items, back to front." },
32
+ flatten: { doc: "One level of nesting removed." },
33
+ sort: { doc: "Sorted by a comparator. For a key, `sortBy` reads better." },
34
+ slice: { doc: "The items between two positions." },
35
+ concat: { doc: "This list followed by another." },
36
+ push: { doc: "A new list with the item appended — the original is untouched." },
37
+ sum: { doc: "The numbers added up." },
38
+ average: { doc: "The mean of the numbers, or 0 when empty." },
39
+ min: { doc: "The smallest number." },
40
+ max: { doc: "The largest number." },
41
+ toMap: { doc: "Entry pairs turned into a map.", example: "[['a', 1]].toMap # { a: 1 }" },
42
+ take: { doc: "The first n items." },
43
+ drop: { doc: "Everything after the first n." },
44
+ takeLast: { doc: "The last n items." },
45
+ dropLast: { doc: "Everything but the last n." },
46
+ takeWhile: { doc: "Items from the start, while the function accepts them." },
47
+ dropWhile: { doc: "Skip the accepted run, keep the rest." },
48
+ distinct: { doc: "Duplicates removed, first occurrence kept." },
49
+ distinctBy: { doc: "Duplicates removed by a derived key." },
50
+ sortBy: { doc: "Sorted by a derived key.", example: "people.sortBy(fn (p) => p.age)" },
51
+ minBy: { doc: "The item with the smallest score." },
52
+ maxBy: { doc: "The item with the largest score.", example: "people.maxBy(fn (p) => p.age)" },
53
+ sumBy: { doc: "The scores added up." },
54
+ flatMap: { doc: "Map, then flatten one level." },
55
+ groupBy: {
56
+ doc: "A map from key to the items sharing it.",
57
+ example: "people.groupBy(fn (p) => p.team)",
58
+ },
59
+ countBy: { doc: "A map from key to how many items share it." },
60
+ keyBy: { doc: "A map from key to item — an index, last one wins." },
61
+ partition: { doc: "Two lists: what the function kept, and what it rejected." },
62
+ chunk: { doc: "Split into runs of n.", example: "[1,2,3,4,5].chunk(2) # [[1,2],[3,4],[5]]" },
63
+ windows: { doc: "Every consecutive run of n.", example: "[1,2,3].windows(2) # [[1,2],[2,3]]" },
64
+ pairwise: { doc: "Every consecutive pair." },
65
+ zip: { doc: "Paired with another list, item by item." },
66
+ unzip: { doc: "Pairs pulled apart into separate lists." },
67
+ };
68
+
69
+ const STRING: Record<string, MemberDoc> = {
70
+ len: { doc: "How many characters it holds." },
71
+ upper: { doc: "In upper case." },
72
+ lower: { doc: "In lower case." },
73
+ trim: { doc: "Without leading or trailing blanks." },
74
+ trimStart: { doc: "Without leading blanks." },
75
+ trimEnd: { doc: "Without trailing blanks." },
76
+ reverse: { doc: "The characters back to front." },
77
+ toNumber: { doc: "Read as a number." },
78
+ isEmpty: { doc: "True when it has no characters." },
79
+ isBlank: { doc: "True when it is empty or only blanks." },
80
+ split: { doc: "Cut into a list on a separator.", example: "'a,b'.split(',') # ['a', 'b']" },
81
+ replace: { doc: "Every occurrence swapped for another." },
82
+ contains: { doc: "True when the text appears inside." },
83
+ startsWith: { doc: "True when it begins with the text." },
84
+ endsWith: { doc: "True when it ends with the text." },
85
+ slice: { doc: "The characters between two positions." },
86
+ repeat: { doc: "Itself, n times over." },
87
+ padStart: { doc: "Padded on the left to a width." },
88
+ padEnd: { doc: "Padded on the right to a width." },
89
+ indexOf: { doc: "Where the text starts, or -1." },
90
+ words: { doc: "Split on whitespace.", example: "'a b c'.words # ['a', 'b', 'c']" },
91
+ lines: { doc: "Split on newlines." },
92
+ chars: { doc: "Each character as its own string." },
93
+ capitalize: { doc: "First letter upper-cased." },
94
+ title: { doc: "Every word capitalised." },
95
+ slugify: {
96
+ doc: "URL-safe: accents stripped, spaces to dashes.",
97
+ example: "'João Silva'.slugify # \"joao-silva\"",
98
+ },
99
+ count: { doc: "How many times the text appears." },
100
+ matches: {
101
+ doc: "Every match of a pattern, as a list.",
102
+ example: "'a1b22'.matches('[0-9]+') # ['1', '22']",
103
+ },
104
+ test: { doc: "True when the pattern matches anywhere." },
105
+ before: { doc: "What comes before the marker." },
106
+ after: { doc: "What comes after the marker." },
107
+ ensureStart: { doc: "The prefix added, unless it is already there." },
108
+ ensureEnd: { doc: "The suffix added, unless it is already there." },
109
+ };
110
+
111
+ const MAP: Record<string, MemberDoc> = {
112
+ len: { doc: "How many entries it holds." },
113
+ keys: { doc: "Its keys, as a list." },
114
+ values: { doc: "Its values, as a list." },
115
+ entries: { doc: "Its `[key, value]` pairs." },
116
+ has: { doc: "True when the key is present." },
117
+ get: { doc: "The value under a key, or null." },
118
+ merge: { doc: "This map with another laid over it." },
119
+ mergeDeep: {
120
+ doc: "Merged recursively, keeping untouched branches.",
121
+ example: "cfg.mergeDeep({ server: { port: 90 } })",
122
+ },
123
+ mapValues: { doc: "Same keys, each value passed through the function." },
124
+ mapKeys: { doc: "Same values, each key passed through the function." },
125
+ filterValues: { doc: "Only the entries whose value the function keeps." },
126
+ pick: { doc: "Only the named keys.", example: "cfg.pick('host', 'port')" },
127
+ omit: { doc: "Everything but the named keys." },
128
+ invert: { doc: "Keys and values swapped." },
129
+ isEmpty: { doc: "True when it has no entries." },
130
+ getPath: {
131
+ doc: "Reach into nested data by a dotted path.",
132
+ example: "cfg.getPath('server.port')",
133
+ },
134
+ hasPath: { doc: "True when the dotted path leads somewhere." },
135
+ };
136
+
137
+ const NUMBER: Record<string, MemberDoc> = {
138
+ abs: { doc: "Its distance from zero." },
139
+ floor: { doc: "Rounded down to a whole number." },
140
+ ceil: { doc: "Rounded up to a whole number." },
141
+ sign: { doc: "-1, 0 or 1." },
142
+ sqrt: { doc: "Its square root." },
143
+ isEven: { doc: "True when it divides by two." },
144
+ isOdd: { doc: "True when it does not." },
145
+ round: { doc: "Rounded to n decimal places.", example: "(3.14159).round(2) # 3.14" },
146
+ toFixed: { doc: "As text with exactly n decimals." },
147
+ clamp: { doc: "Held between a low and a high bound.", example: "(99).clamp(0, 10) # 10" },
148
+ pow: { doc: "Raised to a power." },
149
+ times: { doc: "A list counting from 0 up to it.", example: "(3).times # [0, 1, 2]" },
150
+ toString: { doc: "As text." },
151
+ toMs: { doc: "Read as a duration in milliseconds.", example: "1500.toMs # 1.5s" },
152
+ toSeconds: { doc: "Read as a duration in seconds." },
153
+ toMinutes: { doc: "Read as a duration in minutes." },
154
+ toHours: { doc: "Read as a duration in hours." },
155
+ toBytes: { doc: "Read as a size in bytes." },
156
+ toKb: { doc: "Read as a size in kilobytes.", example: "2048.toKb # 2mb" },
157
+ toMb: { doc: "Read as a size in megabytes." },
158
+ toGb: { doc: "Read as a size in gigabytes." },
159
+ toRatio: { doc: "Read as a percent, from a fraction of one.", example: "0.5.toRatio # 50%" },
160
+ toPercent: { doc: "Read as a percent, from a number out of a hundred." },
161
+ };
162
+
163
+ const DURATION: Record<string, MemberDoc> = {
164
+ ms: { doc: "As a plain number of milliseconds.", example: "1.5s.ms # 1500" },
165
+ seconds: { doc: "As a plain number of seconds." },
166
+ minutes: { doc: "As a plain number of minutes.", example: "90s.minutes # 1.5" },
167
+ hours: { doc: "As a plain number of hours." },
168
+ };
169
+
170
+ const SIZE: Record<string, MemberDoc> = {
171
+ bytes: { doc: "As a plain number of bytes." },
172
+ kb: { doc: "As a plain number of kilobytes.", example: "2mb.kb # 2048" },
173
+ mb: { doc: "As a plain number of megabytes." },
174
+ gb: { doc: "As a plain number of gigabytes." },
175
+ };
176
+
177
+ const PERCENT: Record<string, MemberDoc> = {
178
+ ratio: { doc: "As a fraction of one.", example: "50%.ratio # 0.5" },
179
+ percent: { doc: "As a number out of a hundred.", example: "0.5.round(2)" },
180
+ of: { doc: "That share of a number.", example: "12%.of(50) # 6" },
181
+ };
182
+
183
+ const TASK: Record<string, MemberDoc> = {
184
+ wait: { doc: "The value, once the work has finished.", example: "let page = job.wait" },
185
+ done: { doc: "True once it has finished, either way." },
186
+ failed: { doc: "True when it finished by failing." },
187
+ settle: { doc: "Wait without the failure spreading — the value, or nothing." },
188
+ };
189
+
190
+ /** Documentation for every built-in member, by the kind of value it hangs off. */
191
+ export const MEMBER_DOCS: Readonly<Record<string, Record<string, MemberDoc>>> = {
192
+ list: LIST,
193
+ string: STRING,
194
+ map: MAP,
195
+ number: NUMBER,
196
+ duration: DURATION,
197
+ size: SIZE,
198
+ percent: PERCENT,
199
+ task: TASK,
200
+ };
201
+
202
+ const KINDS = new Set(["string", "number", "duration", "size", "percent"]);
203
+
204
+ /** Which table of members a type answers to, if any. */
205
+ export function memberKind(type: Type): string | undefined {
206
+ const t = prune(type);
207
+ if (t.kind === "list") return "list";
208
+ if (t.kind === "record") return "map";
209
+ if (t.kind !== "prim") return undefined;
210
+ return KINDS.has(t.name) ? t.name : undefined;
211
+ }
@@ -0,0 +1,57 @@
1
+ import type { Document, TypeDecl } from "../generated/ast.js";
2
+ import { isTypeDecl } from "../generated/ast.js";
3
+ import type { TypeCatalog } from "./catalog.types.js";
4
+ import type { TypeContext } from "./context.js";
5
+ import { KIND_TYPES } from "./kind-types.js";
6
+ import { NULL, record, type Type, union } from "./type.types.js";
7
+ import { typeRefToType } from "./type-ref.js";
8
+
9
+ /** The named types a document declares, such as `type User { … }`, by name. */
10
+ export interface NamedTypes {
11
+ get(name: string): Type | undefined;
12
+ }
13
+
14
+ /**
15
+ * Collect every `type` declaration into resolvable record types. A shared table
16
+ * lets one type reference another declared later; an unknown name stays
17
+ * `dynamic`, so annotations never add friction.
18
+ *
19
+ * The decorator kinds are in the table before the file is read: `Fn`, `Flow`
20
+ * and the rest are types like any other, so `deco memoize(target: Fn)` needs no
21
+ * special path to hover, complete and check. A file that declares a type of its
22
+ * own by one of those names wins, because they are a stdlib, not reserved words.
23
+ */
24
+ export function collectNamedTypes(
25
+ doc: Document,
26
+ ctx: TypeContext,
27
+ catalog?: TypeCatalog,
28
+ ): NamedTypes {
29
+ const table = new Map<string, Type>(KIND_TYPES);
30
+ const named: NamedTypes = { get: (name) => table.get(name) };
31
+ for (const decl of doc.decls.filter(isTypeDecl)) {
32
+ table.set(decl.name, declaredType({ decl, ctx, named, catalog }));
33
+ }
34
+ return named;
35
+ }
36
+
37
+ /** A `type` is either a shape of its own or another name for one. */
38
+ function declaredType(args: Scope & { decl: TypeDecl }): Type {
39
+ if (args.decl.alias) return typeRefToType({ ...args, ref: args.decl.alias });
40
+ return recordOf(args);
41
+ }
42
+
43
+ interface Scope {
44
+ ctx: TypeContext;
45
+ named: NamedTypes;
46
+ catalog?: TypeCatalog;
47
+ }
48
+
49
+ /** A record of the declared fields. An optional one reads as `T | null`. */
50
+ function recordOf(args: Scope & { decl: TypeDecl }): Type {
51
+ const fields = new Map<string, Type>();
52
+ for (const field of args.decl.body?.fields ?? []) {
53
+ const type = typeRefToType({ ...args, ref: field.fieldType });
54
+ fields.set(field.name, field.optional ? union([type, NULL]) : type);
55
+ }
56
+ return record(fields);
57
+ }
@@ -0,0 +1,152 @@
1
+ import { DYNAMIC, list, NUMBER, STRING, type Type, variadic } from "./type.types.js";
2
+
3
+ /** One argument of a prelude verb, named so the editor can point at it. */
4
+ export interface PreludeArg {
5
+ name: string;
6
+ type: string;
7
+ doc?: string;
8
+ optional?: boolean;
9
+ }
10
+
11
+ /** What the editor needs to describe a prelude name: its type, and what it is for. */
12
+ export interface PreludeSpec {
13
+ /** How it reads when written out. Shown as the hover's signature. */
14
+ signature: string;
15
+ doc: string;
16
+ example?: string;
17
+ type: Type;
18
+ /**
19
+ * The arguments, one by one. `signature` above is a whole line meant to be
20
+ * read; these are meant to be pointed at, one at a time, as each is typed.
21
+ */
22
+ args?: readonly PreludeArg[];
23
+ }
24
+
25
+ /**
26
+ * The prelude, described once: the checker reads the types, the editor reads
27
+ * the prose. Two tables would drift; this one cannot.
28
+ */
29
+ export const PRELUDE_SPECS: Readonly<Record<string, PreludeSpec>> = {
30
+ spawn: {
31
+ signature: "spawn(fn () -> T) -> task",
32
+ doc: "Start work without waiting for it. Everything else waits by itself, so this is how to carry on — ask for the value later with `.wait`.",
33
+ example: "let job = spawn(fn () => http.get(url))\nlet page = job.wait",
34
+ type: variadic([DYNAMIC], DYNAMIC),
35
+ args: [
36
+ {
37
+ name: "work",
38
+ type: "fn () -> T",
39
+ doc: "What to start. It runs on its own; ask for the answer later.",
40
+ },
41
+ ],
42
+ },
43
+ print: {
44
+ signature: "print(…) -> null",
45
+ doc: "Write to standard output, followed by a newline. A map or list shows as JSON.",
46
+ args: [
47
+ {
48
+ name: "values",
49
+ type: "…",
50
+ doc: "Anything, as many as you like. Spaced apart on one line.",
51
+ },
52
+ ],
53
+ example: 'print "hello" 42\nprint pretty(user)',
54
+ type: variadic([DYNAMIC], { kind: "prim", name: "null" }),
55
+ },
56
+ log: {
57
+ signature: "log(…) -> null",
58
+ doc: "Record a message in the event stream — what a reporter and a test see, not stdout.",
59
+ example: 'log "retrying" attempt',
60
+ args: [
61
+ {
62
+ name: "values",
63
+ type: "…",
64
+ doc: "Anything, as many as you like. Goes to the event stream, not stdout.",
65
+ },
66
+ ],
67
+ type: variadic([DYNAMIC], { kind: "prim", name: "null" }),
68
+ },
69
+ range: {
70
+ signature: "range(from?, to, step?) -> list<number>",
71
+ doc: "A list of numbers, counting up or down. The end is exclusive.",
72
+ example:
73
+ "range(3) # [0, 1, 2]\nrange(1, 4) # [1, 2, 3]\nrange(0, 10, 2) # [0, 2, 4, 6, 8]",
74
+ args: [
75
+ {
76
+ name: "from",
77
+ type: "number",
78
+ doc: "Where to start. Omit it and counting starts at 0.",
79
+ optional: true,
80
+ },
81
+ { name: "to", type: "number", doc: "Where to stop, exclusive." },
82
+ { name: "step", type: "number", doc: "How far apart. Negative counts down.", optional: true },
83
+ ],
84
+ type: variadic([NUMBER], list(NUMBER)),
85
+ },
86
+ str: {
87
+ signature: "str(…) -> string",
88
+ doc: "Every argument as text, spaced. Works on `null`, which has no methods of its own.",
89
+ example: 'str("total:", 42, true) # "total: 42 true"',
90
+ args: [{ name: "values", type: "…", doc: "Anything, as many as you like." }],
91
+ type: variadic([DYNAMIC], STRING),
92
+ },
93
+ typeOf: {
94
+ signature: "typeOf(value) -> string",
95
+ doc: "The name of a value's type: `list`, `map`, `string`, `number`, `bool`, `fn`, `null`.",
96
+ example: 'typeOf([1, 2]) # "list"',
97
+ args: [{ name: "value", type: "dynamic", doc: "Whatever you want the name of." }],
98
+ type: variadic([DYNAMIC], STRING),
99
+ },
100
+ pretty: {
101
+ signature: "pretty(value) -> string",
102
+ doc: "Indented JSON — `fmt.json(x, 2)` without the import.",
103
+ args: [{ name: "value", type: "dynamic", doc: "What to render." }],
104
+ example: "print pretty(user)",
105
+ type: variadic([DYNAMIC], STRING),
106
+ },
107
+ wait: {
108
+ signature: "wait(duration) -> null",
109
+ doc: "Pause for a duration.",
110
+ args: [{ name: "duration", type: "duration", doc: "How long: `500ms`, `2s`, `1m`." }],
111
+ example: "wait 500ms",
112
+ type: variadic([DYNAMIC], { kind: "prim", name: "null" }),
113
+ },
114
+ skip: {
115
+ signature: "skip(reason) -> null",
116
+ doc: "Skip the rest of the current flow.",
117
+ args: [
118
+ {
119
+ name: "reason",
120
+ type: "string",
121
+ doc: "Why — it is reported alongside the skip.",
122
+ optional: true,
123
+ },
124
+ ],
125
+ type: variadic([DYNAMIC], { kind: "prim", name: "null" }),
126
+ },
127
+ fail: {
128
+ signature: "fail(message) -> never",
129
+ doc: "Fail the current step with a message.",
130
+ args: [{ name: "message", type: "string", doc: "What went wrong, in the reader's terms." }],
131
+ type: variadic([DYNAMIC], { kind: "prim", name: "null" }),
132
+ },
133
+ exit: {
134
+ signature: "exit(code) -> never",
135
+ doc: "End the program with an exit code.",
136
+ args: [
137
+ {
138
+ name: "code",
139
+ type: "number",
140
+ doc: "0 means success. Anything else does not.",
141
+ optional: true,
142
+ },
143
+ ],
144
+ example: "exit 1",
145
+ type: variadic([NUMBER], { kind: "prim", name: "null" }),
146
+ },
147
+ };
148
+
149
+ /** Whether a bare name is part of the prelude: no `use` needed, always in scope. */
150
+ export function isPrelude(name: string): boolean {
151
+ return name in PRELUDE_SPECS;
152
+ }
@@ -0,0 +1,58 @@
1
+ import { walkAst } from "../ast/index.js";
2
+ import { decoTarget } from "../expand/index.js";
3
+ import type { DecoDecl, Document, FnDecl } from "../generated/ast.js";
4
+ import * as ast from "../generated/ast.js";
5
+
6
+ /**
7
+ * The verbs that change what a call to a function looks like.
8
+ *
9
+ * `rename` is absent: it moves the name rather than the shape, and the checker
10
+ * has no better answer for the new name than it had for the old one.
11
+ */
12
+ const RESHAPING: readonly string[] = ["addParam", "removeParam", "wrap"];
13
+
14
+ /**
15
+ * The functions a `deco` gives a different shape to.
16
+ *
17
+ * The checker reads the program as written, before expansion has run, so a
18
+ * function that `@inject("who")` gave a parameter to still looks like the one
19
+ * the author typed and every call passing that parameter looks wrong. It is not:
20
+ * the program runs. Inference cannot know the new shape without expanding, so
21
+ * for these it says `dynamic` and stops pinning a signature that is about to
22
+ * change, rather than rejecting a program that works.
23
+ *
24
+ * Narrow on purpose: only a function actually carrying a decorator that
25
+ * actually reshapes. Everything else keeps every check it had.
26
+ */
27
+ export function reshapedFns(args: {
28
+ document: Document;
29
+ decos: ReadonlyMap<string, DecoDecl>;
30
+ }): ReadonlySet<FnDecl> {
31
+ const found = new Set<FnDecl>();
32
+ const names = reshapingNames(args.decos);
33
+ if (names.size === 0) return found;
34
+ for (const decl of args.document.decls) {
35
+ if (ast.isFnDecl(decl) && decl.annotations.some((one) => names.has(one.name))) found.add(decl);
36
+ }
37
+ return found;
38
+ }
39
+
40
+ /** The decorators in reach whose body changes the shape of what it is applied to. */
41
+ function reshapingNames(decos: ReadonlyMap<string, DecoDecl>): Set<string> {
42
+ const names = new Set<string>();
43
+ for (const [name, decl] of decos) {
44
+ if (reshapes(decl)) names.add(name);
45
+ }
46
+ return names;
47
+ }
48
+
49
+ /**
50
+ * Whether the body reaches for one of those verbs on its target, anywhere.
51
+ * Inside an `if` counts, because the checker cannot know which way it goes.
52
+ */
53
+ function reshapes(decl: DecoDecl): boolean {
54
+ const target = decoTarget(decl)?.name;
55
+ if (!target) return false;
56
+ const verbs = RESHAPING.map((verb) => `${target}.${verb}`);
57
+ return walkAst(decl.body).some((node) => ast.isActionCall(node) && verbs.includes(node.target));
58
+ }
@@ -0,0 +1,75 @@
1
+ import type { TypeContext } from "./context.js";
2
+ import type { Type } from "./type.types.js";
3
+ import { prune } from "./unify.js";
4
+
5
+ /**
6
+ * A type scheme: a type universally quantified over some variables. This is what
7
+ * makes generics real. `fn id(x) => x` becomes `∀t. t -> t`, and every call
8
+ * instantiates `t` afresh, so `id(1)` and `id("a")` both check.
9
+ */
10
+ export interface Scheme {
11
+ readonly quantified: readonly number[];
12
+ readonly type: Type;
13
+ }
14
+
15
+ /** A non-generic scheme: a plain type with nothing quantified. */
16
+ export function mono(type: Type): Scheme {
17
+ return { quantified: [], type };
18
+ }
19
+
20
+ /** Collect the ids of the unsolved variables reachable from a type. */
21
+ export function freeVars(type: Type, into: Set<number> = new Set()): Set<number> {
22
+ const t = prune(type);
23
+ if (t.kind === "var") into.add(t.id);
24
+ else if (t.kind === "list") freeVars(t.element, into);
25
+ else if (t.kind === "fn") {
26
+ for (const param of t.params) freeVars(param, into);
27
+ freeVars(t.result, into);
28
+ } else if (t.kind === "record") {
29
+ for (const field of t.fields.values()) freeVars(field, into);
30
+ }
31
+ return into;
32
+ }
33
+
34
+ /**
35
+ * Generalise a type over the variables free in it but not in the environment.
36
+ * Those become the scheme's type parameters.
37
+ */
38
+ export function generalize(type: Type, envFree: ReadonlySet<number>): Scheme {
39
+ const quantified = [...freeVars(type)].filter((id) => !envFree.has(id));
40
+ return { quantified, type };
41
+ }
42
+
43
+ /** Instantiate a scheme with fresh variables for each quantified parameter. */
44
+ export function instantiate(scheme: Scheme, ctx: TypeContext): Type {
45
+ if (scheme.quantified.length === 0) return scheme.type;
46
+ const fresh = new Map(scheme.quantified.map((id) => [id, ctx.fresh() as Type]));
47
+ return substitute(scheme.type, fresh);
48
+ }
49
+
50
+ function substitute(type: Type, mapping: ReadonlyMap<number, Type>): Type {
51
+ const t = prune(type);
52
+ switch (t.kind) {
53
+ case "var":
54
+ return mapping.get(t.id) ?? t;
55
+ case "list":
56
+ return { kind: "list", element: substitute(t.element, mapping) };
57
+ case "fn":
58
+ return {
59
+ kind: "fn",
60
+ params: t.params.map((param) => substitute(param, mapping)),
61
+ result: substitute(t.result, mapping),
62
+ };
63
+ case "record":
64
+ return { kind: "record", open: t.open, fields: substituteFields(t.fields, mapping) };
65
+ default:
66
+ return t;
67
+ }
68
+ }
69
+
70
+ function substituteFields(
71
+ fields: ReadonlyMap<string, Type>,
72
+ mapping: ReadonlyMap<number, Type>,
73
+ ): Map<string, Type> {
74
+ return new Map([...fields].map(([name, field]) => [name, substitute(field, mapping)]));
75
+ }
@@ -0,0 +1,72 @@
1
+ import type { AstNode } from "langium";
2
+ import type { DecoDecl, Document, Param } from "../generated/ast.js";
3
+ import * as ast from "../generated/ast.js";
4
+ import type { TypeCatalog } from "./catalog.types.js";
5
+ import { createContext } from "./context.js";
6
+ import type { Infer, Slot } from "./infer.js";
7
+ import { collectNamedTypes } from "./named-types.js";
8
+ import { solidify } from "./solidify.js";
9
+ import type { Type } from "./type.types.js";
10
+
11
+ /** What the call sites turned out to say about each unwritten parameter. */
12
+ export type ParamSeeds = ReadonlyMap<AstNode, Type>;
13
+
14
+ /** How the checker's two passes are wired, without either importing the other. */
15
+ export type SeedRun = (document: Document, infer: Infer) => void;
16
+
17
+ /**
18
+ * A first pass, in silence: what the callers say the named functions take.
19
+ *
20
+ * A `fn` declared at the top of a file is not written where it is called, so on
21
+ * its own it knows nothing about its parameters. The file does say, though:
22
+ * every call is a statement of what goes in. This pass keeps the functions
23
+ * monomorphic so those calls reach the declaration, reads off whatever got
24
+ * decided, and throws the rest away. The real pass then starts from the answers.
25
+ *
26
+ * Nothing is carried over from a file that did not type-check cleanly: a
27
+ * conflict means the first caller won by accident, and a guess made that way
28
+ * would turn into an error the author never made.
29
+ */
30
+ export function seedParams(args: {
31
+ document: Document;
32
+ catalog?: TypeCatalog;
33
+ /** Carried through so this pass draws the same line around a reshaped `fn`;
34
+ * without it one such function's calls would look like a conflict and every
35
+ * seed in the file would be dropped. */
36
+ decos?: ReadonlyMap<string, DecoDecl>;
37
+ parsed?: Map<AstNode, Slot[]>;
38
+ run: SeedRun;
39
+ }): ParamSeeds {
40
+ const wanted = unwritten(args.document);
41
+ if (wanted.length === 0) return new Map();
42
+ const ctx = createContext();
43
+ const infer: Infer = {
44
+ ctx,
45
+ named: collectNamedTypes(args.document, ctx, args.catalog),
46
+ catalog: args.catalog,
47
+ decos: args.decos,
48
+ parsed: args.parsed,
49
+ types: new Map(),
50
+ seeding: true,
51
+ };
52
+ args.run(args.document, infer);
53
+ return ctx.mismatches.length > 0 ? new Map() : settled(wanted, infer);
54
+ }
55
+
56
+ /** Every parameter of a top-level `fn` that carries no annotation. */
57
+ function unwritten(document: Document): Param[] {
58
+ return document.decls
59
+ .filter(ast.isFnDecl)
60
+ .flatMap((decl) => decl.params?.params ?? [])
61
+ .filter((param) => !param.paramType);
62
+ }
63
+
64
+ function settled(params: readonly Param[], infer: Infer): ParamSeeds {
65
+ const seeds = new Map<AstNode, Type>();
66
+ for (const param of params) {
67
+ const found = infer.types?.get(param);
68
+ const solid = found && solidify(found);
69
+ if (solid) seeds.set(param, solid);
70
+ }
71
+ return seeds;
72
+ }