redscript-mc 1.0.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 (272) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  3. package/.github/ISSUE_TEMPLATE/wrong_output.md +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +34 -0
  5. package/.github/workflows/ci.yml +29 -0
  6. package/.github/workflows/publish-extension.yml +35 -0
  7. package/LICENSE +21 -0
  8. package/README.md +261 -0
  9. package/README.zh.md +261 -0
  10. package/dist/__tests__/cli.test.d.ts +1 -0
  11. package/dist/__tests__/cli.test.js +140 -0
  12. package/dist/__tests__/codegen.test.d.ts +1 -0
  13. package/dist/__tests__/codegen.test.js +121 -0
  14. package/dist/__tests__/diagnostics.test.d.ts +4 -0
  15. package/dist/__tests__/diagnostics.test.js +149 -0
  16. package/dist/__tests__/e2e.test.d.ts +6 -0
  17. package/dist/__tests__/e2e.test.js +1528 -0
  18. package/dist/__tests__/lexer.test.d.ts +1 -0
  19. package/dist/__tests__/lexer.test.js +316 -0
  20. package/dist/__tests__/lowering.test.d.ts +1 -0
  21. package/dist/__tests__/lowering.test.js +819 -0
  22. package/dist/__tests__/mc-integration.test.d.ts +12 -0
  23. package/dist/__tests__/mc-integration.test.js +395 -0
  24. package/dist/__tests__/mc-syntax.test.d.ts +1 -0
  25. package/dist/__tests__/mc-syntax.test.js +112 -0
  26. package/dist/__tests__/nbt.test.d.ts +1 -0
  27. package/dist/__tests__/nbt.test.js +82 -0
  28. package/dist/__tests__/optimizer-advanced.test.d.ts +1 -0
  29. package/dist/__tests__/optimizer-advanced.test.js +124 -0
  30. package/dist/__tests__/optimizer.test.d.ts +1 -0
  31. package/dist/__tests__/optimizer.test.js +118 -0
  32. package/dist/__tests__/parser.test.d.ts +1 -0
  33. package/dist/__tests__/parser.test.js +717 -0
  34. package/dist/__tests__/repl.test.d.ts +1 -0
  35. package/dist/__tests__/repl.test.js +27 -0
  36. package/dist/__tests__/runtime.test.d.ts +1 -0
  37. package/dist/__tests__/runtime.test.js +276 -0
  38. package/dist/__tests__/structure-optimizer.test.d.ts +1 -0
  39. package/dist/__tests__/structure-optimizer.test.js +33 -0
  40. package/dist/__tests__/typechecker.test.d.ts +1 -0
  41. package/dist/__tests__/typechecker.test.js +364 -0
  42. package/dist/ast/types.d.ts +357 -0
  43. package/dist/ast/types.js +9 -0
  44. package/dist/cli.d.ts +11 -0
  45. package/dist/cli.js +407 -0
  46. package/dist/codegen/cmdblock/index.d.ts +26 -0
  47. package/dist/codegen/cmdblock/index.js +45 -0
  48. package/dist/codegen/mcfunction/index.d.ts +34 -0
  49. package/dist/codegen/mcfunction/index.js +413 -0
  50. package/dist/codegen/structure/index.d.ts +18 -0
  51. package/dist/codegen/structure/index.js +249 -0
  52. package/dist/compile.d.ts +30 -0
  53. package/dist/compile.js +152 -0
  54. package/dist/data/arena/function/__load.mcfunction +6 -0
  55. package/dist/data/arena/function/__tick.mcfunction +2 -0
  56. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
  57. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
  58. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
  59. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
  60. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
  61. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
  62. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
  63. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
  64. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
  65. package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
  66. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
  67. package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
  68. package/dist/data/arena/function/arena_tick.mcfunction +11 -0
  69. package/dist/data/counter/function/__load.mcfunction +5 -0
  70. package/dist/data/counter/function/__tick.mcfunction +2 -0
  71. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
  72. package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
  73. package/dist/data/counter/function/counter_tick.mcfunction +11 -0
  74. package/dist/data/minecraft/tags/function/load.json +5 -0
  75. package/dist/data/minecraft/tags/function/tick.json +5 -0
  76. package/dist/data/quiz/function/__load.mcfunction +16 -0
  77. package/dist/data/quiz/function/__tick.mcfunction +6 -0
  78. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
  79. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
  80. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
  81. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
  82. package/dist/data/quiz/function/answer_a.mcfunction +4 -0
  83. package/dist/data/quiz/function/answer_b.mcfunction +4 -0
  84. package/dist/data/quiz/function/answer_c.mcfunction +4 -0
  85. package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
  86. package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
  87. package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
  88. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
  89. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
  90. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
  91. package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
  92. package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
  93. package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
  94. package/dist/data/quiz/function/ask_question.mcfunction +7 -0
  95. package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
  96. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
  97. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
  98. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
  99. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
  100. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
  101. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
  102. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
  103. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
  104. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
  105. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
  106. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
  107. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
  108. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
  109. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
  110. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
  111. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
  112. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
  113. package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
  114. package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
  115. package/dist/data/shop/function/__load.mcfunction +7 -0
  116. package/dist/data/shop/function/__tick.mcfunction +3 -0
  117. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
  118. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
  119. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
  120. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
  121. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
  122. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
  123. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
  124. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
  125. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
  126. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
  127. package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
  128. package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
  129. package/dist/data/turret/function/__load.mcfunction +5 -0
  130. package/dist/data/turret/function/__tick.mcfunction +4 -0
  131. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
  132. package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
  133. package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
  134. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
  135. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
  136. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
  137. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
  138. package/dist/data/turret/function/turret_tick.mcfunction +5 -0
  139. package/dist/diagnostics/index.d.ts +44 -0
  140. package/dist/diagnostics/index.js +140 -0
  141. package/dist/index.d.ts +53 -0
  142. package/dist/index.js +126 -0
  143. package/dist/ir/builder.d.ts +32 -0
  144. package/dist/ir/builder.js +99 -0
  145. package/dist/ir/types.d.ts +117 -0
  146. package/dist/ir/types.js +15 -0
  147. package/dist/lexer/index.d.ts +36 -0
  148. package/dist/lexer/index.js +458 -0
  149. package/dist/lowering/index.d.ts +106 -0
  150. package/dist/lowering/index.js +2041 -0
  151. package/dist/mc-test/client.d.ts +128 -0
  152. package/dist/mc-test/client.js +174 -0
  153. package/dist/mc-test/runner.d.ts +28 -0
  154. package/dist/mc-test/runner.js +150 -0
  155. package/dist/mc-test/setup.d.ts +11 -0
  156. package/dist/mc-test/setup.js +98 -0
  157. package/dist/mc-validator/index.d.ts +17 -0
  158. package/dist/mc-validator/index.js +322 -0
  159. package/dist/nbt/index.d.ts +86 -0
  160. package/dist/nbt/index.js +250 -0
  161. package/dist/optimizer/commands.d.ts +36 -0
  162. package/dist/optimizer/commands.js +349 -0
  163. package/dist/optimizer/passes.d.ts +34 -0
  164. package/dist/optimizer/passes.js +227 -0
  165. package/dist/optimizer/structure.d.ts +8 -0
  166. package/dist/optimizer/structure.js +344 -0
  167. package/dist/pack.mcmeta +6 -0
  168. package/dist/parser/index.d.ts +76 -0
  169. package/dist/parser/index.js +1193 -0
  170. package/dist/repl.d.ts +16 -0
  171. package/dist/repl.js +165 -0
  172. package/dist/runtime/index.d.ts +101 -0
  173. package/dist/runtime/index.js +1288 -0
  174. package/dist/typechecker/index.d.ts +42 -0
  175. package/dist/typechecker/index.js +629 -0
  176. package/docs/COMPILATION_STATS.md +142 -0
  177. package/docs/IMPLEMENTATION_GUIDE.md +512 -0
  178. package/docs/LANGUAGE_REFERENCE.md +415 -0
  179. package/docs/MC_MAPPING.md +280 -0
  180. package/docs/STRUCTURE_TARGET.md +80 -0
  181. package/docs/mc-reference/commands.md +259 -0
  182. package/editors/vscode/.vscodeignore +10 -0
  183. package/editors/vscode/LICENSE +21 -0
  184. package/editors/vscode/README.md +78 -0
  185. package/editors/vscode/build.mjs +28 -0
  186. package/editors/vscode/icon.png +0 -0
  187. package/editors/vscode/mcfunction-language-configuration.json +28 -0
  188. package/editors/vscode/out/extension.js +7236 -0
  189. package/editors/vscode/package-lock.json +566 -0
  190. package/editors/vscode/package.json +137 -0
  191. package/editors/vscode/redscript-language-configuration.json +28 -0
  192. package/editors/vscode/snippets/redscript.json +114 -0
  193. package/editors/vscode/src/codeactions.ts +89 -0
  194. package/editors/vscode/src/completion.ts +130 -0
  195. package/editors/vscode/src/extension.ts +239 -0
  196. package/editors/vscode/src/hover.ts +1120 -0
  197. package/editors/vscode/src/symbols.ts +207 -0
  198. package/editors/vscode/syntaxes/mcfunction.tmLanguage.json +740 -0
  199. package/editors/vscode/syntaxes/redscript.tmLanguage.json +357 -0
  200. package/editors/vscode/tsconfig.json +13 -0
  201. package/jest.config.js +5 -0
  202. package/package.json +38 -0
  203. package/src/__tests__/cli.test.ts +130 -0
  204. package/src/__tests__/codegen.test.ts +128 -0
  205. package/src/__tests__/diagnostics.test.ts +195 -0
  206. package/src/__tests__/e2e.test.ts +1721 -0
  207. package/src/__tests__/fixtures/mc-commands-1.21.4.json +18734 -0
  208. package/src/__tests__/formatter.test.ts +46 -0
  209. package/src/__tests__/lexer.test.ts +356 -0
  210. package/src/__tests__/lowering.test.ts +962 -0
  211. package/src/__tests__/mc-integration.test.ts +409 -0
  212. package/src/__tests__/mc-syntax.test.ts +96 -0
  213. package/src/__tests__/nbt.test.ts +58 -0
  214. package/src/__tests__/optimizer-advanced.test.ts +144 -0
  215. package/src/__tests__/optimizer.test.ts +129 -0
  216. package/src/__tests__/parser.test.ts +800 -0
  217. package/src/__tests__/repl.test.ts +33 -0
  218. package/src/__tests__/runtime.test.ts +289 -0
  219. package/src/__tests__/structure-optimizer.test.ts +38 -0
  220. package/src/__tests__/typechecker.test.ts +395 -0
  221. package/src/ast/types.ts +248 -0
  222. package/src/cli.ts +445 -0
  223. package/src/codegen/cmdblock/index.ts +63 -0
  224. package/src/codegen/mcfunction/index.ts +471 -0
  225. package/src/codegen/structure/index.ts +305 -0
  226. package/src/compile.ts +188 -0
  227. package/src/diagnostics/index.ts +186 -0
  228. package/src/examples/README.md +77 -0
  229. package/src/examples/SHOWCASE_GAME.md +43 -0
  230. package/src/examples/arena.rs +44 -0
  231. package/src/examples/counter.rs +12 -0
  232. package/src/examples/pvp_arena.rs +131 -0
  233. package/src/examples/quiz.rs +90 -0
  234. package/src/examples/rpg.rs +13 -0
  235. package/src/examples/shop.rs +30 -0
  236. package/src/examples/showcase_game.rs +552 -0
  237. package/src/examples/stdlib_demo.rs +181 -0
  238. package/src/examples/turret.rs +27 -0
  239. package/src/examples/world_manager.rs +23 -0
  240. package/src/formatter/index.ts +22 -0
  241. package/src/index.ts +161 -0
  242. package/src/ir/builder.ts +114 -0
  243. package/src/ir/types.ts +119 -0
  244. package/src/lexer/index.ts +555 -0
  245. package/src/lowering/index.ts +2406 -0
  246. package/src/mc-test/client.ts +259 -0
  247. package/src/mc-test/runner.ts +140 -0
  248. package/src/mc-test/setup.ts +70 -0
  249. package/src/mc-validator/index.ts +367 -0
  250. package/src/nbt/index.ts +321 -0
  251. package/src/optimizer/commands.ts +416 -0
  252. package/src/optimizer/passes.ts +233 -0
  253. package/src/optimizer/structure.ts +441 -0
  254. package/src/parser/index.ts +1437 -0
  255. package/src/repl.ts +165 -0
  256. package/src/runtime/index.ts +1403 -0
  257. package/src/stdlib/README.md +156 -0
  258. package/src/stdlib/combat.rs +20 -0
  259. package/src/stdlib/cooldown.rs +45 -0
  260. package/src/stdlib/math.rs +49 -0
  261. package/src/stdlib/mobs.rs +99 -0
  262. package/src/stdlib/player.rs +29 -0
  263. package/src/stdlib/strings.rs +7 -0
  264. package/src/stdlib/timer.rs +51 -0
  265. package/src/templates/README.md +126 -0
  266. package/src/templates/combat.rs +96 -0
  267. package/src/templates/economy.rs +40 -0
  268. package/src/templates/mini-game-framework.rs +117 -0
  269. package/src/templates/quest.rs +78 -0
  270. package/src/test_programs/zombie_game.rs +25 -0
  271. package/src/typechecker/index.ts +737 -0
  272. package/tsconfig.json +16 -0
@@ -0,0 +1,1437 @@
1
+ /**
2
+ * RedScript Parser
3
+ *
4
+ * Recursive descent parser that converts tokens into an AST.
5
+ * Uses precedence climbing for expression parsing.
6
+ */
7
+
8
+ import { Lexer, type Token, type TokenKind } from '../lexer'
9
+ import type {
10
+ Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, LiteralExpr, Param,
11
+ Program, RangeExpr, SelectorFilter, SelectorKind, Span, Stmt, TypeNode, AssignOp,
12
+ StructDecl, StructField, ExecuteSubcommand, EnumDecl, EnumVariant, BlockPosExpr,
13
+ CoordComponent, LambdaParam
14
+ } from '../ast/types'
15
+ import type { BinOp, CmpOp } from '../ir/types'
16
+ import { DiagnosticError } from '../diagnostics'
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Operator Precedence (higher = binds tighter)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const PRECEDENCE: Record<string, number> = {
23
+ '||': 1,
24
+ '&&': 2,
25
+ '==': 3, '!=': 3,
26
+ '<': 4, '<=': 4, '>': 4, '>=': 4,
27
+ '+': 5, '-': 5,
28
+ '*': 6, '/': 6, '%': 6,
29
+ }
30
+
31
+ const BINARY_OPS = new Set(['||', '&&', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '%'])
32
+
33
+ function computeIsSingle(raw: string): boolean {
34
+ if (/^@[spr](\[|$)/.test(raw)) return true
35
+ if (/[\[,\s]limit=1[,\]\s]/.test(raw)) return true
36
+ return false
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Parser Class
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export class Parser {
44
+ private tokens: Token[]
45
+ private pos: number = 0
46
+ private sourceLines: string[]
47
+ private filePath?: string
48
+
49
+ constructor(tokens: Token[], source?: string, filePath?: string) {
50
+ this.tokens = tokens
51
+ this.sourceLines = source?.split('\n') ?? []
52
+ this.filePath = filePath
53
+ }
54
+
55
+ // -------------------------------------------------------------------------
56
+ // Utilities
57
+ // -------------------------------------------------------------------------
58
+
59
+ private peek(offset = 0): Token {
60
+ const idx = this.pos + offset
61
+ if (idx >= this.tokens.length) {
62
+ return this.tokens[this.tokens.length - 1] // eof
63
+ }
64
+ return this.tokens[idx]
65
+ }
66
+
67
+ private advance(): Token {
68
+ const token = this.tokens[this.pos]
69
+ if (token.kind !== 'eof') this.pos++
70
+ return token
71
+ }
72
+
73
+ private check(kind: TokenKind): boolean {
74
+ return this.peek().kind === kind
75
+ }
76
+
77
+ private match(...kinds: TokenKind[]): boolean {
78
+ for (const kind of kinds) {
79
+ if (this.check(kind)) {
80
+ this.advance()
81
+ return true
82
+ }
83
+ }
84
+ return false
85
+ }
86
+
87
+ private expect(kind: TokenKind): Token {
88
+ const token = this.peek()
89
+ if (token.kind !== kind) {
90
+ throw new DiagnosticError(
91
+ 'ParseError',
92
+ `Expected '${kind}' but got '${token.kind}'`,
93
+ { file: this.filePath, line: token.line, col: token.col },
94
+ this.sourceLines
95
+ )
96
+ }
97
+ return this.advance()
98
+ }
99
+
100
+ private error(message: string): never {
101
+ const token = this.peek()
102
+ throw new DiagnosticError(
103
+ 'ParseError',
104
+ message,
105
+ { file: this.filePath, line: token.line, col: token.col },
106
+ this.sourceLines
107
+ )
108
+ }
109
+
110
+ private withLoc<T extends object>(node: T, token: Token): T {
111
+ const span: Span = { line: token.line, col: token.col }
112
+ Object.defineProperty(node, 'span', {
113
+ value: span,
114
+ enumerable: false,
115
+ configurable: true,
116
+ writable: true,
117
+ })
118
+ return node
119
+ }
120
+
121
+ private getLocToken(node: object): Token | null {
122
+ const span = (node as { span?: Span }).span
123
+ if (!span) {
124
+ return null
125
+ }
126
+ return { kind: 'eof', value: '', line: span.line, col: span.col }
127
+ }
128
+
129
+ // -------------------------------------------------------------------------
130
+ // Program
131
+ // -------------------------------------------------------------------------
132
+
133
+ parse(defaultNamespace = 'redscript'): Program {
134
+ let namespace = defaultNamespace
135
+ const declarations: FnDecl[] = []
136
+ const structs: StructDecl[] = []
137
+ const enums: EnumDecl[] = []
138
+ const consts: ConstDecl[] = []
139
+
140
+ // Check for namespace declaration
141
+ if (this.check('namespace')) {
142
+ this.advance()
143
+ const name = this.expect('ident')
144
+ namespace = name.value
145
+ this.expect(';')
146
+ }
147
+
148
+ // Parse struct and function declarations
149
+ while (!this.check('eof')) {
150
+ if (this.check('struct')) {
151
+ structs.push(this.parseStructDecl())
152
+ } else if (this.check('enum')) {
153
+ enums.push(this.parseEnumDecl())
154
+ } else if (this.check('const')) {
155
+ consts.push(this.parseConstDecl())
156
+ } else {
157
+ declarations.push(this.parseFnDecl())
158
+ }
159
+ }
160
+
161
+ return { namespace, declarations, structs, enums, consts }
162
+ }
163
+
164
+ // -------------------------------------------------------------------------
165
+ // Struct Declaration
166
+ // -------------------------------------------------------------------------
167
+
168
+ private parseStructDecl(): StructDecl {
169
+ const structToken = this.expect('struct')
170
+ const name = this.expect('ident').value
171
+ this.expect('{')
172
+
173
+ const fields: StructField[] = []
174
+ while (!this.check('}') && !this.check('eof')) {
175
+ const fieldName = this.expect('ident').value
176
+ this.expect(':')
177
+ const fieldType = this.parseType()
178
+ fields.push({ name: fieldName, type: fieldType })
179
+
180
+ // Allow optional comma or semicolon between fields
181
+ this.match(',')
182
+ }
183
+
184
+ this.expect('}')
185
+ return this.withLoc({ name, fields }, structToken)
186
+ }
187
+
188
+ private parseEnumDecl(): EnumDecl {
189
+ const enumToken = this.expect('enum')
190
+ const name = this.expect('ident').value
191
+ this.expect('{')
192
+
193
+ const variants: EnumVariant[] = []
194
+ let nextValue = 0
195
+
196
+ while (!this.check('}') && !this.check('eof')) {
197
+ const variantToken = this.expect('ident')
198
+ const variant: EnumVariant = { name: variantToken.value }
199
+
200
+ if (this.match('=')) {
201
+ const valueToken = this.expect('int_lit')
202
+ variant.value = parseInt(valueToken.value, 10)
203
+ nextValue = variant.value + 1
204
+ } else {
205
+ variant.value = nextValue++
206
+ }
207
+
208
+ variants.push(variant)
209
+
210
+ if (!this.match(',')) {
211
+ break
212
+ }
213
+ }
214
+
215
+ this.expect('}')
216
+ return this.withLoc({ name, variants }, enumToken)
217
+ }
218
+
219
+ private parseConstDecl(): ConstDecl {
220
+ const constToken = this.expect('const')
221
+ const name = this.expect('ident').value
222
+ this.expect(':')
223
+ const type = this.parseType()
224
+ this.expect('=')
225
+ const value = this.parseLiteralExpr()
226
+ this.match(';')
227
+ return this.withLoc({ name, type, value }, constToken)
228
+ }
229
+
230
+ // -------------------------------------------------------------------------
231
+ // Function Declaration
232
+ // -------------------------------------------------------------------------
233
+
234
+ private parseFnDecl(): FnDecl {
235
+ const decorators = this.parseDecorators()
236
+
237
+ const fnToken = this.expect('fn')
238
+ const name = this.expect('ident').value
239
+ this.expect('(')
240
+ const params = this.parseParams()
241
+ this.expect(')')
242
+
243
+ let returnType: TypeNode = { kind: 'named', name: 'void' }
244
+ if (this.match('->')) {
245
+ returnType = this.parseType()
246
+ }
247
+
248
+ const body = this.parseBlock()
249
+
250
+ return this.withLoc({ name, params, returnType, decorators, body }, fnToken)
251
+ }
252
+
253
+ private parseDecorators(): Decorator[] {
254
+ const decorators: Decorator[] = []
255
+
256
+ while (this.check('decorator')) {
257
+ const token = this.advance()
258
+ const decorator = this.parseDecoratorValue(token.value)
259
+ decorators.push(decorator)
260
+ }
261
+
262
+ return decorators
263
+ }
264
+
265
+ private parseDecoratorValue(value: string): Decorator {
266
+ // Parse @tick or @on_trigger("name") or @on_advancement("story/mine_diamond")
267
+ const match = value.match(/^@(\w+)(?:\(([^)]*)\))?$/)
268
+ if (!match) {
269
+ this.error(`Invalid decorator: ${value}`)
270
+ }
271
+
272
+ const name = match[1] as Decorator['name']
273
+ const argsStr = match[2]
274
+
275
+ if (!argsStr) {
276
+ return { name }
277
+ }
278
+
279
+ const args: Decorator['args'] = {}
280
+
281
+ // Handle @on_trigger("name"), @on_advancement("id"), @on_craft("item"), @on_join_team("team")
282
+ if (name === 'on_trigger' || name === 'on_advancement' || name === 'on_craft' || name === 'on_join_team') {
283
+ const strMatch = argsStr.match(/^"([^"]*)"$/)
284
+ if (strMatch) {
285
+ if (name === 'on_trigger') {
286
+ args.trigger = strMatch[1]
287
+ } else if (name === 'on_advancement') {
288
+ args.advancement = strMatch[1]
289
+ } else if (name === 'on_craft') {
290
+ args.item = strMatch[1]
291
+ } else if (name === 'on_join_team') {
292
+ args.team = strMatch[1]
293
+ }
294
+ return { name, args }
295
+ }
296
+ }
297
+
298
+ // Handle key=value format (e.g., rate=20)
299
+ for (const part of argsStr.split(',')) {
300
+ const [key, val] = part.split('=').map(s => s.trim())
301
+ if (key === 'rate') {
302
+ args.rate = parseInt(val, 10)
303
+ } else if (key === 'trigger') {
304
+ args.trigger = val
305
+ } else if (key === 'advancement') {
306
+ args.advancement = val
307
+ } else if (key === 'item') {
308
+ args.item = val
309
+ } else if (key === 'team') {
310
+ args.team = val
311
+ }
312
+ }
313
+
314
+ return { name, args }
315
+ }
316
+
317
+ private parseParams(): Param[] {
318
+ const params: Param[] = []
319
+
320
+ if (!this.check(')')) {
321
+ do {
322
+ const paramToken = this.expect('ident')
323
+ const name = paramToken.value
324
+ this.expect(':')
325
+ const type = this.parseType()
326
+ let defaultValue: Expr | undefined
327
+ if (this.match('=')) {
328
+ defaultValue = this.parseExpr()
329
+ }
330
+ params.push(this.withLoc({ name, type, default: defaultValue }, paramToken))
331
+ } while (this.match(','))
332
+ }
333
+
334
+ return params
335
+ }
336
+
337
+ private parseType(): TypeNode {
338
+ const token = this.peek()
339
+ let type: TypeNode
340
+
341
+ if (token.kind === '(') {
342
+ return this.parseFunctionType()
343
+ }
344
+
345
+ if (token.kind === 'int' || token.kind === 'bool' ||
346
+ token.kind === 'float' || token.kind === 'string' || token.kind === 'void' ||
347
+ token.kind === 'BlockPos') {
348
+ this.advance()
349
+ type = { kind: 'named', name: token.kind }
350
+ } else if (token.kind === 'ident') {
351
+ this.advance()
352
+ type = { kind: 'struct', name: token.value }
353
+ } else {
354
+ this.error(`Expected type, got '${token.kind}'`)
355
+ }
356
+
357
+ while (this.match('[')) {
358
+ this.expect(']')
359
+ type = { kind: 'array', elem: type }
360
+ }
361
+
362
+ return type
363
+ }
364
+
365
+ private parseFunctionType(): TypeNode {
366
+ this.expect('(')
367
+ const params: TypeNode[] = []
368
+
369
+ if (!this.check(')')) {
370
+ do {
371
+ params.push(this.parseType())
372
+ } while (this.match(','))
373
+ }
374
+
375
+ this.expect(')')
376
+ this.expect('->')
377
+ const returnType = this.parseType()
378
+ return { kind: 'function_type', params, return: returnType }
379
+ }
380
+
381
+ // -------------------------------------------------------------------------
382
+ // Block & Statements
383
+ // -------------------------------------------------------------------------
384
+
385
+ private parseBlock(): Block {
386
+ this.expect('{')
387
+ const stmts: Stmt[] = []
388
+
389
+ while (!this.check('}') && !this.check('eof')) {
390
+ stmts.push(this.parseStmt())
391
+ }
392
+
393
+ this.expect('}')
394
+ return stmts
395
+ }
396
+
397
+ private parseStmt(): Stmt {
398
+ // Let statement
399
+ if (this.check('let')) {
400
+ return this.parseLetStmt()
401
+ }
402
+
403
+ // Return statement
404
+ if (this.check('return')) {
405
+ return this.parseReturnStmt()
406
+ }
407
+
408
+ // If statement
409
+ if (this.check('if')) {
410
+ return this.parseIfStmt()
411
+ }
412
+
413
+ // While statement
414
+ if (this.check('while')) {
415
+ return this.parseWhileStmt()
416
+ }
417
+
418
+ // For statement
419
+ if (this.check('for')) {
420
+ return this.parseForStmt()
421
+ }
422
+
423
+ // Foreach statement
424
+ if (this.check('foreach')) {
425
+ return this.parseForeachStmt()
426
+ }
427
+
428
+ if (this.check('match')) {
429
+ return this.parseMatchStmt()
430
+ }
431
+
432
+ // As block
433
+ if (this.check('as')) {
434
+ return this.parseAsStmt()
435
+ }
436
+
437
+ // At block
438
+ if (this.check('at')) {
439
+ return this.parseAtStmt()
440
+ }
441
+
442
+ // Execute statement: execute as/at/if/unless/in ... run { }
443
+ if (this.check('execute')) {
444
+ return this.parseExecuteStmt()
445
+ }
446
+
447
+ // Raw command
448
+ if (this.check('raw_cmd')) {
449
+ const token = this.advance()
450
+ const cmd = token.value
451
+ this.match(';') // optional semicolon (raw consumes it)
452
+ return this.withLoc({ kind: 'raw', cmd }, token)
453
+ }
454
+
455
+ // Expression statement
456
+ return this.parseExprStmt()
457
+ }
458
+
459
+ private parseLetStmt(): Stmt {
460
+ const letToken = this.expect('let')
461
+ const name = this.expect('ident').value
462
+
463
+ let type: TypeNode | undefined
464
+ if (this.match(':')) {
465
+ type = this.parseType()
466
+ }
467
+
468
+ this.expect('=')
469
+ const init = this.parseExpr()
470
+ this.expect(';')
471
+
472
+ return this.withLoc({ kind: 'let', name, type, init }, letToken)
473
+ }
474
+
475
+ private parseReturnStmt(): Stmt {
476
+ const returnToken = this.expect('return')
477
+
478
+ let value: Expr | undefined
479
+ if (!this.check(';')) {
480
+ value = this.parseExpr()
481
+ }
482
+
483
+ this.expect(';')
484
+ return this.withLoc({ kind: 'return', value }, returnToken)
485
+ }
486
+
487
+ private parseIfStmt(): Stmt {
488
+ const ifToken = this.expect('if')
489
+ this.expect('(')
490
+ const cond = this.parseExpr()
491
+ this.expect(')')
492
+ const then = this.parseBlock()
493
+
494
+ let else_: Block | undefined
495
+ if (this.match('else')) {
496
+ if (this.check('if')) {
497
+ // else if
498
+ else_ = [this.parseIfStmt()]
499
+ } else {
500
+ else_ = this.parseBlock()
501
+ }
502
+ }
503
+
504
+ return this.withLoc({ kind: 'if', cond, then, else_ }, ifToken)
505
+ }
506
+
507
+ private parseWhileStmt(): Stmt {
508
+ const whileToken = this.expect('while')
509
+ this.expect('(')
510
+ const cond = this.parseExpr()
511
+ this.expect(')')
512
+ const body = this.parseBlock()
513
+
514
+ return this.withLoc({ kind: 'while', cond, body }, whileToken)
515
+ }
516
+
517
+ private parseForStmt(): Stmt {
518
+ const forToken = this.expect('for')
519
+
520
+ // Check for for-range syntax: for <ident> in <range_lit> { ... }
521
+ if (this.check('ident') && this.peek(1).kind === 'in') {
522
+ return this.parseForRangeStmt(forToken)
523
+ }
524
+
525
+ this.expect('(')
526
+
527
+ // Init: either let statement (without semicolon) or empty
528
+ let init: Stmt | undefined
529
+ if (this.check('let')) {
530
+ // Parse let without consuming semicolon here (we handle it)
531
+ const letToken = this.expect('let')
532
+ const name = this.expect('ident').value
533
+ let type: TypeNode | undefined
534
+ if (this.match(':')) {
535
+ type = this.parseType()
536
+ }
537
+ this.expect('=')
538
+ const initExpr = this.parseExpr()
539
+ const initStmt: Stmt = { kind: 'let', name, type, init: initExpr }
540
+ init = this.withLoc(initStmt, letToken)
541
+ }
542
+ this.expect(';')
543
+
544
+ // Condition
545
+ const cond = this.parseExpr()
546
+ this.expect(';')
547
+
548
+ // Step expression
549
+ const step = this.parseExpr()
550
+ this.expect(')')
551
+
552
+ const body = this.parseBlock()
553
+
554
+ return this.withLoc({ kind: 'for', init, cond, step, body }, forToken)
555
+ }
556
+
557
+ private parseForRangeStmt(forToken: Token): Stmt {
558
+ const varName = this.expect('ident').value
559
+ this.expect('in')
560
+ const rangeToken = this.expect('range_lit')
561
+ const range = this.parseRangeValue(rangeToken.value)
562
+
563
+ const start: Expr = this.withLoc(
564
+ { kind: 'int_lit', value: range.min ?? 0 },
565
+ rangeToken
566
+ )
567
+ const end: Expr = this.withLoc(
568
+ { kind: 'int_lit', value: range.max ?? 0 },
569
+ rangeToken
570
+ )
571
+
572
+ const body = this.parseBlock()
573
+ return this.withLoc({ kind: 'for_range', varName, start, end, body }, forToken)
574
+ }
575
+
576
+ private parseForeachStmt(): Stmt {
577
+ const foreachToken = this.expect('foreach')
578
+ this.expect('(')
579
+ const binding = this.expect('ident').value
580
+ this.expect('in')
581
+ const iterable = this.parseExpr()
582
+ this.expect(')')
583
+ const body = this.parseBlock()
584
+
585
+ return this.withLoc({ kind: 'foreach', binding, iterable, body }, foreachToken)
586
+ }
587
+
588
+ private parseMatchStmt(): Stmt {
589
+ const matchToken = this.expect('match')
590
+ this.expect('(')
591
+ const expr = this.parseExpr()
592
+ this.expect(')')
593
+ this.expect('{')
594
+
595
+ const arms: Array<{ pattern: Expr | null; body: Block }> = []
596
+ while (!this.check('}') && !this.check('eof')) {
597
+ let pattern: Expr | null
598
+ if (this.check('ident') && this.peek().value === '_') {
599
+ this.advance()
600
+ pattern = null
601
+ } else {
602
+ pattern = this.parseExpr()
603
+ }
604
+
605
+ this.expect('=>')
606
+ const body = this.parseBlock()
607
+ arms.push({ pattern, body })
608
+ }
609
+
610
+ this.expect('}')
611
+ return this.withLoc({ kind: 'match', expr, arms }, matchToken)
612
+ }
613
+
614
+ private parseAsStmt(): Stmt {
615
+ const asToken = this.expect('as')
616
+ const as_sel = this.parseSelector()
617
+
618
+ // Check for combined as/at
619
+ if (this.match('at')) {
620
+ const at_sel = this.parseSelector()
621
+ const body = this.parseBlock()
622
+ return this.withLoc({ kind: 'as_at', as_sel, at_sel, body }, asToken)
623
+ }
624
+
625
+ const body = this.parseBlock()
626
+ return this.withLoc({ kind: 'as_block', selector: as_sel, body }, asToken)
627
+ }
628
+
629
+ private parseAtStmt(): Stmt {
630
+ const atToken = this.expect('at')
631
+ const selector = this.parseSelector()
632
+ const body = this.parseBlock()
633
+ return this.withLoc({ kind: 'at_block', selector, body }, atToken)
634
+ }
635
+
636
+ private parseExecuteStmt(): Stmt {
637
+ const executeToken = this.expect('execute')
638
+ const subcommands: ExecuteSubcommand[] = []
639
+
640
+ // Parse subcommands until we hit 'run'
641
+ while (!this.check('run') && !this.check('eof')) {
642
+ if (this.match('as')) {
643
+ const selector = this.parseSelector()
644
+ subcommands.push({ kind: 'as', selector })
645
+ } else if (this.match('at')) {
646
+ const selector = this.parseSelector()
647
+ subcommands.push({ kind: 'at', selector })
648
+ } else if (this.match('if')) {
649
+ // Expect 'entity' keyword (as ident) or just parse selector directly
650
+ if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
651
+ this.advance() // consume 'entity'
652
+ }
653
+ const selector = this.parseSelector()
654
+ subcommands.push({ kind: 'if_entity', selector })
655
+ } else if (this.match('unless')) {
656
+ // Expect 'entity' keyword (as ident) or just parse selector directly
657
+ if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
658
+ this.advance() // consume 'entity'
659
+ }
660
+ const selector = this.parseSelector()
661
+ subcommands.push({ kind: 'unless_entity', selector })
662
+ } else if (this.match('in')) {
663
+ const dim = this.expect('ident').value
664
+ subcommands.push({ kind: 'in', dimension: dim })
665
+ } else {
666
+ this.error(`Unexpected token in execute statement: ${this.peek().kind}`)
667
+ }
668
+ }
669
+
670
+ this.expect('run')
671
+ const body = this.parseBlock()
672
+
673
+ return this.withLoc({ kind: 'execute', subcommands, body }, executeToken)
674
+ }
675
+
676
+ private parseExprStmt(): Stmt {
677
+ const expr = this.parseExpr()
678
+ this.expect(';')
679
+ const exprToken = this.getLocToken(expr) ?? this.peek()
680
+ return this.withLoc({ kind: 'expr', expr }, exprToken)
681
+ }
682
+
683
+ // -------------------------------------------------------------------------
684
+ // Expressions (Precedence Climbing)
685
+ // -------------------------------------------------------------------------
686
+
687
+ private parseExpr(): Expr {
688
+ return this.parseAssignment()
689
+ }
690
+
691
+ private parseAssignment(): Expr {
692
+ const left = this.parseBinaryExpr(1)
693
+
694
+ // Check for assignment
695
+ const token = this.peek()
696
+ if (token.kind === '=' || token.kind === '+=' || token.kind === '-=' ||
697
+ token.kind === '*=' || token.kind === '/=' || token.kind === '%=') {
698
+ const op = this.advance().kind as AssignOp
699
+
700
+ if (left.kind === 'ident') {
701
+ const value = this.parseAssignment()
702
+ return this.withLoc({ kind: 'assign', target: left.name, op, value }, this.getLocToken(left) ?? token)
703
+ }
704
+
705
+ // Member assignment: p.x = 10, p.x += 5
706
+ if (left.kind === 'member') {
707
+ const value = this.parseAssignment()
708
+ return this.withLoc(
709
+ { kind: 'member_assign', obj: left.obj, field: left.field, op, value },
710
+ this.getLocToken(left) ?? token
711
+ )
712
+ }
713
+ }
714
+
715
+ return left
716
+ }
717
+
718
+ private parseBinaryExpr(minPrec: number): Expr {
719
+ let left = this.parseUnaryExpr()
720
+
721
+ while (true) {
722
+ const op = this.peek().kind
723
+ if (!BINARY_OPS.has(op)) break
724
+
725
+ const prec = PRECEDENCE[op]
726
+ if (prec < minPrec) break
727
+
728
+ const opToken = this.advance()
729
+ const right = this.parseBinaryExpr(prec + 1) // left associative
730
+ left = this.withLoc(
731
+ { kind: 'binary', op: op as BinOp | CmpOp | '&&' | '||', left, right },
732
+ this.getLocToken(left) ?? opToken
733
+ )
734
+ }
735
+
736
+ return left
737
+ }
738
+
739
+ private parseUnaryExpr(): Expr {
740
+ if (this.match('!')) {
741
+ const bangToken = this.tokens[this.pos - 1]
742
+ const operand = this.parseUnaryExpr()
743
+ return this.withLoc({ kind: 'unary', op: '!', operand }, bangToken)
744
+ }
745
+
746
+ if (this.check('-') && !this.isSubtraction()) {
747
+ const minusToken = this.advance()
748
+ const operand = this.parseUnaryExpr()
749
+ return this.withLoc({ kind: 'unary', op: '-', operand }, minusToken)
750
+ }
751
+
752
+ return this.parsePostfixExpr()
753
+ }
754
+
755
+ private isSubtraction(): boolean {
756
+ // Check if this minus is binary (subtraction) by looking at previous token
757
+ // If previous was a value (literal, ident, ), ]) it's subtraction
758
+ if (this.pos === 0) return false
759
+ const prev = this.tokens[this.pos - 1]
760
+ return ['int_lit', 'float_lit', 'ident', ')', ']'].includes(prev.kind)
761
+ }
762
+
763
+ private parsePostfixExpr(): Expr {
764
+ let expr = this.parsePrimaryExpr()
765
+
766
+ while (true) {
767
+ // Function call
768
+ if (this.match('(')) {
769
+ const openParenToken = this.tokens[this.pos - 1]
770
+ if (expr.kind === 'ident') {
771
+ const args = this.parseArgs()
772
+ this.expect(')')
773
+ expr = this.withLoc({ kind: 'call', fn: expr.name, args }, this.getLocToken(expr) ?? openParenToken)
774
+ continue
775
+ }
776
+ // Member call: entity.tag("name") → __entity_tag(entity, "name")
777
+ // Also handle arr.push(val) and arr.length
778
+ if (expr.kind === 'member') {
779
+ const methodMap: Record<string, string> = {
780
+ 'tag': '__entity_tag',
781
+ 'untag': '__entity_untag',
782
+ 'has_tag': '__entity_has_tag',
783
+ 'push': '__array_push',
784
+ 'pop': '__array_pop',
785
+ }
786
+ const internalFn = methodMap[expr.field]
787
+ if (internalFn) {
788
+ const args = this.parseArgs()
789
+ this.expect(')')
790
+ expr = this.withLoc(
791
+ { kind: 'call', fn: internalFn, args: [expr.obj, ...args] },
792
+ this.getLocToken(expr) ?? openParenToken
793
+ )
794
+ continue
795
+ }
796
+ this.error(`Unknown method '${expr.field}'`)
797
+ }
798
+ const args = this.parseArgs()
799
+ this.expect(')')
800
+ expr = this.withLoc(
801
+ { kind: 'invoke', callee: expr, args },
802
+ this.getLocToken(expr) ?? openParenToken
803
+ )
804
+ continue
805
+ }
806
+
807
+ // Array index access: arr[0]
808
+ if (this.match('[')) {
809
+ const index = this.parseExpr()
810
+ this.expect(']')
811
+ expr = this.withLoc(
812
+ { kind: 'index', obj: expr, index },
813
+ this.getLocToken(expr) ?? this.tokens[this.pos - 1]
814
+ )
815
+ continue
816
+ }
817
+
818
+ // Member access
819
+ if (this.match('.')) {
820
+ const field = this.expect('ident').value
821
+ expr = this.withLoc(
822
+ { kind: 'member', obj: expr, field },
823
+ this.getLocToken(expr) ?? this.tokens[this.pos - 1]
824
+ )
825
+ continue
826
+ }
827
+
828
+ break
829
+ }
830
+
831
+ return expr
832
+ }
833
+
834
+ private parseArgs(): Expr[] {
835
+ const args: Expr[] = []
836
+
837
+ if (!this.check(')')) {
838
+ do {
839
+ args.push(this.parseExpr())
840
+ } while (this.match(','))
841
+ }
842
+
843
+ return args
844
+ }
845
+
846
+ private parsePrimaryExpr(): Expr {
847
+ const token = this.peek()
848
+
849
+ if (token.kind === 'ident' && this.peek(1).kind === '=>') {
850
+ return this.parseSingleParamLambda()
851
+ }
852
+
853
+ // Integer literal
854
+ if (token.kind === 'int_lit') {
855
+ this.advance()
856
+ return this.withLoc({ kind: 'int_lit', value: parseInt(token.value, 10) }, token)
857
+ }
858
+
859
+ // Float literal
860
+ if (token.kind === 'float_lit') {
861
+ this.advance()
862
+ return this.withLoc({ kind: 'float_lit', value: parseFloat(token.value) }, token)
863
+ }
864
+
865
+ // NBT suffix literals
866
+ if (token.kind === 'byte_lit') {
867
+ this.advance()
868
+ return this.withLoc({ kind: 'byte_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
869
+ }
870
+ if (token.kind === 'short_lit') {
871
+ this.advance()
872
+ return this.withLoc({ kind: 'short_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
873
+ }
874
+ if (token.kind === 'long_lit') {
875
+ this.advance()
876
+ return this.withLoc({ kind: 'long_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
877
+ }
878
+ if (token.kind === 'double_lit') {
879
+ this.advance()
880
+ return this.withLoc({ kind: 'double_lit', value: parseFloat(token.value.slice(0, -1)) }, token)
881
+ }
882
+
883
+ // String literal
884
+ if (token.kind === 'string_lit') {
885
+ this.advance()
886
+ return this.parseStringExpr(token)
887
+ }
888
+
889
+ // MC name literal: #health → mc_name node (value = "health", without #)
890
+ if (token.kind === 'mc_name') {
891
+ this.advance()
892
+ return this.withLoc({ kind: 'mc_name', value: token.value.slice(1) }, token)
893
+ }
894
+
895
+ // Boolean literal
896
+ if (token.kind === 'true') {
897
+ this.advance()
898
+ return this.withLoc({ kind: 'bool_lit', value: true }, token)
899
+ }
900
+ if (token.kind === 'false') {
901
+ this.advance()
902
+ return this.withLoc({ kind: 'bool_lit', value: false }, token)
903
+ }
904
+
905
+ // Range literal
906
+ if (token.kind === 'range_lit') {
907
+ this.advance()
908
+ return this.withLoc({ kind: 'range_lit', range: this.parseRangeValue(token.value) }, token)
909
+ }
910
+
911
+ // Selector
912
+ if (token.kind === 'selector') {
913
+ this.advance()
914
+ return this.withLoc({
915
+ kind: 'selector',
916
+ raw: token.value,
917
+ isSingle: computeIsSingle(token.value),
918
+ sel: this.parseSelectorValue(token.value),
919
+ }, token)
920
+ }
921
+
922
+ // Identifier
923
+ if (token.kind === 'ident') {
924
+ this.advance()
925
+ return this.withLoc({ kind: 'ident', name: token.value }, token)
926
+ }
927
+
928
+ // Grouped expression
929
+ if (token.kind === '(') {
930
+ if (this.isBlockPosLiteral()) {
931
+ return this.parseBlockPos()
932
+ }
933
+ if (this.isLambdaStart()) {
934
+ return this.parseLambdaExpr()
935
+ }
936
+ this.advance()
937
+ const expr = this.parseExpr()
938
+ this.expect(')')
939
+ return expr
940
+ }
941
+
942
+ // Struct literal or block: { x: 10, y: 20 }
943
+ if (token.kind === '{') {
944
+ return this.parseStructLit()
945
+ }
946
+
947
+ // Array literal: [1, 2, 3] or []
948
+ if (token.kind === '[') {
949
+ return this.parseArrayLit()
950
+ }
951
+
952
+ this.error(`Unexpected token '${token.kind}'`)
953
+ }
954
+
955
+ private parseLiteralExpr(): LiteralExpr {
956
+ const expr = this.parsePrimaryExpr()
957
+ if (
958
+ expr.kind === 'int_lit' ||
959
+ expr.kind === 'float_lit' ||
960
+ expr.kind === 'bool_lit' ||
961
+ expr.kind === 'str_lit'
962
+ ) {
963
+ return expr
964
+ }
965
+ this.error('Const value must be a literal')
966
+ }
967
+
968
+ private parseSingleParamLambda(): Expr {
969
+ const paramToken = this.expect('ident')
970
+ const params: LambdaParam[] = [{ name: paramToken.value }]
971
+ this.expect('=>')
972
+ return this.finishLambdaExpr(params, paramToken)
973
+ }
974
+
975
+ private parseLambdaExpr(): Expr {
976
+ const openParenToken = this.expect('(')
977
+ const params: LambdaParam[] = []
978
+
979
+ if (!this.check(')')) {
980
+ do {
981
+ const name = this.expect('ident').value
982
+ let type: TypeNode | undefined
983
+ if (this.match(':')) {
984
+ type = this.parseType()
985
+ }
986
+ params.push({ name, type })
987
+ } while (this.match(','))
988
+ }
989
+
990
+ this.expect(')')
991
+ let returnType: TypeNode | undefined
992
+ if (this.match('->')) {
993
+ returnType = this.parseType()
994
+ }
995
+ this.expect('=>')
996
+ return this.finishLambdaExpr(params, openParenToken, returnType)
997
+ }
998
+
999
+ private finishLambdaExpr(params: LambdaParam[], token: Token, returnType?: TypeNode): Expr {
1000
+ const body = this.check('{') ? this.parseBlock() : this.parseExpr()
1001
+ return this.withLoc({ kind: 'lambda', params, returnType, body }, token)
1002
+ }
1003
+
1004
+ private parseStringExpr(token: Token): Expr {
1005
+ if (!token.value.includes('${')) {
1006
+ return this.withLoc({ kind: 'str_lit', value: token.value }, token)
1007
+ }
1008
+
1009
+ const parts: Array<string | Expr> = []
1010
+ let current = ''
1011
+ let index = 0
1012
+
1013
+ while (index < token.value.length) {
1014
+ if (token.value[index] === '$' && token.value[index + 1] === '{') {
1015
+ if (current) {
1016
+ parts.push(current)
1017
+ current = ''
1018
+ }
1019
+
1020
+ index += 2
1021
+ let depth = 1
1022
+ let exprSource = ''
1023
+ let inString = false
1024
+
1025
+ while (index < token.value.length && depth > 0) {
1026
+ const char = token.value[index]
1027
+
1028
+ if (char === '"' && token.value[index - 1] !== '\\') {
1029
+ inString = !inString
1030
+ }
1031
+
1032
+ if (!inString) {
1033
+ if (char === '{') {
1034
+ depth++
1035
+ } else if (char === '}') {
1036
+ depth--
1037
+ if (depth === 0) {
1038
+ index++
1039
+ break
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ if (depth > 0) {
1045
+ exprSource += char
1046
+ }
1047
+ index++
1048
+ }
1049
+
1050
+ if (depth !== 0) {
1051
+ this.error('Unterminated string interpolation')
1052
+ }
1053
+
1054
+ parts.push(this.parseEmbeddedExpr(exprSource))
1055
+ continue
1056
+ }
1057
+
1058
+ current += token.value[index]
1059
+ index++
1060
+ }
1061
+
1062
+ if (current) {
1063
+ parts.push(current)
1064
+ }
1065
+
1066
+ return this.withLoc({ kind: 'str_interp', parts }, token)
1067
+ }
1068
+
1069
+ private parseEmbeddedExpr(source: string): Expr {
1070
+ const tokens = new Lexer(source, this.filePath).tokenize()
1071
+ const parser = new Parser(tokens, source, this.filePath)
1072
+ const expr = parser.parseExpr()
1073
+
1074
+ if (!parser.check('eof')) {
1075
+ parser.error(`Unexpected token '${parser.peek().kind}' in string interpolation`)
1076
+ }
1077
+
1078
+ return expr
1079
+ }
1080
+
1081
+ private parseStructLit(): Expr {
1082
+ const braceToken = this.expect('{')
1083
+ const fields: { name: string; value: Expr }[] = []
1084
+
1085
+ if (!this.check('}')) {
1086
+ do {
1087
+ const name = this.expect('ident').value
1088
+ this.expect(':')
1089
+ const value = this.parseExpr()
1090
+ fields.push({ name, value })
1091
+ } while (this.match(','))
1092
+ }
1093
+
1094
+ this.expect('}')
1095
+ return this.withLoc({ kind: 'struct_lit', fields }, braceToken)
1096
+ }
1097
+
1098
+ private parseArrayLit(): Expr {
1099
+ const bracketToken = this.expect('[')
1100
+ const elements: Expr[] = []
1101
+
1102
+ if (!this.check(']')) {
1103
+ do {
1104
+ elements.push(this.parseExpr())
1105
+ } while (this.match(','))
1106
+ }
1107
+
1108
+ this.expect(']')
1109
+ return this.withLoc({ kind: 'array_lit', elements }, bracketToken)
1110
+ }
1111
+
1112
+ private isLambdaStart(): boolean {
1113
+ if (!this.check('(')) return false
1114
+
1115
+ let offset = 1
1116
+ if (this.peek(offset).kind !== ')') {
1117
+ while (true) {
1118
+ if (this.peek(offset).kind !== 'ident') {
1119
+ return false
1120
+ }
1121
+ offset += 1
1122
+
1123
+ if (this.peek(offset).kind === ':') {
1124
+ offset += 1
1125
+ const consumed = this.typeTokenLength(offset)
1126
+ if (consumed === 0) {
1127
+ return false
1128
+ }
1129
+ offset += consumed
1130
+ }
1131
+
1132
+ if (this.peek(offset).kind === ',') {
1133
+ offset += 1
1134
+ continue
1135
+ }
1136
+ break
1137
+ }
1138
+ }
1139
+
1140
+ if (this.peek(offset).kind !== ')') {
1141
+ return false
1142
+ }
1143
+ offset += 1
1144
+
1145
+ if (this.peek(offset).kind === '=>') {
1146
+ return true
1147
+ }
1148
+
1149
+ if (this.peek(offset).kind === '->') {
1150
+ offset += 1
1151
+ const consumed = this.typeTokenLength(offset)
1152
+ if (consumed === 0) {
1153
+ return false
1154
+ }
1155
+ offset += consumed
1156
+ return this.peek(offset).kind === '=>'
1157
+ }
1158
+
1159
+ return false
1160
+ }
1161
+
1162
+ private typeTokenLength(offset: number): number {
1163
+ const token = this.peek(offset)
1164
+
1165
+ if (token.kind === '(') {
1166
+ let inner = offset + 1
1167
+ if (this.peek(inner).kind !== ')') {
1168
+ while (true) {
1169
+ const consumed = this.typeTokenLength(inner)
1170
+ if (consumed === 0) {
1171
+ return 0
1172
+ }
1173
+ inner += consumed
1174
+ if (this.peek(inner).kind === ',') {
1175
+ inner += 1
1176
+ continue
1177
+ }
1178
+ break
1179
+ }
1180
+ }
1181
+
1182
+ if (this.peek(inner).kind !== ')') {
1183
+ return 0
1184
+ }
1185
+ inner += 1
1186
+
1187
+ if (this.peek(inner).kind !== '->') {
1188
+ return 0
1189
+ }
1190
+ inner += 1
1191
+ const returnLen = this.typeTokenLength(inner)
1192
+ return returnLen === 0 ? 0 : inner + returnLen - offset
1193
+ }
1194
+
1195
+ const isNamedType =
1196
+ token.kind === 'int' ||
1197
+ token.kind === 'bool' ||
1198
+ token.kind === 'float' ||
1199
+ token.kind === 'string' ||
1200
+ token.kind === 'void' ||
1201
+ token.kind === 'BlockPos' ||
1202
+ token.kind === 'ident'
1203
+ if (!isNamedType) {
1204
+ return 0
1205
+ }
1206
+
1207
+ let length = 1
1208
+ while (this.peek(offset + length).kind === '[' && this.peek(offset + length + 1).kind === ']') {
1209
+ length += 2
1210
+ }
1211
+ return length
1212
+ }
1213
+
1214
+ private isBlockPosLiteral(): boolean {
1215
+ if (!this.check('(')) return false
1216
+
1217
+ let offset = 1
1218
+ for (let i = 0; i < 3; i++) {
1219
+ const consumed = this.coordComponentTokenLength(offset)
1220
+ if (consumed === 0) return false
1221
+ offset += consumed
1222
+
1223
+ if (i < 2) {
1224
+ if (this.peek(offset).kind !== ',') return false
1225
+ offset += 1
1226
+ }
1227
+ }
1228
+
1229
+ return this.peek(offset).kind === ')'
1230
+ }
1231
+
1232
+ private coordComponentTokenLength(offset: number): number {
1233
+ const token = this.peek(offset)
1234
+
1235
+ if (token.kind === 'int_lit') {
1236
+ return 1
1237
+ }
1238
+
1239
+ if (token.kind === '-') {
1240
+ return this.peek(offset + 1).kind === 'int_lit' ? 2 : 0
1241
+ }
1242
+
1243
+ if (token.kind !== '~' && token.kind !== '^') {
1244
+ return 0
1245
+ }
1246
+
1247
+ const next = this.peek(offset + 1)
1248
+ if (next.kind === ',' || next.kind === ')') {
1249
+ return 1
1250
+ }
1251
+ if (next.kind === 'int_lit') {
1252
+ return 2
1253
+ }
1254
+ if (next.kind === '-' && this.peek(offset + 2).kind === 'int_lit') {
1255
+ return 3
1256
+ }
1257
+ return 0
1258
+ }
1259
+
1260
+ private parseBlockPos(): BlockPosExpr {
1261
+ const openParenToken = this.expect('(')
1262
+ const x = this.parseCoordComponent()
1263
+ this.expect(',')
1264
+ const y = this.parseCoordComponent()
1265
+ this.expect(',')
1266
+ const z = this.parseCoordComponent()
1267
+ this.expect(')')
1268
+ return this.withLoc({ kind: 'blockpos', x, y, z }, openParenToken)
1269
+ }
1270
+
1271
+ private parseCoordComponent(): CoordComponent {
1272
+ const token = this.peek()
1273
+
1274
+ if (token.kind === '~' || token.kind === '^') {
1275
+ this.advance()
1276
+ const offset = this.parseSignedCoordOffset()
1277
+ return token.kind === '~'
1278
+ ? { kind: 'relative', offset }
1279
+ : { kind: 'local', offset }
1280
+ }
1281
+
1282
+ return { kind: 'absolute', value: this.parseSignedCoordOffset(true) }
1283
+ }
1284
+
1285
+ private parseSignedCoordOffset(requireValue = false): number {
1286
+ let sign = 1
1287
+ if (this.match('-')) {
1288
+ sign = -1
1289
+ }
1290
+
1291
+ if (this.check('int_lit')) {
1292
+ return sign * parseInt(this.advance().value, 10)
1293
+ }
1294
+
1295
+ if (requireValue) {
1296
+ this.error('Expected integer coordinate component')
1297
+ }
1298
+
1299
+ return 0
1300
+ }
1301
+
1302
+ // -------------------------------------------------------------------------
1303
+ // Selector Parsing
1304
+ // -------------------------------------------------------------------------
1305
+
1306
+ private parseSelector(): EntitySelector {
1307
+ const token = this.expect('selector')
1308
+ return this.parseSelectorValue(token.value)
1309
+ }
1310
+
1311
+ private parseSelectorValue(value: string): EntitySelector {
1312
+ // Parse @e[type=zombie, distance=..5]
1313
+ const bracketIndex = value.indexOf('[')
1314
+ if (bracketIndex === -1) {
1315
+ return { kind: value as SelectorKind }
1316
+ }
1317
+
1318
+ const kind = value.slice(0, bracketIndex) as SelectorKind
1319
+ const paramsStr = value.slice(bracketIndex + 1, -1) // Remove [ and ]
1320
+ const filters = this.parseSelectorFilters(paramsStr)
1321
+
1322
+ return { kind, filters }
1323
+ }
1324
+
1325
+ private parseSelectorFilters(paramsStr: string): SelectorFilter {
1326
+ const filters: SelectorFilter = {}
1327
+ const parts = this.splitSelectorParams(paramsStr)
1328
+
1329
+ for (const part of parts) {
1330
+ const eqIndex = part.indexOf('=')
1331
+ if (eqIndex === -1) continue
1332
+
1333
+ const key = part.slice(0, eqIndex).trim()
1334
+ const val = part.slice(eqIndex + 1).trim()
1335
+
1336
+ switch (key) {
1337
+ case 'type':
1338
+ filters.type = val
1339
+ break
1340
+ case 'distance':
1341
+ filters.distance = this.parseRangeValue(val)
1342
+ break
1343
+ case 'tag':
1344
+ if (val.startsWith('!')) {
1345
+ filters.notTag = filters.notTag ?? []
1346
+ filters.notTag.push(val.slice(1))
1347
+ } else {
1348
+ filters.tag = filters.tag ?? []
1349
+ filters.tag.push(val)
1350
+ }
1351
+ break
1352
+ case 'limit':
1353
+ filters.limit = parseInt(val, 10)
1354
+ break
1355
+ case 'sort':
1356
+ filters.sort = val as SelectorFilter['sort']
1357
+ break
1358
+ case 'nbt':
1359
+ filters.nbt = val
1360
+ break
1361
+ case 'gamemode':
1362
+ filters.gamemode = val
1363
+ break
1364
+ case 'scores':
1365
+ filters.scores = this.parseScoresFilter(val)
1366
+ break
1367
+ }
1368
+ }
1369
+
1370
+ return filters
1371
+ }
1372
+
1373
+ private splitSelectorParams(str: string): string[] {
1374
+ const parts: string[] = []
1375
+ let current = ''
1376
+ let depth = 0
1377
+
1378
+ for (const char of str) {
1379
+ if (char === '{' || char === '[') depth++
1380
+ else if (char === '}' || char === ']') depth--
1381
+ else if (char === ',' && depth === 0) {
1382
+ parts.push(current.trim())
1383
+ current = ''
1384
+ continue
1385
+ }
1386
+ current += char
1387
+ }
1388
+
1389
+ if (current.trim()) {
1390
+ parts.push(current.trim())
1391
+ }
1392
+
1393
+ return parts
1394
+ }
1395
+
1396
+ private parseScoresFilter(val: string): Record<string, RangeExpr> {
1397
+ // Parse {kills=1.., deaths=..5}
1398
+ const scores: Record<string, RangeExpr> = {}
1399
+ const inner = val.slice(1, -1) // Remove { and }
1400
+ const parts = inner.split(',')
1401
+
1402
+ for (const part of parts) {
1403
+ const [name, range] = part.split('=').map(s => s.trim())
1404
+ scores[name] = this.parseRangeValue(range)
1405
+ }
1406
+
1407
+ return scores
1408
+ }
1409
+
1410
+ private parseRangeValue(value: string): RangeExpr {
1411
+ // ..5 → { max: 5 }
1412
+ // 1.. → { min: 1 }
1413
+ // 1..10 → { min: 1, max: 10 }
1414
+ // 5 → { min: 5, max: 5 } (exact match)
1415
+
1416
+ if (value.startsWith('..')) {
1417
+ const max = parseInt(value.slice(2), 10)
1418
+ return { max }
1419
+ }
1420
+
1421
+ if (value.endsWith('..')) {
1422
+ const min = parseInt(value.slice(0, -2), 10)
1423
+ return { min }
1424
+ }
1425
+
1426
+ const dotIndex = value.indexOf('..')
1427
+ if (dotIndex !== -1) {
1428
+ const min = parseInt(value.slice(0, dotIndex), 10)
1429
+ const max = parseInt(value.slice(dotIndex + 2), 10)
1430
+ return { min, max }
1431
+ }
1432
+
1433
+ // Exact value
1434
+ const val = parseInt(value, 10)
1435
+ return { min: val, max: val }
1436
+ }
1437
+ }