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
package/src/typechecker/index.ts
CHANGED
|
@@ -5,14 +5,92 @@
|
|
|
5
5
|
* Collects errors but doesn't block compilation (warn mode).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Program, FnDecl, Stmt, Expr, TypeNode, Block } from '../ast/types'
|
|
8
|
+
import type { Program, FnDecl, Stmt, Expr, TypeNode, Block, EntityTypeName, EntitySelector } from '../ast/types'
|
|
9
9
|
import { DiagnosticError, DiagnosticCollector } from '../diagnostics'
|
|
10
|
+
import { getEventParamSpecs, isEventTypeName } from '../events/types'
|
|
10
11
|
|
|
11
12
|
interface ScopeSymbol {
|
|
12
13
|
type: TypeNode
|
|
13
14
|
mutable: boolean
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
interface BuiltinSignature {
|
|
18
|
+
params: TypeNode[]
|
|
19
|
+
return: TypeNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Entity type hierarchy for subtype checking
|
|
23
|
+
const ENTITY_HIERARCHY: Record<EntityTypeName, EntityTypeName | null> = {
|
|
24
|
+
'entity': null,
|
|
25
|
+
'Player': 'entity',
|
|
26
|
+
'Mob': 'entity',
|
|
27
|
+
'HostileMob': 'Mob',
|
|
28
|
+
'PassiveMob': 'Mob',
|
|
29
|
+
'Zombie': 'HostileMob',
|
|
30
|
+
'Skeleton': 'HostileMob',
|
|
31
|
+
'Creeper': 'HostileMob',
|
|
32
|
+
'Spider': 'HostileMob',
|
|
33
|
+
'Enderman': 'HostileMob',
|
|
34
|
+
'Pig': 'PassiveMob',
|
|
35
|
+
'Cow': 'PassiveMob',
|
|
36
|
+
'Sheep': 'PassiveMob',
|
|
37
|
+
'Chicken': 'PassiveMob',
|
|
38
|
+
'Villager': 'PassiveMob',
|
|
39
|
+
'ArmorStand': 'entity',
|
|
40
|
+
'Item': 'entity',
|
|
41
|
+
'Arrow': 'entity',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Map Minecraft type names to entity types
|
|
45
|
+
const MC_TYPE_TO_ENTITY: Record<string, EntityTypeName> = {
|
|
46
|
+
'zombie': 'Zombie',
|
|
47
|
+
'minecraft:zombie': 'Zombie',
|
|
48
|
+
'skeleton': 'Skeleton',
|
|
49
|
+
'minecraft:skeleton': 'Skeleton',
|
|
50
|
+
'creeper': 'Creeper',
|
|
51
|
+
'minecraft:creeper': 'Creeper',
|
|
52
|
+
'spider': 'Spider',
|
|
53
|
+
'minecraft:spider': 'Spider',
|
|
54
|
+
'enderman': 'Enderman',
|
|
55
|
+
'minecraft:enderman': 'Enderman',
|
|
56
|
+
'pig': 'Pig',
|
|
57
|
+
'minecraft:pig': 'Pig',
|
|
58
|
+
'cow': 'Cow',
|
|
59
|
+
'minecraft:cow': 'Cow',
|
|
60
|
+
'sheep': 'Sheep',
|
|
61
|
+
'minecraft:sheep': 'Sheep',
|
|
62
|
+
'chicken': 'Chicken',
|
|
63
|
+
'minecraft:chicken': 'Chicken',
|
|
64
|
+
'villager': 'Villager',
|
|
65
|
+
'minecraft:villager': 'Villager',
|
|
66
|
+
'armor_stand': 'ArmorStand',
|
|
67
|
+
'minecraft:armor_stand': 'ArmorStand',
|
|
68
|
+
'item': 'Item',
|
|
69
|
+
'minecraft:item': 'Item',
|
|
70
|
+
'arrow': 'Arrow',
|
|
71
|
+
'minecraft:arrow': 'Arrow',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const VOID_TYPE: TypeNode = { kind: 'named', name: 'void' }
|
|
75
|
+
const INT_TYPE: TypeNode = { kind: 'named', name: 'int' }
|
|
76
|
+
const STRING_TYPE: TypeNode = { kind: 'named', name: 'string' }
|
|
77
|
+
const FORMAT_STRING_TYPE: TypeNode = { kind: 'named', name: 'format_string' }
|
|
78
|
+
|
|
79
|
+
const BUILTIN_SIGNATURES: Record<string, BuiltinSignature> = {
|
|
80
|
+
setTimeout: {
|
|
81
|
+
params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
|
|
82
|
+
return: VOID_TYPE,
|
|
83
|
+
},
|
|
84
|
+
setInterval: {
|
|
85
|
+
params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
|
|
86
|
+
return: INT_TYPE,
|
|
87
|
+
},
|
|
88
|
+
clearInterval: {
|
|
89
|
+
params: [INT_TYPE],
|
|
90
|
+
return: VOID_TYPE,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
16
94
|
// ---------------------------------------------------------------------------
|
|
17
95
|
// Type Checker
|
|
18
96
|
// ---------------------------------------------------------------------------
|
|
@@ -20,12 +98,25 @@ interface ScopeSymbol {
|
|
|
20
98
|
export class TypeChecker {
|
|
21
99
|
private collector: DiagnosticCollector
|
|
22
100
|
private functions: Map<string, FnDecl> = new Map()
|
|
101
|
+
private implMethods: Map<string, Map<string, FnDecl>> = new Map()
|
|
23
102
|
private structs: Map<string, Map<string, TypeNode>> = new Map()
|
|
24
103
|
private enums: Map<string, Map<string, number>> = new Map()
|
|
25
104
|
private consts: Map<string, TypeNode> = new Map()
|
|
26
105
|
private currentFn: FnDecl | null = null
|
|
27
106
|
private currentReturnType: TypeNode | null = null
|
|
28
107
|
private scope: Map<string, ScopeSymbol> = new Map()
|
|
108
|
+
// Stack for tracking @s type in different contexts
|
|
109
|
+
private selfTypeStack: EntityTypeName[] = ['entity']
|
|
110
|
+
|
|
111
|
+
private readonly richTextBuiltins = new Map<string, { messageIndex: number }>([
|
|
112
|
+
['say', { messageIndex: 0 }],
|
|
113
|
+
['announce', { messageIndex: 0 }],
|
|
114
|
+
['tell', { messageIndex: 1 }],
|
|
115
|
+
['tellraw', { messageIndex: 1 }],
|
|
116
|
+
['title', { messageIndex: 1 }],
|
|
117
|
+
['actionbar', { messageIndex: 1 }],
|
|
118
|
+
['subtitle', { messageIndex: 1 }],
|
|
119
|
+
])
|
|
29
120
|
|
|
30
121
|
constructor(source?: string, filePath?: string) {
|
|
31
122
|
this.collector = new DiagnosticCollector(source, filePath)
|
|
@@ -53,6 +144,28 @@ export class TypeChecker {
|
|
|
53
144
|
this.functions.set(fn.name, fn)
|
|
54
145
|
}
|
|
55
146
|
|
|
147
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
148
|
+
let methods = this.implMethods.get(implBlock.typeName)
|
|
149
|
+
if (!methods) {
|
|
150
|
+
methods = new Map()
|
|
151
|
+
this.implMethods.set(implBlock.typeName, methods)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const method of implBlock.methods) {
|
|
155
|
+
const selfIndex = method.params.findIndex(param => param.name === 'self')
|
|
156
|
+
if (selfIndex > 0) {
|
|
157
|
+
this.report(`Method '${method.name}' must declare 'self' as the first parameter`, method.params[selfIndex])
|
|
158
|
+
}
|
|
159
|
+
if (selfIndex === 0) {
|
|
160
|
+
const selfType = this.normalizeType(method.params[0].type)
|
|
161
|
+
if (selfType.kind !== 'struct' || selfType.name !== implBlock.typeName) {
|
|
162
|
+
this.report(`Method '${method.name}' has invalid 'self' type`, method.params[0])
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
methods.set(method.name, method)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
56
169
|
for (const struct of program.structs ?? []) {
|
|
57
170
|
const fields = new Map<string, TypeNode>()
|
|
58
171
|
for (const field of struct.fields) {
|
|
@@ -86,6 +199,12 @@ export class TypeChecker {
|
|
|
86
199
|
this.checkFunction(fn)
|
|
87
200
|
}
|
|
88
201
|
|
|
202
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
203
|
+
for (const method of implBlock.methods) {
|
|
204
|
+
this.checkFunction(method)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
89
208
|
return this.collector.getErrors()
|
|
90
209
|
}
|
|
91
210
|
|
|
@@ -95,6 +214,8 @@ export class TypeChecker {
|
|
|
95
214
|
this.scope = new Map()
|
|
96
215
|
let seenDefault = false
|
|
97
216
|
|
|
217
|
+
this.checkFunctionDecorators(fn)
|
|
218
|
+
|
|
98
219
|
for (const [name, type] of this.consts.entries()) {
|
|
99
220
|
this.scope.set(name, { type, mutable: false })
|
|
100
221
|
}
|
|
@@ -125,6 +246,49 @@ export class TypeChecker {
|
|
|
125
246
|
this.currentReturnType = null
|
|
126
247
|
}
|
|
127
248
|
|
|
249
|
+
private checkFunctionDecorators(fn: FnDecl): void {
|
|
250
|
+
const eventDecorators = fn.decorators.filter(decorator => decorator.name === 'on')
|
|
251
|
+
if (eventDecorators.length === 0) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (eventDecorators.length > 1) {
|
|
256
|
+
this.report(`Function '${fn.name}' cannot have multiple @on decorators`, fn)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const eventType = eventDecorators[0].args?.eventType
|
|
261
|
+
if (!eventType) {
|
|
262
|
+
this.report(`Function '${fn.name}' is missing an event type in @on(...)`, fn)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!isEventTypeName(eventType)) {
|
|
267
|
+
this.report(`Unknown event type '${eventType}'`, fn)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const expectedParams = getEventParamSpecs(eventType)
|
|
272
|
+
if (fn.params.length !== expectedParams.length) {
|
|
273
|
+
this.report(
|
|
274
|
+
`Event handler '${fn.name}' for ${eventType} must declare ${expectedParams.length} parameter(s), got ${fn.params.length}`,
|
|
275
|
+
fn
|
|
276
|
+
)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (let i = 0; i < expectedParams.length; i++) {
|
|
281
|
+
const actual = this.normalizeType(fn.params[i].type)
|
|
282
|
+
const expected = this.normalizeType(expectedParams[i].type)
|
|
283
|
+
if (!this.typesMatch(expected, actual)) {
|
|
284
|
+
this.report(
|
|
285
|
+
`Event handler '${fn.name}' parameter ${i + 1} must be ${this.typeToString(expected)}, got ${this.typeToString(actual)}`,
|
|
286
|
+
fn.params[i]
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
128
292
|
private checkBlock(stmts: Block): void {
|
|
129
293
|
for (const stmt of stmts) {
|
|
130
294
|
this.checkStmt(stmt)
|
|
@@ -141,8 +305,7 @@ export class TypeChecker {
|
|
|
141
305
|
break
|
|
142
306
|
case 'if':
|
|
143
307
|
this.checkExpr(stmt.cond)
|
|
144
|
-
this.
|
|
145
|
-
if (stmt.else_) this.checkBlock(stmt.else_)
|
|
308
|
+
this.checkIfBranches(stmt)
|
|
146
309
|
break
|
|
147
310
|
case 'while':
|
|
148
311
|
this.checkExpr(stmt.cond)
|
|
@@ -157,7 +320,16 @@ export class TypeChecker {
|
|
|
157
320
|
case 'foreach':
|
|
158
321
|
this.checkExpr(stmt.iterable)
|
|
159
322
|
if (stmt.iterable.kind === 'selector') {
|
|
160
|
-
|
|
323
|
+
// Infer entity type from selector (access .sel for the EntitySelector)
|
|
324
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.iterable.sel)
|
|
325
|
+
this.scope.set(stmt.binding, {
|
|
326
|
+
type: { kind: 'entity', entityType },
|
|
327
|
+
mutable: false // Entity bindings are not reassignable
|
|
328
|
+
})
|
|
329
|
+
// Push self type context for @s inside the loop
|
|
330
|
+
this.pushSelfType(entityType)
|
|
331
|
+
this.checkBlock(stmt.body)
|
|
332
|
+
this.popSelfType()
|
|
161
333
|
} else {
|
|
162
334
|
const iterableType = this.inferType(stmt.iterable)
|
|
163
335
|
if (iterableType.kind === 'array') {
|
|
@@ -165,8 +337,8 @@ export class TypeChecker {
|
|
|
165
337
|
} else {
|
|
166
338
|
this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true })
|
|
167
339
|
}
|
|
340
|
+
this.checkBlock(stmt.body)
|
|
168
341
|
}
|
|
169
|
-
this.checkBlock(stmt.body)
|
|
170
342
|
break
|
|
171
343
|
case 'match':
|
|
172
344
|
this.checkExpr(stmt.expr)
|
|
@@ -180,15 +352,41 @@ export class TypeChecker {
|
|
|
180
352
|
this.checkBlock(arm.body)
|
|
181
353
|
}
|
|
182
354
|
break
|
|
183
|
-
case 'as_block':
|
|
355
|
+
case 'as_block': {
|
|
356
|
+
// as block changes @s to the selector's entity type
|
|
357
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.selector)
|
|
358
|
+
this.pushSelfType(entityType)
|
|
359
|
+
this.checkBlock(stmt.body)
|
|
360
|
+
this.popSelfType()
|
|
361
|
+
break
|
|
362
|
+
}
|
|
184
363
|
case 'at_block':
|
|
364
|
+
// at block doesn't change @s type, only position
|
|
185
365
|
this.checkBlock(stmt.body)
|
|
186
366
|
break
|
|
187
|
-
case 'as_at':
|
|
367
|
+
case 'as_at': {
|
|
368
|
+
// as @x at @y - @s becomes the as selector's type
|
|
369
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.as_sel)
|
|
370
|
+
this.pushSelfType(entityType)
|
|
188
371
|
this.checkBlock(stmt.body)
|
|
372
|
+
this.popSelfType()
|
|
189
373
|
break
|
|
374
|
+
}
|
|
190
375
|
case 'execute':
|
|
376
|
+
// execute with subcommands - check for 'as' subcommands
|
|
377
|
+
for (const sub of stmt.subcommands) {
|
|
378
|
+
if (sub.kind === 'as' && sub.selector) {
|
|
379
|
+
const entityType = this.inferEntityTypeFromSelector(sub.selector)
|
|
380
|
+
this.pushSelfType(entityType)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
191
383
|
this.checkBlock(stmt.body)
|
|
384
|
+
// Pop for each 'as' subcommand
|
|
385
|
+
for (const sub of stmt.subcommands) {
|
|
386
|
+
if (sub.kind === 'as') {
|
|
387
|
+
this.popSelfType()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
192
390
|
break
|
|
193
391
|
case 'expr':
|
|
194
392
|
this.checkExpr(stmt.expr)
|
|
@@ -265,12 +463,24 @@ export class TypeChecker {
|
|
|
265
463
|
case 'member':
|
|
266
464
|
this.checkMemberExpr(expr)
|
|
267
465
|
break
|
|
466
|
+
case 'static_call':
|
|
467
|
+
this.checkStaticCallExpr(expr)
|
|
468
|
+
break
|
|
268
469
|
|
|
269
470
|
case 'binary':
|
|
270
471
|
this.checkExpr(expr.left)
|
|
271
472
|
this.checkExpr(expr.right)
|
|
272
473
|
break
|
|
273
474
|
|
|
475
|
+
case 'is_check': {
|
|
476
|
+
this.checkExpr(expr.expr)
|
|
477
|
+
const checkedType = this.inferType(expr.expr)
|
|
478
|
+
if (checkedType.kind !== 'entity') {
|
|
479
|
+
this.report(`'is' checks require an entity expression, got ${this.typeToString(checkedType)}`, expr.expr)
|
|
480
|
+
}
|
|
481
|
+
break
|
|
482
|
+
}
|
|
483
|
+
|
|
274
484
|
case 'unary':
|
|
275
485
|
this.checkExpr(expr.operand)
|
|
276
486
|
break
|
|
@@ -312,6 +522,24 @@ export class TypeChecker {
|
|
|
312
522
|
}
|
|
313
523
|
break
|
|
314
524
|
|
|
525
|
+
case 'f_string':
|
|
526
|
+
for (const part of expr.parts) {
|
|
527
|
+
if (part.kind !== 'expr') {
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
this.checkExpr(part.expr)
|
|
531
|
+
const partType = this.inferType(part.expr)
|
|
532
|
+
if (
|
|
533
|
+
!(partType.kind === 'named' && (partType.name === 'int' || partType.name === 'string' || partType.name === 'format_string'))
|
|
534
|
+
) {
|
|
535
|
+
this.report(
|
|
536
|
+
`f-string placeholder must be int or string, got ${this.typeToString(partType)}`,
|
|
537
|
+
part.expr
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
break
|
|
542
|
+
|
|
315
543
|
case 'array_lit':
|
|
316
544
|
for (const elem of expr.elements) {
|
|
317
545
|
this.checkExpr(elem)
|
|
@@ -325,12 +553,6 @@ export class TypeChecker {
|
|
|
325
553
|
case 'blockpos':
|
|
326
554
|
break
|
|
327
555
|
|
|
328
|
-
case 'static_call':
|
|
329
|
-
for (const arg of expr.args) {
|
|
330
|
-
this.checkExpr(arg)
|
|
331
|
-
}
|
|
332
|
-
break
|
|
333
|
-
|
|
334
556
|
// Literals don't need checking
|
|
335
557
|
case 'int_lit':
|
|
336
558
|
case 'float_lit':
|
|
@@ -352,6 +574,18 @@ export class TypeChecker {
|
|
|
352
574
|
this.checkTpCall(expr)
|
|
353
575
|
}
|
|
354
576
|
|
|
577
|
+
const richTextBuiltin = this.richTextBuiltins.get(expr.fn)
|
|
578
|
+
if (richTextBuiltin) {
|
|
579
|
+
this.checkRichTextBuiltinCall(expr, richTextBuiltin.messageIndex)
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const builtin = BUILTIN_SIGNATURES[expr.fn]
|
|
584
|
+
if (builtin) {
|
|
585
|
+
this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr)
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
|
|
355
589
|
// Check if function exists and arg count matches
|
|
356
590
|
const fn = this.functions.get(expr.fn)
|
|
357
591
|
if (fn) {
|
|
@@ -387,12 +621,45 @@ export class TypeChecker {
|
|
|
387
621
|
return
|
|
388
622
|
}
|
|
389
623
|
|
|
624
|
+
const implMethod = this.resolveInstanceMethod(expr)
|
|
625
|
+
if (implMethod) {
|
|
626
|
+
this.checkFunctionCallArgs(
|
|
627
|
+
expr.args,
|
|
628
|
+
implMethod.params.map(param => this.normalizeType(param.type)),
|
|
629
|
+
implMethod.name,
|
|
630
|
+
expr
|
|
631
|
+
)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
|
|
390
635
|
for (const arg of expr.args) {
|
|
391
636
|
this.checkExpr(arg)
|
|
392
637
|
}
|
|
393
638
|
// Built-in functions are not checked for arg count
|
|
394
639
|
}
|
|
395
640
|
|
|
641
|
+
private checkRichTextBuiltinCall(expr: Extract<Expr, { kind: 'call' }>, messageIndex: number): void {
|
|
642
|
+
for (let i = 0; i < expr.args.length; i++) {
|
|
643
|
+
this.checkExpr(expr.args[i], i === messageIndex ? undefined : STRING_TYPE)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const message = expr.args[messageIndex]
|
|
647
|
+
if (!message) {
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const messageType = this.inferType(message)
|
|
652
|
+
if (
|
|
653
|
+
messageType.kind !== 'named' ||
|
|
654
|
+
(messageType.name !== 'string' && messageType.name !== 'format_string')
|
|
655
|
+
) {
|
|
656
|
+
this.report(
|
|
657
|
+
`Argument ${messageIndex + 1} of '${expr.fn}' expects string or format_string, got ${this.typeToString(messageType)}`,
|
|
658
|
+
message
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
396
663
|
private checkInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): void {
|
|
397
664
|
this.checkExpr(expr.callee)
|
|
398
665
|
const calleeType = this.inferType(expr.callee)
|
|
@@ -497,6 +764,29 @@ export class TypeChecker {
|
|
|
497
764
|
}
|
|
498
765
|
}
|
|
499
766
|
|
|
767
|
+
private checkStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): void {
|
|
768
|
+
const method = this.implMethods.get(expr.type)?.get(expr.method)
|
|
769
|
+
if (!method) {
|
|
770
|
+
this.report(`Type '${expr.type}' has no static method '${expr.method}'`, expr)
|
|
771
|
+
for (const arg of expr.args) {
|
|
772
|
+
this.checkExpr(arg)
|
|
773
|
+
}
|
|
774
|
+
return
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (method.params[0]?.name === 'self') {
|
|
778
|
+
this.report(`Method '${expr.type}::${expr.method}' is an instance method`, expr)
|
|
779
|
+
return
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
this.checkFunctionCallArgs(
|
|
783
|
+
expr.args,
|
|
784
|
+
method.params.map(param => this.normalizeType(param.type)),
|
|
785
|
+
`${expr.type}::${expr.method}`,
|
|
786
|
+
expr
|
|
787
|
+
)
|
|
788
|
+
}
|
|
789
|
+
|
|
500
790
|
private checkLambdaExpr(expr: Extract<Expr, { kind: 'lambda' }>, expectedType?: TypeNode): void {
|
|
501
791
|
const normalizedExpected = expectedType ? this.normalizeType(expectedType) : undefined
|
|
502
792
|
const expectedFnType = normalizedExpected?.kind === 'function_type' ? normalizedExpected : undefined
|
|
@@ -544,6 +834,42 @@ export class TypeChecker {
|
|
|
544
834
|
this.currentReturnType = outerReturnType
|
|
545
835
|
}
|
|
546
836
|
|
|
837
|
+
private checkIfBranches(stmt: Extract<Stmt, { kind: 'if' }>): void {
|
|
838
|
+
const narrowed = this.getThenBranchNarrowing(stmt.cond)
|
|
839
|
+
|
|
840
|
+
if (narrowed) {
|
|
841
|
+
const thenScope = new Map(this.scope)
|
|
842
|
+
thenScope.set(narrowed.name, { type: narrowed.type, mutable: narrowed.mutable })
|
|
843
|
+
const outerScope = this.scope
|
|
844
|
+
this.scope = thenScope
|
|
845
|
+
this.checkBlock(stmt.then)
|
|
846
|
+
this.scope = outerScope
|
|
847
|
+
} else {
|
|
848
|
+
this.checkBlock(stmt.then)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (stmt.else_) {
|
|
852
|
+
this.checkBlock(stmt.else_)
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
private getThenBranchNarrowing(cond: Expr): { name: string; type: Extract<TypeNode, { kind: 'entity' }>; mutable: boolean } | null {
|
|
857
|
+
if (cond.kind !== 'is_check' || cond.expr.kind !== 'ident') {
|
|
858
|
+
return null
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const symbol = this.scope.get(cond.expr.name)
|
|
862
|
+
if (!symbol || symbol.type.kind !== 'entity') {
|
|
863
|
+
return null
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
name: cond.expr.name,
|
|
868
|
+
type: { kind: 'entity', entityType: cond.entityType },
|
|
869
|
+
mutable: symbol.mutable,
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
547
873
|
private inferType(expr: Expr, expectedType?: TypeNode): TypeNode {
|
|
548
874
|
switch (expr.kind) {
|
|
549
875
|
case 'int_lit':
|
|
@@ -570,13 +896,24 @@ export class TypeChecker {
|
|
|
570
896
|
}
|
|
571
897
|
}
|
|
572
898
|
return { kind: 'named', name: 'string' }
|
|
899
|
+
case 'f_string':
|
|
900
|
+
for (const part of expr.parts) {
|
|
901
|
+
if (part.kind === 'expr') {
|
|
902
|
+
this.checkExpr(part.expr)
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return FORMAT_STRING_TYPE
|
|
573
906
|
case 'blockpos':
|
|
574
907
|
return { kind: 'named', name: 'BlockPos' }
|
|
575
908
|
case 'ident':
|
|
576
909
|
return this.scope.get(expr.name)?.type ?? { kind: 'named', name: 'void' }
|
|
577
910
|
case 'call': {
|
|
911
|
+
const builtin = BUILTIN_SIGNATURES[expr.fn]
|
|
912
|
+
if (builtin) {
|
|
913
|
+
return builtin.return
|
|
914
|
+
}
|
|
578
915
|
if (expr.fn === '__array_push') {
|
|
579
|
-
return
|
|
916
|
+
return VOID_TYPE
|
|
580
917
|
}
|
|
581
918
|
if (expr.fn === '__array_pop') {
|
|
582
919
|
const target = expr.args[0]
|
|
@@ -584,20 +921,28 @@ export class TypeChecker {
|
|
|
584
921
|
const targetType = this.scope.get(target.name)?.type
|
|
585
922
|
if (targetType?.kind === 'array') return targetType.elem
|
|
586
923
|
}
|
|
587
|
-
return
|
|
924
|
+
return INT_TYPE
|
|
588
925
|
}
|
|
589
926
|
if (expr.fn === 'bossbar_get_value') {
|
|
590
|
-
return
|
|
927
|
+
return INT_TYPE
|
|
591
928
|
}
|
|
592
929
|
if (expr.fn === 'random_sequence') {
|
|
593
|
-
return
|
|
930
|
+
return VOID_TYPE
|
|
594
931
|
}
|
|
595
932
|
const varType = this.scope.get(expr.fn)?.type
|
|
596
933
|
if (varType?.kind === 'function_type') {
|
|
597
934
|
return varType.return
|
|
598
935
|
}
|
|
936
|
+
const implMethod = this.resolveInstanceMethod(expr)
|
|
937
|
+
if (implMethod) {
|
|
938
|
+
return this.normalizeType(implMethod.returnType)
|
|
939
|
+
}
|
|
599
940
|
const fn = this.functions.get(expr.fn)
|
|
600
|
-
return fn?.returnType ??
|
|
941
|
+
return fn?.returnType ?? INT_TYPE
|
|
942
|
+
}
|
|
943
|
+
case 'static_call': {
|
|
944
|
+
const method = this.implMethods.get(expr.type)?.get(expr.method)
|
|
945
|
+
return method ? this.normalizeType(method.returnType) : { kind: 'named', name: 'void' }
|
|
601
946
|
}
|
|
602
947
|
case 'invoke': {
|
|
603
948
|
const calleeType = this.inferType(expr.callee)
|
|
@@ -627,6 +972,8 @@ export class TypeChecker {
|
|
|
627
972
|
return { kind: 'named', name: 'bool' }
|
|
628
973
|
}
|
|
629
974
|
return this.inferType(expr.left)
|
|
975
|
+
case 'is_check':
|
|
976
|
+
return { kind: 'named', name: 'bool' }
|
|
630
977
|
case 'unary':
|
|
631
978
|
if (expr.op === '!') return { kind: 'named', name: 'bool' }
|
|
632
979
|
return this.inferType(expr.operand)
|
|
@@ -635,6 +982,14 @@ export class TypeChecker {
|
|
|
635
982
|
return { kind: 'array', elem: this.inferType(expr.elements[0]) }
|
|
636
983
|
}
|
|
637
984
|
return { kind: 'array', elem: { kind: 'named', name: 'int' } }
|
|
985
|
+
case 'struct_lit':
|
|
986
|
+
if (expectedType) {
|
|
987
|
+
const normalized = this.normalizeType(expectedType)
|
|
988
|
+
if (normalized.kind === 'struct') {
|
|
989
|
+
return normalized
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return { kind: 'named', name: 'void' }
|
|
638
993
|
case 'lambda':
|
|
639
994
|
return this.inferLambdaType(
|
|
640
995
|
expr,
|
|
@@ -673,6 +1028,80 @@ export class TypeChecker {
|
|
|
673
1028
|
return { kind: 'function_type', params, return: returnType }
|
|
674
1029
|
}
|
|
675
1030
|
|
|
1031
|
+
// ---------------------------------------------------------------------------
|
|
1032
|
+
// Entity Type Helpers
|
|
1033
|
+
// ---------------------------------------------------------------------------
|
|
1034
|
+
|
|
1035
|
+
/** Infer entity type from a selector */
|
|
1036
|
+
private inferEntityTypeFromSelector(selector: EntitySelector): EntityTypeName {
|
|
1037
|
+
// @a, @p, @r always return Player
|
|
1038
|
+
if (selector.kind === '@a' || selector.kind === '@p' || selector.kind === '@r') {
|
|
1039
|
+
return 'Player'
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// @e or @s with type= filter
|
|
1043
|
+
if (selector.filters?.type) {
|
|
1044
|
+
const mcType = selector.filters.type.toLowerCase()
|
|
1045
|
+
return MC_TYPE_TO_ENTITY[mcType] ?? 'entity'
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// @s uses current context
|
|
1049
|
+
if (selector.kind === '@s') {
|
|
1050
|
+
return this.selfTypeStack[this.selfTypeStack.length - 1]
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Default to entity
|
|
1054
|
+
return 'entity'
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private resolveInstanceMethod(expr: Extract<Expr, { kind: 'call' }>): FnDecl | null {
|
|
1058
|
+
const receiver = expr.args[0]
|
|
1059
|
+
if (!receiver) {
|
|
1060
|
+
return null
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const receiverType = this.inferType(receiver)
|
|
1064
|
+
if (receiverType.kind !== 'struct') {
|
|
1065
|
+
return null
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const method = this.implMethods.get(receiverType.name)?.get(expr.fn)
|
|
1069
|
+
if (!method || method.params[0]?.name !== 'self') {
|
|
1070
|
+
return null
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return method
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** Check if childType is a subtype of parentType */
|
|
1077
|
+
private isEntitySubtype(childType: EntityTypeName, parentType: EntityTypeName): boolean {
|
|
1078
|
+
if (childType === parentType) return true
|
|
1079
|
+
|
|
1080
|
+
let current: EntityTypeName | null = childType
|
|
1081
|
+
while (current !== null) {
|
|
1082
|
+
if (current === parentType) return true
|
|
1083
|
+
current = ENTITY_HIERARCHY[current]
|
|
1084
|
+
}
|
|
1085
|
+
return false
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** Push a new self type context */
|
|
1089
|
+
private pushSelfType(entityType: EntityTypeName): void {
|
|
1090
|
+
this.selfTypeStack.push(entityType)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Pop self type context */
|
|
1094
|
+
private popSelfType(): void {
|
|
1095
|
+
if (this.selfTypeStack.length > 1) {
|
|
1096
|
+
this.selfTypeStack.pop()
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** Get current @s type */
|
|
1101
|
+
private getCurrentSelfType(): EntityTypeName {
|
|
1102
|
+
return this.selfTypeStack[this.selfTypeStack.length - 1]
|
|
1103
|
+
}
|
|
1104
|
+
|
|
676
1105
|
private typesMatch(expected: TypeNode, actual: TypeNode): boolean {
|
|
677
1106
|
if (expected.kind !== actual.kind) return false
|
|
678
1107
|
|
|
@@ -700,6 +1129,16 @@ export class TypeChecker {
|
|
|
700
1129
|
this.typesMatch(expected.return, actual.return)
|
|
701
1130
|
}
|
|
702
1131
|
|
|
1132
|
+
// Entity type matching with subtype support
|
|
1133
|
+
if (expected.kind === 'entity' && actual.kind === 'entity') {
|
|
1134
|
+
return this.isEntitySubtype(actual.entityType, expected.entityType)
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Selector matches any entity type
|
|
1138
|
+
if (expected.kind === 'selector' && actual.kind === 'entity') {
|
|
1139
|
+
return true
|
|
1140
|
+
}
|
|
1141
|
+
|
|
703
1142
|
return false
|
|
704
1143
|
}
|
|
705
1144
|
|
|
@@ -715,6 +1154,12 @@ export class TypeChecker {
|
|
|
715
1154
|
return type.name
|
|
716
1155
|
case 'function_type':
|
|
717
1156
|
return `(${type.params.map(param => this.typeToString(param)).join(', ')}) -> ${this.typeToString(type.return)}`
|
|
1157
|
+
case 'entity':
|
|
1158
|
+
return type.entityType
|
|
1159
|
+
case 'selector':
|
|
1160
|
+
return 'selector'
|
|
1161
|
+
default:
|
|
1162
|
+
return 'unknown'
|
|
718
1163
|
}
|
|
719
1164
|
}
|
|
720
1165
|
|
|
@@ -732,6 +1177,12 @@ export class TypeChecker {
|
|
|
732
1177
|
if ((type.kind === 'struct' || type.kind === 'enum') && this.enums.has(type.name)) {
|
|
733
1178
|
return { kind: 'enum', name: type.name }
|
|
734
1179
|
}
|
|
1180
|
+
if (type.kind === 'struct' && type.name in ENTITY_HIERARCHY) {
|
|
1181
|
+
return { kind: 'entity', entityType: type.name as EntityTypeName }
|
|
1182
|
+
}
|
|
1183
|
+
if (type.kind === 'named' && type.name in ENTITY_HIERARCHY) {
|
|
1184
|
+
return { kind: 'entity', entityType: type.name as EntityTypeName }
|
|
1185
|
+
}
|
|
735
1186
|
return type
|
|
736
1187
|
}
|
|
737
1188
|
}
|