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
@@ -5,16 +5,52 @@
5
5
  * Transforms AST into IR (Three-Address Code).
6
6
  * Handles control flow, function extraction for foreach, and builtin calls.
7
7
  */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
8
41
  Object.defineProperty(exports, "__esModule", { value: true });
9
42
  exports.Lowering = void 0;
10
43
  const builder_1 = require("../ir/builder");
11
44
  const diagnostics_1 = require("../diagnostics");
45
+ const path = __importStar(require("path"));
46
+ const types_1 = require("../events/types");
12
47
  // ---------------------------------------------------------------------------
13
48
  // Builtin Functions
14
49
  // ---------------------------------------------------------------------------
15
50
  const BUILTINS = {
16
51
  say: ([msg]) => `say ${msg}`,
17
52
  tell: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
53
+ tellraw: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
18
54
  title: ([sel, msg]) => `title ${sel} title {"text":"${msg}"}`,
19
55
  actionbar: ([sel, msg]) => `title ${sel} actionbar {"text":"${msg}"}`,
20
56
  subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
@@ -80,12 +116,31 @@ const BUILTINS = {
80
116
  set_contains: () => null, // Special handling (returns 1/0)
81
117
  set_remove: () => null, // Special handling
82
118
  set_clear: () => null, // Special handling
119
+ setTimeout: () => null, // Special handling
120
+ setInterval: () => null, // Special handling
121
+ clearInterval: () => null, // Special handling
83
122
  };
84
123
  function getSpan(node) {
85
124
  return node?.span;
86
125
  }
87
126
  const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/;
88
127
  const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/;
128
+ const ENTITY_TO_MC_TYPE = {
129
+ Player: 'player',
130
+ Zombie: 'zombie',
131
+ Skeleton: 'skeleton',
132
+ Creeper: 'creeper',
133
+ Spider: 'spider',
134
+ Enderman: 'enderman',
135
+ Pig: 'pig',
136
+ Cow: 'cow',
137
+ Sheep: 'sheep',
138
+ Chicken: 'chicken',
139
+ Villager: 'villager',
140
+ ArmorStand: 'armor_stand',
141
+ Item: 'item',
142
+ Arrow: 'arrow',
143
+ };
89
144
  function normalizeSelector(selector, warnings) {
90
145
  return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
91
146
  const trimmed = entityType.trim();
@@ -122,18 +177,23 @@ function emitBlockPos(pos) {
122
177
  // Lowering Class
123
178
  // ---------------------------------------------------------------------------
124
179
  class Lowering {
125
- constructor(namespace) {
180
+ constructor(namespace, sourceRanges = []) {
126
181
  this.functions = [];
127
182
  this.globals = [];
128
183
  this.globalNames = new Map();
129
184
  this.fnDecls = new Map();
185
+ this.implMethods = new Map();
130
186
  this.specializedFunctions = new Map();
131
187
  this.currentFn = '';
132
188
  this.foreachCounter = 0;
133
189
  this.lambdaCounter = 0;
190
+ this.timeoutCounter = 0;
191
+ this.intervalCounter = 0;
134
192
  this.warnings = [];
135
193
  this.varMap = new Map();
136
194
  this.lambdaBindings = new Map();
195
+ this.intervalBindings = new Map();
196
+ this.intervalFunctions = new Map();
137
197
  this.currentCallbackBindings = new Map();
138
198
  this.currentContext = {};
139
199
  this.blockPosVars = new Map();
@@ -150,6 +210,7 @@ class Lowering {
150
210
  // World object counter for unique tags
151
211
  this.worldObjCounter = 0;
152
212
  this.namespace = namespace;
213
+ this.sourceRanges = sourceRanges;
153
214
  LoweringBuilder.resetTempCounter();
154
215
  }
155
216
  lower(program) {
@@ -184,9 +245,27 @@ class Lowering {
184
245
  this.fnDecls.set(fn.name, fn);
185
246
  this.functionDefaults.set(fn.name, fn.params.map(param => param.default));
186
247
  }
248
+ for (const implBlock of program.implBlocks ?? []) {
249
+ let methods = this.implMethods.get(implBlock.typeName);
250
+ if (!methods) {
251
+ methods = new Map();
252
+ this.implMethods.set(implBlock.typeName, methods);
253
+ }
254
+ for (const method of implBlock.methods) {
255
+ const loweredName = `${implBlock.typeName}_${method.name}`;
256
+ methods.set(method.name, { fn: method, loweredName });
257
+ this.fnDecls.set(loweredName, method);
258
+ this.functionDefaults.set(loweredName, method.params.map(param => param.default));
259
+ }
260
+ }
187
261
  for (const fn of program.declarations) {
188
262
  this.lowerFn(fn);
189
263
  }
264
+ for (const implBlock of program.implBlocks ?? []) {
265
+ for (const method of implBlock.methods) {
266
+ this.lowerFn(method, { name: `${implBlock.typeName}_${method.name}` });
267
+ }
268
+ }
190
269
  return (0, builder_1.buildModule)(this.namespace, this.functions, this.globals);
191
270
  }
192
271
  // -------------------------------------------------------------------------
@@ -195,21 +274,48 @@ class Lowering {
195
274
  lowerFn(fn, options = {}) {
196
275
  const loweredName = options.name ?? fn.name;
197
276
  const callbackBindings = options.callbackBindings ?? new Map();
198
- const runtimeParams = fn.params.filter(param => !callbackBindings.has(param.name));
277
+ const stdlibCallSite = options.stdlibCallSite;
278
+ const staticEventDec = fn.decorators.find(d => d.name === 'on');
279
+ const eventType = staticEventDec?.args?.eventType;
280
+ const eventParamSpecs = eventType && (0, types_1.isEventTypeName)(eventType) ? (0, types_1.getEventParamSpecs)(eventType) : [];
281
+ const runtimeParams = staticEventDec
282
+ ? []
283
+ : fn.params.filter(param => !callbackBindings.has(param.name));
199
284
  this.currentFn = loweredName;
285
+ this.currentStdlibCallSite = stdlibCallSite;
200
286
  this.foreachCounter = 0;
201
287
  this.varMap = new Map();
202
288
  this.lambdaBindings = new Map();
289
+ this.intervalBindings = new Map();
203
290
  this.currentCallbackBindings = new Map(callbackBindings);
204
291
  this.currentContext = {};
205
292
  this.blockPosVars = new Map();
206
293
  this.stringValues = new Map();
207
294
  this.builder = new LoweringBuilder();
208
295
  // Map parameters
209
- for (const param of runtimeParams) {
210
- const paramName = param.name;
211
- this.varMap.set(paramName, `$${paramName}`);
212
- this.varTypes.set(paramName, this.normalizeType(param.type));
296
+ if (staticEventDec) {
297
+ for (let i = 0; i < fn.params.length; i++) {
298
+ const param = fn.params[i];
299
+ const expected = eventParamSpecs[i];
300
+ const normalizedType = this.normalizeType(param.type);
301
+ this.varTypes.set(param.name, normalizedType);
302
+ if (expected?.type.kind === 'entity') {
303
+ this.varMap.set(param.name, '@s');
304
+ continue;
305
+ }
306
+ if (expected?.type.kind === 'named' && expected.type.name === 'string') {
307
+ this.stringValues.set(param.name, '');
308
+ continue;
309
+ }
310
+ this.varMap.set(param.name, `$${param.name}`);
311
+ }
312
+ }
313
+ else {
314
+ for (const param of runtimeParams) {
315
+ const paramName = param.name;
316
+ this.varMap.set(paramName, `$${paramName}`);
317
+ this.varTypes.set(paramName, this.normalizeType(param.type));
318
+ }
213
319
  }
214
320
  for (const param of fn.params) {
215
321
  if (callbackBindings.has(param.name)) {
@@ -224,6 +330,15 @@ class Lowering {
224
330
  const varName = `$${paramName}`;
225
331
  this.builder.emitAssign(varName, { kind: 'var', name: `$p${i}` });
226
332
  }
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
+ if (expected?.type.kind === 'named' && expected.type.name !== 'string') {
338
+ this.builder.emitAssign(`$${param.name}`, { kind: 'const', value: 0 });
339
+ }
340
+ }
341
+ }
227
342
  // Lower body
228
343
  this.lowerBlock(fn.body);
229
344
  // If no explicit return, add void return
@@ -267,6 +382,12 @@ class Lowering {
267
382
  break;
268
383
  }
269
384
  }
385
+ if (eventType && (0, types_1.isEventTypeName)(eventType)) {
386
+ irFn.eventHandler = {
387
+ eventType,
388
+ tag: types_1.EVENT_TYPES[eventType].tag,
389
+ };
390
+ }
270
391
  // Check for @load decorator
271
392
  if (fn.decorators.some(d => d.name === 'load')) {
272
393
  irFn.isLoadInit = true;
@@ -397,6 +518,15 @@ class Lowering {
397
518
  this.lambdaBindings.set(stmt.name, lambdaName);
398
519
  return;
399
520
  }
521
+ if (stmt.init.kind === 'call' && stmt.init.fn === 'setInterval') {
522
+ const value = this.lowerExpr(stmt.init);
523
+ const intervalFn = this.intervalFunctions.get(value.kind === 'const' ? value.value : NaN);
524
+ if (intervalFn) {
525
+ this.intervalBindings.set(stmt.name, intervalFn);
526
+ }
527
+ this.builder.emitAssign(varName, value);
528
+ return;
529
+ }
400
530
  // Handle struct literal initialization
401
531
  if (stmt.init.kind === 'struct_lit' && stmt.type?.kind === 'struct') {
402
532
  const structName = stmt.type.name.toLowerCase();
@@ -470,6 +600,10 @@ class Lowering {
470
600
  }
471
601
  }
472
602
  lowerIfStmt(stmt) {
603
+ if (stmt.cond.kind === 'is_check') {
604
+ this.lowerIsCheckIfStmt(stmt);
605
+ return;
606
+ }
473
607
  const condVar = this.lowerExpr(stmt.cond);
474
608
  const condName = this.operandToVar(condVar);
475
609
  const thenLabel = this.builder.freshLabel('then');
@@ -493,6 +627,40 @@ class Lowering {
493
627
  // Merge block
494
628
  this.builder.startBlock(mergeLabel);
495
629
  }
630
+ lowerIsCheckIfStmt(stmt) {
631
+ const cond = stmt.cond;
632
+ if (cond.kind !== 'is_check') {
633
+ throw new diagnostics_1.DiagnosticError('LoweringError', "Internal error: expected 'is' check condition", stmt.span ?? { line: 0, col: 0 });
634
+ }
635
+ if (stmt.else_) {
636
+ throw new diagnostics_1.DiagnosticError('LoweringError', "'is' checks with else branches are not yet supported", cond.span ?? stmt.span ?? { line: 0, col: 0 });
637
+ }
638
+ const selector = this.exprToEntitySelector(cond.expr);
639
+ if (!selector) {
640
+ throw new diagnostics_1.DiagnosticError('LoweringError', "'is' checks require an entity selector or entity binding", cond.span ?? stmt.span ?? { line: 0, col: 0 });
641
+ }
642
+ const mcType = ENTITY_TO_MC_TYPE[cond.entityType];
643
+ if (!mcType) {
644
+ throw new diagnostics_1.DiagnosticError('LoweringError', `Cannot lower entity type check for '${cond.entityType}'`, cond.span ?? stmt.span ?? { line: 0, col: 0 });
645
+ }
646
+ const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`;
647
+ this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`);
648
+ const savedBuilder = this.builder;
649
+ const savedVarMap = new Map(this.varMap);
650
+ const savedBlockPosVars = new Map(this.blockPosVars);
651
+ this.builder = new LoweringBuilder();
652
+ this.varMap = new Map(savedVarMap);
653
+ this.blockPosVars = new Map(savedBlockPosVars);
654
+ this.builder.startBlock('entry');
655
+ this.lowerBlock(stmt.then);
656
+ if (!this.builder.isBlockSealed()) {
657
+ this.builder.emitReturn();
658
+ }
659
+ this.functions.push(this.builder.build(thenFnName, [], false));
660
+ this.builder = savedBuilder;
661
+ this.varMap = savedVarMap;
662
+ this.blockPosVars = savedBlockPosVars;
663
+ }
496
664
  lowerWhileStmt(stmt) {
497
665
  const checkLabel = this.builder.freshLabel('loop_check');
498
666
  const bodyLabel = this.builder.freshLabel('loop_body');
@@ -862,6 +1030,7 @@ class Lowering {
862
1030
  // MC names (#health, #red) treated as string constants
863
1031
  return { kind: 'const', value: 0 }; // Handled inline in exprToString
864
1032
  case 'str_interp':
1033
+ case 'f_string':
865
1034
  // Interpolated strings are handled inline in message builtins.
866
1035
  return { kind: 'const', value: 0 };
867
1036
  case 'range_lit':
@@ -899,12 +1068,16 @@ class Lowering {
899
1068
  return { kind: 'var', name: this.selectorToString(expr.sel) };
900
1069
  case 'binary':
901
1070
  return this.lowerBinaryExpr(expr);
1071
+ case 'is_check':
1072
+ throw new diagnostics_1.DiagnosticError('LoweringError', "'is' checks are only supported as if conditions", expr.span ?? { line: 0, col: 0 });
902
1073
  case 'unary':
903
1074
  return this.lowerUnaryExpr(expr);
904
1075
  case 'assign':
905
1076
  return this.lowerAssignExpr(expr);
906
1077
  case 'call':
907
1078
  return this.lowerCallExpr(expr);
1079
+ case 'static_call':
1080
+ return this.lowerStaticCallExpr(expr);
908
1081
  case 'invoke':
909
1082
  return this.lowerInvokeExpr(expr);
910
1083
  case 'member_assign':
@@ -1222,6 +1395,10 @@ class Lowering {
1222
1395
  if (callbackTarget) {
1223
1396
  return this.emitDirectFunctionCall(callbackTarget, expr.args);
1224
1397
  }
1398
+ const implMethod = this.resolveInstanceMethod(expr);
1399
+ if (implMethod) {
1400
+ return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args);
1401
+ }
1225
1402
  // Regular function call
1226
1403
  const fnDecl = this.fnDecls.get(expr.fn);
1227
1404
  const defaultArgs = this.functionDefaults.get(expr.fn) ?? [];
@@ -1248,13 +1425,19 @@ class Lowering {
1248
1425
  }
1249
1426
  runtimeArgs.push(fullArgs[i]);
1250
1427
  }
1251
- const targetFn = callbackBindings.size > 0
1252
- ? this.ensureSpecializedFunction(fnDecl, callbackBindings)
1428
+ const stdlibCallSite = this.getStdlibCallSiteContext(fnDecl, getSpan(expr));
1429
+ const targetFn = callbackBindings.size > 0 || stdlibCallSite
1430
+ ? this.ensureSpecializedFunctionWithContext(fnDecl, callbackBindings, stdlibCallSite)
1253
1431
  : expr.fn;
1254
1432
  return this.emitDirectFunctionCall(targetFn, runtimeArgs);
1255
1433
  }
1256
1434
  return this.emitDirectFunctionCall(expr.fn, fullArgs);
1257
1435
  }
1436
+ lowerStaticCallExpr(expr) {
1437
+ const method = this.implMethods.get(expr.type)?.get(expr.method);
1438
+ const targetFn = method?.loweredName ?? `${expr.type}_${expr.method}`;
1439
+ return this.emitMethodCall(targetFn, method?.fn, expr.args);
1440
+ }
1258
1441
  lowerInvokeExpr(expr) {
1259
1442
  if (expr.callee.kind === 'lambda') {
1260
1443
  if (!Array.isArray(expr.callee.body)) {
@@ -1299,6 +1482,18 @@ class Lowering {
1299
1482
  this.builder.emitCall(fn, loweredArgs, dst);
1300
1483
  return { kind: 'var', name: dst };
1301
1484
  }
1485
+ emitMethodCall(fn, fnDecl, args) {
1486
+ const defaultArgs = this.functionDefaults.get(fn) ?? fnDecl?.params.map(param => param.default) ?? [];
1487
+ const fullArgs = [...args];
1488
+ for (let i = fullArgs.length; i < defaultArgs.length; i++) {
1489
+ const defaultExpr = defaultArgs[i];
1490
+ if (!defaultExpr) {
1491
+ break;
1492
+ }
1493
+ fullArgs.push(defaultExpr);
1494
+ }
1495
+ return this.emitDirectFunctionCall(fn, fullArgs);
1496
+ }
1302
1497
  resolveFunctionRefExpr(expr) {
1303
1498
  if (expr.kind === 'lambda') {
1304
1499
  return this.lowerLambdaExpr(expr);
@@ -1312,9 +1507,16 @@ class Lowering {
1312
1507
  return this.lambdaBindings.get(name) ?? this.currentCallbackBindings.get(name) ?? null;
1313
1508
  }
1314
1509
  ensureSpecializedFunction(fn, callbackBindings) {
1510
+ return this.ensureSpecializedFunctionWithContext(fn, callbackBindings);
1511
+ }
1512
+ ensureSpecializedFunctionWithContext(fn, callbackBindings, stdlibCallSite) {
1315
1513
  const parts = [...callbackBindings.entries()]
1316
1514
  .sort(([left], [right]) => left.localeCompare(right))
1317
1515
  .map(([param, target]) => `${param}_${target.replace(/[^a-zA-Z0-9_]/g, '_')}`);
1516
+ const callSiteHash = stdlibCallSite ? this.shortHash(this.serializeCallSite(stdlibCallSite)) : null;
1517
+ if (callSiteHash) {
1518
+ parts.push(`callsite_${callSiteHash}`);
1519
+ }
1318
1520
  const key = `${fn.name}::${parts.join('::')}`;
1319
1521
  const cached = this.specializedFunctions.get(key);
1320
1522
  if (cached) {
@@ -1323,7 +1525,7 @@ class Lowering {
1323
1525
  const specializedName = `${fn.name}__${parts.join('__')}`;
1324
1526
  this.specializedFunctions.set(key, specializedName);
1325
1527
  this.withSavedFunctionState(() => {
1326
- this.lowerFn(fn, { name: specializedName, callbackBindings });
1528
+ this.lowerFn(fn, { name: specializedName, callbackBindings, stdlibCallSite });
1327
1529
  });
1328
1530
  return specializedName;
1329
1531
  }
@@ -1346,10 +1548,12 @@ class Lowering {
1346
1548
  }
1347
1549
  withSavedFunctionState(callback) {
1348
1550
  const savedCurrentFn = this.currentFn;
1551
+ const savedStdlibCallSite = this.currentStdlibCallSite;
1349
1552
  const savedForeachCounter = this.foreachCounter;
1350
1553
  const savedBuilder = this.builder;
1351
1554
  const savedVarMap = new Map(this.varMap);
1352
1555
  const savedLambdaBindings = new Map(this.lambdaBindings);
1556
+ const savedIntervalBindings = new Map(this.intervalBindings);
1353
1557
  const savedCallbackBindings = new Map(this.currentCallbackBindings);
1354
1558
  const savedContext = this.currentContext;
1355
1559
  const savedBlockPosVars = new Map(this.blockPosVars);
@@ -1360,10 +1564,12 @@ class Lowering {
1360
1564
  }
1361
1565
  finally {
1362
1566
  this.currentFn = savedCurrentFn;
1567
+ this.currentStdlibCallSite = savedStdlibCallSite;
1363
1568
  this.foreachCounter = savedForeachCounter;
1364
1569
  this.builder = savedBuilder;
1365
1570
  this.varMap = savedVarMap;
1366
1571
  this.lambdaBindings = savedLambdaBindings;
1572
+ this.intervalBindings = savedIntervalBindings;
1367
1573
  this.currentCallbackBindings = savedCallbackBindings;
1368
1574
  this.currentContext = savedContext;
1369
1575
  this.blockPosVars = savedBlockPosVars;
@@ -1377,6 +1583,15 @@ class Lowering {
1377
1583
  this.builder.emitRaw(richTextCommand);
1378
1584
  return { kind: 'const', value: 0 };
1379
1585
  }
1586
+ if (name === 'setTimeout') {
1587
+ return this.lowerSetTimeout(args);
1588
+ }
1589
+ if (name === 'setInterval') {
1590
+ return this.lowerSetInterval(args);
1591
+ }
1592
+ if (name === 'clearInterval') {
1593
+ return this.lowerClearInterval(args, callSpan);
1594
+ }
1380
1595
  // Special case: random - legacy scoreboard RNG for pre-1.20.3 compatibility
1381
1596
  if (name === 'random') {
1382
1597
  const dst = this.builder.freshTemp();
@@ -1404,14 +1619,14 @@ class Lowering {
1404
1619
  if (name === 'scoreboard_get' || name === 'score') {
1405
1620
  const dst = this.builder.freshTemp();
1406
1621
  const player = this.exprToTargetString(args[0]);
1407
- const objective = this.exprToString(args[1]);
1622
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan);
1408
1623
  this.builder.emitRaw(`execute store result score ${dst} rs run scoreboard players get ${player} ${objective}`);
1409
1624
  return { kind: 'var', name: dst };
1410
1625
  }
1411
1626
  // Special case: scoreboard_set — write to vanilla MC scoreboard
1412
1627
  if (name === 'scoreboard_set') {
1413
1628
  const player = this.exprToTargetString(args[0]);
1414
- const objective = this.exprToString(args[1]);
1629
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan);
1415
1630
  const value = this.lowerExpr(args[2]);
1416
1631
  if (value.kind === 'const') {
1417
1632
  this.builder.emitRaw(`scoreboard players set ${player} ${objective} ${value.value}`);
@@ -1425,7 +1640,7 @@ class Lowering {
1425
1640
  }
1426
1641
  if (name === 'scoreboard_display') {
1427
1642
  const slot = this.exprToString(args[0]);
1428
- const objective = this.exprToString(args[1]);
1643
+ const objective = this.resolveScoreboardObjective(undefined, args[1], callSpan);
1429
1644
  this.builder.emitRaw(`scoreboard objectives setdisplay ${slot} ${objective}`);
1430
1645
  return { kind: 'const', value: 0 };
1431
1646
  }
@@ -1435,14 +1650,14 @@ class Lowering {
1435
1650
  return { kind: 'const', value: 0 };
1436
1651
  }
1437
1652
  if (name === 'scoreboard_add_objective') {
1438
- const objective = this.exprToString(args[0]);
1653
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan);
1439
1654
  const criteria = this.exprToString(args[1]);
1440
1655
  const displayName = args[2] ? ` ${this.exprToQuotedString(args[2])}` : '';
1441
1656
  this.builder.emitRaw(`scoreboard objectives add ${objective} ${criteria}${displayName}`);
1442
1657
  return { kind: 'const', value: 0 };
1443
1658
  }
1444
1659
  if (name === 'scoreboard_remove_objective') {
1445
- const objective = this.exprToString(args[0]);
1660
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan);
1446
1661
  this.builder.emitRaw(`scoreboard objectives remove ${objective}`);
1447
1662
  return { kind: 'const', value: 0 };
1448
1663
  }
@@ -1616,13 +1831,97 @@ class Lowering {
1616
1831
  }
1617
1832
  return { kind: 'const', value: 0 };
1618
1833
  }
1834
+ lowerSetTimeout(args) {
1835
+ const delay = this.exprToLiteral(args[0]);
1836
+ const callback = args[1];
1837
+ if (!callback || callback.kind !== 'lambda') {
1838
+ throw new diagnostics_1.DiagnosticError('LoweringError', 'setTimeout requires a lambda callback', getSpan(callback) ?? { line: 1, col: 1 });
1839
+ }
1840
+ const fnName = `__timeout_${this.timeoutCounter++}`;
1841
+ this.lowerNamedLambdaFunction(fnName, callback);
1842
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`);
1843
+ return { kind: 'const', value: 0 };
1844
+ }
1845
+ lowerSetInterval(args) {
1846
+ const delay = this.exprToLiteral(args[0]);
1847
+ const callback = args[1];
1848
+ if (!callback || callback.kind !== 'lambda') {
1849
+ throw new diagnostics_1.DiagnosticError('LoweringError', 'setInterval requires a lambda callback', getSpan(callback) ?? { line: 1, col: 1 });
1850
+ }
1851
+ const id = this.intervalCounter++;
1852
+ const bodyName = `__interval_body_${id}`;
1853
+ const fnName = `__interval_${id}`;
1854
+ this.lowerNamedLambdaFunction(bodyName, callback);
1855
+ this.lowerIntervalWrapperFunction(fnName, bodyName, delay);
1856
+ this.intervalFunctions.set(id, fnName);
1857
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`);
1858
+ return { kind: 'const', value: id };
1859
+ }
1860
+ lowerClearInterval(args, callSpan) {
1861
+ const fnName = this.resolveIntervalFunctionName(args[0]);
1862
+ if (!fnName) {
1863
+ throw new diagnostics_1.DiagnosticError('LoweringError', 'clearInterval requires an interval ID returned from setInterval', callSpan ?? getSpan(args[0]) ?? { line: 1, col: 1 });
1864
+ }
1865
+ this.builder.emitRaw(`schedule clear ${this.namespace}:${fnName}`);
1866
+ return { kind: 'const', value: 0 };
1867
+ }
1868
+ lowerNamedLambdaFunction(name, expr) {
1869
+ const lambdaFn = {
1870
+ name,
1871
+ params: expr.params.map(param => ({
1872
+ name: param.name,
1873
+ type: param.type ?? { kind: 'named', name: 'int' },
1874
+ })),
1875
+ returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
1876
+ decorators: [],
1877
+ body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
1878
+ };
1879
+ this.withSavedFunctionState(() => {
1880
+ this.lowerFn(lambdaFn);
1881
+ });
1882
+ }
1883
+ lowerIntervalWrapperFunction(name, bodyName, delay) {
1884
+ const intervalFn = {
1885
+ name,
1886
+ params: [],
1887
+ returnType: { kind: 'named', name: 'void' },
1888
+ decorators: [],
1889
+ body: [
1890
+ { kind: 'raw', cmd: `function ${this.namespace}:${bodyName}` },
1891
+ { kind: 'raw', cmd: `schedule function ${this.namespace}:${name} ${delay}t` },
1892
+ ],
1893
+ };
1894
+ this.withSavedFunctionState(() => {
1895
+ this.lowerFn(intervalFn);
1896
+ });
1897
+ }
1898
+ resolveIntervalFunctionName(expr) {
1899
+ if (!expr) {
1900
+ return null;
1901
+ }
1902
+ if (expr.kind === 'ident') {
1903
+ const boundInterval = this.intervalBindings.get(expr.name);
1904
+ if (boundInterval) {
1905
+ return boundInterval;
1906
+ }
1907
+ const constValue = this.constValues.get(expr.name);
1908
+ if (constValue?.kind === 'int_lit') {
1909
+ return this.intervalFunctions.get(constValue.value) ?? null;
1910
+ }
1911
+ return null;
1912
+ }
1913
+ if (expr.kind === 'int_lit') {
1914
+ return this.intervalFunctions.get(expr.value) ?? null;
1915
+ }
1916
+ return null;
1917
+ }
1619
1918
  lowerRichTextBuiltin(name, args) {
1620
1919
  const messageArgIndex = this.getRichTextArgIndex(name);
1621
1920
  if (messageArgIndex === null) {
1622
1921
  return null;
1623
1922
  }
1624
1923
  const messageExpr = args[messageArgIndex];
1625
- if (!messageExpr || messageExpr.kind !== 'str_interp') {
1924
+ if (!messageExpr || (messageExpr.kind !== 'str_interp' && messageExpr.kind !== 'f_string')) {
1626
1925
  return null;
1627
1926
  }
1628
1927
  const json = this.buildRichTextJson(messageExpr);
@@ -1631,6 +1930,7 @@ class Lowering {
1631
1930
  case 'announce':
1632
1931
  return `tellraw @a ${json}`;
1633
1932
  case 'tell':
1933
+ case 'tellraw':
1634
1934
  return `tellraw ${this.exprToString(args[0])} ${json}`;
1635
1935
  case 'title':
1636
1936
  return `title ${this.exprToString(args[0])} title ${json}`;
@@ -1648,6 +1948,7 @@ class Lowering {
1648
1948
  case 'announce':
1649
1949
  return 0;
1650
1950
  case 'tell':
1951
+ case 'tellraw':
1651
1952
  case 'title':
1652
1953
  case 'actionbar':
1653
1954
  case 'subtitle':
@@ -1658,6 +1959,18 @@ class Lowering {
1658
1959
  }
1659
1960
  buildRichTextJson(expr) {
1660
1961
  const components = [''];
1962
+ if (expr.kind === 'f_string') {
1963
+ for (const part of expr.parts) {
1964
+ if (part.kind === 'text') {
1965
+ if (part.value.length > 0) {
1966
+ components.push({ text: part.value });
1967
+ }
1968
+ continue;
1969
+ }
1970
+ this.appendRichTextExpr(components, part.expr);
1971
+ }
1972
+ return JSON.stringify(components);
1973
+ }
1661
1974
  for (const part of expr.parts) {
1662
1975
  if (typeof part === 'string') {
1663
1976
  if (part.length > 0) {
@@ -1701,6 +2014,19 @@ class Lowering {
1701
2014
  }
1702
2015
  return;
1703
2016
  }
2017
+ if (expr.kind === 'f_string') {
2018
+ for (const part of expr.parts) {
2019
+ if (part.kind === 'text') {
2020
+ if (part.value.length > 0) {
2021
+ components.push({ text: part.value });
2022
+ }
2023
+ }
2024
+ else {
2025
+ this.appendRichTextExpr(components, part.expr);
2026
+ }
2027
+ }
2028
+ return;
2029
+ }
1704
2030
  if (expr.kind === 'bool_lit') {
1705
2031
  components.push({ text: expr.value ? 'true' : 'false' });
1706
2032
  return;
@@ -1734,6 +2060,10 @@ class Lowering {
1734
2060
  return `${expr.value}L`;
1735
2061
  case 'double_lit':
1736
2062
  return `${expr.value}d`;
2063
+ case 'rel_coord':
2064
+ return expr.value; // ~ or ~5 or ~-3 - output as-is for MC commands
2065
+ case 'local_coord':
2066
+ return expr.value; // ^ or ^5 or ^-3 - output as-is for MC commands
1737
2067
  case 'bool_lit':
1738
2068
  return expr.value ? '1' : '0';
1739
2069
  case 'str_lit':
@@ -1741,6 +2071,7 @@ class Lowering {
1741
2071
  case 'mc_name':
1742
2072
  return expr.value; // #health → "health" (no quotes, used as bare MC name)
1743
2073
  case 'str_interp':
2074
+ case 'f_string':
1744
2075
  return this.buildRichTextJson(expr);
1745
2076
  case 'blockpos':
1746
2077
  return emitBlockPos(expr);
@@ -1775,6 +2106,28 @@ class Lowering {
1775
2106
  return this.operandToVar(op);
1776
2107
  }
1777
2108
  }
2109
+ exprToEntitySelector(expr) {
2110
+ if (expr.kind === 'selector') {
2111
+ return this.selectorToString(expr.sel);
2112
+ }
2113
+ if (expr.kind === 'ident') {
2114
+ const constValue = this.constValues.get(expr.name);
2115
+ if (constValue) {
2116
+ return this.exprToEntitySelector(constValue);
2117
+ }
2118
+ const mapped = this.varMap.get(expr.name);
2119
+ if (mapped?.startsWith('@')) {
2120
+ return mapped;
2121
+ }
2122
+ }
2123
+ return null;
2124
+ }
2125
+ appendTypeFilter(selector, mcType) {
2126
+ if (selector.endsWith(']')) {
2127
+ return `${selector.slice(0, -1)},type=${mcType}]`;
2128
+ }
2129
+ return `${selector}[type=${mcType}]`;
2130
+ }
1778
2131
  exprToSnbt(expr) {
1779
2132
  switch (expr.kind) {
1780
2133
  case 'struct_lit': {
@@ -1842,6 +2195,92 @@ class Lowering {
1842
2195
  isTeamTextOption(option) {
1843
2196
  return option === 'displayName' || option === 'prefix' || option === 'suffix';
1844
2197
  }
2198
+ exprToScoreboardObjective(expr, span) {
2199
+ if (expr.kind === 'mc_name') {
2200
+ return expr.value;
2201
+ }
2202
+ const objective = this.exprToString(expr);
2203
+ if (objective.startsWith('#') || objective.includes('.')) {
2204
+ return objective.startsWith('#') ? objective.slice(1) : objective;
2205
+ }
2206
+ return `${this.getObjectiveNamespace(span)}.${objective}`;
2207
+ }
2208
+ resolveScoreboardObjective(playerExpr, objectiveExpr, span) {
2209
+ const stdlibInternalObjective = this.tryGetStdlibInternalObjective(playerExpr, objectiveExpr, span);
2210
+ if (stdlibInternalObjective) {
2211
+ return stdlibInternalObjective;
2212
+ }
2213
+ return this.exprToScoreboardObjective(objectiveExpr, span);
2214
+ }
2215
+ getObjectiveNamespace(span) {
2216
+ const filePath = this.filePathForSpan(span);
2217
+ if (!filePath) {
2218
+ return this.namespace;
2219
+ }
2220
+ return this.isStdlibFile(filePath) ? 'rs' : this.namespace;
2221
+ }
2222
+ tryGetStdlibInternalObjective(playerExpr, objectiveExpr, span) {
2223
+ if (!span || !this.currentStdlibCallSite || objectiveExpr.kind !== 'mc_name' || objectiveExpr.value !== 'rs') {
2224
+ return null;
2225
+ }
2226
+ const filePath = this.filePathForSpan(span);
2227
+ if (!filePath || !this.isStdlibFile(filePath)) {
2228
+ return null;
2229
+ }
2230
+ const resourceBase = this.getStdlibInternalResourceBase(playerExpr);
2231
+ if (!resourceBase) {
2232
+ return null;
2233
+ }
2234
+ const hash = this.shortHash(this.serializeCallSite(this.currentStdlibCallSite));
2235
+ return `rs._${resourceBase}_${hash}`;
2236
+ }
2237
+ getStdlibInternalResourceBase(playerExpr) {
2238
+ if (!playerExpr || playerExpr.kind !== 'str_lit') {
2239
+ return null;
2240
+ }
2241
+ const match = playerExpr.value.match(/^([a-z0-9]+)_/);
2242
+ return match?.[1] ?? null;
2243
+ }
2244
+ getStdlibCallSiteContext(fn, exprSpan) {
2245
+ const fnFilePath = this.filePathForSpan(getSpan(fn));
2246
+ if (!fnFilePath || !this.isStdlibFile(fnFilePath)) {
2247
+ return undefined;
2248
+ }
2249
+ if (this.currentStdlibCallSite) {
2250
+ return this.currentStdlibCallSite;
2251
+ }
2252
+ if (!exprSpan) {
2253
+ return undefined;
2254
+ }
2255
+ return {
2256
+ filePath: this.filePathForSpan(exprSpan),
2257
+ line: exprSpan.line,
2258
+ col: exprSpan.col,
2259
+ };
2260
+ }
2261
+ serializeCallSite(callSite) {
2262
+ return `${callSite.filePath ?? '<memory>'}:${callSite.line}:${callSite.col}`;
2263
+ }
2264
+ shortHash(input) {
2265
+ let hash = 2166136261;
2266
+ for (let i = 0; i < input.length; i++) {
2267
+ hash ^= input.charCodeAt(i);
2268
+ hash = Math.imul(hash, 16777619);
2269
+ }
2270
+ return (hash >>> 0).toString(16).padStart(8, '0').slice(0, 4);
2271
+ }
2272
+ isStdlibFile(filePath) {
2273
+ const normalized = path.normalize(filePath);
2274
+ const stdlibSegment = `${path.sep}src${path.sep}stdlib${path.sep}`;
2275
+ return normalized.includes(stdlibSegment);
2276
+ }
2277
+ filePathForSpan(span) {
2278
+ if (!span) {
2279
+ return undefined;
2280
+ }
2281
+ const line = span.line;
2282
+ return this.sourceRanges.find(range => line >= range.startLine && line <= range.endLine)?.filePath;
2283
+ }
1845
2284
  lowerCoordinateBuiltin(name, args) {
1846
2285
  const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null;
1847
2286
  const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null;
@@ -1923,6 +2362,8 @@ class Lowering {
1923
2362
  return { kind: 'named', name: 'bool' };
1924
2363
  if (expr.kind === 'str_lit' || expr.kind === 'str_interp')
1925
2364
  return { kind: 'named', name: 'string' };
2365
+ if (expr.kind === 'f_string')
2366
+ return { kind: 'named', name: 'format_string' };
1926
2367
  if (expr.kind === 'blockpos')
1927
2368
  return { kind: 'named', name: 'BlockPos' };
1928
2369
  if (expr.kind === 'ident') {
@@ -1949,7 +2390,11 @@ class Lowering {
1949
2390
  };
1950
2391
  }
1951
2392
  if (expr.kind === 'call') {
1952
- return this.fnDecls.get(this.resolveFunctionRefByName(expr.fn) ?? expr.fn)?.returnType;
2393
+ const resolved = this.resolveFunctionRefByName(expr.fn) ?? this.resolveInstanceMethod(expr)?.loweredName ?? expr.fn;
2394
+ return this.fnDecls.get(resolved)?.returnType;
2395
+ }
2396
+ if (expr.kind === 'static_call') {
2397
+ return this.implMethods.get(expr.type)?.get(expr.method)?.fn.returnType;
1953
2398
  }
1954
2399
  if (expr.kind === 'invoke') {
1955
2400
  const calleeType = this.inferExprType(expr.callee);
@@ -1977,6 +2422,21 @@ class Lowering {
1977
2422
  }
1978
2423
  return undefined;
1979
2424
  }
2425
+ resolveInstanceMethod(expr) {
2426
+ const receiver = expr.args[0];
2427
+ if (!receiver) {
2428
+ return null;
2429
+ }
2430
+ const receiverType = this.inferExprType(receiver);
2431
+ if (receiverType?.kind !== 'struct') {
2432
+ return null;
2433
+ }
2434
+ const method = this.implMethods.get(receiverType.name)?.get(expr.fn);
2435
+ if (!method || method.fn.params[0]?.name !== 'self') {
2436
+ return null;
2437
+ }
2438
+ return method;
2439
+ }
1980
2440
  normalizeType(type) {
1981
2441
  if (type.kind === 'array') {
1982
2442
  return { kind: 'array', elem: this.normalizeType(type.elem) };