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
@@ -9,11 +9,14 @@ import type { IRBuilder } from '../ir/builder'
9
9
  import { buildModule } from '../ir/builder'
10
10
  import type { IRFunction, IRModule, Operand, BinOp, CmpOp } from '../ir/types'
11
11
  import { DiagnosticError } from '../diagnostics'
12
+ import type { SourceRange } from '../compile'
12
13
  import type {
13
14
  Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, Program, RangeExpr, Span, Stmt,
14
- StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent
15
+ StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent, EntityTypeName
15
16
  } from '../ast/types'
16
17
  import type { GlobalVar } from '../ir/types'
18
+ import * as path from 'path'
19
+ import { EVENT_TYPES, getEventParamSpecs, isEventTypeName } from '../events/types'
17
20
 
18
21
  // ---------------------------------------------------------------------------
19
22
  // Builtin Functions
@@ -88,6 +91,9 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
88
91
  set_contains: () => null, // Special handling (returns 1/0)
89
92
  set_remove: () => null, // Special handling
90
93
  set_clear: () => null, // Special handling
94
+ setTimeout: () => null, // Special handling
95
+ setInterval: () => null, // Special handling
96
+ clearInterval: () => null, // Special handling
91
97
  }
92
98
 
93
99
  export interface Warning {
@@ -97,6 +103,12 @@ export interface Warning {
97
103
  col?: number
98
104
  }
99
105
 
106
+ interface StdlibCallSiteContext {
107
+ filePath?: string
108
+ line: number
109
+ col: number
110
+ }
111
+
100
112
  function getSpan(node: unknown): Span | undefined {
101
113
  return (node as { span?: Span } | undefined)?.span
102
114
  }
@@ -104,6 +116,23 @@ function getSpan(node: unknown): Span | undefined {
104
116
  const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/
105
117
  const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/
106
118
 
119
+ const ENTITY_TO_MC_TYPE: Partial<Record<EntityTypeName, string>> = {
120
+ Player: 'player',
121
+ Zombie: 'zombie',
122
+ Skeleton: 'skeleton',
123
+ Creeper: 'creeper',
124
+ Spider: 'spider',
125
+ Enderman: 'enderman',
126
+ Pig: 'pig',
127
+ Cow: 'cow',
128
+ Sheep: 'sheep',
129
+ Chicken: 'chicken',
130
+ Villager: 'villager',
131
+ ArmorStand: 'armor_stand',
132
+ Item: 'item',
133
+ Arrow: 'arrow',
134
+ }
135
+
107
136
  function normalizeSelector(selector: string, warnings: Warning[]): string {
108
137
  return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
109
138
  const trimmed = entityType.trim()
@@ -156,20 +185,27 @@ function emitBlockPos(pos: BlockPosExpr): string {
156
185
 
157
186
  export class Lowering {
158
187
  private namespace: string
188
+ private readonly sourceRanges: SourceRange[]
159
189
  private functions: IRFunction[] = []
160
190
  private globals: GlobalVar[] = []
161
191
  private globalNames: Map<string, { mutable: boolean }> = new Map()
162
192
  private fnDecls: Map<string, FnDecl> = new Map()
193
+ private implMethods: Map<string, Map<string, { fn: FnDecl; loweredName: string }>> = new Map()
163
194
  private specializedFunctions: Map<string, string> = new Map()
164
195
  private currentFn: string = ''
196
+ private currentStdlibCallSite?: StdlibCallSiteContext
165
197
  private foreachCounter: number = 0
166
198
  private lambdaCounter: number = 0
199
+ private timeoutCounter: number = 0
200
+ private intervalCounter: number = 0
167
201
  readonly warnings: Warning[] = []
168
202
 
169
203
  // Builder state for current function
170
204
  private builder!: LoweringBuilder
171
205
  private varMap: Map<string, string> = new Map()
172
206
  private lambdaBindings: Map<string, string> = new Map()
207
+ private intervalBindings: Map<string, string> = new Map()
208
+ private intervalFunctions: Map<number, string> = new Map()
173
209
  private currentCallbackBindings: Map<string, string> = new Map()
174
210
  private currentContext: { binding?: string } = {}
175
211
  private blockPosVars: Map<string, BlockPosExpr> = new Map()
@@ -187,8 +223,9 @@ export class Lowering {
187
223
  // World object counter for unique tags
188
224
  private worldObjCounter: number = 0
189
225
 
190
- constructor(namespace: string) {
226
+ constructor(namespace: string, sourceRanges: SourceRange[] = []) {
191
227
  this.namespace = namespace
228
+ this.sourceRanges = sourceRanges
192
229
  LoweringBuilder.resetTempCounter()
193
230
  }
194
231
 
@@ -230,10 +267,31 @@ export class Lowering {
230
267
  this.functionDefaults.set(fn.name, fn.params.map(param => param.default))
231
268
  }
232
269
 
270
+ for (const implBlock of program.implBlocks ?? []) {
271
+ let methods = this.implMethods.get(implBlock.typeName)
272
+ if (!methods) {
273
+ methods = new Map()
274
+ this.implMethods.set(implBlock.typeName, methods)
275
+ }
276
+
277
+ for (const method of implBlock.methods) {
278
+ const loweredName = `${implBlock.typeName}_${method.name}`
279
+ methods.set(method.name, { fn: method, loweredName })
280
+ this.fnDecls.set(loweredName, method)
281
+ this.functionDefaults.set(loweredName, method.params.map(param => param.default))
282
+ }
283
+ }
284
+
233
285
  for (const fn of program.declarations) {
234
286
  this.lowerFn(fn)
235
287
  }
236
288
 
289
+ for (const implBlock of program.implBlocks ?? []) {
290
+ for (const method of implBlock.methods) {
291
+ this.lowerFn(method, { name: `${implBlock.typeName}_${method.name}` })
292
+ }
293
+ }
294
+
237
295
  return buildModule(this.namespace, this.functions, this.globals)
238
296
  }
239
297
 
@@ -246,16 +304,25 @@ export class Lowering {
246
304
  options: {
247
305
  name?: string
248
306
  callbackBindings?: Map<string, string>
307
+ stdlibCallSite?: StdlibCallSiteContext
249
308
  } = {}
250
309
  ): void {
251
310
  const loweredName = options.name ?? fn.name
252
311
  const callbackBindings = options.callbackBindings ?? new Map<string, string>()
253
- const runtimeParams = fn.params.filter(param => !callbackBindings.has(param.name))
312
+ const stdlibCallSite = options.stdlibCallSite
313
+ const staticEventDec = fn.decorators.find(d => d.name === 'on')
314
+ const eventType = staticEventDec?.args?.eventType
315
+ const eventParamSpecs = eventType && isEventTypeName(eventType) ? getEventParamSpecs(eventType) : []
316
+ const runtimeParams = staticEventDec
317
+ ? []
318
+ : fn.params.filter(param => !callbackBindings.has(param.name))
254
319
 
255
320
  this.currentFn = loweredName
321
+ this.currentStdlibCallSite = stdlibCallSite
256
322
  this.foreachCounter = 0
257
323
  this.varMap = new Map()
258
324
  this.lambdaBindings = new Map()
325
+ this.intervalBindings = new Map()
259
326
  this.currentCallbackBindings = new Map(callbackBindings)
260
327
  this.currentContext = {}
261
328
  this.blockPosVars = new Map()
@@ -263,10 +330,31 @@ export class Lowering {
263
330
  this.builder = new LoweringBuilder()
264
331
 
265
332
  // Map parameters
266
- for (const param of runtimeParams) {
267
- const paramName = param.name
268
- this.varMap.set(paramName, `$${paramName}`)
269
- this.varTypes.set(paramName, this.normalizeType(param.type))
333
+ if (staticEventDec) {
334
+ for (let i = 0; i < fn.params.length; i++) {
335
+ const param = fn.params[i]
336
+ const expected = eventParamSpecs[i]
337
+ const normalizedType = this.normalizeType(param.type)
338
+ this.varTypes.set(param.name, normalizedType)
339
+
340
+ if (expected?.type.kind === 'entity') {
341
+ this.varMap.set(param.name, '@s')
342
+ continue
343
+ }
344
+
345
+ if (expected?.type.kind === 'named' && expected.type.name === 'string') {
346
+ this.stringValues.set(param.name, '')
347
+ continue
348
+ }
349
+
350
+ this.varMap.set(param.name, `$${param.name}`)
351
+ }
352
+ } else {
353
+ for (const param of runtimeParams) {
354
+ const paramName = param.name
355
+ this.varMap.set(paramName, `$${paramName}`)
356
+ this.varTypes.set(paramName, this.normalizeType(param.type))
357
+ }
270
358
  }
271
359
  for (const param of fn.params) {
272
360
  if (callbackBindings.has(param.name)) {
@@ -284,6 +372,16 @@ export class Lowering {
284
372
  this.builder.emitAssign(varName, { kind: 'var', name: `$p${i}` })
285
373
  }
286
374
 
375
+ if (staticEventDec) {
376
+ for (let i = 0; i < fn.params.length; i++) {
377
+ const param = fn.params[i]
378
+ const expected = eventParamSpecs[i]
379
+ if (expected?.type.kind === 'named' && expected.type.name !== 'string') {
380
+ this.builder.emitAssign(`$${param.name}`, { kind: 'const', value: 0 })
381
+ }
382
+ }
383
+ }
384
+
287
385
  // Lower body
288
386
  this.lowerBlock(fn.body)
289
387
 
@@ -336,6 +434,13 @@ export class Lowering {
336
434
  }
337
435
  }
338
436
 
437
+ if (eventType && isEventTypeName(eventType)) {
438
+ irFn.eventHandler = {
439
+ eventType,
440
+ tag: EVENT_TYPES[eventType].tag,
441
+ }
442
+ }
443
+
339
444
  // Check for @load decorator
340
445
  if (fn.decorators.some(d => d.name === 'load')) {
341
446
  irFn.isLoadInit = true
@@ -489,6 +594,16 @@ export class Lowering {
489
594
  return
490
595
  }
491
596
 
597
+ if (stmt.init.kind === 'call' && stmt.init.fn === 'setInterval') {
598
+ const value = this.lowerExpr(stmt.init)
599
+ const intervalFn = this.intervalFunctions.get(value.kind === 'const' ? value.value : NaN)
600
+ if (intervalFn) {
601
+ this.intervalBindings.set(stmt.name, intervalFn)
602
+ }
603
+ this.builder.emitAssign(varName, value)
604
+ return
605
+ }
606
+
492
607
  // Handle struct literal initialization
493
608
  if (stmt.init.kind === 'struct_lit' && stmt.type?.kind === 'struct') {
494
609
  const structName = stmt.type.name.toLowerCase()
@@ -567,6 +682,11 @@ export class Lowering {
567
682
  }
568
683
 
569
684
  private lowerIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
685
+ if (stmt.cond.kind === 'is_check') {
686
+ this.lowerIsCheckIfStmt(stmt)
687
+ return
688
+ }
689
+
570
690
  const condVar = this.lowerExpr(stmt.cond)
571
691
  const condName = this.operandToVar(condVar)
572
692
 
@@ -596,6 +716,66 @@ export class Lowering {
596
716
  this.builder.startBlock(mergeLabel)
597
717
  }
598
718
 
719
+ private lowerIsCheckIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
720
+ const cond = stmt.cond
721
+ if (cond.kind !== 'is_check') {
722
+ throw new DiagnosticError(
723
+ 'LoweringError',
724
+ "Internal error: expected 'is' check condition",
725
+ stmt.span ?? { line: 0, col: 0 }
726
+ )
727
+ }
728
+
729
+ if (stmt.else_) {
730
+ throw new DiagnosticError(
731
+ 'LoweringError',
732
+ "'is' checks with else branches are not yet supported",
733
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
734
+ )
735
+ }
736
+
737
+ const selector = this.exprToEntitySelector(cond.expr)
738
+ if (!selector) {
739
+ throw new DiagnosticError(
740
+ 'LoweringError',
741
+ "'is' checks require an entity selector or entity binding",
742
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
743
+ )
744
+ }
745
+
746
+ const mcType = ENTITY_TO_MC_TYPE[cond.entityType]
747
+ if (!mcType) {
748
+ throw new DiagnosticError(
749
+ 'LoweringError',
750
+ `Cannot lower entity type check for '${cond.entityType}'`,
751
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
752
+ )
753
+ }
754
+
755
+ const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`
756
+ this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`)
757
+
758
+ const savedBuilder = this.builder
759
+ const savedVarMap = new Map(this.varMap)
760
+ const savedBlockPosVars = new Map(this.blockPosVars)
761
+
762
+ this.builder = new LoweringBuilder()
763
+ this.varMap = new Map(savedVarMap)
764
+ this.blockPosVars = new Map(savedBlockPosVars)
765
+
766
+ this.builder.startBlock('entry')
767
+ this.lowerBlock(stmt.then)
768
+ if (!this.builder.isBlockSealed()) {
769
+ this.builder.emitReturn()
770
+ }
771
+
772
+ this.functions.push(this.builder.build(thenFnName, [], false))
773
+
774
+ this.builder = savedBuilder
775
+ this.varMap = savedVarMap
776
+ this.blockPosVars = savedBlockPosVars
777
+ }
778
+
599
779
  private lowerWhileStmt(stmt: Extract<Stmt, { kind: 'while' }>): void {
600
780
  const checkLabel = this.builder.freshLabel('loop_check')
601
781
  const bodyLabel = this.builder.freshLabel('loop_body')
@@ -1098,6 +1278,13 @@ export class Lowering {
1098
1278
  case 'binary':
1099
1279
  return this.lowerBinaryExpr(expr)
1100
1280
 
1281
+ case 'is_check':
1282
+ throw new DiagnosticError(
1283
+ 'LoweringError',
1284
+ "'is' checks are only supported as if conditions",
1285
+ expr.span ?? { line: 0, col: 0 }
1286
+ )
1287
+
1101
1288
  case 'unary':
1102
1289
  return this.lowerUnaryExpr(expr)
1103
1290
 
@@ -1107,6 +1294,9 @@ export class Lowering {
1107
1294
  case 'call':
1108
1295
  return this.lowerCallExpr(expr)
1109
1296
 
1297
+ case 'static_call':
1298
+ return this.lowerStaticCallExpr(expr)
1299
+
1110
1300
  case 'invoke':
1111
1301
  return this.lowerInvokeExpr(expr)
1112
1302
 
@@ -1454,6 +1644,11 @@ export class Lowering {
1454
1644
  return this.emitDirectFunctionCall(callbackTarget, expr.args)
1455
1645
  }
1456
1646
 
1647
+ const implMethod = this.resolveInstanceMethod(expr)
1648
+ if (implMethod) {
1649
+ return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args)
1650
+ }
1651
+
1457
1652
  // Regular function call
1458
1653
  const fnDecl = this.fnDecls.get(expr.fn)
1459
1654
  const defaultArgs = this.functionDefaults.get(expr.fn) ?? []
@@ -1483,8 +1678,9 @@ export class Lowering {
1483
1678
  runtimeArgs.push(fullArgs[i])
1484
1679
  }
1485
1680
 
1486
- const targetFn = callbackBindings.size > 0
1487
- ? this.ensureSpecializedFunction(fnDecl, callbackBindings)
1681
+ const stdlibCallSite = this.getStdlibCallSiteContext(fnDecl, getSpan(expr))
1682
+ const targetFn = callbackBindings.size > 0 || stdlibCallSite
1683
+ ? this.ensureSpecializedFunctionWithContext(fnDecl, callbackBindings, stdlibCallSite)
1488
1684
  : expr.fn
1489
1685
  return this.emitDirectFunctionCall(targetFn, runtimeArgs)
1490
1686
  }
@@ -1492,6 +1688,12 @@ export class Lowering {
1492
1688
  return this.emitDirectFunctionCall(expr.fn, fullArgs)
1493
1689
  }
1494
1690
 
1691
+ private lowerStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): Operand {
1692
+ const method = this.implMethods.get(expr.type)?.get(expr.method)
1693
+ const targetFn = method?.loweredName ?? `${expr.type}_${expr.method}`
1694
+ return this.emitMethodCall(targetFn, method?.fn, expr.args)
1695
+ }
1696
+
1495
1697
  private lowerInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): Operand {
1496
1698
  if (expr.callee.kind === 'lambda') {
1497
1699
  if (!Array.isArray(expr.callee.body)) {
@@ -1543,6 +1745,19 @@ export class Lowering {
1543
1745
  return { kind: 'var', name: dst }
1544
1746
  }
1545
1747
 
1748
+ private emitMethodCall(fn: string, fnDecl: FnDecl | undefined, args: Expr[]): Operand {
1749
+ const defaultArgs = this.functionDefaults.get(fn) ?? fnDecl?.params.map(param => param.default) ?? []
1750
+ const fullArgs = [...args]
1751
+ for (let i = fullArgs.length; i < defaultArgs.length; i++) {
1752
+ const defaultExpr = defaultArgs[i]
1753
+ if (!defaultExpr) {
1754
+ break
1755
+ }
1756
+ fullArgs.push(defaultExpr)
1757
+ }
1758
+ return this.emitDirectFunctionCall(fn, fullArgs)
1759
+ }
1760
+
1546
1761
  private resolveFunctionRefExpr(expr: Expr): string | null {
1547
1762
  if (expr.kind === 'lambda') {
1548
1763
  return this.lowerLambdaExpr(expr)
@@ -1558,9 +1773,21 @@ export class Lowering {
1558
1773
  }
1559
1774
 
1560
1775
  private ensureSpecializedFunction(fn: FnDecl, callbackBindings: Map<string, string>): string {
1776
+ return this.ensureSpecializedFunctionWithContext(fn, callbackBindings)
1777
+ }
1778
+
1779
+ private ensureSpecializedFunctionWithContext(
1780
+ fn: FnDecl,
1781
+ callbackBindings: Map<string, string>,
1782
+ stdlibCallSite?: StdlibCallSiteContext
1783
+ ): string {
1561
1784
  const parts = [...callbackBindings.entries()]
1562
1785
  .sort(([left], [right]) => left.localeCompare(right))
1563
1786
  .map(([param, target]) => `${param}_${target.replace(/[^a-zA-Z0-9_]/g, '_')}`)
1787
+ const callSiteHash = stdlibCallSite ? this.shortHash(this.serializeCallSite(stdlibCallSite)) : null
1788
+ if (callSiteHash) {
1789
+ parts.push(`callsite_${callSiteHash}`)
1790
+ }
1564
1791
  const key = `${fn.name}::${parts.join('::')}`
1565
1792
  const cached = this.specializedFunctions.get(key)
1566
1793
  if (cached) {
@@ -1570,7 +1797,7 @@ export class Lowering {
1570
1797
  const specializedName = `${fn.name}__${parts.join('__')}`
1571
1798
  this.specializedFunctions.set(key, specializedName)
1572
1799
  this.withSavedFunctionState(() => {
1573
- this.lowerFn(fn, { name: specializedName, callbackBindings })
1800
+ this.lowerFn(fn, { name: specializedName, callbackBindings, stdlibCallSite })
1574
1801
  })
1575
1802
  return specializedName
1576
1803
  }
@@ -1595,10 +1822,12 @@ export class Lowering {
1595
1822
 
1596
1823
  private withSavedFunctionState<T>(callback: () => T): T {
1597
1824
  const savedCurrentFn = this.currentFn
1825
+ const savedStdlibCallSite = this.currentStdlibCallSite
1598
1826
  const savedForeachCounter = this.foreachCounter
1599
1827
  const savedBuilder = this.builder
1600
1828
  const savedVarMap = new Map(this.varMap)
1601
1829
  const savedLambdaBindings = new Map(this.lambdaBindings)
1830
+ const savedIntervalBindings = new Map(this.intervalBindings)
1602
1831
  const savedCallbackBindings = new Map(this.currentCallbackBindings)
1603
1832
  const savedContext = this.currentContext
1604
1833
  const savedBlockPosVars = new Map(this.blockPosVars)
@@ -1609,10 +1838,12 @@ export class Lowering {
1609
1838
  return callback()
1610
1839
  } finally {
1611
1840
  this.currentFn = savedCurrentFn
1841
+ this.currentStdlibCallSite = savedStdlibCallSite
1612
1842
  this.foreachCounter = savedForeachCounter
1613
1843
  this.builder = savedBuilder
1614
1844
  this.varMap = savedVarMap
1615
1845
  this.lambdaBindings = savedLambdaBindings
1846
+ this.intervalBindings = savedIntervalBindings
1616
1847
  this.currentCallbackBindings = savedCallbackBindings
1617
1848
  this.currentContext = savedContext
1618
1849
  this.blockPosVars = savedBlockPosVars
@@ -1628,6 +1859,18 @@ export class Lowering {
1628
1859
  return { kind: 'const', value: 0 }
1629
1860
  }
1630
1861
 
1862
+ if (name === 'setTimeout') {
1863
+ return this.lowerSetTimeout(args)
1864
+ }
1865
+
1866
+ if (name === 'setInterval') {
1867
+ return this.lowerSetInterval(args)
1868
+ }
1869
+
1870
+ if (name === 'clearInterval') {
1871
+ return this.lowerClearInterval(args, callSpan)
1872
+ }
1873
+
1631
1874
  // Special case: random - legacy scoreboard RNG for pre-1.20.3 compatibility
1632
1875
  if (name === 'random') {
1633
1876
  const dst = this.builder.freshTemp()
@@ -1658,7 +1901,7 @@ export class Lowering {
1658
1901
  if (name === 'scoreboard_get' || name === 'score') {
1659
1902
  const dst = this.builder.freshTemp()
1660
1903
  const player = this.exprToTargetString(args[0])
1661
- const objective = this.exprToString(args[1])
1904
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
1662
1905
  this.builder.emitRaw(`execute store result score ${dst} rs run scoreboard players get ${player} ${objective}`)
1663
1906
  return { kind: 'var', name: dst }
1664
1907
  }
@@ -1666,7 +1909,7 @@ export class Lowering {
1666
1909
  // Special case: scoreboard_set — write to vanilla MC scoreboard
1667
1910
  if (name === 'scoreboard_set') {
1668
1911
  const player = this.exprToTargetString(args[0])
1669
- const objective = this.exprToString(args[1])
1912
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
1670
1913
  const value = this.lowerExpr(args[2])
1671
1914
  if (value.kind === 'const') {
1672
1915
  this.builder.emitRaw(`scoreboard players set ${player} ${objective} ${value.value}`)
@@ -1680,7 +1923,7 @@ export class Lowering {
1680
1923
 
1681
1924
  if (name === 'scoreboard_display') {
1682
1925
  const slot = this.exprToString(args[0])
1683
- const objective = this.exprToString(args[1])
1926
+ const objective = this.resolveScoreboardObjective(undefined, args[1], callSpan)
1684
1927
  this.builder.emitRaw(`scoreboard objectives setdisplay ${slot} ${objective}`)
1685
1928
  return { kind: 'const', value: 0 }
1686
1929
  }
@@ -1692,7 +1935,7 @@ export class Lowering {
1692
1935
  }
1693
1936
 
1694
1937
  if (name === 'scoreboard_add_objective') {
1695
- const objective = this.exprToString(args[0])
1938
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
1696
1939
  const criteria = this.exprToString(args[1])
1697
1940
  const displayName = args[2] ? ` ${this.exprToQuotedString(args[2])}` : ''
1698
1941
  this.builder.emitRaw(`scoreboard objectives add ${objective} ${criteria}${displayName}`)
@@ -1700,7 +1943,7 @@ export class Lowering {
1700
1943
  }
1701
1944
 
1702
1945
  if (name === 'scoreboard_remove_objective') {
1703
- const objective = this.exprToString(args[0])
1946
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
1704
1947
  this.builder.emitRaw(`scoreboard objectives remove ${objective}`)
1705
1948
  return { kind: 'const', value: 0 }
1706
1949
  }
@@ -1902,6 +2145,119 @@ export class Lowering {
1902
2145
  return { kind: 'const', value: 0 }
1903
2146
  }
1904
2147
 
2148
+ private lowerSetTimeout(args: Expr[]): Operand {
2149
+ const delay = this.exprToLiteral(args[0])
2150
+ const callback = args[1]
2151
+ if (!callback || callback.kind !== 'lambda') {
2152
+ throw new DiagnosticError(
2153
+ 'LoweringError',
2154
+ 'setTimeout requires a lambda callback',
2155
+ getSpan(callback) ?? { line: 1, col: 1 }
2156
+ )
2157
+ }
2158
+
2159
+ const fnName = `__timeout_${this.timeoutCounter++}`
2160
+ this.lowerNamedLambdaFunction(fnName, callback)
2161
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
2162
+ return { kind: 'const', value: 0 }
2163
+ }
2164
+
2165
+ private lowerSetInterval(args: Expr[]): Operand {
2166
+ const delay = this.exprToLiteral(args[0])
2167
+ const callback = args[1]
2168
+ if (!callback || callback.kind !== 'lambda') {
2169
+ throw new DiagnosticError(
2170
+ 'LoweringError',
2171
+ 'setInterval requires a lambda callback',
2172
+ getSpan(callback) ?? { line: 1, col: 1 }
2173
+ )
2174
+ }
2175
+
2176
+ const id = this.intervalCounter++
2177
+ const bodyName = `__interval_body_${id}`
2178
+ const fnName = `__interval_${id}`
2179
+
2180
+ this.lowerNamedLambdaFunction(bodyName, callback)
2181
+ this.lowerIntervalWrapperFunction(fnName, bodyName, delay)
2182
+ this.intervalFunctions.set(id, fnName)
2183
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
2184
+
2185
+ return { kind: 'const', value: id }
2186
+ }
2187
+
2188
+ private lowerClearInterval(args: Expr[], callSpan?: Span): Operand {
2189
+ const fnName = this.resolveIntervalFunctionName(args[0])
2190
+ if (!fnName) {
2191
+ throw new DiagnosticError(
2192
+ 'LoweringError',
2193
+ 'clearInterval requires an interval ID returned from setInterval',
2194
+ callSpan ?? getSpan(args[0]) ?? { line: 1, col: 1 }
2195
+ )
2196
+ }
2197
+
2198
+ this.builder.emitRaw(`schedule clear ${this.namespace}:${fnName}`)
2199
+ return { kind: 'const', value: 0 }
2200
+ }
2201
+
2202
+ private lowerNamedLambdaFunction(name: string, expr: Extract<Expr, { kind: 'lambda' }>): void {
2203
+ const lambdaFn: FnDecl = {
2204
+ name,
2205
+ params: expr.params.map(param => ({
2206
+ name: param.name,
2207
+ type: param.type ?? { kind: 'named', name: 'int' },
2208
+ })),
2209
+ returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
2210
+ decorators: [],
2211
+ body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
2212
+ }
2213
+
2214
+ this.withSavedFunctionState(() => {
2215
+ this.lowerFn(lambdaFn)
2216
+ })
2217
+ }
2218
+
2219
+ private lowerIntervalWrapperFunction(name: string, bodyName: string, delay: string): void {
2220
+ const intervalFn: FnDecl = {
2221
+ name,
2222
+ params: [],
2223
+ returnType: { kind: 'named', name: 'void' },
2224
+ decorators: [],
2225
+ body: [
2226
+ { kind: 'raw', cmd: `function ${this.namespace}:${bodyName}` },
2227
+ { kind: 'raw', cmd: `schedule function ${this.namespace}:${name} ${delay}t` },
2228
+ ],
2229
+ }
2230
+
2231
+ this.withSavedFunctionState(() => {
2232
+ this.lowerFn(intervalFn)
2233
+ })
2234
+ }
2235
+
2236
+ private resolveIntervalFunctionName(expr: Expr | undefined): string | null {
2237
+ if (!expr) {
2238
+ return null
2239
+ }
2240
+
2241
+ if (expr.kind === 'ident') {
2242
+ const boundInterval = this.intervalBindings.get(expr.name)
2243
+ if (boundInterval) {
2244
+ return boundInterval
2245
+ }
2246
+
2247
+ const constValue = this.constValues.get(expr.name)
2248
+ if (constValue?.kind === 'int_lit') {
2249
+ return this.intervalFunctions.get(constValue.value) ?? null
2250
+ }
2251
+ return null
2252
+ }
2253
+
2254
+ if (expr.kind === 'int_lit') {
2255
+ return this.intervalFunctions.get(expr.value) ?? null
2256
+ }
2257
+
2258
+ return null
2259
+ }
2260
+
1905
2261
  private lowerRichTextBuiltin(name: string, args: Expr[]): string | null {
1906
2262
  const messageArgIndex = this.getRichTextArgIndex(name)
1907
2263
  if (messageArgIndex === null) {
@@ -2078,6 +2434,32 @@ export class Lowering {
2078
2434
  }
2079
2435
  }
2080
2436
 
2437
+ private exprToEntitySelector(expr: Expr): string | null {
2438
+ if (expr.kind === 'selector') {
2439
+ return this.selectorToString(expr.sel)
2440
+ }
2441
+
2442
+ if (expr.kind === 'ident') {
2443
+ const constValue = this.constValues.get(expr.name)
2444
+ if (constValue) {
2445
+ return this.exprToEntitySelector(constValue)
2446
+ }
2447
+ const mapped = this.varMap.get(expr.name)
2448
+ if (mapped?.startsWith('@')) {
2449
+ return mapped
2450
+ }
2451
+ }
2452
+
2453
+ return null
2454
+ }
2455
+
2456
+ private appendTypeFilter(selector: string, mcType: string): string {
2457
+ if (selector.endsWith(']')) {
2458
+ return `${selector.slice(0, -1)},type=${mcType}]`
2459
+ }
2460
+ return `${selector}[type=${mcType}]`
2461
+ }
2462
+
2081
2463
  private exprToSnbt(expr: Expr): string {
2082
2464
  switch (expr.kind) {
2083
2465
  case 'struct_lit': {
@@ -2152,6 +2534,113 @@ export class Lowering {
2152
2534
  return option === 'displayName' || option === 'prefix' || option === 'suffix'
2153
2535
  }
2154
2536
 
2537
+ private exprToScoreboardObjective(expr: Expr, span?: Span): string {
2538
+ if (expr.kind === 'mc_name') {
2539
+ return expr.value
2540
+ }
2541
+
2542
+ const objective = this.exprToString(expr)
2543
+ if (objective.startsWith('#') || objective.includes('.')) {
2544
+ return objective.startsWith('#') ? objective.slice(1) : objective
2545
+ }
2546
+
2547
+ return `${this.getObjectiveNamespace(span)}.${objective}`
2548
+ }
2549
+
2550
+ private resolveScoreboardObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string {
2551
+ const stdlibInternalObjective = this.tryGetStdlibInternalObjective(playerExpr, objectiveExpr, span)
2552
+ if (stdlibInternalObjective) {
2553
+ return stdlibInternalObjective
2554
+ }
2555
+ return this.exprToScoreboardObjective(objectiveExpr, span)
2556
+ }
2557
+
2558
+ private getObjectiveNamespace(span?: Span): string {
2559
+ const filePath = this.filePathForSpan(span)
2560
+ if (!filePath) {
2561
+ return this.namespace
2562
+ }
2563
+
2564
+ return this.isStdlibFile(filePath) ? 'rs' : this.namespace
2565
+ }
2566
+
2567
+ private tryGetStdlibInternalObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string | null {
2568
+ if (!span || !this.currentStdlibCallSite || objectiveExpr.kind !== 'mc_name' || objectiveExpr.value !== 'rs') {
2569
+ return null
2570
+ }
2571
+
2572
+ const filePath = this.filePathForSpan(span)
2573
+ if (!filePath || !this.isStdlibFile(filePath)) {
2574
+ return null
2575
+ }
2576
+
2577
+ const resourceBase = this.getStdlibInternalResourceBase(playerExpr)
2578
+ if (!resourceBase) {
2579
+ return null
2580
+ }
2581
+
2582
+ const hash = this.shortHash(this.serializeCallSite(this.currentStdlibCallSite))
2583
+ return `rs._${resourceBase}_${hash}`
2584
+ }
2585
+
2586
+ private getStdlibInternalResourceBase(playerExpr: Expr | undefined): string | null {
2587
+ if (!playerExpr || playerExpr.kind !== 'str_lit') {
2588
+ return null
2589
+ }
2590
+
2591
+ const match = playerExpr.value.match(/^([a-z0-9]+)_/)
2592
+ return match?.[1] ?? null
2593
+ }
2594
+
2595
+ private getStdlibCallSiteContext(fn: FnDecl, exprSpan?: Span): StdlibCallSiteContext | undefined {
2596
+ const fnFilePath = this.filePathForSpan(getSpan(fn))
2597
+ if (!fnFilePath || !this.isStdlibFile(fnFilePath)) {
2598
+ return undefined
2599
+ }
2600
+
2601
+ if (this.currentStdlibCallSite) {
2602
+ return this.currentStdlibCallSite
2603
+ }
2604
+
2605
+ if (!exprSpan) {
2606
+ return undefined
2607
+ }
2608
+
2609
+ return {
2610
+ filePath: this.filePathForSpan(exprSpan),
2611
+ line: exprSpan.line,
2612
+ col: exprSpan.col,
2613
+ }
2614
+ }
2615
+
2616
+ private serializeCallSite(callSite: StdlibCallSiteContext): string {
2617
+ return `${callSite.filePath ?? '<memory>'}:${callSite.line}:${callSite.col}`
2618
+ }
2619
+
2620
+ private shortHash(input: string): string {
2621
+ let hash = 2166136261
2622
+ for (let i = 0; i < input.length; i++) {
2623
+ hash ^= input.charCodeAt(i)
2624
+ hash = Math.imul(hash, 16777619)
2625
+ }
2626
+ return (hash >>> 0).toString(16).padStart(8, '0').slice(0, 4)
2627
+ }
2628
+
2629
+ private isStdlibFile(filePath: string): boolean {
2630
+ const normalized = path.normalize(filePath)
2631
+ const stdlibSegment = `${path.sep}src${path.sep}stdlib${path.sep}`
2632
+ return normalized.includes(stdlibSegment)
2633
+ }
2634
+
2635
+ private filePathForSpan(span?: Span): string | undefined {
2636
+ if (!span) {
2637
+ return undefined
2638
+ }
2639
+
2640
+ const line = span.line
2641
+ return this.sourceRanges.find(range => line >= range.startLine && line <= range.endLine)?.filePath
2642
+ }
2643
+
2155
2644
  private lowerCoordinateBuiltin(name: string, args: Expr[]): string | null {
2156
2645
  const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
2157
2646
  const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
@@ -2268,7 +2757,11 @@ export class Lowering {
2268
2757
  }
2269
2758
  }
2270
2759
  if (expr.kind === 'call') {
2271
- return this.fnDecls.get(this.resolveFunctionRefByName(expr.fn) ?? expr.fn)?.returnType
2760
+ const resolved = this.resolveFunctionRefByName(expr.fn) ?? this.resolveInstanceMethod(expr)?.loweredName ?? expr.fn
2761
+ return this.fnDecls.get(resolved)?.returnType
2762
+ }
2763
+ if (expr.kind === 'static_call') {
2764
+ return this.implMethods.get(expr.type)?.get(expr.method)?.fn.returnType
2272
2765
  }
2273
2766
  if (expr.kind === 'invoke') {
2274
2767
  const calleeType = this.inferExprType(expr.callee)
@@ -2297,6 +2790,25 @@ export class Lowering {
2297
2790
  return undefined
2298
2791
  }
2299
2792
 
2793
+ private resolveInstanceMethod(expr: Extract<Expr, { kind: 'call' }>): { fn: FnDecl; loweredName: string } | null {
2794
+ const receiver = expr.args[0]
2795
+ if (!receiver) {
2796
+ return null
2797
+ }
2798
+
2799
+ const receiverType = this.inferExprType(receiver)
2800
+ if (receiverType?.kind !== 'struct') {
2801
+ return null
2802
+ }
2803
+
2804
+ const method = this.implMethods.get(receiverType.name)?.get(expr.fn)
2805
+ if (!method || method.fn.params[0]?.name !== 'self') {
2806
+ return null
2807
+ }
2808
+
2809
+ return method
2810
+ }
2811
+
2300
2812
  private normalizeType(type: TypeNode): TypeNode {
2301
2813
  if (type.kind === 'array') {
2302
2814
  return { kind: 'array', elem: this.normalizeType(type.elem) }