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.
- package/CHANGELOG.md +11 -0
- package/README.md +50 -21
- package/README.zh.md +61 -61
- package/dist/src/__tests__/e2e/basic.test.js +25 -0
- package/dist/src/__tests__/e2e/coroutine.test.js +22 -0
- package/dist/src/__tests__/mc-integration.test.js +25 -13
- package/dist/src/__tests__/schedule.test.js +105 -0
- package/dist/src/__tests__/typechecker.test.js +63 -0
- package/dist/src/emit/compile.js +1 -0
- package/dist/src/emit/index.js +3 -1
- package/dist/src/lir/lower.js +26 -0
- package/dist/src/mir/lower.js +341 -12
- package/dist/src/mir/types.d.ts +10 -0
- package/dist/src/optimizer/copy_prop.js +4 -0
- package/dist/src/optimizer/coroutine.d.ts +2 -0
- package/dist/src/optimizer/coroutine.js +33 -1
- package/dist/src/optimizer/dce.js +7 -1
- package/dist/src/optimizer/lir/const_imm.js +1 -1
- package/dist/src/optimizer/lir/dead_slot.js +1 -1
- package/dist/src/typechecker/index.d.ts +2 -0
- package/dist/src/typechecker/index.js +29 -0
- package/docs/ROADMAP.md +35 -0
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/examples/coroutine-demo.mcrs +11 -10
- package/jest.config.js +19 -0
- package/package.json +1 -1
- package/src/__tests__/e2e/basic.test.ts +27 -0
- package/src/__tests__/e2e/coroutine.test.ts +23 -0
- package/src/__tests__/fixtures/array-test.mcrs +21 -22
- package/src/__tests__/fixtures/counter.mcrs +17 -0
- package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
- package/src/__tests__/mc-integration.test.ts +25 -13
- package/src/__tests__/schedule.test.ts +112 -0
- package/src/__tests__/typechecker.test.ts +68 -0
- package/src/emit/compile.ts +1 -0
- package/src/emit/index.ts +3 -1
- package/src/lir/lower.ts +27 -0
- package/src/mir/lower.ts +355 -9
- package/src/mir/types.ts +4 -0
- package/src/optimizer/copy_prop.ts +4 -0
- package/src/optimizer/coroutine.ts +37 -1
- package/src/optimizer/dce.ts +6 -1
- package/src/optimizer/lir/const_imm.ts +1 -1
- package/src/optimizer/lir/dead_slot.ts +1 -1
- package/src/stdlib/timer.mcrs +10 -5
- 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 }
|
package/src/emit/compile.ts
CHANGED
|
@@ -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
|
|
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++) {
|