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
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Tests for the redscript tuner engine, simulator, and ln-polynomial adapter.
3
+ */
4
+
5
+ import { search, searchSA } from '../../tuner/engine';
6
+ import { i32, fixedMul, isOverflow } from '../../tuner/simulator';
7
+ import { evaluate } from '../../tuner/metrics';
8
+ import { lnPolynomialAdapter, defaultParams as lnDefaultParams } from '../../tuner/adapters/ln-polynomial';
9
+ import { sqrtNewtonAdapter, defaultParams as sqrtDefaultParams } from '../../tuner/adapters/sqrt-newton';
10
+ import { TunerAdapter, ParamSpec } from '../../tuner/types';
11
+
12
+ // ─── simulator tests ──────────────────────────────────────────────────────────
13
+
14
+ describe('simulator', () => {
15
+ test('i32 truncates to int32', () => {
16
+ expect(i32(3.7)).toBe(3);
17
+ expect(i32(-3.7)).toBe(-3);
18
+ expect(i32(2147483648)).toBe(-2147483648); // overflow wraps
19
+ expect(i32(0)).toBe(0);
20
+ });
21
+
22
+ test('fixedMul basic', () => {
23
+ // 10000 * 10000 / 10000 = 10000
24
+ expect(fixedMul(10000, 10000, 10000)).toBe(10000);
25
+ // 5000 * 2 / 10000 = 1
26
+ expect(fixedMul(5000, 2, 10000)).toBe(1);
27
+ });
28
+
29
+ test('fixedMul returns Infinity on overflow', () => {
30
+ expect(fixedMul(2147483647, 2147483647, 1)).toBe(Infinity);
31
+ });
32
+
33
+ test('isOverflow detects out-of-range', () => {
34
+ expect(isOverflow(2147483648)).toBe(true);
35
+ expect(isOverflow(-2147483649)).toBe(true);
36
+ expect(isOverflow(Infinity)).toBe(true);
37
+ expect(isOverflow(NaN)).toBe(true);
38
+ expect(isOverflow(0)).toBe(false);
39
+ expect(isOverflow(2147483647)).toBe(false);
40
+ });
41
+ });
42
+
43
+ // ─── Nelder-Mead convergence test ────────────────────────────────────────────
44
+
45
+ describe('Nelder-Mead engine', () => {
46
+ test('converges to minimum of (x-3)^2', () => {
47
+ // Simple 1D minimization: minimize (x-3)^2
48
+ const mockAdapter: TunerAdapter = {
49
+ name: 'test-quadratic',
50
+ description: 'Minimize (x-3)^2',
51
+ params: [
52
+ { name: 'x', range: [-10, 10], integer: false } as ParamSpec,
53
+ ],
54
+ simulate(input: number, params: Record<string, number>): number {
55
+ // Return the residual as a scaled integer
56
+ const x = params['x'];
57
+ return Math.round(x * 10000);
58
+ },
59
+ reference(_input: number): number {
60
+ // Target: x = 3 → value 30000
61
+ return 30000;
62
+ },
63
+ sampleInputs(): number[] {
64
+ return [1]; // single input, target value is 3.0 (×10000 = 30000)
65
+ },
66
+ generateCode(params: Record<string, number>): string {
67
+ return `// x = ${params['x']}`;
68
+ },
69
+ };
70
+
71
+ const result = search(mockAdapter, 5000);
72
+ // Should converge close to x=3
73
+ expect(result.params['x']).toBeCloseTo(3.0, 1);
74
+ expect(result.maxError).toBeLessThan(0.1);
75
+ });
76
+
77
+ test('handles integer constraints', () => {
78
+ const mockAdapter: TunerAdapter = {
79
+ name: 'test-integer',
80
+ description: 'Integer parameter test',
81
+ params: [
82
+ { name: 'n', range: [0, 10], integer: true } as ParamSpec,
83
+ ],
84
+ simulate(input: number, params: Record<string, number>): number {
85
+ // Should snap to integer 7
86
+ return Math.round(params['n'] * 10000);
87
+ },
88
+ reference(_input: number): number {
89
+ return 70000; // 7.0 × 10000
90
+ },
91
+ sampleInputs(): number[] {
92
+ return [1];
93
+ },
94
+ generateCode(): string {
95
+ return '';
96
+ },
97
+ };
98
+
99
+ const result = search(mockAdapter, 2000);
100
+ // Should find n close to 7
101
+ expect(Math.round(result.params['n'])).toBe(7);
102
+ });
103
+
104
+ test('i32 overflow penalization', () => {
105
+ const mockAdapter: TunerAdapter = {
106
+ name: 'test-overflow',
107
+ description: 'Test overflow penalization',
108
+ params: [
109
+ { name: 'scale', range: [1, 1000], integer: true } as ParamSpec,
110
+ ],
111
+ simulate(_input: number, params: Record<string, number>): number {
112
+ // Always overflow for any scale >= 500
113
+ if (params['scale'] >= 500) return Infinity;
114
+ return params['scale'] * 10000;
115
+ },
116
+ reference(_input: number): number {
117
+ return 2000000; // target: scale=200 → 2000000
118
+ },
119
+ sampleInputs(): number[] {
120
+ return [1];
121
+ },
122
+ generateCode(): string {
123
+ return '';
124
+ },
125
+ };
126
+
127
+ const { maxError, mae, rmse } = evaluate(mockAdapter, { scale: 2147483647 });
128
+ expect(maxError).toBe(Infinity);
129
+ expect(mae).toBe(Infinity);
130
+ expect(rmse).toBe(Infinity);
131
+ });
132
+ });
133
+
134
+ // ─── ln-polynomial adapter tests ─────────────────────────────────────────────
135
+
136
+ describe('ln-polynomial adapter', () => {
137
+ const defaultParams = lnDefaultParams; // { A1: 20000, A3: 6667, A5: 4000 }
138
+
139
+ test('sample inputs cover the valid range', () => {
140
+ const inputs = lnPolynomialAdapter.sampleInputs();
141
+ expect(inputs.length).toBeGreaterThan(50);
142
+ // All inputs should be positive
143
+ expect(inputs.every(x => x > 0)).toBe(true);
144
+ });
145
+
146
+ test('reference matches Math.log', () => {
147
+ const SCALE = 10000;
148
+ // ln(1.0) = 0
149
+ expect(lnPolynomialAdapter.reference(SCALE)).toBeCloseTo(0, 5);
150
+ // ln(2.0) ≈ 0.6931 → 6931
151
+ expect(lnPolynomialAdapter.reference(2 * SCALE)).toBeCloseTo(6931.47, 0);
152
+ // ln(0.5) ≈ -0.6931 → -6931
153
+ expect(lnPolynomialAdapter.reference(5000)).toBeCloseTo(-6931.47, 0);
154
+ });
155
+
156
+ test('simulate produces reasonable output for x=1 (no error)', () => {
157
+ const result = lnPolynomialAdapter.simulate(10000, defaultParams);
158
+ // ln(1.0) = 0; allow some approximation error
159
+ expect(Math.abs(result)).toBeLessThan(500); // within 0.05
160
+ });
161
+
162
+ test('simulate returns Infinity for invalid input', () => {
163
+ const result = lnPolynomialAdapter.simulate(0, defaultParams);
164
+ expect(result).toBeLessThan(0); // negative sentinel or -MAX_INT
165
+ });
166
+
167
+ test('max_error < 0.001 with default atanh coefficients', () => {
168
+ const metrics = evaluate(lnPolynomialAdapter, defaultParams);
169
+ expect(metrics.maxError).toBeLessThan(0.001);
170
+ }, 10000);
171
+
172
+ test('search improves over default params', () => {
173
+ // Run a short search and confirm it doesn't get worse
174
+ const baseMetrics = evaluate(lnPolynomialAdapter, defaultParams);
175
+ const result = search(lnPolynomialAdapter, 500); // short budget for test speed
176
+ // Either same or better
177
+ expect(result.maxError).toBeLessThanOrEqual(baseMetrics.maxError * 2);
178
+ expect(result.maxError).toBeLessThan(0.01);
179
+ }, 30000);
180
+
181
+ test('generateCode produces valid output', () => {
182
+ const meta = {
183
+ maxError: 0.00003,
184
+ mae: 0.000012,
185
+ rmse: 0.000015,
186
+ estimatedCmds: 38,
187
+ tuneDate: '2026-03-17',
188
+ budgetUsed: 5000,
189
+ };
190
+ const code = lnPolynomialAdapter.generateCode(defaultParams, meta);
191
+ expect(code).toContain('AUTO-GENERATED');
192
+ expect(code).toContain('ln-polynomial');
193
+ expect(code).toContain('fn ln');
194
+ expect(code).toContain('A1');
195
+ expect(code).toContain('A3');
196
+ expect(code).toContain('A5');
197
+ expect(code).toContain('2026-03-17');
198
+ });
199
+
200
+ test('searchSA achieves max_error < 0.001 on ln-polynomial', () => {
201
+ const result = searchSA(lnPolynomialAdapter, 3000);
202
+ expect(result.maxError).toBeLessThan(0.001);
203
+ }, 30000);
204
+ });
205
+
206
+ // ─── sqrt-newton adapter tests ────────────────────────────────────────────────
207
+
208
+ describe('sqrt-newton adapter', () => {
209
+ test('simulate(10000, defaultParams) ≈ 10000 (sqrt(1.0)=1.0)', () => {
210
+ const result = sqrtNewtonAdapter.simulate(10000, sqrtDefaultParams);
211
+ // sqrt(1.0) * 10000 = 10000
212
+ expect(Math.abs(result - 10000)).toBeLessThan(10);
213
+ });
214
+
215
+ test('simulate(40000, defaultParams) ≈ 20000 (sqrt(4.0)=2.0)', () => {
216
+ const result = sqrtNewtonAdapter.simulate(40000, sqrtDefaultParams);
217
+ // sqrt(4.0) * 10000 = 20000
218
+ expect(Math.abs(result - 20000)).toBeLessThan(10);
219
+ });
220
+
221
+ test('simulate(0) returns 0', () => {
222
+ expect(sqrtNewtonAdapter.simulate(0, sqrtDefaultParams)).toBe(0);
223
+ expect(sqrtNewtonAdapter.simulate(-1, sqrtDefaultParams)).toBe(0);
224
+ });
225
+
226
+ test('simulate(250000, defaultParams) ≈ 50000 (sqrt(25.0)=5.0)', () => {
227
+ const result = sqrtNewtonAdapter.simulate(250000, sqrtDefaultParams);
228
+ expect(Math.abs(result - 50000)).toBeLessThan(10);
229
+ });
230
+
231
+ test('sample inputs are all positive', () => {
232
+ const inputs = sqrtNewtonAdapter.sampleInputs();
233
+ expect(inputs.length).toBeGreaterThan(50);
234
+ expect(inputs.every(x => x > 0)).toBe(true);
235
+ });
236
+
237
+ test('reference matches Math.sqrt', () => {
238
+ const SCALE = 10000;
239
+ expect(sqrtNewtonAdapter.reference(SCALE)).toBe(SCALE); // sqrt(1.0)
240
+ expect(sqrtNewtonAdapter.reference(4 * SCALE)).toBe(2 * SCALE); // sqrt(4.0)
241
+ expect(sqrtNewtonAdapter.reference(9 * SCALE)).toBe(3 * SCALE); // sqrt(9.0)
242
+ expect(sqrtNewtonAdapter.reference(0)).toBe(0);
243
+ });
244
+
245
+ test('generateCode contains fn sqrt_fx', () => {
246
+ const meta = {
247
+ maxError: 1.5,
248
+ mae: 0.5,
249
+ rmse: 0.8,
250
+ estimatedCmds: 30,
251
+ tuneDate: '2026-03-17',
252
+ budgetUsed: 3000,
253
+ };
254
+ const code = sqrtNewtonAdapter.generateCode(sqrtDefaultParams, meta);
255
+ expect(code).toContain('AUTO-GENERATED');
256
+ expect(code).toContain('sqrt-newton');
257
+ expect(code).toContain('fn sqrt_fx');
258
+ expect(code).toContain('2026-03-17');
259
+ });
260
+ });
@@ -270,6 +270,74 @@ fn test() {
270
270
  expect(errors[0].message).toContain('Return type mismatch: expected void, got int')
271
271
  })
272
272
 
273
+ it('rejects setTimeout inside a loop', () => {
274
+ const errors = typeCheck(`
275
+ fn test() {
276
+ while (true) {
277
+ setTimeout(20, () => { say("x"); });
278
+ }
279
+ }
280
+ `)
281
+ expect(errors.length).toBeGreaterThan(0)
282
+ expect(errors[0].message).toContain('cannot be called inside a loop')
283
+ })
284
+
285
+ it('rejects setTimeout inside an if body', () => {
286
+ const errors = typeCheck(`
287
+ fn test() {
288
+ if (true) {
289
+ setTimeout(20, () => { say("x"); });
290
+ }
291
+ }
292
+ `)
293
+ expect(errors.length).toBeGreaterThan(0)
294
+ expect(errors[0].message).toContain('cannot be called inside an if/else body')
295
+ })
296
+
297
+ it('rejects setInterval inside a loop', () => {
298
+ const errors = typeCheck(`
299
+ fn test() {
300
+ while (true) {
301
+ setInterval(20, () => { say("x"); });
302
+ }
303
+ }
304
+ `)
305
+ expect(errors.length).toBeGreaterThan(0)
306
+ expect(errors[0].message).toContain('cannot be called inside a loop')
307
+ })
308
+
309
+ it('rejects Timer::new() inside a loop', () => {
310
+ const errors = typeCheck(`
311
+ struct Timer { _id: int, _duration: int }
312
+ impl Timer {
313
+ fn new(duration: int) -> Timer { return { _id: 0, _duration: duration }; }
314
+ }
315
+ fn test() {
316
+ while (true) {
317
+ let t: Timer = Timer::new(10);
318
+ }
319
+ }
320
+ `)
321
+ expect(errors.length).toBeGreaterThan(0)
322
+ expect(errors[0].message).toContain('Timer::new() cannot be called inside a loop')
323
+ })
324
+
325
+ it('rejects Timer::new() inside an if body', () => {
326
+ const errors = typeCheck(`
327
+ struct Timer { _id: int, _duration: int }
328
+ impl Timer {
329
+ fn new(duration: int) -> Timer { return { _id: 0, _duration: duration }; }
330
+ }
331
+ fn test() {
332
+ if (true) {
333
+ let t: Timer = Timer::new(10);
334
+ }
335
+ }
336
+ `)
337
+ expect(errors.length).toBeGreaterThan(0)
338
+ expect(errors[0].message).toContain('Timer::new() cannot be called inside an if/else body')
339
+ })
340
+
273
341
  it('allows impl instance methods with inferred self type', () => {
274
342
  const errors = typeCheck(`
275
343
  struct Timer { duration: int }
@@ -146,6 +146,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
146
146
  const coroResult = coroutineTransform(mirOpt, coroutineInfos)
147
147
  const mirFinal = coroResult.module
148
148
  tickFunctions.push(...coroResult.generatedTickFunctions)
149
+ warnings.push(...coroResult.warnings)
149
150
 
150
151
  // Stage 5: MIR → LIR
151
152
  const lir = lowerToLIR(mirFinal)
package/src/emit/index.ts CHANGED
@@ -208,7 +208,9 @@ function emitInstr(instr: LIRInstr, ns: string, obj: string, mcVersion: McVersio
208
208
 
209
209
  case 'call_context': {
210
210
  const subcmds = instr.subcommands.map(emitSubcmd).join(' ')
211
- return `execute ${subcmds} run function ${instr.fn}`
211
+ return subcmds
212
+ ? `execute ${subcmds} run function ${instr.fn}`
213
+ : `function ${instr.fn}`
212
214
  }
213
215
 
214
216
  case 'return_value':
package/src/lir/lower.ts CHANGED
@@ -334,6 +334,33 @@ function lowerInstrInner(
334
334
  break
335
335
  }
336
336
 
337
+ case 'score_read': {
338
+ // execute store result score $dst __obj run scoreboard players get <player> <obj>
339
+ const dst = ctx.slot(instr.dst)
340
+ instrs.push({
341
+ kind: 'store_cmd_to_score',
342
+ dst,
343
+ cmd: { kind: 'raw', cmd: `scoreboard players get ${instr.player} ${instr.obj}` },
344
+ })
345
+ break
346
+ }
347
+
348
+ case 'score_write': {
349
+ // Write a value to a vanilla MC scoreboard objective
350
+ if (instr.src.kind === 'const') {
351
+ instrs.push({ kind: 'raw', cmd: `scoreboard players set ${instr.player} ${instr.obj} ${instr.src.value}` })
352
+ } else {
353
+ // execute store result score <player> <obj> run scoreboard players get $src __ns
354
+ const srcSlot = operandToSlot(instr.src, ctx, instrs)
355
+ instrs.push({
356
+ kind: 'store_cmd_to_score',
357
+ dst: { player: instr.player, obj: instr.obj },
358
+ cmd: { kind: 'raw', cmd: `scoreboard players get ${srcSlot.player} ${srcSlot.obj}` },
359
+ })
360
+ }
361
+ break
362
+ }
363
+
337
364
  case 'call': {
338
365
  // Set parameter slots $p0, $p1, ...
339
366
  for (let i = 0; i < instr.args.length; i++) {