redscript-mc 1.2.0 → 1.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 +5 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/lexer.test.js +19 -2
- package/dist/__tests__/lowering.test.js +8 -0
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/parser.test.js +10 -0
- package/dist/__tests__/runtime.test.js +13 -0
- package/dist/__tests__/typechecker.test.js +30 -0
- package/dist/ast/types.d.ts +22 -2
- package/dist/cli.js +15 -10
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +4 -2
- package/dist/compile.d.ts +1 -0
- package/dist/compile.js +4 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +89 -1
- package/dist/lowering/index.js +37 -1
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +591 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +81 -16
- package/dist/typechecker/index.d.ts +2 -0
- package/dist/typechecker/index.js +49 -0
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
- package/examples/spiral.mcrs +79 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/src/__tests__/dce.test.ts +129 -0
- package/src/__tests__/lexer.test.ts +21 -2
- package/src/__tests__/lowering.test.ts +9 -0
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/parser.test.ts +11 -0
- package/src/__tests__/runtime.test.ts +16 -0
- package/src/__tests__/typechecker.test.ts +33 -0
- package/src/ast/types.ts +14 -1
- package/src/cli.ts +24 -10
- package/src/codegen/structure/index.ts +13 -2
- package/src/compile.ts +5 -1
- package/src/index.ts +5 -1
- package/src/lexer/index.ts +102 -1
- package/src/lowering/index.ts +38 -2
- package/src/optimizer/dce.ts +618 -0
- package/src/parser/index.ts +97 -17
- package/src/typechecker/index.ts +65 -0
package/dist/parser/index.js
CHANGED
|
@@ -792,6 +792,16 @@ class Parser {
|
|
|
792
792
|
this.advance();
|
|
793
793
|
return this.withLoc({ kind: 'float_lit', value: parseFloat(token.value) }, token);
|
|
794
794
|
}
|
|
795
|
+
// Relative coordinate: ~ ~5 ~-3 ~0.5
|
|
796
|
+
if (token.kind === 'rel_coord') {
|
|
797
|
+
this.advance();
|
|
798
|
+
return this.withLoc({ kind: 'rel_coord', value: token.value }, token);
|
|
799
|
+
}
|
|
800
|
+
// Local coordinate: ^ ^5 ^-3 ^0.5
|
|
801
|
+
if (token.kind === 'local_coord') {
|
|
802
|
+
this.advance();
|
|
803
|
+
return this.withLoc({ kind: 'local_coord', value: token.value }, token);
|
|
804
|
+
}
|
|
795
805
|
// NBT suffix literals
|
|
796
806
|
if (token.kind === 'byte_lit') {
|
|
797
807
|
this.advance();
|
|
@@ -814,6 +824,10 @@ class Parser {
|
|
|
814
824
|
this.advance();
|
|
815
825
|
return this.parseStringExpr(token);
|
|
816
826
|
}
|
|
827
|
+
if (token.kind === 'f_string') {
|
|
828
|
+
this.advance();
|
|
829
|
+
return this.parseFStringExpr(token);
|
|
830
|
+
}
|
|
817
831
|
// MC name literal: #health → mc_name node (value = "health", without #)
|
|
818
832
|
if (token.kind === 'mc_name') {
|
|
819
833
|
this.advance();
|
|
@@ -965,6 +979,56 @@ class Parser {
|
|
|
965
979
|
}
|
|
966
980
|
return this.withLoc({ kind: 'str_interp', parts }, token);
|
|
967
981
|
}
|
|
982
|
+
parseFStringExpr(token) {
|
|
983
|
+
const parts = [];
|
|
984
|
+
let current = '';
|
|
985
|
+
let index = 0;
|
|
986
|
+
while (index < token.value.length) {
|
|
987
|
+
if (token.value[index] === '{') {
|
|
988
|
+
if (current) {
|
|
989
|
+
parts.push({ kind: 'text', value: current });
|
|
990
|
+
current = '';
|
|
991
|
+
}
|
|
992
|
+
index++;
|
|
993
|
+
let depth = 1;
|
|
994
|
+
let exprSource = '';
|
|
995
|
+
let inString = false;
|
|
996
|
+
while (index < token.value.length && depth > 0) {
|
|
997
|
+
const char = token.value[index];
|
|
998
|
+
if (char === '"' && token.value[index - 1] !== '\\') {
|
|
999
|
+
inString = !inString;
|
|
1000
|
+
}
|
|
1001
|
+
if (!inString) {
|
|
1002
|
+
if (char === '{') {
|
|
1003
|
+
depth++;
|
|
1004
|
+
}
|
|
1005
|
+
else if (char === '}') {
|
|
1006
|
+
depth--;
|
|
1007
|
+
if (depth === 0) {
|
|
1008
|
+
index++;
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (depth > 0) {
|
|
1014
|
+
exprSource += char;
|
|
1015
|
+
}
|
|
1016
|
+
index++;
|
|
1017
|
+
}
|
|
1018
|
+
if (depth !== 0) {
|
|
1019
|
+
this.error('Unterminated f-string interpolation');
|
|
1020
|
+
}
|
|
1021
|
+
parts.push({ kind: 'expr', expr: this.parseEmbeddedExpr(exprSource) });
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
current += token.value[index];
|
|
1025
|
+
index++;
|
|
1026
|
+
}
|
|
1027
|
+
if (current) {
|
|
1028
|
+
parts.push({ kind: 'text', value: current });
|
|
1029
|
+
}
|
|
1030
|
+
return this.withLoc({ kind: 'f_string', parts }, token);
|
|
1031
|
+
}
|
|
968
1032
|
parseEmbeddedExpr(source) {
|
|
969
1033
|
const tokens = new lexer_1.Lexer(source, this.filePath).tokenize();
|
|
970
1034
|
const parser = new Parser(tokens, source, this.filePath);
|
|
@@ -1112,19 +1176,10 @@ class Parser {
|
|
|
1112
1176
|
if (token.kind === '-') {
|
|
1113
1177
|
return this.peek(offset + 1).kind === 'int_lit' ? 2 : 0;
|
|
1114
1178
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
const next = this.peek(offset + 1);
|
|
1119
|
-
if (next.kind === ',' || next.kind === ')') {
|
|
1179
|
+
// rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) are single tokens now
|
|
1180
|
+
if (token.kind === 'rel_coord' || token.kind === 'local_coord') {
|
|
1120
1181
|
return 1;
|
|
1121
1182
|
}
|
|
1122
|
-
if (next.kind === 'int_lit') {
|
|
1123
|
-
return 2;
|
|
1124
|
-
}
|
|
1125
|
-
if (next.kind === '-' && this.peek(offset + 2).kind === 'int_lit') {
|
|
1126
|
-
return 3;
|
|
1127
|
-
}
|
|
1128
1183
|
return 0;
|
|
1129
1184
|
}
|
|
1130
1185
|
parseBlockPos() {
|
|
@@ -1139,15 +1194,25 @@ class Parser {
|
|
|
1139
1194
|
}
|
|
1140
1195
|
parseCoordComponent() {
|
|
1141
1196
|
const token = this.peek();
|
|
1142
|
-
|
|
1197
|
+
// Handle rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) tokens
|
|
1198
|
+
if (token.kind === 'rel_coord') {
|
|
1143
1199
|
this.advance();
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1200
|
+
// Parse the offset from the token value (e.g., "~5" -> 5, "~" -> 0, "~-3" -> -3)
|
|
1201
|
+
const offset = this.parseCoordOffsetFromValue(token.value.slice(1));
|
|
1202
|
+
return { kind: 'relative', offset };
|
|
1203
|
+
}
|
|
1204
|
+
if (token.kind === 'local_coord') {
|
|
1205
|
+
this.advance();
|
|
1206
|
+
const offset = this.parseCoordOffsetFromValue(token.value.slice(1));
|
|
1207
|
+
return { kind: 'local', offset };
|
|
1148
1208
|
}
|
|
1149
1209
|
return { kind: 'absolute', value: this.parseSignedCoordOffset(true) };
|
|
1150
1210
|
}
|
|
1211
|
+
parseCoordOffsetFromValue(value) {
|
|
1212
|
+
if (value === '' || value === undefined)
|
|
1213
|
+
return 0;
|
|
1214
|
+
return parseFloat(value);
|
|
1215
|
+
}
|
|
1151
1216
|
parseSignedCoordOffset(requireValue = false) {
|
|
1152
1217
|
let sign = 1;
|
|
1153
1218
|
if (this.match('-')) {
|
|
@@ -17,6 +17,7 @@ export declare class TypeChecker {
|
|
|
17
17
|
private currentReturnType;
|
|
18
18
|
private scope;
|
|
19
19
|
private selfTypeStack;
|
|
20
|
+
private readonly richTextBuiltins;
|
|
20
21
|
constructor(source?: string, filePath?: string);
|
|
21
22
|
private getNodeLocation;
|
|
22
23
|
private report;
|
|
@@ -32,6 +33,7 @@ export declare class TypeChecker {
|
|
|
32
33
|
private checkReturnStmt;
|
|
33
34
|
private checkExpr;
|
|
34
35
|
private checkCallExpr;
|
|
36
|
+
private checkRichTextBuiltinCall;
|
|
35
37
|
private checkInvokeExpr;
|
|
36
38
|
private checkFunctionCallArgs;
|
|
37
39
|
private checkTpCall;
|
|
@@ -61,6 +61,8 @@ const MC_TYPE_TO_ENTITY = {
|
|
|
61
61
|
};
|
|
62
62
|
const VOID_TYPE = { kind: 'named', name: 'void' };
|
|
63
63
|
const INT_TYPE = { kind: 'named', name: 'int' };
|
|
64
|
+
const STRING_TYPE = { kind: 'named', name: 'string' };
|
|
65
|
+
const FORMAT_STRING_TYPE = { kind: 'named', name: 'format_string' };
|
|
64
66
|
const BUILTIN_SIGNATURES = {
|
|
65
67
|
setTimeout: {
|
|
66
68
|
params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
|
|
@@ -90,6 +92,15 @@ class TypeChecker {
|
|
|
90
92
|
this.scope = new Map();
|
|
91
93
|
// Stack for tracking @s type in different contexts
|
|
92
94
|
this.selfTypeStack = ['entity'];
|
|
95
|
+
this.richTextBuiltins = new Map([
|
|
96
|
+
['say', { messageIndex: 0 }],
|
|
97
|
+
['announce', { messageIndex: 0 }],
|
|
98
|
+
['tell', { messageIndex: 1 }],
|
|
99
|
+
['tellraw', { messageIndex: 1 }],
|
|
100
|
+
['title', { messageIndex: 1 }],
|
|
101
|
+
['actionbar', { messageIndex: 1 }],
|
|
102
|
+
['subtitle', { messageIndex: 1 }],
|
|
103
|
+
]);
|
|
93
104
|
this.collector = new diagnostics_1.DiagnosticCollector(source, filePath);
|
|
94
105
|
}
|
|
95
106
|
getNodeLocation(node) {
|
|
@@ -435,6 +446,18 @@ class TypeChecker {
|
|
|
435
446
|
}
|
|
436
447
|
}
|
|
437
448
|
break;
|
|
449
|
+
case 'f_string':
|
|
450
|
+
for (const part of expr.parts) {
|
|
451
|
+
if (part.kind !== 'expr') {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
this.checkExpr(part.expr);
|
|
455
|
+
const partType = this.inferType(part.expr);
|
|
456
|
+
if (!(partType.kind === 'named' && (partType.name === 'int' || partType.name === 'string' || partType.name === 'format_string'))) {
|
|
457
|
+
this.report(`f-string placeholder must be int or string, got ${this.typeToString(partType)}`, part.expr);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
438
461
|
case 'array_lit':
|
|
439
462
|
for (const elem of expr.elements) {
|
|
440
463
|
this.checkExpr(elem);
|
|
@@ -464,6 +487,11 @@ class TypeChecker {
|
|
|
464
487
|
if (expr.fn === 'tp' || expr.fn === 'tp_to') {
|
|
465
488
|
this.checkTpCall(expr);
|
|
466
489
|
}
|
|
490
|
+
const richTextBuiltin = this.richTextBuiltins.get(expr.fn);
|
|
491
|
+
if (richTextBuiltin) {
|
|
492
|
+
this.checkRichTextBuiltinCall(expr, richTextBuiltin.messageIndex);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
467
495
|
const builtin = BUILTIN_SIGNATURES[expr.fn];
|
|
468
496
|
if (builtin) {
|
|
469
497
|
this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr);
|
|
@@ -506,6 +534,20 @@ class TypeChecker {
|
|
|
506
534
|
}
|
|
507
535
|
// Built-in functions are not checked for arg count
|
|
508
536
|
}
|
|
537
|
+
checkRichTextBuiltinCall(expr, messageIndex) {
|
|
538
|
+
for (let i = 0; i < expr.args.length; i++) {
|
|
539
|
+
this.checkExpr(expr.args[i], i === messageIndex ? undefined : STRING_TYPE);
|
|
540
|
+
}
|
|
541
|
+
const message = expr.args[messageIndex];
|
|
542
|
+
if (!message) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const messageType = this.inferType(message);
|
|
546
|
+
if (messageType.kind !== 'named' ||
|
|
547
|
+
(messageType.name !== 'string' && messageType.name !== 'format_string')) {
|
|
548
|
+
this.report(`Argument ${messageIndex + 1} of '${expr.fn}' expects string or format_string, got ${this.typeToString(messageType)}`, message);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
509
551
|
checkInvokeExpr(expr) {
|
|
510
552
|
this.checkExpr(expr.callee);
|
|
511
553
|
const calleeType = this.inferType(expr.callee);
|
|
@@ -695,6 +737,13 @@ class TypeChecker {
|
|
|
695
737
|
}
|
|
696
738
|
}
|
|
697
739
|
return { kind: 'named', name: 'string' };
|
|
740
|
+
case 'f_string':
|
|
741
|
+
for (const part of expr.parts) {
|
|
742
|
+
if (part.kind === 'expr') {
|
|
743
|
+
this.checkExpr(part.expr);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return FORMAT_STRING_TYPE;
|
|
698
747
|
case 'blockpos':
|
|
699
748
|
return { kind: 'named', name: 'BlockPos' };
|
|
700
749
|
case 'ident':
|