redscript-mc 3.0.1 → 3.0.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 (225) hide show
  1. package/.github/workflows/ci.yml +1 -0
  2. package/README.md +119 -313
  3. package/README.zh.md +118 -314
  4. package/ROADMAP.md +5 -5
  5. package/dist/data/impl_test/function/counter/get.mcfunction +5 -0
  6. package/dist/data/impl_test/function/counter/inc.mcfunction +7 -0
  7. package/dist/data/impl_test/function/counter/new.mcfunction +4 -0
  8. package/dist/data/impl_test/function/load.mcfunction +1 -0
  9. package/dist/data/impl_test/function/test_impl.mcfunction +10 -0
  10. package/dist/data/minecraft/tags/function/load.json +5 -0
  11. package/dist/data/playground/function/load.mcfunction +1 -0
  12. package/dist/data/playground/function/start.mcfunction +4 -0
  13. package/dist/data/playground/function/start__say_macro_t1.mcfunction +1 -0
  14. package/dist/data/playground/function/stop.mcfunction +5 -0
  15. package/dist/data/playground/function/stop__say_macro_t0.mcfunction +1 -0
  16. package/dist/data/stdlib_queue8_test/function/__queue_append_apply.mcfunction +4 -0
  17. package/dist/data/stdlib_queue8_test/function/__queue_peek_apply.mcfunction +4 -0
  18. package/dist/data/stdlib_queue8_test/function/__queue_size_raw_apply.mcfunction +4 -0
  19. package/dist/data/stdlib_queue8_test/function/load.mcfunction +1 -0
  20. package/dist/data/stdlib_queue8_test/function/queue_clear.mcfunction +6 -0
  21. package/dist/data/stdlib_queue8_test/function/queue_empty__merge_1.mcfunction +5 -0
  22. package/dist/data/stdlib_queue8_test/function/queue_empty__then_0.mcfunction +5 -0
  23. package/dist/data/stdlib_queue8_test/function/queue_peek__merge_1.mcfunction +13 -0
  24. package/dist/data/stdlib_queue8_test/function/queue_peek__then_0.mcfunction +5 -0
  25. package/dist/data/stdlib_queue8_test/function/queue_pop__merge_1.mcfunction +15 -0
  26. package/dist/data/stdlib_queue8_test/function/queue_pop__then_0.mcfunction +5 -0
  27. package/dist/data/stdlib_queue8_test/function/queue_push__const_11.mcfunction +6 -0
  28. package/dist/data/stdlib_queue8_test/function/queue_push__const_22.mcfunction +6 -0
  29. package/dist/data/stdlib_queue8_test/function/queue_size.mcfunction +13 -0
  30. package/dist/data/stdlib_queue8_test/function/test_queue_push_and_size.mcfunction +13 -0
  31. package/dist/data/test/function/load.mcfunction +1 -0
  32. package/dist/data/test/function/say_at.mcfunction +6 -0
  33. package/dist/data/test/function/test.mcfunction +4 -0
  34. package/dist/pack.mcmeta +6 -0
  35. package/dist/package.json +1 -1
  36. package/dist/src/__tests__/formatter-extra.test.d.ts +7 -0
  37. package/dist/src/__tests__/formatter-extra.test.js +123 -0
  38. package/dist/src/__tests__/global-vars.test.d.ts +13 -0
  39. package/dist/src/__tests__/global-vars.test.js +156 -0
  40. package/dist/src/__tests__/lint/new-rules.test.d.ts +9 -0
  41. package/dist/src/__tests__/lint/new-rules.test.js +402 -0
  42. package/dist/src/__tests__/lsp-rename.test.d.ts +8 -0
  43. package/dist/src/__tests__/lsp-rename.test.js +157 -0
  44. package/dist/src/__tests__/mc-integration/say-fstring.test.d.ts +11 -0
  45. package/dist/src/__tests__/mc-integration/say-fstring.test.js +220 -0
  46. package/dist/src/__tests__/mc-integration/stdlib-coverage-2.test.js +1 -1
  47. package/dist/src/__tests__/mc-integration/stdlib-coverage-3.test.js +1 -1
  48. package/dist/src/__tests__/mc-integration/stdlib-coverage-4.test.js +1 -1
  49. package/dist/src/__tests__/mc-integration/stdlib-coverage-5.test.js +1 -1
  50. package/dist/src/__tests__/mc-integration/stdlib-coverage-6.test.js +1 -1
  51. package/dist/src/__tests__/mc-integration/stdlib-coverage-7.test.js +1 -1
  52. package/dist/src/__tests__/mc-integration/stdlib-coverage-8.test.js +1 -1
  53. package/dist/src/__tests__/mc-syntax.test.js +4 -1
  54. package/dist/src/__tests__/monomorphize-coverage.test.d.ts +9 -0
  55. package/dist/src/__tests__/monomorphize-coverage.test.js +204 -0
  56. package/dist/src/__tests__/optimizer-cse.test.d.ts +7 -0
  57. package/dist/src/__tests__/optimizer-cse.test.js +226 -0
  58. package/dist/src/__tests__/parser.test.js +4 -13
  59. package/dist/src/__tests__/repl-server-extra.test.js +6 -7
  60. package/dist/src/__tests__/repl-server.test.js +5 -7
  61. package/dist/src/__tests__/stdlib/queue.test.js +6 -6
  62. package/dist/src/cli.js +0 -0
  63. package/dist/src/lexer/index.js +2 -1
  64. package/dist/src/lint/index.d.ts +12 -5
  65. package/dist/src/lint/index.js +730 -5
  66. package/dist/src/lsp/main.js +0 -0
  67. package/dist/src/mc-test/client.d.ts +21 -0
  68. package/dist/src/mc-test/client.js +34 -0
  69. package/dist/src/mir/lower.js +108 -6
  70. package/dist/src/optimizer/interprocedural.js +37 -2
  71. package/dist/src/parser/decl-parser.d.ts +19 -0
  72. package/dist/src/parser/decl-parser.js +323 -0
  73. package/dist/src/parser/expr-parser.d.ts +46 -0
  74. package/dist/src/parser/expr-parser.js +759 -0
  75. package/dist/src/parser/index.d.ts +8 -129
  76. package/dist/src/parser/index.js +13 -2262
  77. package/dist/src/parser/stmt-parser.d.ts +28 -0
  78. package/dist/src/parser/stmt-parser.js +577 -0
  79. package/dist/src/parser/type-parser.d.ts +20 -0
  80. package/dist/src/parser/type-parser.js +257 -0
  81. package/dist/src/parser/utils.d.ts +34 -0
  82. package/dist/src/parser/utils.js +141 -0
  83. package/docs/dev/README-mc-integration-tests.md +141 -0
  84. package/docs/lint-rules.md +162 -0
  85. package/docs/stdlib/bigint.md +2 -0
  86. package/editors/vscode/README.md +63 -41
  87. package/editors/vscode/out/extension.js +1881 -1776
  88. package/editors/vscode/out/lsp-server.js +4257 -3651
  89. package/editors/vscode/package-lock.json +3 -3
  90. package/editors/vscode/package.json +1 -1
  91. package/examples/loops-demo.mcrs +87 -0
  92. package/package.json +1 -1
  93. package/redscript-docs/docs/en/stdlib/advanced.md +629 -0
  94. package/redscript-docs/docs/en/stdlib/bigint.md +316 -0
  95. package/redscript-docs/docs/en/stdlib/bits.md +292 -0
  96. package/redscript-docs/docs/en/stdlib/bossbar.md +177 -0
  97. package/redscript-docs/docs/en/stdlib/calculus.md +289 -0
  98. package/redscript-docs/docs/en/stdlib/color.md +353 -0
  99. package/redscript-docs/docs/en/stdlib/combat.md +88 -0
  100. package/redscript-docs/docs/en/stdlib/cooldown.md +82 -0
  101. package/redscript-docs/docs/en/stdlib/dialog.md +155 -0
  102. package/redscript-docs/docs/en/stdlib/easing.md +558 -0
  103. package/redscript-docs/docs/en/stdlib/ecs.md +475 -0
  104. package/redscript-docs/docs/en/stdlib/effects.md +324 -0
  105. package/redscript-docs/docs/en/stdlib/events.md +3 -0
  106. package/redscript-docs/docs/en/stdlib/expr.md +45 -0
  107. package/redscript-docs/docs/en/stdlib/fft.md +141 -0
  108. package/redscript-docs/docs/en/stdlib/geometry.md +430 -0
  109. package/redscript-docs/docs/en/stdlib/graph.md +259 -0
  110. package/redscript-docs/docs/en/stdlib/heap.md +185 -0
  111. package/redscript-docs/docs/en/stdlib/interactions.md +179 -0
  112. package/redscript-docs/docs/en/stdlib/inventory.md +97 -0
  113. package/redscript-docs/docs/en/stdlib/linalg.md +557 -0
  114. package/redscript-docs/docs/en/stdlib/list.md +559 -0
  115. package/redscript-docs/docs/en/stdlib/map.md +140 -0
  116. package/redscript-docs/docs/en/stdlib/math.md +193 -0
  117. package/redscript-docs/docs/en/stdlib/math_hp.md +149 -0
  118. package/redscript-docs/docs/en/stdlib/matrix.md +403 -0
  119. package/redscript-docs/docs/en/stdlib/mobs.md +965 -0
  120. package/redscript-docs/docs/en/stdlib/noise.md +244 -0
  121. package/redscript-docs/docs/en/stdlib/ode.md +253 -0
  122. package/redscript-docs/docs/en/stdlib/parabola.md +342 -0
  123. package/redscript-docs/docs/en/stdlib/particles.md +311 -0
  124. package/redscript-docs/docs/en/stdlib/pathfind.md +255 -0
  125. package/redscript-docs/docs/en/stdlib/physics.md +493 -0
  126. package/redscript-docs/docs/en/stdlib/player.md +78 -0
  127. package/redscript-docs/docs/en/stdlib/quaternion.md +673 -0
  128. package/redscript-docs/docs/en/stdlib/queue.md +134 -0
  129. package/redscript-docs/docs/en/stdlib/random.md +223 -0
  130. package/redscript-docs/docs/en/stdlib/result.md +143 -0
  131. package/redscript-docs/docs/en/stdlib/scheduler.md +183 -0
  132. package/redscript-docs/docs/en/stdlib/set_int.md +190 -0
  133. package/redscript-docs/docs/en/stdlib/sets.md +101 -0
  134. package/redscript-docs/docs/en/stdlib/signal.md +400 -0
  135. package/redscript-docs/docs/en/stdlib/sort.md +104 -0
  136. package/redscript-docs/docs/en/stdlib/spawn.md +147 -0
  137. package/redscript-docs/docs/en/stdlib/state.md +142 -0
  138. package/redscript-docs/docs/en/stdlib/strings.md +154 -0
  139. package/redscript-docs/docs/en/stdlib/tags.md +3451 -0
  140. package/redscript-docs/docs/en/stdlib/teams.md +153 -0
  141. package/redscript-docs/docs/en/stdlib/timer.md +246 -0
  142. package/redscript-docs/docs/en/stdlib/vec.md +158 -0
  143. package/redscript-docs/docs/en/stdlib/world.md +298 -0
  144. package/redscript-docs/docs/zh/stdlib/advanced.md +615 -0
  145. package/redscript-docs/docs/zh/stdlib/bigint.md +316 -0
  146. package/redscript-docs/docs/zh/stdlib/bits.md +292 -0
  147. package/redscript-docs/docs/zh/stdlib/bossbar.md +170 -0
  148. package/redscript-docs/docs/zh/stdlib/calculus.md +287 -0
  149. package/redscript-docs/docs/zh/stdlib/color.md +353 -0
  150. package/redscript-docs/docs/zh/stdlib/combat.md +88 -0
  151. package/redscript-docs/docs/zh/stdlib/cooldown.md +84 -0
  152. package/redscript-docs/docs/zh/stdlib/dialog.md +152 -0
  153. package/redscript-docs/docs/zh/stdlib/easing.md +558 -0
  154. package/redscript-docs/docs/zh/stdlib/ecs.md +472 -0
  155. package/redscript-docs/docs/zh/stdlib/effects.md +324 -0
  156. package/redscript-docs/docs/zh/stdlib/events.md +3 -0
  157. package/redscript-docs/docs/zh/stdlib/expr.md +37 -0
  158. package/redscript-docs/docs/zh/stdlib/fft.md +128 -0
  159. package/redscript-docs/docs/zh/stdlib/geometry.md +430 -0
  160. package/redscript-docs/docs/zh/stdlib/graph.md +259 -0
  161. package/redscript-docs/docs/zh/stdlib/heap.md +185 -0
  162. package/redscript-docs/docs/zh/stdlib/interactions.md +160 -0
  163. package/redscript-docs/docs/zh/stdlib/inventory.md +94 -0
  164. package/redscript-docs/docs/zh/stdlib/linalg.md +543 -0
  165. package/redscript-docs/docs/zh/stdlib/list.md +561 -0
  166. package/redscript-docs/docs/zh/stdlib/map.md +132 -0
  167. package/redscript-docs/docs/zh/stdlib/math.md +193 -0
  168. package/redscript-docs/docs/zh/stdlib/math_hp.md +143 -0
  169. package/redscript-docs/docs/zh/stdlib/matrix.md +396 -0
  170. package/redscript-docs/docs/zh/stdlib/mobs.md +965 -0
  171. package/redscript-docs/docs/zh/stdlib/noise.md +244 -0
  172. package/redscript-docs/docs/zh/stdlib/ode.md +243 -0
  173. package/redscript-docs/docs/zh/stdlib/parabola.md +337 -0
  174. package/redscript-docs/docs/zh/stdlib/particles.md +307 -0
  175. package/redscript-docs/docs/zh/stdlib/pathfind.md +255 -0
  176. package/redscript-docs/docs/zh/stdlib/physics.md +493 -0
  177. package/redscript-docs/docs/zh/stdlib/player.md +78 -0
  178. package/redscript-docs/docs/zh/stdlib/quaternion.md +669 -0
  179. package/redscript-docs/docs/zh/stdlib/queue.md +124 -0
  180. package/redscript-docs/docs/zh/stdlib/random.md +222 -0
  181. package/redscript-docs/docs/zh/stdlib/result.md +147 -0
  182. package/redscript-docs/docs/zh/stdlib/scheduler.md +173 -0
  183. package/redscript-docs/docs/zh/stdlib/set_int.md +180 -0
  184. package/redscript-docs/docs/zh/stdlib/sets.md +107 -0
  185. package/redscript-docs/docs/zh/stdlib/signal.md +373 -0
  186. package/redscript-docs/docs/zh/stdlib/sort.md +104 -0
  187. package/redscript-docs/docs/zh/stdlib/spawn.md +142 -0
  188. package/redscript-docs/docs/zh/stdlib/state.md +134 -0
  189. package/redscript-docs/docs/zh/stdlib/strings.md +107 -0
  190. package/redscript-docs/docs/zh/stdlib/tags.md +3451 -0
  191. package/redscript-docs/docs/zh/stdlib/teams.md +150 -0
  192. package/redscript-docs/docs/zh/stdlib/timer.md +254 -0
  193. package/redscript-docs/docs/zh/stdlib/vec.md +158 -0
  194. package/redscript-docs/docs/zh/stdlib/world.md +289 -0
  195. package/src/__tests__/formatter-extra.test.ts +139 -0
  196. package/src/__tests__/global-vars.test.ts +171 -0
  197. package/src/__tests__/lint/new-rules.test.ts +437 -0
  198. package/src/__tests__/lsp-rename.test.ts +171 -0
  199. package/src/__tests__/mc-integration/say-fstring.test.ts +211 -0
  200. package/src/__tests__/mc-integration/stdlib-coverage-2.test.ts +1 -1
  201. package/src/__tests__/mc-integration/stdlib-coverage-3.test.ts +1 -1
  202. package/src/__tests__/mc-integration/stdlib-coverage-4.test.ts +1 -1
  203. package/src/__tests__/mc-integration/stdlib-coverage-5.test.ts +1 -1
  204. package/src/__tests__/mc-integration/stdlib-coverage-6.test.ts +1 -1
  205. package/src/__tests__/mc-integration/stdlib-coverage-7.test.ts +1 -1
  206. package/src/__tests__/mc-integration/stdlib-coverage-8.test.ts +1 -1
  207. package/src/__tests__/mc-syntax.test.ts +3 -0
  208. package/src/__tests__/monomorphize-coverage.test.ts +220 -0
  209. package/src/__tests__/optimizer-cse.test.ts +250 -0
  210. package/src/__tests__/parser.test.ts +4 -13
  211. package/src/__tests__/repl-server-extra.test.ts +6 -6
  212. package/src/__tests__/repl-server.test.ts +5 -6
  213. package/src/__tests__/stdlib/queue.test.ts +6 -6
  214. package/src/lexer/index.ts +2 -1
  215. package/src/lint/index.ts +713 -5
  216. package/src/mc-test/client.ts +40 -0
  217. package/src/mir/lower.ts +111 -2
  218. package/src/optimizer/interprocedural.ts +40 -2
  219. package/src/parser/decl-parser.ts +349 -0
  220. package/src/parser/expr-parser.ts +838 -0
  221. package/src/parser/index.ts +17 -2558
  222. package/src/parser/stmt-parser.ts +585 -0
  223. package/src/parser/type-parser.ts +276 -0
  224. package/src/parser/utils.ts +173 -0
  225. package/src/stdlib/queue.mcrs +19 -6
@@ -3,220 +3,25 @@
3
3
  *
4
4
  * Recursive descent parser that converts tokens into an AST.
5
5
  * Uses precedence climbing for expression parsing.
6
+ *
7
+ * The Parser class extends a chain of sub-parsers:
8
+ * Parser → DeclParser → StmtParser → ExprParser → TypeParser → ParserBase
9
+ *
10
+ * Each layer adds methods for its domain; the full Parser assembles them
11
+ * into the top-level `parse()` entry point.
6
12
  */
7
13
 
8
- import { Lexer, type Token, type TokenKind } from '../lexer'
14
+ import { DiagnosticError } from '../diagnostics'
9
15
  import type {
10
- Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, LiteralExpr, Param,
11
- Program, RangeExpr, SelectorFilter, SelectorKind, Span, Stmt, TypeNode, AssignOp,
12
- StructDecl, StructField, ExecuteSubcommand, EnumDecl, EnumVariant, BlockPosExpr, ImplBlock,
13
- CoordComponent, LambdaParam, EntityTypeName, ImportDecl, MatchPattern,
14
- InterfaceDecl, InterfaceMethod
16
+ Program, FnDecl, GlobalDecl, StructDecl, ImplBlock, EnumDecl,
17
+ ConstDecl, ImportDecl,
18
+ InterfaceDecl,
15
19
  } from '../ast/types'
16
- import type { BinOp, CmpOp } from '../ast/types'
17
- import { DiagnosticError } from '../diagnostics'
18
-
19
- // ---------------------------------------------------------------------------
20
- // Operator Precedence (higher = binds tighter)
21
- // ---------------------------------------------------------------------------
22
-
23
- const PRECEDENCE: Record<string, number> = {
24
- '||': 1,
25
- '&&': 2,
26
- '==': 3, '!=': 3,
27
- '<': 4, '<=': 4, '>': 4, '>=': 4, 'is': 4,
28
- '+': 5, '-': 5,
29
- '*': 6, '/': 6, '%': 6,
30
- }
31
-
32
- const BINARY_OPS = new Set(['||', '&&', '==', '!=', '<', '<=', '>', '>=', 'is', '+', '-', '*', '/', '%'])
33
-
34
- const ENTITY_TYPE_NAMES = new Set<EntityTypeName>([
35
- 'entity',
36
- 'Player',
37
- 'Mob',
38
- 'HostileMob',
39
- 'PassiveMob',
40
- 'Zombie',
41
- 'Skeleton',
42
- 'Creeper',
43
- 'Spider',
44
- 'Enderman',
45
- 'Blaze',
46
- 'Witch',
47
- 'Slime',
48
- 'ZombieVillager',
49
- 'Husk',
50
- 'Drowned',
51
- 'Stray',
52
- 'WitherSkeleton',
53
- 'CaveSpider',
54
- 'Pig',
55
- 'Cow',
56
- 'Sheep',
57
- 'Chicken',
58
- 'Villager',
59
- 'WanderingTrader',
60
- 'ArmorStand',
61
- 'Item',
62
- 'Arrow',
63
- ])
64
-
65
- function computeIsSingle(raw: string): boolean {
66
- if (/^@[spr](\[|$)/.test(raw)) return true
67
- if (/[\[,\s]limit=1[,\]\s]/.test(raw)) return true
68
- return false
69
- }
70
-
71
- // ---------------------------------------------------------------------------
72
- // Parser Class
73
- // ---------------------------------------------------------------------------
74
-
75
- export class Parser {
76
- private tokens: Token[]
77
- private pos: number = 0
78
- private sourceLines: string[]
79
- private filePath?: string
80
- /** Set to true once `module library;` is seen — all subsequent fn declarations
81
- * will be marked isLibraryFn=true. When library sources are parsed via the
82
- * `librarySources` compile option, each source is parsed by its own fresh
83
- * Parser instance, so this flag never bleeds into user code. */
84
- private inLibraryMode: boolean = false
85
- /** Warnings accumulated during parsing (e.g. deprecated keyword usage). */
86
- readonly warnings: string[] = []
87
- /** Parse errors collected during error-recovery mode. */
88
- readonly parseErrors: DiagnosticError[] = []
89
-
90
- constructor(tokens: Token[], source?: string, filePath?: string) {
91
- this.tokens = tokens
92
- this.sourceLines = source?.split('\n') ?? []
93
- this.filePath = filePath
94
- }
95
-
96
- // -------------------------------------------------------------------------
97
- // Utilities
98
- // -------------------------------------------------------------------------
99
-
100
- private peek(offset = 0): Token {
101
- const idx = this.pos + offset
102
- if (idx >= this.tokens.length) {
103
- return this.tokens[this.tokens.length - 1] // eof
104
- }
105
- return this.tokens[idx]
106
- }
107
-
108
- private advance(): Token {
109
- const token = this.tokens[this.pos]
110
- if (token.kind !== 'eof') this.pos++
111
- return token
112
- }
113
-
114
- private check(kind: TokenKind): boolean {
115
- return this.peek().kind === kind
116
- }
117
-
118
- private match(...kinds: TokenKind[]): boolean {
119
- for (const kind of kinds) {
120
- if (this.check(kind)) {
121
- this.advance()
122
- return true
123
- }
124
- }
125
- return false
126
- }
127
-
128
- private expect(kind: TokenKind): Token {
129
- const token = this.peek()
130
- if (token.kind !== kind) {
131
- throw new DiagnosticError(
132
- 'ParseError',
133
- `Expected '${kind}' but got '${token.kind}'`,
134
- { file: this.filePath, line: token.line, col: token.col },
135
- this.sourceLines
136
- )
137
- }
138
- return this.advance()
139
- }
140
-
141
- private error(message: string): never {
142
- const token = this.peek()
143
- throw new DiagnosticError(
144
- 'ParseError',
145
- message,
146
- { file: this.filePath, line: token.line, col: token.col },
147
- this.sourceLines
148
- )
149
- }
150
-
151
- private withLoc<T extends object>(node: T, token: Token): T {
152
- const span: Span = { line: token.line, col: token.col }
153
- Object.defineProperty(node, 'span', {
154
- value: span,
155
- enumerable: false,
156
- configurable: true,
157
- writable: true,
158
- })
159
- return node
160
- }
161
-
162
- private getLocToken(node: object): Token | null {
163
- const span = (node as { span?: Span }).span
164
- if (!span) {
165
- return null
166
- }
167
- return { kind: 'eof', value: '', line: span.line, col: span.col }
168
- }
169
-
170
- // -------------------------------------------------------------------------
171
- // Error Recovery
172
- // -------------------------------------------------------------------------
173
-
174
- /**
175
- * Synchronize to the next top-level declaration boundary after a parse error.
176
- * Skips tokens until we find a keyword that starts a top-level declaration,
177
- * or a `}` (end of a block), or EOF.
178
- */
179
- private syncToNextDecl(): void {
180
- const TOP_LEVEL_KEYWORDS = new Set([
181
- 'fn', 'struct', 'impl', 'enum', 'const', 'let', 'export', 'declare', 'import', 'namespace', 'module'
182
- ])
183
- while (!this.check('eof')) {
184
- const kind = this.peek().kind
185
- if (kind === '}') {
186
- this.advance() // consume the stray `}`
187
- return
188
- }
189
- if (TOP_LEVEL_KEYWORDS.has(kind)) {
190
- return
191
- }
192
- // Also recover on a plain ident that could be 'import' keyword used as ident
193
- if (kind === 'ident' && this.peek().value === 'import') {
194
- return
195
- }
196
- this.advance()
197
- }
198
- }
199
-
200
- /**
201
- * Synchronize to the next statement boundary inside a block after a parse error.
202
- * Skips tokens until we reach `;`, `}`, or EOF.
203
- */
204
- private syncToNextStmt(): void {
205
- while (!this.check('eof')) {
206
- const kind = this.peek().kind
207
- if (kind === ';') {
208
- this.advance() // consume the `;`
209
- return
210
- }
211
- if (kind === '}') {
212
- return // leave `}` for parseBlock to consume
213
- }
214
- this.advance()
215
- }
216
- }
20
+ import { DeclParser } from './decl-parser'
217
21
 
22
+ export class Parser extends DeclParser {
218
23
  // -------------------------------------------------------------------------
219
- // Program
24
+ // Program (top-level entry point)
220
25
  // -------------------------------------------------------------------------
221
26
 
222
27
  parse(defaultNamespace = 'redscript'): Program {
@@ -232,7 +37,6 @@ export class Parser {
232
37
  let isLibrary = false
233
38
  let moduleName: string | undefined
234
39
 
235
- // Check for namespace declaration
236
40
  if (this.check('namespace')) {
237
41
  this.advance()
238
42
  const name = this.expect('ident')
@@ -240,10 +44,6 @@ export class Parser {
240
44
  this.match(';')
241
45
  }
242
46
 
243
- // Check for module declaration: `module library;` or `module <name>;`
244
- // Library-mode: all functions parsed from this point are marked isLibraryFn=true.
245
- // When using the `librarySources` compile option, each library source is parsed
246
- // by its own fresh Parser — so this flag never bleeds into user code.
247
47
  if (this.check('module')) {
248
48
  this.advance()
249
49
  const modKind = this.expect('ident')
@@ -251,17 +51,14 @@ export class Parser {
251
51
  isLibrary = true
252
52
  this.inLibraryMode = true
253
53
  } else {
254
- // Named module declaration: `module math;`
255
54
  moduleName = modKind.value
256
55
  }
257
56
  this.match(';')
258
57
  }
259
58
 
260
- // Parse struct, function, and import declarations
261
59
  while (!this.check('eof')) {
262
60
  try {
263
61
  if (this.check('decorator') && this.peek().value.startsWith('@config')) {
264
- // @config decorator on a global let
265
62
  const decorToken = this.advance()
266
63
  const decorator = this.parseDecoratorValue(decorToken.value)
267
64
  if (!this.check('let')) {
@@ -274,8 +71,7 @@ export class Parser {
274
71
  } else if (this.check('let')) {
275
72
  globals.push(this.parseGlobalDecl(true))
276
73
  } else if (this.check('decorator') && this.peek().value === '@singleton') {
277
- // @singleton decorator on a struct
278
- this.advance() // consume '@singleton'
74
+ this.advance()
279
75
  if (!this.check('struct')) {
280
76
  this.error('@singleton decorator must be followed by a struct declaration')
281
77
  }
@@ -293,19 +89,16 @@ export class Parser {
293
89
  } else if (this.check('const')) {
294
90
  consts.push(this.parseConstDecl())
295
91
  } else if (this.check('declare')) {
296
- // Declaration-only stub (e.g. from builtins.d.mcrs) — parse and discard
297
- this.advance() // consume 'declare'
92
+ this.advance()
298
93
  this.parseDeclareStub()
299
94
  } else if (this.check('export')) {
300
95
  declarations.push(this.parseExportedFnDecl())
301
96
  } else if (this.check('import') || (this.check('ident') && this.peek().value === 'import')) {
302
- // `import math::sin;` or `import math::*;` or `import player_utils;` (whole-module file import)
303
- this.advance() // consume 'import' (keyword or ident)
97
+ this.advance()
304
98
  const importToken = this.peek()
305
99
  const modName = this.expect('ident').value
306
- // Check for `::` — if present, this is a symbol import; otherwise, whole-module import
307
100
  if (this.check('::')) {
308
- this.advance() // consume '::'
101
+ this.advance()
309
102
  let symbol: string
310
103
  if (this.check('*')) {
311
104
  this.advance()
@@ -316,7 +109,6 @@ export class Parser {
316
109
  this.match(';')
317
110
  imports.push(this.withLoc({ moduleName: modName, symbol }, importToken))
318
111
  } else {
319
- // Whole-module import: `import player_utils;`
320
112
  this.match(';')
321
113
  imports.push(this.withLoc({ moduleName: modName, symbol: undefined }, importToken))
322
114
  }
@@ -335,2337 +127,4 @@ export class Parser {
335
127
 
336
128
  return { namespace, moduleName, globals, declarations, structs, implBlocks, enums, consts, imports, interfaces, isLibrary }
337
129
  }
338
-
339
- // -------------------------------------------------------------------------
340
- // Struct Declaration
341
- // -------------------------------------------------------------------------
342
-
343
- private parseStructDecl(): StructDecl {
344
- const structToken = this.expect('struct')
345
- const name = this.expect('ident').value
346
- const extendsName = this.match('extends') ? this.expect('ident').value : undefined
347
- this.expect('{')
348
-
349
- const fields: StructField[] = []
350
- while (!this.check('}') && !this.check('eof')) {
351
- const fieldName = this.expect('ident').value
352
- this.expect(':')
353
- const fieldType = this.parseType()
354
- fields.push({ name: fieldName, type: fieldType })
355
-
356
- // Allow optional comma or semicolon between fields
357
- this.match(',')
358
- }
359
-
360
- this.expect('}')
361
- return this.withLoc({ name, extends: extendsName, fields }, structToken)
362
- }
363
-
364
- private parseEnumDecl(): EnumDecl {
365
- const enumToken = this.expect('enum')
366
- const name = this.expect('ident').value
367
- this.expect('{')
368
-
369
- const variants: EnumVariant[] = []
370
- let nextValue = 0
371
-
372
- while (!this.check('}') && !this.check('eof')) {
373
- const variantToken = this.expect('ident')
374
- const variant: EnumVariant = { name: variantToken.value }
375
-
376
- // Payload fields: Variant(field: Type, ...)
377
- if (this.check('(')) {
378
- this.advance() // consume '('
379
- const fields: { name: string; type: TypeNode }[] = []
380
- while (!this.check(')') && !this.check('eof')) {
381
- const fieldName = this.expect('ident').value
382
- this.expect(':')
383
- const fieldType = this.parseType()
384
- fields.push({ name: fieldName, type: fieldType })
385
- if (!this.match(',')) break
386
- }
387
- this.expect(')')
388
- variant.fields = fields
389
- }
390
-
391
- if (this.match('=')) {
392
- const valueToken = this.expect('int_lit')
393
- variant.value = parseInt(valueToken.value, 10)
394
- nextValue = variant.value + 1
395
- } else {
396
- variant.value = nextValue++
397
- }
398
-
399
- variants.push(variant)
400
-
401
- if (!this.match(',')) {
402
- break
403
- }
404
- }
405
-
406
- this.expect('}')
407
- return this.withLoc({ name, variants }, enumToken)
408
- }
409
-
410
- private parseImplBlock(): ImplBlock {
411
- const implToken = this.expect('impl')
412
- let traitName: string | undefined
413
- let typeName: string
414
- const firstName = this.expect('ident').value
415
- if (this.match('for')) {
416
- traitName = firstName
417
- typeName = this.expect('ident').value
418
- } else {
419
- typeName = firstName
420
- }
421
- this.expect('{')
422
-
423
- const methods: FnDecl[] = []
424
- while (!this.check('}') && !this.check('eof')) {
425
- methods.push(this.parseFnDecl(typeName))
426
- }
427
-
428
- this.expect('}')
429
- return this.withLoc({ kind: 'impl_block', traitName, typeName, methods }, implToken)
430
- }
431
-
432
- /**
433
- * Parse an interface declaration:
434
- * interface <Name> {
435
- * fn <method>(<params>): <retType>
436
- * ...
437
- * }
438
- * Method signatures have no body — they are prototype-only.
439
- */
440
- private parseInterfaceDecl(): InterfaceDecl {
441
- const ifaceToken = this.expect('interface')
442
- const name = this.expect('ident').value
443
- this.expect('{')
444
-
445
- const methods: InterfaceMethod[] = []
446
- while (!this.check('}') && !this.check('eof')) {
447
- const fnToken = this.expect('fn')
448
- const methodName = this.expect('ident').value
449
- this.expect('(')
450
- const params = this.parseInterfaceParams()
451
- this.expect(')')
452
- let returnType: TypeNode | undefined
453
- if (this.match(':')) {
454
- returnType = this.parseType()
455
- }
456
- // No body — interface methods are signature-only
457
- methods.push(this.withLoc({ name: methodName, params, returnType }, fnToken) as InterfaceMethod)
458
- }
459
-
460
- this.expect('}')
461
- return this.withLoc({ name, methods }, ifaceToken) as InterfaceDecl
462
- }
463
-
464
- /**
465
- * Parse interface method params — like parseParams but allows bare `self`
466
- * (no `:` required for the first param named 'self').
467
- */
468
- private parseInterfaceParams(): Param[] {
469
- const params: Param[] = []
470
- if (!this.check(')')) {
471
- do {
472
- const paramToken = this.expect('ident')
473
- const paramName = paramToken.value
474
- let type: TypeNode
475
- if (params.length === 0 && paramName === 'self' && !this.check(':')) {
476
- // self without type annotation — use a sentinel struct type
477
- type = { kind: 'named', name: 'void' }
478
- } else {
479
- this.expect(':')
480
- type = this.parseType()
481
- }
482
- params.push(this.withLoc({ name: paramName, type }, paramToken))
483
- } while (this.match(','))
484
- }
485
- return params
486
- }
487
-
488
- private parseConstDecl(): ConstDecl {
489
- const constToken = this.expect('const')
490
- const name = this.expect('ident').value
491
- let type: TypeNode | undefined
492
- if (this.match(':')) {
493
- type = this.parseType()
494
- }
495
- this.expect('=')
496
- const value = this.parseLiteralExpr()
497
- this.match(';')
498
- // Infer type from value if not provided
499
- const inferredType: TypeNode = type ?? (
500
- value.kind === 'str_lit' ? { kind: 'named', name: 'string' } :
501
- value.kind === 'bool_lit' ? { kind: 'named', name: 'bool' } :
502
- value.kind === 'float_lit' ? { kind: 'named', name: 'fixed' } :
503
- { kind: 'named', name: 'int' }
504
- )
505
- return this.withLoc({ name, type: inferredType, value }, constToken)
506
- }
507
-
508
- private parseGlobalDecl(mutable: boolean): GlobalDecl {
509
- const token = this.advance() // consume 'let'
510
- const name = this.expect('ident').value
511
- this.expect(':')
512
- const type = this.parseType()
513
- let init: Expr
514
- if (this.match('=')) {
515
- init = this.parseExpr()
516
- } else {
517
- // No init — valid only for @config-decorated globals (resolved later)
518
- // Use a placeholder zero literal; will be replaced in compile step
519
- init = { kind: 'int_lit', value: 0 }
520
- }
521
- this.match(';')
522
- return this.withLoc({ kind: 'global', name, type, init, mutable }, token)
523
- }
524
-
525
- // -------------------------------------------------------------------------
526
- // Function Declaration
527
- // -------------------------------------------------------------------------
528
-
529
- /** Parse `export fn name(...)` — marks the function as exported (survives DCE). */
530
- private parseExportedFnDecl(): FnDecl {
531
- this.expect('export')
532
- const fn = this.parseFnDecl()
533
- fn.isExported = true
534
- return fn
535
- }
536
-
537
- private parseFnDecl(implTypeName?: string): FnDecl {
538
- const decorators = this.parseDecorators()
539
- const watchObjective = decorators.find(decorator => decorator.name === 'watch')?.args?.objective
540
-
541
- // Map @keep decorator to isExported flag (backward compat)
542
- let isExported: boolean | undefined
543
- const filteredDecorators = decorators.filter(d => {
544
- if (d.name === 'keep') {
545
- isExported = true
546
- return false
547
- }
548
- return true
549
- })
550
-
551
- const fnToken = this.expect('fn')
552
- const name = this.expect('ident').value
553
-
554
- // Parse optional generic type parameters: fn max<T>(...)
555
- let typeParams: string[] | undefined
556
- if (this.check('<')) {
557
- this.advance() // consume '<'
558
- typeParams = []
559
- do {
560
- typeParams.push(this.expect('ident').value)
561
- } while (this.match(','))
562
- this.expect('>')
563
- }
564
-
565
- this.expect('(')
566
- const params = this.parseParams(implTypeName)
567
- this.expect(')')
568
-
569
- let returnType: TypeNode = { kind: 'named', name: 'void' }
570
- if (this.match('->') || this.match(':')) {
571
- returnType = this.parseType()
572
- }
573
-
574
- const body = this.parseBlock()
575
- // Record the closing '}' line as endLine for accurate LSP scope detection
576
- const closingBraceLine = this.tokens[this.pos - 1]?.line
577
-
578
- const fn: import('../ast/types').FnDecl = this.withLoc(
579
- { name, typeParams, params, returnType, decorators: filteredDecorators, body,
580
- isLibraryFn: this.inLibraryMode || undefined, isExported, watchObjective },
581
- fnToken,
582
- )
583
- if (fn.span && closingBraceLine) fn.span.endLine = closingBraceLine
584
- return fn
585
- }
586
-
587
- /** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
588
- private parseDeclareStub(): void {
589
- this.expect('fn')
590
- this.expect('ident') // name
591
- this.expect('(')
592
- // consume params until ')'
593
- let depth = 1
594
- while (!this.check('eof') && depth > 0) {
595
- const t = this.advance()
596
- if (t.kind === '(') depth++
597
- else if (t.kind === ')') depth--
598
- }
599
- // optional return type annotation `: type` or `-> type`
600
- if (this.match(':') || this.match('->')) {
601
- this.parseType()
602
- }
603
- this.match(';') // consume trailing semicolon
604
- }
605
-
606
- private parseDecorators(): Decorator[] {
607
- const decorators: Decorator[] = []
608
-
609
- while (this.check('decorator')) {
610
- const token = this.advance()
611
- const decorator = this.parseDecoratorValue(token.value)
612
- decorators.push(decorator)
613
- }
614
-
615
- return decorators
616
- }
617
-
618
- private parseDecoratorValue(value: string): Decorator {
619
- // Parse @tick, @on(PlayerDeath), @on_trigger("name"), or @deprecated("msg with ) parens")
620
- // Use a greedy match for args that allows any content inside the outermost parens.
621
- const match = value.match(/^@([A-Za-z_][A-Za-z0-9_-]*)(?:\((.*)\))?$/s)
622
- if (!match) {
623
- this.error(`Invalid decorator: ${value}`)
624
- }
625
-
626
- const name = match[1] as Decorator['name']
627
- const argsStr = match[2]
628
-
629
- if (!argsStr) {
630
- return { name }
631
- }
632
-
633
- if (name === 'profile' || name === 'benchmark' || name === 'memoize') {
634
- this.error(`@${name} decorator does not accept arguments`)
635
- }
636
-
637
- const args: Decorator['args'] = {}
638
-
639
- if (name === 'on') {
640
- const eventTypeMatch = argsStr.match(/^([A-Za-z_][A-Za-z0-9_]*)$/)
641
- if (eventTypeMatch) {
642
- args.eventType = eventTypeMatch[1]
643
- return { name, args }
644
- }
645
- }
646
-
647
- // Handle @watch("objective"), @on_trigger("name"), @on_advancement("id"), @on_craft("item"), @on_join_team("team")
648
- if (name === 'watch' || name === 'on_trigger' || name === 'on_advancement' || name === 'on_craft' || name === 'on_join_team') {
649
- const strMatch = argsStr.match(/^"([^"]*)"$/)
650
- if (strMatch) {
651
- if (name === 'watch') {
652
- args.objective = strMatch[1]
653
- } else if (name === 'on_trigger') {
654
- args.trigger = strMatch[1]
655
- } else if (name === 'on_advancement') {
656
- args.advancement = strMatch[1]
657
- } else if (name === 'on_craft') {
658
- args.item = strMatch[1]
659
- } else if (name === 'on_join_team') {
660
- args.team = strMatch[1]
661
- }
662
- return { name, args }
663
- }
664
- }
665
-
666
- // Handle @config("key", default: value)
667
- if (name === 'config') {
668
- // Format: @config("key_name", default: 42)
669
- const configMatch = argsStr.match(/^"([^"]+)"\s*,\s*default\s*:\s*(-?\d+(?:\.\d+)?)$/)
670
- if (configMatch) {
671
- return { name, args: { configKey: configMatch[1], configDefault: parseFloat(configMatch[2]) } }
672
- }
673
- // Format: @config("key_name") — no default
674
- const keyOnlyMatch = argsStr.match(/^"([^"]+)"$/)
675
- if (keyOnlyMatch) {
676
- return { name, args: { configKey: keyOnlyMatch[1] } }
677
- }
678
- this.error(`Invalid @config syntax. Expected: @config("key", default: value) or @config("key")`)
679
- }
680
-
681
- // Handle @deprecated("message")
682
- if (name === 'deprecated') {
683
- const strMatch = argsStr.match(/^"([^"]*)"$/)
684
- if (strMatch) {
685
- return { name, args: { message: strMatch[1] } }
686
- }
687
- // @deprecated with no message string
688
- return { name, args: {} }
689
- }
690
-
691
- // @test("label") — marks a test function with a human-readable label
692
- if (name === 'test') {
693
- const strMatch = argsStr.match(/^"([^"]*)"$/)
694
- if (strMatch) {
695
- return { name, args: { testLabel: strMatch[1] } }
696
- }
697
- // @test with no label — use empty string
698
- return { name, args: { testLabel: '' } }
699
- }
700
-
701
- // @require_on_load(fn_name) — when this fn is used, fn_name is called from __load.
702
- // Accepts bare identifiers (with optional leading _) or quoted strings.
703
- if (name === 'require_on_load') {
704
- const rawArgs: NonNullable<Decorator['rawArgs']> = []
705
- for (const part of argsStr.split(',')) {
706
- const trimmed = part.trim()
707
- // Bare identifier: @require_on_load(_math_init)
708
- const identMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)$/)
709
- if (identMatch) {
710
- rawArgs.push({ kind: 'string', value: identMatch[1] })
711
- } else {
712
- // Quoted string fallback: @require_on_load("_math_init")
713
- const strMatch = trimmed.match(/^"([^"]*)"$/)
714
- if (strMatch) {
715
- rawArgs.push({ kind: 'string', value: strMatch[1] })
716
- }
717
- }
718
- }
719
- return { name, rawArgs }
720
- }
721
-
722
- // Handle key=value format (e.g., rate=20, batch=10, onDone=fn_name)
723
- for (const part of argsStr.split(',')) {
724
- const [key, val] = part.split('=').map(s => s.trim())
725
- if (key === 'rate') {
726
- args.rate = parseInt(val, 10)
727
- } else if (key === 'ticks') {
728
- args.ticks = parseInt(val, 10)
729
- } else if (key === 'batch') {
730
- args.batch = parseInt(val, 10)
731
- } else if (key === 'onDone') {
732
- args.onDone = val.replace(/^["']|["']$/g, '')
733
- } else if (key === 'trigger') {
734
- args.trigger = val
735
- } else if (key === 'advancement') {
736
- args.advancement = val
737
- } else if (key === 'item') {
738
- args.item = val
739
- } else if (key === 'team') {
740
- args.team = val
741
- } else if (key === 'max') {
742
- args.max = parseInt(val, 10)
743
- }
744
- }
745
-
746
- return { name, args }
747
- }
748
-
749
- private parseParams(implTypeName?: string): Param[] {
750
- const params: Param[] = []
751
-
752
- if (!this.check(')')) {
753
- do {
754
- const paramToken = this.expect('ident')
755
- const name = paramToken.value
756
- let type: TypeNode
757
- if (implTypeName && params.length === 0 && name === 'self' && !this.check(':')) {
758
- type = { kind: 'struct', name: implTypeName }
759
- } else {
760
- this.expect(':')
761
- type = this.parseType()
762
- }
763
- let defaultValue: Expr | undefined
764
- if (this.match('=')) {
765
- defaultValue = this.parseExpr()
766
- }
767
- params.push(this.withLoc({ name, type, default: defaultValue }, paramToken))
768
- } while (this.match(','))
769
- }
770
-
771
- return params
772
- }
773
-
774
- private parseType(): TypeNode {
775
- const token = this.peek()
776
- let type: TypeNode
777
-
778
- if (token.kind === '(') {
779
- // Disambiguate: tuple type `(T, T)` vs function type `(T) -> R`
780
- // Look ahead: parse elements, then check if '->' follows.
781
- const saved = this.pos
782
- this.advance() // consume '('
783
- const elements: TypeNode[] = []
784
- if (!this.check(')')) {
785
- do {
786
- elements.push(this.parseType())
787
- } while (this.match(','))
788
- }
789
- this.expect(')')
790
- if (this.check('->')) {
791
- // It's a function type — restore and use existing parseFunctionType
792
- this.pos = saved
793
- return this.parseFunctionType()
794
- }
795
- // It's a tuple type
796
- return { kind: 'tuple', elements }
797
- }
798
-
799
- if (token.kind === 'float') {
800
- this.advance()
801
- const filePart = this.filePath ? `${this.filePath}:` : ''
802
- this.warnings.push(
803
- `[DeprecatedType] ${filePart}line ${token.line}, col ${token.col}: 'float' is deprecated, use 'fixed' instead (×10000 fixed-point)`
804
- )
805
- type = { kind: 'named', name: 'float' }
806
- } else if (token.kind === 'int' || token.kind === 'bool' ||
807
- token.kind === 'fixed' || token.kind === 'string' || token.kind === 'void' ||
808
- token.kind === 'BlockPos') {
809
- this.advance()
810
- type = { kind: 'named', name: token.kind }
811
- } else if (token.kind === 'ident') {
812
- this.advance()
813
- if (token.value === 'selector' && this.check('<')) {
814
- this.advance() // consume <
815
- const entityType = this.expect('ident').value
816
- this.expect('>')
817
- type = { kind: 'selector', entityType }
818
- } else if (token.value === 'selector') {
819
- type = { kind: 'selector' }
820
- } else if (token.value === 'Option' && this.check('<')) {
821
- this.advance() // consume <
822
- const inner = this.parseType()
823
- this.expect('>')
824
- type = { kind: 'option', inner }
825
- } else if (token.value === 'double' || token.value === 'byte' ||
826
- token.value === 'short' || token.value === 'long' ||
827
- token.value === 'format_string') {
828
- type = { kind: 'named', name: token.value as any }
829
- } else {
830
- type = { kind: 'struct', name: token.value }
831
- }
832
- } else {
833
- this.error(`Expected type, got '${token.kind}'`)
834
- }
835
-
836
- while (this.match('[')) {
837
- this.expect(']')
838
- type = { kind: 'array', elem: type }
839
- }
840
-
841
- return type
842
- }
843
-
844
- private parseFunctionType(): TypeNode {
845
- this.expect('(')
846
- const params: TypeNode[] = []
847
-
848
- if (!this.check(')')) {
849
- do {
850
- params.push(this.parseType())
851
- } while (this.match(','))
852
- }
853
-
854
- this.expect(')')
855
- this.expect('->')
856
- const returnType = this.parseType()
857
- return { kind: 'function_type', params, return: returnType }
858
- }
859
-
860
- // -------------------------------------------------------------------------
861
- // Block & Statements
862
- // -------------------------------------------------------------------------
863
-
864
- private parseBlock(): Block {
865
- this.expect('{')
866
- const stmts: Stmt[] = []
867
-
868
- while (!this.check('}') && !this.check('eof')) {
869
- try {
870
- stmts.push(this.parseStmt())
871
- } catch (err) {
872
- if (err instanceof DiagnosticError) {
873
- this.parseErrors.push(err)
874
- this.syncToNextStmt()
875
- } else {
876
- throw err
877
- }
878
- }
879
- }
880
-
881
- this.expect('}')
882
- return stmts
883
- }
884
-
885
- private parseStmt(): Stmt {
886
- // Let statement
887
- if (this.check('let')) {
888
- return this.parseLetStmt()
889
- }
890
-
891
- // Const declaration (local)
892
- if (this.check('const')) {
893
- return this.parseLocalConstDecl()
894
- }
895
-
896
- // Return statement
897
- if (this.check('return')) {
898
- return this.parseReturnStmt()
899
- }
900
-
901
- // Break statement (with optional label: break outer)
902
- if (this.check('break')) {
903
- const token = this.advance()
904
- // Check if next token is an identifier (label name)
905
- if (this.check('ident')) {
906
- const labelToken = this.advance()
907
- this.match(';')
908
- return this.withLoc({ kind: 'break_label', label: labelToken.value }, token)
909
- }
910
- this.match(';')
911
- return this.withLoc({ kind: 'break' }, token)
912
- }
913
-
914
- // Continue statement (with optional label: continue outer)
915
- if (this.check('continue')) {
916
- const token = this.advance()
917
- // Check if next token is an identifier (label name)
918
- if (this.check('ident')) {
919
- const labelToken = this.advance()
920
- this.match(';')
921
- return this.withLoc({ kind: 'continue_label', label: labelToken.value }, token)
922
- }
923
- this.match(';')
924
- return this.withLoc({ kind: 'continue' }, token)
925
- }
926
-
927
- // If statement
928
- if (this.check('if')) {
929
- return this.parseIfStmt()
930
- }
931
-
932
- // Labeled loop: ident ':' (while|for|foreach|repeat)
933
- if (this.check('ident') && this.peek(1).kind === ':') {
934
- const labelToken = this.advance() // consume ident
935
- const colonToken = this.advance() // consume ':'
936
- // Now parse the loop body
937
- let loopStmt: Stmt
938
- if (this.check('while')) {
939
- loopStmt = this.parseWhileStmt()
940
- } else if (this.check('for')) {
941
- loopStmt = this.parseForStmt()
942
- } else if (this.check('foreach')) {
943
- loopStmt = this.parseForeachStmt()
944
- } else if (this.check('repeat')) {
945
- loopStmt = this.parseRepeatStmt()
946
- } else {
947
- throw new DiagnosticError(
948
- 'ParseError',
949
- `Expected loop statement after label '${labelToken.value}:', found '${this.peek().kind}'`,
950
- { line: labelToken.line, col: labelToken.col },
951
- )
952
- }
953
- return this.withLoc({ kind: 'labeled_loop', label: labelToken.value, body: loopStmt }, labelToken)
954
- }
955
-
956
- // While statement
957
- if (this.check('while')) {
958
- return this.parseWhileStmt()
959
- }
960
-
961
- // Do-while statement
962
- if (this.check('do')) {
963
- return this.parseDoWhileStmt()
964
- }
965
-
966
- // Repeat N statement
967
- if (this.check('repeat')) {
968
- return this.parseRepeatStmt()
969
- }
970
-
971
- // For statement
972
- if (this.check('for')) {
973
- return this.parseForStmt()
974
- }
975
-
976
- // Foreach statement
977
- if (this.check('foreach')) {
978
- return this.parseForeachStmt()
979
- }
980
-
981
- if (this.check('match')) {
982
- return this.parseMatchStmt()
983
- }
984
-
985
- // As block
986
- if (this.check('as')) {
987
- return this.parseAsStmt()
988
- }
989
-
990
- // At block
991
- if (this.check('at')) {
992
- return this.parseAtStmt()
993
- }
994
-
995
- // Execute statement: execute as/at/if/unless/in ... run { }
996
- if (this.check('execute')) {
997
- return this.parseExecuteStmt()
998
- }
999
-
1000
- // Raw command
1001
- if (this.check('raw_cmd')) {
1002
- const token = this.advance()
1003
- const cmd = token.value
1004
- this.match(';') // optional semicolon (raw consumes it)
1005
- return this.withLoc({ kind: 'raw', cmd }, token)
1006
- }
1007
-
1008
- // Expression statement
1009
- return this.parseExprStmt()
1010
- }
1011
-
1012
- private parseLetStmt(): Stmt {
1013
- const letToken = this.expect('let')
1014
-
1015
- // Destructuring: let (a, b, c) = expr;
1016
- if (this.check('(')) {
1017
- this.advance() // consume '('
1018
- const names: string[] = []
1019
- do {
1020
- names.push(this.expect('ident').value)
1021
- } while (this.match(','))
1022
- this.expect(')')
1023
- let type: TypeNode | undefined
1024
- if (this.match(':')) {
1025
- type = this.parseType()
1026
- }
1027
- this.expect('=')
1028
- const init = this.parseExpr()
1029
- this.match(';')
1030
- return this.withLoc({ kind: 'let_destruct', names, type, init }, letToken)
1031
- }
1032
-
1033
- const name = this.expect('ident').value
1034
-
1035
- let type: TypeNode | undefined
1036
- if (this.match(':')) {
1037
- type = this.parseType()
1038
- }
1039
-
1040
- this.expect('=')
1041
- const init = this.parseExpr()
1042
- this.match(';')
1043
-
1044
- return this.withLoc({ kind: 'let', name, type, init }, letToken)
1045
- }
1046
-
1047
- private parseLocalConstDecl(): Stmt {
1048
- const constToken = this.expect('const')
1049
- const name = this.expect('ident').value
1050
- this.expect(':')
1051
- const type = this.parseType()
1052
- this.expect('=')
1053
- const value = this.parseExpr()
1054
- this.match(';')
1055
- return this.withLoc({ kind: 'const_decl', name, type, value }, constToken)
1056
- }
1057
-
1058
- private parseReturnStmt(): Stmt {
1059
- const returnToken = this.expect('return')
1060
-
1061
- let value: Expr | undefined
1062
- if (!this.check(';') && !this.check('}') && !this.check('eof')) {
1063
- value = this.parseExpr()
1064
- }
1065
-
1066
- this.match(';')
1067
- return this.withLoc({ kind: 'return', value }, returnToken)
1068
- }
1069
-
1070
- private parseIfStmt(): Stmt {
1071
- const ifToken = this.expect('if')
1072
-
1073
- // if let Some(x) = expr { ... }
1074
- if (this.check('let') && this.peek(1).kind === 'ident' && this.peek(1).value === 'Some') {
1075
- this.advance() // consume 'let'
1076
- this.advance() // consume 'Some'
1077
- this.expect('(')
1078
- const binding = this.expect('ident').value
1079
- this.expect(')')
1080
- this.expect('=')
1081
- const init = this.parseExpr()
1082
- const then = this.parseBlock()
1083
-
1084
- let else_: Block | undefined
1085
- if (this.match('else')) {
1086
- if (this.check('if')) {
1087
- else_ = [this.parseIfStmt()]
1088
- } else {
1089
- else_ = this.parseBlock()
1090
- }
1091
- }
1092
-
1093
- return this.withLoc({ kind: 'if_let_some', binding, init, then, else_ }, ifToken)
1094
- }
1095
-
1096
- const cond = this.parseParenOptionalCond()
1097
- const then = this.parseBlock()
1098
-
1099
- let else_: Block | undefined
1100
- if (this.match('else')) {
1101
- if (this.check('if')) {
1102
- // else if
1103
- else_ = [this.parseIfStmt()]
1104
- } else {
1105
- else_ = this.parseBlock()
1106
- }
1107
- }
1108
-
1109
- return this.withLoc({ kind: 'if', cond, then, else_ }, ifToken)
1110
- }
1111
-
1112
- private parseWhileStmt(): Stmt {
1113
- const whileToken = this.expect('while')
1114
-
1115
- // while let Some(x) = expr { ... }
1116
- if (this.check('let') && this.peek(1).kind === 'ident' && this.peek(1).value === 'Some') {
1117
- this.advance() // consume 'let'
1118
- this.advance() // consume 'Some'
1119
- this.expect('(')
1120
- const binding = this.expect('ident').value
1121
- this.expect(')')
1122
- this.expect('=')
1123
- const init = this.parseExpr()
1124
- const body = this.parseBlock()
1125
- return this.withLoc({ kind: 'while_let_some', binding, init, body }, whileToken)
1126
- }
1127
-
1128
- const cond = this.parseParenOptionalCond()
1129
- const body = this.parseBlock()
1130
-
1131
- return this.withLoc({ kind: 'while', cond, body }, whileToken)
1132
- }
1133
-
1134
- private parseDoWhileStmt(): Stmt {
1135
- const doToken = this.expect('do')
1136
- const body = this.parseBlock()
1137
- this.expect('while')
1138
- const cond = this.parseParenOptionalCond()
1139
- this.match(';')
1140
- return this.withLoc({ kind: 'do_while', cond, body }, doToken)
1141
- }
1142
-
1143
- private parseRepeatStmt(): Stmt {
1144
- const repeatToken = this.expect('repeat')
1145
- const countToken = this.expect('int_lit')
1146
- const count = parseInt(countToken.value, 10)
1147
- const body = this.parseBlock()
1148
- return this.withLoc({ kind: 'repeat', count, body }, repeatToken)
1149
- }
1150
-
1151
- private parseParenOptionalCond(): Expr {
1152
- if (this.match('(')) {
1153
- const cond = this.parseExpr()
1154
- this.expect(')')
1155
- return cond
1156
- }
1157
- return this.parseExpr()
1158
- }
1159
-
1160
- private parseForStmt(): Stmt {
1161
- const forToken = this.expect('for')
1162
-
1163
- // Check for for-range syntax: for <ident> in <range_lit> { ... }
1164
- if (this.check('ident') && this.peek(1).kind === 'in') {
1165
- return this.parseForRangeStmt(forToken)
1166
- }
1167
-
1168
- this.expect('(')
1169
-
1170
- // Detect for-in-array syntax: for ( let ident in ident , lenExpr ) { ... }
1171
- if (this.check('let') && this.peek(1).kind === 'ident' && this.peek(2).kind === 'in' && this.peek(3).kind === 'ident' && this.peek(4).kind === ',') {
1172
- this.advance() // consume 'let'
1173
- const binding = this.expect('ident').value
1174
- this.expect('in')
1175
- const arrayName = this.expect('ident').value
1176
- this.expect(',')
1177
- const lenExpr = this.parseExpr()
1178
- this.expect(')')
1179
- const body = this.parseBlock()
1180
- return this.withLoc({ kind: 'for_in_array', binding, arrayName, lenExpr, body }, forToken)
1181
- }
1182
-
1183
- // Init: either let statement (without semicolon) or empty
1184
- let init: Stmt | undefined
1185
- if (this.check('let')) {
1186
- // Parse let without consuming semicolon here (we handle it)
1187
- const letToken = this.expect('let')
1188
- const name = this.expect('ident').value
1189
- let type: TypeNode | undefined
1190
- if (this.match(':')) {
1191
- type = this.parseType()
1192
- }
1193
- this.expect('=')
1194
- const initExpr = this.parseExpr()
1195
- const initStmt: Stmt = { kind: 'let', name, type, init: initExpr }
1196
- init = this.withLoc(initStmt, letToken)
1197
- }
1198
- this.expect(';')
1199
-
1200
- // Condition
1201
- const cond = this.parseExpr()
1202
- this.expect(';')
1203
-
1204
- // Step expression
1205
- const step = this.parseExpr()
1206
- this.expect(')')
1207
-
1208
- const body = this.parseBlock()
1209
-
1210
- return this.withLoc({ kind: 'for', init, cond, step, body }, forToken)
1211
- }
1212
-
1213
- private parseForRangeStmt(forToken: Token): Stmt {
1214
- const varName = this.expect('ident').value
1215
- this.expect('in')
1216
-
1217
- let start: Expr
1218
- let end: Expr
1219
- let inclusive = false
1220
-
1221
- if (this.check('range_lit')) {
1222
- // Literal range: 0..10, 0..count, 0..=9, 0..=count
1223
- const rangeToken = this.advance()
1224
- const raw = rangeToken.value
1225
- // Detect inclusive: ends with = after .. (e.g. "0..=" or "..=")
1226
- const incl = raw.includes('..=')
1227
- inclusive = incl
1228
- const range = this.parseRangeValue(raw)
1229
- start = this.withLoc({ kind: 'int_lit', value: range.min ?? 0 }, rangeToken)
1230
- if (range.max !== null && range.max !== undefined) {
1231
- // Fully numeric: 0..10 or 0..=9
1232
- end = this.withLoc({ kind: 'int_lit', value: range.max }, rangeToken)
1233
- } else {
1234
- // Open-ended: "0.." or "0..=" — parse the end expression from next tokens
1235
- end = this.parseUnaryExpr()
1236
- }
1237
- } else {
1238
- // Dynamic range: expr..expr or expr..=expr
1239
- // parseExpr stops before range_lit (not in BINARY_OPS), so this is safe
1240
- const arrayOrStart = this.parseExpr()
1241
-
1242
- // --- for_each detection: for item in arr { ... } ---
1243
- // If after parsing the expression there is no range_lit, it's a for_each (array iteration)
1244
- if (!this.check('range_lit')) {
1245
- const body = this.parseBlock()
1246
- return this.withLoc({ kind: 'for_each', binding: varName, array: arrayOrStart, body }, forToken)
1247
- }
1248
-
1249
- start = arrayOrStart
1250
- // Consume the range_lit token which should be ".." or "..="
1251
- if (this.check('range_lit')) {
1252
- const rangeOp = this.advance()
1253
- inclusive = rangeOp.value.includes('=')
1254
- // If the range_lit captured digits after .., use them as end
1255
- const afterOp = rangeOp.value.replace(/^\.\.=?/, '')
1256
- if (afterOp.length > 0) {
1257
- end = this.withLoc({ kind: 'int_lit', value: parseInt(afterOp, 10) }, rangeOp)
1258
- } else {
1259
- end = this.parseExpr()
1260
- }
1261
- } else {
1262
- this.error('Expected .. or ..= in for-range expression')
1263
- start = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
1264
- end = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
1265
- }
1266
- }
1267
-
1268
- const body = this.parseBlock()
1269
- return this.withLoc({ kind: 'for_range', varName, start, end, inclusive, body }, forToken)
1270
- }
1271
-
1272
- private parseForeachStmt(): Stmt {
1273
- const foreachToken = this.expect('foreach')
1274
- this.expect('(')
1275
- const binding = this.expect('ident').value
1276
- this.expect('in')
1277
- const iterable = this.parseExpr()
1278
- this.expect(')')
1279
-
1280
- // Parse optional execute context modifiers (as, at, positioned, rotated, facing, etc.)
1281
- let executeContext: string | undefined
1282
- // Check for execute subcommand keywords
1283
- const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align', 'on', 'summon']
1284
- if (this.check('as') || this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
1285
- // Collect everything until we hit '{'
1286
- let context = ''
1287
- while (!this.check('{') && !this.check('eof')) {
1288
- context += this.advance().value + ' '
1289
- }
1290
- executeContext = context.trim()
1291
- }
1292
-
1293
- const body = this.parseBlock()
1294
-
1295
- return this.withLoc({ kind: 'foreach', binding, iterable, body, executeContext }, foreachToken)
1296
- }
1297
-
1298
- private parseMatchPattern(): MatchPattern {
1299
- // Wildcard: _
1300
- if (this.check('ident') && this.peek().value === '_') {
1301
- this.advance()
1302
- return { kind: 'PatWild' }
1303
- }
1304
- // None
1305
- if (this.check('ident') && this.peek().value === 'None') {
1306
- this.advance()
1307
- return { kind: 'PatNone' }
1308
- }
1309
- // Some(x)
1310
- if (this.check('ident') && this.peek().value === 'Some') {
1311
- this.advance() // consume 'Some'
1312
- this.expect('(')
1313
- const binding = this.expect('ident').value
1314
- this.expect(')')
1315
- return { kind: 'PatSome', binding }
1316
- }
1317
- // Enum pattern: EnumName::Variant or EnumName::Variant(b1, b2, ...)
1318
- if (this.check('ident') && this.peek(1).kind === '::') {
1319
- const enumName = this.advance().value
1320
- this.expect('::')
1321
- const variant = this.expect('ident').value
1322
- const bindings: string[] = []
1323
- if (this.check('(')) {
1324
- this.advance() // consume '('
1325
- while (!this.check(')') && !this.check('eof')) {
1326
- bindings.push(this.expect('ident').value)
1327
- if (!this.match(',')) break
1328
- }
1329
- this.expect(')')
1330
- }
1331
- return { kind: 'PatEnum', enumName, variant, bindings }
1332
- }
1333
- // Integer literal
1334
- if (this.check('int_lit')) {
1335
- const tok = this.advance()
1336
- return { kind: 'PatInt', value: parseInt(tok.value, 10) }
1337
- }
1338
- // Negative integer literal: -N
1339
- if (this.check('-') && this.peek(1).kind === 'int_lit') {
1340
- this.advance() // consume '-'
1341
- const tok = this.advance()
1342
- return { kind: 'PatInt', value: -parseInt(tok.value, 10) }
1343
- }
1344
- // Legacy: range_lit or any other expression (e.g. 0..59)
1345
- const e = this.parseExpr()
1346
- return { kind: 'PatExpr', expr: e }
1347
- }
1348
-
1349
- private parseMatchStmt(): Stmt {
1350
- const matchToken = this.expect('match')
1351
-
1352
- // Support both `match (expr)` (legacy) and `match expr` (new syntax)
1353
- let expr: Expr
1354
- if (this.check('(')) {
1355
- // Peek ahead — if it looks like `(expr)` followed by `{`, consume parens
1356
- this.advance() // consume '('
1357
- expr = this.parseExpr()
1358
- this.expect(')')
1359
- } else {
1360
- expr = this.parseExpr()
1361
- }
1362
- this.expect('{')
1363
-
1364
- const arms: Array<{ pattern: MatchPattern; body: Block }> = []
1365
- while (!this.check('}') && !this.check('eof')) {
1366
- const pattern = this.parseMatchPattern()
1367
- this.expect('=>')
1368
- const body = this.parseBlock()
1369
- this.match(',') // optional trailing comma
1370
- arms.push({ pattern, body })
1371
- }
1372
-
1373
- this.expect('}')
1374
- return this.withLoc({ kind: 'match', expr, arms }, matchToken)
1375
- }
1376
-
1377
- private parseAsStmt(): Stmt {
1378
- const asToken = this.expect('as')
1379
- const as_sel = this.parseSelector()
1380
-
1381
- // Check for combined as/at
1382
- if (this.match('at')) {
1383
- const at_sel = this.parseSelector()
1384
- const body = this.parseBlock()
1385
- return this.withLoc({ kind: 'as_at', as_sel, at_sel, body }, asToken)
1386
- }
1387
-
1388
- const body = this.parseBlock()
1389
- return this.withLoc({ kind: 'as_block', selector: as_sel, body }, asToken)
1390
- }
1391
-
1392
- private parseAtStmt(): Stmt {
1393
- const atToken = this.expect('at')
1394
- const selector = this.parseSelector()
1395
- const body = this.parseBlock()
1396
- return this.withLoc({ kind: 'at_block', selector, body }, atToken)
1397
- }
1398
-
1399
- private parseExecuteStmt(): Stmt {
1400
- const executeToken = this.expect('execute')
1401
- const subcommands: ExecuteSubcommand[] = []
1402
-
1403
- // Parse subcommands until we hit 'run'
1404
- while (!this.check('run') && !this.check('eof')) {
1405
- if (this.match('as')) {
1406
- const selector = this.parseSelector()
1407
- subcommands.push({ kind: 'as', selector })
1408
- } else if (this.match('at')) {
1409
- const selector = this.parseSelector()
1410
- subcommands.push({ kind: 'at', selector })
1411
- } else if (this.checkIdent('positioned')) {
1412
- this.advance()
1413
- if (this.match('as')) {
1414
- const selector = this.parseSelector()
1415
- subcommands.push({ kind: 'positioned_as', selector })
1416
- } else {
1417
- const x = this.parseCoordToken()
1418
- const y = this.parseCoordToken()
1419
- const z = this.parseCoordToken()
1420
- subcommands.push({ kind: 'positioned', x, y, z })
1421
- }
1422
- } else if (this.checkIdent('rotated')) {
1423
- this.advance()
1424
- if (this.match('as')) {
1425
- const selector = this.parseSelector()
1426
- subcommands.push({ kind: 'rotated_as', selector })
1427
- } else {
1428
- const yaw = this.parseCoordToken()
1429
- const pitch = this.parseCoordToken()
1430
- subcommands.push({ kind: 'rotated', yaw, pitch })
1431
- }
1432
- } else if (this.checkIdent('facing')) {
1433
- this.advance()
1434
- if (this.checkIdent('entity')) {
1435
- this.advance()
1436
- const selector = this.parseSelector()
1437
- const anchor = this.checkIdent('eyes') || this.checkIdent('feet') ? this.advance().value as 'eyes' | 'feet' : 'feet'
1438
- subcommands.push({ kind: 'facing_entity', selector, anchor })
1439
- } else {
1440
- const x = this.parseCoordToken()
1441
- const y = this.parseCoordToken()
1442
- const z = this.parseCoordToken()
1443
- subcommands.push({ kind: 'facing', x, y, z })
1444
- }
1445
- } else if (this.checkIdent('anchored')) {
1446
- this.advance()
1447
- const anchor = this.advance().value as 'eyes' | 'feet'
1448
- subcommands.push({ kind: 'anchored', anchor })
1449
- } else if (this.checkIdent('align')) {
1450
- this.advance()
1451
- const axes = this.advance().value
1452
- subcommands.push({ kind: 'align', axes })
1453
- } else if (this.checkIdent('on')) {
1454
- this.advance()
1455
- const relation = this.advance().value
1456
- subcommands.push({ kind: 'on', relation })
1457
- } else if (this.checkIdent('summon')) {
1458
- this.advance()
1459
- const entity = this.advance().value
1460
- subcommands.push({ kind: 'summon', entity })
1461
- } else if (this.checkIdent('store')) {
1462
- this.advance()
1463
- const storeType = this.advance().value // 'result' or 'success'
1464
- if (this.checkIdent('score')) {
1465
- this.advance()
1466
- const target = this.advance().value
1467
- const targetObj = this.advance().value
1468
- if (storeType === 'result') {
1469
- subcommands.push({ kind: 'store_result', target, targetObj })
1470
- } else {
1471
- subcommands.push({ kind: 'store_success', target, targetObj })
1472
- }
1473
- } else {
1474
- this.error('store currently only supports score target')
1475
- }
1476
- } else if (this.match('if')) {
1477
- this.parseExecuteCondition(subcommands, 'if')
1478
- } else if (this.match('unless')) {
1479
- this.parseExecuteCondition(subcommands, 'unless')
1480
- } else if (this.match('in')) {
1481
- // Dimension can be namespaced: minecraft:the_nether
1482
- let dim = this.advance().value
1483
- if (this.match(':')) {
1484
- dim += ':' + this.advance().value
1485
- }
1486
- subcommands.push({ kind: 'in', dimension: dim })
1487
- } else {
1488
- this.error(`Unexpected token in execute statement: ${this.peek().kind} (${this.peek().value})`)
1489
- }
1490
- }
1491
-
1492
- this.expect('run')
1493
- const body = this.parseBlock()
1494
-
1495
- return this.withLoc({ kind: 'execute', subcommands, body }, executeToken)
1496
- }
1497
-
1498
- private parseExecuteCondition(subcommands: ExecuteSubcommand[], type: 'if' | 'unless'): void {
1499
- if (this.checkIdent('entity') || this.check('selector')) {
1500
- if (this.checkIdent('entity')) this.advance()
1501
- const selectorOrVar = this.parseSelectorOrVarSelector()
1502
- subcommands.push({ kind: type === 'if' ? 'if_entity' : 'unless_entity', ...selectorOrVar })
1503
- } else if (this.checkIdent('block')) {
1504
- this.advance()
1505
- const x = this.parseCoordToken()
1506
- const y = this.parseCoordToken()
1507
- const z = this.parseCoordToken()
1508
- const block = this.parseBlockId()
1509
- subcommands.push({ kind: type === 'if' ? 'if_block' : 'unless_block', pos: [x, y, z], block })
1510
- } else if (this.checkIdent('score')) {
1511
- this.advance()
1512
- const target = this.advance().value
1513
- const targetObj = this.advance().value
1514
- // Check for range or comparison
1515
- if (this.checkIdent('matches')) {
1516
- this.advance()
1517
- const range = this.advance().value
1518
- subcommands.push({ kind: type === 'if' ? 'if_score_range' : 'unless_score_range', target, targetObj, range })
1519
- } else {
1520
- const op = this.advance().value // <, <=, =, >=, >
1521
- const source = this.advance().value
1522
- const sourceObj = this.advance().value
1523
- subcommands.push({
1524
- kind: type === 'if' ? 'if_score' : 'unless_score',
1525
- target, targetObj, op, source, sourceObj
1526
- })
1527
- }
1528
- } else {
1529
- this.error(`Unknown condition type after ${type}`)
1530
- }
1531
- }
1532
-
1533
- private parseCoordToken(): string {
1534
- // Handle ~, ^, numbers, relative coords like ~5, ^-3
1535
- const token = this.peek()
1536
- if (token.kind === 'rel_coord' || token.kind === 'local_coord' ||
1537
- token.kind === 'int_lit' || token.kind === 'float_lit' ||
1538
- token.kind === '-' || token.kind === 'ident') {
1539
- return this.advance().value
1540
- }
1541
- this.error(`Expected coordinate, got ${token.kind}`)
1542
- return '~'
1543
- }
1544
-
1545
- private parseBlockId(): string {
1546
- // Parse block ID like minecraft:stone or stone
1547
- let id = this.advance().value
1548
- if (this.match(':')) {
1549
- id += ':' + this.advance().value
1550
- }
1551
- // Handle block states [facing=north]
1552
- if (this.check('[')) {
1553
- id += this.advance().value // [
1554
- while (!this.check(']') && !this.check('eof')) {
1555
- id += this.advance().value
1556
- }
1557
- id += this.advance().value // ]
1558
- }
1559
- return id
1560
- }
1561
-
1562
- private checkIdent(value: string): boolean {
1563
- return this.check('ident') && this.peek().value === value
1564
- }
1565
-
1566
- private parseExprStmt(): Stmt {
1567
- const expr = this.parseExpr()
1568
- this.match(';')
1569
- const exprToken = this.getLocToken(expr) ?? this.peek()
1570
- return this.withLoc({ kind: 'expr', expr }, exprToken)
1571
- }
1572
-
1573
- // -------------------------------------------------------------------------
1574
- // Expressions (Precedence Climbing)
1575
- // -------------------------------------------------------------------------
1576
-
1577
- private parseExpr(): Expr {
1578
- return this.parseAssignment()
1579
- }
1580
-
1581
- private parseAssignment(): Expr {
1582
- const left = this.parseBinaryExpr(1)
1583
-
1584
- // Check for assignment
1585
- const token = this.peek()
1586
- if (token.kind === '=' || token.kind === '+=' || token.kind === '-=' ||
1587
- token.kind === '*=' || token.kind === '/=' || token.kind === '%=') {
1588
- const op = this.advance().kind as AssignOp
1589
-
1590
- if (left.kind === 'ident') {
1591
- const value = this.parseAssignment()
1592
- return this.withLoc({ kind: 'assign', target: left.name, op, value }, this.getLocToken(left) ?? token)
1593
- }
1594
-
1595
- // Member assignment: p.x = 10, p.x += 5
1596
- if (left.kind === 'member') {
1597
- const value = this.parseAssignment()
1598
- return this.withLoc(
1599
- { kind: 'member_assign', obj: left.obj, field: left.field, op, value },
1600
- this.getLocToken(left) ?? token
1601
- )
1602
- }
1603
-
1604
- // Index assignment: arr[0] = val, arr[i] = val
1605
- if (left.kind === 'index') {
1606
- const value = this.parseAssignment()
1607
- return this.withLoc(
1608
- { kind: 'index_assign', obj: left.obj, index: left.index, op, value },
1609
- this.getLocToken(left) ?? token
1610
- )
1611
- }
1612
- }
1613
-
1614
- return left
1615
- }
1616
-
1617
- private parseBinaryExpr(minPrec: number): Expr {
1618
- let left = this.parseUnaryExpr()
1619
-
1620
- while (true) {
1621
- const op = this.peek().kind
1622
- if (!BINARY_OPS.has(op)) break
1623
-
1624
- const prec = PRECEDENCE[op]
1625
- if (prec < minPrec) break
1626
-
1627
- const opToken = this.advance()
1628
- if (op === 'is') {
1629
- const entityType = this.parseEntityTypeName()
1630
- left = this.withLoc(
1631
- { kind: 'is_check', expr: left, entityType },
1632
- this.getLocToken(left) ?? opToken
1633
- )
1634
- continue
1635
- }
1636
-
1637
- const right = this.parseBinaryExpr(prec + 1) // left associative
1638
- left = this.withLoc(
1639
- { kind: 'binary', op: op as BinOp | CmpOp | '&&' | '||', left, right },
1640
- this.getLocToken(left) ?? opToken
1641
- )
1642
- }
1643
-
1644
- return left
1645
- }
1646
-
1647
- private parseUnaryExpr(): Expr {
1648
- if (this.match('!')) {
1649
- const bangToken = this.tokens[this.pos - 1]
1650
- const operand = this.parseUnaryExpr()
1651
- return this.withLoc({ kind: 'unary', op: '!', operand }, bangToken)
1652
- }
1653
-
1654
- if (this.check('-') && !this.isSubtraction()) {
1655
- const minusToken = this.advance()
1656
- const operand = this.parseUnaryExpr()
1657
- return this.withLoc({ kind: 'unary', op: '-', operand }, minusToken)
1658
- }
1659
-
1660
- return this.parsePostfixExpr()
1661
- }
1662
-
1663
- private parseEntityTypeName(): EntityTypeName {
1664
- const token = this.expect('ident')
1665
- if (ENTITY_TYPE_NAMES.has(token.value as EntityTypeName)) {
1666
- return token.value as EntityTypeName
1667
- }
1668
- this.error(`Unknown entity type '${token.value}'`)
1669
- }
1670
-
1671
- private isSubtraction(): boolean {
1672
- // Check if this minus is binary (subtraction) by looking at previous token
1673
- // If previous was a value (literal, ident, ), ]) it's subtraction
1674
- if (this.pos === 0) return false
1675
- const prev = this.tokens[this.pos - 1]
1676
- return ['int_lit', 'float_lit', 'ident', ')', ']'].includes(prev.kind)
1677
- }
1678
-
1679
- /**
1680
- * Try to parse `<Type, ...>` as explicit generic type arguments.
1681
- * Returns the parsed type list if successful, null if this looks like a comparison.
1682
- * Does NOT consume any tokens if it returns null.
1683
- */
1684
- private tryParseTypeArgs(): import('../ast/types').TypeNode[] | null {
1685
- const saved = this.pos
1686
- this.advance() // consume '<'
1687
- const typeArgs: import('../ast/types').TypeNode[] = []
1688
- try {
1689
- do {
1690
- typeArgs.push(this.parseType())
1691
- } while (this.match(','))
1692
- if (!this.check('>')) {
1693
- this.pos = saved
1694
- return null
1695
- }
1696
- this.advance() // consume '>'
1697
- return typeArgs
1698
- } catch {
1699
- this.pos = saved
1700
- return null
1701
- }
1702
- }
1703
-
1704
- private parsePostfixExpr(): Expr {
1705
- let expr = this.parsePrimaryExpr()
1706
-
1707
- while (true) {
1708
- // Generic call: ident<Type>(args) — check before regular '(' handling
1709
- if (expr.kind === 'ident' && this.check('<')) {
1710
- const typeArgs = this.tryParseTypeArgs()
1711
- if (typeArgs !== null && this.check('(')) {
1712
- const openParenToken = this.peek()
1713
- this.advance() // consume '('
1714
- const args = this.parseArgs()
1715
- this.expect(')')
1716
- expr = this.withLoc(
1717
- { kind: 'call', fn: expr.name, args, typeArgs },
1718
- this.getLocToken(expr) ?? openParenToken
1719
- )
1720
- continue
1721
- }
1722
- // Not a generic call — fall through to normal expression handling
1723
- }
1724
-
1725
- // Function call
1726
- if (this.match('(')) {
1727
- const openParenToken = this.tokens[this.pos - 1]
1728
- if (expr.kind === 'ident') {
1729
- const args = this.parseArgs()
1730
- this.expect(')')
1731
- expr = this.withLoc({ kind: 'call', fn: expr.name, args }, this.getLocToken(expr) ?? openParenToken)
1732
- continue
1733
- }
1734
- // Member call: entity.tag("name") → __entity_tag(entity, "name")
1735
- // Also handle arr.push(val) and arr.length
1736
- if (expr.kind === 'member') {
1737
- // Option.unwrap_or(default) → unwrap_or AST node
1738
- if (expr.field === 'unwrap_or') {
1739
- const defaultExpr = this.parseExpr()
1740
- this.expect(')')
1741
- expr = this.withLoc(
1742
- { kind: 'unwrap_or', opt: expr.obj, default_: defaultExpr },
1743
- this.getLocToken(expr) ?? openParenToken
1744
- )
1745
- continue
1746
- }
1747
-
1748
- const methodMap: Record<string, string> = {
1749
- 'tag': '__entity_tag',
1750
- 'untag': '__entity_untag',
1751
- 'has_tag': '__entity_has_tag',
1752
- 'push': '__array_push',
1753
- 'pop': '__array_pop',
1754
- 'add': 'set_add',
1755
- 'contains': 'set_contains',
1756
- 'remove': 'set_remove',
1757
- 'clear': 'set_clear',
1758
- }
1759
- const internalFn = methodMap[expr.field]
1760
- if (internalFn) {
1761
- const args = this.parseArgs()
1762
- this.expect(')')
1763
- expr = this.withLoc(
1764
- { kind: 'call', fn: internalFn, args: [expr.obj, ...args] },
1765
- this.getLocToken(expr) ?? openParenToken
1766
- )
1767
- continue
1768
- }
1769
- // Generic method sugar: obj.method(args) → method(obj, args)
1770
- const args = this.parseArgs()
1771
- this.expect(')')
1772
- expr = this.withLoc(
1773
- { kind: 'call', fn: expr.field, args: [expr.obj, ...args] },
1774
- this.getLocToken(expr) ?? openParenToken
1775
- )
1776
- continue
1777
- }
1778
- const args = this.parseArgs()
1779
- this.expect(')')
1780
- expr = this.withLoc(
1781
- { kind: 'invoke', callee: expr, args },
1782
- this.getLocToken(expr) ?? openParenToken
1783
- )
1784
- continue
1785
- }
1786
-
1787
- // Array index access: arr[0]
1788
- if (this.match('[')) {
1789
- const index = this.parseExpr()
1790
- this.expect(']')
1791
- expr = this.withLoc(
1792
- { kind: 'index', obj: expr, index },
1793
- this.getLocToken(expr) ?? this.tokens[this.pos - 1]
1794
- )
1795
- continue
1796
- }
1797
-
1798
- // Member access
1799
- if (this.match('.')) {
1800
- const field = this.expect('ident').value
1801
- expr = this.withLoc(
1802
- { kind: 'member', obj: expr, field },
1803
- this.getLocToken(expr) ?? this.tokens[this.pos - 1]
1804
- )
1805
- continue
1806
- }
1807
-
1808
- // Type cast: expr as Type
1809
- // Only parse 'as' as a cast when followed by a type token (not a selector like @a)
1810
- if (this.check('as') && this.isTypeCastAs()) {
1811
- const asToken = this.advance() // consume 'as'
1812
- const targetType = this.parseType()
1813
- expr = this.withLoc(
1814
- { kind: 'type_cast', expr, targetType },
1815
- this.getLocToken(expr) ?? asToken
1816
- )
1817
- continue
1818
- }
1819
-
1820
- break
1821
- }
1822
-
1823
- return expr
1824
- }
1825
-
1826
- /** Returns true if the current 'as' token is a type cast (not a context block) */
1827
- private isTypeCastAs(): boolean {
1828
- // Look ahead past 'as' to see if the next token looks like a type
1829
- const next = this.tokens[this.pos + 1]
1830
- if (!next) return false
1831
- const typeStartTokens = new Set(['int', 'bool', 'float', 'fixed', 'string', 'void', 'BlockPos', '('])
1832
- if (typeStartTokens.has(next.kind)) return true
1833
- if (next.kind === 'ident' && (
1834
- next.value === 'double' || next.value === 'byte' || next.value === 'short' ||
1835
- next.value === 'long' || next.value === 'selector' || next.value === 'Option'
1836
- )) return true
1837
- return false
1838
- }
1839
-
1840
- private parseArgs(): Expr[] {
1841
- const args: Expr[] = []
1842
-
1843
- if (!this.check(')')) {
1844
- do {
1845
- args.push(this.parseExpr())
1846
- } while (this.match(','))
1847
- }
1848
-
1849
- return args
1850
- }
1851
-
1852
- private parsePrimaryExpr(): Expr {
1853
- const token = this.peek()
1854
-
1855
- if (token.kind === 'ident' && this.peek(1).kind === '::') {
1856
- const typeToken = this.advance()
1857
- this.expect('::')
1858
- const memberToken = this.expect('ident')
1859
- if (this.check('(')) {
1860
- // Peek inside: if first non-'(' token is `ident :` it's enum construction with named args.
1861
- // We only treat it as enum_construct when there are actual named args (not empty parens),
1862
- // because empty `()` is ambiguous and most commonly means a static method call.
1863
- const isNamedArgs = this.peek(1).kind === 'ident' && this.peek(2).kind === ':'
1864
- if (isNamedArgs) {
1865
- // Enum variant construction: EnumName::Variant(field: expr, ...)
1866
- this.advance() // consume '('
1867
- const args: { name: string; value: Expr }[] = []
1868
- while (!this.check(')') && !this.check('eof')) {
1869
- const fieldName = this.expect('ident').value
1870
- this.expect(':')
1871
- const value = this.parseExpr()
1872
- args.push({ name: fieldName, value })
1873
- if (!this.match(',')) break
1874
- }
1875
- this.expect(')')
1876
- return this.withLoc({ kind: 'enum_construct', enumName: typeToken.value, variant: memberToken.value, args }, typeToken)
1877
- }
1878
- // Static method call: Type::method(args)
1879
- this.advance() // consume '('
1880
- const args = this.parseArgs()
1881
- this.expect(')')
1882
- return this.withLoc({ kind: 'static_call', type: typeToken.value, method: memberToken.value, args }, typeToken)
1883
- }
1884
- // Enum variant access: Enum::Variant
1885
- return this.withLoc({ kind: 'path_expr', enumName: typeToken.value, variant: memberToken.value }, typeToken)
1886
- }
1887
-
1888
- if (token.kind === 'ident' && this.peek(1).kind === '=>') {
1889
- return this.parseSingleParamLambda()
1890
- }
1891
-
1892
- // Integer literal
1893
- if (token.kind === 'int_lit') {
1894
- this.advance()
1895
- return this.withLoc({ kind: 'int_lit', value: parseInt(token.value, 10) }, token)
1896
- }
1897
-
1898
- // Float literal
1899
- if (token.kind === 'float_lit') {
1900
- this.advance()
1901
- return this.withLoc({ kind: 'float_lit', value: parseFloat(token.value) }, token)
1902
- }
1903
-
1904
- // Relative coordinate: ~ ~5 ~-3 ~0.5
1905
- if (token.kind === 'rel_coord') {
1906
- this.advance()
1907
- return this.withLoc({ kind: 'rel_coord', value: token.value }, token)
1908
- }
1909
-
1910
- // Local coordinate: ^ ^5 ^-3 ^0.5
1911
- if (token.kind === 'local_coord') {
1912
- this.advance()
1913
- return this.withLoc({ kind: 'local_coord', value: token.value }, token)
1914
- }
1915
-
1916
- // NBT suffix literals
1917
- if (token.kind === 'byte_lit') {
1918
- this.advance()
1919
- return this.withLoc({ kind: 'byte_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
1920
- }
1921
- if (token.kind === 'short_lit') {
1922
- this.advance()
1923
- return this.withLoc({ kind: 'short_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
1924
- }
1925
- if (token.kind === 'long_lit') {
1926
- this.advance()
1927
- return this.withLoc({ kind: 'long_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
1928
- }
1929
- if (token.kind === 'double_lit') {
1930
- this.advance()
1931
- return this.withLoc({ kind: 'double_lit', value: parseFloat(token.value.slice(0, -1)) }, token)
1932
- }
1933
-
1934
- // String literal
1935
- if (token.kind === 'string_lit') {
1936
- this.advance()
1937
- return this.parseStringExpr(token)
1938
- }
1939
-
1940
- if (token.kind === 'f_string') {
1941
- this.advance()
1942
- return this.parseFStringExpr(token)
1943
- }
1944
-
1945
- // MC name literal: #health → mc_name node (value = "health", without #)
1946
- if (token.kind === 'mc_name') {
1947
- this.advance()
1948
- return this.withLoc({ kind: 'mc_name', value: token.value.slice(1) }, token)
1949
- }
1950
-
1951
- // Boolean literal
1952
- if (token.kind === 'true') {
1953
- this.advance()
1954
- return this.withLoc({ kind: 'bool_lit', value: true }, token)
1955
- }
1956
- if (token.kind === 'false') {
1957
- this.advance()
1958
- return this.withLoc({ kind: 'bool_lit', value: false }, token)
1959
- }
1960
-
1961
- // Range literal
1962
- if (token.kind === 'range_lit') {
1963
- this.advance()
1964
- return this.withLoc({ kind: 'range_lit', range: this.parseRangeValue(token.value) }, token)
1965
- }
1966
-
1967
- // Selector
1968
- if (token.kind === 'selector') {
1969
- this.advance()
1970
- return this.withLoc({
1971
- kind: 'selector',
1972
- raw: token.value,
1973
- isSingle: computeIsSingle(token.value),
1974
- sel: this.parseSelectorValue(token.value),
1975
- }, token)
1976
- }
1977
-
1978
- // Named struct literal: TypeName { field: value, ... }
1979
- // Require at least one field (ident + :) to avoid ambiguity with blocks.
1980
- if (token.kind === 'ident' && this.peek(1).kind === '{' &&
1981
- this.peek(2).kind === 'ident' && this.peek(3).kind === ':') {
1982
- this.advance() // consume type name (used only for disambiguation, dropped from AST)
1983
- return this.parseStructLit()
1984
- }
1985
-
1986
- // Some(expr) — Option constructor
1987
- if (token.kind === 'ident' && token.value === 'Some' && this.peek(1).kind === '(') {
1988
- this.advance() // consume 'Some'
1989
- this.advance() // consume '('
1990
- const value = this.parseExpr()
1991
- this.expect(')')
1992
- return this.withLoc({ kind: 'some_lit', value }, token)
1993
- }
1994
-
1995
- // None — Option empty constructor
1996
- if (token.kind === 'ident' && token.value === 'None') {
1997
- this.advance()
1998
- return this.withLoc({ kind: 'none_lit' }, token)
1999
- }
2000
-
2001
- // Identifier
2002
- if (token.kind === 'ident') {
2003
- this.advance()
2004
- return this.withLoc({ kind: 'ident', name: token.value }, token)
2005
- }
2006
-
2007
- // Grouped expression or tuple literal
2008
- if (token.kind === '(') {
2009
- if (this.isBlockPosLiteral()) {
2010
- return this.parseBlockPos()
2011
- }
2012
- if (this.isLambdaStart()) {
2013
- return this.parseLambdaExpr()
2014
- }
2015
- this.advance()
2016
- const first = this.parseExpr()
2017
- // If followed by a comma, it's a tuple literal
2018
- if (this.match(',')) {
2019
- const elements: Expr[] = [first]
2020
- if (!this.check(')')) {
2021
- do {
2022
- elements.push(this.parseExpr())
2023
- } while (this.match(','))
2024
- }
2025
- this.expect(')')
2026
- return this.withLoc({ kind: 'tuple_lit', elements }, token)
2027
- }
2028
- this.expect(')')
2029
- return first
2030
- }
2031
-
2032
- // Struct literal or block: { x: 10, y: 20 }
2033
- if (token.kind === '{') {
2034
- return this.parseStructLit()
2035
- }
2036
-
2037
- // Array literal: [1, 2, 3] or []
2038
- if (token.kind === '[') {
2039
- return this.parseArrayLit()
2040
- }
2041
-
2042
- this.error(`Unexpected token '${token.kind}'`)
2043
- }
2044
-
2045
- private parseLiteralExpr(): LiteralExpr {
2046
- // Support negative literals: -5, -3.14
2047
- if (this.check('-')) {
2048
- this.advance()
2049
- const token = this.peek()
2050
- if (token.kind === 'int_lit') {
2051
- this.advance()
2052
- return this.withLoc({ kind: 'int_lit', value: -Number(token.value) }, token)
2053
- }
2054
- if (token.kind === 'float_lit') {
2055
- this.advance()
2056
- return this.withLoc({ kind: 'float_lit', value: -Number(token.value) }, token)
2057
- }
2058
- this.error('Expected number after unary -')
2059
- }
2060
- const expr = this.parsePrimaryExpr()
2061
- if (
2062
- expr.kind === 'int_lit' ||
2063
- expr.kind === 'float_lit' ||
2064
- expr.kind === 'bool_lit' ||
2065
- expr.kind === 'str_lit'
2066
- ) {
2067
- return expr
2068
- }
2069
- this.error('Const value must be a literal')
2070
- }
2071
-
2072
- private parseSingleParamLambda(): Expr {
2073
- const paramToken = this.expect('ident')
2074
- const params: LambdaParam[] = [{ name: paramToken.value }]
2075
- this.expect('=>')
2076
- return this.finishLambdaExpr(params, paramToken)
2077
- }
2078
-
2079
- private parseLambdaExpr(): Expr {
2080
- const openParenToken = this.expect('(')
2081
- const params: LambdaParam[] = []
2082
-
2083
- if (!this.check(')')) {
2084
- do {
2085
- const name = this.expect('ident').value
2086
- let type: TypeNode | undefined
2087
- if (this.match(':')) {
2088
- type = this.parseType()
2089
- }
2090
- params.push({ name, type })
2091
- } while (this.match(','))
2092
- }
2093
-
2094
- this.expect(')')
2095
- let returnType: TypeNode | undefined
2096
- if (this.match('->')) {
2097
- returnType = this.parseType()
2098
- }
2099
- this.expect('=>')
2100
- return this.finishLambdaExpr(params, openParenToken, returnType)
2101
- }
2102
-
2103
- private finishLambdaExpr(params: LambdaParam[], token: Token, returnType?: TypeNode): Expr {
2104
- const body = this.check('{') ? this.parseBlock() : this.parseExpr()
2105
- return this.withLoc({ kind: 'lambda', params, returnType, body }, token)
2106
- }
2107
-
2108
- private parseStringExpr(token: Token): Expr {
2109
- if (!token.value.includes('${')) {
2110
- return this.withLoc({ kind: 'str_lit', value: token.value }, token)
2111
- }
2112
-
2113
- const parts: Array<string | Expr> = []
2114
- let current = ''
2115
- let index = 0
2116
-
2117
- while (index < token.value.length) {
2118
- if (token.value[index] === '$' && token.value[index + 1] === '{') {
2119
- if (current) {
2120
- parts.push(current)
2121
- current = ''
2122
- }
2123
-
2124
- index += 2
2125
- let depth = 1
2126
- let exprSource = ''
2127
- let inString = false
2128
-
2129
- while (index < token.value.length && depth > 0) {
2130
- const char = token.value[index]
2131
-
2132
- if (char === '"' && token.value[index - 1] !== '\\') {
2133
- inString = !inString
2134
- }
2135
-
2136
- if (!inString) {
2137
- if (char === '{') {
2138
- depth++
2139
- } else if (char === '}') {
2140
- depth--
2141
- if (depth === 0) {
2142
- index++
2143
- break
2144
- }
2145
- }
2146
- }
2147
-
2148
- if (depth > 0) {
2149
- exprSource += char
2150
- }
2151
- index++
2152
- }
2153
-
2154
- if (depth !== 0) {
2155
- this.error('Unterminated string interpolation')
2156
- }
2157
-
2158
- parts.push(this.parseEmbeddedExpr(exprSource))
2159
- continue
2160
- }
2161
-
2162
- current += token.value[index]
2163
- index++
2164
- }
2165
-
2166
- if (current) {
2167
- parts.push(current)
2168
- }
2169
-
2170
- return this.withLoc({ kind: 'str_interp', parts }, token)
2171
- }
2172
-
2173
- private parseFStringExpr(token: Token): Expr {
2174
- const parts: Array<{ kind: 'text'; value: string } | { kind: 'expr'; expr: Expr }> = []
2175
- let current = ''
2176
- let index = 0
2177
-
2178
- while (index < token.value.length) {
2179
- if (token.value[index] === '{') {
2180
- if (current) {
2181
- parts.push({ kind: 'text', value: current })
2182
- current = ''
2183
- }
2184
-
2185
- index++
2186
- let depth = 1
2187
- let exprSource = ''
2188
- let inString = false
2189
-
2190
- while (index < token.value.length && depth > 0) {
2191
- const char = token.value[index]
2192
-
2193
- if (char === '"' && token.value[index - 1] !== '\\') {
2194
- inString = !inString
2195
- }
2196
-
2197
- if (!inString) {
2198
- if (char === '{') {
2199
- depth++
2200
- } else if (char === '}') {
2201
- depth--
2202
- if (depth === 0) {
2203
- index++
2204
- break
2205
- }
2206
- }
2207
- }
2208
-
2209
- if (depth > 0) {
2210
- exprSource += char
2211
- }
2212
- index++
2213
- }
2214
-
2215
- if (depth !== 0) {
2216
- this.error('Unterminated f-string interpolation')
2217
- }
2218
-
2219
- parts.push({ kind: 'expr', expr: this.parseEmbeddedExpr(exprSource) })
2220
- continue
2221
- }
2222
-
2223
- current += token.value[index]
2224
- index++
2225
- }
2226
-
2227
- if (current) {
2228
- parts.push({ kind: 'text', value: current })
2229
- }
2230
-
2231
- return this.withLoc({ kind: 'f_string', parts }, token)
2232
- }
2233
-
2234
- private parseEmbeddedExpr(source: string): Expr {
2235
- const tokens = new Lexer(source, this.filePath).tokenize()
2236
- const parser = new Parser(tokens, source, this.filePath)
2237
- const expr = parser.parseExpr()
2238
-
2239
- if (!parser.check('eof')) {
2240
- parser.error(`Unexpected token '${parser.peek().kind}' in string interpolation`)
2241
- }
2242
-
2243
- return expr
2244
- }
2245
-
2246
- private parseStructLit(): Expr {
2247
- const braceToken = this.expect('{')
2248
- const fields: { name: string; value: Expr }[] = []
2249
-
2250
- if (!this.check('}')) {
2251
- do {
2252
- const name = this.expect('ident').value
2253
- this.expect(':')
2254
- const value = this.parseExpr()
2255
- fields.push({ name, value })
2256
- } while (this.match(','))
2257
- }
2258
-
2259
- this.expect('}')
2260
- return this.withLoc({ kind: 'struct_lit', fields }, braceToken)
2261
- }
2262
-
2263
- private parseArrayLit(): Expr {
2264
- const bracketToken = this.expect('[')
2265
- const elements: Expr[] = []
2266
-
2267
- if (!this.check(']')) {
2268
- do {
2269
- elements.push(this.parseExpr())
2270
- } while (this.match(','))
2271
- }
2272
-
2273
- this.expect(']')
2274
- return this.withLoc({ kind: 'array_lit', elements }, bracketToken)
2275
- }
2276
-
2277
- private isLambdaStart(): boolean {
2278
- if (!this.check('(')) return false
2279
-
2280
- let offset = 1
2281
- if (this.peek(offset).kind !== ')') {
2282
- while (true) {
2283
- if (this.peek(offset).kind !== 'ident') {
2284
- return false
2285
- }
2286
- offset += 1
2287
-
2288
- if (this.peek(offset).kind === ':') {
2289
- offset += 1
2290
- const consumed = this.typeTokenLength(offset)
2291
- if (consumed === 0) {
2292
- return false
2293
- }
2294
- offset += consumed
2295
- }
2296
-
2297
- if (this.peek(offset).kind === ',') {
2298
- offset += 1
2299
- continue
2300
- }
2301
- break
2302
- }
2303
- }
2304
-
2305
- if (this.peek(offset).kind !== ')') {
2306
- return false
2307
- }
2308
- offset += 1
2309
-
2310
- if (this.peek(offset).kind === '=>') {
2311
- return true
2312
- }
2313
-
2314
- if (this.peek(offset).kind === '->') {
2315
- offset += 1
2316
- const consumed = this.typeTokenLength(offset)
2317
- if (consumed === 0) {
2318
- return false
2319
- }
2320
- offset += consumed
2321
- return this.peek(offset).kind === '=>'
2322
- }
2323
-
2324
- return false
2325
- }
2326
-
2327
- private typeTokenLength(offset: number): number {
2328
- const token = this.peek(offset)
2329
-
2330
- if (token.kind === '(') {
2331
- let inner = offset + 1
2332
- if (this.peek(inner).kind !== ')') {
2333
- while (true) {
2334
- const consumed = this.typeTokenLength(inner)
2335
- if (consumed === 0) {
2336
- return 0
2337
- }
2338
- inner += consumed
2339
- if (this.peek(inner).kind === ',') {
2340
- inner += 1
2341
- continue
2342
- }
2343
- break
2344
- }
2345
- }
2346
-
2347
- if (this.peek(inner).kind !== ')') {
2348
- return 0
2349
- }
2350
- inner += 1
2351
-
2352
- if (this.peek(inner).kind !== '->') {
2353
- return 0
2354
- }
2355
- inner += 1
2356
- const returnLen = this.typeTokenLength(inner)
2357
- return returnLen === 0 ? 0 : inner + returnLen - offset
2358
- }
2359
-
2360
- const isNamedType =
2361
- token.kind === 'int' ||
2362
- token.kind === 'bool' ||
2363
- token.kind === 'float' ||
2364
- token.kind === 'fixed' ||
2365
- token.kind === 'string' ||
2366
- token.kind === 'void' ||
2367
- token.kind === 'BlockPos' ||
2368
- token.kind === 'ident'
2369
- if (!isNamedType) {
2370
- return 0
2371
- }
2372
-
2373
- let length = 1
2374
- while (this.peek(offset + length).kind === '[' && this.peek(offset + length + 1).kind === ']') {
2375
- length += 2
2376
- }
2377
- return length
2378
- }
2379
-
2380
- private isBlockPosLiteral(): boolean {
2381
- if (!this.check('(')) return false
2382
-
2383
- let offset = 1
2384
- for (let i = 0; i < 3; i++) {
2385
- const consumed = this.coordComponentTokenLength(offset)
2386
- if (consumed === 0) return false
2387
- offset += consumed
2388
-
2389
- if (i < 2) {
2390
- if (this.peek(offset).kind !== ',') return false
2391
- offset += 1
2392
- }
2393
- }
2394
-
2395
- return this.peek(offset).kind === ')'
2396
- }
2397
-
2398
- private coordComponentTokenLength(offset: number): number {
2399
- const token = this.peek(offset)
2400
-
2401
- if (token.kind === 'int_lit') {
2402
- return 1
2403
- }
2404
-
2405
- if (token.kind === '-') {
2406
- return this.peek(offset + 1).kind === 'int_lit' ? 2 : 0
2407
- }
2408
-
2409
- // rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) are single tokens now
2410
- if (token.kind === 'rel_coord' || token.kind === 'local_coord') {
2411
- return 1
2412
- }
2413
-
2414
- return 0
2415
- }
2416
-
2417
- private parseBlockPos(): BlockPosExpr {
2418
- const openParenToken = this.expect('(')
2419
- const x = this.parseCoordComponent()
2420
- this.expect(',')
2421
- const y = this.parseCoordComponent()
2422
- this.expect(',')
2423
- const z = this.parseCoordComponent()
2424
- this.expect(')')
2425
- return this.withLoc({ kind: 'blockpos', x, y, z }, openParenToken)
2426
- }
2427
-
2428
- private parseCoordComponent(): CoordComponent {
2429
- const token = this.peek()
2430
-
2431
- // Handle rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) tokens
2432
- if (token.kind === 'rel_coord') {
2433
- this.advance()
2434
- // Parse the offset from the token value (e.g., "~5" -> 5, "~" -> 0, "~-3" -> -3)
2435
- const offset = this.parseCoordOffsetFromValue(token.value.slice(1))
2436
- return { kind: 'relative', offset }
2437
- }
2438
-
2439
- if (token.kind === 'local_coord') {
2440
- this.advance()
2441
- const offset = this.parseCoordOffsetFromValue(token.value.slice(1))
2442
- return { kind: 'local', offset }
2443
- }
2444
-
2445
- return { kind: 'absolute', value: this.parseSignedCoordOffset(true) }
2446
- }
2447
-
2448
- private parseCoordOffsetFromValue(value: string): number {
2449
- if (value === '' || value === undefined) return 0
2450
- return parseFloat(value)
2451
- }
2452
-
2453
- private parseSignedCoordOffset(requireValue = false): number {
2454
- let sign = 1
2455
- if (this.match('-')) {
2456
- sign = -1
2457
- }
2458
-
2459
- if (this.check('int_lit')) {
2460
- return sign * parseInt(this.advance().value, 10)
2461
- }
2462
-
2463
- if (requireValue) {
2464
- this.error('Expected integer coordinate component')
2465
- }
2466
-
2467
- return 0
2468
- }
2469
-
2470
- // -------------------------------------------------------------------------
2471
- // Selector Parsing
2472
- // -------------------------------------------------------------------------
2473
-
2474
- private parseSelector(): EntitySelector {
2475
- const token = this.expect('selector')
2476
- return this.parseSelectorValue(token.value)
2477
- }
2478
-
2479
- // Parse either a selector (@a[...]) or a variable with filters (p[...])
2480
- // Returns { selector } for selectors or { varName, filters } for variables
2481
- private parseSelectorOrVarSelector(): { selector?: EntitySelector, varName?: string, filters?: SelectorFilter } {
2482
- if (this.check('selector')) {
2483
- return { selector: this.parseSelector() }
2484
- }
2485
-
2486
- // Must be an identifier (variable) possibly with filters
2487
- const varToken = this.expect('ident')
2488
- const varName = varToken.value
2489
-
2490
- // Check for optional filters [...]
2491
- if (this.check('[')) {
2492
- this.advance() // consume '['
2493
- // Collect everything until ']'
2494
- let filterStr = ''
2495
- let depth = 1
2496
- while (depth > 0 && !this.check('eof')) {
2497
- if (this.check('[')) depth++
2498
- else if (this.check(']')) depth--
2499
- if (depth > 0) {
2500
- filterStr += this.peek().value ?? this.peek().kind
2501
- this.advance()
2502
- }
2503
- }
2504
- this.expect(']')
2505
- const filters = this.parseSelectorFilters(filterStr)
2506
- return { varName, filters }
2507
- }
2508
-
2509
- return { varName }
2510
- }
2511
-
2512
- private parseSelectorValue(value: string): EntitySelector {
2513
- // Parse @e[type=zombie, distance=..5]
2514
- const bracketIndex = value.indexOf('[')
2515
- if (bracketIndex === -1) {
2516
- return { kind: value as SelectorKind }
2517
- }
2518
-
2519
- const kind = value.slice(0, bracketIndex) as SelectorKind
2520
- const paramsStr = value.slice(bracketIndex + 1, -1) // Remove [ and ]
2521
- const filters = this.parseSelectorFilters(paramsStr)
2522
-
2523
- return { kind, filters }
2524
- }
2525
-
2526
- private parseSelectorFilters(paramsStr: string): SelectorFilter {
2527
- const filters: SelectorFilter = {}
2528
- const parts = this.splitSelectorParams(paramsStr)
2529
-
2530
- for (const part of parts) {
2531
- const eqIndex = part.indexOf('=')
2532
- if (eqIndex === -1) continue
2533
-
2534
- const key = part.slice(0, eqIndex).trim()
2535
- const val = part.slice(eqIndex + 1).trim()
2536
-
2537
- switch (key) {
2538
- case 'type':
2539
- filters.type = val
2540
- break
2541
- case 'distance':
2542
- filters.distance = this.parseRangeValue(val)
2543
- break
2544
- case 'tag':
2545
- if (val.startsWith('!')) {
2546
- filters.notTag = filters.notTag ?? []
2547
- filters.notTag.push(val.slice(1))
2548
- } else {
2549
- filters.tag = filters.tag ?? []
2550
- filters.tag.push(val)
2551
- }
2552
- break
2553
- case 'limit':
2554
- filters.limit = parseInt(val, 10)
2555
- break
2556
- case 'sort':
2557
- filters.sort = val as SelectorFilter['sort']
2558
- break
2559
- case 'nbt':
2560
- filters.nbt = val
2561
- break
2562
- case 'gamemode':
2563
- filters.gamemode = val
2564
- break
2565
- case 'scores':
2566
- filters.scores = this.parseScoresFilter(val)
2567
- break
2568
- case 'x':
2569
- filters.x = this.parseRangeValue(val)
2570
- break
2571
- case 'y':
2572
- filters.y = this.parseRangeValue(val)
2573
- break
2574
- case 'z':
2575
- filters.z = this.parseRangeValue(val)
2576
- break
2577
- case 'x_rotation':
2578
- filters.x_rotation = this.parseRangeValue(val)
2579
- break
2580
- case 'y_rotation':
2581
- filters.y_rotation = this.parseRangeValue(val)
2582
- break
2583
- }
2584
- }
2585
-
2586
- return filters
2587
- }
2588
-
2589
- private splitSelectorParams(str: string): string[] {
2590
- const parts: string[] = []
2591
- let current = ''
2592
- let depth = 0
2593
-
2594
- for (const char of str) {
2595
- if (char === '{' || char === '[') depth++
2596
- else if (char === '}' || char === ']') depth--
2597
- else if (char === ',' && depth === 0) {
2598
- parts.push(current.trim())
2599
- current = ''
2600
- continue
2601
- }
2602
- current += char
2603
- }
2604
-
2605
- if (current.trim()) {
2606
- parts.push(current.trim())
2607
- }
2608
-
2609
- return parts
2610
- }
2611
-
2612
- private parseScoresFilter(val: string): Record<string, RangeExpr> {
2613
- // Parse {kills=1.., deaths=..5}
2614
- const scores: Record<string, RangeExpr> = {}
2615
- const inner = val.slice(1, -1) // Remove { and }
2616
- const parts = inner.split(',')
2617
-
2618
- for (const part of parts) {
2619
- const [name, range] = part.split('=').map(s => s.trim())
2620
- scores[name] = this.parseRangeValue(range)
2621
- }
2622
-
2623
- return scores
2624
- }
2625
-
2626
- private parseRangeValue(value: string): RangeExpr {
2627
- // ..5 → { max: 5 }
2628
- // ..=5 → { max: 5 }
2629
- // 1.. → { min: 1 }
2630
- // 1..= → { min: 1 } (open-ended inclusive, end parsed separately)
2631
- // 1..10 → { min: 1, max: 10 }
2632
- // 1..=10 → { min: 1, max: 10 }
2633
- // 5 → { min: 5, max: 5 } (exact match)
2634
-
2635
- if (value.startsWith('..=')) {
2636
- const rest = value.slice(3)
2637
- if (!rest) return {} // open upper bound, no max
2638
- const max = parseInt(rest, 10)
2639
- return { max }
2640
- }
2641
-
2642
- if (value.startsWith('..')) {
2643
- const rest = value.slice(2)
2644
- if (!rest) return {} // open upper bound, no max
2645
- const max = parseInt(rest, 10)
2646
- return { max }
2647
- }
2648
-
2649
- const inclIdx = value.indexOf('..=')
2650
- if (inclIdx !== -1) {
2651
- const min = parseInt(value.slice(0, inclIdx), 10)
2652
- const rest = value.slice(inclIdx + 3)
2653
- if (!rest) return { min } // open-ended inclusive
2654
- const max = parseInt(rest, 10)
2655
- return { min, max }
2656
- }
2657
-
2658
- const dotIndex = value.indexOf('..')
2659
- if (dotIndex !== -1) {
2660
- const min = parseInt(value.slice(0, dotIndex), 10)
2661
- const rest = value.slice(dotIndex + 2)
2662
- if (!rest) return { min } // open-ended
2663
- const max = parseInt(rest, 10)
2664
- return { min, max }
2665
- }
2666
-
2667
- // Exact value
2668
- const val = parseInt(value, 10)
2669
- return { min: val, max: val }
2670
- }
2671
130
  }