bonescript-compiler 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/algorithm_catalog.d.ts +32 -0
- package/dist/algorithm_catalog.js +323 -0
- package/dist/algorithm_catalog.js.map +1 -0
- package/dist/ast.d.ts +244 -0
- package/dist/ast.js +8 -0
- package/dist/ast.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +605 -0
- package/dist/cli.js.map +1 -0
- package/dist/emit_batch.d.ts +7 -0
- package/dist/emit_batch.js +133 -0
- package/dist/emit_batch.js.map +1 -0
- package/dist/emit_capability.d.ts +7 -0
- package/dist/emit_capability.js +376 -0
- package/dist/emit_capability.js.map +1 -0
- package/dist/emit_composition.d.ts +22 -0
- package/dist/emit_composition.js +184 -0
- package/dist/emit_composition.js.map +1 -0
- package/dist/emit_deploy.d.ts +9 -0
- package/dist/emit_deploy.js +191 -0
- package/dist/emit_deploy.js.map +1 -0
- package/dist/emit_events.d.ts +14 -0
- package/dist/emit_events.js +305 -0
- package/dist/emit_events.js.map +1 -0
- package/dist/emit_extras.d.ts +12 -0
- package/dist/emit_extras.js +234 -0
- package/dist/emit_extras.js.map +1 -0
- package/dist/emit_full.d.ts +13 -0
- package/dist/emit_full.js +273 -0
- package/dist/emit_full.js.map +1 -0
- package/dist/emit_maintenance.d.ts +16 -0
- package/dist/emit_maintenance.js +442 -0
- package/dist/emit_maintenance.js.map +1 -0
- package/dist/emit_runtime.d.ts +13 -0
- package/dist/emit_runtime.js +691 -0
- package/dist/emit_runtime.js.map +1 -0
- package/dist/emit_sourcemap.d.ts +29 -0
- package/dist/emit_sourcemap.js +123 -0
- package/dist/emit_sourcemap.js.map +1 -0
- package/dist/emit_tests.d.ts +15 -0
- package/dist/emit_tests.js +185 -0
- package/dist/emit_tests.js.map +1 -0
- package/dist/emit_websocket.d.ts +6 -0
- package/dist/emit_websocket.js +223 -0
- package/dist/emit_websocket.js.map +1 -0
- package/dist/emitter.d.ts +25 -0
- package/dist/emitter.js +511 -0
- package/dist/emitter.js.map +1 -0
- package/dist/extension_manager.d.ts +38 -0
- package/dist/extension_manager.js +170 -0
- package/dist/extension_manager.js.map +1 -0
- package/dist/formatter.d.ts +34 -0
- package/dist/formatter.js +317 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/dist/ir.d.ts +168 -0
- package/dist/ir.js +10 -0
- package/dist/ir.js.map +1 -0
- package/dist/lexer.d.ts +195 -0
- package/dist/lexer.js +619 -0
- package/dist/lexer.js.map +1 -0
- package/dist/lowering.d.ts +25 -0
- package/dist/lowering.js +500 -0
- package/dist/lowering.js.map +1 -0
- package/dist/module_loader.d.ts +25 -0
- package/dist/module_loader.js +126 -0
- package/dist/module_loader.js.map +1 -0
- package/dist/optimizer.d.ts +26 -0
- package/dist/optimizer.js +158 -0
- package/dist/optimizer.js.map +1 -0
- package/dist/parse_decls.d.ts +13 -0
- package/dist/parse_decls.js +442 -0
- package/dist/parse_decls.js.map +1 -0
- package/dist/parse_decls2.d.ts +13 -0
- package/dist/parse_decls2.js +295 -0
- package/dist/parse_decls2.js.map +1 -0
- package/dist/parse_expr.d.ts +7 -0
- package/dist/parse_expr.js +197 -0
- package/dist/parse_expr.js.map +1 -0
- package/dist/parse_types.d.ts +6 -0
- package/dist/parse_types.js +51 -0
- package/dist/parse_types.js.map +1 -0
- package/dist/parser.d.ts +10 -0
- package/dist/parser.js +62 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser_base.d.ts +19 -0
- package/dist/parser_base.js +50 -0
- package/dist/parser_base.js.map +1 -0
- package/dist/parser_recovery.d.ts +26 -0
- package/dist/parser_recovery.js +140 -0
- package/dist/parser_recovery.js.map +1 -0
- package/dist/scaffold.d.ts +13 -0
- package/dist/scaffold.js +376 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/solver.d.ts +26 -0
- package/dist/solver.js +281 -0
- package/dist/solver.js.map +1 -0
- package/dist/typechecker.d.ts +52 -0
- package/dist/typechecker.js +534 -0
- package/dist/typechecker.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +85 -0
- package/dist/types.js.map +1 -0
- package/dist/verifier.d.ts +46 -0
- package/dist/verifier.js +307 -0
- package/dist/verifier.js.map +1 -0
- package/package.json +52 -0
- package/src/algorithm_catalog.ts +345 -0
- package/src/ast.ts +334 -0
- package/src/cli.ts +624 -0
- package/src/emit_batch.ts +140 -0
- package/src/emit_capability.ts +436 -0
- package/src/emit_composition.ts +196 -0
- package/src/emit_deploy.ts +190 -0
- package/src/emit_events.ts +307 -0
- package/src/emit_extras.ts +240 -0
- package/src/emit_full.ts +309 -0
- package/src/emit_maintenance.ts +459 -0
- package/src/emit_runtime.ts +731 -0
- package/src/emit_sourcemap.ts +140 -0
- package/src/emit_tests.ts +205 -0
- package/src/emit_websocket.ts +229 -0
- package/src/emitter.ts +566 -0
- package/src/extension_manager.ts +187 -0
- package/src/formatter.ts +297 -0
- package/src/index.ts +88 -0
- package/src/ir.ts +215 -0
- package/src/lexer.ts +630 -0
- package/src/lowering.ts +556 -0
- package/src/module_loader.ts +114 -0
- package/src/optimizer.ts +196 -0
- package/src/parse_decls.ts +409 -0
- package/src/parse_decls2.ts +244 -0
- package/src/parse_expr.ts +197 -0
- package/src/parse_types.ts +54 -0
- package/src/parser.ts +64 -0
- package/src/parser_base.ts +57 -0
- package/src/parser_recovery.ts +153 -0
- package/src/scaffold.ts +375 -0
- package/src/solver.ts +330 -0
- package/src/typechecker.ts +591 -0
- package/src/types.ts +122 -0
- package/src/verifier.ts +348 -0
|
@@ -0,0 +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
|
+
}
|
|
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
|
+
}
|