@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.
@@ -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