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,1437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedScript Parser
|
|
3
|
+
*
|
|
4
|
+
* Recursive descent parser that converts tokens into an AST.
|
|
5
|
+
* Uses precedence climbing for expression parsing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Lexer, type Token, type TokenKind } from '../lexer'
|
|
9
|
+
import type {
|
|
10
|
+
Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, LiteralExpr, Param,
|
|
11
|
+
Program, RangeExpr, SelectorFilter, SelectorKind, Span, Stmt, TypeNode, AssignOp,
|
|
12
|
+
StructDecl, StructField, ExecuteSubcommand, EnumDecl, EnumVariant, BlockPosExpr,
|
|
13
|
+
CoordComponent, LambdaParam
|
|
14
|
+
} from '../ast/types'
|
|
15
|
+
import type { BinOp, CmpOp } from '../ir/types'
|
|
16
|
+
import { DiagnosticError } from '../diagnostics'
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Operator Precedence (higher = binds tighter)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const PRECEDENCE: Record<string, number> = {
|
|
23
|
+
'||': 1,
|
|
24
|
+
'&&': 2,
|
|
25
|
+
'==': 3, '!=': 3,
|
|
26
|
+
'<': 4, '<=': 4, '>': 4, '>=': 4,
|
|
27
|
+
'+': 5, '-': 5,
|
|
28
|
+
'*': 6, '/': 6, '%': 6,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const BINARY_OPS = new Set(['||', '&&', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '%'])
|
|
32
|
+
|
|
33
|
+
function computeIsSingle(raw: string): boolean {
|
|
34
|
+
if (/^@[spr](\[|$)/.test(raw)) return true
|
|
35
|
+
if (/[\[,\s]limit=1[,\]\s]/.test(raw)) return true
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Parser Class
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
export class Parser {
|
|
44
|
+
private tokens: Token[]
|
|
45
|
+
private pos: number = 0
|
|
46
|
+
private sourceLines: string[]
|
|
47
|
+
private filePath?: string
|
|
48
|
+
|
|
49
|
+
constructor(tokens: Token[], source?: string, filePath?: string) {
|
|
50
|
+
this.tokens = tokens
|
|
51
|
+
this.sourceLines = source?.split('\n') ?? []
|
|
52
|
+
this.filePath = filePath
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// -------------------------------------------------------------------------
|
|
56
|
+
// Utilities
|
|
57
|
+
// -------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
private peek(offset = 0): Token {
|
|
60
|
+
const idx = this.pos + offset
|
|
61
|
+
if (idx >= this.tokens.length) {
|
|
62
|
+
return this.tokens[this.tokens.length - 1] // eof
|
|
63
|
+
}
|
|
64
|
+
return this.tokens[idx]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private advance(): Token {
|
|
68
|
+
const token = this.tokens[this.pos]
|
|
69
|
+
if (token.kind !== 'eof') this.pos++
|
|
70
|
+
return token
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private check(kind: TokenKind): boolean {
|
|
74
|
+
return this.peek().kind === kind
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private match(...kinds: TokenKind[]): boolean {
|
|
78
|
+
for (const kind of kinds) {
|
|
79
|
+
if (this.check(kind)) {
|
|
80
|
+
this.advance()
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private expect(kind: TokenKind): Token {
|
|
88
|
+
const token = this.peek()
|
|
89
|
+
if (token.kind !== kind) {
|
|
90
|
+
throw new DiagnosticError(
|
|
91
|
+
'ParseError',
|
|
92
|
+
`Expected '${kind}' but got '${token.kind}'`,
|
|
93
|
+
{ file: this.filePath, line: token.line, col: token.col },
|
|
94
|
+
this.sourceLines
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
return this.advance()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private error(message: string): never {
|
|
101
|
+
const token = this.peek()
|
|
102
|
+
throw new DiagnosticError(
|
|
103
|
+
'ParseError',
|
|
104
|
+
message,
|
|
105
|
+
{ file: this.filePath, line: token.line, col: token.col },
|
|
106
|
+
this.sourceLines
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private withLoc<T extends object>(node: T, token: Token): T {
|
|
111
|
+
const span: Span = { line: token.line, col: token.col }
|
|
112
|
+
Object.defineProperty(node, 'span', {
|
|
113
|
+
value: span,
|
|
114
|
+
enumerable: false,
|
|
115
|
+
configurable: true,
|
|
116
|
+
writable: true,
|
|
117
|
+
})
|
|
118
|
+
return node
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private getLocToken(node: object): Token | null {
|
|
122
|
+
const span = (node as { span?: Span }).span
|
|
123
|
+
if (!span) {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
return { kind: 'eof', value: '', line: span.line, col: span.col }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// -------------------------------------------------------------------------
|
|
130
|
+
// Program
|
|
131
|
+
// -------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
parse(defaultNamespace = 'redscript'): Program {
|
|
134
|
+
let namespace = defaultNamespace
|
|
135
|
+
const declarations: FnDecl[] = []
|
|
136
|
+
const structs: StructDecl[] = []
|
|
137
|
+
const enums: EnumDecl[] = []
|
|
138
|
+
const consts: ConstDecl[] = []
|
|
139
|
+
|
|
140
|
+
// Check for namespace declaration
|
|
141
|
+
if (this.check('namespace')) {
|
|
142
|
+
this.advance()
|
|
143
|
+
const name = this.expect('ident')
|
|
144
|
+
namespace = name.value
|
|
145
|
+
this.expect(';')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse struct and function declarations
|
|
149
|
+
while (!this.check('eof')) {
|
|
150
|
+
if (this.check('struct')) {
|
|
151
|
+
structs.push(this.parseStructDecl())
|
|
152
|
+
} else if (this.check('enum')) {
|
|
153
|
+
enums.push(this.parseEnumDecl())
|
|
154
|
+
} else if (this.check('const')) {
|
|
155
|
+
consts.push(this.parseConstDecl())
|
|
156
|
+
} else {
|
|
157
|
+
declarations.push(this.parseFnDecl())
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { namespace, declarations, structs, enums, consts }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// -------------------------------------------------------------------------
|
|
165
|
+
// Struct Declaration
|
|
166
|
+
// -------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
private parseStructDecl(): StructDecl {
|
|
169
|
+
const structToken = this.expect('struct')
|
|
170
|
+
const name = this.expect('ident').value
|
|
171
|
+
this.expect('{')
|
|
172
|
+
|
|
173
|
+
const fields: StructField[] = []
|
|
174
|
+
while (!this.check('}') && !this.check('eof')) {
|
|
175
|
+
const fieldName = this.expect('ident').value
|
|
176
|
+
this.expect(':')
|
|
177
|
+
const fieldType = this.parseType()
|
|
178
|
+
fields.push({ name: fieldName, type: fieldType })
|
|
179
|
+
|
|
180
|
+
// Allow optional comma or semicolon between fields
|
|
181
|
+
this.match(',')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.expect('}')
|
|
185
|
+
return this.withLoc({ name, fields }, structToken)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private parseEnumDecl(): EnumDecl {
|
|
189
|
+
const enumToken = this.expect('enum')
|
|
190
|
+
const name = this.expect('ident').value
|
|
191
|
+
this.expect('{')
|
|
192
|
+
|
|
193
|
+
const variants: EnumVariant[] = []
|
|
194
|
+
let nextValue = 0
|
|
195
|
+
|
|
196
|
+
while (!this.check('}') && !this.check('eof')) {
|
|
197
|
+
const variantToken = this.expect('ident')
|
|
198
|
+
const variant: EnumVariant = { name: variantToken.value }
|
|
199
|
+
|
|
200
|
+
if (this.match('=')) {
|
|
201
|
+
const valueToken = this.expect('int_lit')
|
|
202
|
+
variant.value = parseInt(valueToken.value, 10)
|
|
203
|
+
nextValue = variant.value + 1
|
|
204
|
+
} else {
|
|
205
|
+
variant.value = nextValue++
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
variants.push(variant)
|
|
209
|
+
|
|
210
|
+
if (!this.match(',')) {
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.expect('}')
|
|
216
|
+
return this.withLoc({ name, variants }, enumToken)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private parseConstDecl(): ConstDecl {
|
|
220
|
+
const constToken = this.expect('const')
|
|
221
|
+
const name = this.expect('ident').value
|
|
222
|
+
this.expect(':')
|
|
223
|
+
const type = this.parseType()
|
|
224
|
+
this.expect('=')
|
|
225
|
+
const value = this.parseLiteralExpr()
|
|
226
|
+
this.match(';')
|
|
227
|
+
return this.withLoc({ name, type, value }, constToken)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// -------------------------------------------------------------------------
|
|
231
|
+
// Function Declaration
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
private parseFnDecl(): FnDecl {
|
|
235
|
+
const decorators = this.parseDecorators()
|
|
236
|
+
|
|
237
|
+
const fnToken = this.expect('fn')
|
|
238
|
+
const name = this.expect('ident').value
|
|
239
|
+
this.expect('(')
|
|
240
|
+
const params = this.parseParams()
|
|
241
|
+
this.expect(')')
|
|
242
|
+
|
|
243
|
+
let returnType: TypeNode = { kind: 'named', name: 'void' }
|
|
244
|
+
if (this.match('->')) {
|
|
245
|
+
returnType = this.parseType()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const body = this.parseBlock()
|
|
249
|
+
|
|
250
|
+
return this.withLoc({ name, params, returnType, decorators, body }, fnToken)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private parseDecorators(): Decorator[] {
|
|
254
|
+
const decorators: Decorator[] = []
|
|
255
|
+
|
|
256
|
+
while (this.check('decorator')) {
|
|
257
|
+
const token = this.advance()
|
|
258
|
+
const decorator = this.parseDecoratorValue(token.value)
|
|
259
|
+
decorators.push(decorator)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return decorators
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private parseDecoratorValue(value: string): Decorator {
|
|
266
|
+
// Parse @tick or @on_trigger("name") or @on_advancement("story/mine_diamond")
|
|
267
|
+
const match = value.match(/^@(\w+)(?:\(([^)]*)\))?$/)
|
|
268
|
+
if (!match) {
|
|
269
|
+
this.error(`Invalid decorator: ${value}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const name = match[1] as Decorator['name']
|
|
273
|
+
const argsStr = match[2]
|
|
274
|
+
|
|
275
|
+
if (!argsStr) {
|
|
276
|
+
return { name }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const args: Decorator['args'] = {}
|
|
280
|
+
|
|
281
|
+
// Handle @on_trigger("name"), @on_advancement("id"), @on_craft("item"), @on_join_team("team")
|
|
282
|
+
if (name === 'on_trigger' || name === 'on_advancement' || name === 'on_craft' || name === 'on_join_team') {
|
|
283
|
+
const strMatch = argsStr.match(/^"([^"]*)"$/)
|
|
284
|
+
if (strMatch) {
|
|
285
|
+
if (name === 'on_trigger') {
|
|
286
|
+
args.trigger = strMatch[1]
|
|
287
|
+
} else if (name === 'on_advancement') {
|
|
288
|
+
args.advancement = strMatch[1]
|
|
289
|
+
} else if (name === 'on_craft') {
|
|
290
|
+
args.item = strMatch[1]
|
|
291
|
+
} else if (name === 'on_join_team') {
|
|
292
|
+
args.team = strMatch[1]
|
|
293
|
+
}
|
|
294
|
+
return { name, args }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Handle key=value format (e.g., rate=20)
|
|
299
|
+
for (const part of argsStr.split(',')) {
|
|
300
|
+
const [key, val] = part.split('=').map(s => s.trim())
|
|
301
|
+
if (key === 'rate') {
|
|
302
|
+
args.rate = parseInt(val, 10)
|
|
303
|
+
} else if (key === 'trigger') {
|
|
304
|
+
args.trigger = val
|
|
305
|
+
} else if (key === 'advancement') {
|
|
306
|
+
args.advancement = val
|
|
307
|
+
} else if (key === 'item') {
|
|
308
|
+
args.item = val
|
|
309
|
+
} else if (key === 'team') {
|
|
310
|
+
args.team = val
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { name, args }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private parseParams(): Param[] {
|
|
318
|
+
const params: Param[] = []
|
|
319
|
+
|
|
320
|
+
if (!this.check(')')) {
|
|
321
|
+
do {
|
|
322
|
+
const paramToken = this.expect('ident')
|
|
323
|
+
const name = paramToken.value
|
|
324
|
+
this.expect(':')
|
|
325
|
+
const type = this.parseType()
|
|
326
|
+
let defaultValue: Expr | undefined
|
|
327
|
+
if (this.match('=')) {
|
|
328
|
+
defaultValue = this.parseExpr()
|
|
329
|
+
}
|
|
330
|
+
params.push(this.withLoc({ name, type, default: defaultValue }, paramToken))
|
|
331
|
+
} while (this.match(','))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return params
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private parseType(): TypeNode {
|
|
338
|
+
const token = this.peek()
|
|
339
|
+
let type: TypeNode
|
|
340
|
+
|
|
341
|
+
if (token.kind === '(') {
|
|
342
|
+
return this.parseFunctionType()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (token.kind === 'int' || token.kind === 'bool' ||
|
|
346
|
+
token.kind === 'float' || token.kind === 'string' || token.kind === 'void' ||
|
|
347
|
+
token.kind === 'BlockPos') {
|
|
348
|
+
this.advance()
|
|
349
|
+
type = { kind: 'named', name: token.kind }
|
|
350
|
+
} else if (token.kind === 'ident') {
|
|
351
|
+
this.advance()
|
|
352
|
+
type = { kind: 'struct', name: token.value }
|
|
353
|
+
} else {
|
|
354
|
+
this.error(`Expected type, got '${token.kind}'`)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
while (this.match('[')) {
|
|
358
|
+
this.expect(']')
|
|
359
|
+
type = { kind: 'array', elem: type }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return type
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private parseFunctionType(): TypeNode {
|
|
366
|
+
this.expect('(')
|
|
367
|
+
const params: TypeNode[] = []
|
|
368
|
+
|
|
369
|
+
if (!this.check(')')) {
|
|
370
|
+
do {
|
|
371
|
+
params.push(this.parseType())
|
|
372
|
+
} while (this.match(','))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.expect(')')
|
|
376
|
+
this.expect('->')
|
|
377
|
+
const returnType = this.parseType()
|
|
378
|
+
return { kind: 'function_type', params, return: returnType }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// -------------------------------------------------------------------------
|
|
382
|
+
// Block & Statements
|
|
383
|
+
// -------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
private parseBlock(): Block {
|
|
386
|
+
this.expect('{')
|
|
387
|
+
const stmts: Stmt[] = []
|
|
388
|
+
|
|
389
|
+
while (!this.check('}') && !this.check('eof')) {
|
|
390
|
+
stmts.push(this.parseStmt())
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.expect('}')
|
|
394
|
+
return stmts
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private parseStmt(): Stmt {
|
|
398
|
+
// Let statement
|
|
399
|
+
if (this.check('let')) {
|
|
400
|
+
return this.parseLetStmt()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Return statement
|
|
404
|
+
if (this.check('return')) {
|
|
405
|
+
return this.parseReturnStmt()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// If statement
|
|
409
|
+
if (this.check('if')) {
|
|
410
|
+
return this.parseIfStmt()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// While statement
|
|
414
|
+
if (this.check('while')) {
|
|
415
|
+
return this.parseWhileStmt()
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// For statement
|
|
419
|
+
if (this.check('for')) {
|
|
420
|
+
return this.parseForStmt()
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Foreach statement
|
|
424
|
+
if (this.check('foreach')) {
|
|
425
|
+
return this.parseForeachStmt()
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (this.check('match')) {
|
|
429
|
+
return this.parseMatchStmt()
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// As block
|
|
433
|
+
if (this.check('as')) {
|
|
434
|
+
return this.parseAsStmt()
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// At block
|
|
438
|
+
if (this.check('at')) {
|
|
439
|
+
return this.parseAtStmt()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Execute statement: execute as/at/if/unless/in ... run { }
|
|
443
|
+
if (this.check('execute')) {
|
|
444
|
+
return this.parseExecuteStmt()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Raw command
|
|
448
|
+
if (this.check('raw_cmd')) {
|
|
449
|
+
const token = this.advance()
|
|
450
|
+
const cmd = token.value
|
|
451
|
+
this.match(';') // optional semicolon (raw consumes it)
|
|
452
|
+
return this.withLoc({ kind: 'raw', cmd }, token)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Expression statement
|
|
456
|
+
return this.parseExprStmt()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private parseLetStmt(): Stmt {
|
|
460
|
+
const letToken = this.expect('let')
|
|
461
|
+
const name = this.expect('ident').value
|
|
462
|
+
|
|
463
|
+
let type: TypeNode | undefined
|
|
464
|
+
if (this.match(':')) {
|
|
465
|
+
type = this.parseType()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this.expect('=')
|
|
469
|
+
const init = this.parseExpr()
|
|
470
|
+
this.expect(';')
|
|
471
|
+
|
|
472
|
+
return this.withLoc({ kind: 'let', name, type, init }, letToken)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private parseReturnStmt(): Stmt {
|
|
476
|
+
const returnToken = this.expect('return')
|
|
477
|
+
|
|
478
|
+
let value: Expr | undefined
|
|
479
|
+
if (!this.check(';')) {
|
|
480
|
+
value = this.parseExpr()
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this.expect(';')
|
|
484
|
+
return this.withLoc({ kind: 'return', value }, returnToken)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private parseIfStmt(): Stmt {
|
|
488
|
+
const ifToken = this.expect('if')
|
|
489
|
+
this.expect('(')
|
|
490
|
+
const cond = this.parseExpr()
|
|
491
|
+
this.expect(')')
|
|
492
|
+
const then = this.parseBlock()
|
|
493
|
+
|
|
494
|
+
let else_: Block | undefined
|
|
495
|
+
if (this.match('else')) {
|
|
496
|
+
if (this.check('if')) {
|
|
497
|
+
// else if
|
|
498
|
+
else_ = [this.parseIfStmt()]
|
|
499
|
+
} else {
|
|
500
|
+
else_ = this.parseBlock()
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return this.withLoc({ kind: 'if', cond, then, else_ }, ifToken)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private parseWhileStmt(): Stmt {
|
|
508
|
+
const whileToken = this.expect('while')
|
|
509
|
+
this.expect('(')
|
|
510
|
+
const cond = this.parseExpr()
|
|
511
|
+
this.expect(')')
|
|
512
|
+
const body = this.parseBlock()
|
|
513
|
+
|
|
514
|
+
return this.withLoc({ kind: 'while', cond, body }, whileToken)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private parseForStmt(): Stmt {
|
|
518
|
+
const forToken = this.expect('for')
|
|
519
|
+
|
|
520
|
+
// Check for for-range syntax: for <ident> in <range_lit> { ... }
|
|
521
|
+
if (this.check('ident') && this.peek(1).kind === 'in') {
|
|
522
|
+
return this.parseForRangeStmt(forToken)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
this.expect('(')
|
|
526
|
+
|
|
527
|
+
// Init: either let statement (without semicolon) or empty
|
|
528
|
+
let init: Stmt | undefined
|
|
529
|
+
if (this.check('let')) {
|
|
530
|
+
// Parse let without consuming semicolon here (we handle it)
|
|
531
|
+
const letToken = this.expect('let')
|
|
532
|
+
const name = this.expect('ident').value
|
|
533
|
+
let type: TypeNode | undefined
|
|
534
|
+
if (this.match(':')) {
|
|
535
|
+
type = this.parseType()
|
|
536
|
+
}
|
|
537
|
+
this.expect('=')
|
|
538
|
+
const initExpr = this.parseExpr()
|
|
539
|
+
const initStmt: Stmt = { kind: 'let', name, type, init: initExpr }
|
|
540
|
+
init = this.withLoc(initStmt, letToken)
|
|
541
|
+
}
|
|
542
|
+
this.expect(';')
|
|
543
|
+
|
|
544
|
+
// Condition
|
|
545
|
+
const cond = this.parseExpr()
|
|
546
|
+
this.expect(';')
|
|
547
|
+
|
|
548
|
+
// Step expression
|
|
549
|
+
const step = this.parseExpr()
|
|
550
|
+
this.expect(')')
|
|
551
|
+
|
|
552
|
+
const body = this.parseBlock()
|
|
553
|
+
|
|
554
|
+
return this.withLoc({ kind: 'for', init, cond, step, body }, forToken)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private parseForRangeStmt(forToken: Token): Stmt {
|
|
558
|
+
const varName = this.expect('ident').value
|
|
559
|
+
this.expect('in')
|
|
560
|
+
const rangeToken = this.expect('range_lit')
|
|
561
|
+
const range = this.parseRangeValue(rangeToken.value)
|
|
562
|
+
|
|
563
|
+
const start: Expr = this.withLoc(
|
|
564
|
+
{ kind: 'int_lit', value: range.min ?? 0 },
|
|
565
|
+
rangeToken
|
|
566
|
+
)
|
|
567
|
+
const end: Expr = this.withLoc(
|
|
568
|
+
{ kind: 'int_lit', value: range.max ?? 0 },
|
|
569
|
+
rangeToken
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
const body = this.parseBlock()
|
|
573
|
+
return this.withLoc({ kind: 'for_range', varName, start, end, body }, forToken)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private parseForeachStmt(): Stmt {
|
|
577
|
+
const foreachToken = this.expect('foreach')
|
|
578
|
+
this.expect('(')
|
|
579
|
+
const binding = this.expect('ident').value
|
|
580
|
+
this.expect('in')
|
|
581
|
+
const iterable = this.parseExpr()
|
|
582
|
+
this.expect(')')
|
|
583
|
+
const body = this.parseBlock()
|
|
584
|
+
|
|
585
|
+
return this.withLoc({ kind: 'foreach', binding, iterable, body }, foreachToken)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private parseMatchStmt(): Stmt {
|
|
589
|
+
const matchToken = this.expect('match')
|
|
590
|
+
this.expect('(')
|
|
591
|
+
const expr = this.parseExpr()
|
|
592
|
+
this.expect(')')
|
|
593
|
+
this.expect('{')
|
|
594
|
+
|
|
595
|
+
const arms: Array<{ pattern: Expr | null; body: Block }> = []
|
|
596
|
+
while (!this.check('}') && !this.check('eof')) {
|
|
597
|
+
let pattern: Expr | null
|
|
598
|
+
if (this.check('ident') && this.peek().value === '_') {
|
|
599
|
+
this.advance()
|
|
600
|
+
pattern = null
|
|
601
|
+
} else {
|
|
602
|
+
pattern = this.parseExpr()
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
this.expect('=>')
|
|
606
|
+
const body = this.parseBlock()
|
|
607
|
+
arms.push({ pattern, body })
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
this.expect('}')
|
|
611
|
+
return this.withLoc({ kind: 'match', expr, arms }, matchToken)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private parseAsStmt(): Stmt {
|
|
615
|
+
const asToken = this.expect('as')
|
|
616
|
+
const as_sel = this.parseSelector()
|
|
617
|
+
|
|
618
|
+
// Check for combined as/at
|
|
619
|
+
if (this.match('at')) {
|
|
620
|
+
const at_sel = this.parseSelector()
|
|
621
|
+
const body = this.parseBlock()
|
|
622
|
+
return this.withLoc({ kind: 'as_at', as_sel, at_sel, body }, asToken)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const body = this.parseBlock()
|
|
626
|
+
return this.withLoc({ kind: 'as_block', selector: as_sel, body }, asToken)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private parseAtStmt(): Stmt {
|
|
630
|
+
const atToken = this.expect('at')
|
|
631
|
+
const selector = this.parseSelector()
|
|
632
|
+
const body = this.parseBlock()
|
|
633
|
+
return this.withLoc({ kind: 'at_block', selector, body }, atToken)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private parseExecuteStmt(): Stmt {
|
|
637
|
+
const executeToken = this.expect('execute')
|
|
638
|
+
const subcommands: ExecuteSubcommand[] = []
|
|
639
|
+
|
|
640
|
+
// Parse subcommands until we hit 'run'
|
|
641
|
+
while (!this.check('run') && !this.check('eof')) {
|
|
642
|
+
if (this.match('as')) {
|
|
643
|
+
const selector = this.parseSelector()
|
|
644
|
+
subcommands.push({ kind: 'as', selector })
|
|
645
|
+
} else if (this.match('at')) {
|
|
646
|
+
const selector = this.parseSelector()
|
|
647
|
+
subcommands.push({ kind: 'at', selector })
|
|
648
|
+
} else if (this.match('if')) {
|
|
649
|
+
// Expect 'entity' keyword (as ident) or just parse selector directly
|
|
650
|
+
if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
|
|
651
|
+
this.advance() // consume 'entity'
|
|
652
|
+
}
|
|
653
|
+
const selector = this.parseSelector()
|
|
654
|
+
subcommands.push({ kind: 'if_entity', selector })
|
|
655
|
+
} else if (this.match('unless')) {
|
|
656
|
+
// Expect 'entity' keyword (as ident) or just parse selector directly
|
|
657
|
+
if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
|
|
658
|
+
this.advance() // consume 'entity'
|
|
659
|
+
}
|
|
660
|
+
const selector = this.parseSelector()
|
|
661
|
+
subcommands.push({ kind: 'unless_entity', selector })
|
|
662
|
+
} else if (this.match('in')) {
|
|
663
|
+
const dim = this.expect('ident').value
|
|
664
|
+
subcommands.push({ kind: 'in', dimension: dim })
|
|
665
|
+
} else {
|
|
666
|
+
this.error(`Unexpected token in execute statement: ${this.peek().kind}`)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
this.expect('run')
|
|
671
|
+
const body = this.parseBlock()
|
|
672
|
+
|
|
673
|
+
return this.withLoc({ kind: 'execute', subcommands, body }, executeToken)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private parseExprStmt(): Stmt {
|
|
677
|
+
const expr = this.parseExpr()
|
|
678
|
+
this.expect(';')
|
|
679
|
+
const exprToken = this.getLocToken(expr) ?? this.peek()
|
|
680
|
+
return this.withLoc({ kind: 'expr', expr }, exprToken)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// -------------------------------------------------------------------------
|
|
684
|
+
// Expressions (Precedence Climbing)
|
|
685
|
+
// -------------------------------------------------------------------------
|
|
686
|
+
|
|
687
|
+
private parseExpr(): Expr {
|
|
688
|
+
return this.parseAssignment()
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private parseAssignment(): Expr {
|
|
692
|
+
const left = this.parseBinaryExpr(1)
|
|
693
|
+
|
|
694
|
+
// Check for assignment
|
|
695
|
+
const token = this.peek()
|
|
696
|
+
if (token.kind === '=' || token.kind === '+=' || token.kind === '-=' ||
|
|
697
|
+
token.kind === '*=' || token.kind === '/=' || token.kind === '%=') {
|
|
698
|
+
const op = this.advance().kind as AssignOp
|
|
699
|
+
|
|
700
|
+
if (left.kind === 'ident') {
|
|
701
|
+
const value = this.parseAssignment()
|
|
702
|
+
return this.withLoc({ kind: 'assign', target: left.name, op, value }, this.getLocToken(left) ?? token)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Member assignment: p.x = 10, p.x += 5
|
|
706
|
+
if (left.kind === 'member') {
|
|
707
|
+
const value = this.parseAssignment()
|
|
708
|
+
return this.withLoc(
|
|
709
|
+
{ kind: 'member_assign', obj: left.obj, field: left.field, op, value },
|
|
710
|
+
this.getLocToken(left) ?? token
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return left
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private parseBinaryExpr(minPrec: number): Expr {
|
|
719
|
+
let left = this.parseUnaryExpr()
|
|
720
|
+
|
|
721
|
+
while (true) {
|
|
722
|
+
const op = this.peek().kind
|
|
723
|
+
if (!BINARY_OPS.has(op)) break
|
|
724
|
+
|
|
725
|
+
const prec = PRECEDENCE[op]
|
|
726
|
+
if (prec < minPrec) break
|
|
727
|
+
|
|
728
|
+
const opToken = this.advance()
|
|
729
|
+
const right = this.parseBinaryExpr(prec + 1) // left associative
|
|
730
|
+
left = this.withLoc(
|
|
731
|
+
{ kind: 'binary', op: op as BinOp | CmpOp | '&&' | '||', left, right },
|
|
732
|
+
this.getLocToken(left) ?? opToken
|
|
733
|
+
)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return left
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private parseUnaryExpr(): Expr {
|
|
740
|
+
if (this.match('!')) {
|
|
741
|
+
const bangToken = this.tokens[this.pos - 1]
|
|
742
|
+
const operand = this.parseUnaryExpr()
|
|
743
|
+
return this.withLoc({ kind: 'unary', op: '!', operand }, bangToken)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (this.check('-') && !this.isSubtraction()) {
|
|
747
|
+
const minusToken = this.advance()
|
|
748
|
+
const operand = this.parseUnaryExpr()
|
|
749
|
+
return this.withLoc({ kind: 'unary', op: '-', operand }, minusToken)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return this.parsePostfixExpr()
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private isSubtraction(): boolean {
|
|
756
|
+
// Check if this minus is binary (subtraction) by looking at previous token
|
|
757
|
+
// If previous was a value (literal, ident, ), ]) it's subtraction
|
|
758
|
+
if (this.pos === 0) return false
|
|
759
|
+
const prev = this.tokens[this.pos - 1]
|
|
760
|
+
return ['int_lit', 'float_lit', 'ident', ')', ']'].includes(prev.kind)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private parsePostfixExpr(): Expr {
|
|
764
|
+
let expr = this.parsePrimaryExpr()
|
|
765
|
+
|
|
766
|
+
while (true) {
|
|
767
|
+
// Function call
|
|
768
|
+
if (this.match('(')) {
|
|
769
|
+
const openParenToken = this.tokens[this.pos - 1]
|
|
770
|
+
if (expr.kind === 'ident') {
|
|
771
|
+
const args = this.parseArgs()
|
|
772
|
+
this.expect(')')
|
|
773
|
+
expr = this.withLoc({ kind: 'call', fn: expr.name, args }, this.getLocToken(expr) ?? openParenToken)
|
|
774
|
+
continue
|
|
775
|
+
}
|
|
776
|
+
// Member call: entity.tag("name") → __entity_tag(entity, "name")
|
|
777
|
+
// Also handle arr.push(val) and arr.length
|
|
778
|
+
if (expr.kind === 'member') {
|
|
779
|
+
const methodMap: Record<string, string> = {
|
|
780
|
+
'tag': '__entity_tag',
|
|
781
|
+
'untag': '__entity_untag',
|
|
782
|
+
'has_tag': '__entity_has_tag',
|
|
783
|
+
'push': '__array_push',
|
|
784
|
+
'pop': '__array_pop',
|
|
785
|
+
}
|
|
786
|
+
const internalFn = methodMap[expr.field]
|
|
787
|
+
if (internalFn) {
|
|
788
|
+
const args = this.parseArgs()
|
|
789
|
+
this.expect(')')
|
|
790
|
+
expr = this.withLoc(
|
|
791
|
+
{ kind: 'call', fn: internalFn, args: [expr.obj, ...args] },
|
|
792
|
+
this.getLocToken(expr) ?? openParenToken
|
|
793
|
+
)
|
|
794
|
+
continue
|
|
795
|
+
}
|
|
796
|
+
this.error(`Unknown method '${expr.field}'`)
|
|
797
|
+
}
|
|
798
|
+
const args = this.parseArgs()
|
|
799
|
+
this.expect(')')
|
|
800
|
+
expr = this.withLoc(
|
|
801
|
+
{ kind: 'invoke', callee: expr, args },
|
|
802
|
+
this.getLocToken(expr) ?? openParenToken
|
|
803
|
+
)
|
|
804
|
+
continue
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Array index access: arr[0]
|
|
808
|
+
if (this.match('[')) {
|
|
809
|
+
const index = this.parseExpr()
|
|
810
|
+
this.expect(']')
|
|
811
|
+
expr = this.withLoc(
|
|
812
|
+
{ kind: 'index', obj: expr, index },
|
|
813
|
+
this.getLocToken(expr) ?? this.tokens[this.pos - 1]
|
|
814
|
+
)
|
|
815
|
+
continue
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Member access
|
|
819
|
+
if (this.match('.')) {
|
|
820
|
+
const field = this.expect('ident').value
|
|
821
|
+
expr = this.withLoc(
|
|
822
|
+
{ kind: 'member', obj: expr, field },
|
|
823
|
+
this.getLocToken(expr) ?? this.tokens[this.pos - 1]
|
|
824
|
+
)
|
|
825
|
+
continue
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
break
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return expr
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private parseArgs(): Expr[] {
|
|
835
|
+
const args: Expr[] = []
|
|
836
|
+
|
|
837
|
+
if (!this.check(')')) {
|
|
838
|
+
do {
|
|
839
|
+
args.push(this.parseExpr())
|
|
840
|
+
} while (this.match(','))
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return args
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private parsePrimaryExpr(): Expr {
|
|
847
|
+
const token = this.peek()
|
|
848
|
+
|
|
849
|
+
if (token.kind === 'ident' && this.peek(1).kind === '=>') {
|
|
850
|
+
return this.parseSingleParamLambda()
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Integer literal
|
|
854
|
+
if (token.kind === 'int_lit') {
|
|
855
|
+
this.advance()
|
|
856
|
+
return this.withLoc({ kind: 'int_lit', value: parseInt(token.value, 10) }, token)
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Float literal
|
|
860
|
+
if (token.kind === 'float_lit') {
|
|
861
|
+
this.advance()
|
|
862
|
+
return this.withLoc({ kind: 'float_lit', value: parseFloat(token.value) }, token)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// NBT suffix literals
|
|
866
|
+
if (token.kind === 'byte_lit') {
|
|
867
|
+
this.advance()
|
|
868
|
+
return this.withLoc({ kind: 'byte_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
|
|
869
|
+
}
|
|
870
|
+
if (token.kind === 'short_lit') {
|
|
871
|
+
this.advance()
|
|
872
|
+
return this.withLoc({ kind: 'short_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
|
|
873
|
+
}
|
|
874
|
+
if (token.kind === 'long_lit') {
|
|
875
|
+
this.advance()
|
|
876
|
+
return this.withLoc({ kind: 'long_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
|
|
877
|
+
}
|
|
878
|
+
if (token.kind === 'double_lit') {
|
|
879
|
+
this.advance()
|
|
880
|
+
return this.withLoc({ kind: 'double_lit', value: parseFloat(token.value.slice(0, -1)) }, token)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// String literal
|
|
884
|
+
if (token.kind === 'string_lit') {
|
|
885
|
+
this.advance()
|
|
886
|
+
return this.parseStringExpr(token)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// MC name literal: #health → mc_name node (value = "health", without #)
|
|
890
|
+
if (token.kind === 'mc_name') {
|
|
891
|
+
this.advance()
|
|
892
|
+
return this.withLoc({ kind: 'mc_name', value: token.value.slice(1) }, token)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Boolean literal
|
|
896
|
+
if (token.kind === 'true') {
|
|
897
|
+
this.advance()
|
|
898
|
+
return this.withLoc({ kind: 'bool_lit', value: true }, token)
|
|
899
|
+
}
|
|
900
|
+
if (token.kind === 'false') {
|
|
901
|
+
this.advance()
|
|
902
|
+
return this.withLoc({ kind: 'bool_lit', value: false }, token)
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Range literal
|
|
906
|
+
if (token.kind === 'range_lit') {
|
|
907
|
+
this.advance()
|
|
908
|
+
return this.withLoc({ kind: 'range_lit', range: this.parseRangeValue(token.value) }, token)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Selector
|
|
912
|
+
if (token.kind === 'selector') {
|
|
913
|
+
this.advance()
|
|
914
|
+
return this.withLoc({
|
|
915
|
+
kind: 'selector',
|
|
916
|
+
raw: token.value,
|
|
917
|
+
isSingle: computeIsSingle(token.value),
|
|
918
|
+
sel: this.parseSelectorValue(token.value),
|
|
919
|
+
}, token)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Identifier
|
|
923
|
+
if (token.kind === 'ident') {
|
|
924
|
+
this.advance()
|
|
925
|
+
return this.withLoc({ kind: 'ident', name: token.value }, token)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Grouped expression
|
|
929
|
+
if (token.kind === '(') {
|
|
930
|
+
if (this.isBlockPosLiteral()) {
|
|
931
|
+
return this.parseBlockPos()
|
|
932
|
+
}
|
|
933
|
+
if (this.isLambdaStart()) {
|
|
934
|
+
return this.parseLambdaExpr()
|
|
935
|
+
}
|
|
936
|
+
this.advance()
|
|
937
|
+
const expr = this.parseExpr()
|
|
938
|
+
this.expect(')')
|
|
939
|
+
return expr
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Struct literal or block: { x: 10, y: 20 }
|
|
943
|
+
if (token.kind === '{') {
|
|
944
|
+
return this.parseStructLit()
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Array literal: [1, 2, 3] or []
|
|
948
|
+
if (token.kind === '[') {
|
|
949
|
+
return this.parseArrayLit()
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
this.error(`Unexpected token '${token.kind}'`)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private parseLiteralExpr(): LiteralExpr {
|
|
956
|
+
const expr = this.parsePrimaryExpr()
|
|
957
|
+
if (
|
|
958
|
+
expr.kind === 'int_lit' ||
|
|
959
|
+
expr.kind === 'float_lit' ||
|
|
960
|
+
expr.kind === 'bool_lit' ||
|
|
961
|
+
expr.kind === 'str_lit'
|
|
962
|
+
) {
|
|
963
|
+
return expr
|
|
964
|
+
}
|
|
965
|
+
this.error('Const value must be a literal')
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private parseSingleParamLambda(): Expr {
|
|
969
|
+
const paramToken = this.expect('ident')
|
|
970
|
+
const params: LambdaParam[] = [{ name: paramToken.value }]
|
|
971
|
+
this.expect('=>')
|
|
972
|
+
return this.finishLambdaExpr(params, paramToken)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
private parseLambdaExpr(): Expr {
|
|
976
|
+
const openParenToken = this.expect('(')
|
|
977
|
+
const params: LambdaParam[] = []
|
|
978
|
+
|
|
979
|
+
if (!this.check(')')) {
|
|
980
|
+
do {
|
|
981
|
+
const name = this.expect('ident').value
|
|
982
|
+
let type: TypeNode | undefined
|
|
983
|
+
if (this.match(':')) {
|
|
984
|
+
type = this.parseType()
|
|
985
|
+
}
|
|
986
|
+
params.push({ name, type })
|
|
987
|
+
} while (this.match(','))
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
this.expect(')')
|
|
991
|
+
let returnType: TypeNode | undefined
|
|
992
|
+
if (this.match('->')) {
|
|
993
|
+
returnType = this.parseType()
|
|
994
|
+
}
|
|
995
|
+
this.expect('=>')
|
|
996
|
+
return this.finishLambdaExpr(params, openParenToken, returnType)
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private finishLambdaExpr(params: LambdaParam[], token: Token, returnType?: TypeNode): Expr {
|
|
1000
|
+
const body = this.check('{') ? this.parseBlock() : this.parseExpr()
|
|
1001
|
+
return this.withLoc({ kind: 'lambda', params, returnType, body }, token)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private parseStringExpr(token: Token): Expr {
|
|
1005
|
+
if (!token.value.includes('${')) {
|
|
1006
|
+
return this.withLoc({ kind: 'str_lit', value: token.value }, token)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const parts: Array<string | Expr> = []
|
|
1010
|
+
let current = ''
|
|
1011
|
+
let index = 0
|
|
1012
|
+
|
|
1013
|
+
while (index < token.value.length) {
|
|
1014
|
+
if (token.value[index] === '$' && token.value[index + 1] === '{') {
|
|
1015
|
+
if (current) {
|
|
1016
|
+
parts.push(current)
|
|
1017
|
+
current = ''
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
index += 2
|
|
1021
|
+
let depth = 1
|
|
1022
|
+
let exprSource = ''
|
|
1023
|
+
let inString = false
|
|
1024
|
+
|
|
1025
|
+
while (index < token.value.length && depth > 0) {
|
|
1026
|
+
const char = token.value[index]
|
|
1027
|
+
|
|
1028
|
+
if (char === '"' && token.value[index - 1] !== '\\') {
|
|
1029
|
+
inString = !inString
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (!inString) {
|
|
1033
|
+
if (char === '{') {
|
|
1034
|
+
depth++
|
|
1035
|
+
} else if (char === '}') {
|
|
1036
|
+
depth--
|
|
1037
|
+
if (depth === 0) {
|
|
1038
|
+
index++
|
|
1039
|
+
break
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (depth > 0) {
|
|
1045
|
+
exprSource += char
|
|
1046
|
+
}
|
|
1047
|
+
index++
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (depth !== 0) {
|
|
1051
|
+
this.error('Unterminated string interpolation')
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
parts.push(this.parseEmbeddedExpr(exprSource))
|
|
1055
|
+
continue
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
current += token.value[index]
|
|
1059
|
+
index++
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (current) {
|
|
1063
|
+
parts.push(current)
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return this.withLoc({ kind: 'str_interp', parts }, token)
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
private parseEmbeddedExpr(source: string): Expr {
|
|
1070
|
+
const tokens = new Lexer(source, this.filePath).tokenize()
|
|
1071
|
+
const parser = new Parser(tokens, source, this.filePath)
|
|
1072
|
+
const expr = parser.parseExpr()
|
|
1073
|
+
|
|
1074
|
+
if (!parser.check('eof')) {
|
|
1075
|
+
parser.error(`Unexpected token '${parser.peek().kind}' in string interpolation`)
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return expr
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
private parseStructLit(): Expr {
|
|
1082
|
+
const braceToken = this.expect('{')
|
|
1083
|
+
const fields: { name: string; value: Expr }[] = []
|
|
1084
|
+
|
|
1085
|
+
if (!this.check('}')) {
|
|
1086
|
+
do {
|
|
1087
|
+
const name = this.expect('ident').value
|
|
1088
|
+
this.expect(':')
|
|
1089
|
+
const value = this.parseExpr()
|
|
1090
|
+
fields.push({ name, value })
|
|
1091
|
+
} while (this.match(','))
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
this.expect('}')
|
|
1095
|
+
return this.withLoc({ kind: 'struct_lit', fields }, braceToken)
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private parseArrayLit(): Expr {
|
|
1099
|
+
const bracketToken = this.expect('[')
|
|
1100
|
+
const elements: Expr[] = []
|
|
1101
|
+
|
|
1102
|
+
if (!this.check(']')) {
|
|
1103
|
+
do {
|
|
1104
|
+
elements.push(this.parseExpr())
|
|
1105
|
+
} while (this.match(','))
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
this.expect(']')
|
|
1109
|
+
return this.withLoc({ kind: 'array_lit', elements }, bracketToken)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
private isLambdaStart(): boolean {
|
|
1113
|
+
if (!this.check('(')) return false
|
|
1114
|
+
|
|
1115
|
+
let offset = 1
|
|
1116
|
+
if (this.peek(offset).kind !== ')') {
|
|
1117
|
+
while (true) {
|
|
1118
|
+
if (this.peek(offset).kind !== 'ident') {
|
|
1119
|
+
return false
|
|
1120
|
+
}
|
|
1121
|
+
offset += 1
|
|
1122
|
+
|
|
1123
|
+
if (this.peek(offset).kind === ':') {
|
|
1124
|
+
offset += 1
|
|
1125
|
+
const consumed = this.typeTokenLength(offset)
|
|
1126
|
+
if (consumed === 0) {
|
|
1127
|
+
return false
|
|
1128
|
+
}
|
|
1129
|
+
offset += consumed
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (this.peek(offset).kind === ',') {
|
|
1133
|
+
offset += 1
|
|
1134
|
+
continue
|
|
1135
|
+
}
|
|
1136
|
+
break
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (this.peek(offset).kind !== ')') {
|
|
1141
|
+
return false
|
|
1142
|
+
}
|
|
1143
|
+
offset += 1
|
|
1144
|
+
|
|
1145
|
+
if (this.peek(offset).kind === '=>') {
|
|
1146
|
+
return true
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (this.peek(offset).kind === '->') {
|
|
1150
|
+
offset += 1
|
|
1151
|
+
const consumed = this.typeTokenLength(offset)
|
|
1152
|
+
if (consumed === 0) {
|
|
1153
|
+
return false
|
|
1154
|
+
}
|
|
1155
|
+
offset += consumed
|
|
1156
|
+
return this.peek(offset).kind === '=>'
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return false
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
private typeTokenLength(offset: number): number {
|
|
1163
|
+
const token = this.peek(offset)
|
|
1164
|
+
|
|
1165
|
+
if (token.kind === '(') {
|
|
1166
|
+
let inner = offset + 1
|
|
1167
|
+
if (this.peek(inner).kind !== ')') {
|
|
1168
|
+
while (true) {
|
|
1169
|
+
const consumed = this.typeTokenLength(inner)
|
|
1170
|
+
if (consumed === 0) {
|
|
1171
|
+
return 0
|
|
1172
|
+
}
|
|
1173
|
+
inner += consumed
|
|
1174
|
+
if (this.peek(inner).kind === ',') {
|
|
1175
|
+
inner += 1
|
|
1176
|
+
continue
|
|
1177
|
+
}
|
|
1178
|
+
break
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (this.peek(inner).kind !== ')') {
|
|
1183
|
+
return 0
|
|
1184
|
+
}
|
|
1185
|
+
inner += 1
|
|
1186
|
+
|
|
1187
|
+
if (this.peek(inner).kind !== '->') {
|
|
1188
|
+
return 0
|
|
1189
|
+
}
|
|
1190
|
+
inner += 1
|
|
1191
|
+
const returnLen = this.typeTokenLength(inner)
|
|
1192
|
+
return returnLen === 0 ? 0 : inner + returnLen - offset
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const isNamedType =
|
|
1196
|
+
token.kind === 'int' ||
|
|
1197
|
+
token.kind === 'bool' ||
|
|
1198
|
+
token.kind === 'float' ||
|
|
1199
|
+
token.kind === 'string' ||
|
|
1200
|
+
token.kind === 'void' ||
|
|
1201
|
+
token.kind === 'BlockPos' ||
|
|
1202
|
+
token.kind === 'ident'
|
|
1203
|
+
if (!isNamedType) {
|
|
1204
|
+
return 0
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
let length = 1
|
|
1208
|
+
while (this.peek(offset + length).kind === '[' && this.peek(offset + length + 1).kind === ']') {
|
|
1209
|
+
length += 2
|
|
1210
|
+
}
|
|
1211
|
+
return length
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private isBlockPosLiteral(): boolean {
|
|
1215
|
+
if (!this.check('(')) return false
|
|
1216
|
+
|
|
1217
|
+
let offset = 1
|
|
1218
|
+
for (let i = 0; i < 3; i++) {
|
|
1219
|
+
const consumed = this.coordComponentTokenLength(offset)
|
|
1220
|
+
if (consumed === 0) return false
|
|
1221
|
+
offset += consumed
|
|
1222
|
+
|
|
1223
|
+
if (i < 2) {
|
|
1224
|
+
if (this.peek(offset).kind !== ',') return false
|
|
1225
|
+
offset += 1
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return this.peek(offset).kind === ')'
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
private coordComponentTokenLength(offset: number): number {
|
|
1233
|
+
const token = this.peek(offset)
|
|
1234
|
+
|
|
1235
|
+
if (token.kind === 'int_lit') {
|
|
1236
|
+
return 1
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (token.kind === '-') {
|
|
1240
|
+
return this.peek(offset + 1).kind === 'int_lit' ? 2 : 0
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (token.kind !== '~' && token.kind !== '^') {
|
|
1244
|
+
return 0
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const next = this.peek(offset + 1)
|
|
1248
|
+
if (next.kind === ',' || next.kind === ')') {
|
|
1249
|
+
return 1
|
|
1250
|
+
}
|
|
1251
|
+
if (next.kind === 'int_lit') {
|
|
1252
|
+
return 2
|
|
1253
|
+
}
|
|
1254
|
+
if (next.kind === '-' && this.peek(offset + 2).kind === 'int_lit') {
|
|
1255
|
+
return 3
|
|
1256
|
+
}
|
|
1257
|
+
return 0
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
private parseBlockPos(): BlockPosExpr {
|
|
1261
|
+
const openParenToken = this.expect('(')
|
|
1262
|
+
const x = this.parseCoordComponent()
|
|
1263
|
+
this.expect(',')
|
|
1264
|
+
const y = this.parseCoordComponent()
|
|
1265
|
+
this.expect(',')
|
|
1266
|
+
const z = this.parseCoordComponent()
|
|
1267
|
+
this.expect(')')
|
|
1268
|
+
return this.withLoc({ kind: 'blockpos', x, y, z }, openParenToken)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
private parseCoordComponent(): CoordComponent {
|
|
1272
|
+
const token = this.peek()
|
|
1273
|
+
|
|
1274
|
+
if (token.kind === '~' || token.kind === '^') {
|
|
1275
|
+
this.advance()
|
|
1276
|
+
const offset = this.parseSignedCoordOffset()
|
|
1277
|
+
return token.kind === '~'
|
|
1278
|
+
? { kind: 'relative', offset }
|
|
1279
|
+
: { kind: 'local', offset }
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return { kind: 'absolute', value: this.parseSignedCoordOffset(true) }
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
private parseSignedCoordOffset(requireValue = false): number {
|
|
1286
|
+
let sign = 1
|
|
1287
|
+
if (this.match('-')) {
|
|
1288
|
+
sign = -1
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (this.check('int_lit')) {
|
|
1292
|
+
return sign * parseInt(this.advance().value, 10)
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (requireValue) {
|
|
1296
|
+
this.error('Expected integer coordinate component')
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return 0
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// -------------------------------------------------------------------------
|
|
1303
|
+
// Selector Parsing
|
|
1304
|
+
// -------------------------------------------------------------------------
|
|
1305
|
+
|
|
1306
|
+
private parseSelector(): EntitySelector {
|
|
1307
|
+
const token = this.expect('selector')
|
|
1308
|
+
return this.parseSelectorValue(token.value)
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
private parseSelectorValue(value: string): EntitySelector {
|
|
1312
|
+
// Parse @e[type=zombie, distance=..5]
|
|
1313
|
+
const bracketIndex = value.indexOf('[')
|
|
1314
|
+
if (bracketIndex === -1) {
|
|
1315
|
+
return { kind: value as SelectorKind }
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const kind = value.slice(0, bracketIndex) as SelectorKind
|
|
1319
|
+
const paramsStr = value.slice(bracketIndex + 1, -1) // Remove [ and ]
|
|
1320
|
+
const filters = this.parseSelectorFilters(paramsStr)
|
|
1321
|
+
|
|
1322
|
+
return { kind, filters }
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
private parseSelectorFilters(paramsStr: string): SelectorFilter {
|
|
1326
|
+
const filters: SelectorFilter = {}
|
|
1327
|
+
const parts = this.splitSelectorParams(paramsStr)
|
|
1328
|
+
|
|
1329
|
+
for (const part of parts) {
|
|
1330
|
+
const eqIndex = part.indexOf('=')
|
|
1331
|
+
if (eqIndex === -1) continue
|
|
1332
|
+
|
|
1333
|
+
const key = part.slice(0, eqIndex).trim()
|
|
1334
|
+
const val = part.slice(eqIndex + 1).trim()
|
|
1335
|
+
|
|
1336
|
+
switch (key) {
|
|
1337
|
+
case 'type':
|
|
1338
|
+
filters.type = val
|
|
1339
|
+
break
|
|
1340
|
+
case 'distance':
|
|
1341
|
+
filters.distance = this.parseRangeValue(val)
|
|
1342
|
+
break
|
|
1343
|
+
case 'tag':
|
|
1344
|
+
if (val.startsWith('!')) {
|
|
1345
|
+
filters.notTag = filters.notTag ?? []
|
|
1346
|
+
filters.notTag.push(val.slice(1))
|
|
1347
|
+
} else {
|
|
1348
|
+
filters.tag = filters.tag ?? []
|
|
1349
|
+
filters.tag.push(val)
|
|
1350
|
+
}
|
|
1351
|
+
break
|
|
1352
|
+
case 'limit':
|
|
1353
|
+
filters.limit = parseInt(val, 10)
|
|
1354
|
+
break
|
|
1355
|
+
case 'sort':
|
|
1356
|
+
filters.sort = val as SelectorFilter['sort']
|
|
1357
|
+
break
|
|
1358
|
+
case 'nbt':
|
|
1359
|
+
filters.nbt = val
|
|
1360
|
+
break
|
|
1361
|
+
case 'gamemode':
|
|
1362
|
+
filters.gamemode = val
|
|
1363
|
+
break
|
|
1364
|
+
case 'scores':
|
|
1365
|
+
filters.scores = this.parseScoresFilter(val)
|
|
1366
|
+
break
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
return filters
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
private splitSelectorParams(str: string): string[] {
|
|
1374
|
+
const parts: string[] = []
|
|
1375
|
+
let current = ''
|
|
1376
|
+
let depth = 0
|
|
1377
|
+
|
|
1378
|
+
for (const char of str) {
|
|
1379
|
+
if (char === '{' || char === '[') depth++
|
|
1380
|
+
else if (char === '}' || char === ']') depth--
|
|
1381
|
+
else if (char === ',' && depth === 0) {
|
|
1382
|
+
parts.push(current.trim())
|
|
1383
|
+
current = ''
|
|
1384
|
+
continue
|
|
1385
|
+
}
|
|
1386
|
+
current += char
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (current.trim()) {
|
|
1390
|
+
parts.push(current.trim())
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
return parts
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
private parseScoresFilter(val: string): Record<string, RangeExpr> {
|
|
1397
|
+
// Parse {kills=1.., deaths=..5}
|
|
1398
|
+
const scores: Record<string, RangeExpr> = {}
|
|
1399
|
+
const inner = val.slice(1, -1) // Remove { and }
|
|
1400
|
+
const parts = inner.split(',')
|
|
1401
|
+
|
|
1402
|
+
for (const part of parts) {
|
|
1403
|
+
const [name, range] = part.split('=').map(s => s.trim())
|
|
1404
|
+
scores[name] = this.parseRangeValue(range)
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
return scores
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
private parseRangeValue(value: string): RangeExpr {
|
|
1411
|
+
// ..5 → { max: 5 }
|
|
1412
|
+
// 1.. → { min: 1 }
|
|
1413
|
+
// 1..10 → { min: 1, max: 10 }
|
|
1414
|
+
// 5 → { min: 5, max: 5 } (exact match)
|
|
1415
|
+
|
|
1416
|
+
if (value.startsWith('..')) {
|
|
1417
|
+
const max = parseInt(value.slice(2), 10)
|
|
1418
|
+
return { max }
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (value.endsWith('..')) {
|
|
1422
|
+
const min = parseInt(value.slice(0, -2), 10)
|
|
1423
|
+
return { min }
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const dotIndex = value.indexOf('..')
|
|
1427
|
+
if (dotIndex !== -1) {
|
|
1428
|
+
const min = parseInt(value.slice(0, dotIndex), 10)
|
|
1429
|
+
const max = parseInt(value.slice(dotIndex + 2), 10)
|
|
1430
|
+
return { min, max }
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Exact value
|
|
1434
|
+
const val = parseInt(value, 10)
|
|
1435
|
+
return { min: val, max: val }
|
|
1436
|
+
}
|
|
1437
|
+
}
|