bonescript-compiler 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/LICENSE +21 -21
  2. package/dist/algorithm_catalog.js +166 -166
  3. package/dist/cli.d.ts +1 -2
  4. package/dist/cli.js +543 -75
  5. package/dist/cli.js.map +1 -1
  6. package/dist/emit_capability.d.ts +0 -13
  7. package/dist/emit_capability.js +128 -292
  8. package/dist/emit_capability.js.map +1 -1
  9. package/dist/emit_composition.js +3 -37
  10. package/dist/emit_composition.js.map +1 -1
  11. package/dist/emit_deploy.js +162 -162
  12. package/dist/emit_events.d.ts +0 -1
  13. package/dist/emit_events.js +275 -342
  14. package/dist/emit_events.js.map +1 -1
  15. package/dist/emit_full.js +106 -268
  16. package/dist/emit_full.js.map +1 -1
  17. package/dist/emit_maintenance.js +249 -249
  18. package/dist/emit_runtime.d.ts +11 -17
  19. package/dist/emit_runtime.js +688 -29
  20. package/dist/emit_runtime.js.map +1 -1
  21. package/dist/emit_sourcemap.js +66 -66
  22. package/dist/emit_tests.js +0 -37
  23. package/dist/emit_tests.js.map +1 -1
  24. package/dist/emitter.js +16 -82
  25. package/dist/emitter.js.map +1 -1
  26. package/dist/extension_manager.d.ts +2 -2
  27. package/dist/extension_manager.js +3 -6
  28. package/dist/extension_manager.js.map +1 -1
  29. package/dist/ir.d.ts +0 -4
  30. package/dist/lowering.d.ts +14 -5
  31. package/dist/lowering.js +417 -66
  32. package/dist/lowering.js.map +1 -1
  33. package/dist/module_loader.d.ts +2 -2
  34. package/dist/module_loader.js +23 -20
  35. package/dist/module_loader.js.map +1 -1
  36. package/dist/optimizer.js +1 -1
  37. package/dist/optimizer.js.map +1 -1
  38. package/dist/scaffold.d.ts +2 -2
  39. package/dist/scaffold.js +319 -315
  40. package/dist/scaffold.js.map +1 -1
  41. package/dist/source_map.js.map +1 -0
  42. package/dist/test.js.map +1 -0
  43. package/dist/test_typechecker.d.ts +5 -0
  44. package/dist/test_typechecker.js +126 -0
  45. package/dist/test_typechecker.js.map +1 -0
  46. package/dist/typechecker.d.ts +0 -5
  47. package/dist/typechecker.js +13 -68
  48. package/dist/typechecker.js.map +1 -1
  49. package/dist/verifier.d.ts +1 -5
  50. package/dist/verifier.js +35 -140
  51. package/dist/verifier.js.map +1 -1
  52. package/package.json +52 -62
  53. package/src/algorithm_catalog.ts +345 -345
  54. package/src/ast.d.ts +244 -0
  55. package/src/ast.ts +334 -334
  56. package/src/cli.ts +624 -98
  57. package/src/emit_batch.ts +140 -140
  58. package/src/emit_capability.ts +436 -617
  59. package/src/emit_composition.ts +196 -229
  60. package/src/emit_deploy.ts +190 -190
  61. package/src/emit_events.ts +307 -377
  62. package/src/emit_extras.ts +240 -240
  63. package/src/emit_full.ts +309 -475
  64. package/src/emit_maintenance.ts +459 -459
  65. package/src/emit_runtime.ts +730 -17
  66. package/src/emit_sourcemap.ts +140 -140
  67. package/src/emit_tests.ts +205 -246
  68. package/src/emit_websocket.ts +229 -229
  69. package/src/emitter.ts +578 -642
  70. package/src/extension_manager.ts +187 -189
  71. package/src/formatter.ts +297 -297
  72. package/src/index.ts +88 -88
  73. package/src/ir.ts +215 -216
  74. package/src/lexer.d.ts +195 -0
  75. package/src/lexer.ts +630 -630
  76. package/src/lowering.ts +556 -168
  77. package/src/module_loader.ts +114 -112
  78. package/src/optimizer.ts +196 -196
  79. package/src/parse_decls.d.ts +13 -0
  80. package/src/parse_decls.ts +409 -409
  81. package/src/parse_decls2.d.ts +13 -0
  82. package/src/parse_decls2.ts +244 -244
  83. package/src/parse_expr.d.ts +7 -0
  84. package/src/parse_expr.ts +197 -197
  85. package/src/parse_types.d.ts +6 -0
  86. package/src/parse_types.ts +54 -54
  87. package/src/parser.d.ts +10 -0
  88. package/src/parser.ts +1 -1
  89. package/src/parser_base.d.ts +19 -0
  90. package/src/parser_base.ts +57 -57
  91. package/src/parser_recovery.ts +153 -153
  92. package/src/scaffold.ts +375 -371
  93. package/src/solver.ts +330 -330
  94. package/src/typechecker.d.ts +52 -0
  95. package/src/typechecker.ts +591 -657
  96. package/src/types.d.ts +38 -0
  97. package/src/types.ts +122 -122
  98. package/src/verifier.ts +46 -152
  99. package/README.md +0 -382
  100. package/dist/commands/check.d.ts +0 -5
  101. package/dist/commands/check.js +0 -34
  102. package/dist/commands/check.js.map +0 -1
  103. package/dist/commands/compile.d.ts +0 -5
  104. package/dist/commands/compile.js +0 -215
  105. package/dist/commands/compile.js.map +0 -1
  106. package/dist/commands/debug.d.ts +0 -5
  107. package/dist/commands/debug.js +0 -59
  108. package/dist/commands/debug.js.map +0 -1
  109. package/dist/commands/diff.d.ts +0 -5
  110. package/dist/commands/diff.js +0 -125
  111. package/dist/commands/diff.js.map +0 -1
  112. package/dist/commands/fmt.d.ts +0 -5
  113. package/dist/commands/fmt.js +0 -49
  114. package/dist/commands/fmt.js.map +0 -1
  115. package/dist/commands/init.d.ts +0 -5
  116. package/dist/commands/init.js +0 -96
  117. package/dist/commands/init.js.map +0 -1
  118. package/dist/commands/ir.d.ts +0 -5
  119. package/dist/commands/ir.js +0 -27
  120. package/dist/commands/ir.js.map +0 -1
  121. package/dist/commands/lex.d.ts +0 -5
  122. package/dist/commands/lex.js +0 -21
  123. package/dist/commands/lex.js.map +0 -1
  124. package/dist/commands/parse.d.ts +0 -5
  125. package/dist/commands/parse.js +0 -30
  126. package/dist/commands/parse.js.map +0 -1
  127. package/dist/commands/test.d.ts +0 -5
  128. package/dist/commands/test.js +0 -61
  129. package/dist/commands/test.js.map +0 -1
  130. package/dist/commands/verify_determinism.d.ts +0 -5
  131. package/dist/commands/verify_determinism.js +0 -64
  132. package/dist/commands/verify_determinism.js.map +0 -1
  133. package/dist/commands/watch.d.ts +0 -5
  134. package/dist/commands/watch.js +0 -50
  135. package/dist/commands/watch.js.map +0 -1
  136. package/dist/emit_auth.d.ts +0 -18
  137. package/dist/emit_auth.js +0 -507
  138. package/dist/emit_auth.js.map +0 -1
  139. package/dist/emit_database.d.ts +0 -7
  140. package/dist/emit_database.js +0 -74
  141. package/dist/emit_database.js.map +0 -1
  142. package/dist/emit_index.d.ts +0 -6
  143. package/dist/emit_index.js +0 -202
  144. package/dist/emit_index.js.map +0 -1
  145. package/dist/emit_models.d.ts +0 -12
  146. package/dist/emit_models.js +0 -171
  147. package/dist/emit_models.js.map +0 -1
  148. package/dist/emit_openapi.d.ts +0 -9
  149. package/dist/emit_openapi.js +0 -308
  150. package/dist/emit_openapi.js.map +0 -1
  151. package/dist/emit_package.d.ts +0 -7
  152. package/dist/emit_package.js +0 -70
  153. package/dist/emit_package.js.map +0 -1
  154. package/dist/emit_router.d.ts +0 -12
  155. package/dist/emit_router.js +0 -390
  156. package/dist/emit_router.js.map +0 -1
  157. package/dist/lowering_channels.d.ts +0 -11
  158. package/dist/lowering_channels.js +0 -103
  159. package/dist/lowering_channels.js.map +0 -1
  160. package/dist/lowering_entities.d.ts +0 -11
  161. package/dist/lowering_entities.js +0 -232
  162. package/dist/lowering_entities.js.map +0 -1
  163. package/dist/lowering_helpers.d.ts +0 -13
  164. package/dist/lowering_helpers.js +0 -76
  165. package/dist/lowering_helpers.js.map +0 -1
  166. package/src/commands/check.ts +0 -33
  167. package/src/commands/compile.ts +0 -191
  168. package/src/commands/debug.ts +0 -33
  169. package/src/commands/diff.ts +0 -108
  170. package/src/commands/fmt.ts +0 -22
  171. package/src/commands/init.ts +0 -72
  172. package/src/commands/ir.ts +0 -23
  173. package/src/commands/lex.ts +0 -17
  174. package/src/commands/parse.ts +0 -24
  175. package/src/commands/test.ts +0 -36
  176. package/src/commands/verify_determinism.ts +0 -66
  177. package/src/commands/watch.ts +0 -25
  178. package/src/emit_auth.ts +0 -513
  179. package/src/emit_database.ts +0 -75
  180. package/src/emit_index.ts +0 -210
  181. package/src/emit_models.ts +0 -176
  182. package/src/emit_openapi.ts +0 -318
  183. package/src/emit_package.ts +0 -69
  184. package/src/emit_router.ts +0 -409
  185. package/src/lowering_channels.ts +0 -108
  186. package/src/lowering_entities.ts +0 -258
  187. package/src/lowering_helpers.ts +0 -75
@@ -1,657 +1,591 @@
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
- }
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
+ for (const step of decl.steps) {
340
+ // Check step action references a valid capability or function
341
+ if (!this.symbols.capabilities.has(step.action.name) &&
342
+ !this.symbols.entities.has(step.action.name)) {
343
+ // Allow — could be a helper function not yet declared
344
+ // In strict mode this would be T012
345
+ }
346
+
347
+ // Check compensation exists if step has one
348
+ if (step.compensate) {
349
+ // Same check — compensation should reference a valid capability
350
+ }
351
+ }
352
+
353
+ // Check at least 2 steps (ontology requirement)
354
+ if (decl.steps.length < 2) {
355
+ this.addError("T012", `Flow '${decl.name}' must have at least 2 steps`, decl.loc);
356
+ }
357
+ }
358
+
359
+ // ─── Constraint Checking ───────────────────────────────────────────────────
360
+
361
+ private checkConstraint(decl: AST.ConstraintDeclNode) {
362
+ // Top-level constraints are checked in a global context
363
+ const globalCtx = new TypeContext(new Map(), this.symbols);
364
+ const ctype = this.inferExprType(decl.expr, globalCtx);
365
+ if (ctype && !this.isBoolish(ctype)) {
366
+ this.addError("T005", `Top-level constraint '${decl.name}' must type to bool`, decl.loc);
367
+ }
368
+ }
369
+
370
+ // ─── Expression Type Inference ─────────────────────────────────────────────
371
+
372
+ private inferExprType(expr: AST.ExprNode, ctx: TypeContext): CVType | null {
373
+ switch (expr.kind) {
374
+ case "Literal":
375
+ return this.inferLiteralType(expr);
376
+ case "FieldRef":
377
+ return this.inferFieldRefType(expr, ctx);
378
+ case "BinaryExpr":
379
+ return this.inferBinaryType(expr, ctx);
380
+ case "UnaryExpr":
381
+ return this.inferUnaryType(expr, ctx);
382
+ case "CallExpr":
383
+ return this.inferCallType(expr, ctx);
384
+ case "TernaryExpr":
385
+ return this.inferTernaryType(expr, ctx);
386
+ default:
387
+ return null;
388
+ }
389
+ }
390
+
391
+ private inferLiteralType(expr: AST.LiteralNode): CVType {
392
+ switch (expr.type) {
393
+ case "string": return prim("string");
394
+ case "int": return prim("uint"); // default to uint per spec
395
+ case "float": return prim("float");
396
+ case "bool": return prim("bool");
397
+ case "none": return BOTTOM;
398
+ case "list": return generic("list", prim("json")); // infer element type later
399
+ case "map": return prim("json");
400
+ }
401
+ }
402
+
403
+ private inferFieldRefType(expr: AST.FieldRefNode, ctx: TypeContext): CVType | null {
404
+ const path = expr.path;
405
+ if (path.length === 0) return null;
406
+
407
+ // First segment: look up in context
408
+ let currentType = ctx.lookup(path[0]);
409
+
410
+ if (!currentType) {
411
+ // Try as entity name (for top-level constraints like Player.active_trades)
412
+ const entity = this.symbols.entities.get(path[0]);
413
+ if (entity) {
414
+ currentType = entity.type;
415
+ return this.resolveFieldPath(currentType, path.slice(1), expr);
416
+ }
417
+ // Unknown — don't error here, could be a forward reference
418
+ return prim("json"); // permissive fallback
419
+ }
420
+
421
+ return this.resolveFieldPath(currentType, path.slice(1), expr);
422
+ }
423
+
424
+ private resolveFieldPath(baseType: CVType, remaining: string[], expr: AST.ExprNode): CVType | null {
425
+ let current = baseType;
426
+
427
+ for (const segment of remaining) {
428
+ // Handle .unique, .length, .size as derived properties
429
+ if (segment === "unique") return prim("bool");
430
+ if (segment === "length") return prim("uint");
431
+ if (segment === "size") return prim("uint");
432
+
433
+ if (current.tag === "record") {
434
+ const field = current.fields.get(segment);
435
+ if (field) {
436
+ current = field;
437
+ } else {
438
+ // Field not found — could be a derived property
439
+ return prim("json"); // permissive
440
+ }
441
+ } else if (current.tag === "generic") {
442
+ // Accessing property on generic type (e.g., list.size)
443
+ if (segment === "size" || segment === "length") return prim("uint");
444
+ return prim("json");
445
+ } else {
446
+ return prim("json"); // permissive fallback
447
+ }
448
+ }
449
+
450
+ return current;
451
+ }
452
+
453
+ private inferBinaryType(expr: AST.BinaryExprNode, ctx: TypeContext): CVType {
454
+ const left = this.inferExprType(expr.left, ctx);
455
+ const right = this.inferExprType(expr.right, ctx);
456
+
457
+ switch (expr.op) {
458
+ // Comparison operators → bool
459
+ case "==": case "!=": case "<": case ">": case "<=": case ">=":
460
+ case "in": case "contains": case "and": case "or":
461
+ return prim("bool");
462
+
463
+ // Range operator
464
+ case "..":
465
+ return generic("list", prim("uint")); // range produces a range object
466
+
467
+ // Arithmetic → numeric
468
+ case "+": case "*": case "/": case "%":
469
+ if (left && right && isNumeric(left) && isNumeric(right)) {
470
+ // Promote to widest type
471
+ if ((left as PrimitiveType).name === "float" || (right as PrimitiveType).name === "float") return prim("float");
472
+ if ((left as PrimitiveType).name === "int" || (right as PrimitiveType).name === "int") return prim("int");
473
+ return prim("uint");
474
+ }
475
+ if (expr.op === "+" && left?.tag === "primitive" && (left as PrimitiveType).name === "string") {
476
+ return prim("string"); // string concatenation
477
+ }
478
+ return prim("uint");
479
+
480
+ case "-":
481
+ return prim("int"); // subtraction may produce negative
482
+
483
+ default:
484
+ return prim("bool");
485
+ }
486
+ }
487
+
488
+ private inferUnaryType(expr: AST.UnaryExprNode, ctx: TypeContext): CVType {
489
+ if (expr.op === "not") return prim("bool");
490
+ if (expr.op === "-") return prim("int");
491
+ return prim("json");
492
+ }
493
+
494
+ private inferCallType(expr: AST.CallExprNode, ctx: TypeContext): CVType {
495
+ // Built-in functions
496
+ if (expr.name === "now") return prim("timestamp");
497
+ if (expr.name === "count") return prim("uint");
498
+ if (expr.name === "sum") return prim("uint");
499
+
500
+ // User-defined — return json as permissive fallback
501
+ return prim("json");
502
+ }
503
+
504
+ private inferTernaryType(expr: AST.TernaryExprNode, ctx: TypeContext): CVType | null {
505
+ // condition must be bool
506
+ const condType = this.inferExprType(expr.condition, ctx);
507
+ if (condType && !this.isBoolish(condType)) {
508
+ this.addError("T005", "Ternary condition must be bool", expr.loc);
509
+ }
510
+ // result type is type of consequent (assume both branches same type)
511
+ return this.inferExprType(expr.consequent, ctx);
512
+ }
513
+
514
+ // ─── Helpers ───────────────────────────────────────────────────────────────
515
+
516
+ private resolveTypeExpr(typeExpr: AST.TypeExprNode): CVType | null {
517
+ switch (typeExpr.kind) {
518
+ case "PrimitiveType":
519
+ return prim(typeExpr.name as PrimitiveType["name"]);
520
+ case "GenericType": {
521
+ const args = typeExpr.typeArgs.map(a => this.resolveTypeExpr(a)).filter(Boolean) as CVType[];
522
+ return generic(typeExpr.name as GenericType["name"], ...args);
523
+ }
524
+ case "EntityRefType": {
525
+ const entity = this.symbols.entities.get(typeExpr.name);
526
+ if (entity) return entity.type;
527
+ // Could be a forward reference — register as unknown record
528
+ return record(typeExpr.name, new Map());
529
+ }
530
+ case "TupleType": {
531
+ const elements = typeExpr.elements.map(e => this.resolveTypeExpr(e)).filter(Boolean) as CVType[];
532
+ return { tag: "tuple", elements };
533
+ }
534
+ case "UnionType": {
535
+ const members = typeExpr.members.map(m => this.resolveTypeExpr(m)).filter(Boolean) as CVType[];
536
+ return { tag: "union", members };
537
+ }
538
+ }
539
+ }
540
+
541
+ private isBoolish(t: CVType): boolean {
542
+ return (t.tag === "primitive" && t.name === "bool") || t.tag === "bottom";
543
+ }
544
+
545
+ private isAssignable(source: CVType, target: CVType): boolean {
546
+ if (typeEquals(source, target)) return true;
547
+ // Numeric widening: uint → int → float
548
+ if (source.tag === "primitive" && target.tag === "primitive") {
549
+ if (source.name === "uint" && (target.name === "int" || target.name === "float")) return true;
550
+ if (source.name === "int" && target.name === "float") return true;
551
+ }
552
+ // json accepts anything and anything accepts json (permissive for unresolved)
553
+ if (target.tag === "primitive" && target.name === "json") return true;
554
+ if (source.tag === "primitive" && source.name === "json") return true;
555
+ return false;
556
+ }
557
+ private checkExtensionPoint(decl: AST.ExtensionPointDeclNode): void {
558
+ for (const p of decl.params) {
559
+ const resolved = this.resolveTypeExpr(p.type);
560
+ if (!resolved) {
561
+ this.addError("T001", "Extension point '" + decl.name + "' param '" + p.name + "' references undefined type", p.loc);
562
+ }
563
+ }
564
+ if (decl.returns) {
565
+ const resolved = this.resolveTypeExpr(decl.returns);
566
+ if (!resolved) {
567
+ this.addError("T001", "Extension point '" + decl.name + "' return type is undefined", decl.loc);
568
+ }
569
+ }
570
+ }
571
+
572
+
573
+ }
574
+ // ─── Type Context ────────────────────────────────────────────────────────────
575
+
576
+ class TypeContext {
577
+ private locals: Map<string, CVType>;
578
+ private symbols: SymbolTable;
579
+
580
+ constructor(locals: Map<string, CVType>, symbols: SymbolTable) {
581
+ this.locals = locals;
582
+ this.symbols = symbols;
583
+ }
584
+ lookup(name: string): CVType | null {
585
+ const local = this.locals.get(name);
586
+ if (local) return local;
587
+ const entity = this.symbols.entities.get(name);
588
+ if (entity) return entity.type;
589
+ return null;
590
+ }
591
+ }