redscript-mc 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/__tests__/cli.test.js +138 -0
  3. package/dist/__tests__/codegen.test.js +25 -0
  4. package/dist/__tests__/e2e.test.js +190 -12
  5. package/dist/__tests__/lexer.test.js +12 -2
  6. package/dist/__tests__/lowering.test.js +164 -9
  7. package/dist/__tests__/mc-integration.test.js +145 -51
  8. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  9. package/dist/__tests__/parser.test.js +80 -0
  10. package/dist/__tests__/runtime.test.js +8 -8
  11. package/dist/__tests__/typechecker.test.js +158 -0
  12. package/dist/ast/types.d.ts +20 -1
  13. package/dist/codegen/mcfunction/index.js +30 -1
  14. package/dist/codegen/structure/index.js +25 -0
  15. package/dist/compile.d.ts +10 -0
  16. package/dist/compile.js +36 -5
  17. package/dist/events/types.d.ts +35 -0
  18. package/dist/events/types.js +59 -0
  19. package/dist/index.js +3 -2
  20. package/dist/ir/types.d.ts +4 -0
  21. package/dist/lexer/index.d.ts +1 -1
  22. package/dist/lexer/index.js +2 -0
  23. package/dist/lowering/index.d.ts +32 -1
  24. package/dist/lowering/index.js +439 -15
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +79 -10
  27. package/dist/typechecker/index.d.ts +17 -0
  28. package/dist/typechecker/index.js +343 -17
  29. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  30. package/editors/vscode/CHANGELOG.md +9 -0
  31. package/editors/vscode/out/extension.js +1144 -72
  32. package/editors/vscode/package-lock.json +2 -2
  33. package/editors/vscode/package.json +1 -1
  34. package/package.json +1 -1
  35. package/src/__tests__/cli.test.ts +166 -0
  36. package/src/__tests__/codegen.test.ts +27 -0
  37. package/src/__tests__/e2e.test.ts +201 -12
  38. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  39. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  40. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  41. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  42. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  43. package/src/__tests__/lexer.test.ts +14 -2
  44. package/src/__tests__/lowering.test.ts +178 -9
  45. package/src/__tests__/mc-integration.test.ts +166 -51
  46. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  47. package/src/__tests__/parser.test.ts +91 -5
  48. package/src/__tests__/runtime.test.ts +8 -8
  49. package/src/__tests__/typechecker.test.ts +171 -0
  50. package/src/ast/types.ts +25 -1
  51. package/src/codegen/mcfunction/index.ts +31 -1
  52. package/src/codegen/structure/index.ts +27 -0
  53. package/src/compile.ts +54 -6
  54. package/src/events/types.ts +69 -0
  55. package/src/index.ts +4 -3
  56. package/src/ir/types.ts +4 -0
  57. package/src/lexer/index.ts +3 -1
  58. package/src/lowering/index.ts +528 -16
  59. package/src/parser/index.ts +90 -12
  60. package/src/stdlib/README.md +34 -4
  61. package/src/stdlib/tags.mcrs +951 -0
  62. package/src/stdlib/timer.mcrs +54 -33
  63. package/src/typechecker/index.ts +404 -18
@@ -5,10 +5,45 @@
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
  // ---------------------------------------------------------------------------
@@ -80,12 +115,31 @@ const BUILTINS = {
80
115
  set_contains: () => null, // Special handling (returns 1/0)
81
116
  set_remove: () => null, // Special handling
82
117
  set_clear: () => null, // Special handling
118
+ setTimeout: () => null, // Special handling
119
+ setInterval: () => null, // Special handling
120
+ clearInterval: () => null, // Special handling
83
121
  };
84
122
  function getSpan(node) {
85
123
  return node?.span;
86
124
  }
87
125
  const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/;
88
126
  const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/;
127
+ const ENTITY_TO_MC_TYPE = {
128
+ Player: 'player',
129
+ Zombie: 'zombie',
130
+ Skeleton: 'skeleton',
131
+ Creeper: 'creeper',
132
+ Spider: 'spider',
133
+ Enderman: 'enderman',
134
+ Pig: 'pig',
135
+ Cow: 'cow',
136
+ Sheep: 'sheep',
137
+ Chicken: 'chicken',
138
+ Villager: 'villager',
139
+ ArmorStand: 'armor_stand',
140
+ Item: 'item',
141
+ Arrow: 'arrow',
142
+ };
89
143
  function normalizeSelector(selector, warnings) {
90
144
  return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
91
145
  const trimmed = entityType.trim();
@@ -122,18 +176,23 @@ function emitBlockPos(pos) {
122
176
  // Lowering Class
123
177
  // ---------------------------------------------------------------------------
124
178
  class Lowering {
125
- constructor(namespace) {
179
+ constructor(namespace, sourceRanges = []) {
126
180
  this.functions = [];
127
181
  this.globals = [];
128
182
  this.globalNames = new Map();
129
183
  this.fnDecls = new Map();
184
+ this.implMethods = new Map();
130
185
  this.specializedFunctions = new Map();
131
186
  this.currentFn = '';
132
187
  this.foreachCounter = 0;
133
188
  this.lambdaCounter = 0;
189
+ this.timeoutCounter = 0;
190
+ this.intervalCounter = 0;
134
191
  this.warnings = [];
135
192
  this.varMap = new Map();
136
193
  this.lambdaBindings = new Map();
194
+ this.intervalBindings = new Map();
195
+ this.intervalFunctions = new Map();
137
196
  this.currentCallbackBindings = new Map();
138
197
  this.currentContext = {};
139
198
  this.blockPosVars = new Map();
@@ -150,6 +209,7 @@ class Lowering {
150
209
  // World object counter for unique tags
151
210
  this.worldObjCounter = 0;
152
211
  this.namespace = namespace;
212
+ this.sourceRanges = sourceRanges;
153
213
  LoweringBuilder.resetTempCounter();
154
214
  }
155
215
  lower(program) {
@@ -184,9 +244,27 @@ class Lowering {
184
244
  this.fnDecls.set(fn.name, fn);
185
245
  this.functionDefaults.set(fn.name, fn.params.map(param => param.default));
186
246
  }
247
+ for (const implBlock of program.implBlocks ?? []) {
248
+ let methods = this.implMethods.get(implBlock.typeName);
249
+ if (!methods) {
250
+ methods = new Map();
251
+ this.implMethods.set(implBlock.typeName, methods);
252
+ }
253
+ for (const method of implBlock.methods) {
254
+ const loweredName = `${implBlock.typeName}_${method.name}`;
255
+ methods.set(method.name, { fn: method, loweredName });
256
+ this.fnDecls.set(loweredName, method);
257
+ this.functionDefaults.set(loweredName, method.params.map(param => param.default));
258
+ }
259
+ }
187
260
  for (const fn of program.declarations) {
188
261
  this.lowerFn(fn);
189
262
  }
263
+ for (const implBlock of program.implBlocks ?? []) {
264
+ for (const method of implBlock.methods) {
265
+ this.lowerFn(method, { name: `${implBlock.typeName}_${method.name}` });
266
+ }
267
+ }
190
268
  return (0, builder_1.buildModule)(this.namespace, this.functions, this.globals);
191
269
  }
192
270
  // -------------------------------------------------------------------------
@@ -195,21 +273,48 @@ class Lowering {
195
273
  lowerFn(fn, options = {}) {
196
274
  const loweredName = options.name ?? fn.name;
197
275
  const callbackBindings = options.callbackBindings ?? new Map();
198
- const runtimeParams = fn.params.filter(param => !callbackBindings.has(param.name));
276
+ const stdlibCallSite = options.stdlibCallSite;
277
+ const staticEventDec = fn.decorators.find(d => d.name === 'on');
278
+ const eventType = staticEventDec?.args?.eventType;
279
+ const eventParamSpecs = eventType && (0, types_1.isEventTypeName)(eventType) ? (0, types_1.getEventParamSpecs)(eventType) : [];
280
+ const runtimeParams = staticEventDec
281
+ ? []
282
+ : fn.params.filter(param => !callbackBindings.has(param.name));
199
283
  this.currentFn = loweredName;
284
+ this.currentStdlibCallSite = stdlibCallSite;
200
285
  this.foreachCounter = 0;
201
286
  this.varMap = new Map();
202
287
  this.lambdaBindings = new Map();
288
+ this.intervalBindings = new Map();
203
289
  this.currentCallbackBindings = new Map(callbackBindings);
204
290
  this.currentContext = {};
205
291
  this.blockPosVars = new Map();
206
292
  this.stringValues = new Map();
207
293
  this.builder = new LoweringBuilder();
208
294
  // 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));
295
+ if (staticEventDec) {
296
+ for (let i = 0; i < fn.params.length; i++) {
297
+ const param = fn.params[i];
298
+ const expected = eventParamSpecs[i];
299
+ const normalizedType = this.normalizeType(param.type);
300
+ this.varTypes.set(param.name, normalizedType);
301
+ if (expected?.type.kind === 'entity') {
302
+ this.varMap.set(param.name, '@s');
303
+ continue;
304
+ }
305
+ if (expected?.type.kind === 'named' && expected.type.name === 'string') {
306
+ this.stringValues.set(param.name, '');
307
+ continue;
308
+ }
309
+ this.varMap.set(param.name, `$${param.name}`);
310
+ }
311
+ }
312
+ else {
313
+ for (const param of runtimeParams) {
314
+ const paramName = param.name;
315
+ this.varMap.set(paramName, `$${paramName}`);
316
+ this.varTypes.set(paramName, this.normalizeType(param.type));
317
+ }
213
318
  }
214
319
  for (const param of fn.params) {
215
320
  if (callbackBindings.has(param.name)) {
@@ -224,6 +329,15 @@ class Lowering {
224
329
  const varName = `$${paramName}`;
225
330
  this.builder.emitAssign(varName, { kind: 'var', name: `$p${i}` });
226
331
  }
332
+ if (staticEventDec) {
333
+ for (let i = 0; i < fn.params.length; i++) {
334
+ const param = fn.params[i];
335
+ const expected = eventParamSpecs[i];
336
+ if (expected?.type.kind === 'named' && expected.type.name !== 'string') {
337
+ this.builder.emitAssign(`$${param.name}`, { kind: 'const', value: 0 });
338
+ }
339
+ }
340
+ }
227
341
  // Lower body
228
342
  this.lowerBlock(fn.body);
229
343
  // If no explicit return, add void return
@@ -267,6 +381,12 @@ class Lowering {
267
381
  break;
268
382
  }
269
383
  }
384
+ if (eventType && (0, types_1.isEventTypeName)(eventType)) {
385
+ irFn.eventHandler = {
386
+ eventType,
387
+ tag: types_1.EVENT_TYPES[eventType].tag,
388
+ };
389
+ }
270
390
  // Check for @load decorator
271
391
  if (fn.decorators.some(d => d.name === 'load')) {
272
392
  irFn.isLoadInit = true;
@@ -397,6 +517,15 @@ class Lowering {
397
517
  this.lambdaBindings.set(stmt.name, lambdaName);
398
518
  return;
399
519
  }
520
+ if (stmt.init.kind === 'call' && stmt.init.fn === 'setInterval') {
521
+ const value = this.lowerExpr(stmt.init);
522
+ const intervalFn = this.intervalFunctions.get(value.kind === 'const' ? value.value : NaN);
523
+ if (intervalFn) {
524
+ this.intervalBindings.set(stmt.name, intervalFn);
525
+ }
526
+ this.builder.emitAssign(varName, value);
527
+ return;
528
+ }
400
529
  // Handle struct literal initialization
401
530
  if (stmt.init.kind === 'struct_lit' && stmt.type?.kind === 'struct') {
402
531
  const structName = stmt.type.name.toLowerCase();
@@ -470,6 +599,10 @@ class Lowering {
470
599
  }
471
600
  }
472
601
  lowerIfStmt(stmt) {
602
+ if (stmt.cond.kind === 'is_check') {
603
+ this.lowerIsCheckIfStmt(stmt);
604
+ return;
605
+ }
473
606
  const condVar = this.lowerExpr(stmt.cond);
474
607
  const condName = this.operandToVar(condVar);
475
608
  const thenLabel = this.builder.freshLabel('then');
@@ -493,6 +626,40 @@ class Lowering {
493
626
  // Merge block
494
627
  this.builder.startBlock(mergeLabel);
495
628
  }
629
+ lowerIsCheckIfStmt(stmt) {
630
+ const cond = stmt.cond;
631
+ if (cond.kind !== 'is_check') {
632
+ throw new diagnostics_1.DiagnosticError('LoweringError', "Internal error: expected 'is' check condition", stmt.span ?? { line: 0, col: 0 });
633
+ }
634
+ if (stmt.else_) {
635
+ throw new diagnostics_1.DiagnosticError('LoweringError', "'is' checks with else branches are not yet supported", cond.span ?? stmt.span ?? { line: 0, col: 0 });
636
+ }
637
+ const selector = this.exprToEntitySelector(cond.expr);
638
+ if (!selector) {
639
+ throw new diagnostics_1.DiagnosticError('LoweringError', "'is' checks require an entity selector or entity binding", cond.span ?? stmt.span ?? { line: 0, col: 0 });
640
+ }
641
+ const mcType = ENTITY_TO_MC_TYPE[cond.entityType];
642
+ if (!mcType) {
643
+ throw new diagnostics_1.DiagnosticError('LoweringError', `Cannot lower entity type check for '${cond.entityType}'`, cond.span ?? stmt.span ?? { line: 0, col: 0 });
644
+ }
645
+ const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`;
646
+ this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`);
647
+ const savedBuilder = this.builder;
648
+ const savedVarMap = new Map(this.varMap);
649
+ const savedBlockPosVars = new Map(this.blockPosVars);
650
+ this.builder = new LoweringBuilder();
651
+ this.varMap = new Map(savedVarMap);
652
+ this.blockPosVars = new Map(savedBlockPosVars);
653
+ this.builder.startBlock('entry');
654
+ this.lowerBlock(stmt.then);
655
+ if (!this.builder.isBlockSealed()) {
656
+ this.builder.emitReturn();
657
+ }
658
+ this.functions.push(this.builder.build(thenFnName, [], false));
659
+ this.builder = savedBuilder;
660
+ this.varMap = savedVarMap;
661
+ this.blockPosVars = savedBlockPosVars;
662
+ }
496
663
  lowerWhileStmt(stmt) {
497
664
  const checkLabel = this.builder.freshLabel('loop_check');
498
665
  const bodyLabel = this.builder.freshLabel('loop_body');
@@ -899,12 +1066,16 @@ class Lowering {
899
1066
  return { kind: 'var', name: this.selectorToString(expr.sel) };
900
1067
  case 'binary':
901
1068
  return this.lowerBinaryExpr(expr);
1069
+ case 'is_check':
1070
+ throw new diagnostics_1.DiagnosticError('LoweringError', "'is' checks are only supported as if conditions", expr.span ?? { line: 0, col: 0 });
902
1071
  case 'unary':
903
1072
  return this.lowerUnaryExpr(expr);
904
1073
  case 'assign':
905
1074
  return this.lowerAssignExpr(expr);
906
1075
  case 'call':
907
1076
  return this.lowerCallExpr(expr);
1077
+ case 'static_call':
1078
+ return this.lowerStaticCallExpr(expr);
908
1079
  case 'invoke':
909
1080
  return this.lowerInvokeExpr(expr);
910
1081
  case 'member_assign':
@@ -1222,6 +1393,10 @@ class Lowering {
1222
1393
  if (callbackTarget) {
1223
1394
  return this.emitDirectFunctionCall(callbackTarget, expr.args);
1224
1395
  }
1396
+ const implMethod = this.resolveInstanceMethod(expr);
1397
+ if (implMethod) {
1398
+ return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args);
1399
+ }
1225
1400
  // Regular function call
1226
1401
  const fnDecl = this.fnDecls.get(expr.fn);
1227
1402
  const defaultArgs = this.functionDefaults.get(expr.fn) ?? [];
@@ -1248,13 +1423,19 @@ class Lowering {
1248
1423
  }
1249
1424
  runtimeArgs.push(fullArgs[i]);
1250
1425
  }
1251
- const targetFn = callbackBindings.size > 0
1252
- ? this.ensureSpecializedFunction(fnDecl, callbackBindings)
1426
+ const stdlibCallSite = this.getStdlibCallSiteContext(fnDecl, getSpan(expr));
1427
+ const targetFn = callbackBindings.size > 0 || stdlibCallSite
1428
+ ? this.ensureSpecializedFunctionWithContext(fnDecl, callbackBindings, stdlibCallSite)
1253
1429
  : expr.fn;
1254
1430
  return this.emitDirectFunctionCall(targetFn, runtimeArgs);
1255
1431
  }
1256
1432
  return this.emitDirectFunctionCall(expr.fn, fullArgs);
1257
1433
  }
1434
+ lowerStaticCallExpr(expr) {
1435
+ const method = this.implMethods.get(expr.type)?.get(expr.method);
1436
+ const targetFn = method?.loweredName ?? `${expr.type}_${expr.method}`;
1437
+ return this.emitMethodCall(targetFn, method?.fn, expr.args);
1438
+ }
1258
1439
  lowerInvokeExpr(expr) {
1259
1440
  if (expr.callee.kind === 'lambda') {
1260
1441
  if (!Array.isArray(expr.callee.body)) {
@@ -1299,6 +1480,18 @@ class Lowering {
1299
1480
  this.builder.emitCall(fn, loweredArgs, dst);
1300
1481
  return { kind: 'var', name: dst };
1301
1482
  }
1483
+ emitMethodCall(fn, fnDecl, args) {
1484
+ const defaultArgs = this.functionDefaults.get(fn) ?? fnDecl?.params.map(param => param.default) ?? [];
1485
+ const fullArgs = [...args];
1486
+ for (let i = fullArgs.length; i < defaultArgs.length; i++) {
1487
+ const defaultExpr = defaultArgs[i];
1488
+ if (!defaultExpr) {
1489
+ break;
1490
+ }
1491
+ fullArgs.push(defaultExpr);
1492
+ }
1493
+ return this.emitDirectFunctionCall(fn, fullArgs);
1494
+ }
1302
1495
  resolveFunctionRefExpr(expr) {
1303
1496
  if (expr.kind === 'lambda') {
1304
1497
  return this.lowerLambdaExpr(expr);
@@ -1312,9 +1505,16 @@ class Lowering {
1312
1505
  return this.lambdaBindings.get(name) ?? this.currentCallbackBindings.get(name) ?? null;
1313
1506
  }
1314
1507
  ensureSpecializedFunction(fn, callbackBindings) {
1508
+ return this.ensureSpecializedFunctionWithContext(fn, callbackBindings);
1509
+ }
1510
+ ensureSpecializedFunctionWithContext(fn, callbackBindings, stdlibCallSite) {
1315
1511
  const parts = [...callbackBindings.entries()]
1316
1512
  .sort(([left], [right]) => left.localeCompare(right))
1317
1513
  .map(([param, target]) => `${param}_${target.replace(/[^a-zA-Z0-9_]/g, '_')}`);
1514
+ const callSiteHash = stdlibCallSite ? this.shortHash(this.serializeCallSite(stdlibCallSite)) : null;
1515
+ if (callSiteHash) {
1516
+ parts.push(`callsite_${callSiteHash}`);
1517
+ }
1318
1518
  const key = `${fn.name}::${parts.join('::')}`;
1319
1519
  const cached = this.specializedFunctions.get(key);
1320
1520
  if (cached) {
@@ -1323,7 +1523,7 @@ class Lowering {
1323
1523
  const specializedName = `${fn.name}__${parts.join('__')}`;
1324
1524
  this.specializedFunctions.set(key, specializedName);
1325
1525
  this.withSavedFunctionState(() => {
1326
- this.lowerFn(fn, { name: specializedName, callbackBindings });
1526
+ this.lowerFn(fn, { name: specializedName, callbackBindings, stdlibCallSite });
1327
1527
  });
1328
1528
  return specializedName;
1329
1529
  }
@@ -1346,10 +1546,12 @@ class Lowering {
1346
1546
  }
1347
1547
  withSavedFunctionState(callback) {
1348
1548
  const savedCurrentFn = this.currentFn;
1549
+ const savedStdlibCallSite = this.currentStdlibCallSite;
1349
1550
  const savedForeachCounter = this.foreachCounter;
1350
1551
  const savedBuilder = this.builder;
1351
1552
  const savedVarMap = new Map(this.varMap);
1352
1553
  const savedLambdaBindings = new Map(this.lambdaBindings);
1554
+ const savedIntervalBindings = new Map(this.intervalBindings);
1353
1555
  const savedCallbackBindings = new Map(this.currentCallbackBindings);
1354
1556
  const savedContext = this.currentContext;
1355
1557
  const savedBlockPosVars = new Map(this.blockPosVars);
@@ -1360,10 +1562,12 @@ class Lowering {
1360
1562
  }
1361
1563
  finally {
1362
1564
  this.currentFn = savedCurrentFn;
1565
+ this.currentStdlibCallSite = savedStdlibCallSite;
1363
1566
  this.foreachCounter = savedForeachCounter;
1364
1567
  this.builder = savedBuilder;
1365
1568
  this.varMap = savedVarMap;
1366
1569
  this.lambdaBindings = savedLambdaBindings;
1570
+ this.intervalBindings = savedIntervalBindings;
1367
1571
  this.currentCallbackBindings = savedCallbackBindings;
1368
1572
  this.currentContext = savedContext;
1369
1573
  this.blockPosVars = savedBlockPosVars;
@@ -1377,6 +1581,15 @@ class Lowering {
1377
1581
  this.builder.emitRaw(richTextCommand);
1378
1582
  return { kind: 'const', value: 0 };
1379
1583
  }
1584
+ if (name === 'setTimeout') {
1585
+ return this.lowerSetTimeout(args);
1586
+ }
1587
+ if (name === 'setInterval') {
1588
+ return this.lowerSetInterval(args);
1589
+ }
1590
+ if (name === 'clearInterval') {
1591
+ return this.lowerClearInterval(args, callSpan);
1592
+ }
1380
1593
  // Special case: random - legacy scoreboard RNG for pre-1.20.3 compatibility
1381
1594
  if (name === 'random') {
1382
1595
  const dst = this.builder.freshTemp();
@@ -1404,14 +1617,14 @@ class Lowering {
1404
1617
  if (name === 'scoreboard_get' || name === 'score') {
1405
1618
  const dst = this.builder.freshTemp();
1406
1619
  const player = this.exprToTargetString(args[0]);
1407
- const objective = this.exprToString(args[1]);
1620
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan);
1408
1621
  this.builder.emitRaw(`execute store result score ${dst} rs run scoreboard players get ${player} ${objective}`);
1409
1622
  return { kind: 'var', name: dst };
1410
1623
  }
1411
1624
  // Special case: scoreboard_set — write to vanilla MC scoreboard
1412
1625
  if (name === 'scoreboard_set') {
1413
1626
  const player = this.exprToTargetString(args[0]);
1414
- const objective = this.exprToString(args[1]);
1627
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan);
1415
1628
  const value = this.lowerExpr(args[2]);
1416
1629
  if (value.kind === 'const') {
1417
1630
  this.builder.emitRaw(`scoreboard players set ${player} ${objective} ${value.value}`);
@@ -1425,7 +1638,7 @@ class Lowering {
1425
1638
  }
1426
1639
  if (name === 'scoreboard_display') {
1427
1640
  const slot = this.exprToString(args[0]);
1428
- const objective = this.exprToString(args[1]);
1641
+ const objective = this.resolveScoreboardObjective(undefined, args[1], callSpan);
1429
1642
  this.builder.emitRaw(`scoreboard objectives setdisplay ${slot} ${objective}`);
1430
1643
  return { kind: 'const', value: 0 };
1431
1644
  }
@@ -1435,14 +1648,14 @@ class Lowering {
1435
1648
  return { kind: 'const', value: 0 };
1436
1649
  }
1437
1650
  if (name === 'scoreboard_add_objective') {
1438
- const objective = this.exprToString(args[0]);
1651
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan);
1439
1652
  const criteria = this.exprToString(args[1]);
1440
1653
  const displayName = args[2] ? ` ${this.exprToQuotedString(args[2])}` : '';
1441
1654
  this.builder.emitRaw(`scoreboard objectives add ${objective} ${criteria}${displayName}`);
1442
1655
  return { kind: 'const', value: 0 };
1443
1656
  }
1444
1657
  if (name === 'scoreboard_remove_objective') {
1445
- const objective = this.exprToString(args[0]);
1658
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan);
1446
1659
  this.builder.emitRaw(`scoreboard objectives remove ${objective}`);
1447
1660
  return { kind: 'const', value: 0 };
1448
1661
  }
@@ -1616,6 +1829,90 @@ class Lowering {
1616
1829
  }
1617
1830
  return { kind: 'const', value: 0 };
1618
1831
  }
1832
+ lowerSetTimeout(args) {
1833
+ const delay = this.exprToLiteral(args[0]);
1834
+ const callback = args[1];
1835
+ if (!callback || callback.kind !== 'lambda') {
1836
+ throw new diagnostics_1.DiagnosticError('LoweringError', 'setTimeout requires a lambda callback', getSpan(callback) ?? { line: 1, col: 1 });
1837
+ }
1838
+ const fnName = `__timeout_${this.timeoutCounter++}`;
1839
+ this.lowerNamedLambdaFunction(fnName, callback);
1840
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`);
1841
+ return { kind: 'const', value: 0 };
1842
+ }
1843
+ lowerSetInterval(args) {
1844
+ const delay = this.exprToLiteral(args[0]);
1845
+ const callback = args[1];
1846
+ if (!callback || callback.kind !== 'lambda') {
1847
+ throw new diagnostics_1.DiagnosticError('LoweringError', 'setInterval requires a lambda callback', getSpan(callback) ?? { line: 1, col: 1 });
1848
+ }
1849
+ const id = this.intervalCounter++;
1850
+ const bodyName = `__interval_body_${id}`;
1851
+ const fnName = `__interval_${id}`;
1852
+ this.lowerNamedLambdaFunction(bodyName, callback);
1853
+ this.lowerIntervalWrapperFunction(fnName, bodyName, delay);
1854
+ this.intervalFunctions.set(id, fnName);
1855
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`);
1856
+ return { kind: 'const', value: id };
1857
+ }
1858
+ lowerClearInterval(args, callSpan) {
1859
+ const fnName = this.resolveIntervalFunctionName(args[0]);
1860
+ if (!fnName) {
1861
+ throw new diagnostics_1.DiagnosticError('LoweringError', 'clearInterval requires an interval ID returned from setInterval', callSpan ?? getSpan(args[0]) ?? { line: 1, col: 1 });
1862
+ }
1863
+ this.builder.emitRaw(`schedule clear ${this.namespace}:${fnName}`);
1864
+ return { kind: 'const', value: 0 };
1865
+ }
1866
+ lowerNamedLambdaFunction(name, expr) {
1867
+ const lambdaFn = {
1868
+ name,
1869
+ params: expr.params.map(param => ({
1870
+ name: param.name,
1871
+ type: param.type ?? { kind: 'named', name: 'int' },
1872
+ })),
1873
+ returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
1874
+ decorators: [],
1875
+ body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
1876
+ };
1877
+ this.withSavedFunctionState(() => {
1878
+ this.lowerFn(lambdaFn);
1879
+ });
1880
+ }
1881
+ lowerIntervalWrapperFunction(name, bodyName, delay) {
1882
+ const intervalFn = {
1883
+ name,
1884
+ params: [],
1885
+ returnType: { kind: 'named', name: 'void' },
1886
+ decorators: [],
1887
+ body: [
1888
+ { kind: 'raw', cmd: `function ${this.namespace}:${bodyName}` },
1889
+ { kind: 'raw', cmd: `schedule function ${this.namespace}:${name} ${delay}t` },
1890
+ ],
1891
+ };
1892
+ this.withSavedFunctionState(() => {
1893
+ this.lowerFn(intervalFn);
1894
+ });
1895
+ }
1896
+ resolveIntervalFunctionName(expr) {
1897
+ if (!expr) {
1898
+ return null;
1899
+ }
1900
+ if (expr.kind === 'ident') {
1901
+ const boundInterval = this.intervalBindings.get(expr.name);
1902
+ if (boundInterval) {
1903
+ return boundInterval;
1904
+ }
1905
+ const constValue = this.constValues.get(expr.name);
1906
+ if (constValue?.kind === 'int_lit') {
1907
+ return this.intervalFunctions.get(constValue.value) ?? null;
1908
+ }
1909
+ return null;
1910
+ }
1911
+ if (expr.kind === 'int_lit') {
1912
+ return this.intervalFunctions.get(expr.value) ?? null;
1913
+ }
1914
+ return null;
1915
+ }
1619
1916
  lowerRichTextBuiltin(name, args) {
1620
1917
  const messageArgIndex = this.getRichTextArgIndex(name);
1621
1918
  if (messageArgIndex === null) {
@@ -1775,6 +2072,28 @@ class Lowering {
1775
2072
  return this.operandToVar(op);
1776
2073
  }
1777
2074
  }
2075
+ exprToEntitySelector(expr) {
2076
+ if (expr.kind === 'selector') {
2077
+ return this.selectorToString(expr.sel);
2078
+ }
2079
+ if (expr.kind === 'ident') {
2080
+ const constValue = this.constValues.get(expr.name);
2081
+ if (constValue) {
2082
+ return this.exprToEntitySelector(constValue);
2083
+ }
2084
+ const mapped = this.varMap.get(expr.name);
2085
+ if (mapped?.startsWith('@')) {
2086
+ return mapped;
2087
+ }
2088
+ }
2089
+ return null;
2090
+ }
2091
+ appendTypeFilter(selector, mcType) {
2092
+ if (selector.endsWith(']')) {
2093
+ return `${selector.slice(0, -1)},type=${mcType}]`;
2094
+ }
2095
+ return `${selector}[type=${mcType}]`;
2096
+ }
1778
2097
  exprToSnbt(expr) {
1779
2098
  switch (expr.kind) {
1780
2099
  case 'struct_lit': {
@@ -1842,6 +2161,92 @@ class Lowering {
1842
2161
  isTeamTextOption(option) {
1843
2162
  return option === 'displayName' || option === 'prefix' || option === 'suffix';
1844
2163
  }
2164
+ exprToScoreboardObjective(expr, span) {
2165
+ if (expr.kind === 'mc_name') {
2166
+ return expr.value;
2167
+ }
2168
+ const objective = this.exprToString(expr);
2169
+ if (objective.startsWith('#') || objective.includes('.')) {
2170
+ return objective.startsWith('#') ? objective.slice(1) : objective;
2171
+ }
2172
+ return `${this.getObjectiveNamespace(span)}.${objective}`;
2173
+ }
2174
+ resolveScoreboardObjective(playerExpr, objectiveExpr, span) {
2175
+ const stdlibInternalObjective = this.tryGetStdlibInternalObjective(playerExpr, objectiveExpr, span);
2176
+ if (stdlibInternalObjective) {
2177
+ return stdlibInternalObjective;
2178
+ }
2179
+ return this.exprToScoreboardObjective(objectiveExpr, span);
2180
+ }
2181
+ getObjectiveNamespace(span) {
2182
+ const filePath = this.filePathForSpan(span);
2183
+ if (!filePath) {
2184
+ return this.namespace;
2185
+ }
2186
+ return this.isStdlibFile(filePath) ? 'rs' : this.namespace;
2187
+ }
2188
+ tryGetStdlibInternalObjective(playerExpr, objectiveExpr, span) {
2189
+ if (!span || !this.currentStdlibCallSite || objectiveExpr.kind !== 'mc_name' || objectiveExpr.value !== 'rs') {
2190
+ return null;
2191
+ }
2192
+ const filePath = this.filePathForSpan(span);
2193
+ if (!filePath || !this.isStdlibFile(filePath)) {
2194
+ return null;
2195
+ }
2196
+ const resourceBase = this.getStdlibInternalResourceBase(playerExpr);
2197
+ if (!resourceBase) {
2198
+ return null;
2199
+ }
2200
+ const hash = this.shortHash(this.serializeCallSite(this.currentStdlibCallSite));
2201
+ return `rs._${resourceBase}_${hash}`;
2202
+ }
2203
+ getStdlibInternalResourceBase(playerExpr) {
2204
+ if (!playerExpr || playerExpr.kind !== 'str_lit') {
2205
+ return null;
2206
+ }
2207
+ const match = playerExpr.value.match(/^([a-z0-9]+)_/);
2208
+ return match?.[1] ?? null;
2209
+ }
2210
+ getStdlibCallSiteContext(fn, exprSpan) {
2211
+ const fnFilePath = this.filePathForSpan(getSpan(fn));
2212
+ if (!fnFilePath || !this.isStdlibFile(fnFilePath)) {
2213
+ return undefined;
2214
+ }
2215
+ if (this.currentStdlibCallSite) {
2216
+ return this.currentStdlibCallSite;
2217
+ }
2218
+ if (!exprSpan) {
2219
+ return undefined;
2220
+ }
2221
+ return {
2222
+ filePath: this.filePathForSpan(exprSpan),
2223
+ line: exprSpan.line,
2224
+ col: exprSpan.col,
2225
+ };
2226
+ }
2227
+ serializeCallSite(callSite) {
2228
+ return `${callSite.filePath ?? '<memory>'}:${callSite.line}:${callSite.col}`;
2229
+ }
2230
+ shortHash(input) {
2231
+ let hash = 2166136261;
2232
+ for (let i = 0; i < input.length; i++) {
2233
+ hash ^= input.charCodeAt(i);
2234
+ hash = Math.imul(hash, 16777619);
2235
+ }
2236
+ return (hash >>> 0).toString(16).padStart(8, '0').slice(0, 4);
2237
+ }
2238
+ isStdlibFile(filePath) {
2239
+ const normalized = path.normalize(filePath);
2240
+ const stdlibSegment = `${path.sep}src${path.sep}stdlib${path.sep}`;
2241
+ return normalized.includes(stdlibSegment);
2242
+ }
2243
+ filePathForSpan(span) {
2244
+ if (!span) {
2245
+ return undefined;
2246
+ }
2247
+ const line = span.line;
2248
+ return this.sourceRanges.find(range => line >= range.startLine && line <= range.endLine)?.filePath;
2249
+ }
1845
2250
  lowerCoordinateBuiltin(name, args) {
1846
2251
  const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null;
1847
2252
  const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null;
@@ -1949,7 +2354,11 @@ class Lowering {
1949
2354
  };
1950
2355
  }
1951
2356
  if (expr.kind === 'call') {
1952
- return this.fnDecls.get(this.resolveFunctionRefByName(expr.fn) ?? expr.fn)?.returnType;
2357
+ const resolved = this.resolveFunctionRefByName(expr.fn) ?? this.resolveInstanceMethod(expr)?.loweredName ?? expr.fn;
2358
+ return this.fnDecls.get(resolved)?.returnType;
2359
+ }
2360
+ if (expr.kind === 'static_call') {
2361
+ return this.implMethods.get(expr.type)?.get(expr.method)?.fn.returnType;
1953
2362
  }
1954
2363
  if (expr.kind === 'invoke') {
1955
2364
  const calleeType = this.inferExprType(expr.callee);
@@ -1977,6 +2386,21 @@ class Lowering {
1977
2386
  }
1978
2387
  return undefined;
1979
2388
  }
2389
+ resolveInstanceMethod(expr) {
2390
+ const receiver = expr.args[0];
2391
+ if (!receiver) {
2392
+ return null;
2393
+ }
2394
+ const receiverType = this.inferExprType(receiver);
2395
+ if (receiverType?.kind !== 'struct') {
2396
+ return null;
2397
+ }
2398
+ const method = this.implMethods.get(receiverType.name)?.get(expr.fn);
2399
+ if (!method || method.fn.params[0]?.name !== 'self') {
2400
+ return null;
2401
+ }
2402
+ return method;
2403
+ }
1980
2404
  normalizeType(type) {
1981
2405
  if (type.kind === 'array') {
1982
2406
  return { kind: 'array', elem: this.normalizeType(type.elem) };