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,305 @@
1
+ import { Lexer } from '../../lexer'
2
+ import { Parser } from '../../parser'
3
+ import { Lowering } from '../../lowering'
4
+ import { nbt, TagType, writeNbt, type CompoundTag, type NbtTag } from '../../nbt'
5
+ import { createEmptyOptimizationStats, mergeOptimizationStats, type OptimizationStats } from '../../optimizer/commands'
6
+ import { optimizeWithStats } from '../../optimizer/passes'
7
+ import { optimizeForStructure, optimizeForStructureWithStats } from '../../optimizer/structure'
8
+ import { preprocessSource } from '../../compile'
9
+ import type { IRCommand, IRFunction, IRModule } from '../../ir/types'
10
+ import type { DatapackFile } from '../mcfunction'
11
+
12
+ const DATA_VERSION = 3953
13
+ const MAX_WIDTH = 16
14
+ const OBJ = 'rs'
15
+
16
+ const PALETTE_IMPULSE = 0
17
+ const PALETTE_CHAIN_UNCONDITIONAL = 1
18
+ const PALETTE_CHAIN_CONDITIONAL = 2
19
+ const PALETTE_REPEAT = 3
20
+
21
+ const palette = [
22
+ { Name: 'minecraft:command_block', Properties: { conditional: 'false', facing: 'east' } },
23
+ { Name: 'minecraft:chain_command_block', Properties: { conditional: 'false', facing: 'east' } },
24
+ { Name: 'minecraft:chain_command_block', Properties: { conditional: 'true', facing: 'east' } },
25
+ { Name: 'minecraft:repeating_command_block', Properties: { conditional: 'false', facing: 'east' } },
26
+ { Name: 'minecraft:air', Properties: {} },
27
+ ]
28
+
29
+ interface CommandEntry {
30
+ functionName: string
31
+ lineNumber: number
32
+ command: string
33
+ state: number
34
+ conditional: boolean
35
+ isRepeat: boolean
36
+ }
37
+
38
+ export interface StructureBlockInfo {
39
+ command: string
40
+ conditional: boolean
41
+ state: number
42
+ functionName: string
43
+ lineNumber: number
44
+ }
45
+
46
+ export interface StructureCompileResult {
47
+ buffer: Buffer
48
+ blockCount: number
49
+ blocks: StructureBlockInfo[]
50
+ stats?: OptimizationStats
51
+ }
52
+
53
+ function escapeJsonString(value: string): string {
54
+ return JSON.stringify(value).slice(1, -1)
55
+ }
56
+
57
+ function varRef(name: string): string {
58
+ return name.startsWith('$') ? name : `$${name}`
59
+ }
60
+
61
+ function collectConsts(fn: IRFunction): Set<number> {
62
+ const consts = new Set<number>()
63
+ for (const block of fn.blocks) {
64
+ for (const instr of block.instrs) {
65
+ if (instr.op === 'assign' && instr.src.kind === 'const') consts.add(instr.src.value)
66
+ if (instr.op === 'binop') {
67
+ if (instr.lhs.kind === 'const') consts.add(instr.lhs.value)
68
+ if (instr.rhs.kind === 'const') consts.add(instr.rhs.value)
69
+ }
70
+ if (instr.op === 'cmp') {
71
+ if (instr.lhs.kind === 'const') consts.add(instr.lhs.value)
72
+ if (instr.rhs.kind === 'const') consts.add(instr.rhs.value)
73
+ }
74
+ }
75
+ if (block.term.op === 'return' && block.term.value?.kind === 'const') {
76
+ consts.add(block.term.value.value)
77
+ }
78
+ }
79
+ return consts
80
+ }
81
+
82
+ function constSetup(value: number): string {
83
+ return `scoreboard players set $const_${value} ${OBJ} ${value}`
84
+ }
85
+
86
+ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
87
+ const entries: CommandEntry[] = []
88
+ const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
89
+ const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
90
+ const loadCommands = [
91
+ `scoreboard objectives add ${OBJ} dummy`,
92
+ ...module.globals.map(globalName => `scoreboard players set ${varRef(globalName)} ${OBJ} 0`),
93
+ ...Array.from(triggerNames).flatMap(triggerName => [
94
+ `scoreboard objectives add ${triggerName} trigger`,
95
+ `scoreboard players enable @a ${triggerName}`,
96
+ ]),
97
+ ...Array.from(
98
+ new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))
99
+ ).map(constSetup),
100
+ ]
101
+
102
+ const sections: Array<{ name: string; commands: IRCommand[]; repeat?: boolean }> = []
103
+
104
+ if (loadCommands.length > 0) {
105
+ sections.push({
106
+ name: '__load',
107
+ commands: loadCommands.map(cmd => ({ cmd })),
108
+ })
109
+ }
110
+
111
+ for (const triggerName of triggerNames) {
112
+ const handlers = triggerHandlers.filter(fn => fn.triggerName === triggerName)
113
+ sections.push({
114
+ name: `__trigger_${triggerName}_dispatch`,
115
+ commands: [
116
+ ...handlers.map(handler => ({ cmd: `function ${module.namespace}:${handler.name}` })),
117
+ { cmd: `scoreboard players set @s ${triggerName} 0` },
118
+ { cmd: `scoreboard players enable @s ${triggerName}` },
119
+ ],
120
+ })
121
+ }
122
+
123
+ for (const fn of module.functions) {
124
+ if (!fn.commands || fn.commands.length === 0) continue
125
+ sections.push({
126
+ name: fn.name,
127
+ commands: fn.commands,
128
+ })
129
+ }
130
+
131
+ const tickCommands: IRCommand[] = []
132
+ for (const fn of module.functions.filter(candidate => candidate.isTickLoop)) {
133
+ tickCommands.push({ cmd: `function ${module.namespace}:${fn.name}` })
134
+ }
135
+ if (triggerNames.size > 0) {
136
+ for (const triggerName of triggerNames) {
137
+ tickCommands.push({
138
+ cmd: `execute as @a[scores={${triggerName}=1..}] run function ${module.namespace}:__trigger_${triggerName}_dispatch`,
139
+ })
140
+ }
141
+ }
142
+ if (tickCommands.length > 0) {
143
+ sections.push({
144
+ name: '__tick',
145
+ commands: tickCommands,
146
+ repeat: true,
147
+ })
148
+ }
149
+
150
+ for (const section of sections) {
151
+ for (let i = 0; i < section.commands.length; i++) {
152
+ const command = section.commands[i]
153
+ const state =
154
+ i === 0
155
+ ? (section.repeat ? PALETTE_REPEAT : PALETTE_IMPULSE)
156
+ : (command.conditional ? PALETTE_CHAIN_CONDITIONAL : PALETTE_CHAIN_UNCONDITIONAL)
157
+
158
+ entries.push({
159
+ functionName: section.name,
160
+ lineNumber: i + 1,
161
+ command: command.cmd,
162
+ conditional: Boolean(command.conditional),
163
+ state,
164
+ isRepeat: Boolean(section.repeat && i === 0),
165
+ })
166
+ }
167
+ }
168
+
169
+ return entries
170
+ }
171
+
172
+ function toFunctionName(file: DatapackFile): string | null {
173
+ const match = file.path.match(/^data\/[^/]+\/function\/(.+)\.mcfunction$/)
174
+ return match?.[1] ?? null
175
+ }
176
+
177
+ function collectCommandEntriesFromFiles(files: DatapackFile[]): CommandEntry[] {
178
+ const entries: CommandEntry[] = []
179
+
180
+ for (const file of files) {
181
+ const functionName = toFunctionName(file)
182
+ if (!functionName) continue
183
+
184
+ const lines = file.content.split('\n')
185
+ let isFirstCommand = true
186
+ const isTickFunction = functionName === '__tick'
187
+
188
+ for (let i = 0; i < lines.length; i++) {
189
+ const command = lines[i].trim()
190
+ if (command === '' || command.startsWith('#')) continue
191
+
192
+ const state = isFirstCommand
193
+ ? (isTickFunction ? PALETTE_REPEAT : PALETTE_IMPULSE)
194
+ : PALETTE_CHAIN_UNCONDITIONAL
195
+
196
+ entries.push({
197
+ functionName,
198
+ lineNumber: i + 1,
199
+ command,
200
+ conditional: false,
201
+ state,
202
+ isRepeat: isTickFunction && isFirstCommand,
203
+ })
204
+
205
+ isFirstCommand = false
206
+ }
207
+ }
208
+
209
+ return entries
210
+ }
211
+
212
+ function createPaletteTag(): CompoundTag[] {
213
+ return palette.map(entry =>
214
+ nbt.compound({
215
+ Name: nbt.string(entry.Name),
216
+ Properties: nbt.compound(
217
+ Object.fromEntries(
218
+ Object.entries(entry.Properties).map(([key, value]) => [key, nbt.string(value)])
219
+ )
220
+ ),
221
+ })
222
+ )
223
+ }
224
+
225
+ function createBlockEntityTag(entry: CommandEntry): NbtTag {
226
+ return nbt.compound({
227
+ id: nbt.string('minecraft:command_block'),
228
+ Command: nbt.string(entry.command),
229
+ auto: nbt.byte(entry.isRepeat ? 1 : 0),
230
+ powered: nbt.byte(0),
231
+ conditionMet: nbt.byte(0),
232
+ UpdateLastExecution: nbt.byte(1),
233
+ LastExecution: nbt.long(0n),
234
+ TrackOutput: nbt.byte(1),
235
+ SuccessCount: nbt.int(0),
236
+ LastOutput: nbt.string(''),
237
+ CustomName: nbt.string(`{"text":"${escapeJsonString(`${entry.functionName}:${entry.lineNumber}`)}"}`),
238
+ })
239
+ }
240
+
241
+ function createBlockTag(entry: CommandEntry, index: number): CompoundTag {
242
+ const x = index % MAX_WIDTH
243
+ const z = Math.floor(index / MAX_WIDTH) % MAX_WIDTH
244
+ const y = Math.floor(index / (MAX_WIDTH * MAX_WIDTH))
245
+
246
+ return nbt.compound({
247
+ pos: nbt.list(TagType.Int, [nbt.int(x), nbt.int(y), nbt.int(z)]),
248
+ state: nbt.int(entry.state),
249
+ nbt: createBlockEntityTag(entry),
250
+ })
251
+ }
252
+
253
+ export function generateStructure(input: IRModule | DatapackFile[]): StructureCompileResult {
254
+ const entries = Array.isArray(input)
255
+ ? collectCommandEntriesFromFiles(input)
256
+ : collectCommandEntriesFromModule(input)
257
+
258
+ const blockTags = entries.map(createBlockTag)
259
+ const sizeX = Math.max(1, Math.min(MAX_WIDTH, entries.length || 1))
260
+ const sizeZ = Math.max(1, Math.min(MAX_WIDTH, Math.ceil(entries.length / MAX_WIDTH) || 1))
261
+ const sizeY = Math.max(1, Math.ceil(entries.length / (MAX_WIDTH * MAX_WIDTH)) || 1)
262
+
263
+ const root = nbt.compound({
264
+ DataVersion: nbt.int(DATA_VERSION),
265
+ size: nbt.list(TagType.Int, [nbt.int(sizeX), nbt.int(sizeY), nbt.int(sizeZ)]),
266
+ palette: nbt.list(TagType.Compound, createPaletteTag()),
267
+ blocks: nbt.list(TagType.Compound, blockTags),
268
+ entities: nbt.list(TagType.Compound, []),
269
+ })
270
+
271
+ return {
272
+ buffer: writeNbt(root, ''),
273
+ blockCount: entries.length,
274
+ blocks: entries.map(entry => ({
275
+ command: entry.command,
276
+ conditional: entry.conditional,
277
+ state: entry.state,
278
+ functionName: entry.functionName,
279
+ lineNumber: entry.lineNumber,
280
+ })),
281
+ }
282
+ }
283
+
284
+ export function compileToStructure(source: string, namespace: string, filePath?: string): StructureCompileResult {
285
+ const preprocessedSource = preprocessSource(source, { filePath })
286
+ const tokens = new Lexer(preprocessedSource, filePath).tokenize()
287
+ const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
288
+ const ir = new Lowering(namespace).lower(ast)
289
+ const stats = createEmptyOptimizationStats()
290
+ const optimizedIRFunctions = ir.functions.map(fn => {
291
+ const optimized = optimizeWithStats(fn)
292
+ mergeOptimizationStats(stats, optimized.stats)
293
+ return optimized.fn
294
+ })
295
+ const structureOptimized = optimizeForStructureWithStats(optimizedIRFunctions, namespace)
296
+ mergeOptimizationStats(stats, structureOptimized.stats)
297
+ const optimizedModule: IRModule = {
298
+ ...ir,
299
+ functions: structureOptimized.functions,
300
+ }
301
+ return {
302
+ ...generateStructure(optimizedModule),
303
+ stats,
304
+ }
305
+ }
package/src/compile.ts ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * RedScript Compile API
3
+ *
4
+ * Main compile function with proper error handling and diagnostics.
5
+ */
6
+
7
+ import * as fs from 'fs'
8
+ import * as path from 'path'
9
+
10
+ import { Lexer } from './lexer'
11
+ import { Parser } from './parser'
12
+ import { Lowering } from './lowering'
13
+ import { optimize } from './optimizer/passes'
14
+ import { generateDatapackWithStats, DatapackFile } from './codegen/mcfunction'
15
+ import { DiagnosticError, formatError, parseErrorMessage } from './diagnostics'
16
+ import type { IRModule } from './ir/types'
17
+ import type { Program } from './ast/types'
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Compile Options
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface CompileOptions {
24
+ namespace?: string
25
+ filePath?: string
26
+ optimize?: boolean
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Compile Result
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export interface CompileResult {
34
+ success: boolean
35
+ files?: DatapackFile[]
36
+ advancements?: DatapackFile[]
37
+ ast?: Program
38
+ ir?: IRModule
39
+ error?: DiagnosticError
40
+ }
41
+
42
+ const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/
43
+
44
+ interface PreprocessOptions {
45
+ filePath?: string
46
+ seen?: Set<string>
47
+ }
48
+
49
+ export function preprocessSource(source: string, options: PreprocessOptions = {}): string {
50
+ const { filePath } = options
51
+ const seen = options.seen ?? new Set<string>()
52
+
53
+ if (filePath) {
54
+ seen.add(path.resolve(filePath))
55
+ }
56
+
57
+ const lines = source.split('\n')
58
+ const imports: string[] = []
59
+ const bodyLines: string[] = []
60
+ let parsingHeader = true
61
+
62
+ for (let i = 0; i < lines.length; i++) {
63
+ const line = lines[i]
64
+ const trimmed = line.trim()
65
+ const match = line.match(IMPORT_RE)
66
+
67
+ if (parsingHeader && match) {
68
+ if (!filePath) {
69
+ throw new DiagnosticError(
70
+ 'ParseError',
71
+ 'Import statements require a file path',
72
+ { line: i + 1, col: 1 },
73
+ lines
74
+ )
75
+ }
76
+
77
+ const importPath = path.resolve(path.dirname(filePath), match[1])
78
+ if (!seen.has(importPath)) {
79
+ seen.add(importPath)
80
+ let importedSource: string
81
+
82
+ try {
83
+ importedSource = fs.readFileSync(importPath, 'utf-8')
84
+ } catch {
85
+ throw new DiagnosticError(
86
+ 'ParseError',
87
+ `Cannot import '${match[1]}'`,
88
+ { file: filePath, line: i + 1, col: 1 },
89
+ lines
90
+ )
91
+ }
92
+
93
+ imports.push(preprocessSource(importedSource, { filePath: importPath, seen }))
94
+ }
95
+ continue
96
+ }
97
+
98
+ if (parsingHeader && (trimmed === '' || trimmed.startsWith('//'))) {
99
+ bodyLines.push(line)
100
+ continue
101
+ }
102
+
103
+ parsingHeader = false
104
+ bodyLines.push(line)
105
+ }
106
+
107
+ return [...imports, bodyLines.join('\n')].filter(Boolean).join('\n')
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Main Compile Function
112
+ // ---------------------------------------------------------------------------
113
+
114
+ export function compile(source: string, options: CompileOptions = {}): CompileResult {
115
+ const { namespace = 'redscript', filePath, optimize: shouldOptimize = true } = options
116
+ let sourceLines = source.split('\n')
117
+
118
+ try {
119
+ const preprocessedSource = preprocessSource(source, { filePath })
120
+ sourceLines = preprocessedSource.split('\n')
121
+
122
+ // Lexing
123
+ const tokens = new Lexer(preprocessedSource, filePath).tokenize()
124
+
125
+ // Parsing
126
+ const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
127
+
128
+ // Lowering
129
+ const ir = new Lowering(namespace).lower(ast)
130
+
131
+ // Optimization
132
+ const optimized: IRModule = shouldOptimize
133
+ ? { ...ir, functions: ir.functions.map(fn => optimize(fn)) }
134
+ : ir
135
+
136
+ // Code generation
137
+ const generated = generateDatapackWithStats(optimized)
138
+
139
+ return {
140
+ success: true,
141
+ files: [...generated.files, ...generated.advancements],
142
+ advancements: generated.advancements,
143
+ ast,
144
+ ir: optimized,
145
+ }
146
+ } catch (err) {
147
+ // Already a DiagnosticError
148
+ if (err instanceof DiagnosticError) {
149
+ return { success: false, error: err }
150
+ }
151
+
152
+ // Try to parse the error message for line/col info
153
+ if (err instanceof Error) {
154
+ const diagnostic = parseErrorMessage(
155
+ 'ParseError',
156
+ err.message,
157
+ sourceLines,
158
+ filePath
159
+ )
160
+ return { success: false, error: diagnostic }
161
+ }
162
+
163
+ // Unknown error
164
+ return {
165
+ success: false,
166
+ error: new DiagnosticError(
167
+ 'ParseError',
168
+ String(err),
169
+ { file: filePath, line: 1, col: 1 },
170
+ sourceLines
171
+ )
172
+ }
173
+ }
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Format Compile Error
178
+ // ---------------------------------------------------------------------------
179
+
180
+ export function formatCompileError(result: CompileResult): string {
181
+ if (result.success) {
182
+ return 'Compilation successful'
183
+ }
184
+ if (result.error) {
185
+ return formatError(result.error, result.error.sourceLines?.join('\n'))
186
+ }
187
+ return 'Unknown error'
188
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * RedScript Diagnostics
3
+ *
4
+ * Error reporting with file path, line, column, and formatted error messages.
5
+ */
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Error Types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export type DiagnosticKind = 'LexError' | 'ParseError' | 'LoweringError' | 'TypeError'
12
+
13
+ export interface DiagnosticLocation {
14
+ file?: string
15
+ line: number
16
+ col: number
17
+ }
18
+
19
+ function formatSourcePointer(sourceLines: string[], line: number, col: number): string[] {
20
+ const lineIdx = line - 1
21
+ if (lineIdx < 0 || lineIdx >= sourceLines.length) {
22
+ return []
23
+ }
24
+
25
+ const sourceLine = sourceLines[lineIdx]
26
+ const safeCol = Math.max(1, Math.min(col, sourceLine.length + 1))
27
+ const pointer = ` ${' '.repeat(safeCol - 1)}^`
28
+ return [` ${sourceLine}`, pointer]
29
+ }
30
+
31
+ export class DiagnosticError extends Error {
32
+ readonly kind: DiagnosticKind
33
+ readonly location: DiagnosticLocation
34
+ readonly sourceLines?: string[]
35
+
36
+ constructor(
37
+ kind: DiagnosticKind,
38
+ message: string,
39
+ location: DiagnosticLocation,
40
+ sourceLines?: string[]
41
+ ) {
42
+ super(message)
43
+ this.name = 'DiagnosticError'
44
+ this.kind = kind
45
+ this.location = location
46
+ this.sourceLines = sourceLines
47
+ }
48
+
49
+ /**
50
+ * Format the error for display:
51
+ * ```
52
+ * Error: [ParseError] line 5, col 12: Expected ';' after statement
53
+ * 5 | let x = 42
54
+ * ^ expected ';'
55
+ * ```
56
+ */
57
+ format(): string {
58
+ const { kind, message, location, sourceLines } = this
59
+ const filePart = location.file ? `${location.file}:` : ''
60
+ const header = `Error: [${kind}] ${filePart}line ${location.line}, col ${location.col}: ${message}`
61
+
62
+ if (!sourceLines || sourceLines.length === 0) {
63
+ return header
64
+ }
65
+
66
+ const pointerLines = formatSourcePointer(sourceLines, location.line, location.col)
67
+ if (pointerLines.length === 0) {
68
+ return header
69
+ }
70
+ const lineNum = String(location.line).padStart(3)
71
+ const prefix = `${lineNum} | `
72
+ const sourceLine = sourceLines[location.line - 1]
73
+ const safeCol = Math.max(1, Math.min(location.col, sourceLine.length + 1))
74
+ const pointer = ' '.repeat(prefix.length + safeCol - 1) + '^'
75
+ const hint = message.toLowerCase().includes('expected')
76
+ ? message.split(':').pop()?.trim() || ''
77
+ : ''
78
+
79
+ return [
80
+ header,
81
+ `${prefix}${sourceLine}`,
82
+ `${pointer}${hint ? ` ${hint}` : ''}`,
83
+ ].join('\n')
84
+ }
85
+
86
+ toString(): string {
87
+ return this.format()
88
+ }
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Diagnostic Collection
93
+ // ---------------------------------------------------------------------------
94
+
95
+ export class DiagnosticCollector {
96
+ private diagnostics: DiagnosticError[] = []
97
+ private sourceLines: string[] = []
98
+ private filePath?: string
99
+
100
+ constructor(source?: string, filePath?: string) {
101
+ if (source) {
102
+ this.sourceLines = source.split('\n')
103
+ }
104
+ this.filePath = filePath
105
+ }
106
+
107
+ error(kind: DiagnosticKind, message: string, line: number, col: number): void {
108
+ const diagnostic = new DiagnosticError(
109
+ kind,
110
+ message,
111
+ { file: this.filePath, line, col },
112
+ this.sourceLines
113
+ )
114
+ this.diagnostics.push(diagnostic)
115
+ }
116
+
117
+ hasErrors(): boolean {
118
+ return this.diagnostics.length > 0
119
+ }
120
+
121
+ getErrors(): DiagnosticError[] {
122
+ return this.diagnostics
123
+ }
124
+
125
+ formatAll(): string {
126
+ return this.diagnostics.map(d => d.format()).join('\n\n')
127
+ }
128
+
129
+ throwFirst(): never {
130
+ if (this.diagnostics.length > 0) {
131
+ throw this.diagnostics[0]
132
+ }
133
+ throw new Error('No diagnostics to throw')
134
+ }
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Helper Functions
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * Create a DiagnosticError from a raw error message that includes line/col info
143
+ * e.g., "Expected ';' at line 5, col 12"
144
+ */
145
+ export function parseErrorMessage(
146
+ kind: DiagnosticKind,
147
+ rawMessage: string,
148
+ sourceLines?: string[],
149
+ filePath?: string
150
+ ): DiagnosticError {
151
+ // Try to extract line and col from message
152
+ const match = rawMessage.match(/at line (\d+), col (\d+)/)
153
+ if (match) {
154
+ const line = parseInt(match[1], 10)
155
+ const col = parseInt(match[2], 10)
156
+ const message = rawMessage.replace(/ at line \d+, col \d+$/, '').trim()
157
+ return new DiagnosticError(kind, message, { file: filePath, line, col }, sourceLines)
158
+ }
159
+
160
+ // Fallback: line 1, col 1
161
+ return new DiagnosticError(kind, rawMessage, { file: filePath, line: 1, col: 1 }, sourceLines)
162
+ }
163
+
164
+ export function formatError(error: Error | DiagnosticError, source?: string): string {
165
+ if (error instanceof DiagnosticError) {
166
+ const sourceLines = source?.split('\n') ?? error.sourceLines ?? []
167
+ const { file, line, col } = error.location
168
+ const locationPart = file
169
+ ? ` in ${file} at line ${line}, col ${col}`
170
+ : ` at line ${line}, col ${col}`
171
+ const lines = [`Error${locationPart}:`]
172
+ const pointerLines = formatSourcePointer(sourceLines, line, col)
173
+ if (pointerLines.length > 0) {
174
+ lines.push(...pointerLines)
175
+ }
176
+ lines.push(error.message)
177
+ return lines.join('\n')
178
+ }
179
+
180
+ if (!source) {
181
+ return error.message
182
+ }
183
+
184
+ const parsed = parseErrorMessage('ParseError', error.message, source.split('\n'))
185
+ return formatError(parsed, source)
186
+ }