redscript-mc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  3. package/.github/ISSUE_TEMPLATE/wrong_output.md +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +34 -0
  5. package/.github/workflows/ci.yml +29 -0
  6. package/.github/workflows/publish-extension.yml +35 -0
  7. package/LICENSE +21 -0
  8. package/README.md +261 -0
  9. package/README.zh.md +261 -0
  10. package/dist/__tests__/cli.test.d.ts +1 -0
  11. package/dist/__tests__/cli.test.js +140 -0
  12. package/dist/__tests__/codegen.test.d.ts +1 -0
  13. package/dist/__tests__/codegen.test.js +121 -0
  14. package/dist/__tests__/diagnostics.test.d.ts +4 -0
  15. package/dist/__tests__/diagnostics.test.js +149 -0
  16. package/dist/__tests__/e2e.test.d.ts +6 -0
  17. package/dist/__tests__/e2e.test.js +1528 -0
  18. package/dist/__tests__/lexer.test.d.ts +1 -0
  19. package/dist/__tests__/lexer.test.js +316 -0
  20. package/dist/__tests__/lowering.test.d.ts +1 -0
  21. package/dist/__tests__/lowering.test.js +819 -0
  22. package/dist/__tests__/mc-integration.test.d.ts +12 -0
  23. package/dist/__tests__/mc-integration.test.js +395 -0
  24. package/dist/__tests__/mc-syntax.test.d.ts +1 -0
  25. package/dist/__tests__/mc-syntax.test.js +112 -0
  26. package/dist/__tests__/nbt.test.d.ts +1 -0
  27. package/dist/__tests__/nbt.test.js +82 -0
  28. package/dist/__tests__/optimizer-advanced.test.d.ts +1 -0
  29. package/dist/__tests__/optimizer-advanced.test.js +124 -0
  30. package/dist/__tests__/optimizer.test.d.ts +1 -0
  31. package/dist/__tests__/optimizer.test.js +118 -0
  32. package/dist/__tests__/parser.test.d.ts +1 -0
  33. package/dist/__tests__/parser.test.js +717 -0
  34. package/dist/__tests__/repl.test.d.ts +1 -0
  35. package/dist/__tests__/repl.test.js +27 -0
  36. package/dist/__tests__/runtime.test.d.ts +1 -0
  37. package/dist/__tests__/runtime.test.js +276 -0
  38. package/dist/__tests__/structure-optimizer.test.d.ts +1 -0
  39. package/dist/__tests__/structure-optimizer.test.js +33 -0
  40. package/dist/__tests__/typechecker.test.d.ts +1 -0
  41. package/dist/__tests__/typechecker.test.js +364 -0
  42. package/dist/ast/types.d.ts +357 -0
  43. package/dist/ast/types.js +9 -0
  44. package/dist/cli.d.ts +11 -0
  45. package/dist/cli.js +407 -0
  46. package/dist/codegen/cmdblock/index.d.ts +26 -0
  47. package/dist/codegen/cmdblock/index.js +45 -0
  48. package/dist/codegen/mcfunction/index.d.ts +34 -0
  49. package/dist/codegen/mcfunction/index.js +413 -0
  50. package/dist/codegen/structure/index.d.ts +18 -0
  51. package/dist/codegen/structure/index.js +249 -0
  52. package/dist/compile.d.ts +30 -0
  53. package/dist/compile.js +152 -0
  54. package/dist/data/arena/function/__load.mcfunction +6 -0
  55. package/dist/data/arena/function/__tick.mcfunction +2 -0
  56. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
  57. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
  58. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
  59. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
  60. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
  61. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
  62. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
  63. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
  64. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
  65. package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
  66. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
  67. package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
  68. package/dist/data/arena/function/arena_tick.mcfunction +11 -0
  69. package/dist/data/counter/function/__load.mcfunction +5 -0
  70. package/dist/data/counter/function/__tick.mcfunction +2 -0
  71. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
  72. package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
  73. package/dist/data/counter/function/counter_tick.mcfunction +11 -0
  74. package/dist/data/minecraft/tags/function/load.json +5 -0
  75. package/dist/data/minecraft/tags/function/tick.json +5 -0
  76. package/dist/data/quiz/function/__load.mcfunction +16 -0
  77. package/dist/data/quiz/function/__tick.mcfunction +6 -0
  78. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
  79. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
  80. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
  81. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
  82. package/dist/data/quiz/function/answer_a.mcfunction +4 -0
  83. package/dist/data/quiz/function/answer_b.mcfunction +4 -0
  84. package/dist/data/quiz/function/answer_c.mcfunction +4 -0
  85. package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
  86. package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
  87. package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
  88. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
  89. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
  90. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
  91. package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
  92. package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
  93. package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
  94. package/dist/data/quiz/function/ask_question.mcfunction +7 -0
  95. package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
  96. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
  97. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
  98. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
  99. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
  100. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
  101. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
  102. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
  103. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
  104. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
  105. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
  106. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
  107. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
  108. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
  109. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
  110. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
  111. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
  112. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
  113. package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
  114. package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
  115. package/dist/data/shop/function/__load.mcfunction +7 -0
  116. package/dist/data/shop/function/__tick.mcfunction +3 -0
  117. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
  118. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
  119. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
  120. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
  121. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
  122. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
  123. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
  124. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
  125. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
  126. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
  127. package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
  128. package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
  129. package/dist/data/turret/function/__load.mcfunction +5 -0
  130. package/dist/data/turret/function/__tick.mcfunction +4 -0
  131. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
  132. package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
  133. package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
  134. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
  135. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
  136. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
  137. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
  138. package/dist/data/turret/function/turret_tick.mcfunction +5 -0
  139. package/dist/diagnostics/index.d.ts +44 -0
  140. package/dist/diagnostics/index.js +140 -0
  141. package/dist/index.d.ts +53 -0
  142. package/dist/index.js +126 -0
  143. package/dist/ir/builder.d.ts +32 -0
  144. package/dist/ir/builder.js +99 -0
  145. package/dist/ir/types.d.ts +117 -0
  146. package/dist/ir/types.js +15 -0
  147. package/dist/lexer/index.d.ts +36 -0
  148. package/dist/lexer/index.js +458 -0
  149. package/dist/lowering/index.d.ts +106 -0
  150. package/dist/lowering/index.js +2041 -0
  151. package/dist/mc-test/client.d.ts +128 -0
  152. package/dist/mc-test/client.js +174 -0
  153. package/dist/mc-test/runner.d.ts +28 -0
  154. package/dist/mc-test/runner.js +150 -0
  155. package/dist/mc-test/setup.d.ts +11 -0
  156. package/dist/mc-test/setup.js +98 -0
  157. package/dist/mc-validator/index.d.ts +17 -0
  158. package/dist/mc-validator/index.js +322 -0
  159. package/dist/nbt/index.d.ts +86 -0
  160. package/dist/nbt/index.js +250 -0
  161. package/dist/optimizer/commands.d.ts +36 -0
  162. package/dist/optimizer/commands.js +349 -0
  163. package/dist/optimizer/passes.d.ts +34 -0
  164. package/dist/optimizer/passes.js +227 -0
  165. package/dist/optimizer/structure.d.ts +8 -0
  166. package/dist/optimizer/structure.js +344 -0
  167. package/dist/pack.mcmeta +6 -0
  168. package/dist/parser/index.d.ts +76 -0
  169. package/dist/parser/index.js +1193 -0
  170. package/dist/repl.d.ts +16 -0
  171. package/dist/repl.js +165 -0
  172. package/dist/runtime/index.d.ts +101 -0
  173. package/dist/runtime/index.js +1288 -0
  174. package/dist/typechecker/index.d.ts +42 -0
  175. package/dist/typechecker/index.js +629 -0
  176. package/docs/COMPILATION_STATS.md +142 -0
  177. package/docs/IMPLEMENTATION_GUIDE.md +512 -0
  178. package/docs/LANGUAGE_REFERENCE.md +415 -0
  179. package/docs/MC_MAPPING.md +280 -0
  180. package/docs/STRUCTURE_TARGET.md +80 -0
  181. package/docs/mc-reference/commands.md +259 -0
  182. package/editors/vscode/.vscodeignore +10 -0
  183. package/editors/vscode/LICENSE +21 -0
  184. package/editors/vscode/README.md +78 -0
  185. package/editors/vscode/build.mjs +28 -0
  186. package/editors/vscode/icon.png +0 -0
  187. package/editors/vscode/mcfunction-language-configuration.json +28 -0
  188. package/editors/vscode/out/extension.js +7236 -0
  189. package/editors/vscode/package-lock.json +566 -0
  190. package/editors/vscode/package.json +137 -0
  191. package/editors/vscode/redscript-language-configuration.json +28 -0
  192. package/editors/vscode/snippets/redscript.json +114 -0
  193. package/editors/vscode/src/codeactions.ts +89 -0
  194. package/editors/vscode/src/completion.ts +130 -0
  195. package/editors/vscode/src/extension.ts +239 -0
  196. package/editors/vscode/src/hover.ts +1120 -0
  197. package/editors/vscode/src/symbols.ts +207 -0
  198. package/editors/vscode/syntaxes/mcfunction.tmLanguage.json +740 -0
  199. package/editors/vscode/syntaxes/redscript.tmLanguage.json +357 -0
  200. package/editors/vscode/tsconfig.json +13 -0
  201. package/jest.config.js +5 -0
  202. package/package.json +38 -0
  203. package/src/__tests__/cli.test.ts +130 -0
  204. package/src/__tests__/codegen.test.ts +128 -0
  205. package/src/__tests__/diagnostics.test.ts +195 -0
  206. package/src/__tests__/e2e.test.ts +1721 -0
  207. package/src/__tests__/fixtures/mc-commands-1.21.4.json +18734 -0
  208. package/src/__tests__/formatter.test.ts +46 -0
  209. package/src/__tests__/lexer.test.ts +356 -0
  210. package/src/__tests__/lowering.test.ts +962 -0
  211. package/src/__tests__/mc-integration.test.ts +409 -0
  212. package/src/__tests__/mc-syntax.test.ts +96 -0
  213. package/src/__tests__/nbt.test.ts +58 -0
  214. package/src/__tests__/optimizer-advanced.test.ts +144 -0
  215. package/src/__tests__/optimizer.test.ts +129 -0
  216. package/src/__tests__/parser.test.ts +800 -0
  217. package/src/__tests__/repl.test.ts +33 -0
  218. package/src/__tests__/runtime.test.ts +289 -0
  219. package/src/__tests__/structure-optimizer.test.ts +38 -0
  220. package/src/__tests__/typechecker.test.ts +395 -0
  221. package/src/ast/types.ts +248 -0
  222. package/src/cli.ts +445 -0
  223. package/src/codegen/cmdblock/index.ts +63 -0
  224. package/src/codegen/mcfunction/index.ts +471 -0
  225. package/src/codegen/structure/index.ts +305 -0
  226. package/src/compile.ts +188 -0
  227. package/src/diagnostics/index.ts +186 -0
  228. package/src/examples/README.md +77 -0
  229. package/src/examples/SHOWCASE_GAME.md +43 -0
  230. package/src/examples/arena.rs +44 -0
  231. package/src/examples/counter.rs +12 -0
  232. package/src/examples/pvp_arena.rs +131 -0
  233. package/src/examples/quiz.rs +90 -0
  234. package/src/examples/rpg.rs +13 -0
  235. package/src/examples/shop.rs +30 -0
  236. package/src/examples/showcase_game.rs +552 -0
  237. package/src/examples/stdlib_demo.rs +181 -0
  238. package/src/examples/turret.rs +27 -0
  239. package/src/examples/world_manager.rs +23 -0
  240. package/src/formatter/index.ts +22 -0
  241. package/src/index.ts +161 -0
  242. package/src/ir/builder.ts +114 -0
  243. package/src/ir/types.ts +119 -0
  244. package/src/lexer/index.ts +555 -0
  245. package/src/lowering/index.ts +2406 -0
  246. package/src/mc-test/client.ts +259 -0
  247. package/src/mc-test/runner.ts +140 -0
  248. package/src/mc-test/setup.ts +70 -0
  249. package/src/mc-validator/index.ts +367 -0
  250. package/src/nbt/index.ts +321 -0
  251. package/src/optimizer/commands.ts +416 -0
  252. package/src/optimizer/passes.ts +233 -0
  253. package/src/optimizer/structure.ts +441 -0
  254. package/src/parser/index.ts +1437 -0
  255. package/src/repl.ts +165 -0
  256. package/src/runtime/index.ts +1403 -0
  257. package/src/stdlib/README.md +156 -0
  258. package/src/stdlib/combat.rs +20 -0
  259. package/src/stdlib/cooldown.rs +45 -0
  260. package/src/stdlib/math.rs +49 -0
  261. package/src/stdlib/mobs.rs +99 -0
  262. package/src/stdlib/player.rs +29 -0
  263. package/src/stdlib/strings.rs +7 -0
  264. package/src/stdlib/timer.rs +51 -0
  265. package/src/templates/README.md +126 -0
  266. package/src/templates/combat.rs +96 -0
  267. package/src/templates/economy.rs +40 -0
  268. package/src/templates/mini-game-framework.rs +117 -0
  269. package/src/templates/quest.rs +78 -0
  270. package/src/test_programs/zombie_game.rs +25 -0
  271. package/src/typechecker/index.ts +737 -0
  272. package/tsconfig.json +16 -0
@@ -0,0 +1,555 @@
1
+ /**
2
+ * RedScript Lexer
3
+ *
4
+ * Tokenizes RedScript source code into a stream of tokens.
5
+ * Handles special cases like entity selectors vs decorators,
6
+ * range literals, and raw commands.
7
+ */
8
+
9
+ import { DiagnosticError } from '../diagnostics'
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Token Types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export type TokenKind =
16
+ // Keywords
17
+ | 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match'
18
+ | 'return' | 'as' | 'at' | 'in' | 'struct' | 'enum' | 'trigger' | 'namespace'
19
+ | 'execute' | 'run' | 'unless'
20
+ // Types
21
+ | 'int' | 'bool' | 'float' | 'string' | 'void'
22
+ | 'BlockPos'
23
+ // Boolean literals
24
+ | 'true' | 'false'
25
+ // Entity selector
26
+ | 'selector' // @a @e @s @p @r @n (with optional [...] params)
27
+ // Decorator
28
+ | 'decorator' // @tick @on_trigger @tick(rate=N)
29
+ // Literals
30
+ | 'int_lit' // 42
31
+ | 'float_lit' // 3.14
32
+ | 'byte_lit' // 20b
33
+ | 'short_lit' // 100s
34
+ | 'long_lit' // 1000L
35
+ | 'double_lit' // 3.14d
36
+ | 'string_lit' // "hello"
37
+ | 'range_lit' // ..5 1.. 1..10
38
+ // Operators
39
+ | '+' | '-' | '*' | '/' | '%'
40
+ | '~' | '^'
41
+ | '==' | '!=' | '<' | '<=' | '>' | '>='
42
+ | '&&' | '||' | '!'
43
+ | '=' | '+=' | '-=' | '*=' | '/=' | '%='
44
+ // Delimiters
45
+ | '{' | '}' | '(' | ')' | '[' | ']'
46
+ | ',' | ';' | ':' | '::' | '->' | '=>' | '.'
47
+ // Special
48
+ | 'ident' // Variable/function names
49
+ | 'mc_name' // #objective, #tag, #team — unquoted MC identifier
50
+ | 'raw_cmd' // raw("...") content
51
+ | 'eof'
52
+
53
+ export interface Token {
54
+ kind: TokenKind
55
+ value: string // Original text
56
+ line: number
57
+ col: number
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Keywords Map
62
+ // ---------------------------------------------------------------------------
63
+
64
+ const KEYWORDS: Record<string, TokenKind> = {
65
+ fn: 'fn',
66
+ let: 'let',
67
+ const: 'const',
68
+ if: 'if',
69
+ else: 'else',
70
+ while: 'while',
71
+ for: 'for',
72
+ foreach: 'foreach',
73
+ match: 'match',
74
+ return: 'return',
75
+ as: 'as',
76
+ at: 'at',
77
+ in: 'in',
78
+ struct: 'struct',
79
+ enum: 'enum',
80
+ trigger: 'trigger',
81
+ namespace: 'namespace',
82
+ execute: 'execute',
83
+ run: 'run',
84
+ unless: 'unless',
85
+ int: 'int',
86
+ bool: 'bool',
87
+ float: 'float',
88
+ string: 'string',
89
+ void: 'void',
90
+ BlockPos: 'BlockPos',
91
+ true: 'true',
92
+ false: 'false',
93
+ }
94
+
95
+ // Entity selector base characters
96
+ const SELECTOR_CHARS = new Set(['a', 'e', 's', 'p', 'r', 'n'])
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Lexer Class
100
+ // ---------------------------------------------------------------------------
101
+
102
+ export class Lexer {
103
+ private source: string
104
+ private sourceLines: string[]
105
+ private pos: number = 0
106
+ private line: number = 1
107
+ private col: number = 1
108
+ private tokens: Token[] = []
109
+ private filePath?: string
110
+
111
+ constructor(source: string, filePath?: string) {
112
+ this.source = source
113
+ this.sourceLines = source.split('\n')
114
+ this.filePath = filePath
115
+ }
116
+
117
+ private error(message: string, line?: number, col?: number): never {
118
+ throw new DiagnosticError(
119
+ 'LexError',
120
+ message,
121
+ { file: this.filePath, line: line ?? this.line, col: col ?? this.col },
122
+ this.sourceLines
123
+ )
124
+ }
125
+
126
+ tokenize(): Token[] {
127
+ while (!this.isAtEnd()) {
128
+ this.scanToken()
129
+ }
130
+ this.tokens.push({ kind: 'eof', value: '', line: this.line, col: this.col })
131
+ return this.tokens
132
+ }
133
+
134
+ private isAtEnd(): boolean {
135
+ return this.pos >= this.source.length
136
+ }
137
+
138
+ private peek(offset = 0): string {
139
+ const idx = this.pos + offset
140
+ if (idx >= this.source.length) return '\0'
141
+ return this.source[idx]
142
+ }
143
+
144
+ private advance(): string {
145
+ const char = this.source[this.pos++]
146
+ if (char === '\n') {
147
+ this.line++
148
+ this.col = 1
149
+ } else {
150
+ this.col++
151
+ }
152
+ return char
153
+ }
154
+
155
+ private addToken(kind: TokenKind, value: string, line: number, col: number): void {
156
+ this.tokens.push({ kind, value, line, col })
157
+ }
158
+
159
+ private scanToken(): void {
160
+ const startLine = this.line
161
+ const startCol = this.col
162
+
163
+ const char = this.advance()
164
+
165
+ // Whitespace
166
+ if (/\s/.test(char)) return
167
+
168
+ // Comments
169
+ if (char === '/' && this.peek() === '/') {
170
+ // Skip to end of line
171
+ while (!this.isAtEnd() && this.peek() !== '\n') {
172
+ this.advance()
173
+ }
174
+ return
175
+ }
176
+
177
+ // Block comments: /* ... */ and /** ... */
178
+ if (char === '/' && this.peek() === '*') {
179
+ this.advance() // consume '*'
180
+ while (!this.isAtEnd()) {
181
+ if (this.peek() === '*' && this.peek(1) === '/') {
182
+ this.advance() // consume '*'
183
+ this.advance() // consume '/'
184
+ break
185
+ }
186
+ this.advance()
187
+ }
188
+ return
189
+ }
190
+
191
+ // Two-character operators
192
+ if (char === '-' && this.peek() === '>') {
193
+ this.advance()
194
+ this.addToken('->', '->', startLine, startCol)
195
+ return
196
+ }
197
+ if (char === '=' && this.peek() === '>') {
198
+ this.advance()
199
+ this.addToken('=>', '=>', startLine, startCol)
200
+ return
201
+ }
202
+ if (char === '=' && this.peek() === '=') {
203
+ this.advance()
204
+ this.addToken('==', '==', startLine, startCol)
205
+ return
206
+ }
207
+ if (char === '!' && this.peek() === '=') {
208
+ this.advance()
209
+ this.addToken('!=', '!=', startLine, startCol)
210
+ return
211
+ }
212
+ if (char === '<' && this.peek() === '=') {
213
+ this.advance()
214
+ this.addToken('<=', '<=', startLine, startCol)
215
+ return
216
+ }
217
+ if (char === '>' && this.peek() === '=') {
218
+ this.advance()
219
+ this.addToken('>=', '>=', startLine, startCol)
220
+ return
221
+ }
222
+ if (char === '&' && this.peek() === '&') {
223
+ this.advance()
224
+ this.addToken('&&', '&&', startLine, startCol)
225
+ return
226
+ }
227
+ if (char === '|' && this.peek() === '|') {
228
+ this.advance()
229
+ this.addToken('||', '||', startLine, startCol)
230
+ return
231
+ }
232
+ if (char === '+' && this.peek() === '=') {
233
+ this.advance()
234
+ this.addToken('+=', '+=', startLine, startCol)
235
+ return
236
+ }
237
+ if (char === '-' && this.peek() === '=') {
238
+ this.advance()
239
+ this.addToken('-=', '-=', startLine, startCol)
240
+ return
241
+ }
242
+ if (char === '*' && this.peek() === '=') {
243
+ this.advance()
244
+ this.addToken('*=', '*=', startLine, startCol)
245
+ return
246
+ }
247
+ if (char === '/' && this.peek() === '=') {
248
+ this.advance()
249
+ this.addToken('/=', '/=', startLine, startCol)
250
+ return
251
+ }
252
+ if (char === '%' && this.peek() === '=') {
253
+ this.advance()
254
+ this.addToken('%=', '%=', startLine, startCol)
255
+ return
256
+ }
257
+
258
+ // Double colon ::
259
+ if (char === ':' && this.peek() === ':') {
260
+ this.advance()
261
+ this.addToken('::', '::', startLine, startCol)
262
+ return
263
+ }
264
+
265
+ // Range literal starting with ..
266
+ if (char === '.' && this.peek() === '.') {
267
+ this.advance() // consume second .
268
+ let value = '..'
269
+ while (/[0-9]/.test(this.peek())) {
270
+ value += this.advance()
271
+ }
272
+ this.addToken('range_lit', value, startLine, startCol)
273
+ return
274
+ }
275
+
276
+ // Single-character operators and delimiters
277
+ const singleChar: TokenKind[] = ['+', '-', '*', '/', '%', '~', '^', '<', '>', '!', '=',
278
+ '{', '}', '(', ')', '[', ']', ',', ';', ':', '.']
279
+ if (singleChar.includes(char as TokenKind)) {
280
+ this.addToken(char as TokenKind, char, startLine, startCol)
281
+ return
282
+ }
283
+
284
+ // @ - selector or decorator
285
+ if (char === '@') {
286
+ this.scanAtToken(startLine, startCol)
287
+ return
288
+ }
289
+
290
+ // String literal
291
+ if (char === '"') {
292
+ this.scanString(startLine, startCol)
293
+ return
294
+ }
295
+
296
+ // MC name literal: #ident (e.g. #health, #red, #hasKey)
297
+ if (char === '#') {
298
+ const nextChar = this.peek()
299
+ if (/[a-zA-Z_]/.test(nextChar)) {
300
+ let name = '#'
301
+ while (/[a-zA-Z0-9_]/.test(this.peek())) {
302
+ name += this.advance()
303
+ }
304
+ this.addToken('mc_name', name, startLine, startCol)
305
+ return
306
+ }
307
+ // Lone # (not followed by ident) — treat as unknown char error
308
+ this.error(`Unexpected character '#'`, startLine, startCol)
309
+ return
310
+ }
311
+
312
+ // Number (int or float) or range literal starting with number
313
+ if (/[0-9]/.test(char)) {
314
+ this.scanNumber(char, startLine, startCol)
315
+ return
316
+ }
317
+
318
+ // Identifier or keyword
319
+ if (/[a-zA-Z_]/.test(char)) {
320
+ this.scanIdentifier(char, startLine, startCol)
321
+ return
322
+ }
323
+
324
+ this.error(`Unexpected character '${char}'`, startLine, startCol)
325
+ }
326
+
327
+ private scanAtToken(startLine: number, startCol: number): void {
328
+ // Check if it's a selector (@a, @e, @s, @p, @r, @n)
329
+ const nextChar = this.peek()
330
+ const afterNext = this.peek(1)
331
+
332
+ // Selector: @a/@e/@s/@p/@r/@n followed by non-letter (or end, or [)
333
+ if (SELECTOR_CHARS.has(nextChar) && !/[a-zA-Z_0-9]/.test(afterNext)) {
334
+ const selectorChar = this.advance() // consume a/e/s/p/r/n
335
+ let value = '@' + selectorChar
336
+
337
+ // Check for [...] parameters
338
+ if (this.peek() === '[') {
339
+ value += this.scanSelectorParams()
340
+ }
341
+
342
+ this.addToken('selector', value, startLine, startCol)
343
+ return
344
+ }
345
+
346
+ // Otherwise it's a decorator (@tick, @on_trigger, etc.)
347
+ let value = '@'
348
+ while (/[a-zA-Z_0-9]/.test(this.peek())) {
349
+ value += this.advance()
350
+ }
351
+
352
+ // Check for decorator arguments (rate=N)
353
+ if (this.peek() === '(') {
354
+ value += this.advance() // (
355
+ let parenDepth = 1
356
+ while (!this.isAtEnd() && parenDepth > 0) {
357
+ const c = this.advance()
358
+ value += c
359
+ if (c === '(') parenDepth++
360
+ if (c === ')') parenDepth--
361
+ }
362
+ }
363
+
364
+ this.addToken('decorator', value, startLine, startCol)
365
+ }
366
+
367
+ private scanSelectorParams(): string {
368
+ let result = this.advance() // consume [
369
+ let depth = 1
370
+ let braceDepth = 0
371
+
372
+ while (!this.isAtEnd() && depth > 0) {
373
+ const c = this.advance()
374
+ result += c
375
+
376
+ if (c === '{') braceDepth++
377
+ else if (c === '}') braceDepth--
378
+ else if (c === '[' && braceDepth === 0) depth++
379
+ else if (c === ']' && braceDepth === 0) depth--
380
+ }
381
+
382
+ return result
383
+ }
384
+
385
+ private scanString(startLine: number, startCol: number): void {
386
+ let value = ''
387
+ let interpolationDepth = 0
388
+ let interpolationString = false
389
+
390
+ while (!this.isAtEnd()) {
391
+ if (interpolationDepth === 0 && this.peek() === '"') {
392
+ break
393
+ }
394
+
395
+ if (this.peek() === '\\' && this.peek(1) === '"') {
396
+ this.advance() // skip backslash
397
+ value += this.advance() // add escaped quote
398
+ continue
399
+ }
400
+
401
+ if (interpolationDepth === 0 && this.peek() === '$' && this.peek(1) === '{') {
402
+ value += this.advance()
403
+ value += this.advance()
404
+ interpolationDepth = 1
405
+ interpolationString = false
406
+ continue
407
+ }
408
+
409
+ const char = this.advance()
410
+ value += char
411
+
412
+ if (interpolationDepth === 0) continue
413
+
414
+ if (char === '"') {
415
+ interpolationString = !interpolationString
416
+ continue
417
+ }
418
+
419
+ if (interpolationString) continue
420
+
421
+ if (char === '{') interpolationDepth++
422
+ if (char === '}') interpolationDepth--
423
+ }
424
+
425
+ if (this.isAtEnd()) {
426
+ this.error(`Unterminated string`, startLine, startCol)
427
+ }
428
+
429
+ this.advance() // closing quote
430
+ this.addToken('string_lit', value, startLine, startCol)
431
+ }
432
+
433
+ private scanNumber(firstChar: string, startLine: number, startCol: number): void {
434
+ let value = firstChar
435
+
436
+ // Consume integer part
437
+ while (/[0-9]/.test(this.peek())) {
438
+ value += this.advance()
439
+ }
440
+
441
+ // Check for range literal (e.g., 1.., 1..10)
442
+ if (this.peek() === '.' && this.peek(1) === '.') {
443
+ value += this.advance() // first .
444
+ value += this.advance() // second .
445
+ // Optional max value
446
+ while (/[0-9]/.test(this.peek())) {
447
+ value += this.advance()
448
+ }
449
+ this.addToken('range_lit', value, startLine, startCol)
450
+ return
451
+ }
452
+
453
+ // Check for float
454
+ if (this.peek() === '.' && /[0-9]/.test(this.peek(1))) {
455
+ value += this.advance() // .
456
+ while (/[0-9]/.test(this.peek())) {
457
+ value += this.advance()
458
+ }
459
+ // Check for NBT float/double suffix
460
+ const floatSuffix = this.peek().toLowerCase()
461
+ if (floatSuffix === 'f') {
462
+ value += this.advance()
463
+ this.addToken('float_lit', value, startLine, startCol)
464
+ return
465
+ }
466
+ if (floatSuffix === 'd') {
467
+ value += this.advance()
468
+ this.addToken('double_lit', value, startLine, startCol)
469
+ return
470
+ }
471
+ this.addToken('float_lit', value, startLine, startCol)
472
+ return
473
+ }
474
+
475
+ // Check for NBT integer suffix (b, s, L/l, f, d)
476
+ const intSuffix = this.peek().toLowerCase()
477
+ if (intSuffix === 'b' && !/[a-zA-Z_0-9]/.test(this.peek(1))) {
478
+ value += this.advance()
479
+ this.addToken('byte_lit', value, startLine, startCol)
480
+ return
481
+ }
482
+ if (intSuffix === 's' && !/[a-zA-Z_0-9]/.test(this.peek(1))) {
483
+ value += this.advance()
484
+ this.addToken('short_lit', value, startLine, startCol)
485
+ return
486
+ }
487
+ if (intSuffix === 'l' && !/[a-zA-Z_0-9]/.test(this.peek(1))) {
488
+ value += this.advance()
489
+ this.addToken('long_lit', value, startLine, startCol)
490
+ return
491
+ }
492
+ if (intSuffix === 'f' && !/[a-zA-Z_0-9]/.test(this.peek(1))) {
493
+ value += this.advance()
494
+ this.addToken('float_lit', value, startLine, startCol)
495
+ return
496
+ }
497
+ if (intSuffix === 'd' && !/[a-zA-Z_0-9]/.test(this.peek(1))) {
498
+ value += this.advance()
499
+ this.addToken('double_lit', value, startLine, startCol)
500
+ return
501
+ }
502
+
503
+ this.addToken('int_lit', value, startLine, startCol)
504
+ }
505
+
506
+ private scanIdentifier(firstChar: string, startLine: number, startCol: number): void {
507
+ let value = firstChar
508
+
509
+ while (/[a-zA-Z_0-9]/.test(this.peek())) {
510
+ value += this.advance()
511
+ }
512
+
513
+ // Check for raw command
514
+ if (value === 'raw' && this.peek() === '(') {
515
+ this.advance() // consume (
516
+ // Skip whitespace
517
+ while (/\s/.test(this.peek())) {
518
+ this.advance()
519
+ }
520
+ // Expect string
521
+ if (this.peek() === '"') {
522
+ this.advance() // consume opening quote
523
+ let rawContent = ''
524
+ while (!this.isAtEnd() && this.peek() !== '"') {
525
+ if (this.peek() === '\\' && this.peek(1) === '"') {
526
+ this.advance()
527
+ rawContent += this.advance()
528
+ } else {
529
+ rawContent += this.advance()
530
+ }
531
+ }
532
+ if (this.peek() === '"') {
533
+ this.advance() // closing quote
534
+ }
535
+ // Skip whitespace and closing paren
536
+ while (/\s/.test(this.peek())) {
537
+ this.advance()
538
+ }
539
+ if (this.peek() === ')') {
540
+ this.advance() // closing paren
541
+ }
542
+ this.addToken('raw_cmd', rawContent, startLine, startCol)
543
+ return
544
+ }
545
+ }
546
+
547
+ // Check for keyword
548
+ const keyword = KEYWORDS[value]
549
+ if (keyword) {
550
+ this.addToken(keyword, value, startLine, startCol)
551
+ } else {
552
+ this.addToken('ident', value, startLine, startCol)
553
+ }
554
+ }
555
+ }