@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,64 @@
1
+ import type { AstNode } from "langium";
2
+ import type { DecoDecl, Document, LetStmt } 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 type { SeedRun } from "./seed-params.js";
9
+ import { solidify } from "./solidify.js";
10
+ import type { Type } from "./type.types.js";
11
+
12
+ /** What the file's own top-level values turned out to be, by the name they carry. */
13
+ export type ValueSeeds = ReadonlyMap<string, Type>;
14
+
15
+ /**
16
+ * A first pass, in silence: what the file's top-level `const`s and `let`s hold.
17
+ *
18
+ * A named `fn` is checked before those bindings exist, because generalising it
19
+ * is what lets two callers use it at two types, and a binding written above it
20
+ * may call it. So the file's values cannot simply be bound first. A function
21
+ * body is still free to read them, since it runs later when they are all there,
22
+ * and this pass is what gives those reads a type.
23
+ *
24
+ * Unlike the parameter seeds, a conflict elsewhere does not throw the answers
25
+ * away. A value's type is written in the file, not guessed from a caller, and a
26
+ * half-typed line somewhere else is the normal state of a file being edited:
27
+ * exactly when the help is worth most.
28
+ */
29
+ export function seedValues(args: {
30
+ document: Document;
31
+ catalog?: TypeCatalog;
32
+ decos?: ReadonlyMap<string, DecoDecl>;
33
+ parsed?: Map<AstNode, Slot[]>;
34
+ run: SeedRun;
35
+ }): ValueSeeds {
36
+ const values = topLevelValues(args.document);
37
+ if (values.length === 0 || !args.document.decls.some(ast.isFnDecl)) return new Map();
38
+ const ctx = createContext();
39
+ const infer: Infer = {
40
+ ctx,
41
+ named: collectNamedTypes(args.document, ctx, args.catalog),
42
+ catalog: args.catalog,
43
+ decos: args.decos,
44
+ parsed: args.parsed,
45
+ types: new Map(),
46
+ seeding: true,
47
+ };
48
+ args.run(args.document, infer);
49
+ return settled(values, infer);
50
+ }
51
+
52
+ function topLevelValues(document: Document): LetStmt[] {
53
+ return document.decls.filter(ast.isLetStmt);
54
+ }
55
+
56
+ function settled(values: readonly LetStmt[], infer: Infer): ValueSeeds {
57
+ const seeds = new Map<string, Type>();
58
+ for (const decl of values) {
59
+ const found = infer.types?.get(decl);
60
+ const solid = found && solidify(found);
61
+ if (solid) seeds.set(decl.name, solid);
62
+ }
63
+ return seeds;
64
+ }
@@ -0,0 +1,64 @@
1
+ import type { RecordType, Type } from "./type.types.js";
2
+ import { prune } from "./unify.js";
3
+
4
+ const LETTERS = "abcdefghijklmnopqrstuvwxyz";
5
+
6
+ /** Render one type for a hover or a diagnostic, naming variables `a`, `b`, … */
7
+ export function showType(type: Type): string {
8
+ return render(type, new Map());
9
+ }
10
+
11
+ /**
12
+ * Several types that belong together, named as one.
13
+ *
14
+ * Rendering them one at a time restarts the alphabet each time, so the two
15
+ * unrelated parameters of `fn (nome, idade)` would both come back as `a`. That
16
+ * says they are the same type, which is the opposite of what is known.
17
+ */
18
+ export function showTypes(types: readonly Type[]): string[] {
19
+ const names = new Map<number, string>();
20
+ return types.map((type) => render(type, names));
21
+ }
22
+
23
+ function render(type: Type, names: Map<number, string>): string {
24
+ const t = prune(type);
25
+ switch (t.kind) {
26
+ case "prim":
27
+ return t.name;
28
+ case "dynamic":
29
+ return "dynamic";
30
+ case "var":
31
+ return nameOf(t.id, names);
32
+ case "list":
33
+ return `list<${render(t.element, names)}>`;
34
+ case "fn":
35
+ return `fn(${t.params.map((p) => render(p, names)).join(", ")}) -> ${render(t.result, names)}`;
36
+ case "record":
37
+ return renderRecord(t, names);
38
+ case "literal":
39
+ return typeof t.value === "string" ? `"${t.value}"` : String(t.value);
40
+ case "union":
41
+ return t.members.map((m) => render(m, names)).join(" | ");
42
+ case "opaque":
43
+ return t.name;
44
+ }
45
+ }
46
+
47
+ /** Named keys read as a shape; unnamed ones read as the map they are. */
48
+ function renderRecord(type: RecordType, names: Map<number, string>): string {
49
+ const rest = type.rest ? `map<${render(type.rest, names)}>` : undefined;
50
+ if (type.fields.size === 0) return rest ?? "{}";
51
+ const body = [...type.fields]
52
+ .map(([name, field]) => `${name}: ${render(field, names)}`)
53
+ .join(", ");
54
+ return `{ ${body}${rest ? `, …: ${render(type.rest as Type, names)}` : ""} }`;
55
+ }
56
+
57
+ function nameOf(id: number, names: Map<number, string>): string {
58
+ const existing = names.get(id);
59
+ if (existing) return existing;
60
+ const index = names.size;
61
+ const name = index < LETTERS.length ? (LETTERS[index] as string) : `t${index}`;
62
+ names.set(id, name);
63
+ return name;
64
+ }
@@ -0,0 +1,60 @@
1
+ import { list, type RecordType, record, type Type, union } from "./type.types.js";
2
+ import { prune } from "./unify.js";
3
+
4
+ /**
5
+ * The same type with every variable resolved, or nothing if any is still open.
6
+ *
7
+ * A type solved during one pass carries variables belonging to that pass's
8
+ * context; handing one to the next pass would smuggle in a variable nobody owns.
9
+ * Only a fully settled type crosses, so `undefined` here is an answer rather
10
+ * than a failure.
11
+ */
12
+ export function solidify(type: Type): Type | undefined {
13
+ const t = prune(type);
14
+ switch (t.kind) {
15
+ case "var":
16
+ return undefined;
17
+ case "list":
18
+ return map1(solidify(t.element), list);
19
+ case "union":
20
+ return all(t.members)?.length ? union(all(t.members) as Type[]) : undefined;
21
+ case "record":
22
+ return solidRecord(t);
23
+ case "fn":
24
+ return solidFn(t.params, t.result, t);
25
+ default:
26
+ return t;
27
+ }
28
+ }
29
+
30
+ function map1(inner: Type | undefined, wrap: (type: Type) => Type): Type | undefined {
31
+ return inner ? wrap(inner) : undefined;
32
+ }
33
+
34
+ function all(types: readonly Type[]): Type[] | undefined {
35
+ const solid = types.map(solidify);
36
+ return solid.some((type) => type === undefined) ? undefined : (solid as Type[]);
37
+ }
38
+
39
+ function solidRecord(type: RecordType): Type | undefined {
40
+ const out = new Map<string, Type>();
41
+ for (const [name, field] of type.fields) {
42
+ const solid = solidify(field);
43
+ if (!solid) return undefined;
44
+ out.set(name, solid);
45
+ }
46
+ const rest = type.rest ? solidify(type.rest) : undefined;
47
+ if (type.rest && !rest) return undefined;
48
+ return record(out, type.open, rest);
49
+ }
50
+
51
+ function solidFn(
52
+ params: readonly Type[],
53
+ result: Type,
54
+ shape: { variadic?: boolean; ignorableFrom?: number },
55
+ ): Type | undefined {
56
+ const solidParams = all(params);
57
+ const solidResult = solidify(result);
58
+ if (!solidParams || !solidResult) return undefined;
59
+ return { kind: "fn", params: solidParams, result: solidResult, ...shape };
60
+ }
@@ -0,0 +1,88 @@
1
+ import type { FnSpec, RecordSpec, TypeSpec } from "@venn-lang/types";
2
+ import {
3
+ DYNAMIC,
4
+ fn,
5
+ list,
6
+ literal,
7
+ mapOf,
8
+ NULL,
9
+ opaque,
10
+ prim,
11
+ record,
12
+ type Type,
13
+ union,
14
+ } from "./type.types.js";
15
+
16
+ /** How a `ref` finds what it points at. Returning undefined is fine: the
17
+ * reference degrades to `dynamic` rather than failing. */
18
+ export type ResolveRef = (name: string) => Type | undefined;
19
+
20
+ /**
21
+ * Read the published form of a type into the checker's own.
22
+ *
23
+ * The wire format is plain data with no inference variables; the checker's type
24
+ * is a live thing that unification writes into. Keeping them apart is what lets
25
+ * a signature be written by hand, generated from a `.d.ts`, or shipped as JSON
26
+ * without any of them reaching into the compiler.
27
+ */
28
+ export function specToType(spec: TypeSpec, resolve: ResolveRef): Type {
29
+ switch (spec.kind) {
30
+ case "prim":
31
+ return prim(spec.name);
32
+ case "literal":
33
+ return literal(spec.value);
34
+ case "list":
35
+ return list(specToType(spec.element, resolve));
36
+ case "map":
37
+ return mapOf(specToType(spec.value, resolve));
38
+ default:
39
+ return compound(spec, resolve);
40
+ }
41
+ }
42
+
43
+ function compound(spec: TypeSpec, resolve: ResolveRef): Type {
44
+ switch (spec.kind) {
45
+ case "record":
46
+ return recordType(spec, resolve);
47
+ case "fn":
48
+ return fnType(spec, resolve);
49
+ case "union":
50
+ return union(spec.members.map((member) => specToType(member, resolve)));
51
+ case "opaque":
52
+ return opaque(spec.name, opaqueMembers(spec.members, resolve));
53
+ case "ref":
54
+ return resolve(spec.name) ?? DYNAMIC;
55
+ default:
56
+ return DYNAMIC;
57
+ }
58
+ }
59
+
60
+ /** An optional field is one that may not be there: `T | null`, said plainly. */
61
+ function recordType(spec: RecordSpec, resolve: ResolveRef): Type {
62
+ const optional = new Set(spec.optional ?? []);
63
+ const fields = new Map<string, Type>();
64
+ for (const [name, field] of Object.entries(spec.fields)) {
65
+ const type = specToType(field, resolve);
66
+ fields.set(name, optional.has(name) ? union([type, NULL]) : type);
67
+ }
68
+ return record(fields, spec.open ?? false);
69
+ }
70
+
71
+ /** What a handle publishes, read into the checker's own types. */
72
+ function opaqueMembers(
73
+ members: Readonly<Record<string, TypeSpec>> | undefined,
74
+ resolve: ResolveRef,
75
+ ): ReadonlyMap<string, Type> | undefined {
76
+ if (!members) return undefined;
77
+ const into = new Map<string, Type>();
78
+ for (const [name, spec] of Object.entries(members)) into.set(name, specToType(spec, resolve));
79
+ return into;
80
+ }
81
+
82
+ function fnType(spec: FnSpec, resolve: ResolveRef): Type {
83
+ const params = spec.params.map((param) => specToType(param, resolve));
84
+ const result = specToType(spec.result, resolve);
85
+ return spec.takes === undefined
86
+ ? fn(params, result)
87
+ : { kind: "fn", params, result, ignorableFrom: spec.takes };
88
+ }
@@ -0,0 +1,46 @@
1
+ import { freeVars, type Scheme } from "./scheme.js";
2
+
3
+ /**
4
+ * The names in scope during inference, mapped to their type schemes. Chained, so
5
+ * a function body sees the outer scope. Names it does not know resolve to
6
+ * `dynamic` at the use site: the type checker only ever settles type
7
+ * consistency, never name resolution, which is the runtime's static check.
8
+ */
9
+ export interface TypeEnv {
10
+ lookup(name: string): Scheme | undefined;
11
+ with(name: string, scheme: Scheme): TypeEnv;
12
+ /** Ids of the variables free across every binding: what must not generalise. */
13
+ freeVars(): Set<number>;
14
+ }
15
+
16
+ export function emptyEnv(): TypeEnv {
17
+ return envOf(new Map(), undefined);
18
+ }
19
+
20
+ function envOf(bindings: Map<string, Scheme>, parent: TypeEnv | undefined): TypeEnv {
21
+ return {
22
+ lookup: (name) => bindings.get(name) ?? parent?.lookup(name),
23
+ with(name, scheme) {
24
+ return envOf(new Map([[name, scheme]]), this);
25
+ },
26
+ freeVars() {
27
+ const free = parent ? parent.freeVars() : new Set<number>();
28
+ for (const scheme of bindings.values()) collect(scheme, free);
29
+ return free;
30
+ },
31
+ };
32
+ }
33
+
34
+ /** A binding's contribution to the environment's free variables: the ones it did
35
+ * not quantify. */
36
+ function collect(scheme: Scheme, into: Set<number>): void {
37
+ const quantified = new Set(scheme.quantified);
38
+ for (const id of freeVars(scheme.type)) if (!quantified.has(id)) into.add(id);
39
+ }
40
+
41
+ /** Extend an environment with several bindings at once. */
42
+ export function withAll(env: TypeEnv, bindings: Iterable<[string, Scheme]>): TypeEnv {
43
+ let next = env;
44
+ for (const [name, scheme] of bindings) next = next.with(name, scheme);
45
+ return next;
46
+ }
@@ -0,0 +1,62 @@
1
+ import type { SingleType, TypeRef } from "../generated/ast.js";
2
+ import { isNamedType } from "../generated/ast.js";
3
+ import type { TypeCatalog } from "./catalog.types.js";
4
+ import type { TypeContext } from "./context.js";
5
+ import type { NamedTypes } from "./named-types.js";
6
+ import { DYNAMIC, fn, list, literal, prim, type Type, union } from "./type.types.js";
7
+
8
+ const PRIMS = new Set(["number", "string", "bool", "null", "void"]);
9
+
10
+ /** Everything resolving a written annotation may need to look something up. */
11
+ export interface RefScope {
12
+ ctx: TypeContext;
13
+ named: NamedTypes;
14
+ /** Where a qualified name like `http.Request` is found. */
15
+ catalog?: TypeCatalog;
16
+ }
17
+
18
+ /**
19
+ * Read a written annotation (`list<number>`, `User`, `http.Request`,
20
+ * `"GET" | "POST"`) into a type. An unknown name stays `dynamic`, so an
21
+ * annotation never adds friction: being wrong about a name must not be worse
22
+ * than saying nothing.
23
+ */
24
+ export function typeRefToType(args: RefScope & { ref: TypeRef | undefined }): Type {
25
+ if (!args.ref) return args.ctx.fresh();
26
+ const members = args.ref.members.map((single) => singleToType({ ...args, single }));
27
+ if (members.length === 0) return DYNAMIC;
28
+ return union(members as Type[]);
29
+ }
30
+
31
+ function singleToType(args: RefScope & { single: SingleType }): Type {
32
+ const { single } = args;
33
+ // A `SingleType` that is not a name is a written-out literal such as `"GET"`,
34
+ // and it means that one value. Widening it to `string` would enforce nothing.
35
+ if (!isNamedType(single)) return literalOf(single);
36
+ const name = single.name;
37
+ const generic = genericType(name, single, args);
38
+ if (generic) return generic;
39
+ if (PRIMS.has(name)) return prim(name as "number");
40
+ return args.named.get(name) ?? args.catalog?.typeOf(name) ?? DYNAMIC;
41
+ }
42
+
43
+ /** `"GET"` written as a type. The quotes are the grammar's, not the value's. */
44
+ function literalOf(single: SingleType): Type {
45
+ const written = (single as { value?: string }).value ?? "";
46
+ return literal(written.replace(/^["']|["']$/g, ""));
47
+ }
48
+
49
+ function genericType(
50
+ name: string,
51
+ single: SingleType & { args?: TypeRef[] },
52
+ args: RefScope,
53
+ ): Type | undefined {
54
+ // `ref` last, deliberately: the scope threaded down here still carries the
55
+ // *outer* annotation, and spreading it afterwards would hand each argument its
56
+ // own parent, leaving `list<number>` reading itself forever.
57
+ const params = (single.args ?? []).map((ref) => typeRefToType({ ...args, ref }));
58
+ if (name === "list") return list(params[0] ?? DYNAMIC);
59
+ if (name === "fn") return fn(params.slice(0, -1), params[params.length - 1] ?? DYNAMIC);
60
+ if (name === "map") return DYNAMIC;
61
+ return undefined;
62
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * A static type. `dynamic` is the escape hatch for the result of a plugin
3
+ * action, an HTTP response, `json`: anything the checker cannot know. It unifies
4
+ * with everything and never produces an error, so the effectful world places no
5
+ * annotation burden on the pure one.
6
+ */
7
+ export type Type =
8
+ | PrimType
9
+ | LiteralType
10
+ | ListType
11
+ | RecordType
12
+ | FnType
13
+ | UnionType
14
+ | OpaqueType
15
+ | DynamicType
16
+ | TypeVar;
17
+
18
+ export type PrimName =
19
+ | "number"
20
+ | "string"
21
+ | "bool"
22
+ | "null"
23
+ | "void"
24
+ | "duration"
25
+ | "size"
26
+ | "percent"
27
+ | "instant";
28
+
29
+ export interface PrimType {
30
+ readonly kind: "prim";
31
+ readonly name: PrimName;
32
+ }
33
+
34
+ /** One value, standing for itself: `"GET"`, `200`, `true`. */
35
+ export interface LiteralType {
36
+ readonly kind: "literal";
37
+ readonly value: string | number | boolean;
38
+ }
39
+
40
+ export interface ListType {
41
+ readonly kind: "list";
42
+ readonly element: Type;
43
+ }
44
+
45
+ /** One of several. What makes `"GET" | "POST"` more than a string. */
46
+ export interface UnionType {
47
+ readonly kind: "union";
48
+ readonly members: readonly Type[];
49
+ }
50
+
51
+ /**
52
+ * A handle with a name and no visible inside: a server, a browser, a connection.
53
+ *
54
+ * This is the border with the world outside. Whatever a JavaScript class really
55
+ * is, it arrives here as a name its own namespace's verbs understand and nothing
56
+ * else can open, which keeps the object graph of another language out of this
57
+ * one.
58
+ */
59
+ export interface OpaqueType {
60
+ readonly kind: "opaque";
61
+ readonly name: string;
62
+ /** What it publishes. Absent means a name and nothing more. */
63
+ readonly members?: ReadonlyMap<string, Type>;
64
+ }
65
+
66
+ /** A map/object with known fields. `open` records tolerate extra fields. */
67
+ export interface RecordType {
68
+ readonly kind: "record";
69
+ readonly fields: ReadonlyMap<string, Type>;
70
+ readonly open: boolean;
71
+ /**
72
+ * What a key nobody listed holds: `Record<string, string>` written as a type.
73
+ * Without it an open record answers `dynamic` for everything it did not name,
74
+ * which is right for a loose map and wrong for `headers`.
75
+ */
76
+ readonly rest?: Type;
77
+ }
78
+
79
+ /** A function: what it takes, what it gives back, and how strictly it is called. */
80
+ export interface FnType {
81
+ readonly kind: "fn";
82
+ readonly params: readonly Type[];
83
+ readonly result: Type;
84
+ /** Takes any number of arguments, as `str(a, b, c)` and `range(1, 4)` do. Its
85
+ * params describe the shape it accepts, not a count to enforce. */
86
+ readonly variadic?: boolean;
87
+ /** Params from this index on are handed over but may be ignored: `map` offers
88
+ * the index to a callback that is free to take only the item. */
89
+ readonly ignorableFrom?: number;
90
+ }
91
+
92
+ export interface DynamicType {
93
+ readonly kind: "dynamic";
94
+ }
95
+
96
+ /** A unification variable. `ref` is filled in as inference solves it. */
97
+ export interface TypeVar {
98
+ readonly kind: "var";
99
+ readonly id: number;
100
+ ref: Type | undefined;
101
+ }
102
+
103
+ /** The one `dynamic`. Shared, since it carries no state to keep apart. */
104
+ export const DYNAMIC: DynamicType = { kind: "dynamic" };
105
+
106
+ /** One of the language's primitive types, by name. */
107
+ export function prim(name: PrimName): PrimType {
108
+ return { kind: "prim", name };
109
+ }
110
+
111
+ export const NUMBER: PrimType = prim("number");
112
+ export const STRING: PrimType = prim("string");
113
+ export const BOOL: PrimType = prim("bool");
114
+ export const NULL: PrimType = prim("null");
115
+ export const VOID: PrimType = prim("void");
116
+
117
+ export function list(element: Type): ListType {
118
+ return { kind: "list", element };
119
+ }
120
+
121
+ export function fn(params: readonly Type[], result: Type): FnType {
122
+ return { kind: "fn", params, result };
123
+ }
124
+
125
+ /** A function of any arity, e.g. `str(…)`: only its result is fixed. */
126
+ export function variadic(params: readonly Type[], result: Type): FnType {
127
+ return { kind: "fn", params, result, variadic: true };
128
+ }
129
+
130
+ /**
131
+ * A callback that is handed `params` but need only take the first `takes` of
132
+ * them. `list.map` passes the index alongside the item, and `p => p.age` is
133
+ * still a perfectly good argument to it.
134
+ */
135
+ export function callback(params: readonly Type[], result: Type, takes: number): FnType {
136
+ return { kind: "fn", params, result, ignorableFrom: takes };
137
+ }
138
+
139
+ export function record(fields: ReadonlyMap<string, Type>, open = false, rest?: Type): RecordType {
140
+ return rest ? { kind: "record", fields, open, rest } : { kind: "record", fields, open };
141
+ }
142
+
143
+ /** Keys unknown, values all alike: `map<string>`. */
144
+ export function mapOf(value: Type): RecordType {
145
+ return record(new Map(), true, value);
146
+ }
147
+
148
+ /** One value standing for itself, as `"GET"` does where a type is written. */
149
+ export function literal(value: string | number | boolean): LiteralType {
150
+ return { kind: "literal", value };
151
+ }
152
+
153
+ /** A named handle, with the surface it publishes, or a bare name without one. */
154
+ export function opaque(name: string, members?: ReadonlyMap<string, Type>): OpaqueType {
155
+ return members ? { kind: "opaque", name, members } : { kind: "opaque", name };
156
+ }
157
+
158
+ /** A union of one is that one: `string | string` helps nobody read anything. */
159
+ export function union(members: readonly Type[]): Type {
160
+ const flat = members.flatMap((m) => (m.kind === "union" ? m.members : [m]));
161
+ return flat.length === 1 ? (flat[0] as Type) : { kind: "union", members: flat };
162
+ }
163
+
164
+ /** Which primitive a literal is one of: `"GET"` is a string, and unifies as one. */
165
+ export function baseOf(value: LiteralType["value"]): PrimName {
166
+ if (typeof value === "number") return "number";
167
+ return typeof value === "boolean" ? "bool" : "string";
168
+ }