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.
Files changed (54) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/dce.test.d.ts +1 -0
  5. package/dist/__tests__/dce.test.js +137 -0
  6. package/dist/__tests__/lexer.test.js +19 -2
  7. package/dist/__tests__/lowering.test.js +8 -0
  8. package/dist/__tests__/mc-syntax.test.js +12 -0
  9. package/dist/__tests__/parser.test.js +10 -0
  10. package/dist/__tests__/runtime.test.js +13 -0
  11. package/dist/__tests__/typechecker.test.js +30 -0
  12. package/dist/ast/types.d.ts +22 -2
  13. package/dist/cli.js +15 -10
  14. package/dist/codegen/structure/index.d.ts +4 -1
  15. package/dist/codegen/structure/index.js +4 -2
  16. package/dist/compile.d.ts +1 -0
  17. package/dist/compile.js +4 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +4 -1
  20. package/dist/lexer/index.d.ts +2 -1
  21. package/dist/lexer/index.js +89 -1
  22. package/dist/lowering/index.js +37 -1
  23. package/dist/optimizer/dce.d.ts +23 -0
  24. package/dist/optimizer/dce.js +591 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +81 -16
  27. package/dist/typechecker/index.d.ts +2 -0
  28. package/dist/typechecker/index.js +49 -0
  29. package/docs/ARCHITECTURE.zh.md +1088 -0
  30. package/editors/vscode/.vscodeignore +3 -0
  31. package/editors/vscode/icon.png +0 -0
  32. package/editors/vscode/package-lock.json +2 -2
  33. package/editors/vscode/package.json +1 -1
  34. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  35. package/examples/spiral.mcrs +79 -0
  36. package/logo.png +0 -0
  37. package/package.json +1 -1
  38. package/src/__tests__/dce.test.ts +129 -0
  39. package/src/__tests__/lexer.test.ts +21 -2
  40. package/src/__tests__/lowering.test.ts +9 -0
  41. package/src/__tests__/mc-syntax.test.ts +14 -0
  42. package/src/__tests__/parser.test.ts +11 -0
  43. package/src/__tests__/runtime.test.ts +16 -0
  44. package/src/__tests__/typechecker.test.ts +33 -0
  45. package/src/ast/types.ts +14 -1
  46. package/src/cli.ts +24 -10
  47. package/src/codegen/structure/index.ts +13 -2
  48. package/src/compile.ts +5 -1
  49. package/src/index.ts +5 -1
  50. package/src/lexer/index.ts +102 -1
  51. package/src/lowering/index.ts +38 -2
  52. package/src/optimizer/dce.ts +618 -0
  53. package/src/parser/index.ts +97 -17
  54. package/src/typechecker/index.ts +65 -0
@@ -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
- if (token.kind !== '~' && token.kind !== '^') {
1116
- return 0;
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
- if (token.kind === '~' || token.kind === '^') {
1197
+ // Handle rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) tokens
1198
+ if (token.kind === 'rel_coord') {
1143
1199
  this.advance();
1144
- const offset = this.parseSignedCoordOffset();
1145
- return token.kind === '~'
1146
- ? { kind: 'relative', offset }
1147
- : { kind: 'local', offset };
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':