redscript-mc 1.1.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 (83) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/cli.test.js +138 -0
  5. package/dist/__tests__/codegen.test.js +25 -0
  6. package/dist/__tests__/dce.test.d.ts +1 -0
  7. package/dist/__tests__/dce.test.js +137 -0
  8. package/dist/__tests__/e2e.test.js +190 -12
  9. package/dist/__tests__/lexer.test.js +31 -4
  10. package/dist/__tests__/lowering.test.js +172 -9
  11. package/dist/__tests__/mc-integration.test.js +145 -51
  12. package/dist/__tests__/mc-syntax.test.js +12 -0
  13. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  14. package/dist/__tests__/parser.test.js +90 -0
  15. package/dist/__tests__/runtime.test.js +21 -8
  16. package/dist/__tests__/typechecker.test.js +188 -0
  17. package/dist/ast/types.d.ts +42 -3
  18. package/dist/cli.js +15 -10
  19. package/dist/codegen/mcfunction/index.js +30 -1
  20. package/dist/codegen/structure/index.d.ts +4 -1
  21. package/dist/codegen/structure/index.js +29 -2
  22. package/dist/compile.d.ts +11 -0
  23. package/dist/compile.js +40 -6
  24. package/dist/events/types.d.ts +35 -0
  25. package/dist/events/types.js +59 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +7 -3
  28. package/dist/ir/types.d.ts +4 -0
  29. package/dist/lexer/index.d.ts +2 -1
  30. package/dist/lexer/index.js +91 -1
  31. package/dist/lowering/index.d.ts +32 -1
  32. package/dist/lowering/index.js +476 -16
  33. package/dist/optimizer/dce.d.ts +23 -0
  34. package/dist/optimizer/dce.js +591 -0
  35. package/dist/parser/index.d.ts +4 -0
  36. package/dist/parser/index.js +160 -26
  37. package/dist/typechecker/index.d.ts +19 -0
  38. package/dist/typechecker/index.js +392 -17
  39. package/docs/ARCHITECTURE.zh.md +1088 -0
  40. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  41. package/editors/vscode/.vscodeignore +3 -0
  42. package/editors/vscode/CHANGELOG.md +9 -0
  43. package/editors/vscode/icon.png +0 -0
  44. package/editors/vscode/out/extension.js +1144 -72
  45. package/editors/vscode/package-lock.json +2 -2
  46. package/editors/vscode/package.json +1 -1
  47. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  48. package/examples/spiral.mcrs +79 -0
  49. package/logo.png +0 -0
  50. package/package.json +1 -1
  51. package/src/__tests__/cli.test.ts +166 -0
  52. package/src/__tests__/codegen.test.ts +27 -0
  53. package/src/__tests__/dce.test.ts +129 -0
  54. package/src/__tests__/e2e.test.ts +201 -12
  55. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  56. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  57. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  58. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  59. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  60. package/src/__tests__/lexer.test.ts +35 -4
  61. package/src/__tests__/lowering.test.ts +187 -9
  62. package/src/__tests__/mc-integration.test.ts +166 -51
  63. package/src/__tests__/mc-syntax.test.ts +14 -0
  64. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  65. package/src/__tests__/parser.test.ts +102 -5
  66. package/src/__tests__/runtime.test.ts +24 -8
  67. package/src/__tests__/typechecker.test.ts +204 -0
  68. package/src/ast/types.ts +39 -2
  69. package/src/cli.ts +24 -10
  70. package/src/codegen/mcfunction/index.ts +31 -1
  71. package/src/codegen/structure/index.ts +40 -2
  72. package/src/compile.ts +59 -7
  73. package/src/events/types.ts +69 -0
  74. package/src/index.ts +9 -4
  75. package/src/ir/types.ts +4 -0
  76. package/src/lexer/index.ts +105 -2
  77. package/src/lowering/index.ts +566 -18
  78. package/src/optimizer/dce.ts +618 -0
  79. package/src/parser/index.ts +187 -29
  80. package/src/stdlib/README.md +34 -4
  81. package/src/stdlib/tags.mcrs +951 -0
  82. package/src/stdlib/timer.mcrs +54 -33
  83. package/src/typechecker/index.ts +469 -18
@@ -16,11 +16,31 @@ const PRECEDENCE = {
16
16
  '||': 1,
17
17
  '&&': 2,
18
18
  '==': 3, '!=': 3,
19
- '<': 4, '<=': 4, '>': 4, '>=': 4,
19
+ '<': 4, '<=': 4, '>': 4, '>=': 4, 'is': 4,
20
20
  '+': 5, '-': 5,
21
21
  '*': 6, '/': 6, '%': 6,
22
22
  };
23
- const BINARY_OPS = new Set(['||', '&&', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '%']);
23
+ const BINARY_OPS = new Set(['||', '&&', '==', '!=', '<', '<=', '>', '>=', 'is', '+', '-', '*', '/', '%']);
24
+ const ENTITY_TYPE_NAMES = new Set([
25
+ 'entity',
26
+ 'Player',
27
+ 'Mob',
28
+ 'HostileMob',
29
+ 'PassiveMob',
30
+ 'Zombie',
31
+ 'Skeleton',
32
+ 'Creeper',
33
+ 'Spider',
34
+ 'Enderman',
35
+ 'Pig',
36
+ 'Cow',
37
+ 'Sheep',
38
+ 'Chicken',
39
+ 'Villager',
40
+ 'ArmorStand',
41
+ 'Item',
42
+ 'Arrow',
43
+ ]);
24
44
  function computeIsSingle(raw) {
25
45
  if (/^@[spr](\[|$)/.test(raw))
26
46
  return true;
@@ -102,6 +122,7 @@ class Parser {
102
122
  const globals = [];
103
123
  const declarations = [];
104
124
  const structs = [];
125
+ const implBlocks = [];
105
126
  const enums = [];
106
127
  const consts = [];
107
128
  // Check for namespace declaration
@@ -119,6 +140,9 @@ class Parser {
119
140
  else if (this.check('struct')) {
120
141
  structs.push(this.parseStructDecl());
121
142
  }
143
+ else if (this.check('impl')) {
144
+ implBlocks.push(this.parseImplBlock());
145
+ }
122
146
  else if (this.check('enum')) {
123
147
  enums.push(this.parseEnumDecl());
124
148
  }
@@ -129,7 +153,7 @@ class Parser {
129
153
  declarations.push(this.parseFnDecl());
130
154
  }
131
155
  }
132
- return { namespace, globals, declarations, structs, enums, consts };
156
+ return { namespace, globals, declarations, structs, implBlocks, enums, consts };
133
157
  }
134
158
  // -------------------------------------------------------------------------
135
159
  // Struct Declaration
@@ -175,6 +199,17 @@ class Parser {
175
199
  this.expect('}');
176
200
  return this.withLoc({ name, variants }, enumToken);
177
201
  }
202
+ parseImplBlock() {
203
+ const implToken = this.expect('impl');
204
+ const typeName = this.expect('ident').value;
205
+ this.expect('{');
206
+ const methods = [];
207
+ while (!this.check('}') && !this.check('eof')) {
208
+ methods.push(this.parseFnDecl(typeName));
209
+ }
210
+ this.expect('}');
211
+ return this.withLoc({ kind: 'impl_block', typeName, methods }, implToken);
212
+ }
178
213
  parseConstDecl() {
179
214
  const constToken = this.expect('const');
180
215
  const name = this.expect('ident').value;
@@ -198,15 +233,15 @@ class Parser {
198
233
  // -------------------------------------------------------------------------
199
234
  // Function Declaration
200
235
  // -------------------------------------------------------------------------
201
- parseFnDecl() {
236
+ parseFnDecl(implTypeName) {
202
237
  const decorators = this.parseDecorators();
203
238
  const fnToken = this.expect('fn');
204
239
  const name = this.expect('ident').value;
205
240
  this.expect('(');
206
- const params = this.parseParams();
241
+ const params = this.parseParams(implTypeName);
207
242
  this.expect(')');
208
243
  let returnType = { kind: 'named', name: 'void' };
209
- if (this.match('->')) {
244
+ if (this.match('->') || this.match(':')) {
210
245
  returnType = this.parseType();
211
246
  }
212
247
  const body = this.parseBlock();
@@ -222,7 +257,7 @@ class Parser {
222
257
  return decorators;
223
258
  }
224
259
  parseDecoratorValue(value) {
225
- // Parse @tick or @on_trigger("name") or @on_advancement("story/mine_diamond")
260
+ // Parse @tick, @on(PlayerDeath), or @on_trigger("name")
226
261
  const match = value.match(/^@(\w+)(?:\(([^)]*)\))?$/);
227
262
  if (!match) {
228
263
  this.error(`Invalid decorator: ${value}`);
@@ -233,6 +268,13 @@ class Parser {
233
268
  return { name };
234
269
  }
235
270
  const args = {};
271
+ if (name === 'on') {
272
+ const eventTypeMatch = argsStr.match(/^([A-Za-z_][A-Za-z0-9_]*)$/);
273
+ if (eventTypeMatch) {
274
+ args.eventType = eventTypeMatch[1];
275
+ return { name, args };
276
+ }
277
+ }
236
278
  // Handle @on_trigger("name"), @on_advancement("id"), @on_craft("item"), @on_join_team("team")
237
279
  if (name === 'on_trigger' || name === 'on_advancement' || name === 'on_craft' || name === 'on_join_team') {
238
280
  const strMatch = argsStr.match(/^"([^"]*)"$/);
@@ -273,14 +315,20 @@ class Parser {
273
315
  }
274
316
  return { name, args };
275
317
  }
276
- parseParams() {
318
+ parseParams(implTypeName) {
277
319
  const params = [];
278
320
  if (!this.check(')')) {
279
321
  do {
280
322
  const paramToken = this.expect('ident');
281
323
  const name = paramToken.value;
282
- this.expect(':');
283
- const type = this.parseType();
324
+ let type;
325
+ if (implTypeName && params.length === 0 && name === 'self' && !this.check(':')) {
326
+ type = { kind: 'struct', name: implTypeName };
327
+ }
328
+ else {
329
+ this.expect(':');
330
+ type = this.parseType();
331
+ }
284
332
  let defaultValue;
285
333
  if (this.match('=')) {
286
334
  defaultValue = this.parseExpr();
@@ -612,6 +660,11 @@ class Parser {
612
660
  if (prec < minPrec)
613
661
  break;
614
662
  const opToken = this.advance();
663
+ if (op === 'is') {
664
+ const entityType = this.parseEntityTypeName();
665
+ left = this.withLoc({ kind: 'is_check', expr: left, entityType }, this.getLocToken(left) ?? opToken);
666
+ continue;
667
+ }
615
668
  const right = this.parseBinaryExpr(prec + 1); // left associative
616
669
  left = this.withLoc({ kind: 'binary', op: op, left, right }, this.getLocToken(left) ?? opToken);
617
670
  }
@@ -630,6 +683,13 @@ class Parser {
630
683
  }
631
684
  return this.parsePostfixExpr();
632
685
  }
686
+ parseEntityTypeName() {
687
+ const token = this.expect('ident');
688
+ if (ENTITY_TYPE_NAMES.has(token.value)) {
689
+ return token.value;
690
+ }
691
+ this.error(`Unknown entity type '${token.value}'`);
692
+ }
633
693
  isSubtraction() {
634
694
  // Check if this minus is binary (subtraction) by looking at previous token
635
695
  // If previous was a value (literal, ident, ), ]) it's subtraction
@@ -710,6 +770,15 @@ class Parser {
710
770
  }
711
771
  parsePrimaryExpr() {
712
772
  const token = this.peek();
773
+ if (token.kind === 'ident' && this.peek(1).kind === '::') {
774
+ const typeToken = this.advance();
775
+ this.expect('::');
776
+ const methodToken = this.expect('ident');
777
+ this.expect('(');
778
+ const args = this.parseArgs();
779
+ this.expect(')');
780
+ return this.withLoc({ kind: 'static_call', type: typeToken.value, method: methodToken.value, args }, typeToken);
781
+ }
713
782
  if (token.kind === 'ident' && this.peek(1).kind === '=>') {
714
783
  return this.parseSingleParamLambda();
715
784
  }
@@ -723,6 +792,16 @@ class Parser {
723
792
  this.advance();
724
793
  return this.withLoc({ kind: 'float_lit', value: parseFloat(token.value) }, token);
725
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
+ }
726
805
  // NBT suffix literals
727
806
  if (token.kind === 'byte_lit') {
728
807
  this.advance();
@@ -745,6 +824,10 @@ class Parser {
745
824
  this.advance();
746
825
  return this.parseStringExpr(token);
747
826
  }
827
+ if (token.kind === 'f_string') {
828
+ this.advance();
829
+ return this.parseFStringExpr(token);
830
+ }
748
831
  // MC name literal: #health → mc_name node (value = "health", without #)
749
832
  if (token.kind === 'mc_name') {
750
833
  this.advance();
@@ -896,6 +979,56 @@ class Parser {
896
979
  }
897
980
  return this.withLoc({ kind: 'str_interp', parts }, token);
898
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
+ }
899
1032
  parseEmbeddedExpr(source) {
900
1033
  const tokens = new lexer_1.Lexer(source, this.filePath).tokenize();
901
1034
  const parser = new Parser(tokens, source, this.filePath);
@@ -1043,19 +1176,10 @@ class Parser {
1043
1176
  if (token.kind === '-') {
1044
1177
  return this.peek(offset + 1).kind === 'int_lit' ? 2 : 0;
1045
1178
  }
1046
- if (token.kind !== '~' && token.kind !== '^') {
1047
- return 0;
1048
- }
1049
- const next = this.peek(offset + 1);
1050
- 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') {
1051
1181
  return 1;
1052
1182
  }
1053
- if (next.kind === 'int_lit') {
1054
- return 2;
1055
- }
1056
- if (next.kind === '-' && this.peek(offset + 2).kind === 'int_lit') {
1057
- return 3;
1058
- }
1059
1183
  return 0;
1060
1184
  }
1061
1185
  parseBlockPos() {
@@ -1070,15 +1194,25 @@ class Parser {
1070
1194
  }
1071
1195
  parseCoordComponent() {
1072
1196
  const token = this.peek();
1073
- if (token.kind === '~' || token.kind === '^') {
1197
+ // Handle rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) tokens
1198
+ if (token.kind === 'rel_coord') {
1074
1199
  this.advance();
1075
- const offset = this.parseSignedCoordOffset();
1076
- return token.kind === '~'
1077
- ? { kind: 'relative', offset }
1078
- : { 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 };
1079
1208
  }
1080
1209
  return { kind: 'absolute', value: this.parseSignedCoordOffset(true) };
1081
1210
  }
1211
+ parseCoordOffsetFromValue(value) {
1212
+ if (value === '' || value === undefined)
1213
+ return 0;
1214
+ return parseFloat(value);
1215
+ }
1082
1216
  parseSignedCoordOffset(requireValue = false) {
1083
1217
  let sign = 1;
1084
1218
  if (this.match('-')) {
@@ -9,12 +9,15 @@ import { DiagnosticError } from '../diagnostics';
9
9
  export declare class TypeChecker {
10
10
  private collector;
11
11
  private functions;
12
+ private implMethods;
12
13
  private structs;
13
14
  private enums;
14
15
  private consts;
15
16
  private currentFn;
16
17
  private currentReturnType;
17
18
  private scope;
19
+ private selfTypeStack;
20
+ private readonly richTextBuiltins;
18
21
  constructor(source?: string, filePath?: string);
19
22
  private getNodeLocation;
20
23
  private report;
@@ -23,19 +26,35 @@ export declare class TypeChecker {
23
26
  */
24
27
  check(program: Program): DiagnosticError[];
25
28
  private checkFunction;
29
+ private checkFunctionDecorators;
26
30
  private checkBlock;
27
31
  private checkStmt;
28
32
  private checkLetStmt;
29
33
  private checkReturnStmt;
30
34
  private checkExpr;
31
35
  private checkCallExpr;
36
+ private checkRichTextBuiltinCall;
32
37
  private checkInvokeExpr;
33
38
  private checkFunctionCallArgs;
34
39
  private checkTpCall;
35
40
  private checkMemberExpr;
41
+ private checkStaticCallExpr;
36
42
  private checkLambdaExpr;
43
+ private checkIfBranches;
44
+ private getThenBranchNarrowing;
37
45
  private inferType;
38
46
  private inferLambdaType;
47
+ /** Infer entity type from a selector */
48
+ private inferEntityTypeFromSelector;
49
+ private resolveInstanceMethod;
50
+ /** Check if childType is a subtype of parentType */
51
+ private isEntitySubtype;
52
+ /** Push a new self type context */
53
+ private pushSelfType;
54
+ /** Pop self type context */
55
+ private popSelfType;
56
+ /** Get current @s type */
57
+ private getCurrentSelfType;
39
58
  private typesMatch;
40
59
  private typeToString;
41
60
  private normalizeType;