@grimoirelabs/core 0.5.0 → 0.7.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/dist/compiler/expression-parser.d.ts.map +1 -1
- package/dist/compiler/expression-parser.js +2 -0
- package/dist/compiler/expression-parser.js.map +1 -1
- package/dist/compiler/grimoire/index.d.ts.map +1 -1
- package/dist/compiler/grimoire/index.js +14 -4
- package/dist/compiler/grimoire/index.js.map +1 -1
- package/dist/compiler/grimoire/parser.d.ts +1 -1
- package/dist/compiler/grimoire/parser.d.ts.map +1 -1
- package/dist/compiler/grimoire/parser.js +49 -13
- package/dist/compiler/grimoire/parser.js.map +1 -1
- package/dist/compiler/index.d.ts +1 -0
- package/dist/compiler/index.d.ts.map +1 -1
- package/dist/compiler/index.js +1 -0
- package/dist/compiler/index.js.map +1 -1
- package/dist/compiler/type-checker.d.ts +31 -0
- package/dist/compiler/type-checker.d.ts.map +1 -0
- package/dist/compiler/type-checker.js +676 -0
- package/dist/compiler/type-checker.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/expression-evaluator.d.ts.map +1 -1
- package/dist/runtime/expression-evaluator.js +18 -0
- package/dist/runtime/expression-evaluator.js.map +1 -1
- package/dist/types/expressions.d.ts +1 -1
- package/dist/types/expressions.d.ts.map +1 -1
- package/dist/types/expressions.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time type checker for SpellIR
|
|
3
|
+
*
|
|
4
|
+
* Operates on SpellIR (same input as the validator), walks all steps/guards,
|
|
5
|
+
* and infers types bottom-up from expressions. Phase 2: type issues are errors.
|
|
6
|
+
*/
|
|
7
|
+
const BUILTIN_SIGNATURES = {
|
|
8
|
+
min: {
|
|
9
|
+
args: [
|
|
10
|
+
["a", "number"],
|
|
11
|
+
["b", "number"],
|
|
12
|
+
],
|
|
13
|
+
returns: "number",
|
|
14
|
+
variadic: true,
|
|
15
|
+
minArgs: 2,
|
|
16
|
+
},
|
|
17
|
+
max: {
|
|
18
|
+
args: [
|
|
19
|
+
["a", "number"],
|
|
20
|
+
["b", "number"],
|
|
21
|
+
],
|
|
22
|
+
returns: "number",
|
|
23
|
+
variadic: true,
|
|
24
|
+
minArgs: 2,
|
|
25
|
+
},
|
|
26
|
+
abs: { args: [["n", "number"]], returns: "number" },
|
|
27
|
+
sum: { args: [["arr", { kind: "array", element: "number" }]], returns: "number" },
|
|
28
|
+
avg: { args: [["arr", { kind: "array", element: "number" }]], returns: "number" },
|
|
29
|
+
balance: { args: [["asset", "asset"]], returns: "bigint" },
|
|
30
|
+
price: {
|
|
31
|
+
args: [
|
|
32
|
+
["base", "asset"],
|
|
33
|
+
["quote", "asset"],
|
|
34
|
+
],
|
|
35
|
+
returns: "number",
|
|
36
|
+
},
|
|
37
|
+
get_apy: {
|
|
38
|
+
args: [
|
|
39
|
+
["venue", "any"],
|
|
40
|
+
["asset", "asset"],
|
|
41
|
+
],
|
|
42
|
+
returns: "number",
|
|
43
|
+
},
|
|
44
|
+
get_health_factor: { args: [["venue", "any"]], returns: "number" },
|
|
45
|
+
get_position: {
|
|
46
|
+
args: [
|
|
47
|
+
["venue", "any"],
|
|
48
|
+
["asset", "asset"],
|
|
49
|
+
],
|
|
50
|
+
returns: "bigint",
|
|
51
|
+
},
|
|
52
|
+
get_debt: {
|
|
53
|
+
args: [
|
|
54
|
+
["venue", "any"],
|
|
55
|
+
["asset", "asset"],
|
|
56
|
+
],
|
|
57
|
+
returns: "bigint",
|
|
58
|
+
},
|
|
59
|
+
to_number: { args: [["n", "bigint"]], returns: "number" },
|
|
60
|
+
to_bigint: { args: [["n", "number"]], returns: "bigint" },
|
|
61
|
+
};
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// TYPE FORMATTING
|
|
64
|
+
// =============================================================================
|
|
65
|
+
function formatSpellType(t) {
|
|
66
|
+
if (typeof t === "string")
|
|
67
|
+
return t;
|
|
68
|
+
if (t.kind === "array")
|
|
69
|
+
return `array<${formatSpellType(t.element)}>`;
|
|
70
|
+
if (t.kind === "record") {
|
|
71
|
+
if (!t.fields || t.fields.size === 0)
|
|
72
|
+
return "record";
|
|
73
|
+
const entries = [...t.fields.entries()].map(([k, v]) => `${k}: ${formatSpellType(v)}`);
|
|
74
|
+
return `record{${entries.join(", ")}}`;
|
|
75
|
+
}
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// SUBTYPE / ASSIGNABILITY
|
|
80
|
+
// =============================================================================
|
|
81
|
+
function isAssignable(source, target) {
|
|
82
|
+
// any is compatible with everything
|
|
83
|
+
if (source === "any" || target === "any")
|
|
84
|
+
return true;
|
|
85
|
+
// exact match for primitives
|
|
86
|
+
if (typeof source === "string" && typeof target === "string") {
|
|
87
|
+
if (source === target)
|
|
88
|
+
return true;
|
|
89
|
+
// subtypes: asset and address are subtypes of string
|
|
90
|
+
if (target === "string" && (source === "asset" || source === "address"))
|
|
91
|
+
return true;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
// array assignability
|
|
95
|
+
if (typeof source === "object" &&
|
|
96
|
+
source.kind === "array" &&
|
|
97
|
+
typeof target === "object" &&
|
|
98
|
+
target.kind === "array") {
|
|
99
|
+
return isAssignable(source.element, target.element);
|
|
100
|
+
}
|
|
101
|
+
// record to record
|
|
102
|
+
if (typeof source === "object" &&
|
|
103
|
+
source.kind === "record" &&
|
|
104
|
+
typeof target === "object" &&
|
|
105
|
+
target.kind === "record") {
|
|
106
|
+
// If target has no field requirements, any record is fine
|
|
107
|
+
if (!target.fields || target.fields.size === 0)
|
|
108
|
+
return true;
|
|
109
|
+
if (!source.fields)
|
|
110
|
+
return false;
|
|
111
|
+
// structural: all target fields must be present and assignable
|
|
112
|
+
for (const [key, targetType] of target.fields) {
|
|
113
|
+
const sourceType = source.fields.get(key);
|
|
114
|
+
if (!sourceType || !isAssignable(sourceType, targetType))
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// TYPE INFERENCE FROM IR VALUES
|
|
123
|
+
// =============================================================================
|
|
124
|
+
/** Map ParamDef.type to SpellType */
|
|
125
|
+
function paramTypeToSpellType(t) {
|
|
126
|
+
switch (t) {
|
|
127
|
+
case "number":
|
|
128
|
+
case "amount":
|
|
129
|
+
case "bps":
|
|
130
|
+
case "duration":
|
|
131
|
+
return "number";
|
|
132
|
+
case "bool":
|
|
133
|
+
return "bool";
|
|
134
|
+
case "address":
|
|
135
|
+
return "address";
|
|
136
|
+
case "asset":
|
|
137
|
+
return "asset";
|
|
138
|
+
case "string":
|
|
139
|
+
return "string";
|
|
140
|
+
default:
|
|
141
|
+
return "any";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Infer SpellType from a state field's initialValue */
|
|
145
|
+
function inferTypeFromValue(v) {
|
|
146
|
+
if (v === null || v === undefined)
|
|
147
|
+
return "any";
|
|
148
|
+
if (typeof v === "number")
|
|
149
|
+
return "number";
|
|
150
|
+
if (typeof v === "boolean")
|
|
151
|
+
return "bool";
|
|
152
|
+
if (typeof v === "string") {
|
|
153
|
+
if (v.startsWith("0x"))
|
|
154
|
+
return "address";
|
|
155
|
+
return "string";
|
|
156
|
+
}
|
|
157
|
+
if (typeof v === "bigint")
|
|
158
|
+
return "bigint";
|
|
159
|
+
if (Array.isArray(v)) {
|
|
160
|
+
if (v.length === 0)
|
|
161
|
+
return { kind: "array", element: "any" };
|
|
162
|
+
return { kind: "array", element: inferTypeFromValue(v[0]) };
|
|
163
|
+
}
|
|
164
|
+
if (typeof v === "object")
|
|
165
|
+
return { kind: "record" };
|
|
166
|
+
return "any";
|
|
167
|
+
}
|
|
168
|
+
/** Map AdvisoryOutputSchema to SpellType */
|
|
169
|
+
function advisorySchemaToSpellType(schema) {
|
|
170
|
+
switch (schema.type) {
|
|
171
|
+
case "boolean":
|
|
172
|
+
return "bool";
|
|
173
|
+
case "number":
|
|
174
|
+
return "number";
|
|
175
|
+
case "string":
|
|
176
|
+
case "enum":
|
|
177
|
+
return "string";
|
|
178
|
+
case "array":
|
|
179
|
+
return {
|
|
180
|
+
kind: "array",
|
|
181
|
+
element: schema.items ? advisorySchemaToSpellType(schema.items) : "any",
|
|
182
|
+
};
|
|
183
|
+
case "object": {
|
|
184
|
+
if (!schema.fields || Object.keys(schema.fields).length === 0) {
|
|
185
|
+
return { kind: "record" };
|
|
186
|
+
}
|
|
187
|
+
const fields = new Map();
|
|
188
|
+
for (const [key, fieldSchema] of Object.entries(schema.fields)) {
|
|
189
|
+
fields.set(key, advisorySchemaToSpellType(fieldSchema));
|
|
190
|
+
}
|
|
191
|
+
return { kind: "record", fields };
|
|
192
|
+
}
|
|
193
|
+
default:
|
|
194
|
+
return "any";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// =============================================================================
|
|
198
|
+
// EXPRESSION TYPE INFERENCE
|
|
199
|
+
// =============================================================================
|
|
200
|
+
/** Arithmetic operators */
|
|
201
|
+
const ARITHMETIC_OPS = new Set(["+", "-", "*", "/", "%"]);
|
|
202
|
+
/** Comparison operators */
|
|
203
|
+
const COMPARISON_OPS = new Set(["==", "!=", "<", ">", "<=", ">="]);
|
|
204
|
+
/** Logical operators */
|
|
205
|
+
const LOGICAL_OPS = new Set(["AND", "OR"]);
|
|
206
|
+
function inferExprType(expr, env, errors, location) {
|
|
207
|
+
switch (expr.kind) {
|
|
208
|
+
case "literal": {
|
|
209
|
+
switch (expr.type) {
|
|
210
|
+
case "int":
|
|
211
|
+
case "float":
|
|
212
|
+
return "number";
|
|
213
|
+
case "bool":
|
|
214
|
+
return "bool";
|
|
215
|
+
case "string":
|
|
216
|
+
return "string";
|
|
217
|
+
case "address":
|
|
218
|
+
return "address";
|
|
219
|
+
case "json":
|
|
220
|
+
// Could be array or record
|
|
221
|
+
if (Array.isArray(expr.value))
|
|
222
|
+
return { kind: "array", element: "any" };
|
|
223
|
+
if (expr.value !== null && typeof expr.value === "object")
|
|
224
|
+
return { kind: "record" };
|
|
225
|
+
return "any";
|
|
226
|
+
}
|
|
227
|
+
return "any";
|
|
228
|
+
}
|
|
229
|
+
case "param": {
|
|
230
|
+
const t = env.params.get(expr.name);
|
|
231
|
+
if (t)
|
|
232
|
+
return t;
|
|
233
|
+
// Unknown param — validator catches this, we just return any
|
|
234
|
+
return "any";
|
|
235
|
+
}
|
|
236
|
+
case "state": {
|
|
237
|
+
const key = `${expr.scope}:${expr.key}`;
|
|
238
|
+
const t = env.state.get(key);
|
|
239
|
+
if (t)
|
|
240
|
+
return t;
|
|
241
|
+
return "any";
|
|
242
|
+
}
|
|
243
|
+
case "binding": {
|
|
244
|
+
const t = env.bindings.get(expr.name);
|
|
245
|
+
if (t)
|
|
246
|
+
return t;
|
|
247
|
+
return "any";
|
|
248
|
+
}
|
|
249
|
+
case "item":
|
|
250
|
+
case "index":
|
|
251
|
+
// Context-dependent: item is the current loop element, index is number
|
|
252
|
+
// Without loop context tracking, we use any for item and number for index
|
|
253
|
+
return expr.kind === "index" ? "number" : "any";
|
|
254
|
+
case "binary": {
|
|
255
|
+
const leftType = inferExprType(expr.left, env, errors, location);
|
|
256
|
+
const rightType = inferExprType(expr.right, env, errors, location);
|
|
257
|
+
if (ARITHMETIC_OPS.has(expr.op)) {
|
|
258
|
+
// Arithmetic: operands should be numeric
|
|
259
|
+
if (leftType !== "any" && rightType !== "any") {
|
|
260
|
+
// number op number -> number
|
|
261
|
+
if (leftType === "number" && rightType === "number")
|
|
262
|
+
return "number";
|
|
263
|
+
// bigint op bigint -> bigint
|
|
264
|
+
if (leftType === "bigint" && rightType === "bigint")
|
|
265
|
+
return "bigint";
|
|
266
|
+
// string + string -> string (concatenation), including subtypes (asset, address)
|
|
267
|
+
if (expr.op === "+" && isStringLike(leftType) && isStringLike(rightType))
|
|
268
|
+
return "string";
|
|
269
|
+
// Mismatch
|
|
270
|
+
if (!isNumericType(leftType) || !isNumericType(rightType)) {
|
|
271
|
+
errors.push({
|
|
272
|
+
code: "TYPE_MISMATCH",
|
|
273
|
+
message: `${location}: Arithmetic operator '${expr.op}' requires numeric operands, got ${formatSpellType(leftType)} and ${formatSpellType(rightType)}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
else if (leftType !== rightType) {
|
|
277
|
+
errors.push({
|
|
278
|
+
code: "TYPE_MISMATCH",
|
|
279
|
+
message: `${location}: Arithmetic operator '${expr.op}' has mismatched operand types: ${formatSpellType(leftType)} and ${formatSpellType(rightType)}`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Default to number for arithmetic
|
|
284
|
+
if (leftType === "bigint" || rightType === "bigint")
|
|
285
|
+
return "bigint";
|
|
286
|
+
return "number";
|
|
287
|
+
}
|
|
288
|
+
if (COMPARISON_OPS.has(expr.op)) {
|
|
289
|
+
// Comparisons always return bool
|
|
290
|
+
// Allow number/bigint auto-promotion for comparisons
|
|
291
|
+
if (leftType !== "any" && rightType !== "any") {
|
|
292
|
+
const leftNumeric = isNumericType(leftType);
|
|
293
|
+
const rightNumeric = isNumericType(rightType);
|
|
294
|
+
if (leftNumeric && rightNumeric) {
|
|
295
|
+
// number vs bigint comparison is ok (auto-promotion)
|
|
296
|
+
}
|
|
297
|
+
else if (!isAssignable(leftType, rightType) && !isAssignable(rightType, leftType)) {
|
|
298
|
+
errors.push({
|
|
299
|
+
code: "TYPE_MISMATCH",
|
|
300
|
+
message: `${location}: Comparison '${expr.op}' between incompatible types: ${formatSpellType(leftType)} and ${formatSpellType(rightType)}`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return "bool";
|
|
305
|
+
}
|
|
306
|
+
if (LOGICAL_OPS.has(expr.op)) {
|
|
307
|
+
// Logical ops require bool operands
|
|
308
|
+
if (leftType !== "any" && leftType !== "bool") {
|
|
309
|
+
errors.push({
|
|
310
|
+
code: "TYPE_MISMATCH",
|
|
311
|
+
message: `${location}: Logical operator '${expr.op}' requires bool operands, got ${formatSpellType(leftType)}`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (rightType !== "any" && rightType !== "bool") {
|
|
315
|
+
errors.push({
|
|
316
|
+
code: "TYPE_MISMATCH",
|
|
317
|
+
message: `${location}: Logical operator '${expr.op}' requires bool operands, got ${formatSpellType(rightType)}`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return "bool";
|
|
321
|
+
}
|
|
322
|
+
return "any";
|
|
323
|
+
}
|
|
324
|
+
case "unary": {
|
|
325
|
+
const argType = inferExprType(expr.arg, env, errors, location);
|
|
326
|
+
if (expr.op === "NOT") {
|
|
327
|
+
if (argType !== "any" && argType !== "bool") {
|
|
328
|
+
errors.push({
|
|
329
|
+
code: "TYPE_MISMATCH",
|
|
330
|
+
message: `${location}: NOT operator requires bool operand, got ${formatSpellType(argType)}`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return "bool";
|
|
334
|
+
}
|
|
335
|
+
if (expr.op === "-" || expr.op === "ABS") {
|
|
336
|
+
if (argType !== "any" && !isNumericType(argType)) {
|
|
337
|
+
errors.push({
|
|
338
|
+
code: "TYPE_MISMATCH",
|
|
339
|
+
message: `${location}: '${expr.op}' operator requires numeric operand, got ${formatSpellType(argType)}`,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return argType === "bigint" ? "bigint" : "number";
|
|
343
|
+
}
|
|
344
|
+
return "any";
|
|
345
|
+
}
|
|
346
|
+
case "ternary": {
|
|
347
|
+
const condType = inferExprType(expr.condition, env, errors, location);
|
|
348
|
+
if (condType !== "any" && condType !== "bool") {
|
|
349
|
+
errors.push({
|
|
350
|
+
code: "TYPE_MISMATCH",
|
|
351
|
+
message: `${location}: Ternary condition must be bool, got ${formatSpellType(condType)}`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const thenType = inferExprType(expr.then, env, errors, location);
|
|
355
|
+
const elseType = inferExprType(expr.else, env, errors, location);
|
|
356
|
+
// If one branch is any, return the other
|
|
357
|
+
if (thenType === "any")
|
|
358
|
+
return elseType;
|
|
359
|
+
if (elseType === "any")
|
|
360
|
+
return thenType;
|
|
361
|
+
if (!isAssignable(thenType, elseType) && !isAssignable(elseType, thenType)) {
|
|
362
|
+
errors.push({
|
|
363
|
+
code: "TYPE_MISMATCH",
|
|
364
|
+
message: `${location}: Ternary branches have incompatible types: ${formatSpellType(thenType)} and ${formatSpellType(elseType)}`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return thenType;
|
|
368
|
+
}
|
|
369
|
+
case "call": {
|
|
370
|
+
const sig = BUILTIN_SIGNATURES[expr.fn];
|
|
371
|
+
if (!sig)
|
|
372
|
+
return "any";
|
|
373
|
+
// Check argument count
|
|
374
|
+
if (sig.variadic) {
|
|
375
|
+
const minArgs = sig.minArgs ?? sig.args.length;
|
|
376
|
+
if (expr.args.length < minArgs) {
|
|
377
|
+
errors.push({
|
|
378
|
+
code: "WRONG_ARG_COUNT",
|
|
379
|
+
message: `${location}: Function '${expr.fn}' expects at least ${minArgs} argument(s), got ${expr.args.length}`,
|
|
380
|
+
});
|
|
381
|
+
return sig.returns;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else if (expr.args.length !== sig.args.length) {
|
|
385
|
+
errors.push({
|
|
386
|
+
code: "WRONG_ARG_COUNT",
|
|
387
|
+
message: `${location}: Function '${expr.fn}' expects ${sig.args.length} argument(s), got ${expr.args.length}`,
|
|
388
|
+
});
|
|
389
|
+
return sig.returns;
|
|
390
|
+
}
|
|
391
|
+
// Check argument types
|
|
392
|
+
for (let i = 0; i < expr.args.length; i++) {
|
|
393
|
+
const argType = inferExprType(expr.args[i], env, errors, location);
|
|
394
|
+
// For variadic functions, extra args use the type of the first arg
|
|
395
|
+
const sigArg = sig.args[Math.min(i, sig.args.length - 1)];
|
|
396
|
+
const expectedType = sigArg[1];
|
|
397
|
+
if (argType !== "any" && !isAssignable(argType, expectedType)) {
|
|
398
|
+
errors.push({
|
|
399
|
+
code: "TYPE_MISMATCH",
|
|
400
|
+
message: `${location}: Function '${expr.fn}' argument '${sigArg[0]}' expects ${formatSpellType(expectedType)}, got ${formatSpellType(argType)}`,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return sig.returns;
|
|
405
|
+
}
|
|
406
|
+
case "array_access": {
|
|
407
|
+
const arrType = inferExprType(expr.array, env, errors, location);
|
|
408
|
+
const idxType = inferExprType(expr.index, env, errors, location);
|
|
409
|
+
if (idxType !== "any" && idxType !== "number") {
|
|
410
|
+
errors.push({
|
|
411
|
+
code: "TYPE_MISMATCH",
|
|
412
|
+
message: `${location}: Array index must be number, got ${formatSpellType(idxType)}`,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (typeof arrType === "object" && arrType.kind === "array") {
|
|
416
|
+
return arrType.element;
|
|
417
|
+
}
|
|
418
|
+
// Accessing a non-array — might be any
|
|
419
|
+
if (arrType !== "any") {
|
|
420
|
+
errors.push({
|
|
421
|
+
code: "TYPE_MISMATCH",
|
|
422
|
+
message: `${location}: Array access on non-array type ${formatSpellType(arrType)}`,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return "any";
|
|
426
|
+
}
|
|
427
|
+
case "property_access": {
|
|
428
|
+
const objType = inferExprType(expr.object, env, errors, location);
|
|
429
|
+
if (typeof objType === "object" && objType.kind === "record" && objType.fields) {
|
|
430
|
+
const fieldType = objType.fields.get(expr.property);
|
|
431
|
+
if (fieldType)
|
|
432
|
+
return fieldType;
|
|
433
|
+
}
|
|
434
|
+
// action_result properties are always any
|
|
435
|
+
if (objType === "action_result")
|
|
436
|
+
return "any";
|
|
437
|
+
return "any";
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** Check if a type is numeric (number or bigint) */
|
|
442
|
+
function isNumericType(t) {
|
|
443
|
+
return t === "number" || t === "bigint";
|
|
444
|
+
}
|
|
445
|
+
/** Check if a type is string-like (string, asset, address) */
|
|
446
|
+
function isStringLike(t) {
|
|
447
|
+
return t === "string" || t === "asset" || t === "address";
|
|
448
|
+
}
|
|
449
|
+
// =============================================================================
|
|
450
|
+
// STEP TYPE CHECKING
|
|
451
|
+
// =============================================================================
|
|
452
|
+
function checkStep(step, env, errors) {
|
|
453
|
+
const loc = `step '${step.id}'`;
|
|
454
|
+
switch (step.kind) {
|
|
455
|
+
case "compute": {
|
|
456
|
+
for (const assignment of step.assignments) {
|
|
457
|
+
const exprType = inferExprType(assignment.expression, env, errors, loc);
|
|
458
|
+
env.bindings.set(assignment.variable, exprType);
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case "action": {
|
|
463
|
+
// Type-check amount expressions in actions
|
|
464
|
+
const action = step.action;
|
|
465
|
+
if ("amount" in action && action.amount !== "max" && typeof action.amount !== "bigint") {
|
|
466
|
+
inferExprType(action.amount, env, errors, loc);
|
|
467
|
+
}
|
|
468
|
+
// Type-check 'to' field if it's an expression
|
|
469
|
+
if ("to" in action &&
|
|
470
|
+
typeof action.to === "object" &&
|
|
471
|
+
action.to !== null &&
|
|
472
|
+
"kind" in action.to) {
|
|
473
|
+
inferExprType(action.to, env, errors, loc);
|
|
474
|
+
}
|
|
475
|
+
// Type-check toChain if it's an expression
|
|
476
|
+
if ("toChain" in action &&
|
|
477
|
+
typeof action.toChain === "object" &&
|
|
478
|
+
action.toChain !== null &&
|
|
479
|
+
"kind" in action.toChain) {
|
|
480
|
+
inferExprType(action.toChain, env, errors, loc);
|
|
481
|
+
}
|
|
482
|
+
// Type-check constraint expressions
|
|
483
|
+
checkConstraintExpressions(step.constraints, env, errors, loc);
|
|
484
|
+
// Record output binding
|
|
485
|
+
if (step.outputBinding) {
|
|
486
|
+
env.bindings.set(step.outputBinding, "action_result");
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case "conditional": {
|
|
491
|
+
const condType = inferExprType(step.condition, env, errors, loc);
|
|
492
|
+
if (condType !== "any" && condType !== "bool") {
|
|
493
|
+
errors.push({
|
|
494
|
+
code: "TYPE_MISMATCH",
|
|
495
|
+
message: `${loc}: Condition must be bool, got ${formatSpellType(condType)}`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case "loop": {
|
|
501
|
+
if (step.loopType.type === "until") {
|
|
502
|
+
const condType = inferExprType(step.loopType.condition, env, errors, loc);
|
|
503
|
+
if (condType !== "any" && condType !== "bool") {
|
|
504
|
+
errors.push({
|
|
505
|
+
code: "TYPE_MISMATCH",
|
|
506
|
+
message: `${loc}: Loop 'until' condition must be bool, got ${formatSpellType(condType)}`,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (step.loopType.type === "for") {
|
|
511
|
+
const srcType = inferExprType(step.loopType.source, env, errors, loc);
|
|
512
|
+
if (srcType !== "any" && (typeof srcType !== "object" || srcType.kind !== "array")) {
|
|
513
|
+
errors.push({
|
|
514
|
+
code: "TYPE_MISMATCH",
|
|
515
|
+
message: `${loc}: 'for' loop source must be an array, got ${formatSpellType(srcType)}`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
// Record loop variable type
|
|
519
|
+
if (typeof srcType === "object" && srcType.kind === "array") {
|
|
520
|
+
env.bindings.set(step.loopType.variable, srcType.element);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
env.bindings.set(step.loopType.variable, "any");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (step.outputBinding) {
|
|
527
|
+
env.bindings.set(step.outputBinding, { kind: "array", element: "any" });
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "parallel": {
|
|
532
|
+
// Check join strategy metric expression if present
|
|
533
|
+
if (step.join.type === "best") {
|
|
534
|
+
inferExprType(step.join.metric, env, errors, loc);
|
|
535
|
+
}
|
|
536
|
+
if (step.outputBinding) {
|
|
537
|
+
env.bindings.set(step.outputBinding, { kind: "array", element: "any" });
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case "pipeline": {
|
|
542
|
+
const srcType = inferExprType(step.source, env, errors, loc);
|
|
543
|
+
// Pipeline source should be an array
|
|
544
|
+
if (srcType !== "any" && (typeof srcType !== "object" || srcType.kind !== "array")) {
|
|
545
|
+
errors.push({
|
|
546
|
+
code: "TYPE_MISMATCH",
|
|
547
|
+
message: `${loc}: Pipeline source must be an array, got ${formatSpellType(srcType)}`,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// Check stage expressions
|
|
551
|
+
for (const stage of step.stages) {
|
|
552
|
+
if (stage.op === "where") {
|
|
553
|
+
const predType = inferExprType(stage.predicate, env, errors, loc);
|
|
554
|
+
if (predType !== "any" && predType !== "bool") {
|
|
555
|
+
errors.push({
|
|
556
|
+
code: "TYPE_MISMATCH",
|
|
557
|
+
message: `${loc}: Pipeline 'where' predicate must be bool, got ${formatSpellType(predType)}`,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (stage.op === "sort") {
|
|
562
|
+
inferExprType(stage.by, env, errors, loc);
|
|
563
|
+
}
|
|
564
|
+
if (stage.op === "reduce") {
|
|
565
|
+
inferExprType(stage.initial, env, errors, loc);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (step.outputBinding) {
|
|
569
|
+
env.bindings.set(step.outputBinding, srcType !== "any" ? srcType : { kind: "array", element: "any" });
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case "try": {
|
|
574
|
+
// No expression-level type checks needed for try structure
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
case "advisory": {
|
|
578
|
+
// Type-check context expressions
|
|
579
|
+
if (step.context) {
|
|
580
|
+
for (const [key, expr] of Object.entries(step.context)) {
|
|
581
|
+
inferExprType(expr, env, errors, `${loc} context '${key}'`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// Type-check fallback expression
|
|
585
|
+
inferExprType(step.fallback, env, errors, loc);
|
|
586
|
+
// Record output binding with schema-derived type
|
|
587
|
+
const outType = advisorySchemaToSpellType(step.outputSchema);
|
|
588
|
+
env.bindings.set(step.outputBinding, outType);
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
case "emit": {
|
|
592
|
+
for (const [key, expr] of Object.entries(step.data)) {
|
|
593
|
+
inferExprType(expr, env, errors, `${loc} data '${key}'`);
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
case "wait":
|
|
598
|
+
case "halt":
|
|
599
|
+
// No type checks needed
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** Type-check constraint expressions */
|
|
604
|
+
function checkConstraintExpressions(constraints, env, errors, location) {
|
|
605
|
+
const exprFields = [
|
|
606
|
+
"minOutput",
|
|
607
|
+
"maxInput",
|
|
608
|
+
"minLiquidity",
|
|
609
|
+
"requireQuote",
|
|
610
|
+
"requireSimulation",
|
|
611
|
+
"maxGas",
|
|
612
|
+
];
|
|
613
|
+
for (const field of exprFields) {
|
|
614
|
+
const val = constraints[field];
|
|
615
|
+
if (val && typeof val === "object" && "kind" in val) {
|
|
616
|
+
inferExprType(val, env, errors, `${location} constraint '${field}'`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// =============================================================================
|
|
621
|
+
// GUARD TYPE CHECKING
|
|
622
|
+
// =============================================================================
|
|
623
|
+
function checkGuard(guard, env, errors) {
|
|
624
|
+
// Advisory guards have no expression to type-check
|
|
625
|
+
if ("advisor" in guard && guard.advisor)
|
|
626
|
+
return;
|
|
627
|
+
// Expression guard: verify check resolves to bool
|
|
628
|
+
if ("check" in guard && guard.check) {
|
|
629
|
+
const loc = `guard '${guard.id}'`;
|
|
630
|
+
const checkType = inferExprType(guard.check, env, errors, loc);
|
|
631
|
+
if (checkType !== "any" && checkType !== "bool") {
|
|
632
|
+
errors.push({
|
|
633
|
+
code: "TYPE_MISMATCH",
|
|
634
|
+
message: `${loc}: Guard check must be bool, got ${formatSpellType(checkType)}`,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// =============================================================================
|
|
640
|
+
// MAIN ENTRY POINT
|
|
641
|
+
// =============================================================================
|
|
642
|
+
/**
|
|
643
|
+
* Type-check a SpellIR and return errors and warnings.
|
|
644
|
+
* Phase 2: type issues are errors that block compilation.
|
|
645
|
+
*/
|
|
646
|
+
export function typeCheckIR(ir) {
|
|
647
|
+
const errors = [];
|
|
648
|
+
// Build type environment from IR
|
|
649
|
+
const env = {
|
|
650
|
+
params: new Map(),
|
|
651
|
+
state: new Map(),
|
|
652
|
+
bindings: new Map(),
|
|
653
|
+
assets: new Set(ir.assets.map((a) => a.symbol)),
|
|
654
|
+
};
|
|
655
|
+
// Populate param types
|
|
656
|
+
for (const p of ir.params) {
|
|
657
|
+
env.params.set(p.name, paramTypeToSpellType(p.type));
|
|
658
|
+
}
|
|
659
|
+
// Populate state types from initial values
|
|
660
|
+
for (const [key, field] of Object.entries(ir.state.persistent)) {
|
|
661
|
+
env.state.set(`persistent:${key}`, inferTypeFromValue(field.initialValue));
|
|
662
|
+
}
|
|
663
|
+
for (const [key, field] of Object.entries(ir.state.ephemeral)) {
|
|
664
|
+
env.state.set(`ephemeral:${key}`, inferTypeFromValue(field.initialValue));
|
|
665
|
+
}
|
|
666
|
+
// Type-check all steps (order matters — bindings are accumulated)
|
|
667
|
+
for (const step of ir.steps) {
|
|
668
|
+
checkStep(step, env, errors);
|
|
669
|
+
}
|
|
670
|
+
// Type-check all guards
|
|
671
|
+
for (const guard of ir.guards) {
|
|
672
|
+
checkGuard(guard, env, errors);
|
|
673
|
+
}
|
|
674
|
+
return { errors, warnings: [] };
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=type-checker.js.map
|