@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.
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/index.d.ts +3654 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13137 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
- package/src/ast/call-args.ts +15 -0
- package/src/ast/dotted-path.ts +22 -0
- package/src/ast/index.ts +7 -0
- package/src/ast/is-statement.ts +40 -0
- package/src/ast/split-call.ts +28 -0
- package/src/ast/walk.ts +6 -0
- package/src/codes/build-problem.ts +24 -0
- package/src/codes/catalog.ts +35 -0
- package/src/codes/code.types.ts +7 -0
- package/src/codes/index.ts +3 -0
- package/src/compile/compile.ts +169 -0
- package/src/compile/compile.types.ts +40 -0
- package/src/compile/index.ts +3 -0
- package/src/compile/lex-scope.ts +78 -0
- package/src/compile/nodes/binary.ts +56 -0
- package/src/compile/nodes/call.ts +50 -0
- package/src/compile/nodes/collection.ts +83 -0
- package/src/compile/nodes/const-lit.ts +30 -0
- package/src/compile/nodes/fast-binary.ts +71 -0
- package/src/compile/nodes/fn.ts +87 -0
- package/src/compile/nodes/index.ts +8 -0
- package/src/compile/nodes/literal.ts +86 -0
- package/src/compile/nodes/map-fields.ts +79 -0
- package/src/compile/nodes/member.ts +32 -0
- package/src/events/envelope.types.ts +17 -0
- package/src/events/event-data.types.ts +25 -0
- package/src/events/ids.types.ts +10 -0
- package/src/events/index.ts +5 -0
- package/src/events/run-plan.types.ts +18 -0
- package/src/events/status.types.ts +2 -0
- package/src/expand/accepted-kinds.ts +23 -0
- package/src/expand/deco/deco-decorator.ts +77 -0
- package/src/expand/deco/deco-env.ts +24 -0
- package/src/expand/deco/deco.types.ts +43 -0
- package/src/expand/deco/document-decos.ts +64 -0
- package/src/expand/deco/index.ts +7 -0
- package/src/expand/deco/read-signature.ts +57 -0
- package/src/expand/deco/run-body.ts +119 -0
- package/src/expand/decorate-callable.ts +49 -0
- package/src/expand/decorations.ts +41 -0
- package/src/expand/expand.ts +108 -0
- package/src/expand/expand.types.ts +78 -0
- package/src/expand/handles/around-verbs.ts +35 -0
- package/src/expand/handles/binding-verbs.ts +27 -0
- package/src/expand/handles/common-verbs.ts +40 -0
- package/src/expand/handles/fn-verbs.ts +58 -0
- package/src/expand/handles/handle.types.ts +33 -0
- package/src/expand/handles/index.ts +4 -0
- package/src/expand/handles/kind-of.ts +36 -0
- package/src/expand/handles/make-handle.ts +100 -0
- package/src/expand/handles/missing-verb.ts +27 -0
- package/src/expand/handles/runnable-verbs.ts +13 -0
- package/src/expand/handles/type-verbs.ts +56 -0
- package/src/expand/index.ts +40 -0
- package/src/expand/make-context.ts +69 -0
- package/src/expand/node-meta.ts +30 -0
- package/src/expand/node-span.ts +17 -0
- package/src/expand/swap-node.ts +28 -0
- package/src/expand/wrong-kind.ts +102 -0
- package/src/expr/cell.types.ts +25 -0
- package/src/expr/closure.ts +53 -0
- package/src/expr/closure.types.ts +31 -0
- package/src/expr/eval-env.types.ts +9 -0
- package/src/expr/evaluate.ts +18 -0
- package/src/expr/frame.ts +60 -0
- package/src/expr/index.ts +14 -0
- package/src/expr/invoke.ts +140 -0
- package/src/expr/member-value.ts +91 -0
- package/src/expr/methods/index.ts +65 -0
- package/src/expr/methods/list-grouping.ts +107 -0
- package/src/expr/methods/list-methods.ts +57 -0
- package/src/expr/methods/list-selection.ts +142 -0
- package/src/expr/methods/map-extras.ts +87 -0
- package/src/expr/methods/map-methods.ts +17 -0
- package/src/expr/methods/number-methods.ts +29 -0
- package/src/expr/methods/string-extras.ts +66 -0
- package/src/expr/methods/string-methods.ts +32 -0
- package/src/expr/methods/unit-methods.ts +54 -0
- package/src/expr/namespace.ts +20 -0
- package/src/expr/native.types.ts +40 -0
- package/src/expr/operators.ts +91 -0
- package/src/expr/pending.ts +47 -0
- package/src/expr/prelude.ts +75 -0
- package/src/expr/task.ts +71 -0
- package/src/format/format-text.ts +22 -0
- package/src/format/format.types.ts +18 -0
- package/src/format/from-settings.ts +40 -0
- package/src/format/index.ts +5 -0
- package/src/format/organize-header.ts +66 -0
- package/src/format/reindent.ts +35 -0
- package/src/generated/ast.ts +2383 -0
- package/src/generated/grammar.ts +5236 -0
- package/src/generated/module.ts +25 -0
- package/src/grammar/venn.langium +291 -0
- package/src/graph/graph.types.ts +20 -0
- package/src/graph/index.ts +2 -0
- package/src/graph/to-graph.ts +87 -0
- package/src/index.ts +93 -0
- package/src/interpolation/compile-template.ts +40 -0
- package/src/interpolation/index.ts +4 -0
- package/src/interpolation/interpolation.types.ts +18 -0
- package/src/interpolation/scan-interpolations.ts +47 -0
- package/src/interpolation/template.types.ts +19 -0
- package/src/lang/default-services.ts +14 -0
- package/src/lang/index.ts +3 -0
- package/src/lang/venn-lexer.ts +34 -0
- package/src/lang/venn-module.ts +51 -0
- package/src/module/index.ts +5 -0
- package/src/module/specifier.ts +21 -0
- package/src/parse/error-to-problem.ts +43 -0
- package/src/parse/index.ts +3 -0
- package/src/parse/parse-expression.ts +27 -0
- package/src/parse/parse-output.types.ts +8 -0
- package/src/parse/parse.ts +26 -0
- package/src/problem/build-diff.ts +116 -0
- package/src/problem/diff.types.ts +20 -0
- package/src/problem/format-value.ts +40 -0
- package/src/problem/index.ts +8 -0
- package/src/problem/problem-error.ts +15 -0
- package/src/problem/problem.types.ts +21 -0
- package/src/problem/related.types.ts +7 -0
- package/src/problem/severity.types.ts +2 -0
- package/src/problem/span.types.ts +8 -0
- package/src/typecheck/action-signature.ts +53 -0
- package/src/typecheck/builtin-types.ts +63 -0
- package/src/typecheck/builtins.ts +267 -0
- package/src/typecheck/catalog.types.ts +16 -0
- package/src/typecheck/check-deco.ts +174 -0
- package/src/typecheck/check-stmts.ts +151 -0
- package/src/typecheck/check-types.ts +232 -0
- package/src/typecheck/context.ts +31 -0
- package/src/typecheck/imported-types.ts +127 -0
- package/src/typecheck/index.ts +26 -0
- package/src/typecheck/infer.ts +416 -0
- package/src/typecheck/kind-types.ts +79 -0
- package/src/typecheck/member-docs.ts +211 -0
- package/src/typecheck/named-types.ts +57 -0
- package/src/typecheck/prelude-types.ts +152 -0
- package/src/typecheck/reshaped-fns.ts +58 -0
- package/src/typecheck/scheme.ts +75 -0
- package/src/typecheck/seed-params.ts +72 -0
- package/src/typecheck/seed-values.ts +64 -0
- package/src/typecheck/show.ts +64 -0
- package/src/typecheck/solidify.ts +60 -0
- package/src/typecheck/spec-to-type.ts +88 -0
- package/src/typecheck/type-env.ts +46 -0
- package/src/typecheck/type-ref.ts +62 -0
- package/src/typecheck/type.types.ts +168 -0
- package/src/typecheck/unify.ts +192 -0
- package/src/typecheck/unit-types.ts +49 -0
- package/src/units/combine.ts +97 -0
- package/src/units/index.ts +6 -0
- package/src/units/parse-instant.ts +12 -0
- package/src/units/parse-number.ts +48 -0
- package/src/units/unit-guards.ts +16 -0
- package/src/units/unit.types.ts +38 -0
- package/src/value/equals.ts +4 -0
- package/src/value/index.ts +4 -0
- package/src/value/is-numeric.ts +6 -0
- package/src/value/truthiness.ts +9 -0
- package/src/value/value.types.ts +12 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import type { AstNode } from "langium";
|
|
2
|
+
import { dottedPath } from "../ast/index.js";
|
|
3
|
+
import type {
|
|
4
|
+
Binary,
|
|
5
|
+
Call,
|
|
6
|
+
DecoDecl,
|
|
7
|
+
Expr,
|
|
8
|
+
FnBody,
|
|
9
|
+
Index,
|
|
10
|
+
ListLit,
|
|
11
|
+
MapLit,
|
|
12
|
+
Member,
|
|
13
|
+
Param,
|
|
14
|
+
ParamList,
|
|
15
|
+
StringLit,
|
|
16
|
+
Ternary,
|
|
17
|
+
TypeRef,
|
|
18
|
+
Unary,
|
|
19
|
+
} from "../generated/ast.js";
|
|
20
|
+
import { scanInterpolations } from "../interpolation/index.js";
|
|
21
|
+
import { parseExpression } from "../parse/parse-expression.js";
|
|
22
|
+
import { callType } from "./action-signature.js";
|
|
23
|
+
import { memberType } from "./builtins.js";
|
|
24
|
+
import type { TypeCatalog } from "./catalog.types.js";
|
|
25
|
+
import type { TypeContext } from "./context.js";
|
|
26
|
+
import type { NamedTypes } from "./named-types.js";
|
|
27
|
+
import { instantiate, mono } from "./scheme.js";
|
|
28
|
+
import type { ParamSeeds } from "./seed-params.js";
|
|
29
|
+
import type { ValueSeeds } from "./seed-values.js";
|
|
30
|
+
import { showType } from "./show.js";
|
|
31
|
+
import { BOOL, DYNAMIC, fn, list, NULL, NUMBER, record, STRING, type Type } from "./type.types.js";
|
|
32
|
+
import type { TypeEnv } from "./type-env.js";
|
|
33
|
+
import { typeRefToType } from "./type-ref.js";
|
|
34
|
+
import { fieldType, prune, unify } from "./unify.js";
|
|
35
|
+
import { combinedType, literalType } from "./unit-types.js";
|
|
36
|
+
|
|
37
|
+
/** One `${…}`: what it parsed to, and whether the source had to be repaired. */
|
|
38
|
+
export interface Slot {
|
|
39
|
+
expr: Expr | undefined;
|
|
40
|
+
guess: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Everything inference threads through: fresh vars, diagnostics, named types. */
|
|
44
|
+
export interface Infer {
|
|
45
|
+
ctx: TypeContext;
|
|
46
|
+
named: NamedTypes;
|
|
47
|
+
/** What the loaded plugins contribute. Absent means "the file stands alone". */
|
|
48
|
+
catalog?: TypeCatalog;
|
|
49
|
+
/** What the callers said a named `fn` takes, found by the first pass. */
|
|
50
|
+
seeds?: ParamSeeds;
|
|
51
|
+
/** What the file's own top-level values hold, so a `fn` body can read them. */
|
|
52
|
+
values?: ValueSeeds;
|
|
53
|
+
/** The types of the names this file imported, from the files it named. */
|
|
54
|
+
imports?: ReadonlyMap<string, Type>;
|
|
55
|
+
/** True during that first pass: keep functions monomorphic so calls reach them. */
|
|
56
|
+
seeding?: boolean;
|
|
57
|
+
/** Every expression's inferred type, for hover. Omit it to save the work. */
|
|
58
|
+
types?: Map<AstNode, Type>;
|
|
59
|
+
/** Per string literal, the expression parsed from each of its ${…} slots. */
|
|
60
|
+
slots?: Map<AstNode, (Expr | undefined)[]>;
|
|
61
|
+
/** Shared across the checker's passes, so a `${…}` is parsed once per document. */
|
|
62
|
+
parsed?: Map<AstNode, Slot[]>;
|
|
63
|
+
/** The `deco`s reachable here, local and imported, by the name an `@` writes. */
|
|
64
|
+
decos?: ReadonlyMap<string, DecoDecl>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Infer an expression's type, recording it for hover and reporting mismatches. */
|
|
68
|
+
export function inferExpr(expr: Expr, env: TypeEnv, infer: Infer): Type {
|
|
69
|
+
const type = inferKind(expr, env, infer);
|
|
70
|
+
infer.types?.set(expr, type);
|
|
71
|
+
return type;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function inferKind(expr: Expr, env: TypeEnv, infer: Infer): Type {
|
|
75
|
+
switch (expr.$type) {
|
|
76
|
+
case "NumberLit":
|
|
77
|
+
return literalType(expr.raw);
|
|
78
|
+
case "StringLit":
|
|
79
|
+
return inferString(expr, env, infer);
|
|
80
|
+
case "BoolLit":
|
|
81
|
+
return BOOL;
|
|
82
|
+
case "NullLit":
|
|
83
|
+
return NULL;
|
|
84
|
+
case "InstantLit":
|
|
85
|
+
return { kind: "prim", name: "instant" };
|
|
86
|
+
case "Ref":
|
|
87
|
+
return inferRef(expr.name, env, infer);
|
|
88
|
+
case "Member":
|
|
89
|
+
return inferMember(expr, env, infer);
|
|
90
|
+
case "Call":
|
|
91
|
+
return inferCall(expr, env, infer);
|
|
92
|
+
case "Index":
|
|
93
|
+
return inferIndex(expr, env, infer);
|
|
94
|
+
case "Binary":
|
|
95
|
+
return inferBinary(expr, env, infer);
|
|
96
|
+
case "Unary":
|
|
97
|
+
return inferUnary(expr, env, infer);
|
|
98
|
+
case "Ternary":
|
|
99
|
+
return inferTernary(expr, env, infer);
|
|
100
|
+
case "ListLit":
|
|
101
|
+
return inferList(expr, env, infer);
|
|
102
|
+
case "MapLit":
|
|
103
|
+
return inferMap(expr, env, infer);
|
|
104
|
+
case "FnExpr":
|
|
105
|
+
return inferFn({ params: expr.params, body: expr.body, returns: expr.returns }, env, infer);
|
|
106
|
+
default:
|
|
107
|
+
return DYNAMIC;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A name the checker does not know types as `dynamic`; it never resolves names. */
|
|
112
|
+
function inferRef(name: string, env: TypeEnv, infer: Infer): Type {
|
|
113
|
+
const scheme = env.lookup(name);
|
|
114
|
+
return scheme ? instantiate(scheme, infer.ctx) : DYNAMIC;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A string is text, but each `${…}` inside it is code. It is parsed apart from
|
|
119
|
+
* the document, so the parsed expressions are kept: they are the only handle the
|
|
120
|
+
* editor has on nodes that inference typed but the document's tree lacks.
|
|
121
|
+
*/
|
|
122
|
+
function inferString(expr: StringLit, env: TypeEnv, infer: Infer): Type {
|
|
123
|
+
const slots = parsedSlots(expr, infer);
|
|
124
|
+
for (const slot of slots) {
|
|
125
|
+
if (slot.expr) inferExpr(contain(slot.expr, expr), env, slot.guess ? quiet(infer) : infer);
|
|
126
|
+
}
|
|
127
|
+
if (slots.length > 0)
|
|
128
|
+
infer.slots?.set(
|
|
129
|
+
expr,
|
|
130
|
+
slots.map((slot) => slot.expr),
|
|
131
|
+
);
|
|
132
|
+
return STRING;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The expressions inside one string, parsed once per document.
|
|
137
|
+
*
|
|
138
|
+
* Parsing a `${…}` means running the parser again, and a file may hold hundreds
|
|
139
|
+
* of them; inference walks the document more than once, and the editor walks it
|
|
140
|
+
* on every keystroke. Keyed by the string's own node, so two strings that happen
|
|
141
|
+
* to read alike never share: the parsed expression is told which string contains
|
|
142
|
+
* it, and a shared one would point at the wrong place.
|
|
143
|
+
*/
|
|
144
|
+
function parsedSlots(expr: StringLit, infer: Infer): Slot[] {
|
|
145
|
+
const known = infer.parsed?.get(expr);
|
|
146
|
+
if (known) return known;
|
|
147
|
+
const text = expr.$cstNode?.text ?? expr.value;
|
|
148
|
+
const slots = scanInterpolations(text).map((slot) => slotExpr(slot.source));
|
|
149
|
+
infer.parsed?.set(expr, slots);
|
|
150
|
+
return slots;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A dot with no name after it yet: what half-typed member access looks like. */
|
|
154
|
+
const UNFINISHED = /\.(?![A-Za-z_])/g;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The expression a placeholder holds.
|
|
158
|
+
*
|
|
159
|
+
* Half-typed code such as `xs.map(fn (p) => p.` does not parse, and that is
|
|
160
|
+
* precisely when the editor is asked what `p` is. So one repair is attempted:
|
|
161
|
+
* drop the member access still being written.
|
|
162
|
+
*/
|
|
163
|
+
function slotExpr(source: string): Slot {
|
|
164
|
+
const whole = parseExpression(source);
|
|
165
|
+
if (whole) return { expr: whole, guess: false };
|
|
166
|
+
// A space, not nothing: every other character keeps the offset the editor
|
|
167
|
+
// will look it up by.
|
|
168
|
+
const repaired = source.replace(UNFINISHED, " ");
|
|
169
|
+
return { expr: repaired === source ? undefined : parseExpression(repaired), guess: true };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Infer without accusing: a repaired expression is a guess, not what was written. */
|
|
173
|
+
function quiet(infer: Infer): Infer {
|
|
174
|
+
return { ...infer, ctx: { ...infer.ctx, mismatches: [] } };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Say in the tree what the source already says: the expression is written
|
|
179
|
+
* inside the string. With a container, looking a name up from inside `${…}`
|
|
180
|
+
* walks out into the scope the string sits in.
|
|
181
|
+
*/
|
|
182
|
+
function contain(child: Expr, parent: StringLit): Expr {
|
|
183
|
+
Object.defineProperty(child, "$container", { value: parent, configurable: true });
|
|
184
|
+
return child;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function inferMember(expr: Member, env: TypeEnv, infer: Infer): Type {
|
|
188
|
+
const receiver = prune(inferExpr(expr.receiver, env, infer));
|
|
189
|
+
if (receiver.kind === "dynamic") return DYNAMIC;
|
|
190
|
+
const built = memberType(receiver, expr.member, infer.ctx);
|
|
191
|
+
if (built) return built;
|
|
192
|
+
if (receiver.kind === "record") return recordField(receiver, expr, infer);
|
|
193
|
+
return unknownMember(receiver, expr, infer);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The kinds whose members are all known: a string, a list, a handle, a literal.
|
|
198
|
+
*
|
|
199
|
+
* There is no shape one of these could turn out to have later, so answering
|
|
200
|
+
* `dynamic` for a member it does not carry is not caution but a wrong answer.
|
|
201
|
+
* Anything still open, `dynamic` above all, is left alone.
|
|
202
|
+
*/
|
|
203
|
+
const CLOSED_MEMBERS = new Set(["list", "prim", "opaque", "literal"]);
|
|
204
|
+
|
|
205
|
+
function unknownMember(receiver: Type, expr: Member, infer: Infer): Type {
|
|
206
|
+
if (!CLOSED_MEMBERS.has(receiver.kind)) return DYNAMIC;
|
|
207
|
+
infer.ctx.mismatches.push({
|
|
208
|
+
node: expr,
|
|
209
|
+
expected: receiver,
|
|
210
|
+
actual: DYNAMIC,
|
|
211
|
+
note: `has no member "${expr.member}"`,
|
|
212
|
+
});
|
|
213
|
+
return DYNAMIC;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function recordField(
|
|
217
|
+
receiver: Extract<Type, { kind: "record" }>,
|
|
218
|
+
expr: Member,
|
|
219
|
+
infer: Infer,
|
|
220
|
+
): Type {
|
|
221
|
+
const found = fieldType(receiver, expr.member);
|
|
222
|
+
if (found) return found;
|
|
223
|
+
infer.ctx.mismatches.push({
|
|
224
|
+
node: expr,
|
|
225
|
+
expected: receiver,
|
|
226
|
+
actual: DYNAMIC,
|
|
227
|
+
note: `has no field "${expr.member}"`,
|
|
228
|
+
});
|
|
229
|
+
return DYNAMIC;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A call, whichever of the two the language spells.
|
|
234
|
+
*
|
|
235
|
+
* `http.get("url")` and `http.get "url"` are the same call and carry the same
|
|
236
|
+
* type. The bracketed form is tried as a verb first, since reading it as an
|
|
237
|
+
* ordinary call on whatever `http.get` evaluates to would answer `dynamic` for
|
|
238
|
+
* every verb written in the form most people reach for.
|
|
239
|
+
*/
|
|
240
|
+
function inferCall(expr: Call, env: TypeEnv, infer: Infer): Type {
|
|
241
|
+
const verb = verbCall(expr, env, infer);
|
|
242
|
+
if (verb) return verb;
|
|
243
|
+
const callee = prune(inferExpr(expr.callee, env, infer));
|
|
244
|
+
const args = (expr.args?.args ?? []).map((arg) => inferExpr(arg.value, env, infer));
|
|
245
|
+
if (callee.kind === "dynamic") return DYNAMIC;
|
|
246
|
+
const result = infer.ctx.fresh();
|
|
247
|
+
expect(infer, expr, callee, fn(args, result));
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** What a plugin verb gives back, when the callee names one. */
|
|
252
|
+
function verbCall(expr: Call, env: TypeEnv, infer: Infer): Type | undefined {
|
|
253
|
+
const target = dottedPath(expr.callee);
|
|
254
|
+
if (!target || !infer.catalog?.signatureOf(target)) return undefined;
|
|
255
|
+
const args = (expr.args?.args ?? []).map((arg) => arg.value);
|
|
256
|
+
return callType({ target, args }, env, infer);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function inferIndex(expr: Index, env: TypeEnv, infer: Infer): Type {
|
|
260
|
+
const receiver = prune(inferExpr(expr.receiver, env, infer));
|
|
261
|
+
inferExpr(expr.index, env, infer);
|
|
262
|
+
if (receiver.kind === "list") return receiver.element;
|
|
263
|
+
return DYNAMIC;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const ARITHMETIC = new Set(["+", "-", "*", "/", "%"]);
|
|
267
|
+
const COMPARISON = new Set(["<", ">", "<=", ">="]);
|
|
268
|
+
|
|
269
|
+
function inferBinary(expr: Binary, env: TypeEnv, infer: Infer): Type {
|
|
270
|
+
const op = expr.operator;
|
|
271
|
+
const left = inferExpr(expr.left, env, infer);
|
|
272
|
+
const right = inferExpr(expr.right, env, infer);
|
|
273
|
+
if (ARITHMETIC.has(op) || COMPARISON.has(op)) return arithmetic({ infer, expr, op, left, right });
|
|
274
|
+
if (op === "&&" || op === "||") return BOOL;
|
|
275
|
+
if (op === "??") return unify(left, right) ? left : DYNAMIC;
|
|
276
|
+
return BOOL;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Arithmetic and comparison, unit-aware.
|
|
281
|
+
*
|
|
282
|
+
* When both sides are known (`300ms` and `1s`, or `2mb` and `5`) the unit rule
|
|
283
|
+
* decides, and a mismatch is a compile error rather than something the run finds
|
|
284
|
+
* out later. When either side is still an unsolved variable this falls back to
|
|
285
|
+
* plain numbers, which is what teaches `fn double(x) => x * 2` that `x` is one.
|
|
286
|
+
*/
|
|
287
|
+
function arithmetic(args: {
|
|
288
|
+
infer: Infer;
|
|
289
|
+
expr: Binary;
|
|
290
|
+
op: string;
|
|
291
|
+
left: Type;
|
|
292
|
+
right: Type;
|
|
293
|
+
}): Type {
|
|
294
|
+
const { infer, expr, op, left, right } = args;
|
|
295
|
+
const combined = combinedType(op, prune(left), prune(right));
|
|
296
|
+
if (!combined) return numeric(infer, expr, left, right, COMPARISON.has(op));
|
|
297
|
+
if (combined.ok) return combined.type;
|
|
298
|
+
mismatched(infer, expr, prune(left), prune(right), op);
|
|
299
|
+
return DYNAMIC;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function mismatched(infer: Infer, node: AstNode, left: Type, right: Type, op: string): void {
|
|
303
|
+
infer.ctx.mismatches.push({
|
|
304
|
+
node,
|
|
305
|
+
expected: left,
|
|
306
|
+
actual: right,
|
|
307
|
+
unit: true,
|
|
308
|
+
// The sentence the evaluator raises, so a mismatch reads the same whether
|
|
309
|
+
// the checker or the run finds it.
|
|
310
|
+
note: `Cannot combine ${showType(left)} with ${showType(right)} using "${op}".`,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function numeric(infer: Infer, node: AstNode, left: Type, right: Type, compare = false): Type {
|
|
315
|
+
expect(infer, node, left, NUMBER);
|
|
316
|
+
expect(infer, node, right, NUMBER);
|
|
317
|
+
return compare ? BOOL : NUMBER;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function inferUnary(expr: Unary, env: TypeEnv, infer: Infer): Type {
|
|
321
|
+
const operand = inferExpr(expr.operand, env, infer);
|
|
322
|
+
if (expr.operator === "!") return BOOL;
|
|
323
|
+
expect(infer, expr, operand, NUMBER);
|
|
324
|
+
return NUMBER;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function inferTernary(expr: Ternary, env: TypeEnv, infer: Infer): Type {
|
|
328
|
+
inferExpr(expr.condition, env, infer);
|
|
329
|
+
const then = inferExpr(expr.then, env, infer);
|
|
330
|
+
const otherwise = inferExpr(expr.otherwise, env, infer);
|
|
331
|
+
return unify(then, otherwise) ? then : DYNAMIC;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function inferList(expr: ListLit, env: TypeEnv, infer: Infer): Type {
|
|
335
|
+
const element = infer.ctx.fresh() as Type;
|
|
336
|
+
for (const item of expr.items) expect(infer, item, inferExpr(item, env, infer), element);
|
|
337
|
+
return list(element);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function inferMap(expr: MapLit, env: TypeEnv, infer: Infer): Type {
|
|
341
|
+
const fields = new Map<string, Type>();
|
|
342
|
+
for (const entry of expr.entries) fields.set(entry.key, inferExpr(entry.value, env, infer));
|
|
343
|
+
return record(fields);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* What a parameter is: what it was annotated with, what the callers said, or an
|
|
348
|
+
* open question for the body to answer.
|
|
349
|
+
*/
|
|
350
|
+
function paramType(param: Param, infer: Infer): Type {
|
|
351
|
+
if (param.paramType) {
|
|
352
|
+
const { ctx, named, catalog } = infer;
|
|
353
|
+
return typeRefToType({ ref: param.paramType, ctx, named, catalog });
|
|
354
|
+
}
|
|
355
|
+
return infer.seeds?.get(param) ?? infer.ctx.fresh();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Infer a function type: params, body, and an inferred (or annotated) result. */
|
|
359
|
+
export function inferFn(
|
|
360
|
+
decl: { params?: ParamList; body: FnBody; returns?: TypeRef },
|
|
361
|
+
env: TypeEnv,
|
|
362
|
+
infer: Infer,
|
|
363
|
+
): Type {
|
|
364
|
+
const params = (decl.params?.params ?? []).map((param) => ({
|
|
365
|
+
node: param,
|
|
366
|
+
name: param.name,
|
|
367
|
+
type: paramType(param, infer),
|
|
368
|
+
}));
|
|
369
|
+
let body = env;
|
|
370
|
+
for (const param of params) body = body.with(param.name, mono(param.type));
|
|
371
|
+
const result = inferBody(decl.body, body, infer);
|
|
372
|
+
// Record each parameter once the body has constrained it, so hover and
|
|
373
|
+
// completion know what `p` is inside `xs.filter(fn (p) => …)`.
|
|
374
|
+
for (const param of params) infer.types?.set(param.node, param.type);
|
|
375
|
+
if (decl.returns)
|
|
376
|
+
expect(
|
|
377
|
+
infer,
|
|
378
|
+
decl.body,
|
|
379
|
+
result,
|
|
380
|
+
typeRefToType({
|
|
381
|
+
ref: decl.returns,
|
|
382
|
+
ctx: infer.ctx,
|
|
383
|
+
named: infer.named,
|
|
384
|
+
catalog: infer.catalog,
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
return fn(
|
|
388
|
+
params.map((param) => param.type),
|
|
389
|
+
result,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* A body's locals, then the expression it ends with.
|
|
395
|
+
*
|
|
396
|
+
* That last expression may be missing. A half-written `fn` is the normal state
|
|
397
|
+
* of a file being edited, and it is exactly then that the editor asks what
|
|
398
|
+
* things are, so missing means unknown rather than broken.
|
|
399
|
+
*/
|
|
400
|
+
function inferBody(body: FnBody, env: TypeEnv, infer: Infer): Type {
|
|
401
|
+
let scope = env;
|
|
402
|
+
for (const local of body.locals) {
|
|
403
|
+
if (!local.value) continue;
|
|
404
|
+
scope = scope.with(local.name, mono(inferExpr(local.value, scope, infer)));
|
|
405
|
+
}
|
|
406
|
+
return body.result ? inferExpr(body.result, scope, infer) : DYNAMIC;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Unify `actual` with `expected`, recording a mismatch on the node if it fails. */
|
|
410
|
+
export function expect(infer: Infer, node: AstNode, actual: Type, expected: Type): void {
|
|
411
|
+
if (!unify(actual, expected)) report(infer, node, expected, actual);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function report(infer: Infer, node: AstNode, expected: Type, actual: Type, _note?: string): void {
|
|
415
|
+
infer.ctx.mismatches.push({ node, expected, actual });
|
|
416
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type TypeSpec, t } from "@venn-lang/types";
|
|
2
|
+
import { TARGET_KINDS, type TargetKind } from "../expand/index.js";
|
|
3
|
+
import { specToType } from "./spec-to-type.js";
|
|
4
|
+
import type { Type } from "./type.types.js";
|
|
5
|
+
|
|
6
|
+
/** A verb that changes the declaration and answers nothing. */
|
|
7
|
+
const NOTHING = t.null;
|
|
8
|
+
|
|
9
|
+
/** A function left behind to run around the real thing, for effect. */
|
|
10
|
+
const AROUND = t.callback([t.dynamic], t.dynamic, 0);
|
|
11
|
+
|
|
12
|
+
/** `wrap` is handed the call itself and its arguments, and decides. */
|
|
13
|
+
const MIDDLEWARE = t.callback([t.dynamic, t.dynamic], t.dynamic, 1);
|
|
14
|
+
|
|
15
|
+
/** What every handle answers, whatever declaration it is holding. */
|
|
16
|
+
const COMMON: Readonly<Record<string, TypeSpec>> = {
|
|
17
|
+
name: t.string,
|
|
18
|
+
meta: t.fn([t.string, t.dynamic], NOTHING),
|
|
19
|
+
remove: t.fn([], NOTHING),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** A flow and a step are one handle: a title, and a way around the body. */
|
|
23
|
+
const RUNNABLE: Readonly<Record<string, TypeSpec>> = {
|
|
24
|
+
...COMMON,
|
|
25
|
+
title: t.string,
|
|
26
|
+
before: t.fn([AROUND], NOTHING),
|
|
27
|
+
after: t.fn([AROUND], NOTHING),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The kinds, published as types.
|
|
32
|
+
*
|
|
33
|
+
* Written as data, the same data `http.Request` is written as, so the checker,
|
|
34
|
+
* the hover and the completion list read one description of what a target
|
|
35
|
+
* offers. This is the static twin of the verb tables in
|
|
36
|
+
* `expand/handles/make-handle.ts`: whatever a kind has there it has here, and
|
|
37
|
+
* `kind-types.test.ts` fails the day they stop agreeing.
|
|
38
|
+
*/
|
|
39
|
+
export const KIND_SPECS: Readonly<Record<TargetKind, TypeSpec>> = {
|
|
40
|
+
Fn: t.record({
|
|
41
|
+
...COMMON,
|
|
42
|
+
params: t.list(t.string),
|
|
43
|
+
addParam: t.fn([t.string], NOTHING),
|
|
44
|
+
removeParam: t.fn([t.string], NOTHING),
|
|
45
|
+
rename: t.fn([t.string], NOTHING),
|
|
46
|
+
wrap: t.fn([MIDDLEWARE], NOTHING),
|
|
47
|
+
before: t.fn([AROUND], NOTHING),
|
|
48
|
+
after: t.fn([AROUND], NOTHING),
|
|
49
|
+
}),
|
|
50
|
+
Flow: t.record(RUNNABLE),
|
|
51
|
+
Step: t.record(RUNNABLE),
|
|
52
|
+
Binding: t.record({ ...COMMON, value: t.dynamic, setValue: t.fn([t.dynamic], NOTHING) }),
|
|
53
|
+
Type: t.record({
|
|
54
|
+
...COMMON,
|
|
55
|
+
fields: t.list(t.string),
|
|
56
|
+
addField: t.fn([t.string, t.dynamic], NOTHING),
|
|
57
|
+
removeField: t.fn([t.string], NOTHING),
|
|
58
|
+
}),
|
|
59
|
+
Node: t.record(COMMON),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The same kinds as the checker's own types, built once.
|
|
64
|
+
*
|
|
65
|
+
* Safe to share across files and across edits: a handle's surface is fixed, so
|
|
66
|
+
* none of these holds an inference variable for a later document to write into.
|
|
67
|
+
*/
|
|
68
|
+
export const KIND_TYPES: ReadonlyMap<string, Type> = build();
|
|
69
|
+
|
|
70
|
+
function build(): Map<string, Type> {
|
|
71
|
+
const table = new Map<string, Type>();
|
|
72
|
+
for (const kind of TARGET_KINDS) {
|
|
73
|
+
table.set(
|
|
74
|
+
kind,
|
|
75
|
+
specToType(KIND_SPECS[kind], (name) => table.get(name)),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return table;
|
|
79
|
+
}
|