redscript-mc 1.1.0 → 1.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/CHANGELOG.md +54 -0
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +164 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +8 -8
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +20 -1
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.js +25 -0
- package/dist/compile.d.ts +10 -0
- package/dist/compile.js +36 -5
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/index.js +3 -2
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +439 -15
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +79 -10
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/out/extension.js +1144 -72
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -0
- package/src/__tests__/e2e.test.ts +201 -12
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +14 -2
- package/src/__tests__/lowering.test.ts +178 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +8 -8
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +25 -1
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +27 -0
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- package/src/index.ts +4 -3
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +528 -16
- package/src/parser/index.ts +90 -12
- package/src/stdlib/README.md +34 -4
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/timer.mcrs +54 -33
- package/src/typechecker/index.ts +404 -18
|
@@ -8,18 +8,88 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.TypeChecker = void 0;
|
|
10
10
|
const diagnostics_1 = require("../diagnostics");
|
|
11
|
+
const types_1 = require("../events/types");
|
|
12
|
+
// Entity type hierarchy for subtype checking
|
|
13
|
+
const ENTITY_HIERARCHY = {
|
|
14
|
+
'entity': null,
|
|
15
|
+
'Player': 'entity',
|
|
16
|
+
'Mob': 'entity',
|
|
17
|
+
'HostileMob': 'Mob',
|
|
18
|
+
'PassiveMob': 'Mob',
|
|
19
|
+
'Zombie': 'HostileMob',
|
|
20
|
+
'Skeleton': 'HostileMob',
|
|
21
|
+
'Creeper': 'HostileMob',
|
|
22
|
+
'Spider': 'HostileMob',
|
|
23
|
+
'Enderman': 'HostileMob',
|
|
24
|
+
'Pig': 'PassiveMob',
|
|
25
|
+
'Cow': 'PassiveMob',
|
|
26
|
+
'Sheep': 'PassiveMob',
|
|
27
|
+
'Chicken': 'PassiveMob',
|
|
28
|
+
'Villager': 'PassiveMob',
|
|
29
|
+
'ArmorStand': 'entity',
|
|
30
|
+
'Item': 'entity',
|
|
31
|
+
'Arrow': 'entity',
|
|
32
|
+
};
|
|
33
|
+
// Map Minecraft type names to entity types
|
|
34
|
+
const MC_TYPE_TO_ENTITY = {
|
|
35
|
+
'zombie': 'Zombie',
|
|
36
|
+
'minecraft:zombie': 'Zombie',
|
|
37
|
+
'skeleton': 'Skeleton',
|
|
38
|
+
'minecraft:skeleton': 'Skeleton',
|
|
39
|
+
'creeper': 'Creeper',
|
|
40
|
+
'minecraft:creeper': 'Creeper',
|
|
41
|
+
'spider': 'Spider',
|
|
42
|
+
'minecraft:spider': 'Spider',
|
|
43
|
+
'enderman': 'Enderman',
|
|
44
|
+
'minecraft:enderman': 'Enderman',
|
|
45
|
+
'pig': 'Pig',
|
|
46
|
+
'minecraft:pig': 'Pig',
|
|
47
|
+
'cow': 'Cow',
|
|
48
|
+
'minecraft:cow': 'Cow',
|
|
49
|
+
'sheep': 'Sheep',
|
|
50
|
+
'minecraft:sheep': 'Sheep',
|
|
51
|
+
'chicken': 'Chicken',
|
|
52
|
+
'minecraft:chicken': 'Chicken',
|
|
53
|
+
'villager': 'Villager',
|
|
54
|
+
'minecraft:villager': 'Villager',
|
|
55
|
+
'armor_stand': 'ArmorStand',
|
|
56
|
+
'minecraft:armor_stand': 'ArmorStand',
|
|
57
|
+
'item': 'Item',
|
|
58
|
+
'minecraft:item': 'Item',
|
|
59
|
+
'arrow': 'Arrow',
|
|
60
|
+
'minecraft:arrow': 'Arrow',
|
|
61
|
+
};
|
|
62
|
+
const VOID_TYPE = { kind: 'named', name: 'void' };
|
|
63
|
+
const INT_TYPE = { kind: 'named', name: 'int' };
|
|
64
|
+
const BUILTIN_SIGNATURES = {
|
|
65
|
+
setTimeout: {
|
|
66
|
+
params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
|
|
67
|
+
return: VOID_TYPE,
|
|
68
|
+
},
|
|
69
|
+
setInterval: {
|
|
70
|
+
params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
|
|
71
|
+
return: INT_TYPE,
|
|
72
|
+
},
|
|
73
|
+
clearInterval: {
|
|
74
|
+
params: [INT_TYPE],
|
|
75
|
+
return: VOID_TYPE,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
11
78
|
// ---------------------------------------------------------------------------
|
|
12
79
|
// Type Checker
|
|
13
80
|
// ---------------------------------------------------------------------------
|
|
14
81
|
class TypeChecker {
|
|
15
82
|
constructor(source, filePath) {
|
|
16
83
|
this.functions = new Map();
|
|
84
|
+
this.implMethods = new Map();
|
|
17
85
|
this.structs = new Map();
|
|
18
86
|
this.enums = new Map();
|
|
19
87
|
this.consts = new Map();
|
|
20
88
|
this.currentFn = null;
|
|
21
89
|
this.currentReturnType = null;
|
|
22
90
|
this.scope = new Map();
|
|
91
|
+
// Stack for tracking @s type in different contexts
|
|
92
|
+
this.selfTypeStack = ['entity'];
|
|
23
93
|
this.collector = new diagnostics_1.DiagnosticCollector(source, filePath);
|
|
24
94
|
}
|
|
25
95
|
getNodeLocation(node) {
|
|
@@ -41,6 +111,26 @@ class TypeChecker {
|
|
|
41
111
|
for (const fn of program.declarations) {
|
|
42
112
|
this.functions.set(fn.name, fn);
|
|
43
113
|
}
|
|
114
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
115
|
+
let methods = this.implMethods.get(implBlock.typeName);
|
|
116
|
+
if (!methods) {
|
|
117
|
+
methods = new Map();
|
|
118
|
+
this.implMethods.set(implBlock.typeName, methods);
|
|
119
|
+
}
|
|
120
|
+
for (const method of implBlock.methods) {
|
|
121
|
+
const selfIndex = method.params.findIndex(param => param.name === 'self');
|
|
122
|
+
if (selfIndex > 0) {
|
|
123
|
+
this.report(`Method '${method.name}' must declare 'self' as the first parameter`, method.params[selfIndex]);
|
|
124
|
+
}
|
|
125
|
+
if (selfIndex === 0) {
|
|
126
|
+
const selfType = this.normalizeType(method.params[0].type);
|
|
127
|
+
if (selfType.kind !== 'struct' || selfType.name !== implBlock.typeName) {
|
|
128
|
+
this.report(`Method '${method.name}' has invalid 'self' type`, method.params[0]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
methods.set(method.name, method);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
44
134
|
for (const struct of program.structs ?? []) {
|
|
45
135
|
const fields = new Map();
|
|
46
136
|
for (const field of struct.fields) {
|
|
@@ -67,6 +157,11 @@ class TypeChecker {
|
|
|
67
157
|
for (const fn of program.declarations) {
|
|
68
158
|
this.checkFunction(fn);
|
|
69
159
|
}
|
|
160
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
161
|
+
for (const method of implBlock.methods) {
|
|
162
|
+
this.checkFunction(method);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
70
165
|
return this.collector.getErrors();
|
|
71
166
|
}
|
|
72
167
|
checkFunction(fn) {
|
|
@@ -74,6 +169,7 @@ class TypeChecker {
|
|
|
74
169
|
this.currentReturnType = this.normalizeType(fn.returnType);
|
|
75
170
|
this.scope = new Map();
|
|
76
171
|
let seenDefault = false;
|
|
172
|
+
this.checkFunctionDecorators(fn);
|
|
77
173
|
for (const [name, type] of this.consts.entries()) {
|
|
78
174
|
this.scope.set(name, { type, mutable: false });
|
|
79
175
|
}
|
|
@@ -98,6 +194,37 @@ class TypeChecker {
|
|
|
98
194
|
this.currentFn = null;
|
|
99
195
|
this.currentReturnType = null;
|
|
100
196
|
}
|
|
197
|
+
checkFunctionDecorators(fn) {
|
|
198
|
+
const eventDecorators = fn.decorators.filter(decorator => decorator.name === 'on');
|
|
199
|
+
if (eventDecorators.length === 0) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (eventDecorators.length > 1) {
|
|
203
|
+
this.report(`Function '${fn.name}' cannot have multiple @on decorators`, fn);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const eventType = eventDecorators[0].args?.eventType;
|
|
207
|
+
if (!eventType) {
|
|
208
|
+
this.report(`Function '${fn.name}' is missing an event type in @on(...)`, fn);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!(0, types_1.isEventTypeName)(eventType)) {
|
|
212
|
+
this.report(`Unknown event type '${eventType}'`, fn);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const expectedParams = (0, types_1.getEventParamSpecs)(eventType);
|
|
216
|
+
if (fn.params.length !== expectedParams.length) {
|
|
217
|
+
this.report(`Event handler '${fn.name}' for ${eventType} must declare ${expectedParams.length} parameter(s), got ${fn.params.length}`, fn);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
for (let i = 0; i < expectedParams.length; i++) {
|
|
221
|
+
const actual = this.normalizeType(fn.params[i].type);
|
|
222
|
+
const expected = this.normalizeType(expectedParams[i].type);
|
|
223
|
+
if (!this.typesMatch(expected, actual)) {
|
|
224
|
+
this.report(`Event handler '${fn.name}' parameter ${i + 1} must be ${this.typeToString(expected)}, got ${this.typeToString(actual)}`, fn.params[i]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
101
228
|
checkBlock(stmts) {
|
|
102
229
|
for (const stmt of stmts) {
|
|
103
230
|
this.checkStmt(stmt);
|
|
@@ -113,9 +240,7 @@ class TypeChecker {
|
|
|
113
240
|
break;
|
|
114
241
|
case 'if':
|
|
115
242
|
this.checkExpr(stmt.cond);
|
|
116
|
-
this.
|
|
117
|
-
if (stmt.else_)
|
|
118
|
-
this.checkBlock(stmt.else_);
|
|
243
|
+
this.checkIfBranches(stmt);
|
|
119
244
|
break;
|
|
120
245
|
case 'while':
|
|
121
246
|
this.checkExpr(stmt.cond);
|
|
@@ -131,7 +256,16 @@ class TypeChecker {
|
|
|
131
256
|
case 'foreach':
|
|
132
257
|
this.checkExpr(stmt.iterable);
|
|
133
258
|
if (stmt.iterable.kind === 'selector') {
|
|
134
|
-
|
|
259
|
+
// Infer entity type from selector (access .sel for the EntitySelector)
|
|
260
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.iterable.sel);
|
|
261
|
+
this.scope.set(stmt.binding, {
|
|
262
|
+
type: { kind: 'entity', entityType },
|
|
263
|
+
mutable: false // Entity bindings are not reassignable
|
|
264
|
+
});
|
|
265
|
+
// Push self type context for @s inside the loop
|
|
266
|
+
this.pushSelfType(entityType);
|
|
267
|
+
this.checkBlock(stmt.body);
|
|
268
|
+
this.popSelfType();
|
|
135
269
|
}
|
|
136
270
|
else {
|
|
137
271
|
const iterableType = this.inferType(stmt.iterable);
|
|
@@ -141,8 +275,8 @@ class TypeChecker {
|
|
|
141
275
|
else {
|
|
142
276
|
this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true });
|
|
143
277
|
}
|
|
278
|
+
this.checkBlock(stmt.body);
|
|
144
279
|
}
|
|
145
|
-
this.checkBlock(stmt.body);
|
|
146
280
|
break;
|
|
147
281
|
case 'match':
|
|
148
282
|
this.checkExpr(stmt.expr);
|
|
@@ -156,15 +290,41 @@ class TypeChecker {
|
|
|
156
290
|
this.checkBlock(arm.body);
|
|
157
291
|
}
|
|
158
292
|
break;
|
|
159
|
-
case 'as_block':
|
|
293
|
+
case 'as_block': {
|
|
294
|
+
// as block changes @s to the selector's entity type
|
|
295
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.selector);
|
|
296
|
+
this.pushSelfType(entityType);
|
|
297
|
+
this.checkBlock(stmt.body);
|
|
298
|
+
this.popSelfType();
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
160
301
|
case 'at_block':
|
|
302
|
+
// at block doesn't change @s type, only position
|
|
161
303
|
this.checkBlock(stmt.body);
|
|
162
304
|
break;
|
|
163
|
-
case 'as_at':
|
|
305
|
+
case 'as_at': {
|
|
306
|
+
// as @x at @y - @s becomes the as selector's type
|
|
307
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.as_sel);
|
|
308
|
+
this.pushSelfType(entityType);
|
|
164
309
|
this.checkBlock(stmt.body);
|
|
310
|
+
this.popSelfType();
|
|
165
311
|
break;
|
|
312
|
+
}
|
|
166
313
|
case 'execute':
|
|
314
|
+
// execute with subcommands - check for 'as' subcommands
|
|
315
|
+
for (const sub of stmt.subcommands) {
|
|
316
|
+
if (sub.kind === 'as' && sub.selector) {
|
|
317
|
+
const entityType = this.inferEntityTypeFromSelector(sub.selector);
|
|
318
|
+
this.pushSelfType(entityType);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
167
321
|
this.checkBlock(stmt.body);
|
|
322
|
+
// Pop for each 'as' subcommand
|
|
323
|
+
for (const sub of stmt.subcommands) {
|
|
324
|
+
if (sub.kind === 'as') {
|
|
325
|
+
this.popSelfType();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
168
328
|
break;
|
|
169
329
|
case 'expr':
|
|
170
330
|
this.checkExpr(stmt.expr);
|
|
@@ -224,10 +384,21 @@ class TypeChecker {
|
|
|
224
384
|
case 'member':
|
|
225
385
|
this.checkMemberExpr(expr);
|
|
226
386
|
break;
|
|
387
|
+
case 'static_call':
|
|
388
|
+
this.checkStaticCallExpr(expr);
|
|
389
|
+
break;
|
|
227
390
|
case 'binary':
|
|
228
391
|
this.checkExpr(expr.left);
|
|
229
392
|
this.checkExpr(expr.right);
|
|
230
393
|
break;
|
|
394
|
+
case 'is_check': {
|
|
395
|
+
this.checkExpr(expr.expr);
|
|
396
|
+
const checkedType = this.inferType(expr.expr);
|
|
397
|
+
if (checkedType.kind !== 'entity') {
|
|
398
|
+
this.report(`'is' checks require an entity expression, got ${this.typeToString(checkedType)}`, expr.expr);
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
231
402
|
case 'unary':
|
|
232
403
|
this.checkExpr(expr.operand);
|
|
233
404
|
break;
|
|
@@ -274,11 +445,6 @@ class TypeChecker {
|
|
|
274
445
|
break;
|
|
275
446
|
case 'blockpos':
|
|
276
447
|
break;
|
|
277
|
-
case 'static_call':
|
|
278
|
-
for (const arg of expr.args) {
|
|
279
|
-
this.checkExpr(arg);
|
|
280
|
-
}
|
|
281
|
-
break;
|
|
282
448
|
// Literals don't need checking
|
|
283
449
|
case 'int_lit':
|
|
284
450
|
case 'float_lit':
|
|
@@ -298,6 +464,11 @@ class TypeChecker {
|
|
|
298
464
|
if (expr.fn === 'tp' || expr.fn === 'tp_to') {
|
|
299
465
|
this.checkTpCall(expr);
|
|
300
466
|
}
|
|
467
|
+
const builtin = BUILTIN_SIGNATURES[expr.fn];
|
|
468
|
+
if (builtin) {
|
|
469
|
+
this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
301
472
|
// Check if function exists and arg count matches
|
|
302
473
|
const fn = this.functions.get(expr.fn);
|
|
303
474
|
if (fn) {
|
|
@@ -325,6 +496,11 @@ class TypeChecker {
|
|
|
325
496
|
this.checkFunctionCallArgs(expr.args, varType.params, expr.fn, expr);
|
|
326
497
|
return;
|
|
327
498
|
}
|
|
499
|
+
const implMethod = this.resolveInstanceMethod(expr);
|
|
500
|
+
if (implMethod) {
|
|
501
|
+
this.checkFunctionCallArgs(expr.args, implMethod.params.map(param => this.normalizeType(param.type)), implMethod.name, expr);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
328
504
|
for (const arg of expr.args) {
|
|
329
505
|
this.checkExpr(arg);
|
|
330
506
|
}
|
|
@@ -412,6 +588,21 @@ class TypeChecker {
|
|
|
412
588
|
}
|
|
413
589
|
}
|
|
414
590
|
}
|
|
591
|
+
checkStaticCallExpr(expr) {
|
|
592
|
+
const method = this.implMethods.get(expr.type)?.get(expr.method);
|
|
593
|
+
if (!method) {
|
|
594
|
+
this.report(`Type '${expr.type}' has no static method '${expr.method}'`, expr);
|
|
595
|
+
for (const arg of expr.args) {
|
|
596
|
+
this.checkExpr(arg);
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (method.params[0]?.name === 'self') {
|
|
601
|
+
this.report(`Method '${expr.type}::${expr.method}' is an instance method`, expr);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
this.checkFunctionCallArgs(expr.args, method.params.map(param => this.normalizeType(param.type)), `${expr.type}::${expr.method}`, expr);
|
|
605
|
+
}
|
|
415
606
|
checkLambdaExpr(expr, expectedType) {
|
|
416
607
|
const normalizedExpected = expectedType ? this.normalizeType(expectedType) : undefined;
|
|
417
608
|
const expectedFnType = normalizedExpected?.kind === 'function_type' ? normalizedExpected : undefined;
|
|
@@ -447,6 +638,37 @@ class TypeChecker {
|
|
|
447
638
|
this.scope = outerScope;
|
|
448
639
|
this.currentReturnType = outerReturnType;
|
|
449
640
|
}
|
|
641
|
+
checkIfBranches(stmt) {
|
|
642
|
+
const narrowed = this.getThenBranchNarrowing(stmt.cond);
|
|
643
|
+
if (narrowed) {
|
|
644
|
+
const thenScope = new Map(this.scope);
|
|
645
|
+
thenScope.set(narrowed.name, { type: narrowed.type, mutable: narrowed.mutable });
|
|
646
|
+
const outerScope = this.scope;
|
|
647
|
+
this.scope = thenScope;
|
|
648
|
+
this.checkBlock(stmt.then);
|
|
649
|
+
this.scope = outerScope;
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
this.checkBlock(stmt.then);
|
|
653
|
+
}
|
|
654
|
+
if (stmt.else_) {
|
|
655
|
+
this.checkBlock(stmt.else_);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
getThenBranchNarrowing(cond) {
|
|
659
|
+
if (cond.kind !== 'is_check' || cond.expr.kind !== 'ident') {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
const symbol = this.scope.get(cond.expr.name);
|
|
663
|
+
if (!symbol || symbol.type.kind !== 'entity') {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
name: cond.expr.name,
|
|
668
|
+
type: { kind: 'entity', entityType: cond.entityType },
|
|
669
|
+
mutable: symbol.mutable,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
450
672
|
inferType(expr, expectedType) {
|
|
451
673
|
switch (expr.kind) {
|
|
452
674
|
case 'int_lit':
|
|
@@ -478,8 +700,12 @@ class TypeChecker {
|
|
|
478
700
|
case 'ident':
|
|
479
701
|
return this.scope.get(expr.name)?.type ?? { kind: 'named', name: 'void' };
|
|
480
702
|
case 'call': {
|
|
703
|
+
const builtin = BUILTIN_SIGNATURES[expr.fn];
|
|
704
|
+
if (builtin) {
|
|
705
|
+
return builtin.return;
|
|
706
|
+
}
|
|
481
707
|
if (expr.fn === '__array_push') {
|
|
482
|
-
return
|
|
708
|
+
return VOID_TYPE;
|
|
483
709
|
}
|
|
484
710
|
if (expr.fn === '__array_pop') {
|
|
485
711
|
const target = expr.args[0];
|
|
@@ -488,20 +714,28 @@ class TypeChecker {
|
|
|
488
714
|
if (targetType?.kind === 'array')
|
|
489
715
|
return targetType.elem;
|
|
490
716
|
}
|
|
491
|
-
return
|
|
717
|
+
return INT_TYPE;
|
|
492
718
|
}
|
|
493
719
|
if (expr.fn === 'bossbar_get_value') {
|
|
494
|
-
return
|
|
720
|
+
return INT_TYPE;
|
|
495
721
|
}
|
|
496
722
|
if (expr.fn === 'random_sequence') {
|
|
497
|
-
return
|
|
723
|
+
return VOID_TYPE;
|
|
498
724
|
}
|
|
499
725
|
const varType = this.scope.get(expr.fn)?.type;
|
|
500
726
|
if (varType?.kind === 'function_type') {
|
|
501
727
|
return varType.return;
|
|
502
728
|
}
|
|
729
|
+
const implMethod = this.resolveInstanceMethod(expr);
|
|
730
|
+
if (implMethod) {
|
|
731
|
+
return this.normalizeType(implMethod.returnType);
|
|
732
|
+
}
|
|
503
733
|
const fn = this.functions.get(expr.fn);
|
|
504
|
-
return fn?.returnType ??
|
|
734
|
+
return fn?.returnType ?? INT_TYPE;
|
|
735
|
+
}
|
|
736
|
+
case 'static_call': {
|
|
737
|
+
const method = this.implMethods.get(expr.type)?.get(expr.method);
|
|
738
|
+
return method ? this.normalizeType(method.returnType) : { kind: 'named', name: 'void' };
|
|
505
739
|
}
|
|
506
740
|
case 'invoke': {
|
|
507
741
|
const calleeType = this.inferType(expr.callee);
|
|
@@ -532,6 +766,8 @@ class TypeChecker {
|
|
|
532
766
|
return { kind: 'named', name: 'bool' };
|
|
533
767
|
}
|
|
534
768
|
return this.inferType(expr.left);
|
|
769
|
+
case 'is_check':
|
|
770
|
+
return { kind: 'named', name: 'bool' };
|
|
535
771
|
case 'unary':
|
|
536
772
|
if (expr.op === '!')
|
|
537
773
|
return { kind: 'named', name: 'bool' };
|
|
@@ -541,6 +777,14 @@ class TypeChecker {
|
|
|
541
777
|
return { kind: 'array', elem: this.inferType(expr.elements[0]) };
|
|
542
778
|
}
|
|
543
779
|
return { kind: 'array', elem: { kind: 'named', name: 'int' } };
|
|
780
|
+
case 'struct_lit':
|
|
781
|
+
if (expectedType) {
|
|
782
|
+
const normalized = this.normalizeType(expectedType);
|
|
783
|
+
if (normalized.kind === 'struct') {
|
|
784
|
+
return normalized;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return { kind: 'named', name: 'void' };
|
|
544
788
|
case 'lambda':
|
|
545
789
|
return this.inferLambdaType(expr, expectedType && this.normalizeType(expectedType).kind === 'function_type'
|
|
546
790
|
? this.normalizeType(expectedType)
|
|
@@ -569,6 +813,68 @@ class TypeChecker {
|
|
|
569
813
|
}
|
|
570
814
|
return { kind: 'function_type', params, return: returnType };
|
|
571
815
|
}
|
|
816
|
+
// ---------------------------------------------------------------------------
|
|
817
|
+
// Entity Type Helpers
|
|
818
|
+
// ---------------------------------------------------------------------------
|
|
819
|
+
/** Infer entity type from a selector */
|
|
820
|
+
inferEntityTypeFromSelector(selector) {
|
|
821
|
+
// @a, @p, @r always return Player
|
|
822
|
+
if (selector.kind === '@a' || selector.kind === '@p' || selector.kind === '@r') {
|
|
823
|
+
return 'Player';
|
|
824
|
+
}
|
|
825
|
+
// @e or @s with type= filter
|
|
826
|
+
if (selector.filters?.type) {
|
|
827
|
+
const mcType = selector.filters.type.toLowerCase();
|
|
828
|
+
return MC_TYPE_TO_ENTITY[mcType] ?? 'entity';
|
|
829
|
+
}
|
|
830
|
+
// @s uses current context
|
|
831
|
+
if (selector.kind === '@s') {
|
|
832
|
+
return this.selfTypeStack[this.selfTypeStack.length - 1];
|
|
833
|
+
}
|
|
834
|
+
// Default to entity
|
|
835
|
+
return 'entity';
|
|
836
|
+
}
|
|
837
|
+
resolveInstanceMethod(expr) {
|
|
838
|
+
const receiver = expr.args[0];
|
|
839
|
+
if (!receiver) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
const receiverType = this.inferType(receiver);
|
|
843
|
+
if (receiverType.kind !== 'struct') {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
const method = this.implMethods.get(receiverType.name)?.get(expr.fn);
|
|
847
|
+
if (!method || method.params[0]?.name !== 'self') {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
return method;
|
|
851
|
+
}
|
|
852
|
+
/** Check if childType is a subtype of parentType */
|
|
853
|
+
isEntitySubtype(childType, parentType) {
|
|
854
|
+
if (childType === parentType)
|
|
855
|
+
return true;
|
|
856
|
+
let current = childType;
|
|
857
|
+
while (current !== null) {
|
|
858
|
+
if (current === parentType)
|
|
859
|
+
return true;
|
|
860
|
+
current = ENTITY_HIERARCHY[current];
|
|
861
|
+
}
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
/** Push a new self type context */
|
|
865
|
+
pushSelfType(entityType) {
|
|
866
|
+
this.selfTypeStack.push(entityType);
|
|
867
|
+
}
|
|
868
|
+
/** Pop self type context */
|
|
869
|
+
popSelfType() {
|
|
870
|
+
if (this.selfTypeStack.length > 1) {
|
|
871
|
+
this.selfTypeStack.pop();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/** Get current @s type */
|
|
875
|
+
getCurrentSelfType() {
|
|
876
|
+
return this.selfTypeStack[this.selfTypeStack.length - 1];
|
|
877
|
+
}
|
|
572
878
|
typesMatch(expected, actual) {
|
|
573
879
|
if (expected.kind !== actual.kind)
|
|
574
880
|
return false;
|
|
@@ -592,6 +898,14 @@ class TypeChecker {
|
|
|
592
898
|
expected.params.every((param, index) => this.typesMatch(param, actual.params[index])) &&
|
|
593
899
|
this.typesMatch(expected.return, actual.return);
|
|
594
900
|
}
|
|
901
|
+
// Entity type matching with subtype support
|
|
902
|
+
if (expected.kind === 'entity' && actual.kind === 'entity') {
|
|
903
|
+
return this.isEntitySubtype(actual.entityType, expected.entityType);
|
|
904
|
+
}
|
|
905
|
+
// Selector matches any entity type
|
|
906
|
+
if (expected.kind === 'selector' && actual.kind === 'entity') {
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
595
909
|
return false;
|
|
596
910
|
}
|
|
597
911
|
typeToString(type) {
|
|
@@ -606,6 +920,12 @@ class TypeChecker {
|
|
|
606
920
|
return type.name;
|
|
607
921
|
case 'function_type':
|
|
608
922
|
return `(${type.params.map(param => this.typeToString(param)).join(', ')}) -> ${this.typeToString(type.return)}`;
|
|
923
|
+
case 'entity':
|
|
924
|
+
return type.entityType;
|
|
925
|
+
case 'selector':
|
|
926
|
+
return 'selector';
|
|
927
|
+
default:
|
|
928
|
+
return 'unknown';
|
|
609
929
|
}
|
|
610
930
|
}
|
|
611
931
|
normalizeType(type) {
|
|
@@ -622,6 +942,12 @@ class TypeChecker {
|
|
|
622
942
|
if ((type.kind === 'struct' || type.kind === 'enum') && this.enums.has(type.name)) {
|
|
623
943
|
return { kind: 'enum', name: type.name };
|
|
624
944
|
}
|
|
945
|
+
if (type.kind === 'struct' && type.name in ENTITY_HIERARCHY) {
|
|
946
|
+
return { kind: 'entity', entityType: type.name };
|
|
947
|
+
}
|
|
948
|
+
if (type.kind === 'named' && type.name in ENTITY_HIERARCHY) {
|
|
949
|
+
return { kind: 'entity', entityType: type.name };
|
|
950
|
+
}
|
|
625
951
|
return type;
|
|
626
952
|
}
|
|
627
953
|
}
|