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