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.
Files changed (83) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/cli.test.js +138 -0
  5. package/dist/__tests__/codegen.test.js +25 -0
  6. package/dist/__tests__/dce.test.d.ts +1 -0
  7. package/dist/__tests__/dce.test.js +137 -0
  8. package/dist/__tests__/e2e.test.js +190 -12
  9. package/dist/__tests__/lexer.test.js +31 -4
  10. package/dist/__tests__/lowering.test.js +172 -9
  11. package/dist/__tests__/mc-integration.test.js +145 -51
  12. package/dist/__tests__/mc-syntax.test.js +12 -0
  13. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  14. package/dist/__tests__/parser.test.js +90 -0
  15. package/dist/__tests__/runtime.test.js +21 -8
  16. package/dist/__tests__/typechecker.test.js +188 -0
  17. package/dist/ast/types.d.ts +42 -3
  18. package/dist/cli.js +15 -10
  19. package/dist/codegen/mcfunction/index.js +30 -1
  20. package/dist/codegen/structure/index.d.ts +4 -1
  21. package/dist/codegen/structure/index.js +29 -2
  22. package/dist/compile.d.ts +11 -0
  23. package/dist/compile.js +40 -6
  24. package/dist/events/types.d.ts +35 -0
  25. package/dist/events/types.js +59 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +7 -3
  28. package/dist/ir/types.d.ts +4 -0
  29. package/dist/lexer/index.d.ts +2 -1
  30. package/dist/lexer/index.js +91 -1
  31. package/dist/lowering/index.d.ts +32 -1
  32. package/dist/lowering/index.js +476 -16
  33. package/dist/optimizer/dce.d.ts +23 -0
  34. package/dist/optimizer/dce.js +591 -0
  35. package/dist/parser/index.d.ts +4 -0
  36. package/dist/parser/index.js +160 -26
  37. package/dist/typechecker/index.d.ts +19 -0
  38. package/dist/typechecker/index.js +392 -17
  39. package/docs/ARCHITECTURE.zh.md +1088 -0
  40. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  41. package/editors/vscode/.vscodeignore +3 -0
  42. package/editors/vscode/CHANGELOG.md +9 -0
  43. package/editors/vscode/icon.png +0 -0
  44. package/editors/vscode/out/extension.js +1144 -72
  45. package/editors/vscode/package-lock.json +2 -2
  46. package/editors/vscode/package.json +1 -1
  47. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  48. package/examples/spiral.mcrs +79 -0
  49. package/logo.png +0 -0
  50. package/package.json +1 -1
  51. package/src/__tests__/cli.test.ts +166 -0
  52. package/src/__tests__/codegen.test.ts +27 -0
  53. package/src/__tests__/dce.test.ts +129 -0
  54. package/src/__tests__/e2e.test.ts +201 -12
  55. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  56. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  57. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  58. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  59. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  60. package/src/__tests__/lexer.test.ts +35 -4
  61. package/src/__tests__/lowering.test.ts +187 -9
  62. package/src/__tests__/mc-integration.test.ts +166 -51
  63. package/src/__tests__/mc-syntax.test.ts +14 -0
  64. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  65. package/src/__tests__/parser.test.ts +102 -5
  66. package/src/__tests__/runtime.test.ts +24 -8
  67. package/src/__tests__/typechecker.test.ts +204 -0
  68. package/src/ast/types.ts +39 -2
  69. package/src/cli.ts +24 -10
  70. package/src/codegen/mcfunction/index.ts +31 -1
  71. package/src/codegen/structure/index.ts +40 -2
  72. package/src/compile.ts +59 -7
  73. package/src/events/types.ts +69 -0
  74. package/src/index.ts +9 -4
  75. package/src/ir/types.ts +4 -0
  76. package/src/lexer/index.ts +105 -2
  77. package/src/lowering/index.ts +566 -18
  78. package/src/optimizer/dce.ts +618 -0
  79. package/src/parser/index.ts +187 -29
  80. package/src/stdlib/README.md +34 -4
  81. package/src/stdlib/tags.mcrs +951 -0
  82. package/src/stdlib/timer.mcrs +54 -33
  83. package/src/typechecker/index.ts +469 -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
@@ -22,6 +25,7 @@ import type { GlobalVar } from '../ir/types'
22
25
  const BUILTINS: Record<string, (args: string[]) => string | null> = {
23
26
  say: ([msg]) => `say ${msg}`,
24
27
  tell: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
28
+ tellraw: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
25
29
  title: ([sel, msg]) => `title ${sel} title {"text":"${msg}"}`,
26
30
  actionbar: ([sel, msg]) => `title ${sel} actionbar {"text":"${msg}"}`,
27
31
  subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
@@ -88,6 +92,9 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
88
92
  set_contains: () => null, // Special handling (returns 1/0)
89
93
  set_remove: () => null, // Special handling
90
94
  set_clear: () => null, // Special handling
95
+ setTimeout: () => null, // Special handling
96
+ setInterval: () => null, // Special handling
97
+ clearInterval: () => null, // Special handling
91
98
  }
92
99
 
93
100
  export interface Warning {
@@ -97,6 +104,12 @@ export interface Warning {
97
104
  col?: number
98
105
  }
99
106
 
107
+ interface StdlibCallSiteContext {
108
+ filePath?: string
109
+ line: number
110
+ col: number
111
+ }
112
+
100
113
  function getSpan(node: unknown): Span | undefined {
101
114
  return (node as { span?: Span } | undefined)?.span
102
115
  }
@@ -104,6 +117,23 @@ function getSpan(node: unknown): Span | undefined {
104
117
  const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/
105
118
  const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/
106
119
 
120
+ const ENTITY_TO_MC_TYPE: Partial<Record<EntityTypeName, string>> = {
121
+ Player: 'player',
122
+ Zombie: 'zombie',
123
+ Skeleton: 'skeleton',
124
+ Creeper: 'creeper',
125
+ Spider: 'spider',
126
+ Enderman: 'enderman',
127
+ Pig: 'pig',
128
+ Cow: 'cow',
129
+ Sheep: 'sheep',
130
+ Chicken: 'chicken',
131
+ Villager: 'villager',
132
+ ArmorStand: 'armor_stand',
133
+ Item: 'item',
134
+ Arrow: 'arrow',
135
+ }
136
+
107
137
  function normalizeSelector(selector: string, warnings: Warning[]): string {
108
138
  return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
109
139
  const trimmed = entityType.trim()
@@ -156,20 +186,27 @@ function emitBlockPos(pos: BlockPosExpr): string {
156
186
 
157
187
  export class Lowering {
158
188
  private namespace: string
189
+ private readonly sourceRanges: SourceRange[]
159
190
  private functions: IRFunction[] = []
160
191
  private globals: GlobalVar[] = []
161
192
  private globalNames: Map<string, { mutable: boolean }> = new Map()
162
193
  private fnDecls: Map<string, FnDecl> = new Map()
194
+ private implMethods: Map<string, Map<string, { fn: FnDecl; loweredName: string }>> = new Map()
163
195
  private specializedFunctions: Map<string, string> = new Map()
164
196
  private currentFn: string = ''
197
+ private currentStdlibCallSite?: StdlibCallSiteContext
165
198
  private foreachCounter: number = 0
166
199
  private lambdaCounter: number = 0
200
+ private timeoutCounter: number = 0
201
+ private intervalCounter: number = 0
167
202
  readonly warnings: Warning[] = []
168
203
 
169
204
  // Builder state for current function
170
205
  private builder!: LoweringBuilder
171
206
  private varMap: Map<string, string> = new Map()
172
207
  private lambdaBindings: Map<string, string> = new Map()
208
+ private intervalBindings: Map<string, string> = new Map()
209
+ private intervalFunctions: Map<number, string> = new Map()
173
210
  private currentCallbackBindings: Map<string, string> = new Map()
174
211
  private currentContext: { binding?: string } = {}
175
212
  private blockPosVars: Map<string, BlockPosExpr> = new Map()
@@ -187,8 +224,9 @@ export class Lowering {
187
224
  // World object counter for unique tags
188
225
  private worldObjCounter: number = 0
189
226
 
190
- constructor(namespace: string) {
227
+ constructor(namespace: string, sourceRanges: SourceRange[] = []) {
191
228
  this.namespace = namespace
229
+ this.sourceRanges = sourceRanges
192
230
  LoweringBuilder.resetTempCounter()
193
231
  }
194
232
 
@@ -230,10 +268,31 @@ export class Lowering {
230
268
  this.functionDefaults.set(fn.name, fn.params.map(param => param.default))
231
269
  }
232
270
 
271
+ for (const implBlock of program.implBlocks ?? []) {
272
+ let methods = this.implMethods.get(implBlock.typeName)
273
+ if (!methods) {
274
+ methods = new Map()
275
+ this.implMethods.set(implBlock.typeName, methods)
276
+ }
277
+
278
+ for (const method of implBlock.methods) {
279
+ const loweredName = `${implBlock.typeName}_${method.name}`
280
+ methods.set(method.name, { fn: method, loweredName })
281
+ this.fnDecls.set(loweredName, method)
282
+ this.functionDefaults.set(loweredName, method.params.map(param => param.default))
283
+ }
284
+ }
285
+
233
286
  for (const fn of program.declarations) {
234
287
  this.lowerFn(fn)
235
288
  }
236
289
 
290
+ for (const implBlock of program.implBlocks ?? []) {
291
+ for (const method of implBlock.methods) {
292
+ this.lowerFn(method, { name: `${implBlock.typeName}_${method.name}` })
293
+ }
294
+ }
295
+
237
296
  return buildModule(this.namespace, this.functions, this.globals)
238
297
  }
239
298
 
@@ -246,16 +305,25 @@ export class Lowering {
246
305
  options: {
247
306
  name?: string
248
307
  callbackBindings?: Map<string, string>
308
+ stdlibCallSite?: StdlibCallSiteContext
249
309
  } = {}
250
310
  ): void {
251
311
  const loweredName = options.name ?? fn.name
252
312
  const callbackBindings = options.callbackBindings ?? new Map<string, string>()
253
- const runtimeParams = fn.params.filter(param => !callbackBindings.has(param.name))
313
+ const stdlibCallSite = options.stdlibCallSite
314
+ const staticEventDec = fn.decorators.find(d => d.name === 'on')
315
+ const eventType = staticEventDec?.args?.eventType
316
+ const eventParamSpecs = eventType && isEventTypeName(eventType) ? getEventParamSpecs(eventType) : []
317
+ const runtimeParams = staticEventDec
318
+ ? []
319
+ : fn.params.filter(param => !callbackBindings.has(param.name))
254
320
 
255
321
  this.currentFn = loweredName
322
+ this.currentStdlibCallSite = stdlibCallSite
256
323
  this.foreachCounter = 0
257
324
  this.varMap = new Map()
258
325
  this.lambdaBindings = new Map()
326
+ this.intervalBindings = new Map()
259
327
  this.currentCallbackBindings = new Map(callbackBindings)
260
328
  this.currentContext = {}
261
329
  this.blockPosVars = new Map()
@@ -263,10 +331,31 @@ export class Lowering {
263
331
  this.builder = new LoweringBuilder()
264
332
 
265
333
  // 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))
334
+ if (staticEventDec) {
335
+ for (let i = 0; i < fn.params.length; i++) {
336
+ const param = fn.params[i]
337
+ const expected = eventParamSpecs[i]
338
+ const normalizedType = this.normalizeType(param.type)
339
+ this.varTypes.set(param.name, normalizedType)
340
+
341
+ if (expected?.type.kind === 'entity') {
342
+ this.varMap.set(param.name, '@s')
343
+ continue
344
+ }
345
+
346
+ if (expected?.type.kind === 'named' && expected.type.name === 'string') {
347
+ this.stringValues.set(param.name, '')
348
+ continue
349
+ }
350
+
351
+ this.varMap.set(param.name, `$${param.name}`)
352
+ }
353
+ } else {
354
+ for (const param of runtimeParams) {
355
+ const paramName = param.name
356
+ this.varMap.set(paramName, `$${paramName}`)
357
+ this.varTypes.set(paramName, this.normalizeType(param.type))
358
+ }
270
359
  }
271
360
  for (const param of fn.params) {
272
361
  if (callbackBindings.has(param.name)) {
@@ -284,6 +373,16 @@ export class Lowering {
284
373
  this.builder.emitAssign(varName, { kind: 'var', name: `$p${i}` })
285
374
  }
286
375
 
376
+ if (staticEventDec) {
377
+ for (let i = 0; i < fn.params.length; i++) {
378
+ const param = fn.params[i]
379
+ const expected = eventParamSpecs[i]
380
+ if (expected?.type.kind === 'named' && expected.type.name !== 'string') {
381
+ this.builder.emitAssign(`$${param.name}`, { kind: 'const', value: 0 })
382
+ }
383
+ }
384
+ }
385
+
287
386
  // Lower body
288
387
  this.lowerBlock(fn.body)
289
388
 
@@ -336,6 +435,13 @@ export class Lowering {
336
435
  }
337
436
  }
338
437
 
438
+ if (eventType && isEventTypeName(eventType)) {
439
+ irFn.eventHandler = {
440
+ eventType,
441
+ tag: EVENT_TYPES[eventType].tag,
442
+ }
443
+ }
444
+
339
445
  // Check for @load decorator
340
446
  if (fn.decorators.some(d => d.name === 'load')) {
341
447
  irFn.isLoadInit = true
@@ -489,6 +595,16 @@ export class Lowering {
489
595
  return
490
596
  }
491
597
 
598
+ if (stmt.init.kind === 'call' && stmt.init.fn === 'setInterval') {
599
+ const value = this.lowerExpr(stmt.init)
600
+ const intervalFn = this.intervalFunctions.get(value.kind === 'const' ? value.value : NaN)
601
+ if (intervalFn) {
602
+ this.intervalBindings.set(stmt.name, intervalFn)
603
+ }
604
+ this.builder.emitAssign(varName, value)
605
+ return
606
+ }
607
+
492
608
  // Handle struct literal initialization
493
609
  if (stmt.init.kind === 'struct_lit' && stmt.type?.kind === 'struct') {
494
610
  const structName = stmt.type.name.toLowerCase()
@@ -567,6 +683,11 @@ export class Lowering {
567
683
  }
568
684
 
569
685
  private lowerIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
686
+ if (stmt.cond.kind === 'is_check') {
687
+ this.lowerIsCheckIfStmt(stmt)
688
+ return
689
+ }
690
+
570
691
  const condVar = this.lowerExpr(stmt.cond)
571
692
  const condName = this.operandToVar(condVar)
572
693
 
@@ -596,6 +717,66 @@ export class Lowering {
596
717
  this.builder.startBlock(mergeLabel)
597
718
  }
598
719
 
720
+ private lowerIsCheckIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
721
+ const cond = stmt.cond
722
+ if (cond.kind !== 'is_check') {
723
+ throw new DiagnosticError(
724
+ 'LoweringError',
725
+ "Internal error: expected 'is' check condition",
726
+ stmt.span ?? { line: 0, col: 0 }
727
+ )
728
+ }
729
+
730
+ if (stmt.else_) {
731
+ throw new DiagnosticError(
732
+ 'LoweringError',
733
+ "'is' checks with else branches are not yet supported",
734
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
735
+ )
736
+ }
737
+
738
+ const selector = this.exprToEntitySelector(cond.expr)
739
+ if (!selector) {
740
+ throw new DiagnosticError(
741
+ 'LoweringError',
742
+ "'is' checks require an entity selector or entity binding",
743
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
744
+ )
745
+ }
746
+
747
+ const mcType = ENTITY_TO_MC_TYPE[cond.entityType]
748
+ if (!mcType) {
749
+ throw new DiagnosticError(
750
+ 'LoweringError',
751
+ `Cannot lower entity type check for '${cond.entityType}'`,
752
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
753
+ )
754
+ }
755
+
756
+ const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`
757
+ this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`)
758
+
759
+ const savedBuilder = this.builder
760
+ const savedVarMap = new Map(this.varMap)
761
+ const savedBlockPosVars = new Map(this.blockPosVars)
762
+
763
+ this.builder = new LoweringBuilder()
764
+ this.varMap = new Map(savedVarMap)
765
+ this.blockPosVars = new Map(savedBlockPosVars)
766
+
767
+ this.builder.startBlock('entry')
768
+ this.lowerBlock(stmt.then)
769
+ if (!this.builder.isBlockSealed()) {
770
+ this.builder.emitReturn()
771
+ }
772
+
773
+ this.functions.push(this.builder.build(thenFnName, [], false))
774
+
775
+ this.builder = savedBuilder
776
+ this.varMap = savedVarMap
777
+ this.blockPosVars = savedBlockPosVars
778
+ }
779
+
599
780
  private lowerWhileStmt(stmt: Extract<Stmt, { kind: 'while' }>): void {
600
781
  const checkLabel = this.builder.freshLabel('loop_check')
601
782
  const bodyLabel = this.builder.freshLabel('loop_body')
@@ -1054,6 +1235,7 @@ export class Lowering {
1054
1235
  return { kind: 'const', value: 0 } // Handled inline in exprToString
1055
1236
 
1056
1237
  case 'str_interp':
1238
+ case 'f_string':
1057
1239
  // Interpolated strings are handled inline in message builtins.
1058
1240
  return { kind: 'const', value: 0 }
1059
1241
 
@@ -1098,6 +1280,13 @@ export class Lowering {
1098
1280
  case 'binary':
1099
1281
  return this.lowerBinaryExpr(expr)
1100
1282
 
1283
+ case 'is_check':
1284
+ throw new DiagnosticError(
1285
+ 'LoweringError',
1286
+ "'is' checks are only supported as if conditions",
1287
+ expr.span ?? { line: 0, col: 0 }
1288
+ )
1289
+
1101
1290
  case 'unary':
1102
1291
  return this.lowerUnaryExpr(expr)
1103
1292
 
@@ -1107,6 +1296,9 @@ export class Lowering {
1107
1296
  case 'call':
1108
1297
  return this.lowerCallExpr(expr)
1109
1298
 
1299
+ case 'static_call':
1300
+ return this.lowerStaticCallExpr(expr)
1301
+
1110
1302
  case 'invoke':
1111
1303
  return this.lowerInvokeExpr(expr)
1112
1304
 
@@ -1454,6 +1646,11 @@ export class Lowering {
1454
1646
  return this.emitDirectFunctionCall(callbackTarget, expr.args)
1455
1647
  }
1456
1648
 
1649
+ const implMethod = this.resolveInstanceMethod(expr)
1650
+ if (implMethod) {
1651
+ return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args)
1652
+ }
1653
+
1457
1654
  // Regular function call
1458
1655
  const fnDecl = this.fnDecls.get(expr.fn)
1459
1656
  const defaultArgs = this.functionDefaults.get(expr.fn) ?? []
@@ -1483,8 +1680,9 @@ export class Lowering {
1483
1680
  runtimeArgs.push(fullArgs[i])
1484
1681
  }
1485
1682
 
1486
- const targetFn = callbackBindings.size > 0
1487
- ? this.ensureSpecializedFunction(fnDecl, callbackBindings)
1683
+ const stdlibCallSite = this.getStdlibCallSiteContext(fnDecl, getSpan(expr))
1684
+ const targetFn = callbackBindings.size > 0 || stdlibCallSite
1685
+ ? this.ensureSpecializedFunctionWithContext(fnDecl, callbackBindings, stdlibCallSite)
1488
1686
  : expr.fn
1489
1687
  return this.emitDirectFunctionCall(targetFn, runtimeArgs)
1490
1688
  }
@@ -1492,6 +1690,12 @@ export class Lowering {
1492
1690
  return this.emitDirectFunctionCall(expr.fn, fullArgs)
1493
1691
  }
1494
1692
 
1693
+ private lowerStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): Operand {
1694
+ const method = this.implMethods.get(expr.type)?.get(expr.method)
1695
+ const targetFn = method?.loweredName ?? `${expr.type}_${expr.method}`
1696
+ return this.emitMethodCall(targetFn, method?.fn, expr.args)
1697
+ }
1698
+
1495
1699
  private lowerInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): Operand {
1496
1700
  if (expr.callee.kind === 'lambda') {
1497
1701
  if (!Array.isArray(expr.callee.body)) {
@@ -1543,6 +1747,19 @@ export class Lowering {
1543
1747
  return { kind: 'var', name: dst }
1544
1748
  }
1545
1749
 
1750
+ private emitMethodCall(fn: string, fnDecl: FnDecl | undefined, args: Expr[]): Operand {
1751
+ const defaultArgs = this.functionDefaults.get(fn) ?? fnDecl?.params.map(param => param.default) ?? []
1752
+ const fullArgs = [...args]
1753
+ for (let i = fullArgs.length; i < defaultArgs.length; i++) {
1754
+ const defaultExpr = defaultArgs[i]
1755
+ if (!defaultExpr) {
1756
+ break
1757
+ }
1758
+ fullArgs.push(defaultExpr)
1759
+ }
1760
+ return this.emitDirectFunctionCall(fn, fullArgs)
1761
+ }
1762
+
1546
1763
  private resolveFunctionRefExpr(expr: Expr): string | null {
1547
1764
  if (expr.kind === 'lambda') {
1548
1765
  return this.lowerLambdaExpr(expr)
@@ -1558,9 +1775,21 @@ export class Lowering {
1558
1775
  }
1559
1776
 
1560
1777
  private ensureSpecializedFunction(fn: FnDecl, callbackBindings: Map<string, string>): string {
1778
+ return this.ensureSpecializedFunctionWithContext(fn, callbackBindings)
1779
+ }
1780
+
1781
+ private ensureSpecializedFunctionWithContext(
1782
+ fn: FnDecl,
1783
+ callbackBindings: Map<string, string>,
1784
+ stdlibCallSite?: StdlibCallSiteContext
1785
+ ): string {
1561
1786
  const parts = [...callbackBindings.entries()]
1562
1787
  .sort(([left], [right]) => left.localeCompare(right))
1563
1788
  .map(([param, target]) => `${param}_${target.replace(/[^a-zA-Z0-9_]/g, '_')}`)
1789
+ const callSiteHash = stdlibCallSite ? this.shortHash(this.serializeCallSite(stdlibCallSite)) : null
1790
+ if (callSiteHash) {
1791
+ parts.push(`callsite_${callSiteHash}`)
1792
+ }
1564
1793
  const key = `${fn.name}::${parts.join('::')}`
1565
1794
  const cached = this.specializedFunctions.get(key)
1566
1795
  if (cached) {
@@ -1570,7 +1799,7 @@ export class Lowering {
1570
1799
  const specializedName = `${fn.name}__${parts.join('__')}`
1571
1800
  this.specializedFunctions.set(key, specializedName)
1572
1801
  this.withSavedFunctionState(() => {
1573
- this.lowerFn(fn, { name: specializedName, callbackBindings })
1802
+ this.lowerFn(fn, { name: specializedName, callbackBindings, stdlibCallSite })
1574
1803
  })
1575
1804
  return specializedName
1576
1805
  }
@@ -1595,10 +1824,12 @@ export class Lowering {
1595
1824
 
1596
1825
  private withSavedFunctionState<T>(callback: () => T): T {
1597
1826
  const savedCurrentFn = this.currentFn
1827
+ const savedStdlibCallSite = this.currentStdlibCallSite
1598
1828
  const savedForeachCounter = this.foreachCounter
1599
1829
  const savedBuilder = this.builder
1600
1830
  const savedVarMap = new Map(this.varMap)
1601
1831
  const savedLambdaBindings = new Map(this.lambdaBindings)
1832
+ const savedIntervalBindings = new Map(this.intervalBindings)
1602
1833
  const savedCallbackBindings = new Map(this.currentCallbackBindings)
1603
1834
  const savedContext = this.currentContext
1604
1835
  const savedBlockPosVars = new Map(this.blockPosVars)
@@ -1609,10 +1840,12 @@ export class Lowering {
1609
1840
  return callback()
1610
1841
  } finally {
1611
1842
  this.currentFn = savedCurrentFn
1843
+ this.currentStdlibCallSite = savedStdlibCallSite
1612
1844
  this.foreachCounter = savedForeachCounter
1613
1845
  this.builder = savedBuilder
1614
1846
  this.varMap = savedVarMap
1615
1847
  this.lambdaBindings = savedLambdaBindings
1848
+ this.intervalBindings = savedIntervalBindings
1616
1849
  this.currentCallbackBindings = savedCallbackBindings
1617
1850
  this.currentContext = savedContext
1618
1851
  this.blockPosVars = savedBlockPosVars
@@ -1628,6 +1861,18 @@ export class Lowering {
1628
1861
  return { kind: 'const', value: 0 }
1629
1862
  }
1630
1863
 
1864
+ if (name === 'setTimeout') {
1865
+ return this.lowerSetTimeout(args)
1866
+ }
1867
+
1868
+ if (name === 'setInterval') {
1869
+ return this.lowerSetInterval(args)
1870
+ }
1871
+
1872
+ if (name === 'clearInterval') {
1873
+ return this.lowerClearInterval(args, callSpan)
1874
+ }
1875
+
1631
1876
  // Special case: random - legacy scoreboard RNG for pre-1.20.3 compatibility
1632
1877
  if (name === 'random') {
1633
1878
  const dst = this.builder.freshTemp()
@@ -1658,7 +1903,7 @@ export class Lowering {
1658
1903
  if (name === 'scoreboard_get' || name === 'score') {
1659
1904
  const dst = this.builder.freshTemp()
1660
1905
  const player = this.exprToTargetString(args[0])
1661
- const objective = this.exprToString(args[1])
1906
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
1662
1907
  this.builder.emitRaw(`execute store result score ${dst} rs run scoreboard players get ${player} ${objective}`)
1663
1908
  return { kind: 'var', name: dst }
1664
1909
  }
@@ -1666,7 +1911,7 @@ export class Lowering {
1666
1911
  // Special case: scoreboard_set — write to vanilla MC scoreboard
1667
1912
  if (name === 'scoreboard_set') {
1668
1913
  const player = this.exprToTargetString(args[0])
1669
- const objective = this.exprToString(args[1])
1914
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
1670
1915
  const value = this.lowerExpr(args[2])
1671
1916
  if (value.kind === 'const') {
1672
1917
  this.builder.emitRaw(`scoreboard players set ${player} ${objective} ${value.value}`)
@@ -1680,7 +1925,7 @@ export class Lowering {
1680
1925
 
1681
1926
  if (name === 'scoreboard_display') {
1682
1927
  const slot = this.exprToString(args[0])
1683
- const objective = this.exprToString(args[1])
1928
+ const objective = this.resolveScoreboardObjective(undefined, args[1], callSpan)
1684
1929
  this.builder.emitRaw(`scoreboard objectives setdisplay ${slot} ${objective}`)
1685
1930
  return { kind: 'const', value: 0 }
1686
1931
  }
@@ -1692,7 +1937,7 @@ export class Lowering {
1692
1937
  }
1693
1938
 
1694
1939
  if (name === 'scoreboard_add_objective') {
1695
- const objective = this.exprToString(args[0])
1940
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
1696
1941
  const criteria = this.exprToString(args[1])
1697
1942
  const displayName = args[2] ? ` ${this.exprToQuotedString(args[2])}` : ''
1698
1943
  this.builder.emitRaw(`scoreboard objectives add ${objective} ${criteria}${displayName}`)
@@ -1700,7 +1945,7 @@ export class Lowering {
1700
1945
  }
1701
1946
 
1702
1947
  if (name === 'scoreboard_remove_objective') {
1703
- const objective = this.exprToString(args[0])
1948
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
1704
1949
  this.builder.emitRaw(`scoreboard objectives remove ${objective}`)
1705
1950
  return { kind: 'const', value: 0 }
1706
1951
  }
@@ -1902,6 +2147,119 @@ export class Lowering {
1902
2147
  return { kind: 'const', value: 0 }
1903
2148
  }
1904
2149
 
2150
+ private lowerSetTimeout(args: Expr[]): Operand {
2151
+ const delay = this.exprToLiteral(args[0])
2152
+ const callback = args[1]
2153
+ if (!callback || callback.kind !== 'lambda') {
2154
+ throw new DiagnosticError(
2155
+ 'LoweringError',
2156
+ 'setTimeout requires a lambda callback',
2157
+ getSpan(callback) ?? { line: 1, col: 1 }
2158
+ )
2159
+ }
2160
+
2161
+ const fnName = `__timeout_${this.timeoutCounter++}`
2162
+ this.lowerNamedLambdaFunction(fnName, callback)
2163
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
2164
+ return { kind: 'const', value: 0 }
2165
+ }
2166
+
2167
+ private lowerSetInterval(args: Expr[]): Operand {
2168
+ const delay = this.exprToLiteral(args[0])
2169
+ const callback = args[1]
2170
+ if (!callback || callback.kind !== 'lambda') {
2171
+ throw new DiagnosticError(
2172
+ 'LoweringError',
2173
+ 'setInterval requires a lambda callback',
2174
+ getSpan(callback) ?? { line: 1, col: 1 }
2175
+ )
2176
+ }
2177
+
2178
+ const id = this.intervalCounter++
2179
+ const bodyName = `__interval_body_${id}`
2180
+ const fnName = `__interval_${id}`
2181
+
2182
+ this.lowerNamedLambdaFunction(bodyName, callback)
2183
+ this.lowerIntervalWrapperFunction(fnName, bodyName, delay)
2184
+ this.intervalFunctions.set(id, fnName)
2185
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
2186
+
2187
+ return { kind: 'const', value: id }
2188
+ }
2189
+
2190
+ private lowerClearInterval(args: Expr[], callSpan?: Span): Operand {
2191
+ const fnName = this.resolveIntervalFunctionName(args[0])
2192
+ if (!fnName) {
2193
+ throw new DiagnosticError(
2194
+ 'LoweringError',
2195
+ 'clearInterval requires an interval ID returned from setInterval',
2196
+ callSpan ?? getSpan(args[0]) ?? { line: 1, col: 1 }
2197
+ )
2198
+ }
2199
+
2200
+ this.builder.emitRaw(`schedule clear ${this.namespace}:${fnName}`)
2201
+ return { kind: 'const', value: 0 }
2202
+ }
2203
+
2204
+ private lowerNamedLambdaFunction(name: string, expr: Extract<Expr, { kind: 'lambda' }>): void {
2205
+ const lambdaFn: FnDecl = {
2206
+ name,
2207
+ params: expr.params.map(param => ({
2208
+ name: param.name,
2209
+ type: param.type ?? { kind: 'named', name: 'int' },
2210
+ })),
2211
+ returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
2212
+ decorators: [],
2213
+ body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
2214
+ }
2215
+
2216
+ this.withSavedFunctionState(() => {
2217
+ this.lowerFn(lambdaFn)
2218
+ })
2219
+ }
2220
+
2221
+ private lowerIntervalWrapperFunction(name: string, bodyName: string, delay: string): void {
2222
+ const intervalFn: FnDecl = {
2223
+ name,
2224
+ params: [],
2225
+ returnType: { kind: 'named', name: 'void' },
2226
+ decorators: [],
2227
+ body: [
2228
+ { kind: 'raw', cmd: `function ${this.namespace}:${bodyName}` },
2229
+ { kind: 'raw', cmd: `schedule function ${this.namespace}:${name} ${delay}t` },
2230
+ ],
2231
+ }
2232
+
2233
+ this.withSavedFunctionState(() => {
2234
+ this.lowerFn(intervalFn)
2235
+ })
2236
+ }
2237
+
2238
+ private resolveIntervalFunctionName(expr: Expr | undefined): string | null {
2239
+ if (!expr) {
2240
+ return null
2241
+ }
2242
+
2243
+ if (expr.kind === 'ident') {
2244
+ const boundInterval = this.intervalBindings.get(expr.name)
2245
+ if (boundInterval) {
2246
+ return boundInterval
2247
+ }
2248
+
2249
+ const constValue = this.constValues.get(expr.name)
2250
+ if (constValue?.kind === 'int_lit') {
2251
+ return this.intervalFunctions.get(constValue.value) ?? null
2252
+ }
2253
+ return null
2254
+ }
2255
+
2256
+ if (expr.kind === 'int_lit') {
2257
+ return this.intervalFunctions.get(expr.value) ?? null
2258
+ }
2259
+
2260
+ return null
2261
+ }
2262
+
1905
2263
  private lowerRichTextBuiltin(name: string, args: Expr[]): string | null {
1906
2264
  const messageArgIndex = this.getRichTextArgIndex(name)
1907
2265
  if (messageArgIndex === null) {
@@ -1909,7 +2267,7 @@ export class Lowering {
1909
2267
  }
1910
2268
 
1911
2269
  const messageExpr = args[messageArgIndex]
1912
- if (!messageExpr || messageExpr.kind !== 'str_interp') {
2270
+ if (!messageExpr || (messageExpr.kind !== 'str_interp' && messageExpr.kind !== 'f_string')) {
1913
2271
  return null
1914
2272
  }
1915
2273
 
@@ -1920,6 +2278,7 @@ export class Lowering {
1920
2278
  case 'announce':
1921
2279
  return `tellraw @a ${json}`
1922
2280
  case 'tell':
2281
+ case 'tellraw':
1923
2282
  return `tellraw ${this.exprToString(args[0])} ${json}`
1924
2283
  case 'title':
1925
2284
  return `title ${this.exprToString(args[0])} title ${json}`
@@ -1938,6 +2297,7 @@ export class Lowering {
1938
2297
  case 'announce':
1939
2298
  return 0
1940
2299
  case 'tell':
2300
+ case 'tellraw':
1941
2301
  case 'title':
1942
2302
  case 'actionbar':
1943
2303
  case 'subtitle':
@@ -1947,9 +2307,22 @@ export class Lowering {
1947
2307
  }
1948
2308
  }
1949
2309
 
1950
- private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' }>): string {
2310
+ private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' | 'f_string' }>): string {
1951
2311
  const components: Array<string | Record<string, unknown>> = ['']
1952
2312
 
2313
+ if (expr.kind === 'f_string') {
2314
+ for (const part of expr.parts) {
2315
+ if (part.kind === 'text') {
2316
+ if (part.value.length > 0) {
2317
+ components.push({ text: part.value })
2318
+ }
2319
+ continue
2320
+ }
2321
+ this.appendRichTextExpr(components, part.expr)
2322
+ }
2323
+ return JSON.stringify(components)
2324
+ }
2325
+
1953
2326
  for (const part of expr.parts) {
1954
2327
  if (typeof part === 'string') {
1955
2328
  if (part.length > 0) {
@@ -1998,6 +2371,19 @@ export class Lowering {
1998
2371
  return
1999
2372
  }
2000
2373
 
2374
+ if (expr.kind === 'f_string') {
2375
+ for (const part of expr.parts) {
2376
+ if (part.kind === 'text') {
2377
+ if (part.value.length > 0) {
2378
+ components.push({ text: part.value })
2379
+ }
2380
+ } else {
2381
+ this.appendRichTextExpr(components, part.expr)
2382
+ }
2383
+ }
2384
+ return
2385
+ }
2386
+
2001
2387
  if (expr.kind === 'bool_lit') {
2002
2388
  components.push({ text: expr.value ? 'true' : 'false' })
2003
2389
  return
@@ -2036,6 +2422,10 @@ export class Lowering {
2036
2422
  return `${expr.value}L`
2037
2423
  case 'double_lit':
2038
2424
  return `${expr.value}d`
2425
+ case 'rel_coord':
2426
+ return expr.value // ~ or ~5 or ~-3 - output as-is for MC commands
2427
+ case 'local_coord':
2428
+ return expr.value // ^ or ^5 or ^-3 - output as-is for MC commands
2039
2429
  case 'bool_lit':
2040
2430
  return expr.value ? '1' : '0'
2041
2431
  case 'str_lit':
@@ -2043,6 +2433,7 @@ export class Lowering {
2043
2433
  case 'mc_name':
2044
2434
  return expr.value // #health → "health" (no quotes, used as bare MC name)
2045
2435
  case 'str_interp':
2436
+ case 'f_string':
2046
2437
  return this.buildRichTextJson(expr)
2047
2438
  case 'blockpos':
2048
2439
  return emitBlockPos(expr)
@@ -2078,6 +2469,32 @@ export class Lowering {
2078
2469
  }
2079
2470
  }
2080
2471
 
2472
+ private exprToEntitySelector(expr: Expr): string | null {
2473
+ if (expr.kind === 'selector') {
2474
+ return this.selectorToString(expr.sel)
2475
+ }
2476
+
2477
+ if (expr.kind === 'ident') {
2478
+ const constValue = this.constValues.get(expr.name)
2479
+ if (constValue) {
2480
+ return this.exprToEntitySelector(constValue)
2481
+ }
2482
+ const mapped = this.varMap.get(expr.name)
2483
+ if (mapped?.startsWith('@')) {
2484
+ return mapped
2485
+ }
2486
+ }
2487
+
2488
+ return null
2489
+ }
2490
+
2491
+ private appendTypeFilter(selector: string, mcType: string): string {
2492
+ if (selector.endsWith(']')) {
2493
+ return `${selector.slice(0, -1)},type=${mcType}]`
2494
+ }
2495
+ return `${selector}[type=${mcType}]`
2496
+ }
2497
+
2081
2498
  private exprToSnbt(expr: Expr): string {
2082
2499
  switch (expr.kind) {
2083
2500
  case 'struct_lit': {
@@ -2152,6 +2569,113 @@ export class Lowering {
2152
2569
  return option === 'displayName' || option === 'prefix' || option === 'suffix'
2153
2570
  }
2154
2571
 
2572
+ private exprToScoreboardObjective(expr: Expr, span?: Span): string {
2573
+ if (expr.kind === 'mc_name') {
2574
+ return expr.value
2575
+ }
2576
+
2577
+ const objective = this.exprToString(expr)
2578
+ if (objective.startsWith('#') || objective.includes('.')) {
2579
+ return objective.startsWith('#') ? objective.slice(1) : objective
2580
+ }
2581
+
2582
+ return `${this.getObjectiveNamespace(span)}.${objective}`
2583
+ }
2584
+
2585
+ private resolveScoreboardObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string {
2586
+ const stdlibInternalObjective = this.tryGetStdlibInternalObjective(playerExpr, objectiveExpr, span)
2587
+ if (stdlibInternalObjective) {
2588
+ return stdlibInternalObjective
2589
+ }
2590
+ return this.exprToScoreboardObjective(objectiveExpr, span)
2591
+ }
2592
+
2593
+ private getObjectiveNamespace(span?: Span): string {
2594
+ const filePath = this.filePathForSpan(span)
2595
+ if (!filePath) {
2596
+ return this.namespace
2597
+ }
2598
+
2599
+ return this.isStdlibFile(filePath) ? 'rs' : this.namespace
2600
+ }
2601
+
2602
+ private tryGetStdlibInternalObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string | null {
2603
+ if (!span || !this.currentStdlibCallSite || objectiveExpr.kind !== 'mc_name' || objectiveExpr.value !== 'rs') {
2604
+ return null
2605
+ }
2606
+
2607
+ const filePath = this.filePathForSpan(span)
2608
+ if (!filePath || !this.isStdlibFile(filePath)) {
2609
+ return null
2610
+ }
2611
+
2612
+ const resourceBase = this.getStdlibInternalResourceBase(playerExpr)
2613
+ if (!resourceBase) {
2614
+ return null
2615
+ }
2616
+
2617
+ const hash = this.shortHash(this.serializeCallSite(this.currentStdlibCallSite))
2618
+ return `rs._${resourceBase}_${hash}`
2619
+ }
2620
+
2621
+ private getStdlibInternalResourceBase(playerExpr: Expr | undefined): string | null {
2622
+ if (!playerExpr || playerExpr.kind !== 'str_lit') {
2623
+ return null
2624
+ }
2625
+
2626
+ const match = playerExpr.value.match(/^([a-z0-9]+)_/)
2627
+ return match?.[1] ?? null
2628
+ }
2629
+
2630
+ private getStdlibCallSiteContext(fn: FnDecl, exprSpan?: Span): StdlibCallSiteContext | undefined {
2631
+ const fnFilePath = this.filePathForSpan(getSpan(fn))
2632
+ if (!fnFilePath || !this.isStdlibFile(fnFilePath)) {
2633
+ return undefined
2634
+ }
2635
+
2636
+ if (this.currentStdlibCallSite) {
2637
+ return this.currentStdlibCallSite
2638
+ }
2639
+
2640
+ if (!exprSpan) {
2641
+ return undefined
2642
+ }
2643
+
2644
+ return {
2645
+ filePath: this.filePathForSpan(exprSpan),
2646
+ line: exprSpan.line,
2647
+ col: exprSpan.col,
2648
+ }
2649
+ }
2650
+
2651
+ private serializeCallSite(callSite: StdlibCallSiteContext): string {
2652
+ return `${callSite.filePath ?? '<memory>'}:${callSite.line}:${callSite.col}`
2653
+ }
2654
+
2655
+ private shortHash(input: string): string {
2656
+ let hash = 2166136261
2657
+ for (let i = 0; i < input.length; i++) {
2658
+ hash ^= input.charCodeAt(i)
2659
+ hash = Math.imul(hash, 16777619)
2660
+ }
2661
+ return (hash >>> 0).toString(16).padStart(8, '0').slice(0, 4)
2662
+ }
2663
+
2664
+ private isStdlibFile(filePath: string): boolean {
2665
+ const normalized = path.normalize(filePath)
2666
+ const stdlibSegment = `${path.sep}src${path.sep}stdlib${path.sep}`
2667
+ return normalized.includes(stdlibSegment)
2668
+ }
2669
+
2670
+ private filePathForSpan(span?: Span): string | undefined {
2671
+ if (!span) {
2672
+ return undefined
2673
+ }
2674
+
2675
+ const line = span.line
2676
+ return this.sourceRanges.find(range => line >= range.startLine && line <= range.endLine)?.filePath
2677
+ }
2678
+
2155
2679
  private lowerCoordinateBuiltin(name: string, args: Expr[]): string | null {
2156
2680
  const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
2157
2681
  const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
@@ -2243,6 +2767,7 @@ export class Lowering {
2243
2767
  if (expr.kind === 'float_lit') return { kind: 'named', name: 'float' }
2244
2768
  if (expr.kind === 'bool_lit') return { kind: 'named', name: 'bool' }
2245
2769
  if (expr.kind === 'str_lit' || expr.kind === 'str_interp') return { kind: 'named', name: 'string' }
2770
+ if (expr.kind === 'f_string') return { kind: 'named', name: 'format_string' }
2246
2771
  if (expr.kind === 'blockpos') return { kind: 'named', name: 'BlockPos' }
2247
2772
  if (expr.kind === 'ident') {
2248
2773
  const constValue = this.constValues.get(expr.name)
@@ -2268,7 +2793,11 @@ export class Lowering {
2268
2793
  }
2269
2794
  }
2270
2795
  if (expr.kind === 'call') {
2271
- return this.fnDecls.get(this.resolveFunctionRefByName(expr.fn) ?? expr.fn)?.returnType
2796
+ const resolved = this.resolveFunctionRefByName(expr.fn) ?? this.resolveInstanceMethod(expr)?.loweredName ?? expr.fn
2797
+ return this.fnDecls.get(resolved)?.returnType
2798
+ }
2799
+ if (expr.kind === 'static_call') {
2800
+ return this.implMethods.get(expr.type)?.get(expr.method)?.fn.returnType
2272
2801
  }
2273
2802
  if (expr.kind === 'invoke') {
2274
2803
  const calleeType = this.inferExprType(expr.callee)
@@ -2297,6 +2826,25 @@ export class Lowering {
2297
2826
  return undefined
2298
2827
  }
2299
2828
 
2829
+ private resolveInstanceMethod(expr: Extract<Expr, { kind: 'call' }>): { fn: FnDecl; loweredName: string } | null {
2830
+ const receiver = expr.args[0]
2831
+ if (!receiver) {
2832
+ return null
2833
+ }
2834
+
2835
+ const receiverType = this.inferExprType(receiver)
2836
+ if (receiverType?.kind !== 'struct') {
2837
+ return null
2838
+ }
2839
+
2840
+ const method = this.implMethods.get(receiverType.name)?.get(expr.fn)
2841
+ if (!method || method.fn.params[0]?.name !== 'self') {
2842
+ return null
2843
+ }
2844
+
2845
+ return method
2846
+ }
2847
+
2300
2848
  private normalizeType(type: TypeNode): TypeNode {
2301
2849
  if (type.kind === 'array') {
2302
2850
  return { kind: 'array', elem: this.normalizeType(type.elem) }