redscript-mc 2.1.1 → 2.3.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 (85) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +66 -21
  3. package/README.zh.md +61 -61
  4. package/dist/src/__tests__/e2e/basic.test.js +25 -0
  5. package/dist/src/__tests__/e2e/coroutine.test.js +22 -0
  6. package/dist/src/__tests__/mc-integration.test.js +25 -13
  7. package/dist/src/__tests__/schedule.test.js +105 -0
  8. package/dist/src/__tests__/tuner/engine.test.d.ts +4 -0
  9. package/dist/src/__tests__/tuner/engine.test.js +232 -0
  10. package/dist/src/__tests__/typechecker.test.js +63 -0
  11. package/dist/src/emit/compile.js +1 -0
  12. package/dist/src/emit/index.js +3 -1
  13. package/dist/src/lir/lower.js +26 -0
  14. package/dist/src/mir/lower.js +341 -12
  15. package/dist/src/mir/types.d.ts +10 -0
  16. package/dist/src/optimizer/copy_prop.js +4 -0
  17. package/dist/src/optimizer/coroutine.d.ts +2 -0
  18. package/dist/src/optimizer/coroutine.js +33 -1
  19. package/dist/src/optimizer/dce.js +7 -1
  20. package/dist/src/optimizer/lir/const_imm.js +1 -1
  21. package/dist/src/optimizer/lir/dead_slot.js +1 -1
  22. package/dist/src/tuner/adapters/ln-polynomial.d.ts +23 -0
  23. package/dist/src/tuner/adapters/ln-polynomial.js +142 -0
  24. package/dist/src/tuner/adapters/sqrt-newton.d.ts +28 -0
  25. package/dist/src/tuner/adapters/sqrt-newton.js +125 -0
  26. package/dist/src/tuner/cli.d.ts +5 -0
  27. package/dist/src/tuner/cli.js +168 -0
  28. package/dist/src/tuner/engine.d.ts +17 -0
  29. package/dist/src/tuner/engine.js +215 -0
  30. package/dist/src/tuner/metrics.d.ts +15 -0
  31. package/dist/src/tuner/metrics.js +51 -0
  32. package/dist/src/tuner/simulator.d.ts +35 -0
  33. package/dist/src/tuner/simulator.js +78 -0
  34. package/dist/src/tuner/types.d.ts +32 -0
  35. package/dist/src/tuner/types.js +6 -0
  36. package/dist/src/typechecker/index.d.ts +2 -0
  37. package/dist/src/typechecker/index.js +29 -0
  38. package/docs/ROADMAP.md +35 -0
  39. package/docs/STDLIB_ROADMAP.md +142 -0
  40. package/editors/vscode/package-lock.json +3 -3
  41. package/editors/vscode/package.json +1 -1
  42. package/examples/coroutine-demo.mcrs +11 -10
  43. package/jest.config.js +19 -0
  44. package/package.json +1 -1
  45. package/src/__tests__/e2e/basic.test.ts +27 -0
  46. package/src/__tests__/e2e/coroutine.test.ts +23 -0
  47. package/src/__tests__/fixtures/array-test.mcrs +21 -22
  48. package/src/__tests__/fixtures/counter.mcrs +17 -0
  49. package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
  50. package/src/__tests__/mc-integration.test.ts +25 -13
  51. package/src/__tests__/schedule.test.ts +112 -0
  52. package/src/__tests__/tuner/engine.test.ts +260 -0
  53. package/src/__tests__/typechecker.test.ts +68 -0
  54. package/src/emit/compile.ts +1 -0
  55. package/src/emit/index.ts +3 -1
  56. package/src/lir/lower.ts +27 -0
  57. package/src/mir/lower.ts +355 -9
  58. package/src/mir/types.ts +4 -0
  59. package/src/optimizer/copy_prop.ts +4 -0
  60. package/src/optimizer/coroutine.ts +37 -1
  61. package/src/optimizer/dce.ts +6 -1
  62. package/src/optimizer/lir/const_imm.ts +1 -1
  63. package/src/optimizer/lir/dead_slot.ts +1 -1
  64. package/src/stdlib/bigint.mcrs +155 -192
  65. package/src/stdlib/bits.mcrs +158 -0
  66. package/src/stdlib/color.mcrs +160 -0
  67. package/src/stdlib/geometry.mcrs +124 -0
  68. package/src/stdlib/list.mcrs +125 -0
  69. package/src/stdlib/math.mcrs +90 -0
  70. package/src/stdlib/math_hp.mcrs +65 -0
  71. package/src/stdlib/random.mcrs +67 -0
  72. package/src/stdlib/signal.mcrs +112 -0
  73. package/src/stdlib/timer.mcrs +10 -5
  74. package/src/stdlib/vec.mcrs +27 -0
  75. package/src/tuner/adapters/ln-polynomial.ts +147 -0
  76. package/src/tuner/adapters/sqrt-newton.ts +135 -0
  77. package/src/tuner/cli.ts +158 -0
  78. package/src/tuner/engine.ts +272 -0
  79. package/src/tuner/metrics.ts +66 -0
  80. package/src/tuner/simulator.ts +69 -0
  81. package/src/tuner/types.ts +44 -0
  82. package/src/typechecker/index.ts +39 -0
  83. package/docs/ARCHITECTURE.zh.md +0 -1088
  84. package/docs/COMPILATION_STATS.md +0 -142
  85. package/docs/IMPLEMENTATION_GUIDE.md +0 -512
@@ -95,4 +95,109 @@ describe('@schedule decorator', () => {
95
95
  expect(startFn).toContain('function test:_schedule_after_one_second');
96
96
  });
97
97
  });
98
+ describe('setTimeout / setInterval codegen', () => {
99
+ test('setTimeout lifts lambda to __timeout_callback_0 and schedules it', () => {
100
+ const source = `
101
+ fn start() {
102
+ setTimeout(20, () => {
103
+ say("later");
104
+ });
105
+ }
106
+ `;
107
+ const result = (0, compile_1.compile)(source, { namespace: 'ns' });
108
+ const startFn = getFile(result.files, 'start.mcfunction');
109
+ const cbFn = getFile(result.files, '__timeout_callback_0.mcfunction');
110
+ expect(startFn).toContain('schedule function ns:__timeout_callback_0 20t');
111
+ expect(cbFn).toBeDefined();
112
+ expect(cbFn).toContain('say later');
113
+ });
114
+ test('setInterval lambda reschedules itself at the end', () => {
115
+ const source = `
116
+ fn start() {
117
+ setInterval(10, () => {
118
+ say("tick");
119
+ });
120
+ }
121
+ `;
122
+ const result = (0, compile_1.compile)(source, { namespace: 'ns' });
123
+ const cbFn = getFile(result.files, '__timeout_callback_0.mcfunction');
124
+ expect(cbFn).toBeDefined();
125
+ expect(cbFn).toContain('schedule function ns:__timeout_callback_0 10t');
126
+ });
127
+ test('multiple setTimeout calls get unique callback names', () => {
128
+ const source = `
129
+ fn start() {
130
+ setTimeout(10, () => { say("a"); });
131
+ setTimeout(20, () => { say("b"); });
132
+ }
133
+ `;
134
+ const result = (0, compile_1.compile)(source, { namespace: 'ns' });
135
+ const cb0 = getFile(result.files, '__timeout_callback_0.mcfunction');
136
+ const cb1 = getFile(result.files, '__timeout_callback_1.mcfunction');
137
+ expect(cb0).toBeDefined();
138
+ expect(cb1).toBeDefined();
139
+ expect(cb0).toContain('say a');
140
+ expect(cb1).toContain('say b');
141
+ });
142
+ });
143
+ const TIMER_STRUCT = `
144
+ struct Timer {
145
+ _id: int,
146
+ _duration: int
147
+ }
148
+ impl Timer {
149
+ fn new(duration: int) -> Timer {
150
+ return { _id: 0, _duration: duration };
151
+ }
152
+ fn start(self) {}
153
+ fn pause(self) {}
154
+ fn reset(self) {}
155
+ fn tick(self) {}
156
+ fn done(self) -> bool { return false; }
157
+ fn elapsed(self) -> int { return 0; }
158
+ }
159
+ `;
160
+ describe('Timer static allocation codegen', () => {
161
+ test('Timer::new() initializes unique scoreboard slots', () => {
162
+ const source = TIMER_STRUCT + `
163
+ fn init() {
164
+ let t: Timer = Timer::new(20);
165
+ }
166
+ `;
167
+ const result = (0, compile_1.compile)(source, { namespace: 'ns' });
168
+ const initFn = getFile(result.files, 'init.mcfunction');
169
+ expect(initFn).toContain('scoreboard players set __timer_0_ticks ns 0');
170
+ expect(initFn).toContain('scoreboard players set __timer_0_active ns 0');
171
+ });
172
+ test('Timer.start() inlines to scoreboard set active=1', () => {
173
+ const source = TIMER_STRUCT + `
174
+ fn init() {
175
+ let t: Timer = Timer::new(20);
176
+ t.start();
177
+ }
178
+ `;
179
+ const result = (0, compile_1.compile)(source, { namespace: 'ns' });
180
+ const initFn = getFile(result.files, 'init.mcfunction');
181
+ expect(initFn).toContain('scoreboard players set __timer_0_active ns 1');
182
+ expect(initFn).not.toContain('function ns:timer/start');
183
+ });
184
+ test('two Timer::new() calls get distinct IDs', () => {
185
+ const source = TIMER_STRUCT + `
186
+ fn init() {
187
+ let t0: Timer = Timer::new(10);
188
+ let t1: Timer = Timer::new(20);
189
+ t0.start();
190
+ t1.start();
191
+ }
192
+ `;
193
+ const result = (0, compile_1.compile)(source, { namespace: 'ns' });
194
+ const initFn = getFile(result.files, 'init.mcfunction');
195
+ // Both timers initialized
196
+ expect(initFn).toContain('__timer_0_ticks');
197
+ expect(initFn).toContain('__timer_1_ticks');
198
+ // Both started with unique slot names
199
+ expect(initFn).toContain('scoreboard players set __timer_0_active ns 1');
200
+ expect(initFn).toContain('scoreboard players set __timer_1_active ns 1');
201
+ });
202
+ });
98
203
  //# sourceMappingURL=schedule.test.js.map
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for the redscript tuner engine, simulator, and ln-polynomial adapter.
3
+ */
4
+ export {};
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for the redscript tuner engine, simulator, and ln-polynomial adapter.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const engine_1 = require("../../tuner/engine");
7
+ const simulator_1 = require("../../tuner/simulator");
8
+ const metrics_1 = require("../../tuner/metrics");
9
+ const ln_polynomial_1 = require("../../tuner/adapters/ln-polynomial");
10
+ const sqrt_newton_1 = require("../../tuner/adapters/sqrt-newton");
11
+ // ─── simulator tests ──────────────────────────────────────────────────────────
12
+ describe('simulator', () => {
13
+ test('i32 truncates to int32', () => {
14
+ expect((0, simulator_1.i32)(3.7)).toBe(3);
15
+ expect((0, simulator_1.i32)(-3.7)).toBe(-3);
16
+ expect((0, simulator_1.i32)(2147483648)).toBe(-2147483648); // overflow wraps
17
+ expect((0, simulator_1.i32)(0)).toBe(0);
18
+ });
19
+ test('fixedMul basic', () => {
20
+ // 10000 * 10000 / 10000 = 10000
21
+ expect((0, simulator_1.fixedMul)(10000, 10000, 10000)).toBe(10000);
22
+ // 5000 * 2 / 10000 = 1
23
+ expect((0, simulator_1.fixedMul)(5000, 2, 10000)).toBe(1);
24
+ });
25
+ test('fixedMul returns Infinity on overflow', () => {
26
+ expect((0, simulator_1.fixedMul)(2147483647, 2147483647, 1)).toBe(Infinity);
27
+ });
28
+ test('isOverflow detects out-of-range', () => {
29
+ expect((0, simulator_1.isOverflow)(2147483648)).toBe(true);
30
+ expect((0, simulator_1.isOverflow)(-2147483649)).toBe(true);
31
+ expect((0, simulator_1.isOverflow)(Infinity)).toBe(true);
32
+ expect((0, simulator_1.isOverflow)(NaN)).toBe(true);
33
+ expect((0, simulator_1.isOverflow)(0)).toBe(false);
34
+ expect((0, simulator_1.isOverflow)(2147483647)).toBe(false);
35
+ });
36
+ });
37
+ // ─── Nelder-Mead convergence test ────────────────────────────────────────────
38
+ describe('Nelder-Mead engine', () => {
39
+ test('converges to minimum of (x-3)^2', () => {
40
+ // Simple 1D minimization: minimize (x-3)^2
41
+ const mockAdapter = {
42
+ name: 'test-quadratic',
43
+ description: 'Minimize (x-3)^2',
44
+ params: [
45
+ { name: 'x', range: [-10, 10], integer: false },
46
+ ],
47
+ simulate(input, params) {
48
+ // Return the residual as a scaled integer
49
+ const x = params['x'];
50
+ return Math.round(x * 10000);
51
+ },
52
+ reference(_input) {
53
+ // Target: x = 3 → value 30000
54
+ return 30000;
55
+ },
56
+ sampleInputs() {
57
+ return [1]; // single input, target value is 3.0 (×10000 = 30000)
58
+ },
59
+ generateCode(params) {
60
+ return `// x = ${params['x']}`;
61
+ },
62
+ };
63
+ const result = (0, engine_1.search)(mockAdapter, 5000);
64
+ // Should converge close to x=3
65
+ expect(result.params['x']).toBeCloseTo(3.0, 1);
66
+ expect(result.maxError).toBeLessThan(0.1);
67
+ });
68
+ test('handles integer constraints', () => {
69
+ const mockAdapter = {
70
+ name: 'test-integer',
71
+ description: 'Integer parameter test',
72
+ params: [
73
+ { name: 'n', range: [0, 10], integer: true },
74
+ ],
75
+ simulate(input, params) {
76
+ // Should snap to integer 7
77
+ return Math.round(params['n'] * 10000);
78
+ },
79
+ reference(_input) {
80
+ return 70000; // 7.0 × 10000
81
+ },
82
+ sampleInputs() {
83
+ return [1];
84
+ },
85
+ generateCode() {
86
+ return '';
87
+ },
88
+ };
89
+ const result = (0, engine_1.search)(mockAdapter, 2000);
90
+ // Should find n close to 7
91
+ expect(Math.round(result.params['n'])).toBe(7);
92
+ });
93
+ test('i32 overflow penalization', () => {
94
+ const mockAdapter = {
95
+ name: 'test-overflow',
96
+ description: 'Test overflow penalization',
97
+ params: [
98
+ { name: 'scale', range: [1, 1000], integer: true },
99
+ ],
100
+ simulate(_input, params) {
101
+ // Always overflow for any scale >= 500
102
+ if (params['scale'] >= 500)
103
+ return Infinity;
104
+ return params['scale'] * 10000;
105
+ },
106
+ reference(_input) {
107
+ return 2000000; // target: scale=200 → 2000000
108
+ },
109
+ sampleInputs() {
110
+ return [1];
111
+ },
112
+ generateCode() {
113
+ return '';
114
+ },
115
+ };
116
+ const { maxError, mae, rmse } = (0, metrics_1.evaluate)(mockAdapter, { scale: 2147483647 });
117
+ expect(maxError).toBe(Infinity);
118
+ expect(mae).toBe(Infinity);
119
+ expect(rmse).toBe(Infinity);
120
+ });
121
+ });
122
+ // ─── ln-polynomial adapter tests ─────────────────────────────────────────────
123
+ describe('ln-polynomial adapter', () => {
124
+ const defaultParams = ln_polynomial_1.defaultParams; // { A1: 20000, A3: 6667, A5: 4000 }
125
+ test('sample inputs cover the valid range', () => {
126
+ const inputs = ln_polynomial_1.lnPolynomialAdapter.sampleInputs();
127
+ expect(inputs.length).toBeGreaterThan(50);
128
+ // All inputs should be positive
129
+ expect(inputs.every(x => x > 0)).toBe(true);
130
+ });
131
+ test('reference matches Math.log', () => {
132
+ const SCALE = 10000;
133
+ // ln(1.0) = 0
134
+ expect(ln_polynomial_1.lnPolynomialAdapter.reference(SCALE)).toBeCloseTo(0, 5);
135
+ // ln(2.0) ≈ 0.6931 → 6931
136
+ expect(ln_polynomial_1.lnPolynomialAdapter.reference(2 * SCALE)).toBeCloseTo(6931.47, 0);
137
+ // ln(0.5) ≈ -0.6931 → -6931
138
+ expect(ln_polynomial_1.lnPolynomialAdapter.reference(5000)).toBeCloseTo(-6931.47, 0);
139
+ });
140
+ test('simulate produces reasonable output for x=1 (no error)', () => {
141
+ const result = ln_polynomial_1.lnPolynomialAdapter.simulate(10000, defaultParams);
142
+ // ln(1.0) = 0; allow some approximation error
143
+ expect(Math.abs(result)).toBeLessThan(500); // within 0.05
144
+ });
145
+ test('simulate returns Infinity for invalid input', () => {
146
+ const result = ln_polynomial_1.lnPolynomialAdapter.simulate(0, defaultParams);
147
+ expect(result).toBeLessThan(0); // negative sentinel or -MAX_INT
148
+ });
149
+ test('max_error < 0.001 with default atanh coefficients', () => {
150
+ const metrics = (0, metrics_1.evaluate)(ln_polynomial_1.lnPolynomialAdapter, defaultParams);
151
+ expect(metrics.maxError).toBeLessThan(0.001);
152
+ }, 10000);
153
+ test('search improves over default params', () => {
154
+ // Run a short search and confirm it doesn't get worse
155
+ const baseMetrics = (0, metrics_1.evaluate)(ln_polynomial_1.lnPolynomialAdapter, defaultParams);
156
+ const result = (0, engine_1.search)(ln_polynomial_1.lnPolynomialAdapter, 500); // short budget for test speed
157
+ // Either same or better
158
+ expect(result.maxError).toBeLessThanOrEqual(baseMetrics.maxError * 2);
159
+ expect(result.maxError).toBeLessThan(0.01);
160
+ }, 30000);
161
+ test('generateCode produces valid output', () => {
162
+ const meta = {
163
+ maxError: 0.00003,
164
+ mae: 0.000012,
165
+ rmse: 0.000015,
166
+ estimatedCmds: 38,
167
+ tuneDate: '2026-03-17',
168
+ budgetUsed: 5000,
169
+ };
170
+ const code = ln_polynomial_1.lnPolynomialAdapter.generateCode(defaultParams, meta);
171
+ expect(code).toContain('AUTO-GENERATED');
172
+ expect(code).toContain('ln-polynomial');
173
+ expect(code).toContain('fn ln');
174
+ expect(code).toContain('A1');
175
+ expect(code).toContain('A3');
176
+ expect(code).toContain('A5');
177
+ expect(code).toContain('2026-03-17');
178
+ });
179
+ test('searchSA achieves max_error < 0.001 on ln-polynomial', () => {
180
+ const result = (0, engine_1.searchSA)(ln_polynomial_1.lnPolynomialAdapter, 3000);
181
+ expect(result.maxError).toBeLessThan(0.001);
182
+ }, 30000);
183
+ });
184
+ // ─── sqrt-newton adapter tests ────────────────────────────────────────────────
185
+ describe('sqrt-newton adapter', () => {
186
+ test('simulate(10000, defaultParams) ≈ 10000 (sqrt(1.0)=1.0)', () => {
187
+ const result = sqrt_newton_1.sqrtNewtonAdapter.simulate(10000, sqrt_newton_1.defaultParams);
188
+ // sqrt(1.0) * 10000 = 10000
189
+ expect(Math.abs(result - 10000)).toBeLessThan(10);
190
+ });
191
+ test('simulate(40000, defaultParams) ≈ 20000 (sqrt(4.0)=2.0)', () => {
192
+ const result = sqrt_newton_1.sqrtNewtonAdapter.simulate(40000, sqrt_newton_1.defaultParams);
193
+ // sqrt(4.0) * 10000 = 20000
194
+ expect(Math.abs(result - 20000)).toBeLessThan(10);
195
+ });
196
+ test('simulate(0) returns 0', () => {
197
+ expect(sqrt_newton_1.sqrtNewtonAdapter.simulate(0, sqrt_newton_1.defaultParams)).toBe(0);
198
+ expect(sqrt_newton_1.sqrtNewtonAdapter.simulate(-1, sqrt_newton_1.defaultParams)).toBe(0);
199
+ });
200
+ test('simulate(250000, defaultParams) ≈ 50000 (sqrt(25.0)=5.0)', () => {
201
+ const result = sqrt_newton_1.sqrtNewtonAdapter.simulate(250000, sqrt_newton_1.defaultParams);
202
+ expect(Math.abs(result - 50000)).toBeLessThan(10);
203
+ });
204
+ test('sample inputs are all positive', () => {
205
+ const inputs = sqrt_newton_1.sqrtNewtonAdapter.sampleInputs();
206
+ expect(inputs.length).toBeGreaterThan(50);
207
+ expect(inputs.every(x => x > 0)).toBe(true);
208
+ });
209
+ test('reference matches Math.sqrt', () => {
210
+ const SCALE = 10000;
211
+ expect(sqrt_newton_1.sqrtNewtonAdapter.reference(SCALE)).toBe(SCALE); // sqrt(1.0)
212
+ expect(sqrt_newton_1.sqrtNewtonAdapter.reference(4 * SCALE)).toBe(2 * SCALE); // sqrt(4.0)
213
+ expect(sqrt_newton_1.sqrtNewtonAdapter.reference(9 * SCALE)).toBe(3 * SCALE); // sqrt(9.0)
214
+ expect(sqrt_newton_1.sqrtNewtonAdapter.reference(0)).toBe(0);
215
+ });
216
+ test('generateCode contains fn sqrt_fx', () => {
217
+ const meta = {
218
+ maxError: 1.5,
219
+ mae: 0.5,
220
+ rmse: 0.8,
221
+ estimatedCmds: 30,
222
+ tuneDate: '2026-03-17',
223
+ budgetUsed: 3000,
224
+ };
225
+ const code = sqrt_newton_1.sqrtNewtonAdapter.generateCode(sqrt_newton_1.defaultParams, meta);
226
+ expect(code).toContain('AUTO-GENERATED');
227
+ expect(code).toContain('sqrt-newton');
228
+ expect(code).toContain('fn sqrt_fx');
229
+ expect(code).toContain('2026-03-17');
230
+ });
231
+ });
232
+ //# sourceMappingURL=engine.test.js.map
@@ -246,6 +246,69 @@ fn test() {
246
246
  expect(errors.length).toBeGreaterThan(0);
247
247
  expect(errors[0].message).toContain('Return type mismatch: expected void, got int');
248
248
  });
249
+ it('rejects setTimeout inside a loop', () => {
250
+ const errors = typeCheck(`
251
+ fn test() {
252
+ while (true) {
253
+ setTimeout(20, () => { say("x"); });
254
+ }
255
+ }
256
+ `);
257
+ expect(errors.length).toBeGreaterThan(0);
258
+ expect(errors[0].message).toContain('cannot be called inside a loop');
259
+ });
260
+ it('rejects setTimeout inside an if body', () => {
261
+ const errors = typeCheck(`
262
+ fn test() {
263
+ if (true) {
264
+ setTimeout(20, () => { say("x"); });
265
+ }
266
+ }
267
+ `);
268
+ expect(errors.length).toBeGreaterThan(0);
269
+ expect(errors[0].message).toContain('cannot be called inside an if/else body');
270
+ });
271
+ it('rejects setInterval inside a loop', () => {
272
+ const errors = typeCheck(`
273
+ fn test() {
274
+ while (true) {
275
+ setInterval(20, () => { say("x"); });
276
+ }
277
+ }
278
+ `);
279
+ expect(errors.length).toBeGreaterThan(0);
280
+ expect(errors[0].message).toContain('cannot be called inside a loop');
281
+ });
282
+ it('rejects Timer::new() inside a loop', () => {
283
+ const errors = typeCheck(`
284
+ struct Timer { _id: int, _duration: int }
285
+ impl Timer {
286
+ fn new(duration: int) -> Timer { return { _id: 0, _duration: duration }; }
287
+ }
288
+ fn test() {
289
+ while (true) {
290
+ let t: Timer = Timer::new(10);
291
+ }
292
+ }
293
+ `);
294
+ expect(errors.length).toBeGreaterThan(0);
295
+ expect(errors[0].message).toContain('Timer::new() cannot be called inside a loop');
296
+ });
297
+ it('rejects Timer::new() inside an if body', () => {
298
+ const errors = typeCheck(`
299
+ struct Timer { _id: int, _duration: int }
300
+ impl Timer {
301
+ fn new(duration: int) -> Timer { return { _id: 0, _duration: duration }; }
302
+ }
303
+ fn test() {
304
+ if (true) {
305
+ let t: Timer = Timer::new(10);
306
+ }
307
+ }
308
+ `);
309
+ expect(errors.length).toBeGreaterThan(0);
310
+ expect(errors[0].message).toContain('Timer::new() cannot be called inside an if/else body');
311
+ });
249
312
  it('allows impl instance methods with inferred self type', () => {
250
313
  const errors = typeCheck(`
251
314
  struct Timer { duration: int }
@@ -116,6 +116,7 @@ function compile(source, options = {}) {
116
116
  const coroResult = (0, coroutine_1.coroutineTransform)(mirOpt, coroutineInfos);
117
117
  const mirFinal = coroResult.module;
118
118
  tickFunctions.push(...coroResult.generatedTickFunctions);
119
+ warnings.push(...coroResult.warnings);
119
120
  // Stage 5: MIR → LIR
120
121
  const lir = (0, lower_3.lowerToLIR)(mirFinal);
121
122
  // Stage 6: LIR optimization
@@ -152,7 +152,9 @@ function emitInstr(instr, ns, obj, mcVersion) {
152
152
  return `execute unless score ${slot(instr.a)} ${cmpToMC(instr.op)} ${slot(instr.b)} run function ${instr.fn}`;
153
153
  case 'call_context': {
154
154
  const subcmds = instr.subcommands.map(emitSubcmd).join(' ');
155
- return `execute ${subcmds} run function ${instr.fn}`;
155
+ return subcmds
156
+ ? `execute ${subcmds} run function ${instr.fn}`
157
+ : `function ${instr.fn}`;
156
158
  }
157
159
  case 'return_value':
158
160
  return `scoreboard players operation $ret ${instr.slot.obj} = ${slot(instr.slot)}`;
@@ -275,6 +275,32 @@ function lowerInstrInner(instr, fn, ctx, instrs) {
275
275
  });
276
276
  break;
277
277
  }
278
+ case 'score_read': {
279
+ // execute store result score $dst __obj run scoreboard players get <player> <obj>
280
+ const dst = ctx.slot(instr.dst);
281
+ instrs.push({
282
+ kind: 'store_cmd_to_score',
283
+ dst,
284
+ cmd: { kind: 'raw', cmd: `scoreboard players get ${instr.player} ${instr.obj}` },
285
+ });
286
+ break;
287
+ }
288
+ case 'score_write': {
289
+ // Write a value to a vanilla MC scoreboard objective
290
+ if (instr.src.kind === 'const') {
291
+ instrs.push({ kind: 'raw', cmd: `scoreboard players set ${instr.player} ${instr.obj} ${instr.src.value}` });
292
+ }
293
+ else {
294
+ // execute store result score <player> <obj> run scoreboard players get $src __ns
295
+ const srcSlot = operandToSlot(instr.src, ctx, instrs);
296
+ instrs.push({
297
+ kind: 'store_cmd_to_score',
298
+ dst: { player: instr.player, obj: instr.obj },
299
+ cmd: { kind: 'raw', cmd: `scoreboard players get ${srcSlot.player} ${srcSlot.obj}` },
300
+ });
301
+ }
302
+ break;
303
+ }
278
304
  case 'call': {
279
305
  // Set parameter slots $p0, $p1, ...
280
306
  for (let i = 0; i < instr.args.length; i++) {