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
@@ -34,7 +34,10 @@ export type TokenKind =
34
34
  | 'long_lit' // 1000L
35
35
  | 'double_lit' // 3.14d
36
36
  | 'string_lit' // "hello"
37
+ | 'f_string' // f"hello {name}"
37
38
  | 'range_lit' // ..5 1.. 1..10
39
+ | 'rel_coord' // ~ ~5 ~-3 (relative coordinate)
40
+ | 'local_coord' // ^ ^5 ^-3 (local/facing coordinate)
38
41
  // Operators
39
42
  | '+' | '-' | '*' | '/' | '%'
40
43
  | '~' | '^'
@@ -275,8 +278,52 @@ export class Lexer {
275
278
  return
276
279
  }
277
280
 
281
+ // Relative coordinate: ~ or ~5 or ~-3 or ~0.5
282
+ if (char === '~') {
283
+ let value = '~'
284
+ // Check for optional sign
285
+ if (this.peek() === '-' || this.peek() === '+') {
286
+ value += this.advance()
287
+ }
288
+ // Check for number
289
+ while (/[0-9]/.test(this.peek())) {
290
+ value += this.advance()
291
+ }
292
+ // Check for decimal part
293
+ if (this.peek() === '.' && /[0-9]/.test(this.peek(1))) {
294
+ value += this.advance() // .
295
+ while (/[0-9]/.test(this.peek())) {
296
+ value += this.advance()
297
+ }
298
+ }
299
+ this.addToken('rel_coord', value, startLine, startCol)
300
+ return
301
+ }
302
+
303
+ // Local coordinate: ^ or ^5 or ^-3 or ^0.5
304
+ if (char === '^') {
305
+ let value = '^'
306
+ // Check for optional sign
307
+ if (this.peek() === '-' || this.peek() === '+') {
308
+ value += this.advance()
309
+ }
310
+ // Check for number
311
+ while (/[0-9]/.test(this.peek())) {
312
+ value += this.advance()
313
+ }
314
+ // Check for decimal part
315
+ if (this.peek() === '.' && /[0-9]/.test(this.peek(1))) {
316
+ value += this.advance() // .
317
+ while (/[0-9]/.test(this.peek())) {
318
+ value += this.advance()
319
+ }
320
+ }
321
+ this.addToken('local_coord', value, startLine, startCol)
322
+ return
323
+ }
324
+
278
325
  // Single-character operators and delimiters
279
- const singleChar: TokenKind[] = ['+', '-', '*', '/', '%', '~', '^', '<', '>', '!', '=',
326
+ const singleChar: TokenKind[] = ['+', '-', '*', '/', '%', '<', '>', '!', '=',
280
327
  '{', '}', '(', ')', '[', ']', ',', ';', ':', '.']
281
328
  if (singleChar.includes(char as TokenKind)) {
282
329
  this.addToken(char as TokenKind, char, startLine, startCol)
@@ -289,6 +336,13 @@ export class Lexer {
289
336
  return
290
337
  }
291
338
 
339
+ // f-string literal
340
+ if (char === 'f' && this.peek() === '"') {
341
+ this.advance()
342
+ this.scanFString(startLine, startCol)
343
+ return
344
+ }
345
+
292
346
  // String literal
293
347
  if (char === '"') {
294
348
  this.scanString(startLine, startCol)
@@ -432,6 +486,53 @@ export class Lexer {
432
486
  this.addToken('string_lit', value, startLine, startCol)
433
487
  }
434
488
 
489
+ private scanFString(startLine: number, startCol: number): void {
490
+ let value = ''
491
+ let interpolationDepth = 0
492
+ let interpolationString = false
493
+
494
+ while (!this.isAtEnd()) {
495
+ if (interpolationDepth === 0 && this.peek() === '"') {
496
+ break
497
+ }
498
+
499
+ if (this.peek() === '\\' && this.peek(1) === '"') {
500
+ this.advance()
501
+ value += this.advance()
502
+ continue
503
+ }
504
+
505
+ if (interpolationDepth === 0 && this.peek() === '{') {
506
+ value += this.advance()
507
+ interpolationDepth = 1
508
+ interpolationString = false
509
+ continue
510
+ }
511
+
512
+ const char = this.advance()
513
+ value += char
514
+
515
+ if (interpolationDepth === 0) continue
516
+
517
+ if (char === '"' && this.source[this.pos - 2] !== '\\') {
518
+ interpolationString = !interpolationString
519
+ continue
520
+ }
521
+
522
+ if (interpolationString) continue
523
+
524
+ if (char === '{') interpolationDepth++
525
+ if (char === '}') interpolationDepth--
526
+ }
527
+
528
+ if (this.isAtEnd()) {
529
+ this.error('Unterminated f-string', startLine, startCol)
530
+ }
531
+
532
+ this.advance() // closing quote
533
+ this.addToken('f_string', value, startLine, startCol)
534
+ }
535
+
435
536
  private scanNumber(firstChar: string, startLine: number, startCol: number): void {
436
537
  let value = firstChar
437
538
 
@@ -25,6 +25,7 @@ import { EVENT_TYPES, getEventParamSpecs, isEventTypeName } from '../events/type
25
25
  const BUILTINS: Record<string, (args: string[]) => string | null> = {
26
26
  say: ([msg]) => `say ${msg}`,
27
27
  tell: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
28
+ tellraw: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
28
29
  title: ([sel, msg]) => `title ${sel} title {"text":"${msg}"}`,
29
30
  actionbar: ([sel, msg]) => `title ${sel} actionbar {"text":"${msg}"}`,
30
31
  subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
@@ -1234,6 +1235,7 @@ export class Lowering {
1234
1235
  return { kind: 'const', value: 0 } // Handled inline in exprToString
1235
1236
 
1236
1237
  case 'str_interp':
1238
+ case 'f_string':
1237
1239
  // Interpolated strings are handled inline in message builtins.
1238
1240
  return { kind: 'const', value: 0 }
1239
1241
 
@@ -2265,7 +2267,7 @@ export class Lowering {
2265
2267
  }
2266
2268
 
2267
2269
  const messageExpr = args[messageArgIndex]
2268
- if (!messageExpr || messageExpr.kind !== 'str_interp') {
2270
+ if (!messageExpr || (messageExpr.kind !== 'str_interp' && messageExpr.kind !== 'f_string')) {
2269
2271
  return null
2270
2272
  }
2271
2273
 
@@ -2276,6 +2278,7 @@ export class Lowering {
2276
2278
  case 'announce':
2277
2279
  return `tellraw @a ${json}`
2278
2280
  case 'tell':
2281
+ case 'tellraw':
2279
2282
  return `tellraw ${this.exprToString(args[0])} ${json}`
2280
2283
  case 'title':
2281
2284
  return `title ${this.exprToString(args[0])} title ${json}`
@@ -2294,6 +2297,7 @@ export class Lowering {
2294
2297
  case 'announce':
2295
2298
  return 0
2296
2299
  case 'tell':
2300
+ case 'tellraw':
2297
2301
  case 'title':
2298
2302
  case 'actionbar':
2299
2303
  case 'subtitle':
@@ -2303,9 +2307,22 @@ export class Lowering {
2303
2307
  }
2304
2308
  }
2305
2309
 
2306
- private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' }>): string {
2310
+ private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' | 'f_string' }>): string {
2307
2311
  const components: Array<string | Record<string, unknown>> = ['']
2308
2312
 
2313
+ if (expr.kind === 'f_string') {
2314
+ for (const part of expr.parts) {
2315
+ if (part.kind === 'text') {
2316
+ if (part.value.length > 0) {
2317
+ components.push({ text: part.value })
2318
+ }
2319
+ continue
2320
+ }
2321
+ this.appendRichTextExpr(components, part.expr)
2322
+ }
2323
+ return JSON.stringify(components)
2324
+ }
2325
+
2309
2326
  for (const part of expr.parts) {
2310
2327
  if (typeof part === 'string') {
2311
2328
  if (part.length > 0) {
@@ -2354,6 +2371,19 @@ export class Lowering {
2354
2371
  return
2355
2372
  }
2356
2373
 
2374
+ if (expr.kind === 'f_string') {
2375
+ for (const part of expr.parts) {
2376
+ if (part.kind === 'text') {
2377
+ if (part.value.length > 0) {
2378
+ components.push({ text: part.value })
2379
+ }
2380
+ } else {
2381
+ this.appendRichTextExpr(components, part.expr)
2382
+ }
2383
+ }
2384
+ return
2385
+ }
2386
+
2357
2387
  if (expr.kind === 'bool_lit') {
2358
2388
  components.push({ text: expr.value ? 'true' : 'false' })
2359
2389
  return
@@ -2392,6 +2422,10 @@ export class Lowering {
2392
2422
  return `${expr.value}L`
2393
2423
  case 'double_lit':
2394
2424
  return `${expr.value}d`
2425
+ case 'rel_coord':
2426
+ return expr.value // ~ or ~5 or ~-3 - output as-is for MC commands
2427
+ case 'local_coord':
2428
+ return expr.value // ^ or ^5 or ^-3 - output as-is for MC commands
2395
2429
  case 'bool_lit':
2396
2430
  return expr.value ? '1' : '0'
2397
2431
  case 'str_lit':
@@ -2399,6 +2433,7 @@ export class Lowering {
2399
2433
  case 'mc_name':
2400
2434
  return expr.value // #health → "health" (no quotes, used as bare MC name)
2401
2435
  case 'str_interp':
2436
+ case 'f_string':
2402
2437
  return this.buildRichTextJson(expr)
2403
2438
  case 'blockpos':
2404
2439
  return emitBlockPos(expr)
@@ -2732,6 +2767,7 @@ export class Lowering {
2732
2767
  if (expr.kind === 'float_lit') return { kind: 'named', name: 'float' }
2733
2768
  if (expr.kind === 'bool_lit') return { kind: 'named', name: 'bool' }
2734
2769
  if (expr.kind === 'str_lit' || expr.kind === 'str_interp') return { kind: 'named', name: 'string' }
2770
+ if (expr.kind === 'f_string') return { kind: 'named', name: 'format_string' }
2735
2771
  if (expr.kind === 'blockpos') return { kind: 'named', name: 'BlockPos' }
2736
2772
  if (expr.kind === 'ident') {
2737
2773
  const constValue = this.constValues.get(expr.name)