redscript-mc 2.1.1 → 2.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 (47) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +50 -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__/typechecker.test.js +63 -0
  9. package/dist/src/emit/compile.js +1 -0
  10. package/dist/src/emit/index.js +3 -1
  11. package/dist/src/lir/lower.js +26 -0
  12. package/dist/src/mir/lower.js +341 -12
  13. package/dist/src/mir/types.d.ts +10 -0
  14. package/dist/src/optimizer/copy_prop.js +4 -0
  15. package/dist/src/optimizer/coroutine.d.ts +2 -0
  16. package/dist/src/optimizer/coroutine.js +33 -1
  17. package/dist/src/optimizer/dce.js +7 -1
  18. package/dist/src/optimizer/lir/const_imm.js +1 -1
  19. package/dist/src/optimizer/lir/dead_slot.js +1 -1
  20. package/dist/src/typechecker/index.d.ts +2 -0
  21. package/dist/src/typechecker/index.js +29 -0
  22. package/docs/ROADMAP.md +35 -0
  23. package/editors/vscode/package-lock.json +3 -3
  24. package/editors/vscode/package.json +1 -1
  25. package/examples/coroutine-demo.mcrs +11 -10
  26. package/jest.config.js +19 -0
  27. package/package.json +1 -1
  28. package/src/__tests__/e2e/basic.test.ts +27 -0
  29. package/src/__tests__/e2e/coroutine.test.ts +23 -0
  30. package/src/__tests__/fixtures/array-test.mcrs +21 -22
  31. package/src/__tests__/fixtures/counter.mcrs +17 -0
  32. package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
  33. package/src/__tests__/mc-integration.test.ts +25 -13
  34. package/src/__tests__/schedule.test.ts +112 -0
  35. package/src/__tests__/typechecker.test.ts +68 -0
  36. package/src/emit/compile.ts +1 -0
  37. package/src/emit/index.ts +3 -1
  38. package/src/lir/lower.ts +27 -0
  39. package/src/mir/lower.ts +355 -9
  40. package/src/mir/types.ts +4 -0
  41. package/src/optimizer/copy_prop.ts +4 -0
  42. package/src/optimizer/coroutine.ts +37 -1
  43. package/src/optimizer/dce.ts +6 -1
  44. package/src/optimizer/lir/const_imm.ts +1 -1
  45. package/src/optimizer/lir/dead_slot.ts +1 -1
  46. package/src/stdlib/timer.mcrs +10 -5
  47. package/src/typechecker/index.ts +39 -0
@@ -103,3 +103,115 @@ describe('@schedule decorator', () => {
103
103
  expect(startFn).toContain('function test:_schedule_after_one_second')
104
104
  })
105
105
  })
106
+
107
+ describe('setTimeout / setInterval codegen', () => {
108
+ test('setTimeout lifts lambda to __timeout_callback_0 and schedules it', () => {
109
+ const source = `
110
+ fn start() {
111
+ setTimeout(20, () => {
112
+ say("later");
113
+ });
114
+ }
115
+ `
116
+ const result = compile(source, { namespace: 'ns' })
117
+ const startFn = getFile(result.files, 'start.mcfunction')
118
+ const cbFn = getFile(result.files, '__timeout_callback_0.mcfunction')
119
+ expect(startFn).toContain('schedule function ns:__timeout_callback_0 20t')
120
+ expect(cbFn).toBeDefined()
121
+ expect(cbFn).toContain('say later')
122
+ })
123
+
124
+ test('setInterval lambda reschedules itself at the end', () => {
125
+ const source = `
126
+ fn start() {
127
+ setInterval(10, () => {
128
+ say("tick");
129
+ });
130
+ }
131
+ `
132
+ const result = compile(source, { namespace: 'ns' })
133
+ const cbFn = getFile(result.files, '__timeout_callback_0.mcfunction')
134
+ expect(cbFn).toBeDefined()
135
+ expect(cbFn).toContain('schedule function ns:__timeout_callback_0 10t')
136
+ })
137
+
138
+ test('multiple setTimeout calls get unique callback names', () => {
139
+ const source = `
140
+ fn start() {
141
+ setTimeout(10, () => { say("a"); });
142
+ setTimeout(20, () => { say("b"); });
143
+ }
144
+ `
145
+ const result = compile(source, { namespace: 'ns' })
146
+ const cb0 = getFile(result.files, '__timeout_callback_0.mcfunction')
147
+ const cb1 = getFile(result.files, '__timeout_callback_1.mcfunction')
148
+ expect(cb0).toBeDefined()
149
+ expect(cb1).toBeDefined()
150
+ expect(cb0).toContain('say a')
151
+ expect(cb1).toContain('say b')
152
+ })
153
+ })
154
+
155
+ const TIMER_STRUCT = `
156
+ struct Timer {
157
+ _id: int,
158
+ _duration: int
159
+ }
160
+ impl Timer {
161
+ fn new(duration: int) -> Timer {
162
+ return { _id: 0, _duration: duration };
163
+ }
164
+ fn start(self) {}
165
+ fn pause(self) {}
166
+ fn reset(self) {}
167
+ fn tick(self) {}
168
+ fn done(self) -> bool { return false; }
169
+ fn elapsed(self) -> int { return 0; }
170
+ }
171
+ `
172
+
173
+ describe('Timer static allocation codegen', () => {
174
+ test('Timer::new() initializes unique scoreboard slots', () => {
175
+ const source = TIMER_STRUCT + `
176
+ fn init() {
177
+ let t: Timer = Timer::new(20);
178
+ }
179
+ `
180
+ const result = compile(source, { namespace: 'ns' })
181
+ const initFn = getFile(result.files, 'init.mcfunction')
182
+ expect(initFn).toContain('scoreboard players set __timer_0_ticks ns 0')
183
+ expect(initFn).toContain('scoreboard players set __timer_0_active ns 0')
184
+ })
185
+
186
+ test('Timer.start() inlines to scoreboard set active=1', () => {
187
+ const source = TIMER_STRUCT + `
188
+ fn init() {
189
+ let t: Timer = Timer::new(20);
190
+ t.start();
191
+ }
192
+ `
193
+ const result = compile(source, { namespace: 'ns' })
194
+ const initFn = getFile(result.files, 'init.mcfunction')
195
+ expect(initFn).toContain('scoreboard players set __timer_0_active ns 1')
196
+ expect(initFn).not.toContain('function ns:timer/start')
197
+ })
198
+
199
+ test('two Timer::new() calls get distinct IDs', () => {
200
+ const source = TIMER_STRUCT + `
201
+ fn init() {
202
+ let t0: Timer = Timer::new(10);
203
+ let t1: Timer = Timer::new(20);
204
+ t0.start();
205
+ t1.start();
206
+ }
207
+ `
208
+ const result = compile(source, { namespace: 'ns' })
209
+ const initFn = getFile(result.files, 'init.mcfunction')
210
+ // Both timers initialized
211
+ expect(initFn).toContain('__timer_0_ticks')
212
+ expect(initFn).toContain('__timer_1_ticks')
213
+ // Both started with unique slot names
214
+ expect(initFn).toContain('scoreboard players set __timer_0_active ns 1')
215
+ expect(initFn).toContain('scoreboard players set __timer_1_active ns 1')
216
+ })
217
+ })
@@ -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++) {