bonescript-compiler 0.4.0 → 0.5.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.
@@ -1,606 +1,657 @@
1
- /**
2
- * BoneScript Type Checker — Stage 3 of the compilation pipeline.
3
- * Implements spec/04_TYPE_SYSTEM.md.
4
- *
5
- * Responsibilities:
6
- * 1. Build symbol table from entity declarations
7
- * 2. Verify all field types resolve to valid types
8
- * 3. Verify all constraint expressions type to bool
9
- * 4. Verify capability preconditions type to bool
10
- * 5. Verify effects are well-typed (target and value match)
11
- * 6. Verify emitted events exist
12
- * 7. Verify state machine transitions reference valid states
13
- * 8. Verify flow steps reference valid capabilities
14
- *
15
- * Deterministic: same AST always produces same errors in same order.
16
- */
17
-
18
- import * as AST from "./ast";
19
- import {
20
- CVType, PrimitiveType, GenericType, RecordType,
21
- prim, generic, record, BOTTOM,
22
- typeEquals, typeToString, isNumeric, isComparable,
23
- } from "./types";
24
-
25
- // ─── Type Error ──────────────────────────────────────────────────────────────
26
-
27
- export interface TypeError {
28
- code: string;
29
- message: string;
30
- loc: AST.ASTNode["loc"];
31
- }
32
-
33
- // ─── Symbol Table ────────────────────────────────────────────────────────────
34
-
35
- interface EntitySymbol {
36
- name: string;
37
- type: RecordType;
38
- states: string[];
39
- capabilities: string[];
40
- }
41
-
42
- interface CapabilitySymbol {
43
- name: string;
44
- params: Map<string, CVType>;
45
- }
46
-
47
- interface EventSymbol {
48
- name: string;
49
- payloadFields: Map<string, CVType>;
50
- }
51
-
52
- interface SymbolTable {
53
- entities: Map<string, EntitySymbol>;
54
- capabilities: Map<string, CapabilitySymbol>;
55
- events: Map<string, EventSymbol>;
56
- stores: Set<string>;
57
- channels: Set<string>;
58
- flows: Set<string>;
59
- }
60
-
61
- // ─── Type Checker ────────────────────────────────────────────────────────────
62
-
63
- export class TypeChecker {
64
- private errors: TypeError[] = [];
65
- private symbols: SymbolTable = {
66
- entities: new Map(),
67
- capabilities: new Map(),
68
- events: new Map(),
69
- stores: new Set(),
70
- channels: new Set(),
71
- flows: new Set(),
72
- };
73
-
74
- check(program: AST.ProgramNode): TypeError[] {
75
- this.errors = [];
76
-
77
- for (const system of program.systems) {
78
- this.checkSystem(system);
79
- }
80
-
81
- return this.errors;
82
- }
83
-
84
- private addError(code: string, message: string, loc: AST.ASTNode["loc"]) {
85
- this.errors.push({ code, message, loc });
86
- }
87
-
88
- // ─── Phase 1: Build Symbol Table ──────────────────────────────────────────
89
-
90
- private checkSystem(system: AST.SystemDeclNode) {
91
- // First pass: register all declarations
92
- for (const decl of system.declarations) {
93
- this.registerDeclaration(decl);
94
- }
95
-
96
- // Second pass: type check all declarations
97
- for (const decl of system.declarations) {
98
- this.checkDeclaration(decl);
99
- }
100
- }
101
-
102
- private registerDeclaration(decl: AST.DeclarationNode) {
103
- switch (decl.kind) {
104
- case "EntityDecl":
105
- this.registerEntity(decl);
106
- break;
107
- case "CapabilityDecl":
108
- this.registerCapability(decl);
109
- break;
110
- case "EventDecl":
111
- this.registerEvent(decl);
112
- break;
113
- case "StoreDecl":
114
- this.symbols.stores.add(decl.name);
115
- break;
116
- case "ChannelDecl":
117
- this.symbols.channels.add(decl.name);
118
- break;
119
- case "FlowDecl":
120
- this.symbols.flows.add(decl.name);
121
- break;
122
- }
123
- }
124
-
125
- private registerEntity(decl: AST.EntityDeclNode) {
126
- const fields = new Map<string, CVType>();
127
-
128
- // Ontology-entailed fields (always present)
129
- fields.set("id", prim("uuid"));
130
- fields.set("created_at", prim("timestamp"));
131
- fields.set("updated_at", prim("timestamp"));
132
-
133
- // Declared fields
134
- for (const field of decl.owns) {
135
- const resolved = this.resolveTypeExpr(field.type);
136
- if (resolved) {
137
- fields.set(field.name, resolved);
138
- }
139
- }
140
-
141
- const states = decl.states
142
- ? decl.states.nodes.map(n => n.name)
143
- : [];
144
-
145
- this.symbols.entities.set(decl.name, {
146
- name: decl.name,
147
- type: record(decl.name, fields),
148
- states,
149
- capabilities: [],
150
- });
151
- }
152
-
153
- private registerCapability(decl: AST.CapabilityDeclNode) {
154
- const params = new Map<string, CVType>();
155
- for (const p of decl.params) {
156
- const resolved = this.resolveTypeExpr(p.type);
157
- if (resolved) params.set(p.name, resolved);
158
- }
159
- this.symbols.capabilities.set(decl.name, { name: decl.name, params });
160
- }
161
-
162
- private registerEvent(decl: AST.EventDeclNode) {
163
- const fields = new Map<string, CVType>();
164
- for (const f of decl.payload) {
165
- const resolved = this.resolveTypeExpr(f.type);
166
- if (resolved) fields.set(f.name, resolved);
167
- }
168
- this.symbols.events.set(decl.name, { name: decl.name, payloadFields: fields });
169
- }
170
-
171
- // ─── Phase 2: Type Check Declarations ─────────────────────────────────────
172
-
173
- private checkDeclaration(decl: AST.DeclarationNode) {
174
- switch (decl.kind) {
175
- case "EntityDecl": this.checkEntity(decl); break;
176
- case "CapabilityDecl": this.checkCapability(decl); break;
177
- case "ChannelDecl": this.checkChannel(decl); break;
178
- case "FlowDecl": this.checkFlow(decl); break;
179
- case "ConstraintDecl": this.checkConstraint(decl); break;
180
- case "ExtensionPointDecl": this.checkExtensionPoint(decl); break;
181
- }
182
- }
183
-
184
- // ─── Entity Checking ──────────────────────────────────────────────────────
185
-
186
- private checkEntity(decl: AST.EntityDeclNode) {
187
- // Check for duplicate field names
188
- const seen = new Set<string>();
189
- for (const field of decl.owns) {
190
- if (seen.has(field.name)) {
191
- this.addError("T009", `Duplicate field name '${field.name}' in entity '${decl.name}'`, field.loc);
192
- }
193
- seen.add(field.name);
194
- }
195
-
196
- // Check field types resolve
197
- for (const field of decl.owns) {
198
- const resolved = this.resolveTypeExpr(field.type);
199
- if (!resolved) {
200
- this.addError("T001", `Undefined type in field '${field.name}'`, field.loc);
201
- }
202
- }
203
-
204
- // Check constraints type to bool
205
- const entitySym = this.symbols.entities.get(decl.name);
206
- if (entitySym) {
207
- const ctx = new TypeContext(entitySym.type.fields, this.symbols);
208
- for (const constraint of decl.constraints) {
209
- const ctype = this.inferExprType(constraint, ctx);
210
- if (ctype && ctype.tag !== "primitive") {
211
- this.addError("T005", `Constraint expression must type to bool, got ${typeToString(ctype)}`, constraint.loc);
212
- } else if (ctype && ctype.tag === "primitive" && ctype.name !== "bool") {
213
- // Allow — many constraints are comparison exprs that return bool
214
- // The expression inferrer returns bool for comparisons
215
- }
216
- }
217
- }
218
-
219
- // Check state machine
220
- if (decl.states) {
221
- const stateNames = new Set(decl.states.nodes.map(n => n.name));
222
- for (const node of decl.states.nodes) {
223
- for (const target of node.transitions) {
224
- if (!stateNames.has(target)) {
225
- this.addError("T010", `Undefined state '${target}' in transition from '${node.name}'`, decl.states.loc);
226
- }
227
- }
228
- for (const target of node.branches) {
229
- if (!stateNames.has(target)) {
230
- this.addError("T010", `Undefined state '${target}' in branch from '${node.name}'`, decl.states.loc);
231
- }
232
- }
233
- }
234
- }
235
- }
236
-
237
- // ─── Capability Checking ───────────────────────────────────────────────────
238
-
239
- private checkCapability(decl: AST.CapabilityDeclNode) {
240
- // Build typing context from parameters
241
- const fields = new Map<string, CVType>();
242
- for (const p of decl.params) {
243
- const resolved = this.resolveTypeExpr(p.type);
244
- if (resolved) {
245
- fields.set(p.name, resolved);
246
- } else {
247
- this.addError("T006", `Parameter '${p.name}' references undeclared type`, p.loc);
248
- }
249
- }
250
- const ctx = new TypeContext(fields, this.symbols);
251
-
252
- // Check requires clauses type to bool
253
- for (const req of decl.requires) {
254
- const rtype = this.inferExprType(req, ctx);
255
- if (rtype && !this.isBoolish(rtype)) {
256
- this.addError("T005", `Requires expression must type to bool, got ${typeToString(rtype)}`, req.loc);
257
- }
258
- }
259
-
260
- // Check effects are well-typed
261
- for (const effect of decl.effects) {
262
- this.checkEffect(effect, ctx);
263
- }
264
-
265
- // Check emitted events exist
266
- for (const emit of decl.emits) {
267
- if (!this.symbols.events.has(emit.eventName)) {
268
- this.addError("T011", `Emitted event '${emit.eventName}' is not declared`, emit.loc);
269
- }
270
- }
271
- }
272
-
273
- private checkEffect(effect: AST.EffectNode, ctx: TypeContext) {
274
- const targetType = this.inferExprType(effect.target, ctx);
275
- const valueType = this.inferExprType(effect.value, ctx);
276
-
277
- if (!targetType || !valueType) return; // already errored
278
-
279
- switch (effect.op) {
280
- case "=":
281
- if (!typeEquals(targetType, valueType) && !this.isAssignable(valueType, targetType)) {
282
- this.addError("T003",
283
- `Type mismatch in assignment: target is ${typeToString(targetType)}, value is ${typeToString(valueType)}`,
284
- effect.loc);
285
- }
286
- break;
287
- case "+=":
288
- // target must be set<T> or numeric, value must be T or numeric
289
- if (targetType.tag === "generic" && targetType.name === "set") {
290
- if (!typeEquals(targetType.args[0], valueType)) {
291
- this.addError("T008",
292
- `Set += requires element type ${typeToString(targetType.args[0])}, got ${typeToString(valueType)}`,
293
- effect.loc);
294
- }
295
- } else if (isNumeric(targetType)) {
296
- if (!isNumeric(valueType)) {
297
- this.addError("T003", `Numeric += requires numeric value, got ${typeToString(valueType)}`, effect.loc);
298
- }
299
- } else {
300
- this.addError("T008", `+= requires set or numeric target, got ${typeToString(targetType)}`, effect.loc);
301
- }
302
- break;
303
- case "-=":
304
- if (targetType.tag === "generic" && targetType.name === "set") {
305
- if (!typeEquals(targetType.args[0], valueType)) {
306
- this.addError("T008",
307
- `Set -= requires element type ${typeToString(targetType.args[0])}, got ${typeToString(valueType)}`,
308
- effect.loc);
309
- }
310
- } else if (isNumeric(targetType)) {
311
- if (!isNumeric(valueType)) {
312
- this.addError("T003", `Numeric -= requires numeric value, got ${typeToString(valueType)}`, effect.loc);
313
- }
314
- } else {
315
- this.addError("T008", `-= requires set or numeric target, got ${typeToString(targetType)}`, effect.loc);
316
- }
317
- break;
318
- }
319
- }
320
-
321
- // ─── Channel Checking ──────────────────────────────────────────────────────
322
-
323
- private checkChannel(decl: AST.ChannelDeclNode) {
324
- // Verify participants type is set<Entity>
325
- if (decl.participants) {
326
- const ptype = this.resolveTypeExpr(decl.participants);
327
- if (ptype && ptype.tag === "generic" && ptype.name === "set") {
328
- const inner = ptype.args[0];
329
- if (inner.tag === "record" && !this.symbols.entities.has(inner.name)) {
330
- this.addError("T001", `Channel participants reference undeclared entity '${inner.name}'`, decl.loc);
331
- }
332
- }
333
- }
334
- }
335
-
336
- // ─── Flow Checking ─────────────────────────────────────────────────────────
337
-
338
- private checkFlow(decl: AST.FlowDeclNode) {
339
- // Check at least 2 steps (ontology requirement)
340
- if (decl.steps.length < 2) {
341
- this.addError("T012", `Flow '${decl.name}' must have at least 2 steps`, decl.loc);
342
- }
343
-
344
- for (const step of decl.steps) {
345
- // Flow steps may call external service endpoints (not just local capabilities).
346
- // Only error if the name collides with a declared entity name, which would be
347
- // a definite mistake. Undeclared names are treated as external HTTP calls.
348
- if (this.symbols.entities.has(step.action.name)) {
349
- this.addError("T013",
350
- `Flow '${decl.name}' step '${step.name}' uses entity name '${step.action.name}' as a call — did you mean a capability?`,
351
- decl.loc);
352
- }
353
- if (step.compensate && this.symbols.entities.has(step.compensate.name)) {
354
- this.addError("T013",
355
- `Flow '${decl.name}' step '${step.name}' compensation uses entity name '${step.compensate.name}' as a call — did you mean a capability?`,
356
- decl.loc);
357
- }
358
- }
359
- }
360
-
361
- // ─── Constraint Checking ───────────────────────────────────────────────────
362
-
363
- private checkConstraint(decl: AST.ConstraintDeclNode) {
364
- // Top-level constraints are checked in a global context
365
- const globalCtx = new TypeContext(new Map(), this.symbols);
366
- const ctype = this.inferExprType(decl.expr, globalCtx);
367
- if (ctype && !this.isBoolish(ctype)) {
368
- this.addError("T005", `Top-level constraint '${decl.name}' must type to bool`, decl.loc);
369
- }
370
- }
371
-
372
- // ─── Expression Type Inference ─────────────────────────────────────────────
373
-
374
- private inferExprType(expr: AST.ExprNode, ctx: TypeContext): CVType | null {
375
- switch (expr.kind) {
376
- case "Literal":
377
- return this.inferLiteralType(expr);
378
- case "FieldRef":
379
- return this.inferFieldRefType(expr, ctx);
380
- case "BinaryExpr":
381
- return this.inferBinaryType(expr, ctx);
382
- case "UnaryExpr":
383
- return this.inferUnaryType(expr, ctx);
384
- case "CallExpr":
385
- return this.inferCallType(expr, ctx);
386
- case "TernaryExpr":
387
- return this.inferTernaryType(expr, ctx);
388
- default:
389
- return null;
390
- }
391
- }
392
-
393
- private inferLiteralType(expr: AST.LiteralNode): CVType {
394
- switch (expr.type) {
395
- case "string": return prim("string");
396
- case "int": return prim("uint"); // default to uint per spec
397
- case "float": return prim("float");
398
- case "bool": return prim("bool");
399
- case "none": return BOTTOM;
400
- case "list": return generic("list", prim("json")); // infer element type later
401
- case "map": return prim("json");
402
- }
403
- }
404
-
405
- private inferFieldRefType(expr: AST.FieldRefNode, ctx: TypeContext): CVType | null {
406
- const path = expr.path;
407
- if (path.length === 0) return null;
408
-
409
- // First segment: look up in context
410
- let currentType = ctx.lookup(path[0]);
411
-
412
- if (!currentType) {
413
- // Try as entity name (for top-level constraints like Player.active_trades)
414
- const entity = this.symbols.entities.get(path[0]);
415
- if (entity) {
416
- currentType = entity.type;
417
- return this.resolveFieldPath(currentType, path.slice(1), expr);
418
- }
419
- // Unknown — don't error here, could be a forward reference
420
- return prim("json"); // permissive fallback
421
- }
422
-
423
- return this.resolveFieldPath(currentType, path.slice(1), expr);
424
- }
425
-
426
- private resolveFieldPath(baseType: CVType, remaining: string[], expr: AST.ExprNode): CVType | null {
427
- let current = baseType;
428
-
429
- for (const segment of remaining) {
430
- // Handle .unique, .length, .size as derived properties
431
- if (segment === "unique") return prim("bool");
432
- if (segment === "length") return prim("uint");
433
- if (segment === "size") return prim("uint");
434
-
435
- if (current.tag === "record") {
436
- const field = current.fields.get(segment);
437
- if (field) {
438
- current = field;
439
- } else {
440
- // Field not found — could be a derived property
441
- return prim("json"); // permissive
442
- }
443
- } else if (current.tag === "generic") {
444
- // Accessing property on generic type (e.g., list.size)
445
- if (segment === "size" || segment === "length") return prim("uint");
446
- return prim("json");
447
- } else {
448
- return prim("json"); // permissive fallback
449
- }
450
- }
451
-
452
- return current;
453
- }
454
-
455
- private inferBinaryType(expr: AST.BinaryExprNode, ctx: TypeContext): CVType {
456
- const left = this.inferExprType(expr.left, ctx);
457
- const right = this.inferExprType(expr.right, ctx);
458
-
459
- switch (expr.op) {
460
- // Comparison operators → bool
461
- case "==": case "!=": case "<": case ">": case "<=": case ">=":
462
- case "in": case "contains": case "and": case "or":
463
- return prim("bool");
464
-
465
- // Range operator
466
- case "..":
467
- return generic("list", prim("uint")); // range produces a range object
468
-
469
- // Arithmetic → numeric
470
- case "+": case "*": case "/": case "%":
471
- if (left && right && isNumeric(left) && isNumeric(right)) {
472
- // Promote to widest type
473
- if ((left as PrimitiveType).name === "float" || (right as PrimitiveType).name === "float") return prim("float");
474
- if ((left as PrimitiveType).name === "int" || (right as PrimitiveType).name === "int") return prim("int");
475
- return prim("uint");
476
- }
477
- if (expr.op === "+" && left?.tag === "primitive" && (left as PrimitiveType).name === "string") {
478
- return prim("string"); // string concatenation
479
- }
480
- return prim("uint");
481
-
482
- case "-":
483
- return prim("int"); // subtraction may produce negative
484
-
485
- default:
486
- return prim("bool");
487
- }
488
- }
489
-
490
- private inferUnaryType(expr: AST.UnaryExprNode, ctx: TypeContext): CVType {
491
- if (expr.op === "not") return prim("bool");
492
- if (expr.op === "-") return prim("int");
493
- return prim("json");
494
- }
495
-
496
- private inferCallType(expr: AST.CallExprNode, ctx: TypeContext): CVType {
497
- // Built-in functions
498
- if (expr.name === "now") return prim("timestamp");
499
- if (expr.name === "count") return prim("uint");
500
- if (expr.name === "sum") return prim("uint");
501
- if (expr.name === "min" || expr.name === "max") return prim("uint");
502
- if (expr.name === "abs") return prim("uint");
503
- if (expr.name === "floor" || expr.name === "ceil" || expr.name === "round") return prim("int");
504
- if (expr.name === "len" || expr.name === "size") return prim("uint");
505
- if (expr.name === "contains" || expr.name === "starts_with" || expr.name === "ends_with") return prim("bool");
506
- if (expr.name === "to_string") return prim("string");
507
- if (expr.name === "to_int" || expr.name === "to_uint") return prim("int");
508
- if (expr.name === "to_float") return prim("float");
509
-
510
- // Check if it's a declared capability — use json as safe fallback for its return
511
- if (this.symbols.capabilities.has(expr.name)) {
512
- return prim("json");
513
- }
514
-
515
- // Unknown user-defined function — permissive fallback
516
- return prim("json");
517
- }
518
-
519
- private inferTernaryType(expr: AST.TernaryExprNode, ctx: TypeContext): CVType | null {
520
- // condition must be bool
521
- const condType = this.inferExprType(expr.condition, ctx);
522
- if (condType && !this.isBoolish(condType)) {
523
- this.addError("T005", "Ternary condition must be bool", expr.loc);
524
- }
525
- // result type is type of consequent (assume both branches same type)
526
- return this.inferExprType(expr.consequent, ctx);
527
- }
528
-
529
- // ─── Helpers ───────────────────────────────────────────────────────────────
530
-
531
- private resolveTypeExpr(typeExpr: AST.TypeExprNode): CVType | null {
532
- switch (typeExpr.kind) {
533
- case "PrimitiveType":
534
- return prim(typeExpr.name as PrimitiveType["name"]);
535
- case "GenericType": {
536
- const args = typeExpr.typeArgs.map(a => this.resolveTypeExpr(a)).filter(Boolean) as CVType[];
537
- return generic(typeExpr.name as GenericType["name"], ...args);
538
- }
539
- case "EntityRefType": {
540
- const entity = this.symbols.entities.get(typeExpr.name);
541
- if (entity) return entity.type;
542
- // Could be a forward reference — register as unknown record
543
- return record(typeExpr.name, new Map());
544
- }
545
- case "TupleType": {
546
- const elements = typeExpr.elements.map(e => this.resolveTypeExpr(e)).filter(Boolean) as CVType[];
547
- return { tag: "tuple", elements };
548
- }
549
- case "UnionType": {
550
- const members = typeExpr.members.map(m => this.resolveTypeExpr(m)).filter(Boolean) as CVType[];
551
- return { tag: "union", members };
552
- }
553
- }
554
- }
555
-
556
- private isBoolish(t: CVType): boolean {
557
- return (t.tag === "primitive" && t.name === "bool") || t.tag === "bottom";
558
- }
559
-
560
- private isAssignable(source: CVType, target: CVType): boolean {
561
- if (typeEquals(source, target)) return true;
562
- // Numeric widening: uint → int → float
563
- if (source.tag === "primitive" && target.tag === "primitive") {
564
- if (source.name === "uint" && (target.name === "int" || target.name === "float")) return true;
565
- if (source.name === "int" && target.name === "float") return true;
566
- }
567
- // json accepts anything and anything accepts json (permissive for unresolved)
568
- if (target.tag === "primitive" && target.name === "json") return true;
569
- if (source.tag === "primitive" && source.name === "json") return true;
570
- return false;
571
- }
572
- private checkExtensionPoint(decl: AST.ExtensionPointDeclNode): void {
573
- for (const p of decl.params) {
574
- const resolved = this.resolveTypeExpr(p.type);
575
- if (!resolved) {
576
- this.addError("T001", "Extension point '" + decl.name + "' param '" + p.name + "' references undefined type", p.loc);
577
- }
578
- }
579
- if (decl.returns) {
580
- const resolved = this.resolveTypeExpr(decl.returns);
581
- if (!resolved) {
582
- this.addError("T001", "Extension point '" + decl.name + "' return type is undefined", decl.loc);
583
- }
584
- }
585
- }
586
-
587
-
588
- }
589
- // ─── Type Context ────────────────────────────────────────────────────────────
590
-
591
- class TypeContext {
592
- private locals: Map<string, CVType>;
593
- private symbols: SymbolTable;
594
-
595
- constructor(locals: Map<string, CVType>, symbols: SymbolTable) {
596
- this.locals = locals;
597
- this.symbols = symbols;
598
- }
599
- lookup(name: string): CVType | null {
600
- const local = this.locals.get(name);
601
- if (local) return local;
602
- const entity = this.symbols.entities.get(name);
603
- if (entity) return entity.type;
604
- return null;
605
- }
606
- }
1
+ /**
2
+ * BoneScript Type Checker — Stage 3 of the compilation pipeline.
3
+ * Implements spec/04_TYPE_SYSTEM.md.
4
+ *
5
+ * Responsibilities:
6
+ * 1. Build symbol table from entity declarations
7
+ * 2. Verify all field types resolve to valid types
8
+ * 3. Verify all constraint expressions type to bool
9
+ * 4. Verify capability preconditions type to bool
10
+ * 5. Verify effects are well-typed (target and value match)
11
+ * 6. Verify emitted events exist
12
+ * 7. Verify state machine transitions reference valid states
13
+ * 8. Verify flow steps reference valid capabilities
14
+ *
15
+ * Deterministic: same AST always produces same errors in same order.
16
+ */
17
+
18
+ import * as AST from "./ast";
19
+ import {
20
+ CVType, PrimitiveType, GenericType, RecordType,
21
+ prim, generic, record, BOTTOM,
22
+ typeEquals, typeToString, isNumeric, isComparable,
23
+ } from "./types";
24
+
25
+ // ─── Type Error ──────────────────────────────────────────────────────────────
26
+
27
+ export interface TypeError {
28
+ code: string;
29
+ message: string;
30
+ loc: AST.ASTNode["loc"];
31
+ }
32
+
33
+ // ─── Symbol Table ────────────────────────────────────────────────────────────
34
+
35
+ interface EntitySymbol {
36
+ name: string;
37
+ type: RecordType;
38
+ states: string[];
39
+ capabilities: string[];
40
+ }
41
+
42
+ interface CapabilitySymbol {
43
+ name: string;
44
+ params: Map<string, CVType>;
45
+ }
46
+
47
+ interface EventSymbol {
48
+ name: string;
49
+ payloadFields: Map<string, CVType>;
50
+ }
51
+
52
+ interface SymbolTable {
53
+ entities: Map<string, EntitySymbol>;
54
+ capabilities: Map<string, CapabilitySymbol>;
55
+ events: Map<string, EventSymbol>;
56
+ stores: Set<string>;
57
+ channels: Set<string>;
58
+ flows: Set<string>;
59
+ }
60
+
61
+ // ─── Type Checker ────────────────────────────────────────────────────────────
62
+
63
+ export class TypeChecker {
64
+ private errors: TypeError[] = [];
65
+ private symbols: SymbolTable = {
66
+ entities: new Map(),
67
+ capabilities: new Map(),
68
+ events: new Map(),
69
+ stores: new Set(),
70
+ channels: new Set(),
71
+ flows: new Set(),
72
+ };
73
+
74
+ check(program: AST.ProgramNode): TypeError[] {
75
+ this.errors = [];
76
+
77
+ for (const system of program.systems) {
78
+ this.checkSystem(system);
79
+ }
80
+
81
+ return this.errors;
82
+ }
83
+
84
+ private addError(code: string, message: string, loc: AST.ASTNode["loc"]) {
85
+ this.errors.push({ code, message, loc });
86
+ }
87
+
88
+ // ─── Phase 1: Build Symbol Table ──────────────────────────────────────────
89
+
90
+ private checkSystem(system: AST.SystemDeclNode) {
91
+ // First pass: register all declarations
92
+ for (const decl of system.declarations) {
93
+ this.registerDeclaration(decl);
94
+ }
95
+
96
+ // Second pass: type check all declarations
97
+ for (const decl of system.declarations) {
98
+ this.checkDeclaration(decl);
99
+ }
100
+ }
101
+
102
+ private registerDeclaration(decl: AST.DeclarationNode) {
103
+ switch (decl.kind) {
104
+ case "EntityDecl":
105
+ this.registerEntity(decl);
106
+ break;
107
+ case "CapabilityDecl":
108
+ this.registerCapability(decl);
109
+ break;
110
+ case "EventDecl":
111
+ this.registerEvent(decl);
112
+ break;
113
+ case "StoreDecl":
114
+ this.symbols.stores.add(decl.name);
115
+ break;
116
+ case "ChannelDecl":
117
+ this.symbols.channels.add(decl.name);
118
+ break;
119
+ case "FlowDecl":
120
+ this.symbols.flows.add(decl.name);
121
+ break;
122
+ }
123
+ }
124
+
125
+ private registerEntity(decl: AST.EntityDeclNode) {
126
+ const fields = new Map<string, CVType>();
127
+
128
+ // Ontology-entailed fields (always present)
129
+ fields.set("id", prim("uuid"));
130
+ fields.set("created_at", prim("timestamp"));
131
+ fields.set("updated_at", prim("timestamp"));
132
+
133
+ // Declared fields
134
+ for (const field of decl.owns) {
135
+ const resolved = this.resolveTypeExpr(field.type);
136
+ if (resolved) {
137
+ fields.set(field.name, resolved);
138
+ }
139
+ }
140
+
141
+ const states = decl.states
142
+ ? decl.states.nodes.map(n => n.name)
143
+ : [];
144
+
145
+ this.symbols.entities.set(decl.name, {
146
+ name: decl.name,
147
+ type: record(decl.name, fields),
148
+ states,
149
+ capabilities: [],
150
+ });
151
+ }
152
+
153
+ private registerCapability(decl: AST.CapabilityDeclNode) {
154
+ const params = new Map<string, CVType>();
155
+ for (const p of decl.params) {
156
+ const resolved = this.resolveTypeExpr(p.type);
157
+ if (resolved) params.set(p.name, resolved);
158
+ }
159
+ this.symbols.capabilities.set(decl.name, { name: decl.name, params });
160
+ }
161
+
162
+ private registerEvent(decl: AST.EventDeclNode) {
163
+ const fields = new Map<string, CVType>();
164
+ for (const f of decl.payload) {
165
+ const resolved = this.resolveTypeExpr(f.type);
166
+ if (resolved) fields.set(f.name, resolved);
167
+ }
168
+ this.symbols.events.set(decl.name, { name: decl.name, payloadFields: fields });
169
+ }
170
+
171
+ // ─── Phase 2: Type Check Declarations ─────────────────────────────────────
172
+
173
+ private checkDeclaration(decl: AST.DeclarationNode) {
174
+ switch (decl.kind) {
175
+ case "EntityDecl": this.checkEntity(decl); break;
176
+ case "CapabilityDecl": this.checkCapability(decl); break;
177
+ case "ChannelDecl": this.checkChannel(decl); break;
178
+ case "FlowDecl": this.checkFlow(decl); break;
179
+ case "ConstraintDecl": this.checkConstraint(decl); break;
180
+ case "ExtensionPointDecl": this.checkExtensionPoint(decl); break;
181
+ case "StoreDecl": this.checkStore(decl); break;
182
+ case "PolicyDecl": this.checkPolicy(decl); break;
183
+ }
184
+ }
185
+
186
+ // ─── Entity Checking ──────────────────────────────────────────────────────
187
+
188
+ private checkEntity(decl: AST.EntityDeclNode) {
189
+ // Check for duplicate field names
190
+ const seen = new Set<string>();
191
+ for (const field of decl.owns) {
192
+ if (seen.has(field.name)) {
193
+ this.addError("T009", `Duplicate field name '${field.name}' in entity '${decl.name}'`, field.loc);
194
+ }
195
+ seen.add(field.name);
196
+ }
197
+
198
+ // Check field types resolve
199
+ for (const field of decl.owns) {
200
+ const resolved = this.resolveTypeExpr(field.type);
201
+ if (!resolved) {
202
+ this.addError("T001", `Undefined type in field '${field.name}'`, field.loc);
203
+ }
204
+ }
205
+
206
+ // Check constraints type to bool
207
+ const entitySym = this.symbols.entities.get(decl.name);
208
+ if (entitySym) {
209
+ const ctx = new TypeContext(entitySym.type.fields, this.symbols);
210
+ for (const constraint of decl.constraints) {
211
+ const ctype = this.inferExprType(constraint, ctx);
212
+ if (ctype && ctype.tag !== "primitive") {
213
+ this.addError("T005", `Constraint expression must type to bool, got ${typeToString(ctype)}`, constraint.loc);
214
+ } else if (ctype && ctype.tag === "primitive" && ctype.name !== "bool") {
215
+ // Allow — many constraints are comparison exprs that return bool
216
+ // The expression inferrer returns bool for comparisons
217
+ }
218
+ }
219
+ }
220
+
221
+ // Check state machine
222
+ if (decl.states) {
223
+ const stateNames = new Set(decl.states.nodes.map(n => n.name));
224
+ for (const node of decl.states.nodes) {
225
+ for (const target of node.transitions) {
226
+ if (!stateNames.has(target)) {
227
+ this.addError("T010", `Undefined state '${target}' in transition from '${node.name}'`, decl.states.loc);
228
+ }
229
+ }
230
+ for (const target of node.branches) {
231
+ if (!stateNames.has(target)) {
232
+ this.addError("T010", `Undefined state '${target}' in branch from '${node.name}'`, decl.states.loc);
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ // ─── Capability Checking ───────────────────────────────────────────────────
240
+
241
+ private checkCapability(decl: AST.CapabilityDeclNode) {
242
+ // Build typing context from parameters
243
+ const fields = new Map<string, CVType>();
244
+ for (const p of decl.params) {
245
+ const resolved = this.resolveTypeExpr(p.type);
246
+ if (resolved) {
247
+ fields.set(p.name, resolved);
248
+ } else {
249
+ this.addError("T006", `Parameter '${p.name}' references undeclared type`, p.loc);
250
+ }
251
+ }
252
+ const ctx = new TypeContext(fields, this.symbols);
253
+
254
+ // Check requires clauses type to bool
255
+ for (const req of decl.requires) {
256
+ const rtype = this.inferExprType(req, ctx);
257
+ if (rtype && !this.isBoolish(rtype)) {
258
+ this.addError("T005", `Requires expression must type to bool, got ${typeToString(rtype)}`, req.loc);
259
+ }
260
+ }
261
+
262
+ // Check effects are well-typed
263
+ for (const effect of decl.effects) {
264
+ this.checkEffect(effect, ctx);
265
+ }
266
+
267
+ // Check emitted events exist
268
+ for (const emit of decl.emits) {
269
+ if (!this.symbols.events.has(emit.eventName)) {
270
+ this.addError("T011", `Emitted event '${emit.eventName}' is not declared`, emit.loc);
271
+ }
272
+ }
273
+ }
274
+
275
+ private checkEffect(effect: AST.EffectNode, ctx: TypeContext) {
276
+ const targetType = this.inferExprType(effect.target, ctx);
277
+ const valueType = this.inferExprType(effect.value, ctx);
278
+
279
+ if (!targetType || !valueType) return; // already errored
280
+
281
+ switch (effect.op) {
282
+ case "=":
283
+ if (!typeEquals(targetType, valueType) && !this.isAssignable(valueType, targetType)) {
284
+ this.addError("T003",
285
+ `Type mismatch in assignment: target is ${typeToString(targetType)}, value is ${typeToString(valueType)}`,
286
+ effect.loc);
287
+ }
288
+ break;
289
+ case "+=":
290
+ // target must be set<T> or numeric, value must be T or numeric
291
+ if (targetType.tag === "generic" && targetType.name === "set") {
292
+ if (!typeEquals(targetType.args[0], valueType)) {
293
+ this.addError("T008",
294
+ `Set += requires element type ${typeToString(targetType.args[0])}, got ${typeToString(valueType)}`,
295
+ effect.loc);
296
+ }
297
+ } else if (isNumeric(targetType)) {
298
+ if (!isNumeric(valueType)) {
299
+ this.addError("T003", `Numeric += requires numeric value, got ${typeToString(valueType)}`, effect.loc);
300
+ }
301
+ } else {
302
+ this.addError("T008", `+= requires set or numeric target, got ${typeToString(targetType)}`, effect.loc);
303
+ }
304
+ break;
305
+ case "-=":
306
+ if (targetType.tag === "generic" && targetType.name === "set") {
307
+ if (!typeEquals(targetType.args[0], valueType)) {
308
+ this.addError("T008",
309
+ `Set -= requires element type ${typeToString(targetType.args[0])}, got ${typeToString(valueType)}`,
310
+ effect.loc);
311
+ }
312
+ } else if (isNumeric(targetType)) {
313
+ if (!isNumeric(valueType)) {
314
+ this.addError("T003", `Numeric -= requires numeric value, got ${typeToString(valueType)}`, effect.loc);
315
+ }
316
+ } else {
317
+ this.addError("T008", `-= requires set or numeric target, got ${typeToString(targetType)}`, effect.loc);
318
+ }
319
+ break;
320
+ }
321
+ }
322
+
323
+ // ─── Channel Checking ──────────────────────────────────────────────────────
324
+
325
+ private checkChannel(decl: AST.ChannelDeclNode) {
326
+ // Verify participants type is set<Entity>
327
+ if (decl.participants) {
328
+ const ptype = this.resolveTypeExpr(decl.participants);
329
+ if (ptype && ptype.tag === "generic" && ptype.name === "set") {
330
+ const inner = ptype.args[0];
331
+ if (inner.tag === "record" && !this.symbols.entities.has(inner.name)) {
332
+ this.addError("T001", `Channel participants reference undeclared entity '${inner.name}'`, decl.loc);
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ // ─── Flow Checking ─────────────────────────────────────────────────────────
339
+
340
+ private checkFlow(decl: AST.FlowDeclNode) {
341
+ // Check at least 2 steps (ontology requirement)
342
+ if (decl.steps.length < 2) {
343
+ this.addError("T012", `Flow '${decl.name}' must have at least 2 steps`, decl.loc);
344
+ }
345
+
346
+ for (const step of decl.steps) {
347
+ // Flow steps may call external service endpoints (not just local capabilities).
348
+ // Only error if the name collides with a declared entity name, which would be
349
+ // a definite mistake. Undeclared names are treated as external HTTP calls.
350
+ if (this.symbols.entities.has(step.action.name)) {
351
+ this.addError("T013",
352
+ `Flow '${decl.name}' step '${step.name}' uses entity name '${step.action.name}' as a call — did you mean a capability?`,
353
+ decl.loc);
354
+ }
355
+ if (step.compensate && this.symbols.entities.has(step.compensate.name)) {
356
+ this.addError("T013",
357
+ `Flow '${decl.name}' step '${step.name}' compensation uses entity name '${step.compensate.name}' as a call — did you mean a capability?`,
358
+ decl.loc);
359
+ }
360
+ }
361
+ }
362
+
363
+ // ─── Constraint Checking ───────────────────────────────────────────────────
364
+
365
+ private checkConstraint(decl: AST.ConstraintDeclNode) {
366
+ // Top-level constraints are checked in a global context
367
+ const globalCtx = new TypeContext(new Map(), this.symbols);
368
+ const ctype = this.inferExprType(decl.expr, globalCtx);
369
+ if (ctype && !this.isBoolish(ctype)) {
370
+ this.addError("T005", `Top-level constraint '${decl.name}' must type to bool`, decl.loc);
371
+ }
372
+ }
373
+
374
+ // ─── Expression Type Inference ─────────────────────────────────────────────
375
+
376
+ private inferExprType(expr: AST.ExprNode, ctx: TypeContext): CVType | null {
377
+ switch (expr.kind) {
378
+ case "Literal":
379
+ return this.inferLiteralType(expr);
380
+ case "FieldRef":
381
+ return this.inferFieldRefType(expr, ctx);
382
+ case "BinaryExpr":
383
+ return this.inferBinaryType(expr, ctx);
384
+ case "UnaryExpr":
385
+ return this.inferUnaryType(expr, ctx);
386
+ case "CallExpr":
387
+ return this.inferCallType(expr, ctx);
388
+ case "TernaryExpr":
389
+ return this.inferTernaryType(expr, ctx);
390
+ default:
391
+ return null;
392
+ }
393
+ }
394
+
395
+ private inferLiteralType(expr: AST.LiteralNode): CVType {
396
+ switch (expr.type) {
397
+ case "string": return prim("string");
398
+ case "int": return prim("uint"); // default to uint per spec
399
+ case "float": return prim("float");
400
+ case "bool": return prim("bool");
401
+ case "none": return BOTTOM;
402
+ case "list": return generic("list", prim("json")); // infer element type later
403
+ case "map": return prim("json");
404
+ }
405
+ }
406
+
407
+ private inferFieldRefType(expr: AST.FieldRefNode, ctx: TypeContext): CVType | null {
408
+ const path = expr.path;
409
+ if (path.length === 0) return null;
410
+
411
+ // First segment: look up in context
412
+ let currentType = ctx.lookup(path[0]);
413
+
414
+ if (!currentType) {
415
+ // Try as entity name (for top-level constraints like Player.active_trades)
416
+ const entity = this.symbols.entities.get(path[0]);
417
+ if (entity) {
418
+ currentType = entity.type;
419
+ return this.resolveFieldPath(currentType, path.slice(1), expr);
420
+ }
421
+ // Unknown — don't error here, could be a forward reference
422
+ return prim("json"); // permissive fallback
423
+ }
424
+
425
+ return this.resolveFieldPath(currentType, path.slice(1), expr);
426
+ }
427
+
428
+ private resolveFieldPath(baseType: CVType, remaining: string[], expr: AST.ExprNode): CVType | null {
429
+ let current = baseType;
430
+
431
+ for (const segment of remaining) {
432
+ // Handle .unique, .length, .size as derived properties
433
+ if (segment === "unique") return prim("bool");
434
+ if (segment === "length") return prim("uint");
435
+ if (segment === "size") return prim("uint");
436
+
437
+ if (current.tag === "record") {
438
+ const field = current.fields.get(segment);
439
+ if (field) {
440
+ current = field;
441
+ } else {
442
+ // Field not found — could be a derived property
443
+ return prim("json"); // permissive
444
+ }
445
+ } else if (current.tag === "generic") {
446
+ // Accessing property on generic type (e.g., list.size)
447
+ if (segment === "size" || segment === "length") return prim("uint");
448
+ return prim("json");
449
+ } else {
450
+ return prim("json"); // permissive fallback
451
+ }
452
+ }
453
+
454
+ return current;
455
+ }
456
+
457
+ private inferBinaryType(expr: AST.BinaryExprNode, ctx: TypeContext): CVType {
458
+ const left = this.inferExprType(expr.left, ctx);
459
+ const right = this.inferExprType(expr.right, ctx);
460
+
461
+ switch (expr.op) {
462
+ // Comparison operators → bool
463
+ case "==": case "!=": case "<": case ">": case "<=": case ">=":
464
+ case "in": case "contains": case "and": case "or":
465
+ return prim("bool");
466
+
467
+ // Range operator
468
+ case "..":
469
+ return generic("list", prim("uint")); // range produces a range object
470
+
471
+ // Arithmetic → numeric
472
+ case "+": case "*": case "/": case "%":
473
+ if (left && right && isNumeric(left) && isNumeric(right)) {
474
+ // Promote to widest type
475
+ if ((left as PrimitiveType).name === "float" || (right as PrimitiveType).name === "float") return prim("float");
476
+ if ((left as PrimitiveType).name === "int" || (right as PrimitiveType).name === "int") return prim("int");
477
+ return prim("uint");
478
+ }
479
+ if (expr.op === "+" && left?.tag === "primitive" && (left as PrimitiveType).name === "string") {
480
+ return prim("string"); // string concatenation
481
+ }
482
+ return prim("uint");
483
+
484
+ case "-":
485
+ return prim("int"); // subtraction may produce negative
486
+
487
+ default:
488
+ return prim("bool");
489
+ }
490
+ }
491
+
492
+ private inferUnaryType(expr: AST.UnaryExprNode, ctx: TypeContext): CVType {
493
+ if (expr.op === "not") return prim("bool");
494
+ if (expr.op === "-") return prim("int");
495
+ return prim("json");
496
+ }
497
+
498
+ private inferCallType(expr: AST.CallExprNode, ctx: TypeContext): CVType {
499
+ // Built-in functions
500
+ if (expr.name === "now") return prim("timestamp");
501
+ if (expr.name === "count") return prim("uint");
502
+ if (expr.name === "sum") return prim("uint");
503
+ if (expr.name === "min" || expr.name === "max") return prim("uint");
504
+ if (expr.name === "abs") return prim("uint");
505
+ if (expr.name === "floor" || expr.name === "ceil" || expr.name === "round") return prim("int");
506
+ if (expr.name === "len" || expr.name === "size") return prim("uint");
507
+ if (expr.name === "contains" || expr.name === "starts_with" || expr.name === "ends_with") return prim("bool");
508
+ if (expr.name === "to_string") return prim("string");
509
+ if (expr.name === "to_int" || expr.name === "to_uint") return prim("int");
510
+ if (expr.name === "to_float") return prim("float");
511
+
512
+ // Check if it's a declared capability — use json as safe fallback for its return
513
+ if (this.symbols.capabilities.has(expr.name)) {
514
+ return prim("json");
515
+ }
516
+
517
+ // Unknown user-defined function — permissive fallback
518
+ return prim("json");
519
+ }
520
+
521
+ private inferTernaryType(expr: AST.TernaryExprNode, ctx: TypeContext): CVType | null {
522
+ // condition must be bool
523
+ const condType = this.inferExprType(expr.condition, ctx);
524
+ if (condType && !this.isBoolish(condType)) {
525
+ this.addError("T005", "Ternary condition must be bool", expr.loc);
526
+ }
527
+ // result type is type of consequent (assume both branches same type)
528
+ return this.inferExprType(expr.consequent, ctx);
529
+ }
530
+
531
+ // ─── Helpers ───────────────────────────────────────────────────────────────
532
+
533
+ private resolveTypeExpr(typeExpr: AST.TypeExprNode): CVType | null {
534
+ switch (typeExpr.kind) {
535
+ case "PrimitiveType":
536
+ return prim(typeExpr.name as PrimitiveType["name"]);
537
+ case "GenericType": {
538
+ const args = typeExpr.typeArgs.map(a => this.resolveTypeExpr(a)).filter(Boolean) as CVType[];
539
+ return generic(typeExpr.name as GenericType["name"], ...args);
540
+ }
541
+ case "EntityRefType": {
542
+ const entity = this.symbols.entities.get(typeExpr.name);
543
+ if (entity) return entity.type;
544
+ // Could be a forward reference — register as unknown record
545
+ return record(typeExpr.name, new Map());
546
+ }
547
+ case "TupleType": {
548
+ const elements = typeExpr.elements.map(e => this.resolveTypeExpr(e)).filter(Boolean) as CVType[];
549
+ return { tag: "tuple", elements };
550
+ }
551
+ case "UnionType": {
552
+ const members = typeExpr.members.map(m => this.resolveTypeExpr(m)).filter(Boolean) as CVType[];
553
+ return { tag: "union", members };
554
+ }
555
+ }
556
+ }
557
+
558
+ private isBoolish(t: CVType): boolean {
559
+ return (t.tag === "primitive" && t.name === "bool") || t.tag === "bottom";
560
+ }
561
+
562
+ private isAssignable(source: CVType, target: CVType): boolean {
563
+ if (typeEquals(source, target)) return true;
564
+ // Numeric widening: uint → int → float
565
+ if (source.tag === "primitive" && target.tag === "primitive") {
566
+ if (source.name === "uint" && (target.name === "int" || target.name === "float")) return true;
567
+ if (source.name === "int" && target.name === "float") return true;
568
+ }
569
+ // json accepts anything and anything accepts json (permissive for unresolved)
570
+ if (target.tag === "primitive" && target.name === "json") return true;
571
+ if (source.tag === "primitive" && source.name === "json") return true;
572
+ return false;
573
+ }
574
+ private checkExtensionPoint(decl: AST.ExtensionPointDeclNode): void {
575
+ for (const p of decl.params) {
576
+ const resolved = this.resolveTypeExpr(p.type);
577
+ if (!resolved) {
578
+ this.addError("T001", "Extension point '" + decl.name + "' param '" + p.name + "' references undefined type", p.loc);
579
+ }
580
+ }
581
+ if (decl.returns) {
582
+ const resolved = this.resolveTypeExpr(decl.returns);
583
+ if (!resolved) {
584
+ this.addError("T001", "Extension point '" + decl.name + "' return type is undefined", decl.loc);
585
+ }
586
+ }
587
+ }
588
+
589
+ // ─── Store Checking ───────────────────────────────────────────────────────────
590
+
591
+ /** Supported database engines. Engines not in this set produce T014. */
592
+ private static readonly SUPPORTED_ENGINES = new Set(["postgresql", "redis"]);
593
+
594
+ private checkStore(decl: AST.StoreDeclNode): void {
595
+ if (decl.engine && !TypeChecker.SUPPORTED_ENGINES.has(decl.engine)) {
596
+ this.addError(
597
+ "T014",
598
+ `Store '${decl.name}' uses unsupported engine '${decl.engine}'. ` +
599
+ `Supported engines: ${[...TypeChecker.SUPPORTED_ENGINES].join(", ")}. ` +
600
+ `Other engines (dynamodb, mongodb, sqlite, s3) are not yet implemented — ` +
601
+ `remove the engine declaration to use the domain default (postgresql), ` +
602
+ `or implement a custom emitter.`,
603
+ decl.loc,
604
+ );
605
+ }
606
+
607
+ // Check schema field types resolve
608
+ for (const field of decl.schema) {
609
+ const resolved = this.resolveTypeExpr(field.type);
610
+ if (!resolved) {
611
+ this.addError("T001", `Store '${decl.name}' field '${field.name}' references undefined type`, field.loc);
612
+ }
613
+ }
614
+ }
615
+
616
+ // ─── Policy Checking ──────────────────────────────────────────────────────────
617
+
618
+ private static readonly VALID_ENCRYPTION = new Set(["at_rest", "in_transit", "both", "none"]);
619
+
620
+ private checkPolicy(decl: AST.PolicyDeclNode): void {
621
+ if (decl.encryption && !TypeChecker.VALID_ENCRYPTION.has(decl.encryption)) {
622
+ this.addError(
623
+ "T015",
624
+ `Policy '${decl.name}' has invalid encryption value '${decl.encryption}'. ` +
625
+ `Valid values: ${[...TypeChecker.VALID_ENCRYPTION].join(", ")}.`,
626
+ decl.loc,
627
+ );
628
+ }
629
+ if (decl.rateLimit && decl.rateLimit.count <= 0) {
630
+ this.addError(
631
+ "T015",
632
+ `Policy '${decl.name}' rate_limit count must be positive (> 0), got ${decl.rateLimit.count}.`,
633
+ decl.loc,
634
+ );
635
+ }
636
+ }
637
+
638
+
639
+ }
640
+ // ─── Type Context ────────────────────────────────────────────────────────────
641
+
642
+ class TypeContext {
643
+ private locals: Map<string, CVType>;
644
+ private symbols: SymbolTable;
645
+
646
+ constructor(locals: Map<string, CVType>, symbols: SymbolTable) {
647
+ this.locals = locals;
648
+ this.symbols = symbols;
649
+ }
650
+ lookup(name: string): CVType | null {
651
+ const local = this.locals.get(name);
652
+ if (local) return local;
653
+ const entity = this.symbols.entities.get(name);
654
+ if (entity) return entity.type;
655
+ return null;
656
+ }
657
+ }