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
@@ -965,6 +965,18 @@ export class Parser {
965
965
  return this.withLoc({ kind: 'float_lit', value: parseFloat(token.value) }, token)
966
966
  }
967
967
 
968
+ // Relative coordinate: ~ ~5 ~-3 ~0.5
969
+ if (token.kind === 'rel_coord') {
970
+ this.advance()
971
+ return this.withLoc({ kind: 'rel_coord', value: token.value }, token)
972
+ }
973
+
974
+ // Local coordinate: ^ ^5 ^-3 ^0.5
975
+ if (token.kind === 'local_coord') {
976
+ this.advance()
977
+ return this.withLoc({ kind: 'local_coord', value: token.value }, token)
978
+ }
979
+
968
980
  // NBT suffix literals
969
981
  if (token.kind === 'byte_lit') {
970
982
  this.advance()
@@ -989,6 +1001,11 @@ export class Parser {
989
1001
  return this.parseStringExpr(token)
990
1002
  }
991
1003
 
1004
+ if (token.kind === 'f_string') {
1005
+ this.advance()
1006
+ return this.parseFStringExpr(token)
1007
+ }
1008
+
992
1009
  // MC name literal: #health → mc_name node (value = "health", without #)
993
1010
  if (token.kind === 'mc_name') {
994
1011
  this.advance()
@@ -1169,6 +1186,67 @@ export class Parser {
1169
1186
  return this.withLoc({ kind: 'str_interp', parts }, token)
1170
1187
  }
1171
1188
 
1189
+ private parseFStringExpr(token: Token): Expr {
1190
+ const parts: Array<{ kind: 'text'; value: string } | { kind: 'expr'; expr: Expr }> = []
1191
+ let current = ''
1192
+ let index = 0
1193
+
1194
+ while (index < token.value.length) {
1195
+ if (token.value[index] === '{') {
1196
+ if (current) {
1197
+ parts.push({ kind: 'text', value: current })
1198
+ current = ''
1199
+ }
1200
+
1201
+ index++
1202
+ let depth = 1
1203
+ let exprSource = ''
1204
+ let inString = false
1205
+
1206
+ while (index < token.value.length && depth > 0) {
1207
+ const char = token.value[index]
1208
+
1209
+ if (char === '"' && token.value[index - 1] !== '\\') {
1210
+ inString = !inString
1211
+ }
1212
+
1213
+ if (!inString) {
1214
+ if (char === '{') {
1215
+ depth++
1216
+ } else if (char === '}') {
1217
+ depth--
1218
+ if (depth === 0) {
1219
+ index++
1220
+ break
1221
+ }
1222
+ }
1223
+ }
1224
+
1225
+ if (depth > 0) {
1226
+ exprSource += char
1227
+ }
1228
+ index++
1229
+ }
1230
+
1231
+ if (depth !== 0) {
1232
+ this.error('Unterminated f-string interpolation')
1233
+ }
1234
+
1235
+ parts.push({ kind: 'expr', expr: this.parseEmbeddedExpr(exprSource) })
1236
+ continue
1237
+ }
1238
+
1239
+ current += token.value[index]
1240
+ index++
1241
+ }
1242
+
1243
+ if (current) {
1244
+ parts.push({ kind: 'text', value: current })
1245
+ }
1246
+
1247
+ return this.withLoc({ kind: 'f_string', parts }, token)
1248
+ }
1249
+
1172
1250
  private parseEmbeddedExpr(source: string): Expr {
1173
1251
  const tokens = new Lexer(source, this.filePath).tokenize()
1174
1252
  const parser = new Parser(tokens, source, this.filePath)
@@ -1343,20 +1421,11 @@ export class Parser {
1343
1421
  return this.peek(offset + 1).kind === 'int_lit' ? 2 : 0
1344
1422
  }
1345
1423
 
1346
- if (token.kind !== '~' && token.kind !== '^') {
1347
- return 0
1348
- }
1349
-
1350
- const next = this.peek(offset + 1)
1351
- if (next.kind === ',' || next.kind === ')') {
1424
+ // rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) are single tokens now
1425
+ if (token.kind === 'rel_coord' || token.kind === 'local_coord') {
1352
1426
  return 1
1353
1427
  }
1354
- if (next.kind === 'int_lit') {
1355
- return 2
1356
- }
1357
- if (next.kind === '-' && this.peek(offset + 2).kind === 'int_lit') {
1358
- return 3
1359
- }
1428
+
1360
1429
  return 0
1361
1430
  }
1362
1431
 
@@ -1374,17 +1443,28 @@ export class Parser {
1374
1443
  private parseCoordComponent(): CoordComponent {
1375
1444
  const token = this.peek()
1376
1445
 
1377
- if (token.kind === '~' || token.kind === '^') {
1446
+ // Handle rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) tokens
1447
+ if (token.kind === 'rel_coord') {
1378
1448
  this.advance()
1379
- const offset = this.parseSignedCoordOffset()
1380
- return token.kind === '~'
1381
- ? { kind: 'relative', offset }
1382
- : { kind: 'local', offset }
1449
+ // Parse the offset from the token value (e.g., "~5" -> 5, "~" -> 0, "~-3" -> -3)
1450
+ const offset = this.parseCoordOffsetFromValue(token.value.slice(1))
1451
+ return { kind: 'relative', offset }
1452
+ }
1453
+
1454
+ if (token.kind === 'local_coord') {
1455
+ this.advance()
1456
+ const offset = this.parseCoordOffsetFromValue(token.value.slice(1))
1457
+ return { kind: 'local', offset }
1383
1458
  }
1384
1459
 
1385
1460
  return { kind: 'absolute', value: this.parseSignedCoordOffset(true) }
1386
1461
  }
1387
1462
 
1463
+ private parseCoordOffsetFromValue(value: string): number {
1464
+ if (value === '' || value === undefined) return 0
1465
+ return parseFloat(value)
1466
+ }
1467
+
1388
1468
  private parseSignedCoordOffset(requireValue = false): number {
1389
1469
  let sign = 1
1390
1470
  if (this.match('-')) {
@@ -73,6 +73,8 @@ const MC_TYPE_TO_ENTITY: Record<string, EntityTypeName> = {
73
73
 
74
74
  const VOID_TYPE: TypeNode = { kind: 'named', name: 'void' }
75
75
  const INT_TYPE: TypeNode = { kind: 'named', name: 'int' }
76
+ const STRING_TYPE: TypeNode = { kind: 'named', name: 'string' }
77
+ const FORMAT_STRING_TYPE: TypeNode = { kind: 'named', name: 'format_string' }
76
78
 
77
79
  const BUILTIN_SIGNATURES: Record<string, BuiltinSignature> = {
78
80
  setTimeout: {
@@ -106,6 +108,16 @@ export class TypeChecker {
106
108
  // Stack for tracking @s type in different contexts
107
109
  private selfTypeStack: EntityTypeName[] = ['entity']
108
110
 
111
+ private readonly richTextBuiltins = new Map<string, { messageIndex: number }>([
112
+ ['say', { messageIndex: 0 }],
113
+ ['announce', { messageIndex: 0 }],
114
+ ['tell', { messageIndex: 1 }],
115
+ ['tellraw', { messageIndex: 1 }],
116
+ ['title', { messageIndex: 1 }],
117
+ ['actionbar', { messageIndex: 1 }],
118
+ ['subtitle', { messageIndex: 1 }],
119
+ ])
120
+
109
121
  constructor(source?: string, filePath?: string) {
110
122
  this.collector = new DiagnosticCollector(source, filePath)
111
123
  }
@@ -510,6 +522,24 @@ export class TypeChecker {
510
522
  }
511
523
  break
512
524
 
525
+ case 'f_string':
526
+ for (const part of expr.parts) {
527
+ if (part.kind !== 'expr') {
528
+ continue
529
+ }
530
+ this.checkExpr(part.expr)
531
+ const partType = this.inferType(part.expr)
532
+ if (
533
+ !(partType.kind === 'named' && (partType.name === 'int' || partType.name === 'string' || partType.name === 'format_string'))
534
+ ) {
535
+ this.report(
536
+ `f-string placeholder must be int or string, got ${this.typeToString(partType)}`,
537
+ part.expr
538
+ )
539
+ }
540
+ }
541
+ break
542
+
513
543
  case 'array_lit':
514
544
  for (const elem of expr.elements) {
515
545
  this.checkExpr(elem)
@@ -544,6 +574,12 @@ export class TypeChecker {
544
574
  this.checkTpCall(expr)
545
575
  }
546
576
 
577
+ const richTextBuiltin = this.richTextBuiltins.get(expr.fn)
578
+ if (richTextBuiltin) {
579
+ this.checkRichTextBuiltinCall(expr, richTextBuiltin.messageIndex)
580
+ return
581
+ }
582
+
547
583
  const builtin = BUILTIN_SIGNATURES[expr.fn]
548
584
  if (builtin) {
549
585
  this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr)
@@ -602,6 +638,28 @@ export class TypeChecker {
602
638
  // Built-in functions are not checked for arg count
603
639
  }
604
640
 
641
+ private checkRichTextBuiltinCall(expr: Extract<Expr, { kind: 'call' }>, messageIndex: number): void {
642
+ for (let i = 0; i < expr.args.length; i++) {
643
+ this.checkExpr(expr.args[i], i === messageIndex ? undefined : STRING_TYPE)
644
+ }
645
+
646
+ const message = expr.args[messageIndex]
647
+ if (!message) {
648
+ return
649
+ }
650
+
651
+ const messageType = this.inferType(message)
652
+ if (
653
+ messageType.kind !== 'named' ||
654
+ (messageType.name !== 'string' && messageType.name !== 'format_string')
655
+ ) {
656
+ this.report(
657
+ `Argument ${messageIndex + 1} of '${expr.fn}' expects string or format_string, got ${this.typeToString(messageType)}`,
658
+ message
659
+ )
660
+ }
661
+ }
662
+
605
663
  private checkInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): void {
606
664
  this.checkExpr(expr.callee)
607
665
  const calleeType = this.inferType(expr.callee)
@@ -838,6 +896,13 @@ export class TypeChecker {
838
896
  }
839
897
  }
840
898
  return { kind: 'named', name: 'string' }
899
+ case 'f_string':
900
+ for (const part of expr.parts) {
901
+ if (part.kind === 'expr') {
902
+ this.checkExpr(part.expr)
903
+ }
904
+ }
905
+ return FORMAT_STRING_TYPE
841
906
  case 'blockpos':
842
907
  return { kind: 'named', name: 'BlockPos' }
843
908
  case 'ident':