redscript-mc 1.2.17 → 1.2.19
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/dist/__tests__/compile-all.test.d.ts +9 -0
- package/dist/__tests__/compile-all.test.js +90 -0
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +1 -0
- package/dist/lowering/index.js +20 -1
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +62 -4
- package/editors/vscode/out/extension.js +64 -7
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/compile-all.test.ts +60 -0
- package/src/examples/capture_the_flag.mcrs +1 -1
- package/src/examples/hunger_games.mcrs +1 -1
- package/src/examples/zombie_survival.mcrs +1 -1
- package/src/lexer/index.ts +2 -1
- package/src/lowering/index.ts +20 -1
- package/src/parser/index.ts +59 -10
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-all smoke test
|
|
3
|
+
*
|
|
4
|
+
* Finds every .mcrs file in the repo (excluding declaration files and node_modules)
|
|
5
|
+
* and verifies that each one compiles without throwing an error.
|
|
6
|
+
*
|
|
7
|
+
* This catches regressions where a language change breaks existing source files.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Compile-all smoke test
|
|
4
|
+
*
|
|
5
|
+
* Finds every .mcrs file in the repo (excluding declaration files and node_modules)
|
|
6
|
+
* and verifies that each one compiles without throwing an error.
|
|
7
|
+
*
|
|
8
|
+
* This catches regressions where a language change breaks existing source files.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const compile_1 = require("../compile");
|
|
47
|
+
const REPO_ROOT = path.resolve(__dirname, '../../');
|
|
48
|
+
/** Patterns to skip */
|
|
49
|
+
const SKIP_GLOBS = [
|
|
50
|
+
'node_modules',
|
|
51
|
+
'.git',
|
|
52
|
+
'builtins.d.mcrs', // declaration-only file, not valid source
|
|
53
|
+
'editors/', // copy of builtins.d.mcrs
|
|
54
|
+
];
|
|
55
|
+
function shouldSkip(filePath) {
|
|
56
|
+
const rel = path.relative(REPO_ROOT, filePath);
|
|
57
|
+
return SKIP_GLOBS.some(pat => rel.includes(pat));
|
|
58
|
+
}
|
|
59
|
+
function findMcrsFiles(dir) {
|
|
60
|
+
const results = [];
|
|
61
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
62
|
+
const fullPath = path.join(dir, entry.name);
|
|
63
|
+
if (shouldSkip(fullPath))
|
|
64
|
+
continue;
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
results.push(...findMcrsFiles(fullPath));
|
|
67
|
+
}
|
|
68
|
+
else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
|
|
69
|
+
results.push(fullPath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
const mcrsFiles = findMcrsFiles(REPO_ROOT);
|
|
75
|
+
describe('compile-all: every .mcrs file should compile without errors', () => {
|
|
76
|
+
test('found at least one .mcrs file', () => {
|
|
77
|
+
expect(mcrsFiles.length).toBeGreaterThan(0);
|
|
78
|
+
});
|
|
79
|
+
for (const filePath of mcrsFiles) {
|
|
80
|
+
const label = path.relative(REPO_ROOT, filePath);
|
|
81
|
+
test(label, () => {
|
|
82
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
83
|
+
// Should not throw
|
|
84
|
+
expect(() => {
|
|
85
|
+
(0, compile_1.compile)(source, { namespace: 'smoke_test', optimize: false });
|
|
86
|
+
}).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=compile-all.test.js.map
|
package/dist/lexer/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Handles special cases like entity selectors vs decorators,
|
|
6
6
|
* range literals, and raw commands.
|
|
7
7
|
*/
|
|
8
|
-
export type TokenKind = 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match' | 'return' | 'break' | 'continue' | 'as' | 'at' | 'in' | 'is' | 'struct' | 'impl' | 'enum' | 'trigger' | 'namespace' | 'execute' | 'run' | 'unless' | 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'true' | 'false' | 'selector' | 'decorator' | 'int_lit' | 'float_lit' | 'byte_lit' | 'short_lit' | 'long_lit' | 'double_lit' | 'string_lit' | 'f_string' | 'range_lit' | 'rel_coord' | 'local_coord' | '+' | '-' | '*' | '/' | '%' | '~' | '^' | '==' | '!=' | '<' | '<=' | '>' | '>=' | '&&' | '||' | '!' | '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '{' | '}' | '(' | ')' | '[' | ']' | ',' | ';' | ':' | '::' | '->' | '=>' | '.' | 'ident' | 'mc_name' | 'raw_cmd' | 'eof';
|
|
8
|
+
export type TokenKind = 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match' | 'return' | 'break' | 'continue' | 'as' | 'at' | 'in' | 'is' | 'struct' | 'impl' | 'enum' | 'trigger' | 'namespace' | 'execute' | 'run' | 'unless' | 'declare' | 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'true' | 'false' | 'selector' | 'decorator' | 'int_lit' | 'float_lit' | 'byte_lit' | 'short_lit' | 'long_lit' | 'double_lit' | 'string_lit' | 'f_string' | 'range_lit' | 'rel_coord' | 'local_coord' | '+' | '-' | '*' | '/' | '%' | '~' | '^' | '==' | '!=' | '<' | '<=' | '>' | '>=' | '&&' | '||' | '!' | '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '{' | '}' | '(' | ')' | '[' | ']' | ',' | ';' | ':' | '::' | '->' | '=>' | '.' | 'ident' | 'mc_name' | 'raw_cmd' | 'eof';
|
|
9
9
|
export interface Token {
|
|
10
10
|
kind: TokenKind;
|
|
11
11
|
value: string;
|
package/dist/lexer/index.js
CHANGED
package/dist/lowering/index.js
CHANGED
|
@@ -372,7 +372,26 @@ class Lowering {
|
|
|
372
372
|
if (macroParam) {
|
|
373
373
|
return { str: `$(${macroParam})`, macroParam };
|
|
374
374
|
}
|
|
375
|
-
// Handle ~ident
|
|
375
|
+
// Handle ~ident / ^ident syntax — relative/local coord with a VARIABLE offset.
|
|
376
|
+
//
|
|
377
|
+
// WHY macros are required here:
|
|
378
|
+
// Minecraft's ~N and ^N coordinate syntax requires N to be a compile-time
|
|
379
|
+
// literal number. There is no command that accepts a scoreboard value as a
|
|
380
|
+
// relative offset. Therefore `~height` (where height is a runtime int) can
|
|
381
|
+
// only be expressed at the MC level via the 1.20.2+ function macro system,
|
|
382
|
+
// which substitutes $(height) into the command text at call time.
|
|
383
|
+
//
|
|
384
|
+
// Contrast with absolute coords: `tp(target, x, y, z)` where x/y/z are
|
|
385
|
+
// plain ints — those become $(x) etc. as literal replacements, same mechanism,
|
|
386
|
+
// but the distinction matters to callers: ~$(height) means "relative by height
|
|
387
|
+
// blocks from current pos", not "teleport to absolute scoreboard value".
|
|
388
|
+
//
|
|
389
|
+
// Example:
|
|
390
|
+
// fn launch_up(target: selector, height: int) {
|
|
391
|
+
// tp(target, ~0, ~height, ~0); // "~height" parsed as rel_coord
|
|
392
|
+
// }
|
|
393
|
+
// Emits: $tp $(target) ~0 ~$(height) ~0
|
|
394
|
+
// Called: function ns:launch_up with storage rs:macro_args
|
|
376
395
|
if (expr.kind === 'rel_coord' || expr.kind === 'local_coord') {
|
|
377
396
|
const val = expr.value; // e.g. "~height" or "^depth"
|
|
378
397
|
const prefix = val[0]; // ~ or ^
|
package/dist/parser/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare class Parser {
|
|
|
27
27
|
private parseConstDecl;
|
|
28
28
|
private parseGlobalDecl;
|
|
29
29
|
private parseFnDecl;
|
|
30
|
+
/** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
|
|
31
|
+
private parseDeclareStub;
|
|
30
32
|
private parseDecorators;
|
|
31
33
|
private parseDecoratorValue;
|
|
32
34
|
private parseParams;
|
package/dist/parser/index.js
CHANGED
|
@@ -149,6 +149,11 @@ class Parser {
|
|
|
149
149
|
else if (this.check('const')) {
|
|
150
150
|
consts.push(this.parseConstDecl());
|
|
151
151
|
}
|
|
152
|
+
else if (this.check('declare')) {
|
|
153
|
+
// Declaration-only stub (e.g. from builtins.d.mcrs) — parse and discard
|
|
154
|
+
this.advance(); // consume 'declare'
|
|
155
|
+
this.parseDeclareStub();
|
|
156
|
+
}
|
|
152
157
|
else {
|
|
153
158
|
declarations.push(this.parseFnDecl());
|
|
154
159
|
}
|
|
@@ -254,6 +259,26 @@ class Parser {
|
|
|
254
259
|
const body = this.parseBlock();
|
|
255
260
|
return this.withLoc({ name, params, returnType, decorators, body }, fnToken);
|
|
256
261
|
}
|
|
262
|
+
/** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
|
|
263
|
+
parseDeclareStub() {
|
|
264
|
+
this.expect('fn');
|
|
265
|
+
this.expect('ident'); // name
|
|
266
|
+
this.expect('(');
|
|
267
|
+
// consume params until ')'
|
|
268
|
+
let depth = 1;
|
|
269
|
+
while (!this.check('eof') && depth > 0) {
|
|
270
|
+
const t = this.advance();
|
|
271
|
+
if (t.kind === '(')
|
|
272
|
+
depth++;
|
|
273
|
+
else if (t.kind === ')')
|
|
274
|
+
depth--;
|
|
275
|
+
}
|
|
276
|
+
// optional return type annotation `: type` or `-> type`
|
|
277
|
+
if (this.match(':') || this.match('->')) {
|
|
278
|
+
this.parseType();
|
|
279
|
+
}
|
|
280
|
+
this.match(';'); // consume trailing semicolon
|
|
281
|
+
}
|
|
257
282
|
parseDecorators() {
|
|
258
283
|
const decorators = [];
|
|
259
284
|
while (this.check('decorator')) {
|
|
@@ -539,10 +564,29 @@ class Parser {
|
|
|
539
564
|
parseForRangeStmt(forToken) {
|
|
540
565
|
const varName = this.expect('ident').value;
|
|
541
566
|
this.expect('in');
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
567
|
+
let start;
|
|
568
|
+
let end;
|
|
569
|
+
if (this.check('range_lit')) {
|
|
570
|
+
// Literal range: 0..10, 0..count, 0..=9
|
|
571
|
+
const rangeToken = this.advance();
|
|
572
|
+
const range = this.parseRangeValue(rangeToken.value);
|
|
573
|
+
start = this.withLoc({ kind: 'int_lit', value: range.min ?? 0 }, rangeToken);
|
|
574
|
+
if (range.max !== null && range.max !== undefined) {
|
|
575
|
+
// Fully numeric: 0..10
|
|
576
|
+
end = this.withLoc({ kind: 'int_lit', value: range.max }, rangeToken);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
// Open-ended: "0.." — parse the end expression from next tokens
|
|
580
|
+
end = this.parseUnaryExpr();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// Dynamic range: expr..expr (e.g. start..end) — not yet supported
|
|
585
|
+
// Fall back to: parse as int_lit 0..0 (safe default)
|
|
586
|
+
start = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek());
|
|
587
|
+
end = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek());
|
|
588
|
+
this.error('Dynamic range start requires a literal integer (e.g. 0..count)');
|
|
589
|
+
}
|
|
546
590
|
const body = this.parseBlock();
|
|
547
591
|
return this.withLoc({ kind: 'for_range', varName, start, end, body }, forToken);
|
|
548
592
|
}
|
|
@@ -1058,6 +1102,20 @@ class Parser {
|
|
|
1058
1102
|
this.error(`Unexpected token '${token.kind}'`);
|
|
1059
1103
|
}
|
|
1060
1104
|
parseLiteralExpr() {
|
|
1105
|
+
// Support negative literals: -5, -3.14
|
|
1106
|
+
if (this.check('-')) {
|
|
1107
|
+
this.advance();
|
|
1108
|
+
const token = this.peek();
|
|
1109
|
+
if (token.kind === 'int_lit') {
|
|
1110
|
+
this.advance();
|
|
1111
|
+
return this.withLoc({ kind: 'int_lit', value: -Number(token.value) }, token);
|
|
1112
|
+
}
|
|
1113
|
+
if (token.kind === 'float_lit') {
|
|
1114
|
+
this.advance();
|
|
1115
|
+
return this.withLoc({ kind: 'float_lit', value: -Number(token.value) }, token);
|
|
1116
|
+
}
|
|
1117
|
+
this.error('Expected number after unary -');
|
|
1118
|
+
}
|
|
1061
1119
|
const expr = this.parsePrimaryExpr();
|
|
1062
1120
|
if (expr.kind === 'int_lit' ||
|
|
1063
1121
|
expr.kind === 'float_lit' ||
|
|
@@ -186,6 +186,7 @@ var require_lexer = __commonJS({
|
|
|
186
186
|
execute: "execute",
|
|
187
187
|
run: "run",
|
|
188
188
|
unless: "unless",
|
|
189
|
+
declare: "declare",
|
|
189
190
|
int: "int",
|
|
190
191
|
bool: "bool",
|
|
191
192
|
float: "float",
|
|
@@ -355,6 +356,13 @@ var require_lexer = __commonJS({
|
|
|
355
356
|
value += this.advance();
|
|
356
357
|
}
|
|
357
358
|
}
|
|
359
|
+
if (/[a-zA-Z_]/.test(this.peek())) {
|
|
360
|
+
let ident = "";
|
|
361
|
+
while (/[a-zA-Z0-9_]/.test(this.peek())) {
|
|
362
|
+
ident += this.advance();
|
|
363
|
+
}
|
|
364
|
+
value += ident;
|
|
365
|
+
}
|
|
358
366
|
this.addToken("rel_coord", value, startLine, startCol);
|
|
359
367
|
return;
|
|
360
368
|
}
|
|
@@ -815,6 +823,9 @@ var require_parser = __commonJS({
|
|
|
815
823
|
enums.push(this.parseEnumDecl());
|
|
816
824
|
} else if (this.check("const")) {
|
|
817
825
|
consts.push(this.parseConstDecl());
|
|
826
|
+
} else if (this.check("declare")) {
|
|
827
|
+
this.advance();
|
|
828
|
+
this.parseDeclareStub();
|
|
818
829
|
} else {
|
|
819
830
|
declarations.push(this.parseFnDecl());
|
|
820
831
|
}
|
|
@@ -877,12 +888,15 @@ var require_parser = __commonJS({
|
|
|
877
888
|
parseConstDecl() {
|
|
878
889
|
const constToken = this.expect("const");
|
|
879
890
|
const name = this.expect("ident").value;
|
|
880
|
-
|
|
881
|
-
|
|
891
|
+
let type;
|
|
892
|
+
if (this.match(":")) {
|
|
893
|
+
type = this.parseType();
|
|
894
|
+
}
|
|
882
895
|
this.expect("=");
|
|
883
896
|
const value = this.parseLiteralExpr();
|
|
884
897
|
this.match(";");
|
|
885
|
-
|
|
898
|
+
const inferredType = type ?? (value.kind === "str_lit" ? { kind: "named", name: "string" } : value.kind === "bool_lit" ? { kind: "named", name: "bool" } : value.kind === "float_lit" ? { kind: "named", name: "float" } : { kind: "named", name: "int" });
|
|
899
|
+
return this.withLoc({ name, type: inferredType, value }, constToken);
|
|
886
900
|
}
|
|
887
901
|
parseGlobalDecl(mutable) {
|
|
888
902
|
const token = this.advance();
|
|
@@ -911,6 +925,24 @@ var require_parser = __commonJS({
|
|
|
911
925
|
const body = this.parseBlock();
|
|
912
926
|
return this.withLoc({ name, params, returnType, decorators, body }, fnToken);
|
|
913
927
|
}
|
|
928
|
+
/** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
|
|
929
|
+
parseDeclareStub() {
|
|
930
|
+
this.expect("fn");
|
|
931
|
+
this.expect("ident");
|
|
932
|
+
this.expect("(");
|
|
933
|
+
let depth = 1;
|
|
934
|
+
while (!this.check("eof") && depth > 0) {
|
|
935
|
+
const t = this.advance();
|
|
936
|
+
if (t.kind === "(")
|
|
937
|
+
depth++;
|
|
938
|
+
else if (t.kind === ")")
|
|
939
|
+
depth--;
|
|
940
|
+
}
|
|
941
|
+
if (this.match(":") || this.match("->")) {
|
|
942
|
+
this.parseType();
|
|
943
|
+
}
|
|
944
|
+
this.match(";");
|
|
945
|
+
}
|
|
914
946
|
parseDecorators() {
|
|
915
947
|
const decorators = [];
|
|
916
948
|
while (this.check("decorator")) {
|
|
@@ -3581,6 +3613,15 @@ var require_lowering = __commonJS({
|
|
|
3581
3613
|
return null;
|
|
3582
3614
|
return expr.name;
|
|
3583
3615
|
}
|
|
3616
|
+
tryGetMacroParamByName(name) {
|
|
3617
|
+
if (!this.currentFnParamNames.has(name))
|
|
3618
|
+
return null;
|
|
3619
|
+
if (this.constValues.has(name))
|
|
3620
|
+
return null;
|
|
3621
|
+
if (this.stringValues.has(name))
|
|
3622
|
+
return null;
|
|
3623
|
+
return name;
|
|
3624
|
+
}
|
|
3584
3625
|
/**
|
|
3585
3626
|
* Converts an expression to a string for use as a builtin arg.
|
|
3586
3627
|
* If the expression is a macro param, returns `$(name)` and sets macroParam.
|
|
@@ -3590,6 +3631,17 @@ var require_lowering = __commonJS({
|
|
|
3590
3631
|
if (macroParam) {
|
|
3591
3632
|
return { str: `$(${macroParam})`, macroParam };
|
|
3592
3633
|
}
|
|
3634
|
+
if (expr.kind === "rel_coord" || expr.kind === "local_coord") {
|
|
3635
|
+
const val = expr.value;
|
|
3636
|
+
const prefix = val[0];
|
|
3637
|
+
const rest = val.slice(1);
|
|
3638
|
+
if (rest && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(rest)) {
|
|
3639
|
+
const paramName = this.tryGetMacroParamByName(rest);
|
|
3640
|
+
if (paramName) {
|
|
3641
|
+
return { str: `${prefix}$(${paramName})`, macroParam: paramName };
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3593
3645
|
if (expr.kind === "struct_lit" || expr.kind === "array_lit") {
|
|
3594
3646
|
return { str: this.exprToSnbt(expr) };
|
|
3595
3647
|
}
|
|
@@ -5875,8 +5927,11 @@ var require_lowering = __commonJS({
|
|
|
5875
5927
|
}
|
|
5876
5928
|
/**
|
|
5877
5929
|
* Checks a raw() command string for `${...}` interpolation containing runtime variables.
|
|
5878
|
-
* - If the interpolated
|
|
5879
|
-
* - If the interpolated name is a
|
|
5930
|
+
* - If the interpolated expression is a numeric literal → OK (MC macro syntax).
|
|
5931
|
+
* - If the interpolated name is a compile-time constant (in constValues) → OK.
|
|
5932
|
+
* - If the interpolated name is a known runtime variable (in varMap) → DiagnosticError.
|
|
5933
|
+
* - Unknown names → OK (could be MC macro params or external constants).
|
|
5934
|
+
*
|
|
5880
5935
|
* This catches the common mistake of writing raw("say ${score}") expecting interpolation,
|
|
5881
5936
|
* which would silently emit a literal `${score}` in the MC command.
|
|
5882
5937
|
*/
|
|
@@ -5891,8 +5946,10 @@ var require_lowering = __commonJS({
|
|
|
5891
5946
|
if (this.constValues.has(name)) {
|
|
5892
5947
|
continue;
|
|
5893
5948
|
}
|
|
5894
|
-
|
|
5895
|
-
|
|
5949
|
+
if (this.varMap.has(name) || this.currentFnParamNames.has(name)) {
|
|
5950
|
+
const loc = span ?? { line: 1, col: 1 };
|
|
5951
|
+
throw new diagnostics_1.DiagnosticError("LoweringError", `raw() command contains runtime variable interpolation '\${${name}}'. Variables cannot be interpolated into raw commands at compile time. Use f-string messages (say/tell/announce) or MC macro syntax '$(${name})' for MC 1.20.2+ commands.`, loc);
|
|
5952
|
+
}
|
|
5896
5953
|
}
|
|
5897
5954
|
}
|
|
5898
5955
|
resolveInstanceMethod(expr) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "redscript-vscode",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.11",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"redscript": "file:../../"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
3
|
"displayName": "RedScript for Minecraft",
|
|
4
4
|
"description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.11",
|
|
6
6
|
"publisher": "bkmashiro",
|
|
7
7
|
"icon": "icon.png",
|
|
8
8
|
"license": "MIT",
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
{
|
|
272
272
|
"comment": "Builtin functions",
|
|
273
273
|
"name": "support.function.builtin.redscript",
|
|
274
|
-
"match": "\\b(say|tell|announce|title|subtitle|actionbar|title_times|give|kill|effect|clear|kick|xp_add|xp_set|tp|tp_to|setblock|fill|clone|summon|particle|playsound|weather|time_set|time_add|gamerule|difficulty|tag_add|tag_remove|scoreboard_get|scoreboard_set|scoreboard_add|scoreboard_display|scoreboard_hide|scoreboard_add_objective|scoreboard_remove_objective|
|
|
274
|
+
"match": "\\b(say|tell|tellraw|announce|title|subtitle|actionbar|title_times|give|kill|effect|effect_clear|clear|kick|xp_add|xp_set|tp|tp_to|setblock|fill|clone|summon|particle|playsound|weather|time_set|time_add|gamerule|difficulty|tag_add|tag_remove|scoreboard_get|score|scoreboard_set|scoreboard_add|scoreboard_display|scoreboard_hide|scoreboard_add_objective|scoreboard_remove_objective|random|random_native|random_sequence|data_get|data_merge|str_len|push|pop|bossbar_add|bossbar_set_value|bossbar_set_max|bossbar_set_color|bossbar_set_style|bossbar_set_visible|bossbar_set_players|bossbar_remove|bossbar_get_value|team_add|team_remove|team_join|team_leave|team_option|set_new|set_add|set_contains|set_remove|set_clear|setTimeout|setInterval|clearInterval|spawn_object|if_entity|unless_entity)(?=\\s*\\()"
|
|
275
275
|
},
|
|
276
276
|
{
|
|
277
277
|
"comment": "User-defined function calls",
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-all smoke test
|
|
3
|
+
*
|
|
4
|
+
* Finds every .mcrs file in the repo (excluding declaration files and node_modules)
|
|
5
|
+
* and verifies that each one compiles without throwing an error.
|
|
6
|
+
*
|
|
7
|
+
* This catches regressions where a language change breaks existing source files.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs'
|
|
11
|
+
import * as path from 'path'
|
|
12
|
+
import { compile } from '../compile'
|
|
13
|
+
|
|
14
|
+
const REPO_ROOT = path.resolve(__dirname, '../../')
|
|
15
|
+
|
|
16
|
+
/** Patterns to skip */
|
|
17
|
+
const SKIP_GLOBS = [
|
|
18
|
+
'node_modules',
|
|
19
|
+
'.git',
|
|
20
|
+
'builtins.d.mcrs', // declaration-only file, not valid source
|
|
21
|
+
'editors/', // copy of builtins.d.mcrs
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function shouldSkip(filePath: string): boolean {
|
|
25
|
+
const rel = path.relative(REPO_ROOT, filePath)
|
|
26
|
+
return SKIP_GLOBS.some(pat => rel.includes(pat))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findMcrsFiles(dir: string): string[] {
|
|
30
|
+
const results: string[] = []
|
|
31
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
+
const fullPath = path.join(dir, entry.name)
|
|
33
|
+
if (shouldSkip(fullPath)) continue
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
results.push(...findMcrsFiles(fullPath))
|
|
36
|
+
} else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
|
|
37
|
+
results.push(fullPath)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return results
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const mcrsFiles = findMcrsFiles(REPO_ROOT)
|
|
44
|
+
|
|
45
|
+
describe('compile-all: every .mcrs file should compile without errors', () => {
|
|
46
|
+
test('found at least one .mcrs file', () => {
|
|
47
|
+
expect(mcrsFiles.length).toBeGreaterThan(0)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
for (const filePath of mcrsFiles) {
|
|
51
|
+
const label = path.relative(REPO_ROOT, filePath)
|
|
52
|
+
test(label, () => {
|
|
53
|
+
const source = fs.readFileSync(filePath, 'utf8')
|
|
54
|
+
// Should not throw
|
|
55
|
+
expect(() => {
|
|
56
|
+
compile(source, { namespace: 'smoke_test', optimize: false })
|
|
57
|
+
}).not.toThrow()
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
})
|
package/src/lexer/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type TokenKind =
|
|
|
16
16
|
// Keywords
|
|
17
17
|
| 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match'
|
|
18
18
|
| 'return' | 'break' | 'continue' | 'as' | 'at' | 'in' | 'is' | 'struct' | 'impl' | 'enum' | 'trigger' | 'namespace'
|
|
19
|
-
| 'execute' | 'run' | 'unless'
|
|
19
|
+
| 'execute' | 'run' | 'unless' | 'declare'
|
|
20
20
|
// Types
|
|
21
21
|
| 'int' | 'bool' | 'float' | 'string' | 'void'
|
|
22
22
|
| 'BlockPos'
|
|
@@ -89,6 +89,7 @@ const KEYWORDS: Record<string, TokenKind> = {
|
|
|
89
89
|
execute: 'execute',
|
|
90
90
|
run: 'run',
|
|
91
91
|
unless: 'unless',
|
|
92
|
+
declare: 'declare',
|
|
92
93
|
int: 'int',
|
|
93
94
|
bool: 'bool',
|
|
94
95
|
float: 'float',
|
package/src/lowering/index.ts
CHANGED
|
@@ -387,7 +387,26 @@ export class Lowering {
|
|
|
387
387
|
if (macroParam) {
|
|
388
388
|
return { str: `$(${macroParam})`, macroParam }
|
|
389
389
|
}
|
|
390
|
-
// Handle ~ident
|
|
390
|
+
// Handle ~ident / ^ident syntax — relative/local coord with a VARIABLE offset.
|
|
391
|
+
//
|
|
392
|
+
// WHY macros are required here:
|
|
393
|
+
// Minecraft's ~N and ^N coordinate syntax requires N to be a compile-time
|
|
394
|
+
// literal number. There is no command that accepts a scoreboard value as a
|
|
395
|
+
// relative offset. Therefore `~height` (where height is a runtime int) can
|
|
396
|
+
// only be expressed at the MC level via the 1.20.2+ function macro system,
|
|
397
|
+
// which substitutes $(height) into the command text at call time.
|
|
398
|
+
//
|
|
399
|
+
// Contrast with absolute coords: `tp(target, x, y, z)` where x/y/z are
|
|
400
|
+
// plain ints — those become $(x) etc. as literal replacements, same mechanism,
|
|
401
|
+
// but the distinction matters to callers: ~$(height) means "relative by height
|
|
402
|
+
// blocks from current pos", not "teleport to absolute scoreboard value".
|
|
403
|
+
//
|
|
404
|
+
// Example:
|
|
405
|
+
// fn launch_up(target: selector, height: int) {
|
|
406
|
+
// tp(target, ~0, ~height, ~0); // "~height" parsed as rel_coord
|
|
407
|
+
// }
|
|
408
|
+
// Emits: $tp $(target) ~0 ~$(height) ~0
|
|
409
|
+
// Called: function ns:launch_up with storage rs:macro_args
|
|
391
410
|
if (expr.kind === 'rel_coord' || expr.kind === 'local_coord') {
|
|
392
411
|
const val = expr.value // e.g. "~height" or "^depth"
|
|
393
412
|
const prefix = val[0] // ~ or ^
|
package/src/parser/index.ts
CHANGED
|
@@ -180,6 +180,10 @@ export class Parser {
|
|
|
180
180
|
enums.push(this.parseEnumDecl())
|
|
181
181
|
} else if (this.check('const')) {
|
|
182
182
|
consts.push(this.parseConstDecl())
|
|
183
|
+
} else if (this.check('declare')) {
|
|
184
|
+
// Declaration-only stub (e.g. from builtins.d.mcrs) — parse and discard
|
|
185
|
+
this.advance() // consume 'declare'
|
|
186
|
+
this.parseDeclareStub()
|
|
183
187
|
} else {
|
|
184
188
|
declarations.push(this.parseFnDecl())
|
|
185
189
|
}
|
|
@@ -311,6 +315,25 @@ export class Parser {
|
|
|
311
315
|
return this.withLoc({ name, params, returnType, decorators, body }, fnToken)
|
|
312
316
|
}
|
|
313
317
|
|
|
318
|
+
/** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
|
|
319
|
+
private parseDeclareStub(): void {
|
|
320
|
+
this.expect('fn')
|
|
321
|
+
this.expect('ident') // name
|
|
322
|
+
this.expect('(')
|
|
323
|
+
// consume params until ')'
|
|
324
|
+
let depth = 1
|
|
325
|
+
while (!this.check('eof') && depth > 0) {
|
|
326
|
+
const t = this.advance()
|
|
327
|
+
if (t.kind === '(') depth++
|
|
328
|
+
else if (t.kind === ')') depth--
|
|
329
|
+
}
|
|
330
|
+
// optional return type annotation `: type` or `-> type`
|
|
331
|
+
if (this.match(':') || this.match('->')) {
|
|
332
|
+
this.parseType()
|
|
333
|
+
}
|
|
334
|
+
this.match(';') // consume trailing semicolon
|
|
335
|
+
}
|
|
336
|
+
|
|
314
337
|
private parseDecorators(): Decorator[] {
|
|
315
338
|
const decorators: Decorator[] = []
|
|
316
339
|
|
|
@@ -645,17 +668,29 @@ export class Parser {
|
|
|
645
668
|
private parseForRangeStmt(forToken: Token): Stmt {
|
|
646
669
|
const varName = this.expect('ident').value
|
|
647
670
|
this.expect('in')
|
|
648
|
-
const rangeToken = this.expect('range_lit')
|
|
649
|
-
const range = this.parseRangeValue(rangeToken.value)
|
|
650
671
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
)
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
rangeToken
|
|
658
|
-
|
|
672
|
+
let start: Expr
|
|
673
|
+
let end: Expr
|
|
674
|
+
|
|
675
|
+
if (this.check('range_lit')) {
|
|
676
|
+
// Literal range: 0..10, 0..count, 0..=9
|
|
677
|
+
const rangeToken = this.advance()
|
|
678
|
+
const range = this.parseRangeValue(rangeToken.value)
|
|
679
|
+
start = this.withLoc({ kind: 'int_lit', value: range.min ?? 0 }, rangeToken)
|
|
680
|
+
if (range.max !== null && range.max !== undefined) {
|
|
681
|
+
// Fully numeric: 0..10
|
|
682
|
+
end = this.withLoc({ kind: 'int_lit', value: range.max }, rangeToken)
|
|
683
|
+
} else {
|
|
684
|
+
// Open-ended: "0.." — parse the end expression from next tokens
|
|
685
|
+
end = this.parseUnaryExpr()
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
// Dynamic range: expr..expr (e.g. start..end) — not yet supported
|
|
689
|
+
// Fall back to: parse as int_lit 0..0 (safe default)
|
|
690
|
+
start = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
|
|
691
|
+
end = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
|
|
692
|
+
this.error('Dynamic range start requires a literal integer (e.g. 0..count)')
|
|
693
|
+
}
|
|
659
694
|
|
|
660
695
|
const body = this.parseBlock()
|
|
661
696
|
return this.withLoc({ kind: 'for_range', varName, start, end, body }, forToken)
|
|
@@ -1237,6 +1272,20 @@ export class Parser {
|
|
|
1237
1272
|
}
|
|
1238
1273
|
|
|
1239
1274
|
private parseLiteralExpr(): LiteralExpr {
|
|
1275
|
+
// Support negative literals: -5, -3.14
|
|
1276
|
+
if (this.check('-')) {
|
|
1277
|
+
this.advance()
|
|
1278
|
+
const token = this.peek()
|
|
1279
|
+
if (token.kind === 'int_lit') {
|
|
1280
|
+
this.advance()
|
|
1281
|
+
return this.withLoc({ kind: 'int_lit', value: -Number(token.value) }, token)
|
|
1282
|
+
}
|
|
1283
|
+
if (token.kind === 'float_lit') {
|
|
1284
|
+
this.advance()
|
|
1285
|
+
return this.withLoc({ kind: 'float_lit', value: -Number(token.value) }, token)
|
|
1286
|
+
}
|
|
1287
|
+
this.error('Expected number after unary -')
|
|
1288
|
+
}
|
|
1240
1289
|
const expr = this.parsePrimaryExpr()
|
|
1241
1290
|
if (
|
|
1242
1291
|
expr.kind === 'int_lit' ||
|