redscript-mc 1.0.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/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
- package/CHANGELOG.md +112 -0
- package/CONTRIBUTING.md +140 -0
- package/README.md +28 -19
- package/README.zh.md +28 -19
- package/dist/__tests__/cli.test.js +148 -10
- package/dist/__tests__/codegen.test.js +26 -1
- package/dist/__tests__/diagnostics.test.js +5 -5
- package/dist/__tests__/e2e.test.js +336 -17
- package/dist/__tests__/formatter.test.d.ts +1 -0
- package/dist/__tests__/formatter.test.js +40 -0
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +200 -12
- package/dist/__tests__/mc-integration.test.js +370 -31
- package/dist/__tests__/mc-syntax.test.js +3 -3
- package/dist/__tests__/nbt.test.js +2 -2
- package/dist/__tests__/optimizer-advanced.test.js +5 -5
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +9 -9
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +40 -3
- package/dist/cli.js +25 -7
- package/dist/codegen/mcfunction/index.d.ts +1 -1
- package/dist/codegen/mcfunction/index.js +38 -3
- package/dist/codegen/structure/index.js +32 -1
- 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/formatter/index.d.ts +1 -0
- package/dist/formatter/index.js +26 -0
- package/dist/index.js +3 -2
- package/dist/ir/builder.d.ts +2 -1
- package/dist/ir/types.d.ts +11 -2
- package/dist/ir/types.js +1 -1
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +34 -1
- package/dist/lowering/index.js +622 -23
- package/dist/mc-test/runner.d.ts +2 -2
- package/dist/mc-test/runner.js +3 -3
- package/dist/mc-test/setup.js +2 -2
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +153 -16
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/COMPILATION_STATS.md +24 -24
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/docs/IMPLEMENTATION_GUIDE.md +1 -1
- package/docs/STRUCTURE_TARGET.md +1 -1
- package/editors/vscode/.vscodeignore +1 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icons/mcrs.svg +7 -0
- package/editors/vscode/icons/redscript-icons.json +10 -0
- package/editors/vscode/out/extension.js +1295 -80
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +10 -3
- package/editors/vscode/src/hover.ts +55 -2
- package/editors/vscode/src/symbols.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +176 -10
- package/src/__tests__/codegen.test.ts +28 -1
- package/src/__tests__/diagnostics.test.ts +5 -5
- package/src/__tests__/e2e.test.ts +335 -17
- 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 +226 -12
- package/src/__tests__/mc-integration.test.ts +421 -31
- package/src/__tests__/mc-syntax.test.ts +3 -3
- package/src/__tests__/nbt.test.ts +2 -2
- package/src/__tests__/optimizer-advanced.test.ts +5 -5
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +9 -9
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +44 -3
- package/src/cli.ts +10 -10
- package/src/codegen/mcfunction/index.ts +40 -3
- package/src/codegen/structure/index.ts +35 -1
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- package/src/examples/capture_the_flag.mcrs +208 -0
- package/src/examples/{counter.rs → counter.mcrs} +1 -1
- package/src/examples/hunger_games.mcrs +301 -0
- package/src/examples/new_features_demo.mcrs +193 -0
- package/src/examples/parkour_race.mcrs +233 -0
- package/src/examples/rpg.mcrs +13 -0
- package/src/examples/{shop.rs → shop.mcrs} +1 -1
- package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
- package/src/examples/{turret.rs → turret.mcrs} +1 -1
- package/src/examples/zombie_survival.mcrs +314 -0
- package/src/index.ts +4 -3
- package/src/ir/builder.ts +3 -1
- package/src/ir/types.ts +12 -2
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +684 -24
- package/src/mc-test/runner.ts +3 -3
- package/src/mc-test/setup.ts +2 -2
- package/src/parser/index.ts +170 -19
- package/src/stdlib/README.md +178 -140
- package/src/stdlib/bossbar.mcrs +68 -0
- package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
- package/src/stdlib/effects.mcrs +64 -0
- package/src/stdlib/interactions.mcrs +195 -0
- package/src/stdlib/inventory.mcrs +38 -0
- package/src/stdlib/mobs.mcrs +99 -0
- package/src/stdlib/particles.mcrs +52 -0
- package/src/stdlib/sets.mcrs +20 -0
- package/src/stdlib/spawn.mcrs +41 -0
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/teams.mcrs +68 -0
- package/src/stdlib/timer.mcrs +72 -0
- package/src/stdlib/world.mcrs +92 -0
- package/src/typechecker/index.ts +404 -18
- package/src/examples/rpg.rs +0 -13
- package/src/stdlib/mobs.rs +0 -99
- package/src/stdlib/timer.rs +0 -51
- /package/src/examples/{arena.rs → arena.mcrs} +0 -0
- /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
- /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
- /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
- /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
- /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
- /package/src/stdlib/{math.rs → math.mcrs} +0 -0
- /package/src/stdlib/{player.rs → player.mcrs} +0 -0
- /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
- /package/src/templates/{combat.rs → combat.mcrs} +0 -0
- /package/src/templates/{economy.rs → economy.mcrs} +0 -0
- /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
- /package/src/templates/{quest.rs → quest.mcrs} +0 -0
- /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
package/src/typechecker/index.ts
CHANGED
|
@@ -5,14 +5,90 @@
|
|
|
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
|
+
|
|
77
|
+
const BUILTIN_SIGNATURES: Record<string, BuiltinSignature> = {
|
|
78
|
+
setTimeout: {
|
|
79
|
+
params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
|
|
80
|
+
return: VOID_TYPE,
|
|
81
|
+
},
|
|
82
|
+
setInterval: {
|
|
83
|
+
params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
|
|
84
|
+
return: INT_TYPE,
|
|
85
|
+
},
|
|
86
|
+
clearInterval: {
|
|
87
|
+
params: [INT_TYPE],
|
|
88
|
+
return: VOID_TYPE,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
16
92
|
// ---------------------------------------------------------------------------
|
|
17
93
|
// Type Checker
|
|
18
94
|
// ---------------------------------------------------------------------------
|
|
@@ -20,12 +96,15 @@ interface ScopeSymbol {
|
|
|
20
96
|
export class TypeChecker {
|
|
21
97
|
private collector: DiagnosticCollector
|
|
22
98
|
private functions: Map<string, FnDecl> = new Map()
|
|
99
|
+
private implMethods: Map<string, Map<string, FnDecl>> = new Map()
|
|
23
100
|
private structs: Map<string, Map<string, TypeNode>> = new Map()
|
|
24
101
|
private enums: Map<string, Map<string, number>> = new Map()
|
|
25
102
|
private consts: Map<string, TypeNode> = new Map()
|
|
26
103
|
private currentFn: FnDecl | null = null
|
|
27
104
|
private currentReturnType: TypeNode | null = null
|
|
28
105
|
private scope: Map<string, ScopeSymbol> = new Map()
|
|
106
|
+
// Stack for tracking @s type in different contexts
|
|
107
|
+
private selfTypeStack: EntityTypeName[] = ['entity']
|
|
29
108
|
|
|
30
109
|
constructor(source?: string, filePath?: string) {
|
|
31
110
|
this.collector = new DiagnosticCollector(source, filePath)
|
|
@@ -53,6 +132,28 @@ export class TypeChecker {
|
|
|
53
132
|
this.functions.set(fn.name, fn)
|
|
54
133
|
}
|
|
55
134
|
|
|
135
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
136
|
+
let methods = this.implMethods.get(implBlock.typeName)
|
|
137
|
+
if (!methods) {
|
|
138
|
+
methods = new Map()
|
|
139
|
+
this.implMethods.set(implBlock.typeName, methods)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const method of implBlock.methods) {
|
|
143
|
+
const selfIndex = method.params.findIndex(param => param.name === 'self')
|
|
144
|
+
if (selfIndex > 0) {
|
|
145
|
+
this.report(`Method '${method.name}' must declare 'self' as the first parameter`, method.params[selfIndex])
|
|
146
|
+
}
|
|
147
|
+
if (selfIndex === 0) {
|
|
148
|
+
const selfType = this.normalizeType(method.params[0].type)
|
|
149
|
+
if (selfType.kind !== 'struct' || selfType.name !== implBlock.typeName) {
|
|
150
|
+
this.report(`Method '${method.name}' has invalid 'self' type`, method.params[0])
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
methods.set(method.name, method)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
56
157
|
for (const struct of program.structs ?? []) {
|
|
57
158
|
const fields = new Map<string, TypeNode>()
|
|
58
159
|
for (const field of struct.fields) {
|
|
@@ -86,6 +187,12 @@ export class TypeChecker {
|
|
|
86
187
|
this.checkFunction(fn)
|
|
87
188
|
}
|
|
88
189
|
|
|
190
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
191
|
+
for (const method of implBlock.methods) {
|
|
192
|
+
this.checkFunction(method)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
89
196
|
return this.collector.getErrors()
|
|
90
197
|
}
|
|
91
198
|
|
|
@@ -95,6 +202,8 @@ export class TypeChecker {
|
|
|
95
202
|
this.scope = new Map()
|
|
96
203
|
let seenDefault = false
|
|
97
204
|
|
|
205
|
+
this.checkFunctionDecorators(fn)
|
|
206
|
+
|
|
98
207
|
for (const [name, type] of this.consts.entries()) {
|
|
99
208
|
this.scope.set(name, { type, mutable: false })
|
|
100
209
|
}
|
|
@@ -125,6 +234,49 @@ export class TypeChecker {
|
|
|
125
234
|
this.currentReturnType = null
|
|
126
235
|
}
|
|
127
236
|
|
|
237
|
+
private checkFunctionDecorators(fn: FnDecl): void {
|
|
238
|
+
const eventDecorators = fn.decorators.filter(decorator => decorator.name === 'on')
|
|
239
|
+
if (eventDecorators.length === 0) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (eventDecorators.length > 1) {
|
|
244
|
+
this.report(`Function '${fn.name}' cannot have multiple @on decorators`, fn)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const eventType = eventDecorators[0].args?.eventType
|
|
249
|
+
if (!eventType) {
|
|
250
|
+
this.report(`Function '${fn.name}' is missing an event type in @on(...)`, fn)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!isEventTypeName(eventType)) {
|
|
255
|
+
this.report(`Unknown event type '${eventType}'`, fn)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const expectedParams = getEventParamSpecs(eventType)
|
|
260
|
+
if (fn.params.length !== expectedParams.length) {
|
|
261
|
+
this.report(
|
|
262
|
+
`Event handler '${fn.name}' for ${eventType} must declare ${expectedParams.length} parameter(s), got ${fn.params.length}`,
|
|
263
|
+
fn
|
|
264
|
+
)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (let i = 0; i < expectedParams.length; i++) {
|
|
269
|
+
const actual = this.normalizeType(fn.params[i].type)
|
|
270
|
+
const expected = this.normalizeType(expectedParams[i].type)
|
|
271
|
+
if (!this.typesMatch(expected, actual)) {
|
|
272
|
+
this.report(
|
|
273
|
+
`Event handler '${fn.name}' parameter ${i + 1} must be ${this.typeToString(expected)}, got ${this.typeToString(actual)}`,
|
|
274
|
+
fn.params[i]
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
128
280
|
private checkBlock(stmts: Block): void {
|
|
129
281
|
for (const stmt of stmts) {
|
|
130
282
|
this.checkStmt(stmt)
|
|
@@ -141,8 +293,7 @@ export class TypeChecker {
|
|
|
141
293
|
break
|
|
142
294
|
case 'if':
|
|
143
295
|
this.checkExpr(stmt.cond)
|
|
144
|
-
this.
|
|
145
|
-
if (stmt.else_) this.checkBlock(stmt.else_)
|
|
296
|
+
this.checkIfBranches(stmt)
|
|
146
297
|
break
|
|
147
298
|
case 'while':
|
|
148
299
|
this.checkExpr(stmt.cond)
|
|
@@ -157,7 +308,16 @@ export class TypeChecker {
|
|
|
157
308
|
case 'foreach':
|
|
158
309
|
this.checkExpr(stmt.iterable)
|
|
159
310
|
if (stmt.iterable.kind === 'selector') {
|
|
160
|
-
|
|
311
|
+
// Infer entity type from selector (access .sel for the EntitySelector)
|
|
312
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.iterable.sel)
|
|
313
|
+
this.scope.set(stmt.binding, {
|
|
314
|
+
type: { kind: 'entity', entityType },
|
|
315
|
+
mutable: false // Entity bindings are not reassignable
|
|
316
|
+
})
|
|
317
|
+
// Push self type context for @s inside the loop
|
|
318
|
+
this.pushSelfType(entityType)
|
|
319
|
+
this.checkBlock(stmt.body)
|
|
320
|
+
this.popSelfType()
|
|
161
321
|
} else {
|
|
162
322
|
const iterableType = this.inferType(stmt.iterable)
|
|
163
323
|
if (iterableType.kind === 'array') {
|
|
@@ -165,8 +325,8 @@ export class TypeChecker {
|
|
|
165
325
|
} else {
|
|
166
326
|
this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true })
|
|
167
327
|
}
|
|
328
|
+
this.checkBlock(stmt.body)
|
|
168
329
|
}
|
|
169
|
-
this.checkBlock(stmt.body)
|
|
170
330
|
break
|
|
171
331
|
case 'match':
|
|
172
332
|
this.checkExpr(stmt.expr)
|
|
@@ -180,15 +340,41 @@ export class TypeChecker {
|
|
|
180
340
|
this.checkBlock(arm.body)
|
|
181
341
|
}
|
|
182
342
|
break
|
|
183
|
-
case 'as_block':
|
|
343
|
+
case 'as_block': {
|
|
344
|
+
// as block changes @s to the selector's entity type
|
|
345
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.selector)
|
|
346
|
+
this.pushSelfType(entityType)
|
|
347
|
+
this.checkBlock(stmt.body)
|
|
348
|
+
this.popSelfType()
|
|
349
|
+
break
|
|
350
|
+
}
|
|
184
351
|
case 'at_block':
|
|
352
|
+
// at block doesn't change @s type, only position
|
|
185
353
|
this.checkBlock(stmt.body)
|
|
186
354
|
break
|
|
187
|
-
case 'as_at':
|
|
355
|
+
case 'as_at': {
|
|
356
|
+
// as @x at @y - @s becomes the as selector's type
|
|
357
|
+
const entityType = this.inferEntityTypeFromSelector(stmt.as_sel)
|
|
358
|
+
this.pushSelfType(entityType)
|
|
188
359
|
this.checkBlock(stmt.body)
|
|
360
|
+
this.popSelfType()
|
|
189
361
|
break
|
|
362
|
+
}
|
|
190
363
|
case 'execute':
|
|
364
|
+
// execute with subcommands - check for 'as' subcommands
|
|
365
|
+
for (const sub of stmt.subcommands) {
|
|
366
|
+
if (sub.kind === 'as' && sub.selector) {
|
|
367
|
+
const entityType = this.inferEntityTypeFromSelector(sub.selector)
|
|
368
|
+
this.pushSelfType(entityType)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
191
371
|
this.checkBlock(stmt.body)
|
|
372
|
+
// Pop for each 'as' subcommand
|
|
373
|
+
for (const sub of stmt.subcommands) {
|
|
374
|
+
if (sub.kind === 'as') {
|
|
375
|
+
this.popSelfType()
|
|
376
|
+
}
|
|
377
|
+
}
|
|
192
378
|
break
|
|
193
379
|
case 'expr':
|
|
194
380
|
this.checkExpr(stmt.expr)
|
|
@@ -265,12 +451,24 @@ export class TypeChecker {
|
|
|
265
451
|
case 'member':
|
|
266
452
|
this.checkMemberExpr(expr)
|
|
267
453
|
break
|
|
454
|
+
case 'static_call':
|
|
455
|
+
this.checkStaticCallExpr(expr)
|
|
456
|
+
break
|
|
268
457
|
|
|
269
458
|
case 'binary':
|
|
270
459
|
this.checkExpr(expr.left)
|
|
271
460
|
this.checkExpr(expr.right)
|
|
272
461
|
break
|
|
273
462
|
|
|
463
|
+
case 'is_check': {
|
|
464
|
+
this.checkExpr(expr.expr)
|
|
465
|
+
const checkedType = this.inferType(expr.expr)
|
|
466
|
+
if (checkedType.kind !== 'entity') {
|
|
467
|
+
this.report(`'is' checks require an entity expression, got ${this.typeToString(checkedType)}`, expr.expr)
|
|
468
|
+
}
|
|
469
|
+
break
|
|
470
|
+
}
|
|
471
|
+
|
|
274
472
|
case 'unary':
|
|
275
473
|
this.checkExpr(expr.operand)
|
|
276
474
|
break
|
|
@@ -325,12 +523,6 @@ export class TypeChecker {
|
|
|
325
523
|
case 'blockpos':
|
|
326
524
|
break
|
|
327
525
|
|
|
328
|
-
case 'static_call':
|
|
329
|
-
for (const arg of expr.args) {
|
|
330
|
-
this.checkExpr(arg)
|
|
331
|
-
}
|
|
332
|
-
break
|
|
333
|
-
|
|
334
526
|
// Literals don't need checking
|
|
335
527
|
case 'int_lit':
|
|
336
528
|
case 'float_lit':
|
|
@@ -352,6 +544,12 @@ export class TypeChecker {
|
|
|
352
544
|
this.checkTpCall(expr)
|
|
353
545
|
}
|
|
354
546
|
|
|
547
|
+
const builtin = BUILTIN_SIGNATURES[expr.fn]
|
|
548
|
+
if (builtin) {
|
|
549
|
+
this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr)
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
|
|
355
553
|
// Check if function exists and arg count matches
|
|
356
554
|
const fn = this.functions.get(expr.fn)
|
|
357
555
|
if (fn) {
|
|
@@ -387,6 +585,17 @@ export class TypeChecker {
|
|
|
387
585
|
return
|
|
388
586
|
}
|
|
389
587
|
|
|
588
|
+
const implMethod = this.resolveInstanceMethod(expr)
|
|
589
|
+
if (implMethod) {
|
|
590
|
+
this.checkFunctionCallArgs(
|
|
591
|
+
expr.args,
|
|
592
|
+
implMethod.params.map(param => this.normalizeType(param.type)),
|
|
593
|
+
implMethod.name,
|
|
594
|
+
expr
|
|
595
|
+
)
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
|
|
390
599
|
for (const arg of expr.args) {
|
|
391
600
|
this.checkExpr(arg)
|
|
392
601
|
}
|
|
@@ -497,6 +706,29 @@ export class TypeChecker {
|
|
|
497
706
|
}
|
|
498
707
|
}
|
|
499
708
|
|
|
709
|
+
private checkStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): void {
|
|
710
|
+
const method = this.implMethods.get(expr.type)?.get(expr.method)
|
|
711
|
+
if (!method) {
|
|
712
|
+
this.report(`Type '${expr.type}' has no static method '${expr.method}'`, expr)
|
|
713
|
+
for (const arg of expr.args) {
|
|
714
|
+
this.checkExpr(arg)
|
|
715
|
+
}
|
|
716
|
+
return
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (method.params[0]?.name === 'self') {
|
|
720
|
+
this.report(`Method '${expr.type}::${expr.method}' is an instance method`, expr)
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
this.checkFunctionCallArgs(
|
|
725
|
+
expr.args,
|
|
726
|
+
method.params.map(param => this.normalizeType(param.type)),
|
|
727
|
+
`${expr.type}::${expr.method}`,
|
|
728
|
+
expr
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
500
732
|
private checkLambdaExpr(expr: Extract<Expr, { kind: 'lambda' }>, expectedType?: TypeNode): void {
|
|
501
733
|
const normalizedExpected = expectedType ? this.normalizeType(expectedType) : undefined
|
|
502
734
|
const expectedFnType = normalizedExpected?.kind === 'function_type' ? normalizedExpected : undefined
|
|
@@ -544,6 +776,42 @@ export class TypeChecker {
|
|
|
544
776
|
this.currentReturnType = outerReturnType
|
|
545
777
|
}
|
|
546
778
|
|
|
779
|
+
private checkIfBranches(stmt: Extract<Stmt, { kind: 'if' }>): void {
|
|
780
|
+
const narrowed = this.getThenBranchNarrowing(stmt.cond)
|
|
781
|
+
|
|
782
|
+
if (narrowed) {
|
|
783
|
+
const thenScope = new Map(this.scope)
|
|
784
|
+
thenScope.set(narrowed.name, { type: narrowed.type, mutable: narrowed.mutable })
|
|
785
|
+
const outerScope = this.scope
|
|
786
|
+
this.scope = thenScope
|
|
787
|
+
this.checkBlock(stmt.then)
|
|
788
|
+
this.scope = outerScope
|
|
789
|
+
} else {
|
|
790
|
+
this.checkBlock(stmt.then)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (stmt.else_) {
|
|
794
|
+
this.checkBlock(stmt.else_)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private getThenBranchNarrowing(cond: Expr): { name: string; type: Extract<TypeNode, { kind: 'entity' }>; mutable: boolean } | null {
|
|
799
|
+
if (cond.kind !== 'is_check' || cond.expr.kind !== 'ident') {
|
|
800
|
+
return null
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const symbol = this.scope.get(cond.expr.name)
|
|
804
|
+
if (!symbol || symbol.type.kind !== 'entity') {
|
|
805
|
+
return null
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return {
|
|
809
|
+
name: cond.expr.name,
|
|
810
|
+
type: { kind: 'entity', entityType: cond.entityType },
|
|
811
|
+
mutable: symbol.mutable,
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
547
815
|
private inferType(expr: Expr, expectedType?: TypeNode): TypeNode {
|
|
548
816
|
switch (expr.kind) {
|
|
549
817
|
case 'int_lit':
|
|
@@ -575,8 +843,12 @@ export class TypeChecker {
|
|
|
575
843
|
case 'ident':
|
|
576
844
|
return this.scope.get(expr.name)?.type ?? { kind: 'named', name: 'void' }
|
|
577
845
|
case 'call': {
|
|
846
|
+
const builtin = BUILTIN_SIGNATURES[expr.fn]
|
|
847
|
+
if (builtin) {
|
|
848
|
+
return builtin.return
|
|
849
|
+
}
|
|
578
850
|
if (expr.fn === '__array_push') {
|
|
579
|
-
return
|
|
851
|
+
return VOID_TYPE
|
|
580
852
|
}
|
|
581
853
|
if (expr.fn === '__array_pop') {
|
|
582
854
|
const target = expr.args[0]
|
|
@@ -584,20 +856,28 @@ export class TypeChecker {
|
|
|
584
856
|
const targetType = this.scope.get(target.name)?.type
|
|
585
857
|
if (targetType?.kind === 'array') return targetType.elem
|
|
586
858
|
}
|
|
587
|
-
return
|
|
859
|
+
return INT_TYPE
|
|
588
860
|
}
|
|
589
861
|
if (expr.fn === 'bossbar_get_value') {
|
|
590
|
-
return
|
|
862
|
+
return INT_TYPE
|
|
591
863
|
}
|
|
592
864
|
if (expr.fn === 'random_sequence') {
|
|
593
|
-
return
|
|
865
|
+
return VOID_TYPE
|
|
594
866
|
}
|
|
595
867
|
const varType = this.scope.get(expr.fn)?.type
|
|
596
868
|
if (varType?.kind === 'function_type') {
|
|
597
869
|
return varType.return
|
|
598
870
|
}
|
|
871
|
+
const implMethod = this.resolveInstanceMethod(expr)
|
|
872
|
+
if (implMethod) {
|
|
873
|
+
return this.normalizeType(implMethod.returnType)
|
|
874
|
+
}
|
|
599
875
|
const fn = this.functions.get(expr.fn)
|
|
600
|
-
return fn?.returnType ??
|
|
876
|
+
return fn?.returnType ?? INT_TYPE
|
|
877
|
+
}
|
|
878
|
+
case 'static_call': {
|
|
879
|
+
const method = this.implMethods.get(expr.type)?.get(expr.method)
|
|
880
|
+
return method ? this.normalizeType(method.returnType) : { kind: 'named', name: 'void' }
|
|
601
881
|
}
|
|
602
882
|
case 'invoke': {
|
|
603
883
|
const calleeType = this.inferType(expr.callee)
|
|
@@ -627,6 +907,8 @@ export class TypeChecker {
|
|
|
627
907
|
return { kind: 'named', name: 'bool' }
|
|
628
908
|
}
|
|
629
909
|
return this.inferType(expr.left)
|
|
910
|
+
case 'is_check':
|
|
911
|
+
return { kind: 'named', name: 'bool' }
|
|
630
912
|
case 'unary':
|
|
631
913
|
if (expr.op === '!') return { kind: 'named', name: 'bool' }
|
|
632
914
|
return this.inferType(expr.operand)
|
|
@@ -635,6 +917,14 @@ export class TypeChecker {
|
|
|
635
917
|
return { kind: 'array', elem: this.inferType(expr.elements[0]) }
|
|
636
918
|
}
|
|
637
919
|
return { kind: 'array', elem: { kind: 'named', name: 'int' } }
|
|
920
|
+
case 'struct_lit':
|
|
921
|
+
if (expectedType) {
|
|
922
|
+
const normalized = this.normalizeType(expectedType)
|
|
923
|
+
if (normalized.kind === 'struct') {
|
|
924
|
+
return normalized
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return { kind: 'named', name: 'void' }
|
|
638
928
|
case 'lambda':
|
|
639
929
|
return this.inferLambdaType(
|
|
640
930
|
expr,
|
|
@@ -673,6 +963,80 @@ export class TypeChecker {
|
|
|
673
963
|
return { kind: 'function_type', params, return: returnType }
|
|
674
964
|
}
|
|
675
965
|
|
|
966
|
+
// ---------------------------------------------------------------------------
|
|
967
|
+
// Entity Type Helpers
|
|
968
|
+
// ---------------------------------------------------------------------------
|
|
969
|
+
|
|
970
|
+
/** Infer entity type from a selector */
|
|
971
|
+
private inferEntityTypeFromSelector(selector: EntitySelector): EntityTypeName {
|
|
972
|
+
// @a, @p, @r always return Player
|
|
973
|
+
if (selector.kind === '@a' || selector.kind === '@p' || selector.kind === '@r') {
|
|
974
|
+
return 'Player'
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// @e or @s with type= filter
|
|
978
|
+
if (selector.filters?.type) {
|
|
979
|
+
const mcType = selector.filters.type.toLowerCase()
|
|
980
|
+
return MC_TYPE_TO_ENTITY[mcType] ?? 'entity'
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// @s uses current context
|
|
984
|
+
if (selector.kind === '@s') {
|
|
985
|
+
return this.selfTypeStack[this.selfTypeStack.length - 1]
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Default to entity
|
|
989
|
+
return 'entity'
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private resolveInstanceMethod(expr: Extract<Expr, { kind: 'call' }>): FnDecl | null {
|
|
993
|
+
const receiver = expr.args[0]
|
|
994
|
+
if (!receiver) {
|
|
995
|
+
return null
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const receiverType = this.inferType(receiver)
|
|
999
|
+
if (receiverType.kind !== 'struct') {
|
|
1000
|
+
return null
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const method = this.implMethods.get(receiverType.name)?.get(expr.fn)
|
|
1004
|
+
if (!method || method.params[0]?.name !== 'self') {
|
|
1005
|
+
return null
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return method
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** Check if childType is a subtype of parentType */
|
|
1012
|
+
private isEntitySubtype(childType: EntityTypeName, parentType: EntityTypeName): boolean {
|
|
1013
|
+
if (childType === parentType) return true
|
|
1014
|
+
|
|
1015
|
+
let current: EntityTypeName | null = childType
|
|
1016
|
+
while (current !== null) {
|
|
1017
|
+
if (current === parentType) return true
|
|
1018
|
+
current = ENTITY_HIERARCHY[current]
|
|
1019
|
+
}
|
|
1020
|
+
return false
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** Push a new self type context */
|
|
1024
|
+
private pushSelfType(entityType: EntityTypeName): void {
|
|
1025
|
+
this.selfTypeStack.push(entityType)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/** Pop self type context */
|
|
1029
|
+
private popSelfType(): void {
|
|
1030
|
+
if (this.selfTypeStack.length > 1) {
|
|
1031
|
+
this.selfTypeStack.pop()
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** Get current @s type */
|
|
1036
|
+
private getCurrentSelfType(): EntityTypeName {
|
|
1037
|
+
return this.selfTypeStack[this.selfTypeStack.length - 1]
|
|
1038
|
+
}
|
|
1039
|
+
|
|
676
1040
|
private typesMatch(expected: TypeNode, actual: TypeNode): boolean {
|
|
677
1041
|
if (expected.kind !== actual.kind) return false
|
|
678
1042
|
|
|
@@ -700,6 +1064,16 @@ export class TypeChecker {
|
|
|
700
1064
|
this.typesMatch(expected.return, actual.return)
|
|
701
1065
|
}
|
|
702
1066
|
|
|
1067
|
+
// Entity type matching with subtype support
|
|
1068
|
+
if (expected.kind === 'entity' && actual.kind === 'entity') {
|
|
1069
|
+
return this.isEntitySubtype(actual.entityType, expected.entityType)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Selector matches any entity type
|
|
1073
|
+
if (expected.kind === 'selector' && actual.kind === 'entity') {
|
|
1074
|
+
return true
|
|
1075
|
+
}
|
|
1076
|
+
|
|
703
1077
|
return false
|
|
704
1078
|
}
|
|
705
1079
|
|
|
@@ -715,6 +1089,12 @@ export class TypeChecker {
|
|
|
715
1089
|
return type.name
|
|
716
1090
|
case 'function_type':
|
|
717
1091
|
return `(${type.params.map(param => this.typeToString(param)).join(', ')}) -> ${this.typeToString(type.return)}`
|
|
1092
|
+
case 'entity':
|
|
1093
|
+
return type.entityType
|
|
1094
|
+
case 'selector':
|
|
1095
|
+
return 'selector'
|
|
1096
|
+
default:
|
|
1097
|
+
return 'unknown'
|
|
718
1098
|
}
|
|
719
1099
|
}
|
|
720
1100
|
|
|
@@ -732,6 +1112,12 @@ export class TypeChecker {
|
|
|
732
1112
|
if ((type.kind === 'struct' || type.kind === 'enum') && this.enums.has(type.name)) {
|
|
733
1113
|
return { kind: 'enum', name: type.name }
|
|
734
1114
|
}
|
|
1115
|
+
if (type.kind === 'struct' && type.name in ENTITY_HIERARCHY) {
|
|
1116
|
+
return { kind: 'entity', entityType: type.name as EntityTypeName }
|
|
1117
|
+
}
|
|
1118
|
+
if (type.kind === 'named' && type.name in ENTITY_HIERARCHY) {
|
|
1119
|
+
return { kind: 'entity', entityType: type.name as EntityTypeName }
|
|
1120
|
+
}
|
|
735
1121
|
return type
|
|
736
1122
|
}
|
|
737
1123
|
}
|
package/src/examples/rpg.rs
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import "../stdlib/math.rs"
|
|
2
|
-
import "../stdlib/combat.rs"
|
|
3
|
-
|
|
4
|
-
fn attack(enemy: string, base: int, bonus: int) {
|
|
5
|
-
let raw_damage: int = weapon_damage(base, bonus);
|
|
6
|
-
let damage: int = clamp(raw_damage, 1, 20);
|
|
7
|
-
apply_damage(enemy, damage);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
@tick
|
|
11
|
-
fn battle_tick() {
|
|
12
|
-
attack("goblin", 4, 2);
|
|
13
|
-
}
|