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,800 @@
1
+ import { Lexer } from '../lexer'
2
+ import { Parser } from '../parser'
3
+ import type { Program, FnDecl, Stmt, Expr } from '../ast/types'
4
+
5
+ function parse(source: string, namespace = 'test'): Program {
6
+ const tokens = new Lexer(source).tokenize()
7
+ return new Parser(tokens).parse(namespace)
8
+ }
9
+
10
+ function parseExpr(source: string): Expr {
11
+ const program = parse(`fn _test() { ${source}; }`)
12
+ const stmt = program.declarations[0].body[0]
13
+ if (stmt.kind !== 'expr') throw new Error('Expected expr stmt')
14
+ return stmt.expr
15
+ }
16
+
17
+ function parseStmt(source: string): Stmt {
18
+ const program = parse(`fn _test() { ${source} }`)
19
+ return program.declarations[0].body[0]
20
+ }
21
+
22
+ describe('Parser', () => {
23
+ describe('program structure', () => {
24
+ it('parses empty program', () => {
25
+ const program = parse('')
26
+ expect(program.namespace).toBe('test')
27
+ expect(program.declarations).toEqual([])
28
+ expect(program.enums).toEqual([])
29
+ expect(program.consts).toEqual([])
30
+ })
31
+
32
+ it('parses namespace declaration', () => {
33
+ const program = parse('namespace mypack;', 'default')
34
+ expect(program.namespace).toBe('mypack')
35
+ })
36
+
37
+ it('parses function declaration', () => {
38
+ const program = parse('fn foo() {}')
39
+ expect(program.declarations).toHaveLength(1)
40
+ expect(program.declarations[0].name).toBe('foo')
41
+ })
42
+
43
+ it('parses top-level const declarations', () => {
44
+ const program = parse('const MAX_HP: int = 100\nconst NAME: string = "Arena"')
45
+ expect(program.consts).toEqual([
46
+ { name: 'MAX_HP', type: { kind: 'named', name: 'int' }, value: { kind: 'int_lit', value: 100 } },
47
+ { name: 'NAME', type: { kind: 'named', name: 'string' }, value: { kind: 'str_lit', value: 'Arena' } },
48
+ ])
49
+ })
50
+ })
51
+
52
+ describe('function declarations', () => {
53
+ it('parses function with no params', () => {
54
+ const program = parse('fn hello() {}')
55
+ const fn = program.declarations[0]
56
+ expect(fn.name).toBe('hello')
57
+ expect(fn.params).toEqual([])
58
+ expect(fn.returnType).toEqual({ kind: 'named', name: 'void' })
59
+ })
60
+
61
+ it('parses function with params', () => {
62
+ const program = parse('fn add(a: int, b: int) -> int { return a + b; }')
63
+ const fn = program.declarations[0]
64
+ expect(fn.name).toBe('add')
65
+ expect(fn.params).toEqual([
66
+ { name: 'a', type: { kind: 'named', name: 'int' }, default: undefined },
67
+ { name: 'b', type: { kind: 'named', name: 'int' }, default: undefined },
68
+ ])
69
+ expect(fn.returnType).toEqual({ kind: 'named', name: 'int' })
70
+ })
71
+
72
+ it('parses function params with defaults', () => {
73
+ const program = parse('fn greet(name: string, formal: bool = false) {}')
74
+ expect(program.declarations[0].params).toEqual([
75
+ { name: 'name', type: { kind: 'named', name: 'string' }, default: undefined },
76
+ { name: 'formal', type: { kind: 'named', name: 'bool' }, default: { kind: 'bool_lit', value: false } },
77
+ ])
78
+ })
79
+
80
+ it('parses function with decorators', () => {
81
+ const program = parse('@tick\nfn game_loop() {}')
82
+ const fn = program.declarations[0]
83
+ expect(fn.decorators).toEqual([{ name: 'tick' }])
84
+ })
85
+
86
+ it('parses decorator with args', () => {
87
+ const program = parse('@tick(rate=20)\nfn slow_loop() {}')
88
+ const fn = program.declarations[0]
89
+ expect(fn.decorators).toEqual([{ name: 'tick', args: { rate: 20 } }])
90
+ })
91
+
92
+ it('parses multiple decorators', () => {
93
+ const program = parse('@tick\n@on_trigger\nfn both() {}')
94
+ const fn = program.declarations[0]
95
+ expect(fn.decorators).toHaveLength(2)
96
+ })
97
+
98
+ it('parses advancement and death decorators', () => {
99
+ const program = parse('@on_advancement("story/mine_diamond")\n@on_death\nfn handler() {}')
100
+ expect(program.declarations[0].decorators).toEqual([
101
+ { name: 'on_advancement', args: { advancement: 'story/mine_diamond' } },
102
+ { name: 'on_death' },
103
+ ])
104
+ })
105
+ })
106
+
107
+ describe('types', () => {
108
+ it('parses primitive types', () => {
109
+ const program = parse('fn f(a: int, b: bool, c: float, d: string) {}')
110
+ const params = program.declarations[0].params
111
+ expect(params.map(p => p.type)).toEqual([
112
+ { kind: 'named', name: 'int' },
113
+ { kind: 'named', name: 'bool' },
114
+ { kind: 'named', name: 'float' },
115
+ { kind: 'named', name: 'string' },
116
+ ])
117
+ })
118
+
119
+ it('parses array types', () => {
120
+ const program = parse('fn f(a: int[]) {}')
121
+ const param = program.declarations[0].params[0]
122
+ expect(param.type).toEqual({ kind: 'array', elem: { kind: 'named', name: 'int' } })
123
+ })
124
+
125
+ it('parses BlockPos types', () => {
126
+ const program = parse('fn f(pos: BlockPos) {}')
127
+ const param = program.declarations[0].params[0]
128
+ expect(param.type).toEqual({ kind: 'named', name: 'BlockPos' })
129
+ })
130
+
131
+ it('parses function types', () => {
132
+ const program = parse('fn apply(val: int, cb: (int) -> int) -> int { return cb(val); }')
133
+ expect(program.declarations[0].params[1].type).toEqual({
134
+ kind: 'function_type',
135
+ params: [{ kind: 'named', name: 'int' }],
136
+ return: { kind: 'named', name: 'int' },
137
+ })
138
+ })
139
+
140
+ it('parses enum declarations', () => {
141
+ const program = parse('enum Direction { North, South = 3, East, West }')
142
+ expect(program.enums).toEqual([
143
+ {
144
+ name: 'Direction',
145
+ variants: [
146
+ { name: 'North', value: 0 },
147
+ { name: 'South', value: 3 },
148
+ { name: 'East', value: 4 },
149
+ { name: 'West', value: 5 },
150
+ ],
151
+ },
152
+ ])
153
+ })
154
+ })
155
+
156
+ describe('statements', () => {
157
+ it('parses let statement', () => {
158
+ const stmt = parseStmt('let x: int = 5;')
159
+ expect(stmt).toEqual({
160
+ kind: 'let',
161
+ name: 'x',
162
+ type: { kind: 'named', name: 'int' },
163
+ init: { kind: 'int_lit', value: 5 },
164
+ })
165
+ })
166
+
167
+ it('parses let without type annotation', () => {
168
+ const stmt = parseStmt('let x = 5;')
169
+ expect(stmt.kind).toBe('let')
170
+ expect((stmt as any).type).toBeUndefined()
171
+ })
172
+
173
+ it('parses return statement', () => {
174
+ const stmt = parseStmt('return 42;')
175
+ expect(stmt).toEqual({
176
+ kind: 'return',
177
+ value: { kind: 'int_lit', value: 42 },
178
+ })
179
+ })
180
+
181
+ it('parses empty return', () => {
182
+ const stmt = parseStmt('return;')
183
+ expect(stmt).toEqual({ kind: 'return', value: undefined })
184
+ })
185
+
186
+ it('parses if statement', () => {
187
+ const stmt = parseStmt('if (x > 0) { y = 1; }')
188
+ expect(stmt.kind).toBe('if')
189
+ expect((stmt as any).cond.kind).toBe('binary')
190
+ expect((stmt as any).then).toHaveLength(1)
191
+ expect((stmt as any).else_).toBeUndefined()
192
+ })
193
+
194
+ it('parses if-else statement', () => {
195
+ const stmt = parseStmt('if (x > 0) { y = 1; } else { y = 2; }')
196
+ expect(stmt.kind).toBe('if')
197
+ expect((stmt as any).else_).toHaveLength(1)
198
+ })
199
+
200
+ it('parses while statement', () => {
201
+ const stmt = parseStmt('while (i > 0) { i = i - 1; }')
202
+ expect(stmt.kind).toBe('while')
203
+ expect((stmt as any).cond.kind).toBe('binary')
204
+ expect((stmt as any).body).toHaveLength(1)
205
+ })
206
+
207
+ it('parses for statement', () => {
208
+ const stmt = parseStmt('for (let i: int = 0; i < 10; i = i + 1) { say("loop"); }')
209
+ expect(stmt.kind).toBe('for')
210
+ expect((stmt as any).init.kind).toBe('let')
211
+ expect((stmt as any).init.name).toBe('i')
212
+ expect((stmt as any).cond.kind).toBe('binary')
213
+ expect((stmt as any).cond.op).toBe('<')
214
+ expect((stmt as any).step.kind).toBe('assign')
215
+ expect((stmt as any).body).toHaveLength(1)
216
+ })
217
+
218
+ it('parses for statement without init', () => {
219
+ const stmt = parseStmt('for (; i < 10; i = i + 1) { say("loop"); }')
220
+ expect(stmt.kind).toBe('for')
221
+ expect((stmt as any).init).toBeUndefined()
222
+ expect((stmt as any).cond.kind).toBe('binary')
223
+ })
224
+
225
+ it('parses foreach statement', () => {
226
+ const stmt = parseStmt('foreach (z in @e[type=zombie]) { kill(z); }')
227
+ expect(stmt.kind).toBe('foreach')
228
+ expect((stmt as any).binding).toBe('z')
229
+ expect((stmt as any).iterable.kind).toBe('selector')
230
+ expect((stmt as any).iterable.sel.kind).toBe('@e')
231
+ })
232
+
233
+ it('parses match statement', () => {
234
+ const stmt = parseStmt('match (choice) { 1 => { say("one"); } 2 => { say("two"); } _ => { say("other"); } }')
235
+ expect(stmt.kind).toBe('match')
236
+ expect((stmt as any).expr).toEqual({ kind: 'ident', name: 'choice' })
237
+ expect((stmt as any).arms).toEqual([
238
+ { pattern: { kind: 'int_lit', value: 1 }, body: [{ kind: 'expr', expr: { kind: 'call', fn: 'say', args: [{ kind: 'str_lit', value: 'one' }] } }] },
239
+ { pattern: { kind: 'int_lit', value: 2 }, body: [{ kind: 'expr', expr: { kind: 'call', fn: 'say', args: [{ kind: 'str_lit', value: 'two' }] } }] },
240
+ { pattern: null, body: [{ kind: 'expr', expr: { kind: 'call', fn: 'say', args: [{ kind: 'str_lit', value: 'other' }] } }] },
241
+ ])
242
+ })
243
+
244
+ it('parses as block', () => {
245
+ const stmt = parseStmt('as @a { say("hello"); }')
246
+ expect(stmt.kind).toBe('as_block')
247
+ expect((stmt as any).selector.kind).toBe('@a')
248
+ })
249
+
250
+ it('parses at block', () => {
251
+ const stmt = parseStmt('at @s { summon("zombie"); }')
252
+ expect(stmt.kind).toBe('at_block')
253
+ expect((stmt as any).selector.kind).toBe('@s')
254
+ })
255
+
256
+ it('parses as at combined', () => {
257
+ const stmt = parseStmt('as @a at @s { particle("flame"); }')
258
+ expect(stmt.kind).toBe('as_at')
259
+ expect((stmt as any).as_sel.kind).toBe('@a')
260
+ expect((stmt as any).at_sel.kind).toBe('@s')
261
+ })
262
+
263
+ it('parses raw command', () => {
264
+ const stmt = parseStmt('raw("say hello");')
265
+ expect(stmt).toEqual({ kind: 'raw', cmd: 'say hello' })
266
+ })
267
+
268
+ it('parses execute as run block', () => {
269
+ const stmt = parseStmt('execute as @a run { say("hello"); }')
270
+ expect(stmt.kind).toBe('execute')
271
+ expect((stmt as any).subcommands).toHaveLength(1)
272
+ expect((stmt as any).subcommands[0]).toEqual({ kind: 'as', selector: { kind: '@a' } })
273
+ expect((stmt as any).body).toHaveLength(1)
274
+ })
275
+
276
+ it('parses execute as at run block', () => {
277
+ const stmt = parseStmt('execute as @a at @s run { particle("flame"); }')
278
+ expect(stmt.kind).toBe('execute')
279
+ expect((stmt as any).subcommands).toHaveLength(2)
280
+ expect((stmt as any).subcommands[0]).toEqual({ kind: 'as', selector: { kind: '@a' } })
281
+ expect((stmt as any).subcommands[1]).toEqual({ kind: 'at', selector: { kind: '@s' } })
282
+ })
283
+
284
+ it('parses execute with if entity condition', () => {
285
+ const stmt = parseStmt('execute as @a if entity @s[tag=admin] run { give(@s, "diamond", 1); }')
286
+ expect(stmt.kind).toBe('execute')
287
+ expect((stmt as any).subcommands).toHaveLength(2)
288
+ expect((stmt as any).subcommands[1].kind).toBe('if_entity')
289
+ expect((stmt as any).subcommands[1].selector.filters.tag).toEqual(['admin'])
290
+ })
291
+
292
+ it('parses execute with unless entity condition', () => {
293
+ const stmt = parseStmt('execute as @a unless entity @s[tag=dead] run { effect(@s, "regeneration", 5); }')
294
+ expect(stmt.kind).toBe('execute')
295
+ expect((stmt as any).subcommands).toHaveLength(2)
296
+ expect((stmt as any).subcommands[1].kind).toBe('unless_entity')
297
+ })
298
+
299
+ it('parses execute with in dimension', () => {
300
+ const stmt = parseStmt('execute in the_nether run { say("in nether"); }')
301
+ expect(stmt.kind).toBe('execute')
302
+ expect((stmt as any).subcommands).toHaveLength(1)
303
+ expect((stmt as any).subcommands[0]).toEqual({ kind: 'in', dimension: 'the_nether' })
304
+ })
305
+
306
+ it('parses complex execute chain', () => {
307
+ const stmt = parseStmt('execute as @a at @s if entity @s[tag=vip] in overworld run { particle("heart"); }')
308
+ expect(stmt.kind).toBe('execute')
309
+ expect((stmt as any).subcommands).toHaveLength(4)
310
+ })
311
+ })
312
+
313
+ describe('lambda expressions', () => {
314
+ it('parses expression-body lambdas', () => {
315
+ const stmt = parseStmt('let double = (x: int) => x * 2;')
316
+ expect(stmt.kind).toBe('let')
317
+ expect((stmt as any).init).toEqual({
318
+ kind: 'lambda',
319
+ params: [{ name: 'x', type: { kind: 'named', name: 'int' } }],
320
+ returnType: undefined,
321
+ body: {
322
+ kind: 'binary',
323
+ op: '*',
324
+ left: { kind: 'ident', name: 'x' },
325
+ right: { kind: 'int_lit', value: 2 },
326
+ },
327
+ })
328
+ })
329
+
330
+ it('parses block-body lambdas', () => {
331
+ const stmt = parseStmt('let process: (int) -> int = (x: int) => { let doubled: int = x * 2; return doubled + 1; };')
332
+ expect(stmt.kind).toBe('let')
333
+ expect((stmt as any).init.kind).toBe('lambda')
334
+ expect(Array.isArray((stmt as any).init.body)).toBe(true)
335
+ })
336
+
337
+ it('parses single-parameter lambdas without parens', () => {
338
+ const stmt = parseStmt('let double: (int) -> int = x => x * 2;')
339
+ expect(stmt.kind).toBe('let')
340
+ expect((stmt as any).init).toEqual({
341
+ kind: 'lambda',
342
+ params: [{ name: 'x' }],
343
+ returnType: undefined,
344
+ body: {
345
+ kind: 'binary',
346
+ op: '*',
347
+ left: { kind: 'ident', name: 'x' },
348
+ right: { kind: 'int_lit', value: 2 },
349
+ },
350
+ })
351
+ })
352
+
353
+ it('parses immediately-invoked lambdas', () => {
354
+ const expr = parseExpr('((x: int) => x * 2)(5)')
355
+ expect(expr).toEqual({
356
+ kind: 'invoke',
357
+ callee: {
358
+ kind: 'lambda',
359
+ params: [{ name: 'x', type: { kind: 'named', name: 'int' } }],
360
+ returnType: undefined,
361
+ body: {
362
+ kind: 'binary',
363
+ op: '*',
364
+ left: { kind: 'ident', name: 'x' },
365
+ right: { kind: 'int_lit', value: 2 },
366
+ },
367
+ },
368
+ args: [{ kind: 'int_lit', value: 5 }],
369
+ })
370
+ })
371
+ })
372
+
373
+ describe('expressions', () => {
374
+ describe('literals', () => {
375
+ it('parses integer literal', () => {
376
+ const expr = parseExpr('42')
377
+ expect(expr).toEqual({ kind: 'int_lit', value: 42 })
378
+ })
379
+
380
+ it('parses float literal', () => {
381
+ const expr = parseExpr('3.14')
382
+ expect(expr).toEqual({ kind: 'float_lit', value: 3.14 })
383
+ })
384
+
385
+ it('parses string literal', () => {
386
+ const expr = parseExpr('"hello"')
387
+ expect(expr).toEqual({ kind: 'str_lit', value: 'hello' })
388
+ })
389
+
390
+ it('parses interpolated string literal', () => {
391
+ const expr = parseExpr('"Hello ${name}, score is ${score + 1}"')
392
+ expect(expr).toEqual({
393
+ kind: 'str_interp',
394
+ parts: [
395
+ 'Hello ',
396
+ { kind: 'ident', name: 'name' },
397
+ ', score is ',
398
+ {
399
+ kind: 'binary',
400
+ op: '+',
401
+ left: { kind: 'ident', name: 'score' },
402
+ right: { kind: 'int_lit', value: 1 },
403
+ },
404
+ ],
405
+ })
406
+ })
407
+
408
+ it('parses boolean literals', () => {
409
+ expect(parseExpr('true')).toEqual({ kind: 'bool_lit', value: true })
410
+ expect(parseExpr('false')).toEqual({ kind: 'bool_lit', value: false })
411
+ })
412
+
413
+ it('parses range literals', () => {
414
+ expect(parseExpr('..5')).toEqual({ kind: 'range_lit', range: { max: 5 } })
415
+ expect(parseExpr('1..')).toEqual({ kind: 'range_lit', range: { min: 1 } })
416
+ expect(parseExpr('1..10')).toEqual({ kind: 'range_lit', range: { min: 1, max: 10 } })
417
+ })
418
+
419
+ it('parses absolute block positions', () => {
420
+ expect(parseExpr('(0, 64, 0)')).toEqual({
421
+ kind: 'blockpos',
422
+ x: { kind: 'absolute', value: 0 },
423
+ y: { kind: 'absolute', value: 64 },
424
+ z: { kind: 'absolute', value: 0 },
425
+ })
426
+ })
427
+
428
+ it('parses relative block positions', () => {
429
+ expect(parseExpr('(~1, ~0, ~-1)')).toEqual({
430
+ kind: 'blockpos',
431
+ x: { kind: 'relative', offset: 1 },
432
+ y: { kind: 'relative', offset: 0 },
433
+ z: { kind: 'relative', offset: -1 },
434
+ })
435
+ })
436
+
437
+ it('parses local block positions', () => {
438
+ expect(parseExpr('(^0, ^1, ^0)')).toEqual({
439
+ kind: 'blockpos',
440
+ x: { kind: 'local', offset: 0 },
441
+ y: { kind: 'local', offset: 1 },
442
+ z: { kind: 'local', offset: 0 },
443
+ })
444
+ })
445
+
446
+ it('parses mixed block positions', () => {
447
+ expect(parseExpr('(~0, 64, ~0)')).toEqual({
448
+ kind: 'blockpos',
449
+ x: { kind: 'relative', offset: 0 },
450
+ y: { kind: 'absolute', value: 64 },
451
+ z: { kind: 'relative', offset: 0 },
452
+ })
453
+ })
454
+ })
455
+
456
+ describe('identifiers and calls', () => {
457
+ it('parses identifier', () => {
458
+ const expr = parseExpr('foo')
459
+ expect(expr).toEqual({ kind: 'ident', name: 'foo' })
460
+ })
461
+
462
+ it('parses function call', () => {
463
+ const expr = parseExpr('foo(1, 2)')
464
+ expect(expr).toEqual({
465
+ kind: 'call',
466
+ fn: 'foo',
467
+ args: [
468
+ { kind: 'int_lit', value: 1 },
469
+ { kind: 'int_lit', value: 2 },
470
+ ],
471
+ })
472
+ })
473
+
474
+ it('parses no-arg call', () => {
475
+ const expr = parseExpr('foo()')
476
+ expect(expr).toEqual({ kind: 'call', fn: 'foo', args: [] })
477
+ })
478
+
479
+ it('parses enum variant member access', () => {
480
+ const expr = parseExpr('Direction.North')
481
+ expect(expr).toEqual({
482
+ kind: 'member',
483
+ obj: { kind: 'ident', name: 'Direction' },
484
+ field: 'North',
485
+ })
486
+ })
487
+ })
488
+
489
+ describe('binary operators', () => {
490
+ it('parses arithmetic', () => {
491
+ const expr = parseExpr('1 + 2')
492
+ expect(expr).toEqual({
493
+ kind: 'binary',
494
+ op: '+',
495
+ left: { kind: 'int_lit', value: 1 },
496
+ right: { kind: 'int_lit', value: 2 },
497
+ })
498
+ })
499
+
500
+ it('respects precedence (mul before add)', () => {
501
+ const expr = parseExpr('1 + 2 * 3')
502
+ expect(expr.kind).toBe('binary')
503
+ expect((expr as any).op).toBe('+')
504
+ expect((expr as any).right.op).toBe('*')
505
+ })
506
+
507
+ it('respects precedence (compare before logical)', () => {
508
+ const expr = parseExpr('a < b && c > d')
509
+ expect(expr.kind).toBe('binary')
510
+ expect((expr as any).op).toBe('&&')
511
+ expect((expr as any).left.op).toBe('<')
512
+ expect((expr as any).right.op).toBe('>')
513
+ })
514
+
515
+ it('is left associative', () => {
516
+ const expr = parseExpr('1 - 2 - 3')
517
+ // Should be (1 - 2) - 3
518
+ expect(expr.kind).toBe('binary')
519
+ expect((expr as any).op).toBe('-')
520
+ expect((expr as any).left.kind).toBe('binary')
521
+ expect((expr as any).right.kind).toBe('int_lit')
522
+ })
523
+ })
524
+
525
+ describe('unary operators', () => {
526
+ it('parses negation', () => {
527
+ const expr = parseExpr('-5')
528
+ expect(expr).toEqual({
529
+ kind: 'unary',
530
+ op: '-',
531
+ operand: { kind: 'int_lit', value: 5 },
532
+ })
533
+ })
534
+
535
+ it('parses logical not', () => {
536
+ const expr = parseExpr('!flag')
537
+ expect(expr).toEqual({
538
+ kind: 'unary',
539
+ op: '!',
540
+ operand: { kind: 'ident', name: 'flag' },
541
+ })
542
+ })
543
+ })
544
+
545
+ describe('assignment', () => {
546
+ it('parses simple assignment', () => {
547
+ const expr = parseExpr('x = 5')
548
+ expect(expr).toEqual({
549
+ kind: 'assign',
550
+ target: 'x',
551
+ op: '=',
552
+ value: { kind: 'int_lit', value: 5 },
553
+ })
554
+ })
555
+
556
+ it('parses compound assignment', () => {
557
+ const expr = parseExpr('x += 1')
558
+ expect(expr).toEqual({
559
+ kind: 'assign',
560
+ target: 'x',
561
+ op: '+=',
562
+ value: { kind: 'int_lit', value: 1 },
563
+ })
564
+ })
565
+ })
566
+
567
+ describe('selectors', () => {
568
+ it('parses simple selector', () => {
569
+ const expr = parseExpr('@a')
570
+ expect(expr).toEqual({
571
+ kind: 'selector',
572
+ raw: '@a',
573
+ isSingle: false,
574
+ sel: { kind: '@a' },
575
+ })
576
+ })
577
+
578
+ it('marks single-entity selectors', () => {
579
+ expect(parseExpr('@p')).toEqual({
580
+ kind: 'selector',
581
+ raw: '@p',
582
+ isSingle: true,
583
+ sel: { kind: '@p' },
584
+ })
585
+ expect(parseExpr('@e[limit=1, tag=target]')).toEqual({
586
+ kind: 'selector',
587
+ raw: '@e[limit=1, tag=target]',
588
+ isSingle: true,
589
+ sel: {
590
+ kind: '@e',
591
+ filters: { limit: 1, tag: ['target'] },
592
+ },
593
+ })
594
+ })
595
+
596
+ it('parses selector with type filter', () => {
597
+ const expr = parseExpr('@e[type=zombie]')
598
+ expect(expr).toEqual({
599
+ kind: 'selector',
600
+ raw: '@e[type=zombie]',
601
+ isSingle: false,
602
+ sel: {
603
+ kind: '@e',
604
+ filters: { type: 'zombie' },
605
+ },
606
+ })
607
+ })
608
+
609
+ it('parses selector with distance filter', () => {
610
+ const expr = parseExpr('@e[distance=..5]')
611
+ expect((expr as any).sel.filters.distance).toEqual({ max: 5 })
612
+ })
613
+
614
+ it('parses selector with tag filter', () => {
615
+ const expr = parseExpr('@e[tag=boss, tag=!excluded]')
616
+ expect((expr as any).sel.filters.tag).toEqual(['boss'])
617
+ expect((expr as any).sel.filters.notTag).toEqual(['excluded'])
618
+ })
619
+
620
+ it('parses selector with limit and sort', () => {
621
+ const expr = parseExpr('@e[limit=1, sort=nearest]')
622
+ expect((expr as any).sel.filters.limit).toBe(1)
623
+ expect((expr as any).sel.filters.sort).toBe('nearest')
624
+ })
625
+
626
+ it('parses selector with scores', () => {
627
+ const expr = parseExpr('@a[scores={kills=1..}]')
628
+ expect((expr as any).sel.filters.scores).toEqual({
629
+ kills: { min: 1 },
630
+ })
631
+ })
632
+ })
633
+
634
+ describe('member access', () => {
635
+ it('parses member access', () => {
636
+ const expr = parseExpr('entity.health')
637
+ expect(expr).toEqual({
638
+ kind: 'member',
639
+ obj: { kind: 'ident', name: 'entity' },
640
+ field: 'health',
641
+ })
642
+ })
643
+
644
+ it('parses array len property', () => {
645
+ const expr = parseExpr('arr.len')
646
+ expect(expr).toEqual({
647
+ kind: 'member',
648
+ obj: { kind: 'ident', name: 'arr' },
649
+ field: 'len',
650
+ })
651
+ })
652
+ })
653
+
654
+ describe('arrays', () => {
655
+ it('parses array literal', () => {
656
+ expect(parseExpr('[1, 2, 3]')).toEqual({
657
+ kind: 'array_lit',
658
+ elements: [
659
+ { kind: 'int_lit', value: 1 },
660
+ { kind: 'int_lit', value: 2 },
661
+ { kind: 'int_lit', value: 3 },
662
+ ],
663
+ })
664
+ })
665
+
666
+ it('parses array index access', () => {
667
+ expect(parseExpr('arr[i]')).toEqual({
668
+ kind: 'index',
669
+ obj: { kind: 'ident', name: 'arr' },
670
+ index: { kind: 'ident', name: 'i' },
671
+ })
672
+ })
673
+
674
+ it('parses array push call', () => {
675
+ expect(parseExpr('arr.push(4)')).toEqual({
676
+ kind: 'call',
677
+ fn: '__array_push',
678
+ args: [
679
+ { kind: 'ident', name: 'arr' },
680
+ { kind: 'int_lit', value: 4 },
681
+ ],
682
+ })
683
+ })
684
+
685
+ it('parses array pop call', () => {
686
+ expect(parseExpr('arr.pop()')).toEqual({
687
+ kind: 'call',
688
+ fn: '__array_pop',
689
+ args: [
690
+ { kind: 'ident', name: 'arr' },
691
+ ],
692
+ })
693
+ })
694
+ })
695
+
696
+ describe('grouping', () => {
697
+ it('parses parenthesized expression', () => {
698
+ const expr = parseExpr('(1 + 2) * 3')
699
+ expect(expr.kind).toBe('binary')
700
+ expect((expr as any).op).toBe('*')
701
+ expect((expr as any).left.kind).toBe('binary')
702
+ })
703
+ })
704
+ })
705
+
706
+ describe('complex programs', () => {
707
+ it('parses add function', () => {
708
+ const source = `
709
+ fn add(a: int, b: int) -> int {
710
+ return a + b;
711
+ }
712
+ `
713
+ const program = parse(source)
714
+ expect(program.declarations).toHaveLength(1)
715
+ const fn = program.declarations[0]
716
+ expect(fn.name).toBe('add')
717
+ expect(fn.body).toHaveLength(1)
718
+ expect(fn.body[0].kind).toBe('return')
719
+ })
720
+
721
+ it('parses abs function with if/else', () => {
722
+ const source = `
723
+ fn abs(x: int) -> int {
724
+ if (x < 0) {
725
+ return -x;
726
+ } else {
727
+ return x;
728
+ }
729
+ }
730
+ `
731
+ const program = parse(source)
732
+ const fn = program.declarations[0]
733
+ expect(fn.body).toHaveLength(1)
734
+ const ifStmt = fn.body[0]
735
+ expect(ifStmt.kind).toBe('if')
736
+ })
737
+
738
+ it('parses tick function', () => {
739
+ const source = `
740
+ @tick(rate=20)
741
+ fn heartbeat() {
742
+ say("still alive");
743
+ }
744
+ `
745
+ const program = parse(source)
746
+ const fn = program.declarations[0]
747
+ expect(fn.decorators).toEqual([{ name: 'tick', args: { rate: 20 } }])
748
+ expect(fn.body).toHaveLength(1)
749
+ })
750
+
751
+ it('parses foreach with kill', () => {
752
+ const source = `
753
+ fn kill_zombies() {
754
+ foreach (z in @e[type=zombie, distance=..10]) {
755
+ kill(z);
756
+ }
757
+ }
758
+ `
759
+ const program = parse(source)
760
+ const fn = program.declarations[0]
761
+ const stmt = fn.body[0]
762
+ expect(stmt.kind).toBe('foreach')
763
+ expect((stmt as any).binding).toBe('z')
764
+ expect((stmt as any).iterable.sel.filters.type).toBe('zombie')
765
+ expect((stmt as any).iterable.sel.filters.distance).toEqual({ max: 10 })
766
+ })
767
+
768
+ it('parses foreach over array', () => {
769
+ const source = `
770
+ fn walk() {
771
+ let arr: int[] = [1, 2, 3];
772
+ foreach (x in arr) {
773
+ say("tick");
774
+ }
775
+ }
776
+ `
777
+ const program = parse(source)
778
+ const stmt = program.declarations[0].body[1]
779
+ expect(stmt.kind).toBe('foreach')
780
+ expect((stmt as any).binding).toBe('x')
781
+ expect((stmt as any).iterable).toEqual({ kind: 'ident', name: 'arr' })
782
+ })
783
+
784
+ it('parses while loop', () => {
785
+ const source = `
786
+ fn count_down() {
787
+ let i: int = 10;
788
+ while (i > 0) {
789
+ i = i - 1;
790
+ }
791
+ }
792
+ `
793
+ const program = parse(source)
794
+ const fn = program.declarations[0]
795
+ expect(fn.body).toHaveLength(2)
796
+ expect(fn.body[0].kind).toBe('let')
797
+ expect(fn.body[1].kind).toBe('while')
798
+ })
799
+ })
800
+ })