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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
- package/.github/ISSUE_TEMPLATE/wrong_output.md +33 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +34 -0
- package/.github/workflows/ci.yml +29 -0
- package/.github/workflows/publish-extension.yml +35 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/README.zh.md +261 -0
- package/dist/__tests__/cli.test.d.ts +1 -0
- package/dist/__tests__/cli.test.js +140 -0
- package/dist/__tests__/codegen.test.d.ts +1 -0
- package/dist/__tests__/codegen.test.js +121 -0
- package/dist/__tests__/diagnostics.test.d.ts +4 -0
- package/dist/__tests__/diagnostics.test.js +149 -0
- package/dist/__tests__/e2e.test.d.ts +6 -0
- package/dist/__tests__/e2e.test.js +1528 -0
- package/dist/__tests__/lexer.test.d.ts +1 -0
- package/dist/__tests__/lexer.test.js +316 -0
- package/dist/__tests__/lowering.test.d.ts +1 -0
- package/dist/__tests__/lowering.test.js +819 -0
- package/dist/__tests__/mc-integration.test.d.ts +12 -0
- package/dist/__tests__/mc-integration.test.js +395 -0
- package/dist/__tests__/mc-syntax.test.d.ts +1 -0
- package/dist/__tests__/mc-syntax.test.js +112 -0
- package/dist/__tests__/nbt.test.d.ts +1 -0
- package/dist/__tests__/nbt.test.js +82 -0
- package/dist/__tests__/optimizer-advanced.test.d.ts +1 -0
- package/dist/__tests__/optimizer-advanced.test.js +124 -0
- package/dist/__tests__/optimizer.test.d.ts +1 -0
- package/dist/__tests__/optimizer.test.js +118 -0
- package/dist/__tests__/parser.test.d.ts +1 -0
- package/dist/__tests__/parser.test.js +717 -0
- package/dist/__tests__/repl.test.d.ts +1 -0
- package/dist/__tests__/repl.test.js +27 -0
- package/dist/__tests__/runtime.test.d.ts +1 -0
- package/dist/__tests__/runtime.test.js +276 -0
- package/dist/__tests__/structure-optimizer.test.d.ts +1 -0
- package/dist/__tests__/structure-optimizer.test.js +33 -0
- package/dist/__tests__/typechecker.test.d.ts +1 -0
- package/dist/__tests__/typechecker.test.js +364 -0
- package/dist/ast/types.d.ts +357 -0
- package/dist/ast/types.js +9 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +407 -0
- package/dist/codegen/cmdblock/index.d.ts +26 -0
- package/dist/codegen/cmdblock/index.js +45 -0
- package/dist/codegen/mcfunction/index.d.ts +34 -0
- package/dist/codegen/mcfunction/index.js +413 -0
- package/dist/codegen/structure/index.d.ts +18 -0
- package/dist/codegen/structure/index.js +249 -0
- package/dist/compile.d.ts +30 -0
- package/dist/compile.js +152 -0
- package/dist/data/arena/function/__load.mcfunction +6 -0
- package/dist/data/arena/function/__tick.mcfunction +2 -0
- package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
- package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
- package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
- package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
- package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
- package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
- package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
- package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
- package/dist/data/arena/function/arena_tick.mcfunction +11 -0
- package/dist/data/counter/function/__load.mcfunction +5 -0
- package/dist/data/counter/function/__tick.mcfunction +2 -0
- package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
- package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
- package/dist/data/counter/function/counter_tick.mcfunction +11 -0
- package/dist/data/minecraft/tags/function/load.json +5 -0
- package/dist/data/minecraft/tags/function/tick.json +5 -0
- package/dist/data/quiz/function/__load.mcfunction +16 -0
- package/dist/data/quiz/function/__tick.mcfunction +6 -0
- package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/answer_a.mcfunction +4 -0
- package/dist/data/quiz/function/answer_b.mcfunction +4 -0
- package/dist/data/quiz/function/answer_c.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
- package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
- package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
- package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
- package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
- package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question.mcfunction +7 -0
- package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
- package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
- package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
- package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
- package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
- package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
- package/dist/data/shop/function/__load.mcfunction +7 -0
- package/dist/data/shop/function/__tick.mcfunction +3 -0
- package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
- package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
- package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
- package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
- package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
- package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
- package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
- package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
- package/dist/data/turret/function/__load.mcfunction +5 -0
- package/dist/data/turret/function/__tick.mcfunction +4 -0
- package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
- package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
- package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
- package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
- package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
- package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
- package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
- package/dist/data/turret/function/turret_tick.mcfunction +5 -0
- package/dist/diagnostics/index.d.ts +44 -0
- package/dist/diagnostics/index.js +140 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +126 -0
- package/dist/ir/builder.d.ts +32 -0
- package/dist/ir/builder.js +99 -0
- package/dist/ir/types.d.ts +117 -0
- package/dist/ir/types.js +15 -0
- package/dist/lexer/index.d.ts +36 -0
- package/dist/lexer/index.js +458 -0
- package/dist/lowering/index.d.ts +106 -0
- package/dist/lowering/index.js +2041 -0
- package/dist/mc-test/client.d.ts +128 -0
- package/dist/mc-test/client.js +174 -0
- package/dist/mc-test/runner.d.ts +28 -0
- package/dist/mc-test/runner.js +150 -0
- package/dist/mc-test/setup.d.ts +11 -0
- package/dist/mc-test/setup.js +98 -0
- package/dist/mc-validator/index.d.ts +17 -0
- package/dist/mc-validator/index.js +322 -0
- package/dist/nbt/index.d.ts +86 -0
- package/dist/nbt/index.js +250 -0
- package/dist/optimizer/commands.d.ts +36 -0
- package/dist/optimizer/commands.js +349 -0
- package/dist/optimizer/passes.d.ts +34 -0
- package/dist/optimizer/passes.js +227 -0
- package/dist/optimizer/structure.d.ts +8 -0
- package/dist/optimizer/structure.js +344 -0
- package/dist/pack.mcmeta +6 -0
- package/dist/parser/index.d.ts +76 -0
- package/dist/parser/index.js +1193 -0
- package/dist/repl.d.ts +16 -0
- package/dist/repl.js +165 -0
- package/dist/runtime/index.d.ts +101 -0
- package/dist/runtime/index.js +1288 -0
- package/dist/typechecker/index.d.ts +42 -0
- package/dist/typechecker/index.js +629 -0
- package/docs/COMPILATION_STATS.md +142 -0
- package/docs/IMPLEMENTATION_GUIDE.md +512 -0
- package/docs/LANGUAGE_REFERENCE.md +415 -0
- package/docs/MC_MAPPING.md +280 -0
- package/docs/STRUCTURE_TARGET.md +80 -0
- package/docs/mc-reference/commands.md +259 -0
- package/editors/vscode/.vscodeignore +10 -0
- package/editors/vscode/LICENSE +21 -0
- package/editors/vscode/README.md +78 -0
- package/editors/vscode/build.mjs +28 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/mcfunction-language-configuration.json +28 -0
- package/editors/vscode/out/extension.js +7236 -0
- package/editors/vscode/package-lock.json +566 -0
- package/editors/vscode/package.json +137 -0
- package/editors/vscode/redscript-language-configuration.json +28 -0
- package/editors/vscode/snippets/redscript.json +114 -0
- package/editors/vscode/src/codeactions.ts +89 -0
- package/editors/vscode/src/completion.ts +130 -0
- package/editors/vscode/src/extension.ts +239 -0
- package/editors/vscode/src/hover.ts +1120 -0
- package/editors/vscode/src/symbols.ts +207 -0
- package/editors/vscode/syntaxes/mcfunction.tmLanguage.json +740 -0
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +357 -0
- package/editors/vscode/tsconfig.json +13 -0
- package/jest.config.js +5 -0
- package/package.json +38 -0
- package/src/__tests__/cli.test.ts +130 -0
- package/src/__tests__/codegen.test.ts +128 -0
- package/src/__tests__/diagnostics.test.ts +195 -0
- package/src/__tests__/e2e.test.ts +1721 -0
- package/src/__tests__/fixtures/mc-commands-1.21.4.json +18734 -0
- package/src/__tests__/formatter.test.ts +46 -0
- package/src/__tests__/lexer.test.ts +356 -0
- package/src/__tests__/lowering.test.ts +962 -0
- package/src/__tests__/mc-integration.test.ts +409 -0
- package/src/__tests__/mc-syntax.test.ts +96 -0
- package/src/__tests__/nbt.test.ts +58 -0
- package/src/__tests__/optimizer-advanced.test.ts +144 -0
- package/src/__tests__/optimizer.test.ts +129 -0
- package/src/__tests__/parser.test.ts +800 -0
- package/src/__tests__/repl.test.ts +33 -0
- package/src/__tests__/runtime.test.ts +289 -0
- package/src/__tests__/structure-optimizer.test.ts +38 -0
- package/src/__tests__/typechecker.test.ts +395 -0
- package/src/ast/types.ts +248 -0
- package/src/cli.ts +445 -0
- package/src/codegen/cmdblock/index.ts +63 -0
- package/src/codegen/mcfunction/index.ts +471 -0
- package/src/codegen/structure/index.ts +305 -0
- package/src/compile.ts +188 -0
- package/src/diagnostics/index.ts +186 -0
- package/src/examples/README.md +77 -0
- package/src/examples/SHOWCASE_GAME.md +43 -0
- package/src/examples/arena.rs +44 -0
- package/src/examples/counter.rs +12 -0
- package/src/examples/pvp_arena.rs +131 -0
- package/src/examples/quiz.rs +90 -0
- package/src/examples/rpg.rs +13 -0
- package/src/examples/shop.rs +30 -0
- package/src/examples/showcase_game.rs +552 -0
- package/src/examples/stdlib_demo.rs +181 -0
- package/src/examples/turret.rs +27 -0
- package/src/examples/world_manager.rs +23 -0
- package/src/formatter/index.ts +22 -0
- package/src/index.ts +161 -0
- package/src/ir/builder.ts +114 -0
- package/src/ir/types.ts +119 -0
- package/src/lexer/index.ts +555 -0
- package/src/lowering/index.ts +2406 -0
- package/src/mc-test/client.ts +259 -0
- package/src/mc-test/runner.ts +140 -0
- package/src/mc-test/setup.ts +70 -0
- package/src/mc-validator/index.ts +367 -0
- package/src/nbt/index.ts +321 -0
- package/src/optimizer/commands.ts +416 -0
- package/src/optimizer/passes.ts +233 -0
- package/src/optimizer/structure.ts +441 -0
- package/src/parser/index.ts +1437 -0
- package/src/repl.ts +165 -0
- package/src/runtime/index.ts +1403 -0
- package/src/stdlib/README.md +156 -0
- package/src/stdlib/combat.rs +20 -0
- package/src/stdlib/cooldown.rs +45 -0
- package/src/stdlib/math.rs +49 -0
- package/src/stdlib/mobs.rs +99 -0
- package/src/stdlib/player.rs +29 -0
- package/src/stdlib/strings.rs +7 -0
- package/src/stdlib/timer.rs +51 -0
- package/src/templates/README.md +126 -0
- package/src/templates/combat.rs +96 -0
- package/src/templates/economy.rs +40 -0
- package/src/templates/mini-game-framework.rs +117 -0
- package/src/templates/quest.rs +78 -0
- package/src/test_programs/zombie_game.rs +25 -0
- package/src/typechecker/index.ts +737 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,2406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedScript Lowering
|
|
3
|
+
*
|
|
4
|
+
* Transforms AST into IR (Three-Address Code).
|
|
5
|
+
* Handles control flow, function extraction for foreach, and builtin calls.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { IRBuilder } from '../ir/builder'
|
|
9
|
+
import { buildModule } from '../ir/builder'
|
|
10
|
+
import type { IRFunction, IRModule, Operand, BinOp, CmpOp } from '../ir/types'
|
|
11
|
+
import { DiagnosticError } from '../diagnostics'
|
|
12
|
+
import type {
|
|
13
|
+
Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, Program, RangeExpr, Span, Stmt,
|
|
14
|
+
StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent
|
|
15
|
+
} from '../ast/types'
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Builtin Functions
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const BUILTINS: Record<string, (args: string[]) => string | null> = {
|
|
22
|
+
say: ([msg]) => `say ${msg}`,
|
|
23
|
+
tell: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
|
|
24
|
+
title: ([sel, msg]) => `title ${sel} title {"text":"${msg}"}`,
|
|
25
|
+
actionbar: ([sel, msg]) => `title ${sel} actionbar {"text":"${msg}"}`,
|
|
26
|
+
subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
|
|
27
|
+
title_times: ([sel, fadeIn, stay, fadeOut]) => `title ${sel} times ${fadeIn} ${stay} ${fadeOut}`,
|
|
28
|
+
announce: ([msg]) => `tellraw @a {"text":"${msg}"}`,
|
|
29
|
+
give: ([sel, item, count, nbt]) => nbt ? `give ${sel} ${item}${nbt} ${count ?? '1'}` : `give ${sel} ${item} ${count ?? '1'}`,
|
|
30
|
+
kill: ([sel]) => `kill ${sel ?? '@s'}`,
|
|
31
|
+
effect: ([sel, eff, dur, amp]) => `effect give ${sel} ${eff} ${dur ?? '30'} ${amp ?? '0'}`,
|
|
32
|
+
summon: ([type, x, y, z, nbt]) => {
|
|
33
|
+
const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
|
|
34
|
+
return nbt ? `summon ${type} ${pos} ${nbt}` : `summon ${type} ${pos}`
|
|
35
|
+
},
|
|
36
|
+
particle: ([name, x, y, z]) => {
|
|
37
|
+
const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
|
|
38
|
+
return `particle ${name} ${pos}`
|
|
39
|
+
},
|
|
40
|
+
playsound: ([sound, source, sel, x, y, z, volume, pitch, minVolume]) =>
|
|
41
|
+
['playsound', sound, source, sel, x, y, z, volume, pitch, minVolume].filter(Boolean).join(' '),
|
|
42
|
+
tp: () => null, // Special handling
|
|
43
|
+
tp_to: () => null, // Special handling (deprecated alias)
|
|
44
|
+
clear: ([sel, item]) => `clear ${sel} ${item ?? ''}`.trim(),
|
|
45
|
+
weather: ([type]) => `weather ${type}`,
|
|
46
|
+
time_set: ([val]) => `time set ${val}`,
|
|
47
|
+
time_add: ([val]) => `time add ${val}`,
|
|
48
|
+
gamerule: ([rule, val]) => `gamerule ${rule} ${val}`,
|
|
49
|
+
tag_add: ([sel, tag]) => `tag ${sel} add ${tag}`,
|
|
50
|
+
tag_remove: ([sel, tag]) => `tag ${sel} remove ${tag}`,
|
|
51
|
+
kick: ([player, reason]) => `kick ${player} ${reason ?? ''}`.trim(),
|
|
52
|
+
setblock: ([x, y, z, block]) => `setblock ${x} ${y} ${z} ${block}`,
|
|
53
|
+
fill: ([x1, y1, z1, x2, y2, z2, block]) => `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`,
|
|
54
|
+
clone: ([x1, y1, z1, x2, y2, z2, dx, dy, dz]) => `clone ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${dx} ${dy} ${dz}`,
|
|
55
|
+
difficulty: ([level]) => `difficulty ${level}`,
|
|
56
|
+
xp_add: ([sel, amount, type]) => `xp add ${sel} ${amount} ${type ?? 'points'}`,
|
|
57
|
+
xp_set: ([sel, amount, type]) => `xp set ${sel} ${amount} ${type ?? 'points'}`,
|
|
58
|
+
random: () => null, // Special handling
|
|
59
|
+
random_native: () => null, // Special handling
|
|
60
|
+
random_sequence: () => null, // Special handling
|
|
61
|
+
scoreboard_get: () => null, // Special handling (returns value)
|
|
62
|
+
scoreboard_set: () => null, // Special handling
|
|
63
|
+
score: () => null, // Special handling (same as scoreboard_get)
|
|
64
|
+
scoreboard_display: () => null, // Special handling
|
|
65
|
+
scoreboard_hide: () => null, // Special handling
|
|
66
|
+
scoreboard_add_objective: () => null, // Special handling
|
|
67
|
+
scoreboard_remove_objective: () => null, // Special handling
|
|
68
|
+
bossbar_add: () => null, // Special handling
|
|
69
|
+
bossbar_set_value: () => null, // Special handling
|
|
70
|
+
bossbar_set_max: () => null, // Special handling
|
|
71
|
+
bossbar_set_color: () => null, // Special handling
|
|
72
|
+
bossbar_set_style: () => null, // Special handling
|
|
73
|
+
bossbar_set_visible: () => null, // Special handling
|
|
74
|
+
bossbar_set_players: () => null, // Special handling
|
|
75
|
+
bossbar_remove: () => null, // Special handling
|
|
76
|
+
bossbar_get_value: () => null, // Special handling
|
|
77
|
+
team_add: () => null, // Special handling
|
|
78
|
+
team_remove: () => null, // Special handling
|
|
79
|
+
team_join: () => null, // Special handling
|
|
80
|
+
team_leave: () => null, // Special handling
|
|
81
|
+
team_option: () => null, // Special handling
|
|
82
|
+
data_get: () => null, // Special handling (returns value from NBT)
|
|
83
|
+
set_new: () => null, // Special handling (returns set ID)
|
|
84
|
+
set_add: () => null, // Special handling
|
|
85
|
+
set_contains: () => null, // Special handling (returns 1/0)
|
|
86
|
+
set_remove: () => null, // Special handling
|
|
87
|
+
set_clear: () => null, // Special handling
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface Warning {
|
|
91
|
+
message: string
|
|
92
|
+
code: string
|
|
93
|
+
line?: number
|
|
94
|
+
col?: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getSpan(node: unknown): Span | undefined {
|
|
98
|
+
return (node as { span?: Span } | undefined)?.span
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/
|
|
102
|
+
const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/
|
|
103
|
+
|
|
104
|
+
function normalizeSelector(selector: string, warnings: Warning[]): string {
|
|
105
|
+
return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
|
|
106
|
+
const trimmed = entityType.trim()
|
|
107
|
+
|
|
108
|
+
if (trimmed.includes(':')) {
|
|
109
|
+
if (!NAMESPACED_ENTITY_TYPE_RE.test(trimmed)) {
|
|
110
|
+
throw new DiagnosticError(
|
|
111
|
+
'LoweringError',
|
|
112
|
+
`Invalid entity type format: "${trimmed}" (must be namespace:name)`,
|
|
113
|
+
{ line: 1, col: 1 }
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
return match
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!BARE_ENTITY_TYPE_RE.test(trimmed)) {
|
|
120
|
+
throw new DiagnosticError(
|
|
121
|
+
'LoweringError',
|
|
122
|
+
`Invalid entity type format: "${trimmed}" (must be namespace:name or bare_name)`,
|
|
123
|
+
{ line: 1, col: 1 }
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
warnings.push({
|
|
128
|
+
message: `Unnamespaced entity type "${trimmed}", auto-qualifying to "minecraft:${trimmed}"`,
|
|
129
|
+
code: 'W_UNNAMESPACED_TYPE',
|
|
130
|
+
})
|
|
131
|
+
return `type=minecraft:${trimmed}`
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function emitCoord(component: CoordComponent): string {
|
|
136
|
+
switch (component.kind) {
|
|
137
|
+
case 'absolute':
|
|
138
|
+
return String(component.value)
|
|
139
|
+
case 'relative':
|
|
140
|
+
return component.offset === 0 ? '~' : `~${component.offset}`
|
|
141
|
+
case 'local':
|
|
142
|
+
return component.offset === 0 ? '^' : `^${component.offset}`
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function emitBlockPos(pos: BlockPosExpr): string {
|
|
147
|
+
return `${emitCoord(pos.x)} ${emitCoord(pos.y)} ${emitCoord(pos.z)}`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Lowering Class
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
export class Lowering {
|
|
155
|
+
private namespace: string
|
|
156
|
+
private functions: IRFunction[] = []
|
|
157
|
+
private globals: string[] = []
|
|
158
|
+
private fnDecls: Map<string, FnDecl> = new Map()
|
|
159
|
+
private specializedFunctions: Map<string, string> = new Map()
|
|
160
|
+
private currentFn: string = ''
|
|
161
|
+
private foreachCounter: number = 0
|
|
162
|
+
private lambdaCounter: number = 0
|
|
163
|
+
readonly warnings: Warning[] = []
|
|
164
|
+
|
|
165
|
+
// Builder state for current function
|
|
166
|
+
private builder!: LoweringBuilder
|
|
167
|
+
private varMap: Map<string, string> = new Map()
|
|
168
|
+
private lambdaBindings: Map<string, string> = new Map()
|
|
169
|
+
private currentCallbackBindings: Map<string, string> = new Map()
|
|
170
|
+
private currentContext: { binding?: string } = {}
|
|
171
|
+
private blockPosVars: Map<string, BlockPosExpr> = new Map()
|
|
172
|
+
|
|
173
|
+
// Struct definitions: name → { fieldName: TypeNode }
|
|
174
|
+
private structDefs: Map<string, Map<string, TypeNode>> = new Map()
|
|
175
|
+
private enumDefs: Map<string, Map<string, number>> = new Map()
|
|
176
|
+
private functionDefaults: Map<string, Array<Expr | undefined>> = new Map()
|
|
177
|
+
private constValues: Map<string, ConstDecl['value']> = new Map()
|
|
178
|
+
private stringValues: Map<string, string> = new Map()
|
|
179
|
+
// Variable types: varName → TypeNode
|
|
180
|
+
private varTypes: Map<string, TypeNode> = new Map()
|
|
181
|
+
// Float variables (stored as fixed-point × 1000)
|
|
182
|
+
private floatVars: Set<string> = new Set()
|
|
183
|
+
// World object counter for unique tags
|
|
184
|
+
private worldObjCounter: number = 0
|
|
185
|
+
|
|
186
|
+
constructor(namespace: string) {
|
|
187
|
+
this.namespace = namespace
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lower(program: Program): IRModule {
|
|
191
|
+
this.namespace = program.namespace
|
|
192
|
+
|
|
193
|
+
// Load struct definitions
|
|
194
|
+
for (const struct of program.structs ?? []) {
|
|
195
|
+
const fields = new Map<string, TypeNode>()
|
|
196
|
+
for (const field of struct.fields) {
|
|
197
|
+
fields.set(field.name, field.type)
|
|
198
|
+
}
|
|
199
|
+
this.structDefs.set(struct.name, fields)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const enumDecl of program.enums ?? []) {
|
|
203
|
+
const variants = new Map<string, number>()
|
|
204
|
+
for (const variant of enumDecl.variants) {
|
|
205
|
+
variants.set(variant.name, variant.value ?? 0)
|
|
206
|
+
}
|
|
207
|
+
this.enumDefs.set(enumDecl.name, variants)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const constDecl of program.consts ?? []) {
|
|
211
|
+
this.constValues.set(constDecl.name, constDecl.value)
|
|
212
|
+
this.varTypes.set(constDecl.name, this.normalizeType(constDecl.type))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const fn of program.declarations) {
|
|
216
|
+
this.fnDecls.set(fn.name, fn)
|
|
217
|
+
this.functionDefaults.set(fn.name, fn.params.map(param => param.default))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const fn of program.declarations) {
|
|
221
|
+
this.lowerFn(fn)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return buildModule(this.namespace, this.functions, this.globals)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// -------------------------------------------------------------------------
|
|
228
|
+
// Function Lowering
|
|
229
|
+
// -------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
private lowerFn(
|
|
232
|
+
fn: FnDecl,
|
|
233
|
+
options: {
|
|
234
|
+
name?: string
|
|
235
|
+
callbackBindings?: Map<string, string>
|
|
236
|
+
} = {}
|
|
237
|
+
): void {
|
|
238
|
+
const loweredName = options.name ?? fn.name
|
|
239
|
+
const callbackBindings = options.callbackBindings ?? new Map<string, string>()
|
|
240
|
+
const runtimeParams = fn.params.filter(param => !callbackBindings.has(param.name))
|
|
241
|
+
|
|
242
|
+
this.currentFn = loweredName
|
|
243
|
+
this.foreachCounter = 0
|
|
244
|
+
this.varMap = new Map()
|
|
245
|
+
this.lambdaBindings = new Map()
|
|
246
|
+
this.currentCallbackBindings = new Map(callbackBindings)
|
|
247
|
+
this.currentContext = {}
|
|
248
|
+
this.blockPosVars = new Map()
|
|
249
|
+
this.stringValues = new Map()
|
|
250
|
+
this.builder = new LoweringBuilder()
|
|
251
|
+
|
|
252
|
+
// Map parameters
|
|
253
|
+
for (const param of runtimeParams) {
|
|
254
|
+
const paramName = param.name
|
|
255
|
+
this.varMap.set(paramName, `$${paramName}`)
|
|
256
|
+
this.varTypes.set(paramName, this.normalizeType(param.type))
|
|
257
|
+
}
|
|
258
|
+
for (const param of fn.params) {
|
|
259
|
+
if (callbackBindings.has(param.name)) {
|
|
260
|
+
this.varTypes.set(param.name, this.normalizeType(param.type))
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Start entry block
|
|
265
|
+
this.builder.startBlock('entry')
|
|
266
|
+
|
|
267
|
+
// Copy params from $p0, $p1, ... to named variables
|
|
268
|
+
for (let i = 0; i < runtimeParams.length; i++) {
|
|
269
|
+
const paramName = runtimeParams[i].name
|
|
270
|
+
const varName = `$${paramName}`
|
|
271
|
+
this.builder.emitAssign(varName, { kind: 'var', name: `$p${i}` })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Lower body
|
|
275
|
+
this.lowerBlock(fn.body)
|
|
276
|
+
|
|
277
|
+
// If no explicit return, add void return
|
|
278
|
+
if (!this.builder.isBlockSealed()) {
|
|
279
|
+
this.builder.emitReturn()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Build function
|
|
283
|
+
const isTickLoop = fn.decorators.some(d => d.name === 'tick')
|
|
284
|
+
const tickRate = this.getTickRate(fn.decorators)
|
|
285
|
+
|
|
286
|
+
// Check for trigger handler
|
|
287
|
+
const triggerDec = fn.decorators.find(d => d.name === 'on_trigger')
|
|
288
|
+
const isTriggerHandler = !!triggerDec
|
|
289
|
+
const triggerName = triggerDec?.args?.trigger
|
|
290
|
+
|
|
291
|
+
const irFn = this.builder.build(loweredName, runtimeParams.map(p => `$${p.name}`), isTickLoop)
|
|
292
|
+
|
|
293
|
+
// Add trigger metadata if applicable
|
|
294
|
+
if (isTriggerHandler && triggerName) {
|
|
295
|
+
irFn.isTriggerHandler = true
|
|
296
|
+
irFn.triggerName = triggerName
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const eventDec = fn.decorators.find(d =>
|
|
300
|
+
d.name === 'on_advancement' ||
|
|
301
|
+
d.name === 'on_craft' ||
|
|
302
|
+
d.name === 'on_death' ||
|
|
303
|
+
d.name === 'on_login' ||
|
|
304
|
+
d.name === 'on_join_team'
|
|
305
|
+
)
|
|
306
|
+
if (eventDec) {
|
|
307
|
+
switch (eventDec.name) {
|
|
308
|
+
case 'on_advancement':
|
|
309
|
+
irFn.eventTrigger = { kind: 'advancement', value: eventDec.args?.advancement }
|
|
310
|
+
break
|
|
311
|
+
case 'on_craft':
|
|
312
|
+
irFn.eventTrigger = { kind: 'craft', value: eventDec.args?.item }
|
|
313
|
+
break
|
|
314
|
+
case 'on_death':
|
|
315
|
+
irFn.eventTrigger = { kind: 'death' }
|
|
316
|
+
break
|
|
317
|
+
case 'on_login':
|
|
318
|
+
irFn.eventTrigger = { kind: 'login' }
|
|
319
|
+
break
|
|
320
|
+
case 'on_join_team':
|
|
321
|
+
irFn.eventTrigger = { kind: 'join_team', value: eventDec.args?.team }
|
|
322
|
+
break
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Handle tick rate counter if needed
|
|
327
|
+
if (tickRate && tickRate > 1) {
|
|
328
|
+
this.wrapWithTickRate(irFn, tickRate)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this.functions.push(irFn)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private getTickRate(decorators: Decorator[]): number | undefined {
|
|
335
|
+
const tickDec = decorators.find(d => d.name === 'tick')
|
|
336
|
+
return tickDec?.args?.rate
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private wrapWithTickRate(fn: IRFunction, rate: number): void {
|
|
340
|
+
// Add tick counter logic to entry block
|
|
341
|
+
const counterVar = `$__tick_${fn.name}`
|
|
342
|
+
this.globals.push(counterVar)
|
|
343
|
+
|
|
344
|
+
// Prepend counter logic to entry block
|
|
345
|
+
const entry = fn.blocks[0]
|
|
346
|
+
const originalInstrs = [...entry.instrs]
|
|
347
|
+
const originalTerm = entry.term
|
|
348
|
+
|
|
349
|
+
entry.instrs = [
|
|
350
|
+
{ op: 'raw', cmd: `scoreboard players add ${counterVar} rs 1` },
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
// Create conditional jump
|
|
354
|
+
const bodyLabel = 'tick_body'
|
|
355
|
+
const skipLabel = 'tick_skip'
|
|
356
|
+
|
|
357
|
+
entry.term = {
|
|
358
|
+
op: 'jump_if',
|
|
359
|
+
cond: `${counterVar}_check`,
|
|
360
|
+
then: bodyLabel,
|
|
361
|
+
else_: skipLabel,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Add check instruction
|
|
365
|
+
entry.instrs.push({
|
|
366
|
+
op: 'raw',
|
|
367
|
+
cmd: `execute store success score ${counterVar}_check rs if score ${counterVar} rs matches ${rate}..`,
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// Body block (original logic + counter reset)
|
|
371
|
+
fn.blocks.push({
|
|
372
|
+
label: bodyLabel,
|
|
373
|
+
instrs: [
|
|
374
|
+
{ op: 'raw', cmd: `scoreboard players set ${counterVar} rs 0` },
|
|
375
|
+
...originalInstrs,
|
|
376
|
+
],
|
|
377
|
+
term: originalTerm,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// Skip block (just return)
|
|
381
|
+
fn.blocks.push({
|
|
382
|
+
label: skipLabel,
|
|
383
|
+
instrs: [],
|
|
384
|
+
term: { op: 'return' },
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// -------------------------------------------------------------------------
|
|
389
|
+
// Statement Lowering
|
|
390
|
+
// -------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
private lowerBlock(stmts: Block): void {
|
|
393
|
+
for (const stmt of stmts) {
|
|
394
|
+
this.lowerStmt(stmt)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private lowerStmt(stmt: Stmt): void {
|
|
399
|
+
switch (stmt.kind) {
|
|
400
|
+
case 'let':
|
|
401
|
+
this.lowerLetStmt(stmt)
|
|
402
|
+
break
|
|
403
|
+
case 'expr':
|
|
404
|
+
this.lowerExpr(stmt.expr)
|
|
405
|
+
break
|
|
406
|
+
case 'return':
|
|
407
|
+
this.lowerReturnStmt(stmt)
|
|
408
|
+
break
|
|
409
|
+
case 'if':
|
|
410
|
+
this.lowerIfStmt(stmt)
|
|
411
|
+
break
|
|
412
|
+
case 'while':
|
|
413
|
+
this.lowerWhileStmt(stmt)
|
|
414
|
+
break
|
|
415
|
+
case 'for':
|
|
416
|
+
this.lowerForStmt(stmt)
|
|
417
|
+
break
|
|
418
|
+
case 'foreach':
|
|
419
|
+
this.lowerForeachStmt(stmt)
|
|
420
|
+
break
|
|
421
|
+
case 'for_range':
|
|
422
|
+
this.lowerForRangeStmt(stmt)
|
|
423
|
+
break
|
|
424
|
+
case 'match':
|
|
425
|
+
this.lowerMatchStmt(stmt)
|
|
426
|
+
break
|
|
427
|
+
case 'as_block':
|
|
428
|
+
this.lowerAsBlockStmt(stmt)
|
|
429
|
+
break
|
|
430
|
+
case 'at_block':
|
|
431
|
+
this.lowerAtBlockStmt(stmt)
|
|
432
|
+
break
|
|
433
|
+
case 'as_at':
|
|
434
|
+
this.lowerAsAtStmt(stmt)
|
|
435
|
+
break
|
|
436
|
+
case 'execute':
|
|
437
|
+
this.lowerExecuteStmt(stmt)
|
|
438
|
+
break
|
|
439
|
+
case 'raw':
|
|
440
|
+
this.builder.emitRaw(stmt.cmd)
|
|
441
|
+
break
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private lowerLetStmt(stmt: Extract<Stmt, { kind: 'let' }>): void {
|
|
446
|
+
const varName = `$${stmt.name}`
|
|
447
|
+
this.varMap.set(stmt.name, varName)
|
|
448
|
+
|
|
449
|
+
// Track variable type
|
|
450
|
+
const declaredType = stmt.type ? this.normalizeType(stmt.type) : this.inferExprType(stmt.init)
|
|
451
|
+
if (declaredType) {
|
|
452
|
+
this.varTypes.set(stmt.name, declaredType)
|
|
453
|
+
// Track float variables for fixed-point arithmetic
|
|
454
|
+
if (declaredType.kind === 'named' && declaredType.name === 'float') {
|
|
455
|
+
this.floatVars.add(stmt.name)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (stmt.init.kind === 'lambda') {
|
|
460
|
+
const lambdaName = this.lowerLambdaExpr(stmt.init)
|
|
461
|
+
this.lambdaBindings.set(stmt.name, lambdaName)
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Handle struct literal initialization
|
|
466
|
+
if (stmt.init.kind === 'struct_lit' && stmt.type?.kind === 'struct') {
|
|
467
|
+
const structName = stmt.type.name.toLowerCase()
|
|
468
|
+
for (const field of stmt.init.fields) {
|
|
469
|
+
const path = `rs:heap ${structName}_${stmt.name}.${field.name}`
|
|
470
|
+
const fieldValue = this.lowerExpr(field.value)
|
|
471
|
+
if (fieldValue.kind === 'const') {
|
|
472
|
+
this.builder.emitRaw(`data modify storage ${path} set value ${fieldValue.value}`)
|
|
473
|
+
} else if (fieldValue.kind === 'var') {
|
|
474
|
+
// Copy from scoreboard to NBT
|
|
475
|
+
this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${fieldValue.name} rs`)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Handle array literal initialization
|
|
482
|
+
if (stmt.init.kind === 'array_lit') {
|
|
483
|
+
// Initialize empty NBT array
|
|
484
|
+
this.builder.emitRaw(`data modify storage rs:heap ${stmt.name} set value []`)
|
|
485
|
+
// Add each element
|
|
486
|
+
for (const elem of stmt.init.elements) {
|
|
487
|
+
const elemValue = this.lowerExpr(elem)
|
|
488
|
+
if (elemValue.kind === 'const') {
|
|
489
|
+
this.builder.emitRaw(`data modify storage rs:heap ${stmt.name} append value ${elemValue.value}`)
|
|
490
|
+
} else if (elemValue.kind === 'var') {
|
|
491
|
+
this.builder.emitRaw(`data modify storage rs:heap ${stmt.name} append value 0`)
|
|
492
|
+
this.builder.emitRaw(`execute store result storage rs:heap ${stmt.name}[-1] int 1 run scoreboard players get ${elemValue.name} rs`)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Handle spawn_object returning entity handle
|
|
499
|
+
if (stmt.init.kind === 'call' && stmt.init.fn === 'spawn_object') {
|
|
500
|
+
const value = this.lowerExpr(stmt.init)
|
|
501
|
+
// value is the selector like @e[tag=__rs_obj_0,limit=1]
|
|
502
|
+
if (value.kind === 'var' && value.name.startsWith('@e[tag=__rs_obj_')) {
|
|
503
|
+
this.varMap.set(stmt.name, value.name)
|
|
504
|
+
// Mark as entity type for later member access
|
|
505
|
+
this.varTypes.set(stmt.name, { kind: 'named', name: 'void' }) // Marker
|
|
506
|
+
}
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const blockPosValue = this.resolveBlockPosExpr(stmt.init)
|
|
511
|
+
if (blockPosValue) {
|
|
512
|
+
this.blockPosVars.set(stmt.name, blockPosValue)
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const stmtType = stmt.type ? this.normalizeType(stmt.type) : this.inferExprType(stmt.init)
|
|
517
|
+
if (stmtType?.kind === 'named' && stmtType.name === 'string' && this.storeStringValue(stmt.name, stmt.init)) {
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const value = this.lowerExpr(stmt.init)
|
|
522
|
+
this.builder.emitAssign(varName, value)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private lowerReturnStmt(stmt: Extract<Stmt, { kind: 'return' }>): void {
|
|
526
|
+
if (stmt.value) {
|
|
527
|
+
const value = this.lowerExpr(stmt.value)
|
|
528
|
+
this.builder.emitReturn(value)
|
|
529
|
+
} else {
|
|
530
|
+
this.builder.emitReturn()
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private lowerIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
|
|
535
|
+
const condVar = this.lowerExpr(stmt.cond)
|
|
536
|
+
const condName = this.operandToVar(condVar)
|
|
537
|
+
|
|
538
|
+
const thenLabel = this.builder.freshLabel('then')
|
|
539
|
+
const elseLabel = this.builder.freshLabel('else')
|
|
540
|
+
const mergeLabel = this.builder.freshLabel('merge')
|
|
541
|
+
|
|
542
|
+
this.builder.emitJumpIf(condName, thenLabel, stmt.else_ ? elseLabel : mergeLabel)
|
|
543
|
+
|
|
544
|
+
// Then block
|
|
545
|
+
this.builder.startBlock(thenLabel)
|
|
546
|
+
this.lowerBlock(stmt.then)
|
|
547
|
+
if (!this.builder.isBlockSealed()) {
|
|
548
|
+
this.builder.emitJump(mergeLabel)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Else block (if present)
|
|
552
|
+
if (stmt.else_) {
|
|
553
|
+
this.builder.startBlock(elseLabel)
|
|
554
|
+
this.lowerBlock(stmt.else_)
|
|
555
|
+
if (!this.builder.isBlockSealed()) {
|
|
556
|
+
this.builder.emitJump(mergeLabel)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Merge block
|
|
561
|
+
this.builder.startBlock(mergeLabel)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private lowerWhileStmt(stmt: Extract<Stmt, { kind: 'while' }>): void {
|
|
565
|
+
const checkLabel = this.builder.freshLabel('loop_check')
|
|
566
|
+
const bodyLabel = this.builder.freshLabel('loop_body')
|
|
567
|
+
const exitLabel = this.builder.freshLabel('loop_exit')
|
|
568
|
+
|
|
569
|
+
this.builder.emitJump(checkLabel)
|
|
570
|
+
|
|
571
|
+
// Check block
|
|
572
|
+
this.builder.startBlock(checkLabel)
|
|
573
|
+
const condVar = this.lowerExpr(stmt.cond)
|
|
574
|
+
const condName = this.operandToVar(condVar)
|
|
575
|
+
this.builder.emitJumpIf(condName, bodyLabel, exitLabel)
|
|
576
|
+
|
|
577
|
+
// Body block
|
|
578
|
+
this.builder.startBlock(bodyLabel)
|
|
579
|
+
this.lowerBlock(stmt.body)
|
|
580
|
+
if (!this.builder.isBlockSealed()) {
|
|
581
|
+
this.builder.emitJump(checkLabel)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Exit block
|
|
585
|
+
this.builder.startBlock(exitLabel)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private lowerForStmt(stmt: Extract<Stmt, { kind: 'for' }>): void {
|
|
589
|
+
// For loop is lowered to: init; while(cond) { body; step; }
|
|
590
|
+
|
|
591
|
+
// Init statement (if present)
|
|
592
|
+
if (stmt.init) {
|
|
593
|
+
this.lowerStmt(stmt.init)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const checkLabel = this.builder.freshLabel('for_check')
|
|
597
|
+
const bodyLabel = this.builder.freshLabel('for_body')
|
|
598
|
+
const exitLabel = this.builder.freshLabel('for_exit')
|
|
599
|
+
|
|
600
|
+
this.builder.emitJump(checkLabel)
|
|
601
|
+
|
|
602
|
+
// Check block
|
|
603
|
+
this.builder.startBlock(checkLabel)
|
|
604
|
+
const condVar = this.lowerExpr(stmt.cond)
|
|
605
|
+
const condName = this.operandToVar(condVar)
|
|
606
|
+
this.builder.emitJumpIf(condName, bodyLabel, exitLabel)
|
|
607
|
+
|
|
608
|
+
// Body block
|
|
609
|
+
this.builder.startBlock(bodyLabel)
|
|
610
|
+
this.lowerBlock(stmt.body)
|
|
611
|
+
// Step expression
|
|
612
|
+
this.lowerExpr(stmt.step)
|
|
613
|
+
if (!this.builder.isBlockSealed()) {
|
|
614
|
+
this.builder.emitJump(checkLabel)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Exit block
|
|
618
|
+
this.builder.startBlock(exitLabel)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private lowerForRangeStmt(stmt: Extract<Stmt, { kind: 'for_range' }>): void {
|
|
622
|
+
const loopVar = `$${stmt.varName}`
|
|
623
|
+
const subFnName = `${this.currentFn}/__for_${this.foreachCounter++}`
|
|
624
|
+
|
|
625
|
+
// Initialize loop variable
|
|
626
|
+
this.varMap.set(stmt.varName, loopVar)
|
|
627
|
+
const startVal = this.lowerExpr(stmt.start)
|
|
628
|
+
if (startVal.kind === 'const') {
|
|
629
|
+
this.builder.emitRaw(`scoreboard players set ${loopVar} rs ${startVal.value}`)
|
|
630
|
+
} else if (startVal.kind === 'var') {
|
|
631
|
+
this.builder.emitRaw(`scoreboard players operation ${loopVar} rs = ${startVal.name} rs`)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Call loop function
|
|
635
|
+
this.builder.emitRaw(`function ${this.namespace}:${subFnName}`)
|
|
636
|
+
|
|
637
|
+
// Generate loop sub-function
|
|
638
|
+
const savedBuilder = this.builder
|
|
639
|
+
const savedVarMap = new Map(this.varMap)
|
|
640
|
+
const savedContext = this.currentContext
|
|
641
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
642
|
+
|
|
643
|
+
this.builder = new LoweringBuilder()
|
|
644
|
+
this.varMap = new Map(savedVarMap)
|
|
645
|
+
this.currentContext = savedContext
|
|
646
|
+
this.blockPosVars = new Map(savedBlockPosVars)
|
|
647
|
+
|
|
648
|
+
this.builder.startBlock('entry')
|
|
649
|
+
|
|
650
|
+
// Body
|
|
651
|
+
this.lowerBlock(stmt.body)
|
|
652
|
+
|
|
653
|
+
// Increment
|
|
654
|
+
this.builder.emitRaw(`scoreboard players add ${loopVar} rs 1`)
|
|
655
|
+
|
|
656
|
+
// Loop condition: execute if score matches ..<end-1> run function
|
|
657
|
+
const endVal = this.lowerExpr(stmt.end)
|
|
658
|
+
const endNum = endVal.kind === 'const' ? endVal.value - 1 : '?'
|
|
659
|
+
this.builder.emitRaw(`execute if score ${loopVar} rs matches ..${endNum} run function ${this.namespace}:${subFnName}`)
|
|
660
|
+
|
|
661
|
+
if (!this.builder.isBlockSealed()) {
|
|
662
|
+
this.builder.emitReturn()
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const subFn = this.builder.build(subFnName, [], false)
|
|
666
|
+
this.functions.push(subFn)
|
|
667
|
+
|
|
668
|
+
// Restore
|
|
669
|
+
this.builder = savedBuilder
|
|
670
|
+
this.varMap = savedVarMap
|
|
671
|
+
this.currentContext = savedContext
|
|
672
|
+
this.blockPosVars = savedBlockPosVars
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private lowerForeachStmt(stmt: Extract<Stmt, { kind: 'foreach' }>): void {
|
|
676
|
+
if (stmt.iterable.kind !== 'selector') {
|
|
677
|
+
this.lowerArrayForeachStmt(stmt)
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Extract body into a separate function
|
|
682
|
+
const subFnName = `${this.currentFn}/foreach_${this.foreachCounter++}`
|
|
683
|
+
const selector = this.exprToString(stmt.iterable)
|
|
684
|
+
|
|
685
|
+
// Emit execute as ... run function ...
|
|
686
|
+
this.builder.emitRaw(`execute as ${selector} run function ${this.namespace}:${subFnName}`)
|
|
687
|
+
|
|
688
|
+
// Create the sub-function
|
|
689
|
+
const savedBuilder = this.builder
|
|
690
|
+
const savedVarMap = new Map(this.varMap)
|
|
691
|
+
const savedContext = this.currentContext
|
|
692
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
693
|
+
|
|
694
|
+
this.builder = new LoweringBuilder()
|
|
695
|
+
this.varMap = new Map(savedVarMap)
|
|
696
|
+
this.currentContext = { binding: stmt.binding }
|
|
697
|
+
this.blockPosVars = new Map(savedBlockPosVars)
|
|
698
|
+
|
|
699
|
+
// In foreach body, the binding maps to @s
|
|
700
|
+
this.varMap.set(stmt.binding, '@s')
|
|
701
|
+
|
|
702
|
+
this.builder.startBlock('entry')
|
|
703
|
+
this.lowerBlock(stmt.body)
|
|
704
|
+
if (!this.builder.isBlockSealed()) {
|
|
705
|
+
this.builder.emitReturn()
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const subFn = this.builder.build(subFnName, [], false)
|
|
709
|
+
this.functions.push(subFn)
|
|
710
|
+
|
|
711
|
+
// Restore
|
|
712
|
+
this.builder = savedBuilder
|
|
713
|
+
this.varMap = savedVarMap
|
|
714
|
+
this.currentContext = savedContext
|
|
715
|
+
this.blockPosVars = savedBlockPosVars
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private lowerMatchStmt(stmt: Extract<Stmt, { kind: 'match' }>): void {
|
|
719
|
+
const subject = this.operandToVar(this.lowerExpr(stmt.expr))
|
|
720
|
+
const matchedVar = this.builder.freshTemp()
|
|
721
|
+
this.builder.emitAssign(matchedVar, { kind: 'const', value: 0 })
|
|
722
|
+
|
|
723
|
+
let defaultArm: { pattern: Expr | null; body: Block } | null = null
|
|
724
|
+
|
|
725
|
+
for (const arm of stmt.arms) {
|
|
726
|
+
if (arm.pattern === null) {
|
|
727
|
+
defaultArm = arm
|
|
728
|
+
continue
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const patternValue = this.lowerExpr(arm.pattern)
|
|
732
|
+
if (patternValue.kind !== 'const') {
|
|
733
|
+
throw new Error('Match patterns must lower to compile-time constants')
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const subFnName = `${this.currentFn}/match_${this.foreachCounter++}`
|
|
737
|
+
this.builder.emitRaw(`execute if score ${matchedVar} rs matches ..0 if score ${subject} rs matches ${patternValue.value} run function ${this.namespace}:${subFnName}`)
|
|
738
|
+
this.emitMatchArmSubFunction(subFnName, matchedVar, arm.body, true)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (defaultArm) {
|
|
742
|
+
const subFnName = `${this.currentFn}/match_${this.foreachCounter++}`
|
|
743
|
+
this.builder.emitRaw(`execute if score ${matchedVar} rs matches ..0 run function ${this.namespace}:${subFnName}`)
|
|
744
|
+
this.emitMatchArmSubFunction(subFnName, matchedVar, defaultArm.body, false)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private emitMatchArmSubFunction(name: string, matchedVar: string, body: Block, setMatched: boolean): void {
|
|
749
|
+
const savedBuilder = this.builder
|
|
750
|
+
const savedVarMap = new Map(this.varMap)
|
|
751
|
+
const savedContext = this.currentContext
|
|
752
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
753
|
+
|
|
754
|
+
this.builder = new LoweringBuilder()
|
|
755
|
+
this.varMap = new Map(savedVarMap)
|
|
756
|
+
this.currentContext = savedContext
|
|
757
|
+
this.blockPosVars = new Map(savedBlockPosVars)
|
|
758
|
+
|
|
759
|
+
this.builder.startBlock('entry')
|
|
760
|
+
if (setMatched) {
|
|
761
|
+
this.builder.emitRaw(`scoreboard players set ${matchedVar} rs 1`)
|
|
762
|
+
}
|
|
763
|
+
this.lowerBlock(body)
|
|
764
|
+
if (!this.builder.isBlockSealed()) {
|
|
765
|
+
this.builder.emitReturn()
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
this.functions.push(this.builder.build(name, [], false))
|
|
769
|
+
|
|
770
|
+
this.builder = savedBuilder
|
|
771
|
+
this.varMap = savedVarMap
|
|
772
|
+
this.currentContext = savedContext
|
|
773
|
+
this.blockPosVars = savedBlockPosVars
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
private lowerArrayForeachStmt(stmt: Extract<Stmt, { kind: 'foreach' }>): void {
|
|
777
|
+
const arrayName = this.getArrayStorageName(stmt.iterable)
|
|
778
|
+
if (!arrayName) {
|
|
779
|
+
this.builder.emitRaw('# Unsupported foreach iterable')
|
|
780
|
+
return
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const arrayType = this.inferExprType(stmt.iterable)
|
|
784
|
+
const bindingVar = `$${stmt.binding}`
|
|
785
|
+
const indexVar = this.builder.freshTemp()
|
|
786
|
+
const lengthVar = this.builder.freshTemp()
|
|
787
|
+
const condVar = this.builder.freshTemp()
|
|
788
|
+
const oneVar = this.builder.freshTemp()
|
|
789
|
+
|
|
790
|
+
const savedBinding = this.varMap.get(stmt.binding)
|
|
791
|
+
const savedType = this.varTypes.get(stmt.binding)
|
|
792
|
+
|
|
793
|
+
this.varMap.set(stmt.binding, bindingVar)
|
|
794
|
+
if (arrayType?.kind === 'array') {
|
|
795
|
+
this.varTypes.set(stmt.binding, arrayType.elem)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
this.builder.emitAssign(indexVar, { kind: 'const', value: 0 })
|
|
799
|
+
this.builder.emitAssign(oneVar, { kind: 'const', value: 1 })
|
|
800
|
+
this.builder.emitRaw(`execute store result score ${lengthVar} rs run data get storage rs:heap ${arrayName}`)
|
|
801
|
+
|
|
802
|
+
const checkLabel = this.builder.freshLabel('foreach_array_check')
|
|
803
|
+
const bodyLabel = this.builder.freshLabel('foreach_array_body')
|
|
804
|
+
const exitLabel = this.builder.freshLabel('foreach_array_exit')
|
|
805
|
+
|
|
806
|
+
this.builder.emitJump(checkLabel)
|
|
807
|
+
|
|
808
|
+
this.builder.startBlock(checkLabel)
|
|
809
|
+
this.builder.emitCmp(condVar, { kind: 'var', name: indexVar }, '<', { kind: 'var', name: lengthVar })
|
|
810
|
+
this.builder.emitJumpIf(condVar, bodyLabel, exitLabel)
|
|
811
|
+
|
|
812
|
+
this.builder.startBlock(bodyLabel)
|
|
813
|
+
const element = this.readArrayElement(arrayName, { kind: 'var', name: indexVar })
|
|
814
|
+
this.builder.emitAssign(bindingVar, element)
|
|
815
|
+
this.lowerBlock(stmt.body)
|
|
816
|
+
if (!this.builder.isBlockSealed()) {
|
|
817
|
+
this.builder.emitRaw(`scoreboard players operation ${indexVar} rs += ${oneVar} rs`)
|
|
818
|
+
this.builder.emitJump(checkLabel)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
this.builder.startBlock(exitLabel)
|
|
822
|
+
|
|
823
|
+
if (savedBinding) {
|
|
824
|
+
this.varMap.set(stmt.binding, savedBinding)
|
|
825
|
+
} else {
|
|
826
|
+
this.varMap.delete(stmt.binding)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (savedType) {
|
|
830
|
+
this.varTypes.set(stmt.binding, savedType)
|
|
831
|
+
} else {
|
|
832
|
+
this.varTypes.delete(stmt.binding)
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private lowerAsBlockStmt(stmt: Extract<Stmt, { kind: 'as_block' }>): void {
|
|
837
|
+
const selector = this.selectorToString(stmt.selector)
|
|
838
|
+
const subFnName = `${this.currentFn}/as_${this.foreachCounter++}`
|
|
839
|
+
|
|
840
|
+
this.builder.emitRaw(`execute as ${selector} run function ${this.namespace}:${subFnName}`)
|
|
841
|
+
|
|
842
|
+
// Create sub-function
|
|
843
|
+
const savedBuilder = this.builder
|
|
844
|
+
const savedVarMap = new Map(this.varMap)
|
|
845
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
846
|
+
|
|
847
|
+
this.builder = new LoweringBuilder()
|
|
848
|
+
this.varMap = new Map(savedVarMap)
|
|
849
|
+
this.blockPosVars = new Map(savedBlockPosVars)
|
|
850
|
+
|
|
851
|
+
this.builder.startBlock('entry')
|
|
852
|
+
this.lowerBlock(stmt.body)
|
|
853
|
+
if (!this.builder.isBlockSealed()) {
|
|
854
|
+
this.builder.emitReturn()
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const subFn = this.builder.build(subFnName, [], false)
|
|
858
|
+
this.functions.push(subFn)
|
|
859
|
+
|
|
860
|
+
this.builder = savedBuilder
|
|
861
|
+
this.varMap = savedVarMap
|
|
862
|
+
this.blockPosVars = savedBlockPosVars
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private lowerAtBlockStmt(stmt: Extract<Stmt, { kind: 'at_block' }>): void {
|
|
866
|
+
const selector = this.selectorToString(stmt.selector)
|
|
867
|
+
const subFnName = `${this.currentFn}/at_${this.foreachCounter++}`
|
|
868
|
+
|
|
869
|
+
this.builder.emitRaw(`execute at ${selector} run function ${this.namespace}:${subFnName}`)
|
|
870
|
+
|
|
871
|
+
// Create sub-function
|
|
872
|
+
const savedBuilder = this.builder
|
|
873
|
+
const savedVarMap = new Map(this.varMap)
|
|
874
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
875
|
+
|
|
876
|
+
this.builder = new LoweringBuilder()
|
|
877
|
+
this.varMap = new Map(savedVarMap)
|
|
878
|
+
this.blockPosVars = new Map(savedBlockPosVars)
|
|
879
|
+
|
|
880
|
+
this.builder.startBlock('entry')
|
|
881
|
+
this.lowerBlock(stmt.body)
|
|
882
|
+
if (!this.builder.isBlockSealed()) {
|
|
883
|
+
this.builder.emitReturn()
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const subFn = this.builder.build(subFnName, [], false)
|
|
887
|
+
this.functions.push(subFn)
|
|
888
|
+
|
|
889
|
+
this.builder = savedBuilder
|
|
890
|
+
this.varMap = savedVarMap
|
|
891
|
+
this.blockPosVars = savedBlockPosVars
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private lowerAsAtStmt(stmt: Extract<Stmt, { kind: 'as_at' }>): void {
|
|
895
|
+
const asSel = this.selectorToString(stmt.as_sel)
|
|
896
|
+
const atSel = this.selectorToString(stmt.at_sel)
|
|
897
|
+
const subFnName = `${this.currentFn}/as_at_${this.foreachCounter++}`
|
|
898
|
+
|
|
899
|
+
this.builder.emitRaw(`execute as ${asSel} at ${atSel} run function ${this.namespace}:${subFnName}`)
|
|
900
|
+
|
|
901
|
+
// Create sub-function
|
|
902
|
+
const savedBuilder = this.builder
|
|
903
|
+
const savedVarMap = new Map(this.varMap)
|
|
904
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
905
|
+
|
|
906
|
+
this.builder = new LoweringBuilder()
|
|
907
|
+
this.varMap = new Map(savedVarMap)
|
|
908
|
+
this.blockPosVars = new Map(savedBlockPosVars)
|
|
909
|
+
|
|
910
|
+
this.builder.startBlock('entry')
|
|
911
|
+
this.lowerBlock(stmt.body)
|
|
912
|
+
if (!this.builder.isBlockSealed()) {
|
|
913
|
+
this.builder.emitReturn()
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const subFn = this.builder.build(subFnName, [], false)
|
|
917
|
+
this.functions.push(subFn)
|
|
918
|
+
|
|
919
|
+
this.builder = savedBuilder
|
|
920
|
+
this.varMap = savedVarMap
|
|
921
|
+
this.blockPosVars = savedBlockPosVars
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private lowerExecuteStmt(stmt: Extract<Stmt, { kind: 'execute' }>): void {
|
|
925
|
+
// Build the execute prefix from subcommands
|
|
926
|
+
const parts: string[] = ['execute']
|
|
927
|
+
for (const sub of stmt.subcommands) {
|
|
928
|
+
switch (sub.kind) {
|
|
929
|
+
case 'as':
|
|
930
|
+
parts.push(`as ${this.selectorToString(sub.selector)}`)
|
|
931
|
+
break
|
|
932
|
+
case 'at':
|
|
933
|
+
parts.push(`at ${this.selectorToString(sub.selector)}`)
|
|
934
|
+
break
|
|
935
|
+
case 'if_entity':
|
|
936
|
+
parts.push(`if entity ${this.selectorToString(sub.selector)}`)
|
|
937
|
+
break
|
|
938
|
+
case 'unless_entity':
|
|
939
|
+
parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
|
|
940
|
+
break
|
|
941
|
+
case 'in':
|
|
942
|
+
parts.push(`in ${sub.dimension}`)
|
|
943
|
+
break
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const subFnName = `${this.currentFn}/exec_${this.foreachCounter++}`
|
|
948
|
+
this.builder.emitRaw(`${parts.join(' ')} run function ${this.namespace}:${subFnName}`)
|
|
949
|
+
|
|
950
|
+
// Create sub-function for the body
|
|
951
|
+
const savedBuilder = this.builder
|
|
952
|
+
const savedVarMap = new Map(this.varMap)
|
|
953
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
954
|
+
|
|
955
|
+
this.builder = new LoweringBuilder()
|
|
956
|
+
this.varMap = new Map(savedVarMap)
|
|
957
|
+
this.blockPosVars = new Map(savedBlockPosVars)
|
|
958
|
+
|
|
959
|
+
this.builder.startBlock('entry')
|
|
960
|
+
this.lowerBlock(stmt.body)
|
|
961
|
+
if (!this.builder.isBlockSealed()) {
|
|
962
|
+
this.builder.emitReturn()
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const subFn = this.builder.build(subFnName, [], false)
|
|
966
|
+
this.functions.push(subFn)
|
|
967
|
+
|
|
968
|
+
this.builder = savedBuilder
|
|
969
|
+
this.varMap = savedVarMap
|
|
970
|
+
this.blockPosVars = savedBlockPosVars
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// -------------------------------------------------------------------------
|
|
974
|
+
// Expression Lowering
|
|
975
|
+
// -------------------------------------------------------------------------
|
|
976
|
+
|
|
977
|
+
private lowerExpr(expr: Expr): Operand {
|
|
978
|
+
switch (expr.kind) {
|
|
979
|
+
case 'int_lit':
|
|
980
|
+
return { kind: 'const', value: expr.value }
|
|
981
|
+
|
|
982
|
+
case 'float_lit':
|
|
983
|
+
// Float stored as fixed-point × 1000
|
|
984
|
+
return { kind: 'const', value: Math.round(expr.value * 1000) }
|
|
985
|
+
|
|
986
|
+
case 'byte_lit':
|
|
987
|
+
return { kind: 'const', value: expr.value }
|
|
988
|
+
|
|
989
|
+
case 'short_lit':
|
|
990
|
+
return { kind: 'const', value: expr.value }
|
|
991
|
+
|
|
992
|
+
case 'long_lit':
|
|
993
|
+
return { kind: 'const', value: expr.value }
|
|
994
|
+
|
|
995
|
+
case 'double_lit':
|
|
996
|
+
return { kind: 'const', value: Math.round(expr.value * 1000) }
|
|
997
|
+
|
|
998
|
+
case 'bool_lit':
|
|
999
|
+
return { kind: 'const', value: expr.value ? 1 : 0 }
|
|
1000
|
+
|
|
1001
|
+
case 'str_lit':
|
|
1002
|
+
// Strings are handled inline in builtins
|
|
1003
|
+
return { kind: 'const', value: 0 } // Placeholder
|
|
1004
|
+
|
|
1005
|
+
case 'mc_name':
|
|
1006
|
+
// MC names (#health, #red) treated as string constants
|
|
1007
|
+
return { kind: 'const', value: 0 } // Handled inline in exprToString
|
|
1008
|
+
|
|
1009
|
+
case 'str_interp':
|
|
1010
|
+
// Interpolated strings are handled inline in message builtins.
|
|
1011
|
+
return { kind: 'const', value: 0 }
|
|
1012
|
+
|
|
1013
|
+
case 'range_lit':
|
|
1014
|
+
// Ranges are handled in context (selectors, etc.)
|
|
1015
|
+
return { kind: 'const', value: 0 }
|
|
1016
|
+
|
|
1017
|
+
case 'blockpos':
|
|
1018
|
+
return { kind: 'const', value: 0 }
|
|
1019
|
+
|
|
1020
|
+
case 'ident': {
|
|
1021
|
+
const constValue = this.constValues.get(expr.name)
|
|
1022
|
+
if (constValue) {
|
|
1023
|
+
return this.lowerConstLiteral(constValue)
|
|
1024
|
+
}
|
|
1025
|
+
const mapped = this.varMap.get(expr.name)
|
|
1026
|
+
if (mapped) {
|
|
1027
|
+
// Check if it's a selector reference (like @s)
|
|
1028
|
+
if (mapped.startsWith('@')) {
|
|
1029
|
+
return { kind: 'var', name: mapped }
|
|
1030
|
+
}
|
|
1031
|
+
return { kind: 'var', name: mapped }
|
|
1032
|
+
}
|
|
1033
|
+
return { kind: 'var', name: `$${expr.name}` }
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
case 'member':
|
|
1037
|
+
if (expr.obj.kind === 'ident' && this.enumDefs.has(expr.obj.name)) {
|
|
1038
|
+
const variants = this.enumDefs.get(expr.obj.name)!
|
|
1039
|
+
const value = variants.get(expr.field)
|
|
1040
|
+
if (value === undefined) {
|
|
1041
|
+
throw new Error(`Unknown enum variant ${expr.obj.name}.${expr.field}`)
|
|
1042
|
+
}
|
|
1043
|
+
return { kind: 'const', value }
|
|
1044
|
+
}
|
|
1045
|
+
return this.lowerMemberExpr(expr)
|
|
1046
|
+
|
|
1047
|
+
case 'selector':
|
|
1048
|
+
// Selectors are handled inline in builtins
|
|
1049
|
+
return { kind: 'var', name: this.selectorToString(expr.sel) }
|
|
1050
|
+
|
|
1051
|
+
case 'binary':
|
|
1052
|
+
return this.lowerBinaryExpr(expr)
|
|
1053
|
+
|
|
1054
|
+
case 'unary':
|
|
1055
|
+
return this.lowerUnaryExpr(expr)
|
|
1056
|
+
|
|
1057
|
+
case 'assign':
|
|
1058
|
+
return this.lowerAssignExpr(expr)
|
|
1059
|
+
|
|
1060
|
+
case 'call':
|
|
1061
|
+
return this.lowerCallExpr(expr)
|
|
1062
|
+
|
|
1063
|
+
case 'invoke':
|
|
1064
|
+
return this.lowerInvokeExpr(expr)
|
|
1065
|
+
|
|
1066
|
+
case 'member_assign':
|
|
1067
|
+
return this.lowerMemberAssign(expr)
|
|
1068
|
+
|
|
1069
|
+
case 'index':
|
|
1070
|
+
return this.lowerIndexExpr(expr)
|
|
1071
|
+
|
|
1072
|
+
case 'struct_lit':
|
|
1073
|
+
// Struct literals should be handled in let statement
|
|
1074
|
+
return { kind: 'const', value: 0 }
|
|
1075
|
+
|
|
1076
|
+
case 'array_lit':
|
|
1077
|
+
// Array literals should be handled in let statement
|
|
1078
|
+
return { kind: 'const', value: 0 }
|
|
1079
|
+
|
|
1080
|
+
case 'lambda':
|
|
1081
|
+
throw new Error('Lambda expressions must be used in a function context')
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
throw new Error(`Unhandled expression kind: ${(expr as { kind: string }).kind}`)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
private lowerMemberExpr(expr: Extract<Expr, { kind: 'member' }>): Operand {
|
|
1088
|
+
// Check if this is a struct field access
|
|
1089
|
+
if (expr.obj.kind === 'ident') {
|
|
1090
|
+
const varType = this.varTypes.get(expr.obj.name)
|
|
1091
|
+
|
|
1092
|
+
// Check for world object handle (entity selector)
|
|
1093
|
+
const mapped = this.varMap.get(expr.obj.name)
|
|
1094
|
+
if (mapped && mapped.startsWith('@e[tag=__rs_obj_')) {
|
|
1095
|
+
// World object field access → scoreboard get
|
|
1096
|
+
const dst = this.builder.freshTemp()
|
|
1097
|
+
this.builder.emitRaw(`scoreboard players operation ${dst} rs = ${mapped} rs`)
|
|
1098
|
+
return { kind: 'var', name: dst }
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (varType?.kind === 'struct') {
|
|
1102
|
+
const structName = varType.name.toLowerCase()
|
|
1103
|
+
const path = `rs:heap ${structName}_${expr.obj.name}.${expr.field}`
|
|
1104
|
+
const dst = this.builder.freshTemp()
|
|
1105
|
+
// Read from NBT storage into scoreboard
|
|
1106
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run data get storage ${path}`)
|
|
1107
|
+
return { kind: 'var', name: dst }
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Array length property
|
|
1111
|
+
if (varType?.kind === 'array' && expr.field === 'len') {
|
|
1112
|
+
const dst = this.builder.freshTemp()
|
|
1113
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run data get storage rs:heap ${expr.obj.name}`)
|
|
1114
|
+
return { kind: 'var', name: dst }
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Default behavior: simple member access
|
|
1119
|
+
return { kind: 'var', name: `$${(expr.obj as any).name}_${expr.field}` }
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
private lowerMemberAssign(expr: Extract<Expr, { kind: 'member_assign' }>): Operand {
|
|
1123
|
+
if (expr.obj.kind === 'ident') {
|
|
1124
|
+
const varType = this.varTypes.get(expr.obj.name)
|
|
1125
|
+
|
|
1126
|
+
// Check for world object handle
|
|
1127
|
+
const mapped = this.varMap.get(expr.obj.name)
|
|
1128
|
+
if (mapped && mapped.startsWith('@e[tag=__rs_obj_')) {
|
|
1129
|
+
const value = this.lowerExpr(expr.value)
|
|
1130
|
+
if (expr.op === '=') {
|
|
1131
|
+
if (value.kind === 'const') {
|
|
1132
|
+
this.builder.emitRaw(`scoreboard players set ${mapped} rs ${value.value}`)
|
|
1133
|
+
} else if (value.kind === 'var') {
|
|
1134
|
+
this.builder.emitRaw(`scoreboard players operation ${mapped} rs = ${value.name} rs`)
|
|
1135
|
+
}
|
|
1136
|
+
} else {
|
|
1137
|
+
// Compound assignment
|
|
1138
|
+
const binOp = expr.op.slice(0, -1)
|
|
1139
|
+
const opMap: Record<string, string> = { '+': '+=', '-': '-=', '*': '*=', '/': '/=', '%': '%=' }
|
|
1140
|
+
if (value.kind === 'const') {
|
|
1141
|
+
const constTemp = this.builder.freshTemp()
|
|
1142
|
+
this.builder.emitAssign(constTemp, value)
|
|
1143
|
+
this.builder.emitRaw(`scoreboard players operation ${mapped} rs ${opMap[binOp]} ${constTemp} rs`)
|
|
1144
|
+
} else if (value.kind === 'var') {
|
|
1145
|
+
this.builder.emitRaw(`scoreboard players operation ${mapped} rs ${opMap[binOp]} ${value.name} rs`)
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return { kind: 'const', value: 0 }
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (varType?.kind === 'struct') {
|
|
1152
|
+
const structName = varType.name.toLowerCase()
|
|
1153
|
+
const path = `rs:heap ${structName}_${expr.obj.name}.${expr.field}`
|
|
1154
|
+
const value = this.lowerExpr(expr.value)
|
|
1155
|
+
|
|
1156
|
+
if (expr.op === '=') {
|
|
1157
|
+
if (value.kind === 'const') {
|
|
1158
|
+
this.builder.emitRaw(`data modify storage ${path} set value ${value.value}`)
|
|
1159
|
+
} else if (value.kind === 'var') {
|
|
1160
|
+
this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${value.name} rs`)
|
|
1161
|
+
}
|
|
1162
|
+
} else {
|
|
1163
|
+
// Compound assignment: read, modify, write back
|
|
1164
|
+
const dst = this.builder.freshTemp()
|
|
1165
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run data get storage ${path}`)
|
|
1166
|
+
const binOp = expr.op.slice(0, -1)
|
|
1167
|
+
this.builder.emitBinop(dst, { kind: 'var', name: dst }, binOp as any, value)
|
|
1168
|
+
this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${dst} rs`)
|
|
1169
|
+
}
|
|
1170
|
+
return { kind: 'const', value: 0 }
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Default: simple assignment
|
|
1175
|
+
const varName = `$${(expr.obj as any).name}_${expr.field}`
|
|
1176
|
+
const value = this.lowerExpr(expr.value)
|
|
1177
|
+
this.builder.emitAssign(varName, value)
|
|
1178
|
+
return { kind: 'var', name: varName }
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
private lowerIndexExpr(expr: Extract<Expr, { kind: 'index' }>): Operand {
|
|
1182
|
+
const arrayName = this.getArrayStorageName(expr.obj)
|
|
1183
|
+
if (arrayName) {
|
|
1184
|
+
return this.readArrayElement(arrayName, this.lowerExpr(expr.index))
|
|
1185
|
+
}
|
|
1186
|
+
return { kind: 'const', value: 0 }
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
private lowerBinaryExpr(expr: Extract<Expr, { kind: 'binary' }>): Operand {
|
|
1190
|
+
const left = this.lowerExpr(expr.left)
|
|
1191
|
+
const right = this.lowerExpr(expr.right)
|
|
1192
|
+
const dst = this.builder.freshTemp()
|
|
1193
|
+
|
|
1194
|
+
if (['&&', '||'].includes(expr.op)) {
|
|
1195
|
+
// Logical operators need special handling
|
|
1196
|
+
if (expr.op === '&&') {
|
|
1197
|
+
// Short-circuit AND
|
|
1198
|
+
this.builder.emitAssign(dst, left)
|
|
1199
|
+
const rightVar = this.operandToVar(right)
|
|
1200
|
+
// dst = dst && right → if dst != 0 then dst = right
|
|
1201
|
+
this.builder.emitRaw(`execute if score ${dst} rs matches 1.. run scoreboard players operation ${dst} rs = ${rightVar} rs`)
|
|
1202
|
+
} else {
|
|
1203
|
+
// Short-circuit OR
|
|
1204
|
+
this.builder.emitAssign(dst, left)
|
|
1205
|
+
const rightVar = this.operandToVar(right)
|
|
1206
|
+
// dst = dst || right → if dst == 0 then dst = right
|
|
1207
|
+
this.builder.emitRaw(`execute if score ${dst} rs matches ..0 run scoreboard players operation ${dst} rs = ${rightVar} rs`)
|
|
1208
|
+
}
|
|
1209
|
+
return { kind: 'var', name: dst }
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (['==', '!=', '<', '<=', '>', '>='].includes(expr.op)) {
|
|
1213
|
+
this.builder.emitCmp(dst, left, expr.op as CmpOp, right)
|
|
1214
|
+
} else {
|
|
1215
|
+
// Check if this is float arithmetic
|
|
1216
|
+
const isFloatOp = this.isFloatExpr(expr.left) || this.isFloatExpr(expr.right)
|
|
1217
|
+
|
|
1218
|
+
if (isFloatOp && (expr.op === '*' || expr.op === '/')) {
|
|
1219
|
+
// Float multiplication: a * b / 1000
|
|
1220
|
+
// Float division: a * 1000 / b
|
|
1221
|
+
if (expr.op === '*') {
|
|
1222
|
+
this.builder.emitBinop(dst, left, '*', right)
|
|
1223
|
+
// Divide by 1000 to correct for double scaling
|
|
1224
|
+
const constDiv = this.builder.freshTemp()
|
|
1225
|
+
this.builder.emitAssign(constDiv, { kind: 'const', value: 1000 })
|
|
1226
|
+
this.builder.emitRaw(`scoreboard players operation ${dst} rs /= ${constDiv} rs`)
|
|
1227
|
+
} else {
|
|
1228
|
+
// Division: a * 1000 / b
|
|
1229
|
+
const constMul = this.builder.freshTemp()
|
|
1230
|
+
this.builder.emitAssign(constMul, { kind: 'const', value: 1000 })
|
|
1231
|
+
this.builder.emitAssign(dst, left)
|
|
1232
|
+
this.builder.emitRaw(`scoreboard players operation ${dst} rs *= ${constMul} rs`)
|
|
1233
|
+
const rightVar = this.operandToVar(right)
|
|
1234
|
+
this.builder.emitRaw(`scoreboard players operation ${dst} rs /= ${rightVar} rs`)
|
|
1235
|
+
}
|
|
1236
|
+
return { kind: 'var', name: dst }
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
this.builder.emitBinop(dst, left, expr.op as BinOp, right)
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return { kind: 'var', name: dst }
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
private isFloatExpr(expr: Expr): boolean {
|
|
1246
|
+
if (expr.kind === 'float_lit') return true
|
|
1247
|
+
if (expr.kind === 'ident') {
|
|
1248
|
+
return this.floatVars.has(expr.name)
|
|
1249
|
+
}
|
|
1250
|
+
if (expr.kind === 'binary') {
|
|
1251
|
+
return this.isFloatExpr(expr.left) || this.isFloatExpr(expr.right)
|
|
1252
|
+
}
|
|
1253
|
+
return false
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
private lowerUnaryExpr(expr: Extract<Expr, { kind: 'unary' }>): Operand {
|
|
1257
|
+
const operand = this.lowerExpr(expr.operand)
|
|
1258
|
+
const dst = this.builder.freshTemp()
|
|
1259
|
+
|
|
1260
|
+
if (expr.op === '!') {
|
|
1261
|
+
// Logical NOT: dst = (operand == 0) ? 1 : 0
|
|
1262
|
+
this.builder.emitCmp(dst, operand, '==', { kind: 'const', value: 0 })
|
|
1263
|
+
} else if (expr.op === '-') {
|
|
1264
|
+
// Negation: dst = 0 - operand
|
|
1265
|
+
this.builder.emitBinop(dst, { kind: 'const', value: 0 }, '-', operand)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
return { kind: 'var', name: dst }
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
private lowerAssignExpr(expr: Extract<Expr, { kind: 'assign' }>): Operand {
|
|
1272
|
+
const blockPosValue = this.resolveBlockPosExpr(expr.value)
|
|
1273
|
+
if (blockPosValue) {
|
|
1274
|
+
this.blockPosVars.set(expr.target, blockPosValue)
|
|
1275
|
+
return { kind: 'const', value: 0 }
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
this.blockPosVars.delete(expr.target)
|
|
1279
|
+
const targetType = this.varTypes.get(expr.target)
|
|
1280
|
+
if (targetType?.kind === 'named' && targetType.name === 'string' && this.storeStringValue(expr.target, expr.value)) {
|
|
1281
|
+
return { kind: 'const', value: 0 }
|
|
1282
|
+
}
|
|
1283
|
+
const varName = this.varMap.get(expr.target) ?? `$${expr.target}`
|
|
1284
|
+
const value = this.lowerExpr(expr.value)
|
|
1285
|
+
|
|
1286
|
+
if (expr.op === '=') {
|
|
1287
|
+
this.builder.emitAssign(varName, value)
|
|
1288
|
+
} else {
|
|
1289
|
+
// Compound assignment
|
|
1290
|
+
const binOp = expr.op.slice(0, -1) as BinOp // Remove '='
|
|
1291
|
+
const dst = this.builder.freshTemp()
|
|
1292
|
+
this.builder.emitBinop(dst, { kind: 'var', name: varName }, binOp, value)
|
|
1293
|
+
this.builder.emitAssign(varName, { kind: 'var', name: dst })
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
return { kind: 'var', name: varName }
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
private lowerCallExpr(expr: Extract<Expr, { kind: 'call' }>): Operand {
|
|
1300
|
+
if (expr.fn === 'str_len') {
|
|
1301
|
+
const storagePath = this.getStringStoragePath(expr.args[0])
|
|
1302
|
+
if (storagePath) {
|
|
1303
|
+
const dst = this.builder.freshTemp()
|
|
1304
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run data get storage ${storagePath}`)
|
|
1305
|
+
return { kind: 'var', name: dst }
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const staticString = this.resolveStaticString(expr.args[0])
|
|
1309
|
+
if (staticString !== null) {
|
|
1310
|
+
return { kind: 'const', value: Array.from(staticString).length }
|
|
1311
|
+
} else {
|
|
1312
|
+
const dst = this.builder.freshTemp()
|
|
1313
|
+
this.builder.emitAssign(dst, { kind: 'const', value: 0 })
|
|
1314
|
+
return { kind: 'var', name: dst }
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Check for builtin
|
|
1319
|
+
if (expr.fn in BUILTINS) {
|
|
1320
|
+
return this.lowerBuiltinCall(expr.fn, expr.args, getSpan(expr))
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Handle entity methods: __entity_tag, __entity_untag, __entity_has_tag
|
|
1324
|
+
if (expr.fn === '__entity_tag') {
|
|
1325
|
+
const entity = this.exprToString(expr.args[0])
|
|
1326
|
+
const tagName = this.exprToString(expr.args[1])
|
|
1327
|
+
this.builder.emitRaw(`tag ${entity} add ${tagName}`)
|
|
1328
|
+
return { kind: 'const', value: 0 }
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (expr.fn === '__entity_untag') {
|
|
1332
|
+
const entity = this.exprToString(expr.args[0])
|
|
1333
|
+
const tagName = this.exprToString(expr.args[1])
|
|
1334
|
+
this.builder.emitRaw(`tag ${entity} remove ${tagName}`)
|
|
1335
|
+
return { kind: 'const', value: 0 }
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (expr.fn === '__entity_has_tag') {
|
|
1339
|
+
const entity = this.exprToString(expr.args[0])
|
|
1340
|
+
const tagName = this.exprToString(expr.args[1])
|
|
1341
|
+
const dst = this.builder.freshTemp()
|
|
1342
|
+
this.builder.emitRaw(`execute store result score ${dst} rs if entity ${entity}[tag=${tagName}]`)
|
|
1343
|
+
return { kind: 'var', name: dst }
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Handle array push
|
|
1347
|
+
if (expr.fn === '__array_push') {
|
|
1348
|
+
const arrExpr = expr.args[0]
|
|
1349
|
+
const valueExpr = expr.args[1]
|
|
1350
|
+
const arrName = this.getArrayStorageName(arrExpr)
|
|
1351
|
+
if (arrName) {
|
|
1352
|
+
const value = this.lowerExpr(valueExpr)
|
|
1353
|
+
if (value.kind === 'const') {
|
|
1354
|
+
this.builder.emitRaw(`data modify storage rs:heap ${arrName} append value ${value.value}`)
|
|
1355
|
+
} else if (value.kind === 'var') {
|
|
1356
|
+
this.builder.emitRaw(`data modify storage rs:heap ${arrName} append value 0`)
|
|
1357
|
+
this.builder.emitRaw(`execute store result storage rs:heap ${arrName}[-1] int 1 run scoreboard players get ${value.name} rs`)
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return { kind: 'const', value: 0 }
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
if (expr.fn === '__array_pop') {
|
|
1364
|
+
const arrName = this.getArrayStorageName(expr.args[0])
|
|
1365
|
+
const dst = this.builder.freshTemp()
|
|
1366
|
+
if (arrName) {
|
|
1367
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run data get storage rs:heap ${arrName}[-1]`)
|
|
1368
|
+
this.builder.emitRaw(`data remove storage rs:heap ${arrName}[-1]`)
|
|
1369
|
+
} else {
|
|
1370
|
+
this.builder.emitAssign(dst, { kind: 'const', value: 0 })
|
|
1371
|
+
}
|
|
1372
|
+
return { kind: 'var', name: dst }
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Handle spawn_object - creates world object (invisible armor stand)
|
|
1376
|
+
if (expr.fn === 'spawn_object') {
|
|
1377
|
+
const x = this.exprToString(expr.args[0])
|
|
1378
|
+
const y = this.exprToString(expr.args[1])
|
|
1379
|
+
const z = this.exprToString(expr.args[2])
|
|
1380
|
+
const tag = `__rs_obj_${this.worldObjCounter++}`
|
|
1381
|
+
this.builder.emitRaw(`summon minecraft:armor_stand ${x} ${y} ${z} {Invisible:1b,Marker:1b,NoGravity:1b,Tags:["${tag}"]}`)
|
|
1382
|
+
// Return a selector pointing to this entity
|
|
1383
|
+
const selector = `@e[tag=${tag},limit=1]`
|
|
1384
|
+
return { kind: 'var', name: selector }
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Handle kill for world objects
|
|
1388
|
+
if (expr.fn === 'kill' && expr.args.length === 1 && expr.args[0].kind === 'ident') {
|
|
1389
|
+
const mapped = this.varMap.get(expr.args[0].name)
|
|
1390
|
+
if (mapped && mapped.startsWith('@e[tag=__rs_obj_')) {
|
|
1391
|
+
this.builder.emitRaw(`kill ${mapped}`)
|
|
1392
|
+
return { kind: 'const', value: 0 }
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const callbackTarget = this.resolveFunctionRefByName(expr.fn)
|
|
1397
|
+
if (callbackTarget) {
|
|
1398
|
+
return this.emitDirectFunctionCall(callbackTarget, expr.args)
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// Regular function call
|
|
1402
|
+
const fnDecl = this.fnDecls.get(expr.fn)
|
|
1403
|
+
const defaultArgs = this.functionDefaults.get(expr.fn) ?? []
|
|
1404
|
+
const fullArgs = [...expr.args]
|
|
1405
|
+
for (let i = fullArgs.length; i < defaultArgs.length; i++) {
|
|
1406
|
+
const defaultExpr = defaultArgs[i]
|
|
1407
|
+
if (!defaultExpr) {
|
|
1408
|
+
break
|
|
1409
|
+
}
|
|
1410
|
+
fullArgs.push(defaultExpr)
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (fnDecl) {
|
|
1414
|
+
const callbackBindings = new Map<string, string>()
|
|
1415
|
+
const runtimeArgs: Expr[] = []
|
|
1416
|
+
|
|
1417
|
+
for (let i = 0; i < fullArgs.length; i++) {
|
|
1418
|
+
const param = fnDecl.params[i]
|
|
1419
|
+
if (param && this.normalizeType(param.type).kind === 'function_type') {
|
|
1420
|
+
const functionRef = this.resolveFunctionRefExpr(fullArgs[i])
|
|
1421
|
+
if (!functionRef) {
|
|
1422
|
+
throw new Error(`Cannot lower callback argument for parameter '${param.name}'`)
|
|
1423
|
+
}
|
|
1424
|
+
callbackBindings.set(param.name, functionRef)
|
|
1425
|
+
continue
|
|
1426
|
+
}
|
|
1427
|
+
runtimeArgs.push(fullArgs[i])
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const targetFn = callbackBindings.size > 0
|
|
1431
|
+
? this.ensureSpecializedFunction(fnDecl, callbackBindings)
|
|
1432
|
+
: expr.fn
|
|
1433
|
+
return this.emitDirectFunctionCall(targetFn, runtimeArgs)
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
return this.emitDirectFunctionCall(expr.fn, fullArgs)
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
private lowerInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): Operand {
|
|
1440
|
+
if (expr.callee.kind === 'lambda') {
|
|
1441
|
+
if (!Array.isArray(expr.callee.body)) {
|
|
1442
|
+
return this.inlineLambdaInvoke(expr.callee, expr.args)
|
|
1443
|
+
}
|
|
1444
|
+
const lambdaName = this.lowerLambdaExpr(expr.callee)
|
|
1445
|
+
return this.emitDirectFunctionCall(lambdaName, expr.args)
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const functionRef = this.resolveFunctionRefExpr(expr.callee)
|
|
1449
|
+
if (!functionRef) {
|
|
1450
|
+
throw new Error('Cannot invoke a non-function value')
|
|
1451
|
+
}
|
|
1452
|
+
return this.emitDirectFunctionCall(functionRef, expr.args)
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
private inlineLambdaInvoke(expr: Extract<Expr, { kind: 'lambda' }>, args: Expr[]): Operand {
|
|
1456
|
+
const savedVarMap = new Map(this.varMap)
|
|
1457
|
+
const savedVarTypes = new Map(this.varTypes)
|
|
1458
|
+
const savedLambdaBindings = new Map(this.lambdaBindings)
|
|
1459
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
1460
|
+
|
|
1461
|
+
for (let i = 0; i < expr.params.length; i++) {
|
|
1462
|
+
const param = expr.params[i]
|
|
1463
|
+
const temp = this.builder.freshTemp()
|
|
1464
|
+
const arg = args[i]
|
|
1465
|
+
this.builder.emitAssign(temp, arg ? this.lowerExpr(arg) : { kind: 'const', value: 0 })
|
|
1466
|
+
this.varMap.set(param.name, temp)
|
|
1467
|
+
if (param.type) {
|
|
1468
|
+
this.varTypes.set(param.name, this.normalizeType(param.type))
|
|
1469
|
+
}
|
|
1470
|
+
this.lambdaBindings.delete(param.name)
|
|
1471
|
+
this.blockPosVars.delete(param.name)
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const result = this.lowerExpr(expr.body as Expr)
|
|
1475
|
+
|
|
1476
|
+
this.varMap = savedVarMap
|
|
1477
|
+
this.varTypes = savedVarTypes
|
|
1478
|
+
this.lambdaBindings = savedLambdaBindings
|
|
1479
|
+
this.blockPosVars = savedBlockPosVars
|
|
1480
|
+
return result
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
private emitDirectFunctionCall(fn: string, args: Expr[]): Operand {
|
|
1484
|
+
const loweredArgs: Operand[] = args.map(arg => this.lowerExpr(arg))
|
|
1485
|
+
const dst = this.builder.freshTemp()
|
|
1486
|
+
this.builder.emitCall(fn, loweredArgs, dst)
|
|
1487
|
+
return { kind: 'var', name: dst }
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
private resolveFunctionRefExpr(expr: Expr): string | null {
|
|
1491
|
+
if (expr.kind === 'lambda') {
|
|
1492
|
+
return this.lowerLambdaExpr(expr)
|
|
1493
|
+
}
|
|
1494
|
+
if (expr.kind === 'ident') {
|
|
1495
|
+
return this.resolveFunctionRefByName(expr.name) ?? (this.fnDecls.has(expr.name) ? expr.name : null)
|
|
1496
|
+
}
|
|
1497
|
+
return null
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
private resolveFunctionRefByName(name: string): string | null {
|
|
1501
|
+
return this.lambdaBindings.get(name) ?? this.currentCallbackBindings.get(name) ?? null
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
private ensureSpecializedFunction(fn: FnDecl, callbackBindings: Map<string, string>): string {
|
|
1505
|
+
const parts = [...callbackBindings.entries()]
|
|
1506
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1507
|
+
.map(([param, target]) => `${param}_${target.replace(/[^a-zA-Z0-9_]/g, '_')}`)
|
|
1508
|
+
const key = `${fn.name}::${parts.join('::')}`
|
|
1509
|
+
const cached = this.specializedFunctions.get(key)
|
|
1510
|
+
if (cached) {
|
|
1511
|
+
return cached
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
const specializedName = `${fn.name}__${parts.join('__')}`
|
|
1515
|
+
this.specializedFunctions.set(key, specializedName)
|
|
1516
|
+
this.withSavedFunctionState(() => {
|
|
1517
|
+
this.lowerFn(fn, { name: specializedName, callbackBindings })
|
|
1518
|
+
})
|
|
1519
|
+
return specializedName
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
private lowerLambdaExpr(expr: Extract<Expr, { kind: 'lambda' }>): string {
|
|
1523
|
+
const lambdaName = `__lambda_${this.lambdaCounter++}`
|
|
1524
|
+
const lambdaFn: FnDecl = {
|
|
1525
|
+
name: lambdaName,
|
|
1526
|
+
params: expr.params.map(param => ({
|
|
1527
|
+
name: param.name,
|
|
1528
|
+
type: param.type ?? { kind: 'named', name: 'int' },
|
|
1529
|
+
})),
|
|
1530
|
+
returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
|
|
1531
|
+
decorators: [],
|
|
1532
|
+
body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
|
|
1533
|
+
}
|
|
1534
|
+
this.withSavedFunctionState(() => {
|
|
1535
|
+
this.lowerFn(lambdaFn)
|
|
1536
|
+
})
|
|
1537
|
+
return lambdaName
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
private withSavedFunctionState<T>(callback: () => T): T {
|
|
1541
|
+
const savedCurrentFn = this.currentFn
|
|
1542
|
+
const savedForeachCounter = this.foreachCounter
|
|
1543
|
+
const savedBuilder = this.builder
|
|
1544
|
+
const savedVarMap = new Map(this.varMap)
|
|
1545
|
+
const savedLambdaBindings = new Map(this.lambdaBindings)
|
|
1546
|
+
const savedCallbackBindings = new Map(this.currentCallbackBindings)
|
|
1547
|
+
const savedContext = this.currentContext
|
|
1548
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
1549
|
+
const savedStringValues = new Map(this.stringValues)
|
|
1550
|
+
const savedVarTypes = new Map(this.varTypes)
|
|
1551
|
+
|
|
1552
|
+
try {
|
|
1553
|
+
return callback()
|
|
1554
|
+
} finally {
|
|
1555
|
+
this.currentFn = savedCurrentFn
|
|
1556
|
+
this.foreachCounter = savedForeachCounter
|
|
1557
|
+
this.builder = savedBuilder
|
|
1558
|
+
this.varMap = savedVarMap
|
|
1559
|
+
this.lambdaBindings = savedLambdaBindings
|
|
1560
|
+
this.currentCallbackBindings = savedCallbackBindings
|
|
1561
|
+
this.currentContext = savedContext
|
|
1562
|
+
this.blockPosVars = savedBlockPosVars
|
|
1563
|
+
this.stringValues = savedStringValues
|
|
1564
|
+
this.varTypes = savedVarTypes
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
private lowerBuiltinCall(name: string, args: Expr[], callSpan?: Span): Operand {
|
|
1569
|
+
const richTextCommand = this.lowerRichTextBuiltin(name, args)
|
|
1570
|
+
if (richTextCommand) {
|
|
1571
|
+
this.builder.emitRaw(richTextCommand)
|
|
1572
|
+
return { kind: 'const', value: 0 }
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Special case: random - legacy scoreboard RNG for pre-1.20.3 compatibility
|
|
1576
|
+
if (name === 'random') {
|
|
1577
|
+
const dst = this.builder.freshTemp()
|
|
1578
|
+
const min = args[0] ? this.exprToLiteral(args[0]) : '0'
|
|
1579
|
+
const max = args[1] ? this.exprToLiteral(args[1]) : '100'
|
|
1580
|
+
this.builder.emitRaw(`scoreboard players random ${dst} rs ${min} ${max}`)
|
|
1581
|
+
return { kind: 'var', name: dst }
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Special case: random_native - /random value (MC 1.20.3+)
|
|
1585
|
+
if (name === 'random_native') {
|
|
1586
|
+
const dst = this.builder.freshTemp()
|
|
1587
|
+
const min = args[0] ? this.exprToLiteral(args[0]) : '0'
|
|
1588
|
+
const max = args[1] ? this.exprToLiteral(args[1]) : '100'
|
|
1589
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run random value ${min} ${max}`)
|
|
1590
|
+
return { kind: 'var', name: dst }
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// Special case: random_sequence - /random reset (MC 1.20.3+)
|
|
1594
|
+
if (name === 'random_sequence') {
|
|
1595
|
+
const sequence = this.exprToString(args[0])
|
|
1596
|
+
const seed = args[1] ? this.exprToLiteral(args[1]) : '0'
|
|
1597
|
+
this.builder.emitRaw(`random reset ${sequence} ${seed}`)
|
|
1598
|
+
return { kind: 'const', value: 0 }
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Special case: scoreboard_get / score — read from vanilla MC scoreboard
|
|
1602
|
+
if (name === 'scoreboard_get' || name === 'score') {
|
|
1603
|
+
const dst = this.builder.freshTemp()
|
|
1604
|
+
const player = this.exprToTargetString(args[0])
|
|
1605
|
+
const objective = this.exprToString(args[1])
|
|
1606
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run scoreboard players get ${player} ${objective}`)
|
|
1607
|
+
return { kind: 'var', name: dst }
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Special case: scoreboard_set — write to vanilla MC scoreboard
|
|
1611
|
+
if (name === 'scoreboard_set') {
|
|
1612
|
+
const player = this.exprToTargetString(args[0])
|
|
1613
|
+
const objective = this.exprToString(args[1])
|
|
1614
|
+
const value = this.lowerExpr(args[2])
|
|
1615
|
+
if (value.kind === 'const') {
|
|
1616
|
+
this.builder.emitRaw(`scoreboard players set ${player} ${objective} ${value.value}`)
|
|
1617
|
+
} else if (value.kind === 'var') {
|
|
1618
|
+
// Read directly from the computed scoreboard temp. Routing through a fresh
|
|
1619
|
+
// temp here breaks once optimization removes the apparently-dead assign.
|
|
1620
|
+
this.builder.emitRaw(`execute store result score ${player} ${objective} run scoreboard players get ${value.name} rs`)
|
|
1621
|
+
}
|
|
1622
|
+
return { kind: 'const', value: 0 }
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (name === 'scoreboard_display') {
|
|
1626
|
+
const slot = this.exprToString(args[0])
|
|
1627
|
+
const objective = this.exprToString(args[1])
|
|
1628
|
+
this.builder.emitRaw(`scoreboard objectives setdisplay ${slot} ${objective}`)
|
|
1629
|
+
return { kind: 'const', value: 0 }
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
if (name === 'scoreboard_hide') {
|
|
1633
|
+
const slot = this.exprToString(args[0])
|
|
1634
|
+
this.builder.emitRaw(`scoreboard objectives setdisplay ${slot}`)
|
|
1635
|
+
return { kind: 'const', value: 0 }
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (name === 'scoreboard_add_objective') {
|
|
1639
|
+
const objective = this.exprToString(args[0])
|
|
1640
|
+
const criteria = this.exprToString(args[1])
|
|
1641
|
+
const displayName = args[2] ? ` ${this.exprToQuotedString(args[2])}` : ''
|
|
1642
|
+
this.builder.emitRaw(`scoreboard objectives add ${objective} ${criteria}${displayName}`)
|
|
1643
|
+
return { kind: 'const', value: 0 }
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (name === 'scoreboard_remove_objective') {
|
|
1647
|
+
const objective = this.exprToString(args[0])
|
|
1648
|
+
this.builder.emitRaw(`scoreboard objectives remove ${objective}`)
|
|
1649
|
+
return { kind: 'const', value: 0 }
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
if (name === 'bossbar_add') {
|
|
1653
|
+
const id = this.exprToString(args[0])
|
|
1654
|
+
const title = this.exprToTextComponent(args[1])
|
|
1655
|
+
this.builder.emitRaw(`bossbar add ${id} ${title}`)
|
|
1656
|
+
return { kind: 'const', value: 0 }
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (name === 'bossbar_set_value') {
|
|
1660
|
+
this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} value ${this.exprToString(args[1])}`)
|
|
1661
|
+
return { kind: 'const', value: 0 }
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
if (name === 'bossbar_set_max') {
|
|
1665
|
+
this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} max ${this.exprToString(args[1])}`)
|
|
1666
|
+
return { kind: 'const', value: 0 }
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
if (name === 'bossbar_set_color') {
|
|
1670
|
+
this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} color ${this.exprToString(args[1])}`)
|
|
1671
|
+
return { kind: 'const', value: 0 }
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
if (name === 'bossbar_set_style') {
|
|
1675
|
+
this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} style ${this.exprToString(args[1])}`)
|
|
1676
|
+
return { kind: 'const', value: 0 }
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
if (name === 'bossbar_set_visible') {
|
|
1680
|
+
this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} visible ${this.exprToBoolString(args[1])}`)
|
|
1681
|
+
return { kind: 'const', value: 0 }
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (name === 'bossbar_set_players') {
|
|
1685
|
+
this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} players ${this.exprToTargetString(args[1])}`)
|
|
1686
|
+
return { kind: 'const', value: 0 }
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (name === 'bossbar_remove') {
|
|
1690
|
+
this.builder.emitRaw(`bossbar remove ${this.exprToString(args[0])}`)
|
|
1691
|
+
return { kind: 'const', value: 0 }
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (name === 'bossbar_get_value') {
|
|
1695
|
+
const dst = this.builder.freshTemp()
|
|
1696
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run bossbar get ${this.exprToString(args[0])} value`)
|
|
1697
|
+
return { kind: 'var', name: dst }
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
if (name === 'team_add') {
|
|
1701
|
+
const team = this.exprToString(args[0])
|
|
1702
|
+
const displayName = args[1] ? ` ${this.exprToTextComponent(args[1])}` : ''
|
|
1703
|
+
this.builder.emitRaw(`team add ${team}${displayName}`)
|
|
1704
|
+
return { kind: 'const', value: 0 }
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (name === 'team_remove') {
|
|
1708
|
+
this.builder.emitRaw(`team remove ${this.exprToString(args[0])}`)
|
|
1709
|
+
return { kind: 'const', value: 0 }
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
if (name === 'team_join') {
|
|
1713
|
+
this.builder.emitRaw(`team join ${this.exprToString(args[0])} ${this.exprToTargetString(args[1])}`)
|
|
1714
|
+
return { kind: 'const', value: 0 }
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
if (name === 'team_leave') {
|
|
1718
|
+
this.builder.emitRaw(`team leave ${this.exprToTargetString(args[0])}`)
|
|
1719
|
+
return { kind: 'const', value: 0 }
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
if (name === 'team_option') {
|
|
1723
|
+
const team = this.exprToString(args[0])
|
|
1724
|
+
const option = this.exprToString(args[1])
|
|
1725
|
+
const value = this.isTeamTextOption(option)
|
|
1726
|
+
? this.exprToTextComponent(args[2])
|
|
1727
|
+
: this.exprToString(args[2])
|
|
1728
|
+
this.builder.emitRaw(`team modify ${team} ${option} ${value}`)
|
|
1729
|
+
return { kind: 'const', value: 0 }
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// Special case: data_get — read NBT data into a variable
|
|
1733
|
+
// data_get(target_type, target, path, scale?)
|
|
1734
|
+
// target_type: "entity", "block", "storage"
|
|
1735
|
+
if (name === 'data_get') {
|
|
1736
|
+
const dst = this.builder.freshTemp()
|
|
1737
|
+
const targetType = this.exprToString(args[0])
|
|
1738
|
+
const target = targetType === 'entity'
|
|
1739
|
+
? this.exprToTargetString(args[1])
|
|
1740
|
+
: this.exprToString(args[1])
|
|
1741
|
+
const path = this.exprToString(args[2])
|
|
1742
|
+
const scale = args[3] ? this.exprToString(args[3]) : '1'
|
|
1743
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run data get ${targetType} ${target} ${path} ${scale}`)
|
|
1744
|
+
return { kind: 'var', name: dst }
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
const coordCommand = this.lowerCoordinateBuiltin(name, args)
|
|
1748
|
+
if (coordCommand) {
|
|
1749
|
+
this.builder.emitRaw(coordCommand)
|
|
1750
|
+
return { kind: 'const', value: 0 }
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
if (name === 'tp_to') {
|
|
1754
|
+
this.warnings.push({
|
|
1755
|
+
message: 'tp_to is deprecated; use tp instead',
|
|
1756
|
+
code: 'W_DEPRECATED',
|
|
1757
|
+
...(callSpan ? { line: callSpan.line, col: callSpan.col } : {}),
|
|
1758
|
+
})
|
|
1759
|
+
const tpCommand = this.lowerTpCommand(args)
|
|
1760
|
+
if (tpCommand) {
|
|
1761
|
+
this.builder.emitRaw(tpCommand)
|
|
1762
|
+
}
|
|
1763
|
+
return { kind: 'const', value: 0 }
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
if (name === 'tp') {
|
|
1767
|
+
const tpCommand = this.lowerTpCommand(args)
|
|
1768
|
+
if (tpCommand) {
|
|
1769
|
+
this.builder.emitRaw(tpCommand)
|
|
1770
|
+
}
|
|
1771
|
+
return { kind: 'const', value: 0 }
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Convert args to strings for builtin (use SNBT for struct/array literals)
|
|
1775
|
+
const strArgs = args.map(arg =>
|
|
1776
|
+
arg.kind === 'struct_lit' || arg.kind === 'array_lit'
|
|
1777
|
+
? this.exprToSnbt(arg)
|
|
1778
|
+
: this.exprToString(arg)
|
|
1779
|
+
)
|
|
1780
|
+
const cmd = BUILTINS[name](strArgs)
|
|
1781
|
+
if (cmd) {
|
|
1782
|
+
this.builder.emitRaw(cmd)
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
return { kind: 'const', value: 0 }
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
private lowerRichTextBuiltin(name: string, args: Expr[]): string | null {
|
|
1789
|
+
const messageArgIndex = this.getRichTextArgIndex(name)
|
|
1790
|
+
if (messageArgIndex === null) {
|
|
1791
|
+
return null
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const messageExpr = args[messageArgIndex]
|
|
1795
|
+
if (!messageExpr || messageExpr.kind !== 'str_interp') {
|
|
1796
|
+
return null
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const json = this.buildRichTextJson(messageExpr)
|
|
1800
|
+
|
|
1801
|
+
switch (name) {
|
|
1802
|
+
case 'say':
|
|
1803
|
+
case 'announce':
|
|
1804
|
+
return `tellraw @a ${json}`
|
|
1805
|
+
case 'tell':
|
|
1806
|
+
return `tellraw ${this.exprToString(args[0])} ${json}`
|
|
1807
|
+
case 'title':
|
|
1808
|
+
return `title ${this.exprToString(args[0])} title ${json}`
|
|
1809
|
+
case 'actionbar':
|
|
1810
|
+
return `title ${this.exprToString(args[0])} actionbar ${json}`
|
|
1811
|
+
case 'subtitle':
|
|
1812
|
+
return `title ${this.exprToString(args[0])} subtitle ${json}`
|
|
1813
|
+
default:
|
|
1814
|
+
return null
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
private getRichTextArgIndex(name: string): number | null {
|
|
1819
|
+
switch (name) {
|
|
1820
|
+
case 'say':
|
|
1821
|
+
case 'announce':
|
|
1822
|
+
return 0
|
|
1823
|
+
case 'tell':
|
|
1824
|
+
case 'title':
|
|
1825
|
+
case 'actionbar':
|
|
1826
|
+
case 'subtitle':
|
|
1827
|
+
return 1
|
|
1828
|
+
default:
|
|
1829
|
+
return null
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' }>): string {
|
|
1834
|
+
const components: Array<string | Record<string, unknown>> = ['']
|
|
1835
|
+
|
|
1836
|
+
for (const part of expr.parts) {
|
|
1837
|
+
if (typeof part === 'string') {
|
|
1838
|
+
if (part.length > 0) {
|
|
1839
|
+
components.push({ text: part })
|
|
1840
|
+
}
|
|
1841
|
+
continue
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
this.appendRichTextExpr(components, part)
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
return JSON.stringify(components)
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
private appendRichTextExpr(components: Array<string | Record<string, unknown>>, expr: Expr): void {
|
|
1851
|
+
if (expr.kind === 'ident') {
|
|
1852
|
+
const constValue = this.constValues.get(expr.name)
|
|
1853
|
+
if (constValue) {
|
|
1854
|
+
this.appendRichTextExpr(components, constValue)
|
|
1855
|
+
return
|
|
1856
|
+
}
|
|
1857
|
+
const stringValue = this.stringValues.get(expr.name)
|
|
1858
|
+
if (stringValue !== undefined) {
|
|
1859
|
+
components.push({ text: stringValue })
|
|
1860
|
+
return
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
if (expr.kind === 'str_lit') {
|
|
1865
|
+
if (expr.value.length > 0) {
|
|
1866
|
+
components.push({ text: expr.value })
|
|
1867
|
+
}
|
|
1868
|
+
return
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
if (expr.kind === 'str_interp') {
|
|
1872
|
+
for (const part of expr.parts) {
|
|
1873
|
+
if (typeof part === 'string') {
|
|
1874
|
+
if (part.length > 0) {
|
|
1875
|
+
components.push({ text: part })
|
|
1876
|
+
}
|
|
1877
|
+
} else {
|
|
1878
|
+
this.appendRichTextExpr(components, part)
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (expr.kind === 'bool_lit') {
|
|
1885
|
+
components.push({ text: expr.value ? 'true' : 'false' })
|
|
1886
|
+
return
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
if (expr.kind === 'int_lit') {
|
|
1890
|
+
components.push({ text: expr.value.toString() })
|
|
1891
|
+
return
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
if (expr.kind === 'float_lit') {
|
|
1895
|
+
components.push({ text: expr.value.toString() })
|
|
1896
|
+
return
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const operand = this.lowerExpr(expr)
|
|
1900
|
+
if (operand.kind === 'const') {
|
|
1901
|
+
components.push({ text: operand.value.toString() })
|
|
1902
|
+
return
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
components.push({ score: { name: this.operandToVar(operand), objective: 'rs' } })
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
private exprToString(expr: Expr): string {
|
|
1909
|
+
switch (expr.kind) {
|
|
1910
|
+
case 'int_lit':
|
|
1911
|
+
return expr.value.toString()
|
|
1912
|
+
case 'float_lit':
|
|
1913
|
+
return Math.trunc(expr.value).toString()
|
|
1914
|
+
case 'byte_lit':
|
|
1915
|
+
return `${expr.value}b`
|
|
1916
|
+
case 'short_lit':
|
|
1917
|
+
return `${expr.value}s`
|
|
1918
|
+
case 'long_lit':
|
|
1919
|
+
return `${expr.value}L`
|
|
1920
|
+
case 'double_lit':
|
|
1921
|
+
return `${expr.value}d`
|
|
1922
|
+
case 'bool_lit':
|
|
1923
|
+
return expr.value ? '1' : '0'
|
|
1924
|
+
case 'str_lit':
|
|
1925
|
+
return expr.value
|
|
1926
|
+
case 'mc_name':
|
|
1927
|
+
return expr.value // #health → "health" (no quotes, used as bare MC name)
|
|
1928
|
+
case 'str_interp':
|
|
1929
|
+
return this.buildRichTextJson(expr)
|
|
1930
|
+
case 'blockpos':
|
|
1931
|
+
return emitBlockPos(expr)
|
|
1932
|
+
case 'ident': {
|
|
1933
|
+
const constValue = this.constValues.get(expr.name)
|
|
1934
|
+
if (constValue) {
|
|
1935
|
+
return this.exprToString(constValue)
|
|
1936
|
+
}
|
|
1937
|
+
const stringValue = this.stringValues.get(expr.name)
|
|
1938
|
+
if (stringValue !== undefined) {
|
|
1939
|
+
return stringValue
|
|
1940
|
+
}
|
|
1941
|
+
const mapped = this.varMap.get(expr.name)
|
|
1942
|
+
return mapped ?? `$${expr.name}`
|
|
1943
|
+
}
|
|
1944
|
+
case 'selector':
|
|
1945
|
+
return this.selectorToString(expr.sel)
|
|
1946
|
+
default:
|
|
1947
|
+
// Complex expression - lower and return var name
|
|
1948
|
+
const op = this.lowerExpr(expr)
|
|
1949
|
+
return this.operandToVar(op)
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
private exprToSnbt(expr: Expr): string {
|
|
1954
|
+
switch (expr.kind) {
|
|
1955
|
+
case 'struct_lit': {
|
|
1956
|
+
const entries = expr.fields.map(f => `${f.name}:${this.exprToSnbt(f.value)}`)
|
|
1957
|
+
return `{${entries.join(',')}}`
|
|
1958
|
+
}
|
|
1959
|
+
case 'array_lit': {
|
|
1960
|
+
const items = expr.elements.map(e => this.exprToSnbt(e))
|
|
1961
|
+
return `[${items.join(',')}]`
|
|
1962
|
+
}
|
|
1963
|
+
case 'str_lit':
|
|
1964
|
+
return `"${expr.value}"`
|
|
1965
|
+
case 'int_lit':
|
|
1966
|
+
return String(expr.value)
|
|
1967
|
+
case 'float_lit':
|
|
1968
|
+
return String(expr.value)
|
|
1969
|
+
case 'byte_lit':
|
|
1970
|
+
return `${expr.value}b`
|
|
1971
|
+
case 'short_lit':
|
|
1972
|
+
return `${expr.value}s`
|
|
1973
|
+
case 'long_lit':
|
|
1974
|
+
return `${expr.value}L`
|
|
1975
|
+
case 'double_lit':
|
|
1976
|
+
return `${expr.value}d`
|
|
1977
|
+
case 'bool_lit':
|
|
1978
|
+
return expr.value ? '1b' : '0b'
|
|
1979
|
+
default:
|
|
1980
|
+
return this.exprToString(expr)
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
private exprToTargetString(expr: Expr): string {
|
|
1985
|
+
if (expr.kind === 'selector') {
|
|
1986
|
+
return this.selectorToString(expr.sel)
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
if (expr.kind === 'str_lit' && expr.value.startsWith('@')) {
|
|
1990
|
+
const span = getSpan(expr)
|
|
1991
|
+
this.warnings.push({
|
|
1992
|
+
message: `Quoted selector "${expr.value}" is deprecated; pass ${expr.value} without quotes`,
|
|
1993
|
+
code: 'W_QUOTED_SELECTOR',
|
|
1994
|
+
...(span ? { line: span.line, col: span.col } : {}),
|
|
1995
|
+
})
|
|
1996
|
+
return expr.value
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
return this.exprToString(expr)
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
private exprToLiteral(expr: Expr): string {
|
|
2003
|
+
if (expr.kind === 'int_lit') return expr.value.toString()
|
|
2004
|
+
if (expr.kind === 'float_lit') return Math.trunc(expr.value).toString()
|
|
2005
|
+
return '0'
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
private exprToQuotedString(expr: Expr): string {
|
|
2009
|
+
return JSON.stringify(this.exprToString(expr))
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
private exprToTextComponent(expr: Expr): string {
|
|
2013
|
+
return JSON.stringify({ text: this.exprToString(expr) })
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
private exprToBoolString(expr: Expr): string {
|
|
2017
|
+
if (expr.kind === 'bool_lit') {
|
|
2018
|
+
return expr.value ? 'true' : 'false'
|
|
2019
|
+
}
|
|
2020
|
+
return this.exprToString(expr)
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
private isTeamTextOption(option: string): boolean {
|
|
2024
|
+
return option === 'displayName' || option === 'prefix' || option === 'suffix'
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
private lowerCoordinateBuiltin(name: string, args: Expr[]): string | null {
|
|
2028
|
+
const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
|
|
2029
|
+
const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
|
|
2030
|
+
const pos2 = args[2] ? this.resolveBlockPosExpr(args[2]) : null
|
|
2031
|
+
|
|
2032
|
+
if (name === 'setblock') {
|
|
2033
|
+
if (args.length === 2 && pos0) {
|
|
2034
|
+
return `setblock ${emitBlockPos(pos0)} ${this.exprToString(args[1])}`
|
|
2035
|
+
}
|
|
2036
|
+
return null
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
if (name === 'fill') {
|
|
2040
|
+
if (args.length === 3 && pos0 && pos1) {
|
|
2041
|
+
return `fill ${emitBlockPos(pos0)} ${emitBlockPos(pos1)} ${this.exprToString(args[2])}`
|
|
2042
|
+
}
|
|
2043
|
+
return null
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (name === 'clone') {
|
|
2047
|
+
if (args.length === 3 && pos0 && pos1 && pos2) {
|
|
2048
|
+
return `clone ${emitBlockPos(pos0)} ${emitBlockPos(pos1)} ${emitBlockPos(pos2)}`
|
|
2049
|
+
}
|
|
2050
|
+
return null
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
return null
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
private lowerTpCommand(args: Expr[]): string | null {
|
|
2057
|
+
const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
|
|
2058
|
+
const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
|
|
2059
|
+
|
|
2060
|
+
if (args.length === 1 && pos0) {
|
|
2061
|
+
return `tp ${emitBlockPos(pos0)}`
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (args.length === 2) {
|
|
2065
|
+
if (pos1) {
|
|
2066
|
+
return `tp ${this.exprToString(args[0])} ${emitBlockPos(pos1)}`
|
|
2067
|
+
}
|
|
2068
|
+
return `tp ${this.exprToString(args[0])} ${this.exprToString(args[1])}`
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (args.length === 4) {
|
|
2072
|
+
return `tp ${this.exprToString(args[0])} ${this.exprToString(args[1])} ${this.exprToString(args[2])} ${this.exprToString(args[3])}`
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
return null
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
private resolveBlockPosExpr(expr: Expr): BlockPosExpr | null {
|
|
2079
|
+
if (expr.kind === 'blockpos') {
|
|
2080
|
+
return expr
|
|
2081
|
+
}
|
|
2082
|
+
if (expr.kind === 'ident') {
|
|
2083
|
+
return this.blockPosVars.get(expr.name) ?? null
|
|
2084
|
+
}
|
|
2085
|
+
return null
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
private getArrayStorageName(expr: Expr): string | null {
|
|
2089
|
+
if (expr.kind === 'ident') {
|
|
2090
|
+
return expr.name
|
|
2091
|
+
}
|
|
2092
|
+
return null
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
private inferLambdaReturnType(expr: Extract<Expr, { kind: 'lambda' }>): TypeNode {
|
|
2096
|
+
if (expr.returnType) {
|
|
2097
|
+
return this.normalizeType(expr.returnType)
|
|
2098
|
+
}
|
|
2099
|
+
if (Array.isArray(expr.body)) {
|
|
2100
|
+
return { kind: 'named', name: 'void' }
|
|
2101
|
+
}
|
|
2102
|
+
return this.inferExprType(expr.body) ?? { kind: 'named', name: 'void' }
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
private inferExprType(expr: Expr): TypeNode | undefined {
|
|
2106
|
+
if (expr.kind === 'int_lit') return { kind: 'named', name: 'int' }
|
|
2107
|
+
if (expr.kind === 'float_lit') return { kind: 'named', name: 'float' }
|
|
2108
|
+
if (expr.kind === 'bool_lit') return { kind: 'named', name: 'bool' }
|
|
2109
|
+
if (expr.kind === 'str_lit' || expr.kind === 'str_interp') return { kind: 'named', name: 'string' }
|
|
2110
|
+
if (expr.kind === 'blockpos') return { kind: 'named', name: 'BlockPos' }
|
|
2111
|
+
if (expr.kind === 'ident') {
|
|
2112
|
+
const constValue = this.constValues.get(expr.name)
|
|
2113
|
+
if (constValue) {
|
|
2114
|
+
switch (constValue.kind) {
|
|
2115
|
+
case 'int_lit':
|
|
2116
|
+
return { kind: 'named', name: 'int' }
|
|
2117
|
+
case 'float_lit':
|
|
2118
|
+
return { kind: 'named', name: 'float' }
|
|
2119
|
+
case 'bool_lit':
|
|
2120
|
+
return { kind: 'named', name: 'bool' }
|
|
2121
|
+
case 'str_lit':
|
|
2122
|
+
return { kind: 'named', name: 'string' }
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return this.varTypes.get(expr.name)
|
|
2126
|
+
}
|
|
2127
|
+
if (expr.kind === 'lambda') {
|
|
2128
|
+
return {
|
|
2129
|
+
kind: 'function_type',
|
|
2130
|
+
params: expr.params.map(param => this.normalizeType(param.type ?? { kind: 'named', name: 'int' })),
|
|
2131
|
+
return: this.inferLambdaReturnType(expr),
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
if (expr.kind === 'call') {
|
|
2135
|
+
return this.fnDecls.get(this.resolveFunctionRefByName(expr.fn) ?? expr.fn)?.returnType
|
|
2136
|
+
}
|
|
2137
|
+
if (expr.kind === 'invoke') {
|
|
2138
|
+
const calleeType = this.inferExprType(expr.callee)
|
|
2139
|
+
if (calleeType?.kind === 'function_type') {
|
|
2140
|
+
return calleeType.return
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (expr.kind === 'binary') {
|
|
2144
|
+
if (['==', '!=', '<', '<=', '>', '>=', '&&', '||'].includes(expr.op)) {
|
|
2145
|
+
return { kind: 'named', name: 'bool' }
|
|
2146
|
+
}
|
|
2147
|
+
return this.inferExprType(expr.left)
|
|
2148
|
+
}
|
|
2149
|
+
if (expr.kind === 'unary') {
|
|
2150
|
+
return expr.op === '!' ? { kind: 'named', name: 'bool' } : this.inferExprType(expr.operand)
|
|
2151
|
+
}
|
|
2152
|
+
if (expr.kind === 'array_lit') {
|
|
2153
|
+
return {
|
|
2154
|
+
kind: 'array',
|
|
2155
|
+
elem: expr.elements[0] ? (this.inferExprType(expr.elements[0]) ?? { kind: 'named', name: 'int' }) : { kind: 'named', name: 'int' },
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
if (expr.kind === 'member' && expr.obj.kind === 'ident' && this.enumDefs.has(expr.obj.name)) {
|
|
2159
|
+
return { kind: 'enum', name: expr.obj.name }
|
|
2160
|
+
}
|
|
2161
|
+
return undefined
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
private normalizeType(type: TypeNode): TypeNode {
|
|
2165
|
+
if (type.kind === 'array') {
|
|
2166
|
+
return { kind: 'array', elem: this.normalizeType(type.elem) }
|
|
2167
|
+
}
|
|
2168
|
+
if (type.kind === 'function_type') {
|
|
2169
|
+
return {
|
|
2170
|
+
kind: 'function_type',
|
|
2171
|
+
params: type.params.map(param => this.normalizeType(param)),
|
|
2172
|
+
return: this.normalizeType(type.return),
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if ((type.kind === 'struct' || type.kind === 'enum') && this.enumDefs.has(type.name)) {
|
|
2176
|
+
return { kind: 'enum', name: type.name }
|
|
2177
|
+
}
|
|
2178
|
+
return type
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
private readArrayElement(arrayName: string, index: Operand): Operand {
|
|
2182
|
+
const dst = this.builder.freshTemp()
|
|
2183
|
+
|
|
2184
|
+
if (index.kind === 'const') {
|
|
2185
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run data get storage rs:heap ${arrayName}[${index.value}]`)
|
|
2186
|
+
return { kind: 'var', name: dst }
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
const macroKey = `__rs_index_${this.foreachCounter++}`
|
|
2190
|
+
const subFnName = `${this.currentFn}/array_get_${this.foreachCounter++}`
|
|
2191
|
+
const indexVar = index.kind === 'var' ? index.name : this.operandToVar(index)
|
|
2192
|
+
this.builder.emitRaw(`execute store result storage rs:heap ${macroKey} int 1 run scoreboard players get ${indexVar} rs`)
|
|
2193
|
+
this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`)
|
|
2194
|
+
this.emitRawSubFunction(
|
|
2195
|
+
subFnName,
|
|
2196
|
+
`$execute store result score ${dst} rs run data get storage rs:heap ${arrayName}[$(${macroKey})]`
|
|
2197
|
+
)
|
|
2198
|
+
return { kind: 'var', name: dst }
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
private emitRawSubFunction(name: string, ...commands: string[]): void {
|
|
2202
|
+
const builder = new LoweringBuilder()
|
|
2203
|
+
builder.startBlock('entry')
|
|
2204
|
+
for (const cmd of commands) {
|
|
2205
|
+
builder.emitRaw(cmd)
|
|
2206
|
+
}
|
|
2207
|
+
builder.emitReturn()
|
|
2208
|
+
this.functions.push(builder.build(name, [], false))
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// -------------------------------------------------------------------------
|
|
2212
|
+
// Helpers
|
|
2213
|
+
// -------------------------------------------------------------------------
|
|
2214
|
+
|
|
2215
|
+
private storeStringValue(name: string, expr: Expr): boolean {
|
|
2216
|
+
const value = this.resolveStaticString(expr)
|
|
2217
|
+
if (value === null) {
|
|
2218
|
+
this.stringValues.delete(name)
|
|
2219
|
+
return false
|
|
2220
|
+
}
|
|
2221
|
+
this.stringValues.set(name, value)
|
|
2222
|
+
this.builder.emitRaw(`data modify storage rs:strings ${name} set value ${JSON.stringify(value)}`)
|
|
2223
|
+
return true
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
private resolveStaticString(expr: Expr | undefined): string | null {
|
|
2227
|
+
if (!expr) {
|
|
2228
|
+
return null
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
if (expr.kind === 'str_lit') {
|
|
2232
|
+
return expr.value
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (expr.kind === 'ident') {
|
|
2236
|
+
const constValue = this.constValues.get(expr.name)
|
|
2237
|
+
if (constValue?.kind === 'str_lit') {
|
|
2238
|
+
return constValue.value
|
|
2239
|
+
}
|
|
2240
|
+
return this.stringValues.get(expr.name) ?? null
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
return null
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
private getStringStoragePath(expr: Expr | undefined): string | null {
|
|
2247
|
+
if (!expr || expr.kind !== 'ident') {
|
|
2248
|
+
return null
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
if (this.stringValues.has(expr.name)) {
|
|
2252
|
+
return `rs:strings ${expr.name}`
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
return null
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
private lowerConstLiteral(expr: ConstDecl['value']): Operand {
|
|
2259
|
+
switch (expr.kind) {
|
|
2260
|
+
case 'int_lit':
|
|
2261
|
+
return { kind: 'const', value: expr.value }
|
|
2262
|
+
case 'float_lit':
|
|
2263
|
+
return { kind: 'const', value: Math.round(expr.value * 1000) }
|
|
2264
|
+
case 'bool_lit':
|
|
2265
|
+
return { kind: 'const', value: expr.value ? 1 : 0 }
|
|
2266
|
+
case 'str_lit':
|
|
2267
|
+
return { kind: 'const', value: 0 }
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
private operandToVar(op: Operand): string {
|
|
2272
|
+
if (op.kind === 'var') return op.name
|
|
2273
|
+
// Constant needs to be stored in a temp
|
|
2274
|
+
const dst = this.builder.freshTemp()
|
|
2275
|
+
this.builder.emitAssign(dst, op)
|
|
2276
|
+
return dst
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
private selectorToString(sel: EntitySelector): string {
|
|
2280
|
+
const { kind, filters } = sel
|
|
2281
|
+
if (!filters) return this.finalizeSelector(kind)
|
|
2282
|
+
|
|
2283
|
+
const parts: string[] = []
|
|
2284
|
+
if (filters.type) parts.push(`type=${filters.type}`)
|
|
2285
|
+
if (filters.distance) parts.push(`distance=${this.rangeToString(filters.distance)}`)
|
|
2286
|
+
if (filters.tag) filters.tag.forEach(t => parts.push(`tag=${t}`))
|
|
2287
|
+
if (filters.notTag) filters.notTag.forEach(t => parts.push(`tag=!${t}`))
|
|
2288
|
+
if (filters.limit !== undefined) parts.push(`limit=${filters.limit}`)
|
|
2289
|
+
if (filters.sort) parts.push(`sort=${filters.sort}`)
|
|
2290
|
+
if (filters.scores) {
|
|
2291
|
+
const scoreStr = Object.entries(filters.scores)
|
|
2292
|
+
.map(([k, v]) => `${k}=${this.rangeToString(v)}`).join(',')
|
|
2293
|
+
parts.push(`scores={${scoreStr}}`)
|
|
2294
|
+
}
|
|
2295
|
+
if (filters.nbt) parts.push(`nbt=${filters.nbt}`)
|
|
2296
|
+
if (filters.gamemode) parts.push(`gamemode=${filters.gamemode}`)
|
|
2297
|
+
|
|
2298
|
+
return this.finalizeSelector(parts.length ? `${kind}[${parts.join(',')}]` : kind)
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
private finalizeSelector(selector: string): string {
|
|
2302
|
+
return normalizeSelector(selector, this.warnings)
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
private rangeToString(r: RangeExpr): string {
|
|
2306
|
+
if (r.min !== undefined && r.max !== undefined) {
|
|
2307
|
+
if (r.min === r.max) return `${r.min}`
|
|
2308
|
+
return `${r.min}..${r.max}`
|
|
2309
|
+
}
|
|
2310
|
+
if (r.min !== undefined) return `${r.min}..`
|
|
2311
|
+
if (r.max !== undefined) return `..${r.max}`
|
|
2312
|
+
return '..'
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// ---------------------------------------------------------------------------
|
|
2317
|
+
// LoweringBuilder - Wrapper around IR construction
|
|
2318
|
+
// ---------------------------------------------------------------------------
|
|
2319
|
+
|
|
2320
|
+
class LoweringBuilder {
|
|
2321
|
+
private tempCount = 0
|
|
2322
|
+
private labelCount = 0
|
|
2323
|
+
private blocks: any[] = []
|
|
2324
|
+
private currentBlock: any = null
|
|
2325
|
+
private locals = new Set<string>()
|
|
2326
|
+
|
|
2327
|
+
freshTemp(): string {
|
|
2328
|
+
const name = `$t${this.tempCount++}`
|
|
2329
|
+
this.locals.add(name)
|
|
2330
|
+
return name
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
freshLabel(hint = 'L'): string {
|
|
2334
|
+
return `${hint}_${this.labelCount++}`
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
startBlock(label: string): void {
|
|
2338
|
+
this.currentBlock = { label, instrs: [], term: null }
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
isBlockSealed(): boolean {
|
|
2342
|
+
return this.currentBlock === null || this.currentBlock.term !== null
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
private sealBlock(term: any): void {
|
|
2346
|
+
if (this.currentBlock) {
|
|
2347
|
+
this.currentBlock.term = term
|
|
2348
|
+
this.blocks.push(this.currentBlock)
|
|
2349
|
+
this.currentBlock = null
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
emitAssign(dst: string, src: Operand): void {
|
|
2354
|
+
if (!dst.startsWith('$') && !dst.startsWith('@')) {
|
|
2355
|
+
dst = '$' + dst
|
|
2356
|
+
}
|
|
2357
|
+
this.locals.add(dst)
|
|
2358
|
+
this.currentBlock?.instrs.push({ op: 'assign', dst, src })
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
emitBinop(dst: string, lhs: Operand, bop: BinOp, rhs: Operand): void {
|
|
2362
|
+
this.locals.add(dst)
|
|
2363
|
+
this.currentBlock?.instrs.push({ op: 'binop', dst, lhs, bop, rhs })
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
emitCmp(dst: string, lhs: Operand, cop: CmpOp, rhs: Operand): void {
|
|
2367
|
+
this.locals.add(dst)
|
|
2368
|
+
this.currentBlock?.instrs.push({ op: 'cmp', dst, lhs, cop, rhs })
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
emitCall(fn: string, args: Operand[], dst?: string): void {
|
|
2372
|
+
if (dst) this.locals.add(dst)
|
|
2373
|
+
this.currentBlock?.instrs.push({ op: 'call', fn, args, dst })
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
emitRaw(cmd: string): void {
|
|
2377
|
+
this.currentBlock?.instrs.push({ op: 'raw', cmd })
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
emitJump(target: string): void {
|
|
2381
|
+
this.sealBlock({ op: 'jump', target })
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
emitJumpIf(cond: string, then: string, else_: string): void {
|
|
2385
|
+
this.sealBlock({ op: 'jump_if', cond, then, else_ })
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
emitReturn(value?: Operand): void {
|
|
2389
|
+
this.sealBlock({ op: 'return', value })
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
build(name: string, params: string[], isTickLoop = false): IRFunction {
|
|
2393
|
+
// Ensure current block is sealed
|
|
2394
|
+
if (this.currentBlock && !this.currentBlock.term) {
|
|
2395
|
+
this.sealBlock({ op: 'return' })
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
return {
|
|
2399
|
+
name,
|
|
2400
|
+
params,
|
|
2401
|
+
locals: Array.from(this.locals),
|
|
2402
|
+
blocks: this.blocks,
|
|
2403
|
+
isTickLoop,
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|