redscript-mc 1.1.0 → 1.2.1
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 +59 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +31 -4
- package/dist/__tests__/lowering.test.js +172 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +90 -0
- package/dist/__tests__/runtime.test.js +21 -8
- package/dist/__tests__/typechecker.test.js +188 -0
- package/dist/ast/types.d.ts +42 -3
- package/dist/cli.js +15 -10
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +29 -2
- package/dist/compile.d.ts +11 -0
- package/dist/compile.js +40 -6
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -3
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +91 -1
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +476 -16
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +591 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +160 -26
- package/dist/typechecker/index.d.ts +19 -0
- package/dist/typechecker/index.js +392 -17
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icon.png +0 -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/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
- package/examples/spiral.mcrs +79 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -0
- package/src/__tests__/dce.test.ts +129 -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 +35 -4
- package/src/__tests__/lowering.test.ts +187 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +102 -5
- package/src/__tests__/runtime.test.ts +24 -8
- package/src/__tests__/typechecker.test.ts +204 -0
- package/src/ast/types.ts +39 -2
- package/src/cli.ts +24 -10
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +40 -2
- package/src/compile.ts +59 -7
- package/src/events/types.ts +69 -0
- package/src/index.ts +9 -4
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +105 -2
- package/src/lowering/index.ts +566 -18
- package/src/optimizer/dce.ts +618 -0
- package/src/parser/index.ts +187 -29
- 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 +469 -18
|
@@ -8,18 +8,99 @@
|
|
|
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 STRING_TYPE = { kind: 'named', name: 'string' };
|
|
65
|
+
const FORMAT_STRING_TYPE = { kind: 'named', name: 'format_string' };
|
|
66
|
+
const BUILTIN_SIGNATURES = {
|
|
67
|
+
setTimeout: {
|
|
68
|
+
params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
|
|
69
|
+
return: VOID_TYPE,
|
|
70
|
+
},
|
|
71
|
+
setInterval: {
|
|
72
|
+
params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
|
|
73
|
+
return: INT_TYPE,
|
|
74
|
+
},
|
|
75
|
+
clearInterval: {
|
|
76
|
+
params: [INT_TYPE],
|
|
77
|
+
return: VOID_TYPE,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
11
80
|
// ---------------------------------------------------------------------------
|
|
12
81
|
// Type Checker
|
|
13
82
|
// ---------------------------------------------------------------------------
|
|
14
83
|
class TypeChecker {
|
|
15
84
|
constructor(source, filePath) {
|
|
16
85
|
this.functions = new Map();
|
|
86
|
+
this.implMethods = new Map();
|
|
17
87
|
this.structs = new Map();
|
|
18
88
|
this.enums = new Map();
|
|
19
89
|
this.consts = new Map();
|
|
20
90
|
this.currentFn = null;
|
|
21
91
|
this.currentReturnType = null;
|
|
22
92
|
this.scope = new Map();
|
|
93
|
+
// Stack for tracking @s type in different contexts
|
|
94
|
+
this.selfTypeStack = ['entity'];
|
|
95
|
+
this.richTextBuiltins = new Map([
|
|
96
|
+
['say', { messageIndex: 0 }],
|
|
97
|
+
['announce', { messageIndex: 0 }],
|
|
98
|
+
['tell', { messageIndex: 1 }],
|
|
99
|
+
['tellraw', { messageIndex: 1 }],
|
|
100
|
+
['title', { messageIndex: 1 }],
|
|
101
|
+
['actionbar', { messageIndex: 1 }],
|
|
102
|
+
['subtitle', { messageIndex: 1 }],
|
|
103
|
+
]);
|
|
23
104
|
this.collector = new diagnostics_1.DiagnosticCollector(source, filePath);
|
|
24
105
|
}
|
|
25
106
|
getNodeLocation(node) {
|
|
@@ -41,6 +122,26 @@ class TypeChecker {
|
|
|
41
122
|
for (const fn of program.declarations) {
|
|
42
123
|
this.functions.set(fn.name, fn);
|
|
43
124
|
}
|
|
125
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
126
|
+
let methods = this.implMethods.get(implBlock.typeName);
|
|
127
|
+
if (!methods) {
|
|
128
|
+
methods = new Map();
|
|
129
|
+
this.implMethods.set(implBlock.typeName, methods);
|
|
130
|
+
}
|
|
131
|
+
for (const method of implBlock.methods) {
|
|
132
|
+
const selfIndex = method.params.findIndex(param => param.name === 'self');
|
|
133
|
+
if (selfIndex > 0) {
|
|
134
|
+
this.report(`Method '${method.name}' must declare 'self' as the first parameter`, method.params[selfIndex]);
|
|
135
|
+
}
|
|
136
|
+
if (selfIndex === 0) {
|
|
137
|
+
const selfType = this.normalizeType(method.params[0].type);
|
|
138
|
+
if (selfType.kind !== 'struct' || selfType.name !== implBlock.typeName) {
|
|
139
|
+
this.report(`Method '${method.name}' has invalid 'self' type`, method.params[0]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
methods.set(method.name, method);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
44
145
|
for (const struct of program.structs ?? []) {
|
|
45
146
|
const fields = new Map();
|
|
46
147
|
for (const field of struct.fields) {
|
|
@@ -67,6 +168,11 @@ class TypeChecker {
|
|
|
67
168
|
for (const fn of program.declarations) {
|
|
68
169
|
this.checkFunction(fn);
|
|
69
170
|
}
|
|
171
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
172
|
+
for (const method of implBlock.methods) {
|
|
173
|
+
this.checkFunction(method);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
70
176
|
return this.collector.getErrors();
|
|
71
177
|
}
|
|
72
178
|
checkFunction(fn) {
|
|
@@ -74,6 +180,7 @@ class TypeChecker {
|
|
|
74
180
|
this.currentReturnType = this.normalizeType(fn.returnType);
|
|
75
181
|
this.scope = new Map();
|
|
76
182
|
let seenDefault = false;
|
|
183
|
+
this.checkFunctionDecorators(fn);
|
|
77
184
|
for (const [name, type] of this.consts.entries()) {
|
|
78
185
|
this.scope.set(name, { type, mutable: false });
|
|
79
186
|
}
|
|
@@ -98,6 +205,37 @@ class TypeChecker {
|
|
|
98
205
|
this.currentFn = null;
|
|
99
206
|
this.currentReturnType = null;
|
|
100
207
|
}
|
|
208
|
+
checkFunctionDecorators(fn) {
|
|
209
|
+
const eventDecorators = fn.decorators.filter(decorator => decorator.name === 'on');
|
|
210
|
+
if (eventDecorators.length === 0) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (eventDecorators.length > 1) {
|
|
214
|
+
this.report(`Function '${fn.name}' cannot have multiple @on decorators`, fn);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const eventType = eventDecorators[0].args?.eventType;
|
|
218
|
+
if (!eventType) {
|
|
219
|
+
this.report(`Function '${fn.name}' is missing an event type in @on(...)`, fn);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!(0, types_1.isEventTypeName)(eventType)) {
|
|
223
|
+
this.report(`Unknown event type '${eventType}'`, fn);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const expectedParams = (0, types_1.getEventParamSpecs)(eventType);
|
|
227
|
+
if (fn.params.length !== expectedParams.length) {
|
|
228
|
+
this.report(`Event handler '${fn.name}' for ${eventType} must declare ${expectedParams.length} parameter(s), got ${fn.params.length}`, fn);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (let i = 0; i < expectedParams.length; i++) {
|
|
232
|
+
const actual = this.normalizeType(fn.params[i].type);
|
|
233
|
+
const expected = this.normalizeType(expectedParams[i].type);
|
|
234
|
+
if (!this.typesMatch(expected, actual)) {
|
|
235
|
+
this.report(`Event handler '${fn.name}' parameter ${i + 1} must be ${this.typeToString(expected)}, got ${this.typeToString(actual)}`, fn.params[i]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
101
239
|
checkBlock(stmts) {
|
|
102
240
|
for (const stmt of stmts) {
|
|
103
241
|
this.checkStmt(stmt);
|
|
@@ -113,9 +251,7 @@ class TypeChecker {
|
|
|
113
251
|
break;
|
|
114
252
|
case 'if':
|
|
115
253
|
this.checkExpr(stmt.cond);
|
|
116
|
-
this.
|
|
117
|
-
if (stmt.else_)
|
|
118
|
-
this.checkBlock(stmt.else_);
|
|
254
|
+
this.checkIfBranches(stmt);
|
|
119
255
|
break;
|
|
120
256
|
case 'while':
|
|
121
257
|
this.checkExpr(stmt.cond);
|
|
@@ -131,7 +267,16 @@ class TypeChecker {
|
|
|
131
267
|
case 'foreach':
|
|
132
268
|
this.checkExpr(stmt.iterable);
|
|
133
269
|
if (stmt.iterable.kind === 'selector') {
|
|
134
|
-
|
|
270
|
+
// Infer entity type from selector (access .sel for the EntitySelector)
|
|
271
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.iterable.sel);
|
|
272
|
+
this.scope.set(stmt.binding, {
|
|
273
|
+
type: { kind: 'entity', entityType },
|
|
274
|
+
mutable: false // Entity bindings are not reassignable
|
|
275
|
+
});
|
|
276
|
+
// Push self type context for @s inside the loop
|
|
277
|
+
this.pushSelfType(entityType);
|
|
278
|
+
this.checkBlock(stmt.body);
|
|
279
|
+
this.popSelfType();
|
|
135
280
|
}
|
|
136
281
|
else {
|
|
137
282
|
const iterableType = this.inferType(stmt.iterable);
|
|
@@ -141,8 +286,8 @@ class TypeChecker {
|
|
|
141
286
|
else {
|
|
142
287
|
this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true });
|
|
143
288
|
}
|
|
289
|
+
this.checkBlock(stmt.body);
|
|
144
290
|
}
|
|
145
|
-
this.checkBlock(stmt.body);
|
|
146
291
|
break;
|
|
147
292
|
case 'match':
|
|
148
293
|
this.checkExpr(stmt.expr);
|
|
@@ -156,15 +301,41 @@ class TypeChecker {
|
|
|
156
301
|
this.checkBlock(arm.body);
|
|
157
302
|
}
|
|
158
303
|
break;
|
|
159
|
-
case 'as_block':
|
|
304
|
+
case 'as_block': {
|
|
305
|
+
// as block changes @s to the selector's entity type
|
|
306
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.selector);
|
|
307
|
+
this.pushSelfType(entityType);
|
|
308
|
+
this.checkBlock(stmt.body);
|
|
309
|
+
this.popSelfType();
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
160
312
|
case 'at_block':
|
|
313
|
+
// at block doesn't change @s type, only position
|
|
161
314
|
this.checkBlock(stmt.body);
|
|
162
315
|
break;
|
|
163
|
-
case 'as_at':
|
|
316
|
+
case 'as_at': {
|
|
317
|
+
// as @x at @y - @s becomes the as selector's type
|
|
318
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.as_sel);
|
|
319
|
+
this.pushSelfType(entityType);
|
|
164
320
|
this.checkBlock(stmt.body);
|
|
321
|
+
this.popSelfType();
|
|
165
322
|
break;
|
|
323
|
+
}
|
|
166
324
|
case 'execute':
|
|
325
|
+
// execute with subcommands - check for 'as' subcommands
|
|
326
|
+
for (const sub of stmt.subcommands) {
|
|
327
|
+
if (sub.kind === 'as' && sub.selector) {
|
|
328
|
+
const entityType = this.inferEntityTypeFromSelector(sub.selector);
|
|
329
|
+
this.pushSelfType(entityType);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
167
332
|
this.checkBlock(stmt.body);
|
|
333
|
+
// Pop for each 'as' subcommand
|
|
334
|
+
for (const sub of stmt.subcommands) {
|
|
335
|
+
if (sub.kind === 'as') {
|
|
336
|
+
this.popSelfType();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
168
339
|
break;
|
|
169
340
|
case 'expr':
|
|
170
341
|
this.checkExpr(stmt.expr);
|
|
@@ -224,10 +395,21 @@ class TypeChecker {
|
|
|
224
395
|
case 'member':
|
|
225
396
|
this.checkMemberExpr(expr);
|
|
226
397
|
break;
|
|
398
|
+
case 'static_call':
|
|
399
|
+
this.checkStaticCallExpr(expr);
|
|
400
|
+
break;
|
|
227
401
|
case 'binary':
|
|
228
402
|
this.checkExpr(expr.left);
|
|
229
403
|
this.checkExpr(expr.right);
|
|
230
404
|
break;
|
|
405
|
+
case 'is_check': {
|
|
406
|
+
this.checkExpr(expr.expr);
|
|
407
|
+
const checkedType = this.inferType(expr.expr);
|
|
408
|
+
if (checkedType.kind !== 'entity') {
|
|
409
|
+
this.report(`'is' checks require an entity expression, got ${this.typeToString(checkedType)}`, expr.expr);
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
231
413
|
case 'unary':
|
|
232
414
|
this.checkExpr(expr.operand);
|
|
233
415
|
break;
|
|
@@ -264,6 +446,18 @@ class TypeChecker {
|
|
|
264
446
|
}
|
|
265
447
|
}
|
|
266
448
|
break;
|
|
449
|
+
case 'f_string':
|
|
450
|
+
for (const part of expr.parts) {
|
|
451
|
+
if (part.kind !== 'expr') {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
this.checkExpr(part.expr);
|
|
455
|
+
const partType = this.inferType(part.expr);
|
|
456
|
+
if (!(partType.kind === 'named' && (partType.name === 'int' || partType.name === 'string' || partType.name === 'format_string'))) {
|
|
457
|
+
this.report(`f-string placeholder must be int or string, got ${this.typeToString(partType)}`, part.expr);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
267
461
|
case 'array_lit':
|
|
268
462
|
for (const elem of expr.elements) {
|
|
269
463
|
this.checkExpr(elem);
|
|
@@ -274,11 +468,6 @@ class TypeChecker {
|
|
|
274
468
|
break;
|
|
275
469
|
case 'blockpos':
|
|
276
470
|
break;
|
|
277
|
-
case 'static_call':
|
|
278
|
-
for (const arg of expr.args) {
|
|
279
|
-
this.checkExpr(arg);
|
|
280
|
-
}
|
|
281
|
-
break;
|
|
282
471
|
// Literals don't need checking
|
|
283
472
|
case 'int_lit':
|
|
284
473
|
case 'float_lit':
|
|
@@ -298,6 +487,16 @@ class TypeChecker {
|
|
|
298
487
|
if (expr.fn === 'tp' || expr.fn === 'tp_to') {
|
|
299
488
|
this.checkTpCall(expr);
|
|
300
489
|
}
|
|
490
|
+
const richTextBuiltin = this.richTextBuiltins.get(expr.fn);
|
|
491
|
+
if (richTextBuiltin) {
|
|
492
|
+
this.checkRichTextBuiltinCall(expr, richTextBuiltin.messageIndex);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const builtin = BUILTIN_SIGNATURES[expr.fn];
|
|
496
|
+
if (builtin) {
|
|
497
|
+
this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
301
500
|
// Check if function exists and arg count matches
|
|
302
501
|
const fn = this.functions.get(expr.fn);
|
|
303
502
|
if (fn) {
|
|
@@ -325,11 +524,30 @@ class TypeChecker {
|
|
|
325
524
|
this.checkFunctionCallArgs(expr.args, varType.params, expr.fn, expr);
|
|
326
525
|
return;
|
|
327
526
|
}
|
|
527
|
+
const implMethod = this.resolveInstanceMethod(expr);
|
|
528
|
+
if (implMethod) {
|
|
529
|
+
this.checkFunctionCallArgs(expr.args, implMethod.params.map(param => this.normalizeType(param.type)), implMethod.name, expr);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
328
532
|
for (const arg of expr.args) {
|
|
329
533
|
this.checkExpr(arg);
|
|
330
534
|
}
|
|
331
535
|
// Built-in functions are not checked for arg count
|
|
332
536
|
}
|
|
537
|
+
checkRichTextBuiltinCall(expr, messageIndex) {
|
|
538
|
+
for (let i = 0; i < expr.args.length; i++) {
|
|
539
|
+
this.checkExpr(expr.args[i], i === messageIndex ? undefined : STRING_TYPE);
|
|
540
|
+
}
|
|
541
|
+
const message = expr.args[messageIndex];
|
|
542
|
+
if (!message) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const messageType = this.inferType(message);
|
|
546
|
+
if (messageType.kind !== 'named' ||
|
|
547
|
+
(messageType.name !== 'string' && messageType.name !== 'format_string')) {
|
|
548
|
+
this.report(`Argument ${messageIndex + 1} of '${expr.fn}' expects string or format_string, got ${this.typeToString(messageType)}`, message);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
333
551
|
checkInvokeExpr(expr) {
|
|
334
552
|
this.checkExpr(expr.callee);
|
|
335
553
|
const calleeType = this.inferType(expr.callee);
|
|
@@ -412,6 +630,21 @@ class TypeChecker {
|
|
|
412
630
|
}
|
|
413
631
|
}
|
|
414
632
|
}
|
|
633
|
+
checkStaticCallExpr(expr) {
|
|
634
|
+
const method = this.implMethods.get(expr.type)?.get(expr.method);
|
|
635
|
+
if (!method) {
|
|
636
|
+
this.report(`Type '${expr.type}' has no static method '${expr.method}'`, expr);
|
|
637
|
+
for (const arg of expr.args) {
|
|
638
|
+
this.checkExpr(arg);
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (method.params[0]?.name === 'self') {
|
|
643
|
+
this.report(`Method '${expr.type}::${expr.method}' is an instance method`, expr);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
this.checkFunctionCallArgs(expr.args, method.params.map(param => this.normalizeType(param.type)), `${expr.type}::${expr.method}`, expr);
|
|
647
|
+
}
|
|
415
648
|
checkLambdaExpr(expr, expectedType) {
|
|
416
649
|
const normalizedExpected = expectedType ? this.normalizeType(expectedType) : undefined;
|
|
417
650
|
const expectedFnType = normalizedExpected?.kind === 'function_type' ? normalizedExpected : undefined;
|
|
@@ -447,6 +680,37 @@ class TypeChecker {
|
|
|
447
680
|
this.scope = outerScope;
|
|
448
681
|
this.currentReturnType = outerReturnType;
|
|
449
682
|
}
|
|
683
|
+
checkIfBranches(stmt) {
|
|
684
|
+
const narrowed = this.getThenBranchNarrowing(stmt.cond);
|
|
685
|
+
if (narrowed) {
|
|
686
|
+
const thenScope = new Map(this.scope);
|
|
687
|
+
thenScope.set(narrowed.name, { type: narrowed.type, mutable: narrowed.mutable });
|
|
688
|
+
const outerScope = this.scope;
|
|
689
|
+
this.scope = thenScope;
|
|
690
|
+
this.checkBlock(stmt.then);
|
|
691
|
+
this.scope = outerScope;
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
this.checkBlock(stmt.then);
|
|
695
|
+
}
|
|
696
|
+
if (stmt.else_) {
|
|
697
|
+
this.checkBlock(stmt.else_);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
getThenBranchNarrowing(cond) {
|
|
701
|
+
if (cond.kind !== 'is_check' || cond.expr.kind !== 'ident') {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
const symbol = this.scope.get(cond.expr.name);
|
|
705
|
+
if (!symbol || symbol.type.kind !== 'entity') {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
name: cond.expr.name,
|
|
710
|
+
type: { kind: 'entity', entityType: cond.entityType },
|
|
711
|
+
mutable: symbol.mutable,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
450
714
|
inferType(expr, expectedType) {
|
|
451
715
|
switch (expr.kind) {
|
|
452
716
|
case 'int_lit':
|
|
@@ -473,13 +737,24 @@ class TypeChecker {
|
|
|
473
737
|
}
|
|
474
738
|
}
|
|
475
739
|
return { kind: 'named', name: 'string' };
|
|
740
|
+
case 'f_string':
|
|
741
|
+
for (const part of expr.parts) {
|
|
742
|
+
if (part.kind === 'expr') {
|
|
743
|
+
this.checkExpr(part.expr);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return FORMAT_STRING_TYPE;
|
|
476
747
|
case 'blockpos':
|
|
477
748
|
return { kind: 'named', name: 'BlockPos' };
|
|
478
749
|
case 'ident':
|
|
479
750
|
return this.scope.get(expr.name)?.type ?? { kind: 'named', name: 'void' };
|
|
480
751
|
case 'call': {
|
|
752
|
+
const builtin = BUILTIN_SIGNATURES[expr.fn];
|
|
753
|
+
if (builtin) {
|
|
754
|
+
return builtin.return;
|
|
755
|
+
}
|
|
481
756
|
if (expr.fn === '__array_push') {
|
|
482
|
-
return
|
|
757
|
+
return VOID_TYPE;
|
|
483
758
|
}
|
|
484
759
|
if (expr.fn === '__array_pop') {
|
|
485
760
|
const target = expr.args[0];
|
|
@@ -488,20 +763,28 @@ class TypeChecker {
|
|
|
488
763
|
if (targetType?.kind === 'array')
|
|
489
764
|
return targetType.elem;
|
|
490
765
|
}
|
|
491
|
-
return
|
|
766
|
+
return INT_TYPE;
|
|
492
767
|
}
|
|
493
768
|
if (expr.fn === 'bossbar_get_value') {
|
|
494
|
-
return
|
|
769
|
+
return INT_TYPE;
|
|
495
770
|
}
|
|
496
771
|
if (expr.fn === 'random_sequence') {
|
|
497
|
-
return
|
|
772
|
+
return VOID_TYPE;
|
|
498
773
|
}
|
|
499
774
|
const varType = this.scope.get(expr.fn)?.type;
|
|
500
775
|
if (varType?.kind === 'function_type') {
|
|
501
776
|
return varType.return;
|
|
502
777
|
}
|
|
778
|
+
const implMethod = this.resolveInstanceMethod(expr);
|
|
779
|
+
if (implMethod) {
|
|
780
|
+
return this.normalizeType(implMethod.returnType);
|
|
781
|
+
}
|
|
503
782
|
const fn = this.functions.get(expr.fn);
|
|
504
|
-
return fn?.returnType ??
|
|
783
|
+
return fn?.returnType ?? INT_TYPE;
|
|
784
|
+
}
|
|
785
|
+
case 'static_call': {
|
|
786
|
+
const method = this.implMethods.get(expr.type)?.get(expr.method);
|
|
787
|
+
return method ? this.normalizeType(method.returnType) : { kind: 'named', name: 'void' };
|
|
505
788
|
}
|
|
506
789
|
case 'invoke': {
|
|
507
790
|
const calleeType = this.inferType(expr.callee);
|
|
@@ -532,6 +815,8 @@ class TypeChecker {
|
|
|
532
815
|
return { kind: 'named', name: 'bool' };
|
|
533
816
|
}
|
|
534
817
|
return this.inferType(expr.left);
|
|
818
|
+
case 'is_check':
|
|
819
|
+
return { kind: 'named', name: 'bool' };
|
|
535
820
|
case 'unary':
|
|
536
821
|
if (expr.op === '!')
|
|
537
822
|
return { kind: 'named', name: 'bool' };
|
|
@@ -541,6 +826,14 @@ class TypeChecker {
|
|
|
541
826
|
return { kind: 'array', elem: this.inferType(expr.elements[0]) };
|
|
542
827
|
}
|
|
543
828
|
return { kind: 'array', elem: { kind: 'named', name: 'int' } };
|
|
829
|
+
case 'struct_lit':
|
|
830
|
+
if (expectedType) {
|
|
831
|
+
const normalized = this.normalizeType(expectedType);
|
|
832
|
+
if (normalized.kind === 'struct') {
|
|
833
|
+
return normalized;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return { kind: 'named', name: 'void' };
|
|
544
837
|
case 'lambda':
|
|
545
838
|
return this.inferLambdaType(expr, expectedType && this.normalizeType(expectedType).kind === 'function_type'
|
|
546
839
|
? this.normalizeType(expectedType)
|
|
@@ -569,6 +862,68 @@ class TypeChecker {
|
|
|
569
862
|
}
|
|
570
863
|
return { kind: 'function_type', params, return: returnType };
|
|
571
864
|
}
|
|
865
|
+
// ---------------------------------------------------------------------------
|
|
866
|
+
// Entity Type Helpers
|
|
867
|
+
// ---------------------------------------------------------------------------
|
|
868
|
+
/** Infer entity type from a selector */
|
|
869
|
+
inferEntityTypeFromSelector(selector) {
|
|
870
|
+
// @a, @p, @r always return Player
|
|
871
|
+
if (selector.kind === '@a' || selector.kind === '@p' || selector.kind === '@r') {
|
|
872
|
+
return 'Player';
|
|
873
|
+
}
|
|
874
|
+
// @e or @s with type= filter
|
|
875
|
+
if (selector.filters?.type) {
|
|
876
|
+
const mcType = selector.filters.type.toLowerCase();
|
|
877
|
+
return MC_TYPE_TO_ENTITY[mcType] ?? 'entity';
|
|
878
|
+
}
|
|
879
|
+
// @s uses current context
|
|
880
|
+
if (selector.kind === '@s') {
|
|
881
|
+
return this.selfTypeStack[this.selfTypeStack.length - 1];
|
|
882
|
+
}
|
|
883
|
+
// Default to entity
|
|
884
|
+
return 'entity';
|
|
885
|
+
}
|
|
886
|
+
resolveInstanceMethod(expr) {
|
|
887
|
+
const receiver = expr.args[0];
|
|
888
|
+
if (!receiver) {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
const receiverType = this.inferType(receiver);
|
|
892
|
+
if (receiverType.kind !== 'struct') {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
const method = this.implMethods.get(receiverType.name)?.get(expr.fn);
|
|
896
|
+
if (!method || method.params[0]?.name !== 'self') {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
return method;
|
|
900
|
+
}
|
|
901
|
+
/** Check if childType is a subtype of parentType */
|
|
902
|
+
isEntitySubtype(childType, parentType) {
|
|
903
|
+
if (childType === parentType)
|
|
904
|
+
return true;
|
|
905
|
+
let current = childType;
|
|
906
|
+
while (current !== null) {
|
|
907
|
+
if (current === parentType)
|
|
908
|
+
return true;
|
|
909
|
+
current = ENTITY_HIERARCHY[current];
|
|
910
|
+
}
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
/** Push a new self type context */
|
|
914
|
+
pushSelfType(entityType) {
|
|
915
|
+
this.selfTypeStack.push(entityType);
|
|
916
|
+
}
|
|
917
|
+
/** Pop self type context */
|
|
918
|
+
popSelfType() {
|
|
919
|
+
if (this.selfTypeStack.length > 1) {
|
|
920
|
+
this.selfTypeStack.pop();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/** Get current @s type */
|
|
924
|
+
getCurrentSelfType() {
|
|
925
|
+
return this.selfTypeStack[this.selfTypeStack.length - 1];
|
|
926
|
+
}
|
|
572
927
|
typesMatch(expected, actual) {
|
|
573
928
|
if (expected.kind !== actual.kind)
|
|
574
929
|
return false;
|
|
@@ -592,6 +947,14 @@ class TypeChecker {
|
|
|
592
947
|
expected.params.every((param, index) => this.typesMatch(param, actual.params[index])) &&
|
|
593
948
|
this.typesMatch(expected.return, actual.return);
|
|
594
949
|
}
|
|
950
|
+
// Entity type matching with subtype support
|
|
951
|
+
if (expected.kind === 'entity' && actual.kind === 'entity') {
|
|
952
|
+
return this.isEntitySubtype(actual.entityType, expected.entityType);
|
|
953
|
+
}
|
|
954
|
+
// Selector matches any entity type
|
|
955
|
+
if (expected.kind === 'selector' && actual.kind === 'entity') {
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
595
958
|
return false;
|
|
596
959
|
}
|
|
597
960
|
typeToString(type) {
|
|
@@ -606,6 +969,12 @@ class TypeChecker {
|
|
|
606
969
|
return type.name;
|
|
607
970
|
case 'function_type':
|
|
608
971
|
return `(${type.params.map(param => this.typeToString(param)).join(', ')}) -> ${this.typeToString(type.return)}`;
|
|
972
|
+
case 'entity':
|
|
973
|
+
return type.entityType;
|
|
974
|
+
case 'selector':
|
|
975
|
+
return 'selector';
|
|
976
|
+
default:
|
|
977
|
+
return 'unknown';
|
|
609
978
|
}
|
|
610
979
|
}
|
|
611
980
|
normalizeType(type) {
|
|
@@ -622,6 +991,12 @@ class TypeChecker {
|
|
|
622
991
|
if ((type.kind === 'struct' || type.kind === 'enum') && this.enums.has(type.name)) {
|
|
623
992
|
return { kind: 'enum', name: type.name };
|
|
624
993
|
}
|
|
994
|
+
if (type.kind === 'struct' && type.name in ENTITY_HIERARCHY) {
|
|
995
|
+
return { kind: 'entity', entityType: type.name };
|
|
996
|
+
}
|
|
997
|
+
if (type.kind === 'named' && type.name in ENTITY_HIERARCHY) {
|
|
998
|
+
return { kind: 'entity', entityType: type.name };
|
|
999
|
+
}
|
|
625
1000
|
return type;
|
|
626
1001
|
}
|
|
627
1002
|
}
|