redscript-mc 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/__tests__/cli.test.js +138 -0
  3. package/dist/__tests__/codegen.test.js +25 -0
  4. package/dist/__tests__/e2e.test.js +190 -12
  5. package/dist/__tests__/lexer.test.js +12 -2
  6. package/dist/__tests__/lowering.test.js +164 -9
  7. package/dist/__tests__/mc-integration.test.js +145 -51
  8. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  9. package/dist/__tests__/parser.test.js +80 -0
  10. package/dist/__tests__/runtime.test.js +8 -8
  11. package/dist/__tests__/typechecker.test.js +158 -0
  12. package/dist/ast/types.d.ts +20 -1
  13. package/dist/codegen/mcfunction/index.js +30 -1
  14. package/dist/codegen/structure/index.js +25 -0
  15. package/dist/compile.d.ts +10 -0
  16. package/dist/compile.js +36 -5
  17. package/dist/events/types.d.ts +35 -0
  18. package/dist/events/types.js +59 -0
  19. package/dist/index.js +3 -2
  20. package/dist/ir/types.d.ts +4 -0
  21. package/dist/lexer/index.d.ts +1 -1
  22. package/dist/lexer/index.js +2 -0
  23. package/dist/lowering/index.d.ts +32 -1
  24. package/dist/lowering/index.js +439 -15
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +79 -10
  27. package/dist/typechecker/index.d.ts +17 -0
  28. package/dist/typechecker/index.js +343 -17
  29. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  30. package/editors/vscode/CHANGELOG.md +9 -0
  31. package/editors/vscode/out/extension.js +1144 -72
  32. package/editors/vscode/package-lock.json +2 -2
  33. package/editors/vscode/package.json +1 -1
  34. package/package.json +1 -1
  35. package/src/__tests__/cli.test.ts +166 -0
  36. package/src/__tests__/codegen.test.ts +27 -0
  37. package/src/__tests__/e2e.test.ts +201 -12
  38. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  39. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  40. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  41. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  42. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  43. package/src/__tests__/lexer.test.ts +14 -2
  44. package/src/__tests__/lowering.test.ts +178 -9
  45. package/src/__tests__/mc-integration.test.ts +166 -51
  46. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  47. package/src/__tests__/parser.test.ts +91 -5
  48. package/src/__tests__/runtime.test.ts +8 -8
  49. package/src/__tests__/typechecker.test.ts +171 -0
  50. package/src/ast/types.ts +25 -1
  51. package/src/codegen/mcfunction/index.ts +31 -1
  52. package/src/codegen/structure/index.ts +27 -0
  53. package/src/compile.ts +54 -6
  54. package/src/events/types.ts +69 -0
  55. package/src/index.ts +4 -3
  56. package/src/ir/types.ts +4 -0
  57. package/src/lexer/index.ts +3 -1
  58. package/src/lowering/index.ts +528 -16
  59. package/src/parser/index.ts +90 -12
  60. package/src/stdlib/README.md +34 -4
  61. package/src/stdlib/tags.mcrs +951 -0
  62. package/src/stdlib/timer.mcrs +54 -33
  63. package/src/typechecker/index.ts +404 -18
@@ -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.checkBlock(stmt.then)
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
- this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true }) // Entity marker
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 { kind: 'named', name: 'void' }
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 { kind: 'named', name: 'int' }
859
+ return INT_TYPE
588
860
  }
589
861
  if (expr.fn === 'bossbar_get_value') {
590
- return { kind: 'named', name: 'int' }
862
+ return INT_TYPE
591
863
  }
592
864
  if (expr.fn === 'random_sequence') {
593
- return { kind: 'named', name: 'void' }
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 ?? { kind: 'named', name: 'int' }
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
  }