redscript-mc 1.0.0 → 1.1.0

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 (106) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
  4. package/CHANGELOG.md +58 -0
  5. package/CONTRIBUTING.md +140 -0
  6. package/README.md +28 -19
  7. package/README.zh.md +28 -19
  8. package/dist/__tests__/cli.test.js +10 -10
  9. package/dist/__tests__/codegen.test.js +1 -1
  10. package/dist/__tests__/diagnostics.test.js +5 -5
  11. package/dist/__tests__/e2e.test.js +146 -5
  12. package/dist/__tests__/formatter.test.d.ts +1 -0
  13. package/dist/__tests__/formatter.test.js +40 -0
  14. package/dist/__tests__/lowering.test.js +36 -3
  15. package/dist/__tests__/mc-integration.test.js +255 -10
  16. package/dist/__tests__/mc-syntax.test.js +3 -3
  17. package/dist/__tests__/nbt.test.js +2 -2
  18. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  19. package/dist/__tests__/runtime.test.js +1 -1
  20. package/dist/ast/types.d.ts +21 -3
  21. package/dist/cli.js +25 -7
  22. package/dist/codegen/mcfunction/index.d.ts +1 -1
  23. package/dist/codegen/mcfunction/index.js +8 -2
  24. package/dist/codegen/structure/index.js +7 -1
  25. package/dist/formatter/index.d.ts +1 -0
  26. package/dist/formatter/index.js +26 -0
  27. package/dist/ir/builder.d.ts +2 -1
  28. package/dist/ir/types.d.ts +7 -2
  29. package/dist/ir/types.js +1 -1
  30. package/dist/lowering/index.d.ts +2 -0
  31. package/dist/lowering/index.js +183 -8
  32. package/dist/mc-test/runner.d.ts +2 -2
  33. package/dist/mc-test/runner.js +3 -3
  34. package/dist/mc-test/setup.js +2 -2
  35. package/dist/parser/index.d.ts +2 -0
  36. package/dist/parser/index.js +75 -7
  37. package/docs/COMPILATION_STATS.md +24 -24
  38. package/docs/IMPLEMENTATION_GUIDE.md +1 -1
  39. package/docs/STRUCTURE_TARGET.md +1 -1
  40. package/editors/vscode/.vscodeignore +1 -0
  41. package/editors/vscode/icons/mcrs.svg +7 -0
  42. package/editors/vscode/icons/redscript-icons.json +10 -0
  43. package/editors/vscode/out/extension.js +152 -9
  44. package/editors/vscode/package.json +10 -3
  45. package/editors/vscode/src/hover.ts +55 -2
  46. package/editors/vscode/src/symbols.ts +42 -0
  47. package/package.json +1 -1
  48. package/src/__tests__/cli.test.ts +10 -10
  49. package/src/__tests__/codegen.test.ts +1 -1
  50. package/src/__tests__/diagnostics.test.ts +5 -5
  51. package/src/__tests__/e2e.test.ts +134 -5
  52. package/src/__tests__/lowering.test.ts +48 -3
  53. package/src/__tests__/mc-integration.test.ts +285 -10
  54. package/src/__tests__/mc-syntax.test.ts +3 -3
  55. package/src/__tests__/nbt.test.ts +2 -2
  56. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  57. package/src/__tests__/runtime.test.ts +1 -1
  58. package/src/ast/types.ts +20 -3
  59. package/src/cli.ts +10 -10
  60. package/src/codegen/mcfunction/index.ts +9 -2
  61. package/src/codegen/structure/index.ts +8 -1
  62. package/src/examples/capture_the_flag.mcrs +208 -0
  63. package/src/examples/{counter.rs → counter.mcrs} +1 -1
  64. package/src/examples/hunger_games.mcrs +301 -0
  65. package/src/examples/new_features_demo.mcrs +193 -0
  66. package/src/examples/parkour_race.mcrs +233 -0
  67. package/src/examples/rpg.mcrs +13 -0
  68. package/src/examples/{shop.rs → shop.mcrs} +1 -1
  69. package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
  70. package/src/examples/{turret.rs → turret.mcrs} +1 -1
  71. package/src/examples/zombie_survival.mcrs +314 -0
  72. package/src/ir/builder.ts +3 -1
  73. package/src/ir/types.ts +8 -2
  74. package/src/lowering/index.ts +156 -8
  75. package/src/mc-test/runner.ts +3 -3
  76. package/src/mc-test/setup.ts +2 -2
  77. package/src/parser/index.ts +81 -8
  78. package/src/stdlib/README.md +155 -147
  79. package/src/stdlib/bossbar.mcrs +68 -0
  80. package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
  81. package/src/stdlib/effects.mcrs +64 -0
  82. package/src/stdlib/interactions.mcrs +195 -0
  83. package/src/stdlib/inventory.mcrs +38 -0
  84. package/src/stdlib/mobs.mcrs +99 -0
  85. package/src/stdlib/particles.mcrs +52 -0
  86. package/src/stdlib/sets.mcrs +20 -0
  87. package/src/stdlib/spawn.mcrs +41 -0
  88. package/src/stdlib/teams.mcrs +68 -0
  89. package/src/stdlib/world.mcrs +92 -0
  90. package/src/examples/rpg.rs +0 -13
  91. package/src/stdlib/mobs.rs +0 -99
  92. /package/src/examples/{arena.rs → arena.mcrs} +0 -0
  93. /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
  94. /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
  95. /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
  96. /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
  97. /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
  98. /package/src/stdlib/{math.rs → math.mcrs} +0 -0
  99. /package/src/stdlib/{player.rs → player.mcrs} +0 -0
  100. /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
  101. /package/src/stdlib/{timer.rs → timer.mcrs} +0 -0
  102. /package/src/templates/{combat.rs → combat.mcrs} +0 -0
  103. /package/src/templates/{economy.rs → economy.mcrs} +0 -0
  104. /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
  105. /package/src/templates/{quest.rs → quest.mcrs} +0 -0
  106. /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
@@ -10,9 +10,10 @@ import { buildModule } from '../ir/builder'
10
10
  import type { IRFunction, IRModule, Operand, BinOp, CmpOp } from '../ir/types'
11
11
  import { DiagnosticError } from '../diagnostics'
12
12
  import type {
13
- Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, Program, RangeExpr, Span, Stmt,
13
+ Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, Program, RangeExpr, Span, Stmt,
14
14
  StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent
15
15
  } from '../ast/types'
16
+ import type { GlobalVar } from '../ir/types'
16
17
 
17
18
  // ---------------------------------------------------------------------------
18
19
  // Builtin Functions
@@ -28,7 +29,8 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
28
29
  announce: ([msg]) => `tellraw @a {"text":"${msg}"}`,
29
30
  give: ([sel, item, count, nbt]) => nbt ? `give ${sel} ${item}${nbt} ${count ?? '1'}` : `give ${sel} ${item} ${count ?? '1'}`,
30
31
  kill: ([sel]) => `kill ${sel ?? '@s'}`,
31
- effect: ([sel, eff, dur, amp]) => `effect give ${sel} ${eff} ${dur ?? '30'} ${amp ?? '0'}`,
32
+ effect: ([sel, eff, dur, amp]) => `effect give ${sel} ${eff} ${dur ?? '30'} ${amp ?? '0'}`,
33
+ effect_clear: ([sel, eff]) => eff ? `effect clear ${sel} ${eff}` : `effect clear ${sel}`,
32
34
  summon: ([type, x, y, z, nbt]) => {
33
35
  const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
34
36
  return nbt ? `summon ${type} ${pos} ${nbt}` : `summon ${type} ${pos}`
@@ -80,6 +82,7 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
80
82
  team_leave: () => null, // Special handling
81
83
  team_option: () => null, // Special handling
82
84
  data_get: () => null, // Special handling (returns value from NBT)
85
+ data_merge: () => null, // Special handling (merge NBT)
83
86
  set_new: () => null, // Special handling (returns set ID)
84
87
  set_add: () => null, // Special handling
85
88
  set_contains: () => null, // Special handling (returns 1/0)
@@ -154,7 +157,8 @@ function emitBlockPos(pos: BlockPosExpr): string {
154
157
  export class Lowering {
155
158
  private namespace: string
156
159
  private functions: IRFunction[] = []
157
- private globals: string[] = []
160
+ private globals: GlobalVar[] = []
161
+ private globalNames: Map<string, { mutable: boolean }> = new Map()
158
162
  private fnDecls: Map<string, FnDecl> = new Map()
159
163
  private specializedFunctions: Map<string, string> = new Map()
160
164
  private currentFn: string = ''
@@ -185,6 +189,7 @@ export class Lowering {
185
189
 
186
190
  constructor(namespace: string) {
187
191
  this.namespace = namespace
192
+ LoweringBuilder.resetTempCounter()
188
193
  }
189
194
 
190
195
  lower(program: Program): IRModule {
@@ -212,6 +217,14 @@ export class Lowering {
212
217
  this.varTypes.set(constDecl.name, this.normalizeType(constDecl.type))
213
218
  }
214
219
 
220
+ // Process global variable declarations (top-level let)
221
+ for (const g of program.globals ?? []) {
222
+ this.globalNames.set(g.name, { mutable: g.mutable })
223
+ this.varTypes.set(g.name, this.normalizeType(g.type))
224
+ const initValue = g.init.kind === 'int_lit' ? g.init.value : 0
225
+ this.globals.push({ name: `$${g.name}`, init: initValue })
226
+ }
227
+
215
228
  for (const fn of program.declarations) {
216
229
  this.fnDecls.set(fn.name, fn)
217
230
  this.functionDefaults.set(fn.name, fn.params.map(param => param.default))
@@ -323,6 +336,11 @@ export class Lowering {
323
336
  }
324
337
  }
325
338
 
339
+ // Check for @load decorator
340
+ if (fn.decorators.some(d => d.name === 'load')) {
341
+ irFn.isLoadInit = true
342
+ }
343
+
326
344
  // Handle tick rate counter if needed
327
345
  if (tickRate && tickRate > 1) {
328
346
  this.wrapWithTickRate(irFn, tickRate)
@@ -339,7 +357,7 @@ export class Lowering {
339
357
  private wrapWithTickRate(fn: IRFunction, rate: number): void {
340
358
  // Add tick counter logic to entry block
341
359
  const counterVar = `$__tick_${fn.name}`
342
- this.globals.push(counterVar)
360
+ this.globals.push({ name: counterVar, init: 0 })
343
361
 
344
362
  // Prepend counter logic to entry block
345
363
  const entry = fn.blocks[0]
@@ -443,6 +461,15 @@ export class Lowering {
443
461
  }
444
462
 
445
463
  private lowerLetStmt(stmt: Extract<Stmt, { kind: 'let' }>): void {
464
+ // Check for duplicate declaration of foreach binding
465
+ if (this.currentContext.binding === stmt.name) {
466
+ throw new DiagnosticError(
467
+ 'LoweringError',
468
+ `Cannot redeclare foreach binding '${stmt.name}'`,
469
+ stmt.span ?? { line: 0, col: 0 }
470
+ )
471
+ }
472
+
446
473
  const varName = `$${stmt.name}`
447
474
  this.varMap.set(stmt.name, varName)
448
475
 
@@ -495,6 +522,14 @@ export class Lowering {
495
522
  return
496
523
  }
497
524
 
525
+ // Handle set_new returning a set ID string
526
+ if (stmt.init.kind === 'call' && stmt.init.fn === 'set_new') {
527
+ const setId = `__set_${this.foreachCounter++}`
528
+ this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
529
+ this.stringValues.set(stmt.name, setId)
530
+ return
531
+ }
532
+
498
533
  // Handle spawn_object returning entity handle
499
534
  if (stmt.init.kind === 'call' && stmt.init.fn === 'spawn_object') {
500
535
  const value = this.lowerExpr(stmt.init)
@@ -933,10 +968,22 @@ export class Lowering {
933
968
  parts.push(`at ${this.selectorToString(sub.selector)}`)
934
969
  break
935
970
  case 'if_entity':
936
- parts.push(`if entity ${this.selectorToString(sub.selector)}`)
971
+ if (sub.selector) {
972
+ parts.push(`if entity ${this.selectorToString(sub.selector)}`)
973
+ } else if (sub.varName) {
974
+ // Variable with filters - substitute with @s and apply filters
975
+ const sel: EntitySelector = { kind: '@s', filters: sub.filters }
976
+ parts.push(`if entity ${this.selectorToString(sel)}`)
977
+ }
937
978
  break
938
979
  case 'unless_entity':
939
- parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
980
+ if (sub.selector) {
981
+ parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
982
+ } else if (sub.varName) {
983
+ // Variable with filters - substitute with @s and apply filters
984
+ const sel: EntitySelector = { kind: '@s', filters: sub.filters }
985
+ parts.push(`unless entity ${this.selectorToString(sel)}`)
986
+ }
940
987
  break
941
988
  case 'in':
942
989
  parts.push(`in ${sub.dimension}`)
@@ -1269,6 +1316,15 @@ export class Lowering {
1269
1316
  }
1270
1317
 
1271
1318
  private lowerAssignExpr(expr: Extract<Expr, { kind: 'assign' }>): Operand {
1319
+ // Check for const reassignment (both compile-time consts and immutable globals)
1320
+ if (this.constValues.has(expr.target)) {
1321
+ throw new DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 })
1322
+ }
1323
+ const globalInfo = this.globalNames.get(expr.target)
1324
+ if (globalInfo && !globalInfo.mutable) {
1325
+ throw new DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 })
1326
+ }
1327
+
1272
1328
  const blockPosValue = this.resolveBlockPosExpr(expr.value)
1273
1329
  if (blockPosValue) {
1274
1330
  this.blockPosVars.set(expr.target, blockPosValue)
@@ -1744,6 +1800,67 @@ export class Lowering {
1744
1800
  return { kind: 'var', name: dst }
1745
1801
  }
1746
1802
 
1803
+ // data_merge(target, nbt) — merge NBT into entity/block/storage
1804
+ // data_merge(@s, { Invisible: 1b, Silent: 1b })
1805
+ if (name === 'data_merge') {
1806
+ const target = args[0]
1807
+ const nbt = args[1]
1808
+ const nbtStr = this.exprToSnbt ? this.exprToSnbt(nbt) : this.exprToString(nbt)
1809
+
1810
+ // Check if target is a selector (entity) or string (block/storage)
1811
+ if (target.kind === 'selector') {
1812
+ const sel = this.exprToTargetString(target)
1813
+ this.builder.emitRaw(`data merge entity ${sel} ${nbtStr}`)
1814
+ } else {
1815
+ // Assume block position or storage
1816
+ const targetStr = this.exprToString(target)
1817
+ // If it looks like coordinates, use block; otherwise storage
1818
+ if (targetStr.match(/^~|^\d|^\^/)) {
1819
+ this.builder.emitRaw(`data merge block ${targetStr} ${nbtStr}`)
1820
+ } else {
1821
+ this.builder.emitRaw(`data merge storage ${targetStr} ${nbtStr}`)
1822
+ }
1823
+ }
1824
+ return { kind: 'const', value: 0 }
1825
+ }
1826
+
1827
+ // Set data structure operations — unique collections via NBT storage
1828
+ // set_new is primarily handled in lowerLetStmt for proper string tracking.
1829
+ // This fallback handles standalone set_new() calls without assignment.
1830
+ if (name === 'set_new') {
1831
+ const setId = `__set_${this.foreachCounter++}`
1832
+ this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
1833
+ return { kind: 'const', value: 0 }
1834
+ }
1835
+
1836
+ if (name === 'set_add') {
1837
+ const setId = this.exprToString(args[0])
1838
+ const value = this.exprToString(args[1])
1839
+ this.builder.emitRaw(`execute unless data storage rs:sets ${setId}[{value:${value}}] run data modify storage rs:sets ${setId} append value {value:${value}}`)
1840
+ return { kind: 'const', value: 0 }
1841
+ }
1842
+
1843
+ if (name === 'set_contains') {
1844
+ const dst = this.builder.freshTemp()
1845
+ const setId = this.exprToString(args[0])
1846
+ const value = this.exprToString(args[1])
1847
+ this.builder.emitRaw(`execute store result score ${dst} rs if data storage rs:sets ${setId}[{value:${value}}]`)
1848
+ return { kind: 'var', name: dst }
1849
+ }
1850
+
1851
+ if (name === 'set_remove') {
1852
+ const setId = this.exprToString(args[0])
1853
+ const value = this.exprToString(args[1])
1854
+ this.builder.emitRaw(`data remove storage rs:sets ${setId}[{value:${value}}]`)
1855
+ return { kind: 'const', value: 0 }
1856
+ }
1857
+
1858
+ if (name === 'set_clear') {
1859
+ const setId = this.exprToString(args[0])
1860
+ this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
1861
+ return { kind: 'const', value: 0 }
1862
+ }
1863
+
1747
1864
  const coordCommand = this.lowerCoordinateBuiltin(name, args)
1748
1865
  if (coordCommand) {
1749
1866
  this.builder.emitRaw(coordCommand)
@@ -1943,6 +2060,17 @@ export class Lowering {
1943
2060
  }
1944
2061
  case 'selector':
1945
2062
  return this.selectorToString(expr.sel)
2063
+ case 'unary':
2064
+ // Handle unary minus on literals directly
2065
+ if (expr.op === '-' && expr.operand.kind === 'int_lit') {
2066
+ return (-expr.operand.value).toString()
2067
+ }
2068
+ if (expr.op === '-' && expr.operand.kind === 'float_lit') {
2069
+ return Math.trunc(-expr.operand.value).toString()
2070
+ }
2071
+ // Fall through to default for complex cases
2072
+ const unaryOp = this.lowerExpr(expr)
2073
+ return this.operandToVar(unaryOp)
1946
2074
  default:
1947
2075
  // Complex expression - lower and return var name
1948
2076
  const op = this.lowerExpr(expr)
@@ -2050,6 +2178,14 @@ export class Lowering {
2050
2178
  return null
2051
2179
  }
2052
2180
 
2181
+ if (name === 'summon') {
2182
+ if (args.length >= 2 && pos1) {
2183
+ const nbt = args[2] ? ` ${this.exprToString(args[2])}` : ''
2184
+ return `summon ${this.exprToString(args[0])} ${emitBlockPos(pos1)}${nbt}`
2185
+ }
2186
+ return null
2187
+ }
2188
+
2053
2189
  return null
2054
2190
  }
2055
2191
 
@@ -2294,6 +2430,13 @@ export class Lowering {
2294
2430
  }
2295
2431
  if (filters.nbt) parts.push(`nbt=${filters.nbt}`)
2296
2432
  if (filters.gamemode) parts.push(`gamemode=${filters.gamemode}`)
2433
+ // Position filters
2434
+ if (filters.x) parts.push(`x=${this.rangeToString(filters.x)}`)
2435
+ if (filters.y) parts.push(`y=${this.rangeToString(filters.y)}`)
2436
+ if (filters.z) parts.push(`z=${this.rangeToString(filters.z)}`)
2437
+ // Rotation filters
2438
+ if (filters.x_rotation) parts.push(`x_rotation=${this.rangeToString(filters.x_rotation)}`)
2439
+ if (filters.y_rotation) parts.push(`y_rotation=${this.rangeToString(filters.y_rotation)}`)
2297
2440
 
2298
2441
  return this.finalizeSelector(parts.length ? `${kind}[${parts.join(',')}]` : kind)
2299
2442
  }
@@ -2318,14 +2461,19 @@ export class Lowering {
2318
2461
  // ---------------------------------------------------------------------------
2319
2462
 
2320
2463
  class LoweringBuilder {
2321
- private tempCount = 0
2464
+ private static globalTempId = 0
2322
2465
  private labelCount = 0
2323
2466
  private blocks: any[] = []
2324
2467
  private currentBlock: any = null
2325
2468
  private locals = new Set<string>()
2326
2469
 
2470
+ /** Reset the global temp counter (call between compilations). */
2471
+ static resetTempCounter(): void {
2472
+ LoweringBuilder.globalTempId = 0
2473
+ }
2474
+
2327
2475
  freshTemp(): string {
2328
- const name = `$t${this.tempCount++}`
2476
+ const name = `$_${LoweringBuilder.globalTempId++}`
2329
2477
  this.locals.add(name)
2330
2478
  return name
2331
2479
  }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * RedScript MC Integration Test Runner
3
3
  *
4
- * Compiles a .rs file, installs it to a running Paper server,
4
+ * Compiles a .mcrs file, installs it to a running Paper server,
5
5
  * runs test scenarios, and reports results.
6
6
  *
7
7
  * Usage:
8
- * npx ts-node src/mc-test/runner.ts src/examples/counter.rs
8
+ * npx ts-node src/mc-test/runner.ts src/examples/counter.mcrs
9
9
  *
10
10
  * Requires:
11
11
  * - Paper server running with TestHarnessPlugin
@@ -105,7 +105,7 @@ export async function runMCTests(
105
105
  if (require.main === module) {
106
106
  const sourceFile = process.argv[2]
107
107
  if (!sourceFile) {
108
- console.error('Usage: ts-node runner.ts <source.rs>')
108
+ console.error('Usage: ts-node runner.ts <source.mcrs>')
109
109
  process.exit(1)
110
110
  }
111
111
 
@@ -36,11 +36,11 @@ function main() {
36
36
  // Example files
37
37
  const exampleNamespaces = ['counter', 'world_manager']
38
38
  for (const ns of exampleNamespaces) {
39
- const file = path.join(EXAMPLES_DIR, `${ns}.rs`)
39
+ const file = path.join(EXAMPLES_DIR, `${ns}.mcrs`)
40
40
  if (fs.existsSync(file)) {
41
41
  writeFixture(fs.readFileSync(file, 'utf-8'), ns)
42
42
  } else {
43
- console.log(` ⚠ ${ns}.rs not found, skipping`)
43
+ console.log(` ⚠ ${ns}.mcrs not found, skipping`)
44
44
  }
45
45
  }
46
46
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { Lexer, type Token, type TokenKind } from '../lexer'
9
9
  import type {
10
- Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, LiteralExpr, Param,
10
+ Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, LiteralExpr, Param,
11
11
  Program, RangeExpr, SelectorFilter, SelectorKind, Span, Stmt, TypeNode, AssignOp,
12
12
  StructDecl, StructField, ExecuteSubcommand, EnumDecl, EnumVariant, BlockPosExpr,
13
13
  CoordComponent, LambdaParam
@@ -132,6 +132,7 @@ export class Parser {
132
132
 
133
133
  parse(defaultNamespace = 'redscript'): Program {
134
134
  let namespace = defaultNamespace
135
+ const globals: GlobalDecl[] = []
135
136
  const declarations: FnDecl[] = []
136
137
  const structs: StructDecl[] = []
137
138
  const enums: EnumDecl[] = []
@@ -147,7 +148,9 @@ export class Parser {
147
148
 
148
149
  // Parse struct and function declarations
149
150
  while (!this.check('eof')) {
150
- if (this.check('struct')) {
151
+ if (this.check('let')) {
152
+ globals.push(this.parseGlobalDecl(true))
153
+ } else if (this.check('struct')) {
151
154
  structs.push(this.parseStructDecl())
152
155
  } else if (this.check('enum')) {
153
156
  enums.push(this.parseEnumDecl())
@@ -158,7 +161,7 @@ export class Parser {
158
161
  }
159
162
  }
160
163
 
161
- return { namespace, declarations, structs, enums, consts }
164
+ return { namespace, globals, declarations, structs, enums, consts }
162
165
  }
163
166
 
164
167
  // -------------------------------------------------------------------------
@@ -227,6 +230,17 @@ export class Parser {
227
230
  return this.withLoc({ name, type, value }, constToken)
228
231
  }
229
232
 
233
+ private parseGlobalDecl(mutable: boolean): GlobalDecl {
234
+ const token = this.advance() // consume 'let'
235
+ const name = this.expect('ident').value
236
+ this.expect(':')
237
+ const type = this.parseType()
238
+ this.expect('=')
239
+ const init = this.parseExpr()
240
+ this.expect(';')
241
+ return this.withLoc({ kind: 'global', name, type, init, mutable }, token)
242
+ }
243
+
230
244
  // -------------------------------------------------------------------------
231
245
  // Function Declaration
232
246
  // -------------------------------------------------------------------------
@@ -650,15 +664,15 @@ export class Parser {
650
664
  if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
651
665
  this.advance() // consume 'entity'
652
666
  }
653
- const selector = this.parseSelector()
654
- subcommands.push({ kind: 'if_entity', selector })
667
+ const selectorOrVar = this.parseSelectorOrVarSelector()
668
+ subcommands.push({ kind: 'if_entity', ...selectorOrVar })
655
669
  } else if (this.match('unless')) {
656
670
  // Expect 'entity' keyword (as ident) or just parse selector directly
657
671
  if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
658
672
  this.advance() // consume 'entity'
659
673
  }
660
- const selector = this.parseSelector()
661
- subcommands.push({ kind: 'unless_entity', selector })
674
+ const selectorOrVar = this.parseSelectorOrVarSelector()
675
+ subcommands.push({ kind: 'unless_entity', ...selectorOrVar })
662
676
  } else if (this.match('in')) {
663
677
  const dim = this.expect('ident').value
664
678
  subcommands.push({ kind: 'in', dimension: dim })
@@ -782,6 +796,10 @@ export class Parser {
782
796
  'has_tag': '__entity_has_tag',
783
797
  'push': '__array_push',
784
798
  'pop': '__array_pop',
799
+ 'add': 'set_add',
800
+ 'contains': 'set_contains',
801
+ 'remove': 'set_remove',
802
+ 'clear': 'set_clear',
785
803
  }
786
804
  const internalFn = methodMap[expr.field]
787
805
  if (internalFn) {
@@ -793,7 +811,14 @@ export class Parser {
793
811
  )
794
812
  continue
795
813
  }
796
- this.error(`Unknown method '${expr.field}'`)
814
+ // Generic method sugar: obj.method(args) method(obj, args)
815
+ const args = this.parseArgs()
816
+ this.expect(')')
817
+ expr = this.withLoc(
818
+ { kind: 'call', fn: expr.field, args: [expr.obj, ...args] },
819
+ this.getLocToken(expr) ?? openParenToken
820
+ )
821
+ continue
797
822
  }
798
823
  const args = this.parseArgs()
799
824
  this.expect(')')
@@ -1308,6 +1333,39 @@ export class Parser {
1308
1333
  return this.parseSelectorValue(token.value)
1309
1334
  }
1310
1335
 
1336
+ // Parse either a selector (@a[...]) or a variable with filters (p[...])
1337
+ // Returns { selector } for selectors or { varName, filters } for variables
1338
+ private parseSelectorOrVarSelector(): { selector?: EntitySelector, varName?: string, filters?: SelectorFilter } {
1339
+ if (this.check('selector')) {
1340
+ return { selector: this.parseSelector() }
1341
+ }
1342
+
1343
+ // Must be an identifier (variable) possibly with filters
1344
+ const varToken = this.expect('ident')
1345
+ const varName = varToken.value
1346
+
1347
+ // Check for optional filters [...]
1348
+ if (this.check('[')) {
1349
+ this.advance() // consume '['
1350
+ // Collect everything until ']'
1351
+ let filterStr = ''
1352
+ let depth = 1
1353
+ while (depth > 0 && !this.check('eof')) {
1354
+ if (this.check('[')) depth++
1355
+ else if (this.check(']')) depth--
1356
+ if (depth > 0) {
1357
+ filterStr += this.peek().value ?? this.peek().kind
1358
+ this.advance()
1359
+ }
1360
+ }
1361
+ this.expect(']')
1362
+ const filters = this.parseSelectorFilters(filterStr)
1363
+ return { varName, filters }
1364
+ }
1365
+
1366
+ return { varName }
1367
+ }
1368
+
1311
1369
  private parseSelectorValue(value: string): EntitySelector {
1312
1370
  // Parse @e[type=zombie, distance=..5]
1313
1371
  const bracketIndex = value.indexOf('[')
@@ -1364,6 +1422,21 @@ export class Parser {
1364
1422
  case 'scores':
1365
1423
  filters.scores = this.parseScoresFilter(val)
1366
1424
  break
1425
+ case 'x':
1426
+ filters.x = this.parseRangeValue(val)
1427
+ break
1428
+ case 'y':
1429
+ filters.y = this.parseRangeValue(val)
1430
+ break
1431
+ case 'z':
1432
+ filters.z = this.parseRangeValue(val)
1433
+ break
1434
+ case 'x_rotation':
1435
+ filters.x_rotation = this.parseRangeValue(val)
1436
+ break
1437
+ case 'y_rotation':
1438
+ filters.y_rotation = this.parseRangeValue(val)
1439
+ break
1367
1440
  }
1368
1441
  }
1369
1442