redscript-mc 1.2.0 → 1.2.2

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 (55) 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 +592 -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/out/extension.js +834 -19
  33. package/editors/vscode/package-lock.json +2 -2
  34. package/editors/vscode/package.json +1 -1
  35. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  36. package/examples/spiral.mcrs +41 -0
  37. package/logo.png +0 -0
  38. package/package.json +1 -1
  39. package/src/__tests__/dce.test.ts +129 -0
  40. package/src/__tests__/lexer.test.ts +21 -2
  41. package/src/__tests__/lowering.test.ts +9 -0
  42. package/src/__tests__/mc-syntax.test.ts +14 -0
  43. package/src/__tests__/parser.test.ts +11 -0
  44. package/src/__tests__/runtime.test.ts +16 -0
  45. package/src/__tests__/typechecker.test.ts +33 -0
  46. package/src/ast/types.ts +14 -1
  47. package/src/cli.ts +24 -10
  48. package/src/codegen/structure/index.ts +13 -2
  49. package/src/compile.ts +5 -1
  50. package/src/index.ts +5 -1
  51. package/src/lexer/index.ts +102 -1
  52. package/src/lowering/index.ts +38 -2
  53. package/src/optimizer/dce.ts +619 -0
  54. package/src/parser/index.ts +97 -17
  55. package/src/typechecker/index.ts +65 -0
@@ -14,5 +14,8 @@ export interface StructureCompileResult {
14
14
  blocks: StructureBlockInfo[];
15
15
  stats?: OptimizationStats;
16
16
  }
17
+ export interface StructureCompileOptions {
18
+ dce?: boolean;
19
+ }
17
20
  export declare function generateStructure(input: IRModule | DatapackFile[]): StructureCompileResult;
18
- export declare function compileToStructure(source: string, namespace: string, filePath?: string): StructureCompileResult;
21
+ export declare function compileToStructure(source: string, namespace: string, filePath?: string, options?: StructureCompileOptions): StructureCompileResult;
@@ -9,6 +9,7 @@ const nbt_1 = require("../../nbt");
9
9
  const commands_1 = require("../../optimizer/commands");
10
10
  const passes_1 = require("../../optimizer/passes");
11
11
  const structure_1 = require("../../optimizer/structure");
12
+ const dce_1 = require("../../optimizer/dce");
12
13
  const compile_1 = require("../../compile");
13
14
  const types_1 = require("../../events/types");
14
15
  const DATA_VERSION = 3953;
@@ -255,10 +256,11 @@ function generateStructure(input) {
255
256
  })),
256
257
  };
257
258
  }
258
- function compileToStructure(source, namespace, filePath) {
259
+ function compileToStructure(source, namespace, filePath, options = {}) {
259
260
  const preprocessedSource = (0, compile_1.preprocessSource)(source, { filePath });
260
261
  const tokens = new lexer_1.Lexer(preprocessedSource, filePath).tokenize();
261
- const ast = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
262
+ const parsedAst = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
263
+ const ast = options.dce ?? true ? (0, dce_1.eliminateDeadCode)(parsedAst) : parsedAst;
262
264
  const ir = new lowering_1.Lowering(namespace).lower(ast);
263
265
  const stats = (0, commands_1.createEmptyOptimizationStats)();
264
266
  const optimizedIRFunctions = ir.functions.map(fn => {
package/dist/compile.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface CompileOptions {
11
11
  namespace?: string;
12
12
  filePath?: string;
13
13
  optimize?: boolean;
14
+ dce?: boolean;
14
15
  }
15
16
  export interface CompileResult {
16
17
  success: boolean;
package/dist/compile.js CHANGED
@@ -48,6 +48,7 @@ const lexer_1 = require("./lexer");
48
48
  const parser_1 = require("./parser");
49
49
  const lowering_1 = require("./lowering");
50
50
  const passes_1 = require("./optimizer/passes");
51
+ const dce_1 = require("./optimizer/dce");
51
52
  const mcfunction_1 = require("./codegen/mcfunction");
52
53
  const diagnostics_1 = require("./diagnostics");
53
54
  const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/;
@@ -126,6 +127,7 @@ function preprocessSource(source, options = {}) {
126
127
  // ---------------------------------------------------------------------------
127
128
  function compile(source, options = {}) {
128
129
  const { namespace = 'redscript', filePath, optimize: shouldOptimize = true } = options;
130
+ const shouldRunDce = options.dce ?? shouldOptimize;
129
131
  let sourceLines = source.split('\n');
130
132
  try {
131
133
  const preprocessed = preprocessSourceWithMetadata(source, { filePath });
@@ -134,7 +136,8 @@ function compile(source, options = {}) {
134
136
  // Lexing
135
137
  const tokens = new lexer_1.Lexer(preprocessedSource, filePath).tokenize();
136
138
  // Parsing
137
- const ast = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
139
+ const parsedAst = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
140
+ const ast = shouldRunDce ? (0, dce_1.eliminateDeadCode)(parsedAst) : parsedAst;
138
141
  // Lowering
139
142
  const ir = new lowering_1.Lowering(namespace, preprocessed.ranges).lower(ast);
140
143
  // Optimization
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface CompileOptions {
14
14
  optimize?: boolean;
15
15
  typeCheck?: boolean;
16
16
  filePath?: string;
17
+ dce?: boolean;
17
18
  }
18
19
  export interface CompileResult {
19
20
  files: DatapackFile[];
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ const parser_1 = require("./parser");
13
13
  const typechecker_1 = require("./typechecker");
14
14
  const lowering_1 = require("./lowering");
15
15
  const passes_1 = require("./optimizer/passes");
16
+ const dce_1 = require("./optimizer/dce");
16
17
  const mcfunction_1 = require("./codegen/mcfunction");
17
18
  const compile_1 = require("./compile");
18
19
  const commands_1 = require("./optimizer/commands");
@@ -27,13 +28,15 @@ function compile(source, options = {}) {
27
28
  const namespace = options.namespace ?? 'redscript';
28
29
  const shouldOptimize = options.optimize ?? true;
29
30
  const shouldTypeCheck = options.typeCheck ?? true;
31
+ const shouldRunDce = options.dce ?? shouldOptimize;
30
32
  const filePath = options.filePath;
31
33
  const preprocessed = (0, compile_1.preprocessSourceWithMetadata)(source, { filePath });
32
34
  const preprocessedSource = preprocessed.source;
33
35
  // Lexing
34
36
  const tokens = new lexer_1.Lexer(preprocessedSource, filePath).tokenize();
35
37
  // Parsing
36
- const ast = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
38
+ const parsedAst = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
39
+ const ast = shouldRunDce ? (0, dce_1.eliminateDeadCode)(parsedAst) : parsedAst;
37
40
  // Type checking (warn mode - collect errors but don't block)
38
41
  let typeErrors;
39
42
  if (shouldTypeCheck) {
@@ -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' | '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' | 'range_lit' | '+' | '-' | '*' | '/' | '%' | '~' | '^' | '==' | '!=' | '<' | '<=' | '>' | '>=' | '&&' | '||' | '!' | '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '{' | '}' | '(' | ')' | '[' | ']' | ',' | ';' | ':' | '::' | '->' | '=>' | '.' | 'ident' | 'mc_name' | 'raw_cmd' | 'eof';
8
+ export type TokenKind = 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match' | 'return' | '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';
9
9
  export interface Token {
10
10
  kind: TokenKind;
11
11
  value: string;
@@ -31,6 +31,7 @@ export declare class Lexer {
31
31
  private scanAtToken;
32
32
  private scanSelectorParams;
33
33
  private scanString;
34
+ private scanFString;
34
35
  private scanNumber;
35
36
  private scanIdentifier;
36
37
  }
@@ -202,8 +202,50 @@ class Lexer {
202
202
  this.addToken('range_lit', value, startLine, startCol);
203
203
  return;
204
204
  }
205
+ // Relative coordinate: ~ or ~5 or ~-3 or ~0.5
206
+ if (char === '~') {
207
+ let value = '~';
208
+ // Check for optional sign
209
+ if (this.peek() === '-' || this.peek() === '+') {
210
+ value += this.advance();
211
+ }
212
+ // Check for number
213
+ while (/[0-9]/.test(this.peek())) {
214
+ value += this.advance();
215
+ }
216
+ // Check for decimal part
217
+ if (this.peek() === '.' && /[0-9]/.test(this.peek(1))) {
218
+ value += this.advance(); // .
219
+ while (/[0-9]/.test(this.peek())) {
220
+ value += this.advance();
221
+ }
222
+ }
223
+ this.addToken('rel_coord', value, startLine, startCol);
224
+ return;
225
+ }
226
+ // Local coordinate: ^ or ^5 or ^-3 or ^0.5
227
+ if (char === '^') {
228
+ let value = '^';
229
+ // Check for optional sign
230
+ if (this.peek() === '-' || this.peek() === '+') {
231
+ value += this.advance();
232
+ }
233
+ // Check for number
234
+ while (/[0-9]/.test(this.peek())) {
235
+ value += this.advance();
236
+ }
237
+ // Check for decimal part
238
+ if (this.peek() === '.' && /[0-9]/.test(this.peek(1))) {
239
+ value += this.advance(); // .
240
+ while (/[0-9]/.test(this.peek())) {
241
+ value += this.advance();
242
+ }
243
+ }
244
+ this.addToken('local_coord', value, startLine, startCol);
245
+ return;
246
+ }
205
247
  // Single-character operators and delimiters
206
- const singleChar = ['+', '-', '*', '/', '%', '~', '^', '<', '>', '!', '=',
248
+ const singleChar = ['+', '-', '*', '/', '%', '<', '>', '!', '=',
207
249
  '{', '}', '(', ')', '[', ']', ',', ';', ':', '.'];
208
250
  if (singleChar.includes(char)) {
209
251
  this.addToken(char, char, startLine, startCol);
@@ -214,6 +256,12 @@ class Lexer {
214
256
  this.scanAtToken(startLine, startCol);
215
257
  return;
216
258
  }
259
+ // f-string literal
260
+ if (char === 'f' && this.peek() === '"') {
261
+ this.advance();
262
+ this.scanFString(startLine, startCol);
263
+ return;
264
+ }
217
265
  // String literal
218
266
  if (char === '"') {
219
267
  this.scanString(startLine, startCol);
@@ -340,6 +388,46 @@ class Lexer {
340
388
  this.advance(); // closing quote
341
389
  this.addToken('string_lit', value, startLine, startCol);
342
390
  }
391
+ scanFString(startLine, startCol) {
392
+ let value = '';
393
+ let interpolationDepth = 0;
394
+ let interpolationString = false;
395
+ while (!this.isAtEnd()) {
396
+ if (interpolationDepth === 0 && this.peek() === '"') {
397
+ break;
398
+ }
399
+ if (this.peek() === '\\' && this.peek(1) === '"') {
400
+ this.advance();
401
+ value += this.advance();
402
+ continue;
403
+ }
404
+ if (interpolationDepth === 0 && this.peek() === '{') {
405
+ value += this.advance();
406
+ interpolationDepth = 1;
407
+ interpolationString = false;
408
+ continue;
409
+ }
410
+ const char = this.advance();
411
+ value += char;
412
+ if (interpolationDepth === 0)
413
+ continue;
414
+ if (char === '"' && this.source[this.pos - 2] !== '\\') {
415
+ interpolationString = !interpolationString;
416
+ continue;
417
+ }
418
+ if (interpolationString)
419
+ continue;
420
+ if (char === '{')
421
+ interpolationDepth++;
422
+ if (char === '}')
423
+ interpolationDepth--;
424
+ }
425
+ if (this.isAtEnd()) {
426
+ this.error('Unterminated f-string', startLine, startCol);
427
+ }
428
+ this.advance(); // closing quote
429
+ this.addToken('f_string', value, startLine, startCol);
430
+ }
343
431
  scanNumber(firstChar, startLine, startCol) {
344
432
  let value = firstChar;
345
433
  // Consume integer part
@@ -50,6 +50,7 @@ const types_1 = require("../events/types");
50
50
  const BUILTINS = {
51
51
  say: ([msg]) => `say ${msg}`,
52
52
  tell: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
53
+ tellraw: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
53
54
  title: ([sel, msg]) => `title ${sel} title {"text":"${msg}"}`,
54
55
  actionbar: ([sel, msg]) => `title ${sel} actionbar {"text":"${msg}"}`,
55
56
  subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
@@ -1029,6 +1030,7 @@ class Lowering {
1029
1030
  // MC names (#health, #red) treated as string constants
1030
1031
  return { kind: 'const', value: 0 }; // Handled inline in exprToString
1031
1032
  case 'str_interp':
1033
+ case 'f_string':
1032
1034
  // Interpolated strings are handled inline in message builtins.
1033
1035
  return { kind: 'const', value: 0 };
1034
1036
  case 'range_lit':
@@ -1919,7 +1921,7 @@ class Lowering {
1919
1921
  return null;
1920
1922
  }
1921
1923
  const messageExpr = args[messageArgIndex];
1922
- if (!messageExpr || messageExpr.kind !== 'str_interp') {
1924
+ if (!messageExpr || (messageExpr.kind !== 'str_interp' && messageExpr.kind !== 'f_string')) {
1923
1925
  return null;
1924
1926
  }
1925
1927
  const json = this.buildRichTextJson(messageExpr);
@@ -1928,6 +1930,7 @@ class Lowering {
1928
1930
  case 'announce':
1929
1931
  return `tellraw @a ${json}`;
1930
1932
  case 'tell':
1933
+ case 'tellraw':
1931
1934
  return `tellraw ${this.exprToString(args[0])} ${json}`;
1932
1935
  case 'title':
1933
1936
  return `title ${this.exprToString(args[0])} title ${json}`;
@@ -1945,6 +1948,7 @@ class Lowering {
1945
1948
  case 'announce':
1946
1949
  return 0;
1947
1950
  case 'tell':
1951
+ case 'tellraw':
1948
1952
  case 'title':
1949
1953
  case 'actionbar':
1950
1954
  case 'subtitle':
@@ -1955,6 +1959,18 @@ class Lowering {
1955
1959
  }
1956
1960
  buildRichTextJson(expr) {
1957
1961
  const components = [''];
1962
+ if (expr.kind === 'f_string') {
1963
+ for (const part of expr.parts) {
1964
+ if (part.kind === 'text') {
1965
+ if (part.value.length > 0) {
1966
+ components.push({ text: part.value });
1967
+ }
1968
+ continue;
1969
+ }
1970
+ this.appendRichTextExpr(components, part.expr);
1971
+ }
1972
+ return JSON.stringify(components);
1973
+ }
1958
1974
  for (const part of expr.parts) {
1959
1975
  if (typeof part === 'string') {
1960
1976
  if (part.length > 0) {
@@ -1998,6 +2014,19 @@ class Lowering {
1998
2014
  }
1999
2015
  return;
2000
2016
  }
2017
+ if (expr.kind === 'f_string') {
2018
+ for (const part of expr.parts) {
2019
+ if (part.kind === 'text') {
2020
+ if (part.value.length > 0) {
2021
+ components.push({ text: part.value });
2022
+ }
2023
+ }
2024
+ else {
2025
+ this.appendRichTextExpr(components, part.expr);
2026
+ }
2027
+ }
2028
+ return;
2029
+ }
2001
2030
  if (expr.kind === 'bool_lit') {
2002
2031
  components.push({ text: expr.value ? 'true' : 'false' });
2003
2032
  return;
@@ -2031,6 +2060,10 @@ class Lowering {
2031
2060
  return `${expr.value}L`;
2032
2061
  case 'double_lit':
2033
2062
  return `${expr.value}d`;
2063
+ case 'rel_coord':
2064
+ return expr.value; // ~ or ~5 or ~-3 - output as-is for MC commands
2065
+ case 'local_coord':
2066
+ return expr.value; // ^ or ^5 or ^-3 - output as-is for MC commands
2034
2067
  case 'bool_lit':
2035
2068
  return expr.value ? '1' : '0';
2036
2069
  case 'str_lit':
@@ -2038,6 +2071,7 @@ class Lowering {
2038
2071
  case 'mc_name':
2039
2072
  return expr.value; // #health → "health" (no quotes, used as bare MC name)
2040
2073
  case 'str_interp':
2074
+ case 'f_string':
2041
2075
  return this.buildRichTextJson(expr);
2042
2076
  case 'blockpos':
2043
2077
  return emitBlockPos(expr);
@@ -2328,6 +2362,8 @@ class Lowering {
2328
2362
  return { kind: 'named', name: 'bool' };
2329
2363
  if (expr.kind === 'str_lit' || expr.kind === 'str_interp')
2330
2364
  return { kind: 'named', name: 'string' };
2365
+ if (expr.kind === 'f_string')
2366
+ return { kind: 'named', name: 'format_string' };
2331
2367
  if (expr.kind === 'blockpos')
2332
2368
  return { kind: 'named', name: 'BlockPos' };
2333
2369
  if (expr.kind === 'ident') {
@@ -0,0 +1,23 @@
1
+ import type { Program } from '../ast/types';
2
+ export declare class DeadCodeEliminator {
3
+ private readonly functionMap;
4
+ private readonly reachableFunctions;
5
+ private readonly usedConstants;
6
+ private readonly localReads;
7
+ private readonly localDeclIds;
8
+ private localIdCounter;
9
+ eliminate(program: Program): Program;
10
+ private findEntryPoints;
11
+ private markReachable;
12
+ private collectFunctionRefs;
13
+ private collectStmtRefs;
14
+ private collectStmtRef;
15
+ private collectNestedStmtRefs;
16
+ private collectExprRefs;
17
+ private resolveLocal;
18
+ private transformFunction;
19
+ private transformBlock;
20
+ private transformStmt;
21
+ private transformExpr;
22
+ }
23
+ export declare function eliminateDeadCode(program: Program): Program;