redscript-mc 2.1.0 → 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 +86 -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__/lsp.test.js +76 -0
- package/dist/src/__tests__/mc-integration.test.js +25 -13
- package/dist/src/__tests__/mc-syntax.test.js +1 -6
- package/dist/src/__tests__/schedule.test.js +105 -0
- package/dist/src/__tests__/stdlib-include.test.d.ts +1 -0
- package/dist/src/__tests__/stdlib-include.test.js +86 -0
- package/dist/src/__tests__/typechecker.test.js +63 -0
- package/dist/src/cli.js +10 -3
- package/dist/src/compile.d.ts +1 -0
- package/dist/src/compile.js +33 -10
- package/dist/src/emit/compile.d.ts +2 -0
- package/dist/src/emit/compile.js +3 -2
- package/dist/src/emit/index.js +3 -1
- package/dist/src/lir/lower.js +26 -0
- package/dist/src/lsp/server.js +51 -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/editors/vscode/syntaxes/redscript.tmLanguage.json +34 -0
- package/examples/coroutine-demo.mcrs +51 -0
- package/examples/enum-demo.mcrs +95 -0
- package/examples/scheduler-demo.mcrs +59 -0
- 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__/lsp.test.ts +89 -0
- package/src/__tests__/mc-integration.test.ts +25 -13
- package/src/__tests__/mc-syntax.test.ts +1 -7
- package/src/__tests__/schedule.test.ts +112 -0
- package/src/__tests__/stdlib-include.test.ts +61 -0
- package/src/__tests__/typechecker.test.ts +68 -0
- package/src/cli.ts +9 -1
- package/src/compile.ts +44 -15
- package/src/emit/compile.ts +5 -2
- package/src/emit/index.ts +3 -1
- package/src/lir/lower.ts +27 -0
- package/src/lsp/server.ts +55 -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
- package/examples/spiral.mcrs +0 -43
- package/src/examples/arena.mcrs +0 -44
- package/src/examples/counter.mcrs +0 -12
- package/src/examples/new_features_demo.mcrs +0 -193
- package/src/examples/rpg.mcrs +0 -13
- package/src/examples/stdlib_demo.mcrs +0 -181
|
@@ -38,7 +38,7 @@ const path = __importStar(require("path"));
|
|
|
38
38
|
const compile_1 = require("../compile");
|
|
39
39
|
const mc_validator_1 = require("../mc-validator");
|
|
40
40
|
const FIXTURE_PATH = path.join(__dirname, 'fixtures', 'mc-commands-1.21.4.json');
|
|
41
|
-
const EXAMPLES = ['
|
|
41
|
+
const EXAMPLES = ['shop', 'quiz', 'turret'];
|
|
42
42
|
function getCommands(source, namespace = 'test') {
|
|
43
43
|
const result = (0, compile_1.compile)(source, { namespace });
|
|
44
44
|
expect(result.success).toBe(true);
|
|
@@ -56,11 +56,6 @@ function validateSource(validator, source, namespace) {
|
|
|
56
56
|
}
|
|
57
57
|
describe('MC Command Syntax Validation', () => {
|
|
58
58
|
const validator = new mc_validator_1.MCCommandValidator(FIXTURE_PATH);
|
|
59
|
-
test('counter example generates valid MC commands', () => {
|
|
60
|
-
const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.mcrs'), 'utf-8');
|
|
61
|
-
const errors = validateSource(validator, src, 'counter');
|
|
62
|
-
expect(errors).toHaveLength(0);
|
|
63
|
-
});
|
|
64
59
|
EXAMPLES.forEach(name => {
|
|
65
60
|
test(`${name}.mcrs generates valid MC commands`, () => {
|
|
66
61
|
const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.mcrs`), 'utf-8');
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const path = __importStar(require("path"));
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const compile_1 = require("../emit/compile");
|
|
40
|
+
describe('stdlib include path', () => {
|
|
41
|
+
it('import "stdlib/math" resolves to the stdlib math module', () => {
|
|
42
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-stdlib-'));
|
|
43
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
44
|
+
fs.writeFileSync(mainPath, 'import "stdlib/math";\nfn main() { let x: int = abs(-5); }\n');
|
|
45
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
46
|
+
const result = (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath });
|
|
47
|
+
expect(result.files.some(f => f.path.includes('abs'))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('import "stdlib/math.mcrs" also resolves (explicit extension)', () => {
|
|
50
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-stdlib-'));
|
|
51
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
52
|
+
fs.writeFileSync(mainPath, 'import "stdlib/math.mcrs";\nfn main() { let x: int = abs(-5); }\n');
|
|
53
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
54
|
+
const result = (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath });
|
|
55
|
+
expect(result.files.some(f => f.path.includes('abs'))).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it('import "stdlib/vec" resolves to the stdlib vec module', () => {
|
|
58
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-stdlib-'));
|
|
59
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
60
|
+
fs.writeFileSync(mainPath, 'import "stdlib/vec";\nfn main() { let d: int = dot2d(1, 2, 3, 4); }\n');
|
|
61
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
62
|
+
const result = (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath });
|
|
63
|
+
expect(result.files.some(f => f.path.includes('dot2d'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('non-existent stdlib module gives a clear error', () => {
|
|
66
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-stdlib-'));
|
|
67
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
68
|
+
fs.writeFileSync(mainPath, 'import "stdlib/nonexistent";\nfn main() {}\n');
|
|
69
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
70
|
+
expect(() => (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath }))
|
|
71
|
+
.toThrow(/Cannot import/);
|
|
72
|
+
});
|
|
73
|
+
it('--include flag allows importing from custom directory', () => {
|
|
74
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-include-'));
|
|
75
|
+
const libDir = path.join(tempDir, 'mylibs');
|
|
76
|
+
fs.mkdirSync(libDir);
|
|
77
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
78
|
+
const libPath = path.join(libDir, 'helpers.mcrs');
|
|
79
|
+
fs.writeFileSync(libPath, 'fn triple(x: int) -> int { return x + x + x; }\n');
|
|
80
|
+
fs.writeFileSync(mainPath, 'import "helpers";\nfn main() { let x: int = triple(3); }\n');
|
|
81
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
82
|
+
const result = (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath, includeDirs: [libDir] });
|
|
83
|
+
expect(result.files.some(f => f.path.includes('triple'))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
//# sourceMappingURL=stdlib-include.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 }
|
package/dist/src/cli.js
CHANGED
|
@@ -89,6 +89,7 @@ Options:
|
|
|
89
89
|
--mc-version <ver> Target Minecraft version (default: 1.21). Affects codegen features.
|
|
90
90
|
e.g. --mc-version 1.20.2, --mc-version 1.19
|
|
91
91
|
--lenient Treat type errors as warnings instead of blocking compilation
|
|
92
|
+
--include <dir> Add a directory to the import search path (repeatable)
|
|
92
93
|
-h, --help Show this help message
|
|
93
94
|
`);
|
|
94
95
|
}
|
|
@@ -210,6 +211,12 @@ function parseArgs(args) {
|
|
|
210
211
|
result.lenient = true;
|
|
211
212
|
i++;
|
|
212
213
|
}
|
|
214
|
+
else if (arg === '--include') {
|
|
215
|
+
if (!result.includeDirs)
|
|
216
|
+
result.includeDirs = [];
|
|
217
|
+
result.includeDirs.push(args[++i]);
|
|
218
|
+
i++;
|
|
219
|
+
}
|
|
213
220
|
else if (!result.command) {
|
|
214
221
|
result.command = arg;
|
|
215
222
|
i++;
|
|
@@ -229,7 +236,7 @@ function deriveNamespace(filePath) {
|
|
|
229
236
|
// Convert to valid identifier: lowercase, replace non-alphanumeric with underscore
|
|
230
237
|
return basename.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
231
238
|
}
|
|
232
|
-
function compileCommand(file, output, namespace, sourceMap = false, mcVersionStr, lenient = false) {
|
|
239
|
+
function compileCommand(file, output, namespace, sourceMap = false, mcVersionStr, lenient = false, includeDirs) {
|
|
233
240
|
// Read source file
|
|
234
241
|
if (!fs.existsSync(file)) {
|
|
235
242
|
console.error(`Error: File not found: ${file}`);
|
|
@@ -247,7 +254,7 @@ function compileCommand(file, output, namespace, sourceMap = false, mcVersionStr
|
|
|
247
254
|
}
|
|
248
255
|
const source = fs.readFileSync(file, 'utf-8');
|
|
249
256
|
try {
|
|
250
|
-
const result = (0, index_1.compile)(source, { namespace, filePath: file, generateSourceMap: sourceMap, mcVersion, lenient });
|
|
257
|
+
const result = (0, index_1.compile)(source, { namespace, filePath: file, generateSourceMap: sourceMap, mcVersion, lenient, includeDirs });
|
|
251
258
|
for (const w of result.warnings) {
|
|
252
259
|
console.error(`Warning: ${w}`);
|
|
253
260
|
}
|
|
@@ -411,7 +418,7 @@ async function main() {
|
|
|
411
418
|
{
|
|
412
419
|
const namespace = parsed.namespace ?? deriveNamespace(parsed.file);
|
|
413
420
|
const output = parsed.output ?? './dist';
|
|
414
|
-
compileCommand(parsed.file, output, namespace, parsed.sourceMap, parsed.mcVersionStr, parsed.lenient);
|
|
421
|
+
compileCommand(parsed.file, output, namespace, parsed.sourceMap, parsed.mcVersionStr, parsed.lenient, parsed.includeDirs);
|
|
415
422
|
}
|
|
416
423
|
break;
|
|
417
424
|
case 'watch':
|
package/dist/src/compile.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export declare function resolveSourceLine(combinedLine: number, ranges: SourceRa
|
|
|
32
32
|
interface PreprocessOptions {
|
|
33
33
|
filePath?: string;
|
|
34
34
|
seen?: Set<string>;
|
|
35
|
+
includeDirs?: string[];
|
|
35
36
|
}
|
|
36
37
|
export declare function preprocessSourceWithMetadata(source: string, options?: PreprocessOptions): PreprocessedSource;
|
|
37
38
|
export declare function preprocessSource(source: string, options?: PreprocessOptions): string;
|
package/dist/src/compile.js
CHANGED
|
@@ -75,6 +75,31 @@ function isLibrarySource(source) {
|
|
|
75
75
|
}
|
|
76
76
|
return false;
|
|
77
77
|
}
|
|
78
|
+
/** Resolve an import specifier to an absolute file path, trying multiple locations. */
|
|
79
|
+
function resolveImportPath(spec, fromFile, includeDirs) {
|
|
80
|
+
const candidates = spec.endsWith('.mcrs') ? [spec] : [spec, spec + '.mcrs'];
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
// 1. Relative to the importing file
|
|
83
|
+
const rel = path.resolve(path.dirname(fromFile), candidate);
|
|
84
|
+
if (fs.existsSync(rel))
|
|
85
|
+
return rel;
|
|
86
|
+
// 2. stdlib directory (package root / src / stdlib)
|
|
87
|
+
// Strip leading 'stdlib/' prefix so `import "stdlib/math"` resolves to
|
|
88
|
+
// <stdlibDir>/math.mcrs rather than <stdlibDir>/stdlib/math.mcrs.
|
|
89
|
+
const stdlibDir = path.resolve(__dirname, '..', 'src', 'stdlib');
|
|
90
|
+
const stdlibCandidate = candidate.replace(/^stdlib\//, '');
|
|
91
|
+
const stdlib = path.resolve(stdlibDir, stdlibCandidate);
|
|
92
|
+
if (fs.existsSync(stdlib))
|
|
93
|
+
return stdlib;
|
|
94
|
+
// 3. Extra include dirs
|
|
95
|
+
for (const dir of includeDirs) {
|
|
96
|
+
const extra = path.resolve(dir, candidate);
|
|
97
|
+
if (fs.existsSync(extra))
|
|
98
|
+
return extra;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
78
103
|
function countLines(source) {
|
|
79
104
|
return source === '' ? 0 : source.split('\n').length;
|
|
80
105
|
}
|
|
@@ -88,6 +113,7 @@ function offsetRanges(ranges, lineOffset) {
|
|
|
88
113
|
function preprocessSourceWithMetadata(source, options = {}) {
|
|
89
114
|
const { filePath } = options;
|
|
90
115
|
const seen = options.seen ?? new Set();
|
|
116
|
+
const includeDirs = options.includeDirs ?? [];
|
|
91
117
|
if (filePath) {
|
|
92
118
|
seen.add(path.resolve(filePath));
|
|
93
119
|
}
|
|
@@ -105,27 +131,24 @@ function preprocessSourceWithMetadata(source, options = {}) {
|
|
|
105
131
|
if (!filePath) {
|
|
106
132
|
throw new diagnostics_1.DiagnosticError('ParseError', 'Import statements require a file path', { line: i + 1, col: 1 }, lines);
|
|
107
133
|
}
|
|
108
|
-
const importPath =
|
|
134
|
+
const importPath = resolveImportPath(match[1], filePath, includeDirs);
|
|
135
|
+
if (!importPath) {
|
|
136
|
+
throw new diagnostics_1.DiagnosticError('ParseError', `Cannot import '${match[1]}'`, { file: filePath, line: i + 1, col: 1 }, lines);
|
|
137
|
+
}
|
|
109
138
|
if (!seen.has(importPath)) {
|
|
110
139
|
seen.add(importPath);
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
importedSource = fs.readFileSync(importPath, 'utf-8');
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
throw new diagnostics_1.DiagnosticError('ParseError', `Cannot import '${match[1]}'`, { file: filePath, line: i + 1, col: 1 }, lines);
|
|
117
|
-
}
|
|
140
|
+
const importedSource = fs.readFileSync(importPath, 'utf-8');
|
|
118
141
|
if (isLibrarySource(importedSource)) {
|
|
119
142
|
// Library file: parse separately so its functions are DCE-eligible.
|
|
120
143
|
// Also collect any transitive library imports inside it.
|
|
121
|
-
const nested = preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen });
|
|
144
|
+
const nested = preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen, includeDirs });
|
|
122
145
|
libraryImports.push({ source: importedSource, filePath: importPath });
|
|
123
146
|
// Propagate transitive library imports (e.g. math.mcrs imports vec.mcrs)
|
|
124
147
|
if (nested.libraryImports)
|
|
125
148
|
libraryImports.push(...nested.libraryImports);
|
|
126
149
|
}
|
|
127
150
|
else {
|
|
128
|
-
imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }));
|
|
151
|
+
imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen, includeDirs }));
|
|
129
152
|
}
|
|
130
153
|
}
|
|
131
154
|
continue;
|
|
@@ -19,6 +19,8 @@ export interface CompileOptions {
|
|
|
19
19
|
* Use for gradual migration or testing with existing codebases that have type errors.
|
|
20
20
|
*/
|
|
21
21
|
lenient?: boolean;
|
|
22
|
+
/** Extra directories to search when resolving imports (in addition to relative and stdlib). */
|
|
23
|
+
includeDirs?: string[];
|
|
22
24
|
}
|
|
23
25
|
export interface CompileResult {
|
|
24
26
|
files: DatapackFile[];
|
package/dist/src/emit/compile.js
CHANGED
|
@@ -22,10 +22,10 @@ const budget_1 = require("../lir/budget");
|
|
|
22
22
|
const mc_version_1 = require("../types/mc-version");
|
|
23
23
|
const typechecker_1 = require("../typechecker");
|
|
24
24
|
function compile(source, options = {}) {
|
|
25
|
-
const { namespace = 'redscript', filePath, generateSourceMap = false, mcVersion = mc_version_1.DEFAULT_MC_VERSION, lenient = false } = options;
|
|
25
|
+
const { namespace = 'redscript', filePath, generateSourceMap = false, mcVersion = mc_version_1.DEFAULT_MC_VERSION, lenient = false, includeDirs } = options;
|
|
26
26
|
const warnings = [];
|
|
27
27
|
// Preprocess: resolve import directives, merge imported sources
|
|
28
|
-
const preprocessed = (0, compile_1.preprocessSourceWithMetadata)(source, { filePath });
|
|
28
|
+
const preprocessed = (0, compile_1.preprocessSourceWithMetadata)(source, { filePath, includeDirs });
|
|
29
29
|
const processedSource = preprocessed.source;
|
|
30
30
|
// Stage 1: Lex + Parse → AST
|
|
31
31
|
const lexer = new lexer_1.Lexer(processedSource);
|
|
@@ -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
|
package/dist/src/emit/index.js
CHANGED
|
@@ -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
|
|
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)}`;
|
package/dist/src/lir/lower.js
CHANGED
|
@@ -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++) {
|
package/dist/src/lsp/server.js
CHANGED
|
@@ -16,6 +16,7 @@ const lexer_1 = require("../lexer");
|
|
|
16
16
|
const parser_1 = require("../parser");
|
|
17
17
|
const typechecker_1 = require("../typechecker");
|
|
18
18
|
const diagnostics_1 = require("../diagnostics");
|
|
19
|
+
const metadata_1 = require("../builtins/metadata");
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Connection and document manager
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
@@ -82,6 +83,23 @@ function toDiagnostic(err) {
|
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
// ---------------------------------------------------------------------------
|
|
86
|
+
// Decorator hover docs
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
const DECORATOR_DOCS = {
|
|
89
|
+
tick: 'Runs every MC game tick (~20 Hz). No arguments.',
|
|
90
|
+
load: 'Runs on `/reload`. Use for initialization logic.',
|
|
91
|
+
coroutine: 'Splits loops into tick-spread continuations. Arg: `batch=N` (steps per tick, default 1).',
|
|
92
|
+
schedule: 'Schedules the function to run after N ticks. Arg: `ticks=N`.',
|
|
93
|
+
on_trigger: 'Runs when a trigger scoreboard objective is set by a player. Arg: trigger name.',
|
|
94
|
+
keep: 'Prevents the compiler from dead-code-eliminating this function.',
|
|
95
|
+
on: 'Generic event handler decorator.',
|
|
96
|
+
on_advancement: 'Runs when a player earns an advancement. Arg: advancement id.',
|
|
97
|
+
on_craft: 'Runs when a player crafts an item. Arg: item id.',
|
|
98
|
+
on_death: 'Runs when a player dies.',
|
|
99
|
+
on_join_team: 'Runs when a player joins a team. Arg: team name.',
|
|
100
|
+
on_login: 'Runs when a player logs in.',
|
|
101
|
+
};
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
85
103
|
// Hover helpers
|
|
86
104
|
// ---------------------------------------------------------------------------
|
|
87
105
|
/** Find the word at a position in a text. */
|
|
@@ -239,9 +257,42 @@ connection.onHover((params) => {
|
|
|
239
257
|
const program = cached?.program ?? null;
|
|
240
258
|
if (!program)
|
|
241
259
|
return null;
|
|
260
|
+
// Check if cursor is on a decorator (@tick, @load, etc.)
|
|
261
|
+
const lines = source.split('\n');
|
|
262
|
+
const lineText = lines[params.position.line] ?? '';
|
|
263
|
+
const decoratorMatch = lineText.match(/@([a-zA-Z_][a-zA-Z0-9_]*)/);
|
|
264
|
+
if (decoratorMatch) {
|
|
265
|
+
const ch = params.position.character;
|
|
266
|
+
const atIdx = lineText.indexOf('@');
|
|
267
|
+
const decoratorEnd = atIdx + 1 + decoratorMatch[1].length;
|
|
268
|
+
if (ch >= atIdx && ch <= decoratorEnd) {
|
|
269
|
+
const decoratorName = decoratorMatch[1];
|
|
270
|
+
const decoratorDoc = DECORATOR_DOCS[decoratorName];
|
|
271
|
+
if (decoratorDoc) {
|
|
272
|
+
const content = {
|
|
273
|
+
kind: node_1.MarkupKind.Markdown,
|
|
274
|
+
value: `**@${decoratorName}** — ${decoratorDoc}`,
|
|
275
|
+
};
|
|
276
|
+
return { contents: content };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
242
280
|
const word = wordAt(source, params.position);
|
|
243
281
|
if (!word)
|
|
244
282
|
return null;
|
|
283
|
+
// Check builtins
|
|
284
|
+
const builtin = metadata_1.BUILTIN_METADATA[word];
|
|
285
|
+
if (builtin) {
|
|
286
|
+
const paramStr = builtin.params
|
|
287
|
+
.map(p => `${p.name}: ${p.type}${p.required ? '' : '?'}`)
|
|
288
|
+
.join(', ');
|
|
289
|
+
const sig = `fn ${builtin.name}(${paramStr}): ${builtin.returns}`;
|
|
290
|
+
const content = {
|
|
291
|
+
kind: node_1.MarkupKind.Markdown,
|
|
292
|
+
value: `\`\`\`redscript\n${sig}\n\`\`\`\n${builtin.doc}`,
|
|
293
|
+
};
|
|
294
|
+
return { contents: content };
|
|
295
|
+
}
|
|
245
296
|
// Check if it's a known function
|
|
246
297
|
const fn = findFunction(program, word);
|
|
247
298
|
if (fn) {
|