redscript-mc 3.0.1 → 3.0.2
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/workflows/ci.yml +1 -0
- package/README.md +119 -313
- package/README.zh.md +118 -314
- package/ROADMAP.md +5 -5
- package/dist/data/impl_test/function/counter/get.mcfunction +5 -0
- package/dist/data/impl_test/function/counter/inc.mcfunction +7 -0
- package/dist/data/impl_test/function/counter/new.mcfunction +4 -0
- package/dist/data/impl_test/function/load.mcfunction +1 -0
- package/dist/data/impl_test/function/test_impl.mcfunction +10 -0
- package/dist/data/minecraft/tags/function/load.json +5 -0
- package/dist/data/playground/function/load.mcfunction +1 -0
- package/dist/data/playground/function/start.mcfunction +4 -0
- package/dist/data/playground/function/start__say_macro_t1.mcfunction +1 -0
- package/dist/data/playground/function/stop.mcfunction +5 -0
- package/dist/data/playground/function/stop__say_macro_t0.mcfunction +1 -0
- package/dist/data/stdlib_queue8_test/function/__queue_append_apply.mcfunction +4 -0
- package/dist/data/stdlib_queue8_test/function/__queue_peek_apply.mcfunction +4 -0
- package/dist/data/stdlib_queue8_test/function/__queue_size_raw_apply.mcfunction +4 -0
- package/dist/data/stdlib_queue8_test/function/load.mcfunction +1 -0
- package/dist/data/stdlib_queue8_test/function/queue_clear.mcfunction +6 -0
- package/dist/data/stdlib_queue8_test/function/queue_empty__merge_1.mcfunction +5 -0
- package/dist/data/stdlib_queue8_test/function/queue_empty__then_0.mcfunction +5 -0
- package/dist/data/stdlib_queue8_test/function/queue_peek__merge_1.mcfunction +13 -0
- package/dist/data/stdlib_queue8_test/function/queue_peek__then_0.mcfunction +5 -0
- package/dist/data/stdlib_queue8_test/function/queue_pop__merge_1.mcfunction +15 -0
- package/dist/data/stdlib_queue8_test/function/queue_pop__then_0.mcfunction +5 -0
- package/dist/data/stdlib_queue8_test/function/queue_push__const_11.mcfunction +6 -0
- package/dist/data/stdlib_queue8_test/function/queue_push__const_22.mcfunction +6 -0
- package/dist/data/stdlib_queue8_test/function/queue_size.mcfunction +13 -0
- package/dist/data/stdlib_queue8_test/function/test_queue_push_and_size.mcfunction +13 -0
- package/dist/data/test/function/load.mcfunction +1 -0
- package/dist/data/test/function/say_at.mcfunction +6 -0
- package/dist/data/test/function/test.mcfunction +4 -0
- package/dist/pack.mcmeta +6 -0
- package/dist/package.json +1 -1
- package/dist/src/__tests__/formatter-extra.test.d.ts +7 -0
- package/dist/src/__tests__/formatter-extra.test.js +123 -0
- package/dist/src/__tests__/global-vars.test.d.ts +13 -0
- package/dist/src/__tests__/global-vars.test.js +156 -0
- package/dist/src/__tests__/lint/new-rules.test.d.ts +9 -0
- package/dist/src/__tests__/lint/new-rules.test.js +402 -0
- package/dist/src/__tests__/lsp-rename.test.d.ts +8 -0
- package/dist/src/__tests__/lsp-rename.test.js +157 -0
- package/dist/src/__tests__/mc-integration/say-fstring.test.d.ts +11 -0
- package/dist/src/__tests__/mc-integration/say-fstring.test.js +220 -0
- package/dist/src/__tests__/mc-integration/stdlib-coverage-2.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-3.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-4.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-5.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-6.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-7.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-8.test.js +1 -1
- package/dist/src/__tests__/mc-syntax.test.js +4 -1
- package/dist/src/__tests__/monomorphize-coverage.test.d.ts +9 -0
- package/dist/src/__tests__/monomorphize-coverage.test.js +204 -0
- package/dist/src/__tests__/optimizer-cse.test.d.ts +7 -0
- package/dist/src/__tests__/optimizer-cse.test.js +226 -0
- package/dist/src/__tests__/parser.test.js +4 -13
- package/dist/src/__tests__/repl-server-extra.test.js +6 -7
- package/dist/src/__tests__/repl-server.test.js +5 -7
- package/dist/src/__tests__/stdlib/queue.test.js +6 -6
- package/dist/src/cli.js +0 -0
- package/dist/src/lexer/index.js +2 -1
- package/dist/src/lint/index.d.ts +12 -5
- package/dist/src/lint/index.js +730 -5
- package/dist/src/lsp/main.js +0 -0
- package/dist/src/mc-test/client.d.ts +21 -0
- package/dist/src/mc-test/client.js +34 -0
- package/dist/src/mir/lower.js +108 -6
- package/dist/src/optimizer/interprocedural.js +37 -2
- package/dist/src/parser/decl-parser.d.ts +19 -0
- package/dist/src/parser/decl-parser.js +323 -0
- package/dist/src/parser/expr-parser.d.ts +46 -0
- package/dist/src/parser/expr-parser.js +759 -0
- package/dist/src/parser/index.d.ts +8 -129
- package/dist/src/parser/index.js +13 -2262
- package/dist/src/parser/stmt-parser.d.ts +28 -0
- package/dist/src/parser/stmt-parser.js +577 -0
- package/dist/src/parser/type-parser.d.ts +20 -0
- package/dist/src/parser/type-parser.js +257 -0
- package/dist/src/parser/utils.d.ts +34 -0
- package/dist/src/parser/utils.js +141 -0
- package/docs/dev/README-mc-integration-tests.md +141 -0
- package/docs/lint-rules.md +162 -0
- package/docs/stdlib/bigint.md +2 -0
- package/editors/vscode/README.md +63 -41
- package/editors/vscode/out/extension.js +1881 -1776
- package/editors/vscode/out/lsp-server.js +4257 -3651
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/examples/loops-demo.mcrs +87 -0
- package/package.json +1 -1
- package/redscript-docs/docs/en/stdlib/advanced.md +629 -0
- package/redscript-docs/docs/en/stdlib/bigint.md +316 -0
- package/redscript-docs/docs/en/stdlib/bits.md +292 -0
- package/redscript-docs/docs/en/stdlib/bossbar.md +177 -0
- package/redscript-docs/docs/en/stdlib/calculus.md +289 -0
- package/redscript-docs/docs/en/stdlib/color.md +353 -0
- package/redscript-docs/docs/en/stdlib/combat.md +88 -0
- package/redscript-docs/docs/en/stdlib/cooldown.md +82 -0
- package/redscript-docs/docs/en/stdlib/dialog.md +155 -0
- package/redscript-docs/docs/en/stdlib/easing.md +558 -0
- package/redscript-docs/docs/en/stdlib/ecs.md +475 -0
- package/redscript-docs/docs/en/stdlib/effects.md +324 -0
- package/redscript-docs/docs/en/stdlib/events.md +3 -0
- package/redscript-docs/docs/en/stdlib/expr.md +45 -0
- package/redscript-docs/docs/en/stdlib/fft.md +141 -0
- package/redscript-docs/docs/en/stdlib/geometry.md +430 -0
- package/redscript-docs/docs/en/stdlib/graph.md +259 -0
- package/redscript-docs/docs/en/stdlib/heap.md +185 -0
- package/redscript-docs/docs/en/stdlib/interactions.md +179 -0
- package/redscript-docs/docs/en/stdlib/inventory.md +97 -0
- package/redscript-docs/docs/en/stdlib/linalg.md +557 -0
- package/redscript-docs/docs/en/stdlib/list.md +559 -0
- package/redscript-docs/docs/en/stdlib/map.md +140 -0
- package/redscript-docs/docs/en/stdlib/math.md +193 -0
- package/redscript-docs/docs/en/stdlib/math_hp.md +149 -0
- package/redscript-docs/docs/en/stdlib/matrix.md +403 -0
- package/redscript-docs/docs/en/stdlib/mobs.md +965 -0
- package/redscript-docs/docs/en/stdlib/noise.md +244 -0
- package/redscript-docs/docs/en/stdlib/ode.md +253 -0
- package/redscript-docs/docs/en/stdlib/parabola.md +342 -0
- package/redscript-docs/docs/en/stdlib/particles.md +311 -0
- package/redscript-docs/docs/en/stdlib/pathfind.md +255 -0
- package/redscript-docs/docs/en/stdlib/physics.md +493 -0
- package/redscript-docs/docs/en/stdlib/player.md +78 -0
- package/redscript-docs/docs/en/stdlib/quaternion.md +673 -0
- package/redscript-docs/docs/en/stdlib/queue.md +134 -0
- package/redscript-docs/docs/en/stdlib/random.md +223 -0
- package/redscript-docs/docs/en/stdlib/result.md +143 -0
- package/redscript-docs/docs/en/stdlib/scheduler.md +183 -0
- package/redscript-docs/docs/en/stdlib/set_int.md +190 -0
- package/redscript-docs/docs/en/stdlib/sets.md +101 -0
- package/redscript-docs/docs/en/stdlib/signal.md +400 -0
- package/redscript-docs/docs/en/stdlib/sort.md +104 -0
- package/redscript-docs/docs/en/stdlib/spawn.md +147 -0
- package/redscript-docs/docs/en/stdlib/state.md +142 -0
- package/redscript-docs/docs/en/stdlib/strings.md +154 -0
- package/redscript-docs/docs/en/stdlib/tags.md +3451 -0
- package/redscript-docs/docs/en/stdlib/teams.md +153 -0
- package/redscript-docs/docs/en/stdlib/timer.md +246 -0
- package/redscript-docs/docs/en/stdlib/vec.md +158 -0
- package/redscript-docs/docs/en/stdlib/world.md +298 -0
- package/redscript-docs/docs/zh/stdlib/advanced.md +615 -0
- package/redscript-docs/docs/zh/stdlib/bigint.md +316 -0
- package/redscript-docs/docs/zh/stdlib/bits.md +292 -0
- package/redscript-docs/docs/zh/stdlib/bossbar.md +170 -0
- package/redscript-docs/docs/zh/stdlib/calculus.md +287 -0
- package/redscript-docs/docs/zh/stdlib/color.md +353 -0
- package/redscript-docs/docs/zh/stdlib/combat.md +88 -0
- package/redscript-docs/docs/zh/stdlib/cooldown.md +84 -0
- package/redscript-docs/docs/zh/stdlib/dialog.md +152 -0
- package/redscript-docs/docs/zh/stdlib/easing.md +558 -0
- package/redscript-docs/docs/zh/stdlib/ecs.md +472 -0
- package/redscript-docs/docs/zh/stdlib/effects.md +324 -0
- package/redscript-docs/docs/zh/stdlib/events.md +3 -0
- package/redscript-docs/docs/zh/stdlib/expr.md +37 -0
- package/redscript-docs/docs/zh/stdlib/fft.md +128 -0
- package/redscript-docs/docs/zh/stdlib/geometry.md +430 -0
- package/redscript-docs/docs/zh/stdlib/graph.md +259 -0
- package/redscript-docs/docs/zh/stdlib/heap.md +185 -0
- package/redscript-docs/docs/zh/stdlib/interactions.md +160 -0
- package/redscript-docs/docs/zh/stdlib/inventory.md +94 -0
- package/redscript-docs/docs/zh/stdlib/linalg.md +543 -0
- package/redscript-docs/docs/zh/stdlib/list.md +561 -0
- package/redscript-docs/docs/zh/stdlib/map.md +132 -0
- package/redscript-docs/docs/zh/stdlib/math.md +193 -0
- package/redscript-docs/docs/zh/stdlib/math_hp.md +143 -0
- package/redscript-docs/docs/zh/stdlib/matrix.md +396 -0
- package/redscript-docs/docs/zh/stdlib/mobs.md +965 -0
- package/redscript-docs/docs/zh/stdlib/noise.md +244 -0
- package/redscript-docs/docs/zh/stdlib/ode.md +243 -0
- package/redscript-docs/docs/zh/stdlib/parabola.md +337 -0
- package/redscript-docs/docs/zh/stdlib/particles.md +307 -0
- package/redscript-docs/docs/zh/stdlib/pathfind.md +255 -0
- package/redscript-docs/docs/zh/stdlib/physics.md +493 -0
- package/redscript-docs/docs/zh/stdlib/player.md +78 -0
- package/redscript-docs/docs/zh/stdlib/quaternion.md +669 -0
- package/redscript-docs/docs/zh/stdlib/queue.md +124 -0
- package/redscript-docs/docs/zh/stdlib/random.md +222 -0
- package/redscript-docs/docs/zh/stdlib/result.md +147 -0
- package/redscript-docs/docs/zh/stdlib/scheduler.md +173 -0
- package/redscript-docs/docs/zh/stdlib/set_int.md +180 -0
- package/redscript-docs/docs/zh/stdlib/sets.md +107 -0
- package/redscript-docs/docs/zh/stdlib/signal.md +373 -0
- package/redscript-docs/docs/zh/stdlib/sort.md +104 -0
- package/redscript-docs/docs/zh/stdlib/spawn.md +142 -0
- package/redscript-docs/docs/zh/stdlib/state.md +134 -0
- package/redscript-docs/docs/zh/stdlib/strings.md +107 -0
- package/redscript-docs/docs/zh/stdlib/tags.md +3451 -0
- package/redscript-docs/docs/zh/stdlib/teams.md +150 -0
- package/redscript-docs/docs/zh/stdlib/timer.md +254 -0
- package/redscript-docs/docs/zh/stdlib/vec.md +158 -0
- package/redscript-docs/docs/zh/stdlib/world.md +289 -0
- package/src/__tests__/formatter-extra.test.ts +139 -0
- package/src/__tests__/global-vars.test.ts +171 -0
- package/src/__tests__/lint/new-rules.test.ts +437 -0
- package/src/__tests__/lsp-rename.test.ts +171 -0
- package/src/__tests__/mc-integration/say-fstring.test.ts +211 -0
- package/src/__tests__/mc-integration/stdlib-coverage-2.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-3.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-4.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-5.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-6.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-7.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-8.test.ts +1 -1
- package/src/__tests__/mc-syntax.test.ts +3 -0
- package/src/__tests__/monomorphize-coverage.test.ts +220 -0
- package/src/__tests__/optimizer-cse.test.ts +250 -0
- package/src/__tests__/parser.test.ts +4 -13
- package/src/__tests__/repl-server-extra.test.ts +6 -6
- package/src/__tests__/repl-server.test.ts +5 -6
- package/src/__tests__/stdlib/queue.test.ts +6 -6
- package/src/lexer/index.ts +2 -1
- package/src/lint/index.ts +713 -5
- package/src/mc-test/client.ts +40 -0
- package/src/mir/lower.ts +111 -2
- package/src/optimizer/interprocedural.ts +40 -2
- package/src/parser/decl-parser.ts +349 -0
- package/src/parser/expr-parser.ts +838 -0
- package/src/parser/index.ts +17 -2558
- package/src/parser/stmt-parser.ts +585 -0
- package/src/parser/type-parser.ts +276 -0
- package/src/parser/utils.ts +173 -0
- package/src/stdlib/queue.mcrs +19 -6
package/src/parser/index.ts
CHANGED
|
@@ -3,220 +3,25 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Recursive descent parser that converts tokens into an AST.
|
|
5
5
|
* Uses precedence climbing for expression parsing.
|
|
6
|
+
*
|
|
7
|
+
* The Parser class extends a chain of sub-parsers:
|
|
8
|
+
* Parser → DeclParser → StmtParser → ExprParser → TypeParser → ParserBase
|
|
9
|
+
*
|
|
10
|
+
* Each layer adds methods for its domain; the full Parser assembles them
|
|
11
|
+
* into the top-level `parse()` entry point.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
|
-
import {
|
|
14
|
+
import { DiagnosticError } from '../diagnostics'
|
|
9
15
|
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
CoordComponent, LambdaParam, EntityTypeName, ImportDecl, MatchPattern,
|
|
14
|
-
InterfaceDecl, InterfaceMethod
|
|
16
|
+
Program, FnDecl, GlobalDecl, StructDecl, ImplBlock, EnumDecl,
|
|
17
|
+
ConstDecl, ImportDecl,
|
|
18
|
+
InterfaceDecl,
|
|
15
19
|
} from '../ast/types'
|
|
16
|
-
import
|
|
17
|
-
import { DiagnosticError } from '../diagnostics'
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Operator Precedence (higher = binds tighter)
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
const PRECEDENCE: Record<string, number> = {
|
|
24
|
-
'||': 1,
|
|
25
|
-
'&&': 2,
|
|
26
|
-
'==': 3, '!=': 3,
|
|
27
|
-
'<': 4, '<=': 4, '>': 4, '>=': 4, 'is': 4,
|
|
28
|
-
'+': 5, '-': 5,
|
|
29
|
-
'*': 6, '/': 6, '%': 6,
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const BINARY_OPS = new Set(['||', '&&', '==', '!=', '<', '<=', '>', '>=', 'is', '+', '-', '*', '/', '%'])
|
|
33
|
-
|
|
34
|
-
const ENTITY_TYPE_NAMES = new Set<EntityTypeName>([
|
|
35
|
-
'entity',
|
|
36
|
-
'Player',
|
|
37
|
-
'Mob',
|
|
38
|
-
'HostileMob',
|
|
39
|
-
'PassiveMob',
|
|
40
|
-
'Zombie',
|
|
41
|
-
'Skeleton',
|
|
42
|
-
'Creeper',
|
|
43
|
-
'Spider',
|
|
44
|
-
'Enderman',
|
|
45
|
-
'Blaze',
|
|
46
|
-
'Witch',
|
|
47
|
-
'Slime',
|
|
48
|
-
'ZombieVillager',
|
|
49
|
-
'Husk',
|
|
50
|
-
'Drowned',
|
|
51
|
-
'Stray',
|
|
52
|
-
'WitherSkeleton',
|
|
53
|
-
'CaveSpider',
|
|
54
|
-
'Pig',
|
|
55
|
-
'Cow',
|
|
56
|
-
'Sheep',
|
|
57
|
-
'Chicken',
|
|
58
|
-
'Villager',
|
|
59
|
-
'WanderingTrader',
|
|
60
|
-
'ArmorStand',
|
|
61
|
-
'Item',
|
|
62
|
-
'Arrow',
|
|
63
|
-
])
|
|
64
|
-
|
|
65
|
-
function computeIsSingle(raw: string): boolean {
|
|
66
|
-
if (/^@[spr](\[|$)/.test(raw)) return true
|
|
67
|
-
if (/[\[,\s]limit=1[,\]\s]/.test(raw)) return true
|
|
68
|
-
return false
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Parser Class
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
export class Parser {
|
|
76
|
-
private tokens: Token[]
|
|
77
|
-
private pos: number = 0
|
|
78
|
-
private sourceLines: string[]
|
|
79
|
-
private filePath?: string
|
|
80
|
-
/** Set to true once `module library;` is seen — all subsequent fn declarations
|
|
81
|
-
* will be marked isLibraryFn=true. When library sources are parsed via the
|
|
82
|
-
* `librarySources` compile option, each source is parsed by its own fresh
|
|
83
|
-
* Parser instance, so this flag never bleeds into user code. */
|
|
84
|
-
private inLibraryMode: boolean = false
|
|
85
|
-
/** Warnings accumulated during parsing (e.g. deprecated keyword usage). */
|
|
86
|
-
readonly warnings: string[] = []
|
|
87
|
-
/** Parse errors collected during error-recovery mode. */
|
|
88
|
-
readonly parseErrors: DiagnosticError[] = []
|
|
89
|
-
|
|
90
|
-
constructor(tokens: Token[], source?: string, filePath?: string) {
|
|
91
|
-
this.tokens = tokens
|
|
92
|
-
this.sourceLines = source?.split('\n') ?? []
|
|
93
|
-
this.filePath = filePath
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// -------------------------------------------------------------------------
|
|
97
|
-
// Utilities
|
|
98
|
-
// -------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
private peek(offset = 0): Token {
|
|
101
|
-
const idx = this.pos + offset
|
|
102
|
-
if (idx >= this.tokens.length) {
|
|
103
|
-
return this.tokens[this.tokens.length - 1] // eof
|
|
104
|
-
}
|
|
105
|
-
return this.tokens[idx]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private advance(): Token {
|
|
109
|
-
const token = this.tokens[this.pos]
|
|
110
|
-
if (token.kind !== 'eof') this.pos++
|
|
111
|
-
return token
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private check(kind: TokenKind): boolean {
|
|
115
|
-
return this.peek().kind === kind
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private match(...kinds: TokenKind[]): boolean {
|
|
119
|
-
for (const kind of kinds) {
|
|
120
|
-
if (this.check(kind)) {
|
|
121
|
-
this.advance()
|
|
122
|
-
return true
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return false
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private expect(kind: TokenKind): Token {
|
|
129
|
-
const token = this.peek()
|
|
130
|
-
if (token.kind !== kind) {
|
|
131
|
-
throw new DiagnosticError(
|
|
132
|
-
'ParseError',
|
|
133
|
-
`Expected '${kind}' but got '${token.kind}'`,
|
|
134
|
-
{ file: this.filePath, line: token.line, col: token.col },
|
|
135
|
-
this.sourceLines
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
return this.advance()
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
private error(message: string): never {
|
|
142
|
-
const token = this.peek()
|
|
143
|
-
throw new DiagnosticError(
|
|
144
|
-
'ParseError',
|
|
145
|
-
message,
|
|
146
|
-
{ file: this.filePath, line: token.line, col: token.col },
|
|
147
|
-
this.sourceLines
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private withLoc<T extends object>(node: T, token: Token): T {
|
|
152
|
-
const span: Span = { line: token.line, col: token.col }
|
|
153
|
-
Object.defineProperty(node, 'span', {
|
|
154
|
-
value: span,
|
|
155
|
-
enumerable: false,
|
|
156
|
-
configurable: true,
|
|
157
|
-
writable: true,
|
|
158
|
-
})
|
|
159
|
-
return node
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private getLocToken(node: object): Token | null {
|
|
163
|
-
const span = (node as { span?: Span }).span
|
|
164
|
-
if (!span) {
|
|
165
|
-
return null
|
|
166
|
-
}
|
|
167
|
-
return { kind: 'eof', value: '', line: span.line, col: span.col }
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// -------------------------------------------------------------------------
|
|
171
|
-
// Error Recovery
|
|
172
|
-
// -------------------------------------------------------------------------
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Synchronize to the next top-level declaration boundary after a parse error.
|
|
176
|
-
* Skips tokens until we find a keyword that starts a top-level declaration,
|
|
177
|
-
* or a `}` (end of a block), or EOF.
|
|
178
|
-
*/
|
|
179
|
-
private syncToNextDecl(): void {
|
|
180
|
-
const TOP_LEVEL_KEYWORDS = new Set([
|
|
181
|
-
'fn', 'struct', 'impl', 'enum', 'const', 'let', 'export', 'declare', 'import', 'namespace', 'module'
|
|
182
|
-
])
|
|
183
|
-
while (!this.check('eof')) {
|
|
184
|
-
const kind = this.peek().kind
|
|
185
|
-
if (kind === '}') {
|
|
186
|
-
this.advance() // consume the stray `}`
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
if (TOP_LEVEL_KEYWORDS.has(kind)) {
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
// Also recover on a plain ident that could be 'import' keyword used as ident
|
|
193
|
-
if (kind === 'ident' && this.peek().value === 'import') {
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
this.advance()
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Synchronize to the next statement boundary inside a block after a parse error.
|
|
202
|
-
* Skips tokens until we reach `;`, `}`, or EOF.
|
|
203
|
-
*/
|
|
204
|
-
private syncToNextStmt(): void {
|
|
205
|
-
while (!this.check('eof')) {
|
|
206
|
-
const kind = this.peek().kind
|
|
207
|
-
if (kind === ';') {
|
|
208
|
-
this.advance() // consume the `;`
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
if (kind === '}') {
|
|
212
|
-
return // leave `}` for parseBlock to consume
|
|
213
|
-
}
|
|
214
|
-
this.advance()
|
|
215
|
-
}
|
|
216
|
-
}
|
|
20
|
+
import { DeclParser } from './decl-parser'
|
|
217
21
|
|
|
22
|
+
export class Parser extends DeclParser {
|
|
218
23
|
// -------------------------------------------------------------------------
|
|
219
|
-
// Program
|
|
24
|
+
// Program (top-level entry point)
|
|
220
25
|
// -------------------------------------------------------------------------
|
|
221
26
|
|
|
222
27
|
parse(defaultNamespace = 'redscript'): Program {
|
|
@@ -232,7 +37,6 @@ export class Parser {
|
|
|
232
37
|
let isLibrary = false
|
|
233
38
|
let moduleName: string | undefined
|
|
234
39
|
|
|
235
|
-
// Check for namespace declaration
|
|
236
40
|
if (this.check('namespace')) {
|
|
237
41
|
this.advance()
|
|
238
42
|
const name = this.expect('ident')
|
|
@@ -240,10 +44,6 @@ export class Parser {
|
|
|
240
44
|
this.match(';')
|
|
241
45
|
}
|
|
242
46
|
|
|
243
|
-
// Check for module declaration: `module library;` or `module <name>;`
|
|
244
|
-
// Library-mode: all functions parsed from this point are marked isLibraryFn=true.
|
|
245
|
-
// When using the `librarySources` compile option, each library source is parsed
|
|
246
|
-
// by its own fresh Parser — so this flag never bleeds into user code.
|
|
247
47
|
if (this.check('module')) {
|
|
248
48
|
this.advance()
|
|
249
49
|
const modKind = this.expect('ident')
|
|
@@ -251,17 +51,14 @@ export class Parser {
|
|
|
251
51
|
isLibrary = true
|
|
252
52
|
this.inLibraryMode = true
|
|
253
53
|
} else {
|
|
254
|
-
// Named module declaration: `module math;`
|
|
255
54
|
moduleName = modKind.value
|
|
256
55
|
}
|
|
257
56
|
this.match(';')
|
|
258
57
|
}
|
|
259
58
|
|
|
260
|
-
// Parse struct, function, and import declarations
|
|
261
59
|
while (!this.check('eof')) {
|
|
262
60
|
try {
|
|
263
61
|
if (this.check('decorator') && this.peek().value.startsWith('@config')) {
|
|
264
|
-
// @config decorator on a global let
|
|
265
62
|
const decorToken = this.advance()
|
|
266
63
|
const decorator = this.parseDecoratorValue(decorToken.value)
|
|
267
64
|
if (!this.check('let')) {
|
|
@@ -274,8 +71,7 @@ export class Parser {
|
|
|
274
71
|
} else if (this.check('let')) {
|
|
275
72
|
globals.push(this.parseGlobalDecl(true))
|
|
276
73
|
} else if (this.check('decorator') && this.peek().value === '@singleton') {
|
|
277
|
-
|
|
278
|
-
this.advance() // consume '@singleton'
|
|
74
|
+
this.advance()
|
|
279
75
|
if (!this.check('struct')) {
|
|
280
76
|
this.error('@singleton decorator must be followed by a struct declaration')
|
|
281
77
|
}
|
|
@@ -293,19 +89,16 @@ export class Parser {
|
|
|
293
89
|
} else if (this.check('const')) {
|
|
294
90
|
consts.push(this.parseConstDecl())
|
|
295
91
|
} else if (this.check('declare')) {
|
|
296
|
-
|
|
297
|
-
this.advance() // consume 'declare'
|
|
92
|
+
this.advance()
|
|
298
93
|
this.parseDeclareStub()
|
|
299
94
|
} else if (this.check('export')) {
|
|
300
95
|
declarations.push(this.parseExportedFnDecl())
|
|
301
96
|
} else if (this.check('import') || (this.check('ident') && this.peek().value === 'import')) {
|
|
302
|
-
|
|
303
|
-
this.advance() // consume 'import' (keyword or ident)
|
|
97
|
+
this.advance()
|
|
304
98
|
const importToken = this.peek()
|
|
305
99
|
const modName = this.expect('ident').value
|
|
306
|
-
// Check for `::` — if present, this is a symbol import; otherwise, whole-module import
|
|
307
100
|
if (this.check('::')) {
|
|
308
|
-
this.advance()
|
|
101
|
+
this.advance()
|
|
309
102
|
let symbol: string
|
|
310
103
|
if (this.check('*')) {
|
|
311
104
|
this.advance()
|
|
@@ -316,7 +109,6 @@ export class Parser {
|
|
|
316
109
|
this.match(';')
|
|
317
110
|
imports.push(this.withLoc({ moduleName: modName, symbol }, importToken))
|
|
318
111
|
} else {
|
|
319
|
-
// Whole-module import: `import player_utils;`
|
|
320
112
|
this.match(';')
|
|
321
113
|
imports.push(this.withLoc({ moduleName: modName, symbol: undefined }, importToken))
|
|
322
114
|
}
|
|
@@ -335,2337 +127,4 @@ export class Parser {
|
|
|
335
127
|
|
|
336
128
|
return { namespace, moduleName, globals, declarations, structs, implBlocks, enums, consts, imports, interfaces, isLibrary }
|
|
337
129
|
}
|
|
338
|
-
|
|
339
|
-
// -------------------------------------------------------------------------
|
|
340
|
-
// Struct Declaration
|
|
341
|
-
// -------------------------------------------------------------------------
|
|
342
|
-
|
|
343
|
-
private parseStructDecl(): StructDecl {
|
|
344
|
-
const structToken = this.expect('struct')
|
|
345
|
-
const name = this.expect('ident').value
|
|
346
|
-
const extendsName = this.match('extends') ? this.expect('ident').value : undefined
|
|
347
|
-
this.expect('{')
|
|
348
|
-
|
|
349
|
-
const fields: StructField[] = []
|
|
350
|
-
while (!this.check('}') && !this.check('eof')) {
|
|
351
|
-
const fieldName = this.expect('ident').value
|
|
352
|
-
this.expect(':')
|
|
353
|
-
const fieldType = this.parseType()
|
|
354
|
-
fields.push({ name: fieldName, type: fieldType })
|
|
355
|
-
|
|
356
|
-
// Allow optional comma or semicolon between fields
|
|
357
|
-
this.match(',')
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
this.expect('}')
|
|
361
|
-
return this.withLoc({ name, extends: extendsName, fields }, structToken)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
private parseEnumDecl(): EnumDecl {
|
|
365
|
-
const enumToken = this.expect('enum')
|
|
366
|
-
const name = this.expect('ident').value
|
|
367
|
-
this.expect('{')
|
|
368
|
-
|
|
369
|
-
const variants: EnumVariant[] = []
|
|
370
|
-
let nextValue = 0
|
|
371
|
-
|
|
372
|
-
while (!this.check('}') && !this.check('eof')) {
|
|
373
|
-
const variantToken = this.expect('ident')
|
|
374
|
-
const variant: EnumVariant = { name: variantToken.value }
|
|
375
|
-
|
|
376
|
-
// Payload fields: Variant(field: Type, ...)
|
|
377
|
-
if (this.check('(')) {
|
|
378
|
-
this.advance() // consume '('
|
|
379
|
-
const fields: { name: string; type: TypeNode }[] = []
|
|
380
|
-
while (!this.check(')') && !this.check('eof')) {
|
|
381
|
-
const fieldName = this.expect('ident').value
|
|
382
|
-
this.expect(':')
|
|
383
|
-
const fieldType = this.parseType()
|
|
384
|
-
fields.push({ name: fieldName, type: fieldType })
|
|
385
|
-
if (!this.match(',')) break
|
|
386
|
-
}
|
|
387
|
-
this.expect(')')
|
|
388
|
-
variant.fields = fields
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (this.match('=')) {
|
|
392
|
-
const valueToken = this.expect('int_lit')
|
|
393
|
-
variant.value = parseInt(valueToken.value, 10)
|
|
394
|
-
nextValue = variant.value + 1
|
|
395
|
-
} else {
|
|
396
|
-
variant.value = nextValue++
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
variants.push(variant)
|
|
400
|
-
|
|
401
|
-
if (!this.match(',')) {
|
|
402
|
-
break
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
this.expect('}')
|
|
407
|
-
return this.withLoc({ name, variants }, enumToken)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
private parseImplBlock(): ImplBlock {
|
|
411
|
-
const implToken = this.expect('impl')
|
|
412
|
-
let traitName: string | undefined
|
|
413
|
-
let typeName: string
|
|
414
|
-
const firstName = this.expect('ident').value
|
|
415
|
-
if (this.match('for')) {
|
|
416
|
-
traitName = firstName
|
|
417
|
-
typeName = this.expect('ident').value
|
|
418
|
-
} else {
|
|
419
|
-
typeName = firstName
|
|
420
|
-
}
|
|
421
|
-
this.expect('{')
|
|
422
|
-
|
|
423
|
-
const methods: FnDecl[] = []
|
|
424
|
-
while (!this.check('}') && !this.check('eof')) {
|
|
425
|
-
methods.push(this.parseFnDecl(typeName))
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
this.expect('}')
|
|
429
|
-
return this.withLoc({ kind: 'impl_block', traitName, typeName, methods }, implToken)
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Parse an interface declaration:
|
|
434
|
-
* interface <Name> {
|
|
435
|
-
* fn <method>(<params>): <retType>
|
|
436
|
-
* ...
|
|
437
|
-
* }
|
|
438
|
-
* Method signatures have no body — they are prototype-only.
|
|
439
|
-
*/
|
|
440
|
-
private parseInterfaceDecl(): InterfaceDecl {
|
|
441
|
-
const ifaceToken = this.expect('interface')
|
|
442
|
-
const name = this.expect('ident').value
|
|
443
|
-
this.expect('{')
|
|
444
|
-
|
|
445
|
-
const methods: InterfaceMethod[] = []
|
|
446
|
-
while (!this.check('}') && !this.check('eof')) {
|
|
447
|
-
const fnToken = this.expect('fn')
|
|
448
|
-
const methodName = this.expect('ident').value
|
|
449
|
-
this.expect('(')
|
|
450
|
-
const params = this.parseInterfaceParams()
|
|
451
|
-
this.expect(')')
|
|
452
|
-
let returnType: TypeNode | undefined
|
|
453
|
-
if (this.match(':')) {
|
|
454
|
-
returnType = this.parseType()
|
|
455
|
-
}
|
|
456
|
-
// No body — interface methods are signature-only
|
|
457
|
-
methods.push(this.withLoc({ name: methodName, params, returnType }, fnToken) as InterfaceMethod)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
this.expect('}')
|
|
461
|
-
return this.withLoc({ name, methods }, ifaceToken) as InterfaceDecl
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Parse interface method params — like parseParams but allows bare `self`
|
|
466
|
-
* (no `:` required for the first param named 'self').
|
|
467
|
-
*/
|
|
468
|
-
private parseInterfaceParams(): Param[] {
|
|
469
|
-
const params: Param[] = []
|
|
470
|
-
if (!this.check(')')) {
|
|
471
|
-
do {
|
|
472
|
-
const paramToken = this.expect('ident')
|
|
473
|
-
const paramName = paramToken.value
|
|
474
|
-
let type: TypeNode
|
|
475
|
-
if (params.length === 0 && paramName === 'self' && !this.check(':')) {
|
|
476
|
-
// self without type annotation — use a sentinel struct type
|
|
477
|
-
type = { kind: 'named', name: 'void' }
|
|
478
|
-
} else {
|
|
479
|
-
this.expect(':')
|
|
480
|
-
type = this.parseType()
|
|
481
|
-
}
|
|
482
|
-
params.push(this.withLoc({ name: paramName, type }, paramToken))
|
|
483
|
-
} while (this.match(','))
|
|
484
|
-
}
|
|
485
|
-
return params
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
private parseConstDecl(): ConstDecl {
|
|
489
|
-
const constToken = this.expect('const')
|
|
490
|
-
const name = this.expect('ident').value
|
|
491
|
-
let type: TypeNode | undefined
|
|
492
|
-
if (this.match(':')) {
|
|
493
|
-
type = this.parseType()
|
|
494
|
-
}
|
|
495
|
-
this.expect('=')
|
|
496
|
-
const value = this.parseLiteralExpr()
|
|
497
|
-
this.match(';')
|
|
498
|
-
// Infer type from value if not provided
|
|
499
|
-
const inferredType: TypeNode = type ?? (
|
|
500
|
-
value.kind === 'str_lit' ? { kind: 'named', name: 'string' } :
|
|
501
|
-
value.kind === 'bool_lit' ? { kind: 'named', name: 'bool' } :
|
|
502
|
-
value.kind === 'float_lit' ? { kind: 'named', name: 'fixed' } :
|
|
503
|
-
{ kind: 'named', name: 'int' }
|
|
504
|
-
)
|
|
505
|
-
return this.withLoc({ name, type: inferredType, value }, constToken)
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
private parseGlobalDecl(mutable: boolean): GlobalDecl {
|
|
509
|
-
const token = this.advance() // consume 'let'
|
|
510
|
-
const name = this.expect('ident').value
|
|
511
|
-
this.expect(':')
|
|
512
|
-
const type = this.parseType()
|
|
513
|
-
let init: Expr
|
|
514
|
-
if (this.match('=')) {
|
|
515
|
-
init = this.parseExpr()
|
|
516
|
-
} else {
|
|
517
|
-
// No init — valid only for @config-decorated globals (resolved later)
|
|
518
|
-
// Use a placeholder zero literal; will be replaced in compile step
|
|
519
|
-
init = { kind: 'int_lit', value: 0 }
|
|
520
|
-
}
|
|
521
|
-
this.match(';')
|
|
522
|
-
return this.withLoc({ kind: 'global', name, type, init, mutable }, token)
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// -------------------------------------------------------------------------
|
|
526
|
-
// Function Declaration
|
|
527
|
-
// -------------------------------------------------------------------------
|
|
528
|
-
|
|
529
|
-
/** Parse `export fn name(...)` — marks the function as exported (survives DCE). */
|
|
530
|
-
private parseExportedFnDecl(): FnDecl {
|
|
531
|
-
this.expect('export')
|
|
532
|
-
const fn = this.parseFnDecl()
|
|
533
|
-
fn.isExported = true
|
|
534
|
-
return fn
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
private parseFnDecl(implTypeName?: string): FnDecl {
|
|
538
|
-
const decorators = this.parseDecorators()
|
|
539
|
-
const watchObjective = decorators.find(decorator => decorator.name === 'watch')?.args?.objective
|
|
540
|
-
|
|
541
|
-
// Map @keep decorator to isExported flag (backward compat)
|
|
542
|
-
let isExported: boolean | undefined
|
|
543
|
-
const filteredDecorators = decorators.filter(d => {
|
|
544
|
-
if (d.name === 'keep') {
|
|
545
|
-
isExported = true
|
|
546
|
-
return false
|
|
547
|
-
}
|
|
548
|
-
return true
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
const fnToken = this.expect('fn')
|
|
552
|
-
const name = this.expect('ident').value
|
|
553
|
-
|
|
554
|
-
// Parse optional generic type parameters: fn max<T>(...)
|
|
555
|
-
let typeParams: string[] | undefined
|
|
556
|
-
if (this.check('<')) {
|
|
557
|
-
this.advance() // consume '<'
|
|
558
|
-
typeParams = []
|
|
559
|
-
do {
|
|
560
|
-
typeParams.push(this.expect('ident').value)
|
|
561
|
-
} while (this.match(','))
|
|
562
|
-
this.expect('>')
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
this.expect('(')
|
|
566
|
-
const params = this.parseParams(implTypeName)
|
|
567
|
-
this.expect(')')
|
|
568
|
-
|
|
569
|
-
let returnType: TypeNode = { kind: 'named', name: 'void' }
|
|
570
|
-
if (this.match('->') || this.match(':')) {
|
|
571
|
-
returnType = this.parseType()
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const body = this.parseBlock()
|
|
575
|
-
// Record the closing '}' line as endLine for accurate LSP scope detection
|
|
576
|
-
const closingBraceLine = this.tokens[this.pos - 1]?.line
|
|
577
|
-
|
|
578
|
-
const fn: import('../ast/types').FnDecl = this.withLoc(
|
|
579
|
-
{ name, typeParams, params, returnType, decorators: filteredDecorators, body,
|
|
580
|
-
isLibraryFn: this.inLibraryMode || undefined, isExported, watchObjective },
|
|
581
|
-
fnToken,
|
|
582
|
-
)
|
|
583
|
-
if (fn.span && closingBraceLine) fn.span.endLine = closingBraceLine
|
|
584
|
-
return fn
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
|
|
588
|
-
private parseDeclareStub(): void {
|
|
589
|
-
this.expect('fn')
|
|
590
|
-
this.expect('ident') // name
|
|
591
|
-
this.expect('(')
|
|
592
|
-
// consume params until ')'
|
|
593
|
-
let depth = 1
|
|
594
|
-
while (!this.check('eof') && depth > 0) {
|
|
595
|
-
const t = this.advance()
|
|
596
|
-
if (t.kind === '(') depth++
|
|
597
|
-
else if (t.kind === ')') depth--
|
|
598
|
-
}
|
|
599
|
-
// optional return type annotation `: type` or `-> type`
|
|
600
|
-
if (this.match(':') || this.match('->')) {
|
|
601
|
-
this.parseType()
|
|
602
|
-
}
|
|
603
|
-
this.match(';') // consume trailing semicolon
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
private parseDecorators(): Decorator[] {
|
|
607
|
-
const decorators: Decorator[] = []
|
|
608
|
-
|
|
609
|
-
while (this.check('decorator')) {
|
|
610
|
-
const token = this.advance()
|
|
611
|
-
const decorator = this.parseDecoratorValue(token.value)
|
|
612
|
-
decorators.push(decorator)
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return decorators
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
private parseDecoratorValue(value: string): Decorator {
|
|
619
|
-
// Parse @tick, @on(PlayerDeath), @on_trigger("name"), or @deprecated("msg with ) parens")
|
|
620
|
-
// Use a greedy match for args that allows any content inside the outermost parens.
|
|
621
|
-
const match = value.match(/^@([A-Za-z_][A-Za-z0-9_-]*)(?:\((.*)\))?$/s)
|
|
622
|
-
if (!match) {
|
|
623
|
-
this.error(`Invalid decorator: ${value}`)
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
const name = match[1] as Decorator['name']
|
|
627
|
-
const argsStr = match[2]
|
|
628
|
-
|
|
629
|
-
if (!argsStr) {
|
|
630
|
-
return { name }
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (name === 'profile' || name === 'benchmark' || name === 'memoize') {
|
|
634
|
-
this.error(`@${name} decorator does not accept arguments`)
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const args: Decorator['args'] = {}
|
|
638
|
-
|
|
639
|
-
if (name === 'on') {
|
|
640
|
-
const eventTypeMatch = argsStr.match(/^([A-Za-z_][A-Za-z0-9_]*)$/)
|
|
641
|
-
if (eventTypeMatch) {
|
|
642
|
-
args.eventType = eventTypeMatch[1]
|
|
643
|
-
return { name, args }
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Handle @watch("objective"), @on_trigger("name"), @on_advancement("id"), @on_craft("item"), @on_join_team("team")
|
|
648
|
-
if (name === 'watch' || name === 'on_trigger' || name === 'on_advancement' || name === 'on_craft' || name === 'on_join_team') {
|
|
649
|
-
const strMatch = argsStr.match(/^"([^"]*)"$/)
|
|
650
|
-
if (strMatch) {
|
|
651
|
-
if (name === 'watch') {
|
|
652
|
-
args.objective = strMatch[1]
|
|
653
|
-
} else if (name === 'on_trigger') {
|
|
654
|
-
args.trigger = strMatch[1]
|
|
655
|
-
} else if (name === 'on_advancement') {
|
|
656
|
-
args.advancement = strMatch[1]
|
|
657
|
-
} else if (name === 'on_craft') {
|
|
658
|
-
args.item = strMatch[1]
|
|
659
|
-
} else if (name === 'on_join_team') {
|
|
660
|
-
args.team = strMatch[1]
|
|
661
|
-
}
|
|
662
|
-
return { name, args }
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Handle @config("key", default: value)
|
|
667
|
-
if (name === 'config') {
|
|
668
|
-
// Format: @config("key_name", default: 42)
|
|
669
|
-
const configMatch = argsStr.match(/^"([^"]+)"\s*,\s*default\s*:\s*(-?\d+(?:\.\d+)?)$/)
|
|
670
|
-
if (configMatch) {
|
|
671
|
-
return { name, args: { configKey: configMatch[1], configDefault: parseFloat(configMatch[2]) } }
|
|
672
|
-
}
|
|
673
|
-
// Format: @config("key_name") — no default
|
|
674
|
-
const keyOnlyMatch = argsStr.match(/^"([^"]+)"$/)
|
|
675
|
-
if (keyOnlyMatch) {
|
|
676
|
-
return { name, args: { configKey: keyOnlyMatch[1] } }
|
|
677
|
-
}
|
|
678
|
-
this.error(`Invalid @config syntax. Expected: @config("key", default: value) or @config("key")`)
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// Handle @deprecated("message")
|
|
682
|
-
if (name === 'deprecated') {
|
|
683
|
-
const strMatch = argsStr.match(/^"([^"]*)"$/)
|
|
684
|
-
if (strMatch) {
|
|
685
|
-
return { name, args: { message: strMatch[1] } }
|
|
686
|
-
}
|
|
687
|
-
// @deprecated with no message string
|
|
688
|
-
return { name, args: {} }
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// @test("label") — marks a test function with a human-readable label
|
|
692
|
-
if (name === 'test') {
|
|
693
|
-
const strMatch = argsStr.match(/^"([^"]*)"$/)
|
|
694
|
-
if (strMatch) {
|
|
695
|
-
return { name, args: { testLabel: strMatch[1] } }
|
|
696
|
-
}
|
|
697
|
-
// @test with no label — use empty string
|
|
698
|
-
return { name, args: { testLabel: '' } }
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// @require_on_load(fn_name) — when this fn is used, fn_name is called from __load.
|
|
702
|
-
// Accepts bare identifiers (with optional leading _) or quoted strings.
|
|
703
|
-
if (name === 'require_on_load') {
|
|
704
|
-
const rawArgs: NonNullable<Decorator['rawArgs']> = []
|
|
705
|
-
for (const part of argsStr.split(',')) {
|
|
706
|
-
const trimmed = part.trim()
|
|
707
|
-
// Bare identifier: @require_on_load(_math_init)
|
|
708
|
-
const identMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)$/)
|
|
709
|
-
if (identMatch) {
|
|
710
|
-
rawArgs.push({ kind: 'string', value: identMatch[1] })
|
|
711
|
-
} else {
|
|
712
|
-
// Quoted string fallback: @require_on_load("_math_init")
|
|
713
|
-
const strMatch = trimmed.match(/^"([^"]*)"$/)
|
|
714
|
-
if (strMatch) {
|
|
715
|
-
rawArgs.push({ kind: 'string', value: strMatch[1] })
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
return { name, rawArgs }
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Handle key=value format (e.g., rate=20, batch=10, onDone=fn_name)
|
|
723
|
-
for (const part of argsStr.split(',')) {
|
|
724
|
-
const [key, val] = part.split('=').map(s => s.trim())
|
|
725
|
-
if (key === 'rate') {
|
|
726
|
-
args.rate = parseInt(val, 10)
|
|
727
|
-
} else if (key === 'ticks') {
|
|
728
|
-
args.ticks = parseInt(val, 10)
|
|
729
|
-
} else if (key === 'batch') {
|
|
730
|
-
args.batch = parseInt(val, 10)
|
|
731
|
-
} else if (key === 'onDone') {
|
|
732
|
-
args.onDone = val.replace(/^["']|["']$/g, '')
|
|
733
|
-
} else if (key === 'trigger') {
|
|
734
|
-
args.trigger = val
|
|
735
|
-
} else if (key === 'advancement') {
|
|
736
|
-
args.advancement = val
|
|
737
|
-
} else if (key === 'item') {
|
|
738
|
-
args.item = val
|
|
739
|
-
} else if (key === 'team') {
|
|
740
|
-
args.team = val
|
|
741
|
-
} else if (key === 'max') {
|
|
742
|
-
args.max = parseInt(val, 10)
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
return { name, args }
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
private parseParams(implTypeName?: string): Param[] {
|
|
750
|
-
const params: Param[] = []
|
|
751
|
-
|
|
752
|
-
if (!this.check(')')) {
|
|
753
|
-
do {
|
|
754
|
-
const paramToken = this.expect('ident')
|
|
755
|
-
const name = paramToken.value
|
|
756
|
-
let type: TypeNode
|
|
757
|
-
if (implTypeName && params.length === 0 && name === 'self' && !this.check(':')) {
|
|
758
|
-
type = { kind: 'struct', name: implTypeName }
|
|
759
|
-
} else {
|
|
760
|
-
this.expect(':')
|
|
761
|
-
type = this.parseType()
|
|
762
|
-
}
|
|
763
|
-
let defaultValue: Expr | undefined
|
|
764
|
-
if (this.match('=')) {
|
|
765
|
-
defaultValue = this.parseExpr()
|
|
766
|
-
}
|
|
767
|
-
params.push(this.withLoc({ name, type, default: defaultValue }, paramToken))
|
|
768
|
-
} while (this.match(','))
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
return params
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
private parseType(): TypeNode {
|
|
775
|
-
const token = this.peek()
|
|
776
|
-
let type: TypeNode
|
|
777
|
-
|
|
778
|
-
if (token.kind === '(') {
|
|
779
|
-
// Disambiguate: tuple type `(T, T)` vs function type `(T) -> R`
|
|
780
|
-
// Look ahead: parse elements, then check if '->' follows.
|
|
781
|
-
const saved = this.pos
|
|
782
|
-
this.advance() // consume '('
|
|
783
|
-
const elements: TypeNode[] = []
|
|
784
|
-
if (!this.check(')')) {
|
|
785
|
-
do {
|
|
786
|
-
elements.push(this.parseType())
|
|
787
|
-
} while (this.match(','))
|
|
788
|
-
}
|
|
789
|
-
this.expect(')')
|
|
790
|
-
if (this.check('->')) {
|
|
791
|
-
// It's a function type — restore and use existing parseFunctionType
|
|
792
|
-
this.pos = saved
|
|
793
|
-
return this.parseFunctionType()
|
|
794
|
-
}
|
|
795
|
-
// It's a tuple type
|
|
796
|
-
return { kind: 'tuple', elements }
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
if (token.kind === 'float') {
|
|
800
|
-
this.advance()
|
|
801
|
-
const filePart = this.filePath ? `${this.filePath}:` : ''
|
|
802
|
-
this.warnings.push(
|
|
803
|
-
`[DeprecatedType] ${filePart}line ${token.line}, col ${token.col}: 'float' is deprecated, use 'fixed' instead (×10000 fixed-point)`
|
|
804
|
-
)
|
|
805
|
-
type = { kind: 'named', name: 'float' }
|
|
806
|
-
} else if (token.kind === 'int' || token.kind === 'bool' ||
|
|
807
|
-
token.kind === 'fixed' || token.kind === 'string' || token.kind === 'void' ||
|
|
808
|
-
token.kind === 'BlockPos') {
|
|
809
|
-
this.advance()
|
|
810
|
-
type = { kind: 'named', name: token.kind }
|
|
811
|
-
} else if (token.kind === 'ident') {
|
|
812
|
-
this.advance()
|
|
813
|
-
if (token.value === 'selector' && this.check('<')) {
|
|
814
|
-
this.advance() // consume <
|
|
815
|
-
const entityType = this.expect('ident').value
|
|
816
|
-
this.expect('>')
|
|
817
|
-
type = { kind: 'selector', entityType }
|
|
818
|
-
} else if (token.value === 'selector') {
|
|
819
|
-
type = { kind: 'selector' }
|
|
820
|
-
} else if (token.value === 'Option' && this.check('<')) {
|
|
821
|
-
this.advance() // consume <
|
|
822
|
-
const inner = this.parseType()
|
|
823
|
-
this.expect('>')
|
|
824
|
-
type = { kind: 'option', inner }
|
|
825
|
-
} else if (token.value === 'double' || token.value === 'byte' ||
|
|
826
|
-
token.value === 'short' || token.value === 'long' ||
|
|
827
|
-
token.value === 'format_string') {
|
|
828
|
-
type = { kind: 'named', name: token.value as any }
|
|
829
|
-
} else {
|
|
830
|
-
type = { kind: 'struct', name: token.value }
|
|
831
|
-
}
|
|
832
|
-
} else {
|
|
833
|
-
this.error(`Expected type, got '${token.kind}'`)
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
while (this.match('[')) {
|
|
837
|
-
this.expect(']')
|
|
838
|
-
type = { kind: 'array', elem: type }
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
return type
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
private parseFunctionType(): TypeNode {
|
|
845
|
-
this.expect('(')
|
|
846
|
-
const params: TypeNode[] = []
|
|
847
|
-
|
|
848
|
-
if (!this.check(')')) {
|
|
849
|
-
do {
|
|
850
|
-
params.push(this.parseType())
|
|
851
|
-
} while (this.match(','))
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
this.expect(')')
|
|
855
|
-
this.expect('->')
|
|
856
|
-
const returnType = this.parseType()
|
|
857
|
-
return { kind: 'function_type', params, return: returnType }
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// -------------------------------------------------------------------------
|
|
861
|
-
// Block & Statements
|
|
862
|
-
// -------------------------------------------------------------------------
|
|
863
|
-
|
|
864
|
-
private parseBlock(): Block {
|
|
865
|
-
this.expect('{')
|
|
866
|
-
const stmts: Stmt[] = []
|
|
867
|
-
|
|
868
|
-
while (!this.check('}') && !this.check('eof')) {
|
|
869
|
-
try {
|
|
870
|
-
stmts.push(this.parseStmt())
|
|
871
|
-
} catch (err) {
|
|
872
|
-
if (err instanceof DiagnosticError) {
|
|
873
|
-
this.parseErrors.push(err)
|
|
874
|
-
this.syncToNextStmt()
|
|
875
|
-
} else {
|
|
876
|
-
throw err
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
this.expect('}')
|
|
882
|
-
return stmts
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
private parseStmt(): Stmt {
|
|
886
|
-
// Let statement
|
|
887
|
-
if (this.check('let')) {
|
|
888
|
-
return this.parseLetStmt()
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// Const declaration (local)
|
|
892
|
-
if (this.check('const')) {
|
|
893
|
-
return this.parseLocalConstDecl()
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Return statement
|
|
897
|
-
if (this.check('return')) {
|
|
898
|
-
return this.parseReturnStmt()
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// Break statement (with optional label: break outer)
|
|
902
|
-
if (this.check('break')) {
|
|
903
|
-
const token = this.advance()
|
|
904
|
-
// Check if next token is an identifier (label name)
|
|
905
|
-
if (this.check('ident')) {
|
|
906
|
-
const labelToken = this.advance()
|
|
907
|
-
this.match(';')
|
|
908
|
-
return this.withLoc({ kind: 'break_label', label: labelToken.value }, token)
|
|
909
|
-
}
|
|
910
|
-
this.match(';')
|
|
911
|
-
return this.withLoc({ kind: 'break' }, token)
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// Continue statement (with optional label: continue outer)
|
|
915
|
-
if (this.check('continue')) {
|
|
916
|
-
const token = this.advance()
|
|
917
|
-
// Check if next token is an identifier (label name)
|
|
918
|
-
if (this.check('ident')) {
|
|
919
|
-
const labelToken = this.advance()
|
|
920
|
-
this.match(';')
|
|
921
|
-
return this.withLoc({ kind: 'continue_label', label: labelToken.value }, token)
|
|
922
|
-
}
|
|
923
|
-
this.match(';')
|
|
924
|
-
return this.withLoc({ kind: 'continue' }, token)
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// If statement
|
|
928
|
-
if (this.check('if')) {
|
|
929
|
-
return this.parseIfStmt()
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Labeled loop: ident ':' (while|for|foreach|repeat)
|
|
933
|
-
if (this.check('ident') && this.peek(1).kind === ':') {
|
|
934
|
-
const labelToken = this.advance() // consume ident
|
|
935
|
-
const colonToken = this.advance() // consume ':'
|
|
936
|
-
// Now parse the loop body
|
|
937
|
-
let loopStmt: Stmt
|
|
938
|
-
if (this.check('while')) {
|
|
939
|
-
loopStmt = this.parseWhileStmt()
|
|
940
|
-
} else if (this.check('for')) {
|
|
941
|
-
loopStmt = this.parseForStmt()
|
|
942
|
-
} else if (this.check('foreach')) {
|
|
943
|
-
loopStmt = this.parseForeachStmt()
|
|
944
|
-
} else if (this.check('repeat')) {
|
|
945
|
-
loopStmt = this.parseRepeatStmt()
|
|
946
|
-
} else {
|
|
947
|
-
throw new DiagnosticError(
|
|
948
|
-
'ParseError',
|
|
949
|
-
`Expected loop statement after label '${labelToken.value}:', found '${this.peek().kind}'`,
|
|
950
|
-
{ line: labelToken.line, col: labelToken.col },
|
|
951
|
-
)
|
|
952
|
-
}
|
|
953
|
-
return this.withLoc({ kind: 'labeled_loop', label: labelToken.value, body: loopStmt }, labelToken)
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// While statement
|
|
957
|
-
if (this.check('while')) {
|
|
958
|
-
return this.parseWhileStmt()
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
// Do-while statement
|
|
962
|
-
if (this.check('do')) {
|
|
963
|
-
return this.parseDoWhileStmt()
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// Repeat N statement
|
|
967
|
-
if (this.check('repeat')) {
|
|
968
|
-
return this.parseRepeatStmt()
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// For statement
|
|
972
|
-
if (this.check('for')) {
|
|
973
|
-
return this.parseForStmt()
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// Foreach statement
|
|
977
|
-
if (this.check('foreach')) {
|
|
978
|
-
return this.parseForeachStmt()
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
if (this.check('match')) {
|
|
982
|
-
return this.parseMatchStmt()
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// As block
|
|
986
|
-
if (this.check('as')) {
|
|
987
|
-
return this.parseAsStmt()
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
// At block
|
|
991
|
-
if (this.check('at')) {
|
|
992
|
-
return this.parseAtStmt()
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
// Execute statement: execute as/at/if/unless/in ... run { }
|
|
996
|
-
if (this.check('execute')) {
|
|
997
|
-
return this.parseExecuteStmt()
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// Raw command
|
|
1001
|
-
if (this.check('raw_cmd')) {
|
|
1002
|
-
const token = this.advance()
|
|
1003
|
-
const cmd = token.value
|
|
1004
|
-
this.match(';') // optional semicolon (raw consumes it)
|
|
1005
|
-
return this.withLoc({ kind: 'raw', cmd }, token)
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Expression statement
|
|
1009
|
-
return this.parseExprStmt()
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
private parseLetStmt(): Stmt {
|
|
1013
|
-
const letToken = this.expect('let')
|
|
1014
|
-
|
|
1015
|
-
// Destructuring: let (a, b, c) = expr;
|
|
1016
|
-
if (this.check('(')) {
|
|
1017
|
-
this.advance() // consume '('
|
|
1018
|
-
const names: string[] = []
|
|
1019
|
-
do {
|
|
1020
|
-
names.push(this.expect('ident').value)
|
|
1021
|
-
} while (this.match(','))
|
|
1022
|
-
this.expect(')')
|
|
1023
|
-
let type: TypeNode | undefined
|
|
1024
|
-
if (this.match(':')) {
|
|
1025
|
-
type = this.parseType()
|
|
1026
|
-
}
|
|
1027
|
-
this.expect('=')
|
|
1028
|
-
const init = this.parseExpr()
|
|
1029
|
-
this.match(';')
|
|
1030
|
-
return this.withLoc({ kind: 'let_destruct', names, type, init }, letToken)
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
const name = this.expect('ident').value
|
|
1034
|
-
|
|
1035
|
-
let type: TypeNode | undefined
|
|
1036
|
-
if (this.match(':')) {
|
|
1037
|
-
type = this.parseType()
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
this.expect('=')
|
|
1041
|
-
const init = this.parseExpr()
|
|
1042
|
-
this.match(';')
|
|
1043
|
-
|
|
1044
|
-
return this.withLoc({ kind: 'let', name, type, init }, letToken)
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
private parseLocalConstDecl(): Stmt {
|
|
1048
|
-
const constToken = this.expect('const')
|
|
1049
|
-
const name = this.expect('ident').value
|
|
1050
|
-
this.expect(':')
|
|
1051
|
-
const type = this.parseType()
|
|
1052
|
-
this.expect('=')
|
|
1053
|
-
const value = this.parseExpr()
|
|
1054
|
-
this.match(';')
|
|
1055
|
-
return this.withLoc({ kind: 'const_decl', name, type, value }, constToken)
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
private parseReturnStmt(): Stmt {
|
|
1059
|
-
const returnToken = this.expect('return')
|
|
1060
|
-
|
|
1061
|
-
let value: Expr | undefined
|
|
1062
|
-
if (!this.check(';') && !this.check('}') && !this.check('eof')) {
|
|
1063
|
-
value = this.parseExpr()
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
this.match(';')
|
|
1067
|
-
return this.withLoc({ kind: 'return', value }, returnToken)
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
private parseIfStmt(): Stmt {
|
|
1071
|
-
const ifToken = this.expect('if')
|
|
1072
|
-
|
|
1073
|
-
// if let Some(x) = expr { ... }
|
|
1074
|
-
if (this.check('let') && this.peek(1).kind === 'ident' && this.peek(1).value === 'Some') {
|
|
1075
|
-
this.advance() // consume 'let'
|
|
1076
|
-
this.advance() // consume 'Some'
|
|
1077
|
-
this.expect('(')
|
|
1078
|
-
const binding = this.expect('ident').value
|
|
1079
|
-
this.expect(')')
|
|
1080
|
-
this.expect('=')
|
|
1081
|
-
const init = this.parseExpr()
|
|
1082
|
-
const then = this.parseBlock()
|
|
1083
|
-
|
|
1084
|
-
let else_: Block | undefined
|
|
1085
|
-
if (this.match('else')) {
|
|
1086
|
-
if (this.check('if')) {
|
|
1087
|
-
else_ = [this.parseIfStmt()]
|
|
1088
|
-
} else {
|
|
1089
|
-
else_ = this.parseBlock()
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
return this.withLoc({ kind: 'if_let_some', binding, init, then, else_ }, ifToken)
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const cond = this.parseParenOptionalCond()
|
|
1097
|
-
const then = this.parseBlock()
|
|
1098
|
-
|
|
1099
|
-
let else_: Block | undefined
|
|
1100
|
-
if (this.match('else')) {
|
|
1101
|
-
if (this.check('if')) {
|
|
1102
|
-
// else if
|
|
1103
|
-
else_ = [this.parseIfStmt()]
|
|
1104
|
-
} else {
|
|
1105
|
-
else_ = this.parseBlock()
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
return this.withLoc({ kind: 'if', cond, then, else_ }, ifToken)
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
private parseWhileStmt(): Stmt {
|
|
1113
|
-
const whileToken = this.expect('while')
|
|
1114
|
-
|
|
1115
|
-
// while let Some(x) = expr { ... }
|
|
1116
|
-
if (this.check('let') && this.peek(1).kind === 'ident' && this.peek(1).value === 'Some') {
|
|
1117
|
-
this.advance() // consume 'let'
|
|
1118
|
-
this.advance() // consume 'Some'
|
|
1119
|
-
this.expect('(')
|
|
1120
|
-
const binding = this.expect('ident').value
|
|
1121
|
-
this.expect(')')
|
|
1122
|
-
this.expect('=')
|
|
1123
|
-
const init = this.parseExpr()
|
|
1124
|
-
const body = this.parseBlock()
|
|
1125
|
-
return this.withLoc({ kind: 'while_let_some', binding, init, body }, whileToken)
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
const cond = this.parseParenOptionalCond()
|
|
1129
|
-
const body = this.parseBlock()
|
|
1130
|
-
|
|
1131
|
-
return this.withLoc({ kind: 'while', cond, body }, whileToken)
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
private parseDoWhileStmt(): Stmt {
|
|
1135
|
-
const doToken = this.expect('do')
|
|
1136
|
-
const body = this.parseBlock()
|
|
1137
|
-
this.expect('while')
|
|
1138
|
-
const cond = this.parseParenOptionalCond()
|
|
1139
|
-
this.match(';')
|
|
1140
|
-
return this.withLoc({ kind: 'do_while', cond, body }, doToken)
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
private parseRepeatStmt(): Stmt {
|
|
1144
|
-
const repeatToken = this.expect('repeat')
|
|
1145
|
-
const countToken = this.expect('int_lit')
|
|
1146
|
-
const count = parseInt(countToken.value, 10)
|
|
1147
|
-
const body = this.parseBlock()
|
|
1148
|
-
return this.withLoc({ kind: 'repeat', count, body }, repeatToken)
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
private parseParenOptionalCond(): Expr {
|
|
1152
|
-
if (this.match('(')) {
|
|
1153
|
-
const cond = this.parseExpr()
|
|
1154
|
-
this.expect(')')
|
|
1155
|
-
return cond
|
|
1156
|
-
}
|
|
1157
|
-
return this.parseExpr()
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
private parseForStmt(): Stmt {
|
|
1161
|
-
const forToken = this.expect('for')
|
|
1162
|
-
|
|
1163
|
-
// Check for for-range syntax: for <ident> in <range_lit> { ... }
|
|
1164
|
-
if (this.check('ident') && this.peek(1).kind === 'in') {
|
|
1165
|
-
return this.parseForRangeStmt(forToken)
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
this.expect('(')
|
|
1169
|
-
|
|
1170
|
-
// Detect for-in-array syntax: for ( let ident in ident , lenExpr ) { ... }
|
|
1171
|
-
if (this.check('let') && this.peek(1).kind === 'ident' && this.peek(2).kind === 'in' && this.peek(3).kind === 'ident' && this.peek(4).kind === ',') {
|
|
1172
|
-
this.advance() // consume 'let'
|
|
1173
|
-
const binding = this.expect('ident').value
|
|
1174
|
-
this.expect('in')
|
|
1175
|
-
const arrayName = this.expect('ident').value
|
|
1176
|
-
this.expect(',')
|
|
1177
|
-
const lenExpr = this.parseExpr()
|
|
1178
|
-
this.expect(')')
|
|
1179
|
-
const body = this.parseBlock()
|
|
1180
|
-
return this.withLoc({ kind: 'for_in_array', binding, arrayName, lenExpr, body }, forToken)
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Init: either let statement (without semicolon) or empty
|
|
1184
|
-
let init: Stmt | undefined
|
|
1185
|
-
if (this.check('let')) {
|
|
1186
|
-
// Parse let without consuming semicolon here (we handle it)
|
|
1187
|
-
const letToken = this.expect('let')
|
|
1188
|
-
const name = this.expect('ident').value
|
|
1189
|
-
let type: TypeNode | undefined
|
|
1190
|
-
if (this.match(':')) {
|
|
1191
|
-
type = this.parseType()
|
|
1192
|
-
}
|
|
1193
|
-
this.expect('=')
|
|
1194
|
-
const initExpr = this.parseExpr()
|
|
1195
|
-
const initStmt: Stmt = { kind: 'let', name, type, init: initExpr }
|
|
1196
|
-
init = this.withLoc(initStmt, letToken)
|
|
1197
|
-
}
|
|
1198
|
-
this.expect(';')
|
|
1199
|
-
|
|
1200
|
-
// Condition
|
|
1201
|
-
const cond = this.parseExpr()
|
|
1202
|
-
this.expect(';')
|
|
1203
|
-
|
|
1204
|
-
// Step expression
|
|
1205
|
-
const step = this.parseExpr()
|
|
1206
|
-
this.expect(')')
|
|
1207
|
-
|
|
1208
|
-
const body = this.parseBlock()
|
|
1209
|
-
|
|
1210
|
-
return this.withLoc({ kind: 'for', init, cond, step, body }, forToken)
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
private parseForRangeStmt(forToken: Token): Stmt {
|
|
1214
|
-
const varName = this.expect('ident').value
|
|
1215
|
-
this.expect('in')
|
|
1216
|
-
|
|
1217
|
-
let start: Expr
|
|
1218
|
-
let end: Expr
|
|
1219
|
-
let inclusive = false
|
|
1220
|
-
|
|
1221
|
-
if (this.check('range_lit')) {
|
|
1222
|
-
// Literal range: 0..10, 0..count, 0..=9, 0..=count
|
|
1223
|
-
const rangeToken = this.advance()
|
|
1224
|
-
const raw = rangeToken.value
|
|
1225
|
-
// Detect inclusive: ends with = after .. (e.g. "0..=" or "..=")
|
|
1226
|
-
const incl = raw.includes('..=')
|
|
1227
|
-
inclusive = incl
|
|
1228
|
-
const range = this.parseRangeValue(raw)
|
|
1229
|
-
start = this.withLoc({ kind: 'int_lit', value: range.min ?? 0 }, rangeToken)
|
|
1230
|
-
if (range.max !== null && range.max !== undefined) {
|
|
1231
|
-
// Fully numeric: 0..10 or 0..=9
|
|
1232
|
-
end = this.withLoc({ kind: 'int_lit', value: range.max }, rangeToken)
|
|
1233
|
-
} else {
|
|
1234
|
-
// Open-ended: "0.." or "0..=" — parse the end expression from next tokens
|
|
1235
|
-
end = this.parseUnaryExpr()
|
|
1236
|
-
}
|
|
1237
|
-
} else {
|
|
1238
|
-
// Dynamic range: expr..expr or expr..=expr
|
|
1239
|
-
// parseExpr stops before range_lit (not in BINARY_OPS), so this is safe
|
|
1240
|
-
const arrayOrStart = this.parseExpr()
|
|
1241
|
-
|
|
1242
|
-
// --- for_each detection: for item in arr { ... } ---
|
|
1243
|
-
// If after parsing the expression there is no range_lit, it's a for_each (array iteration)
|
|
1244
|
-
if (!this.check('range_lit')) {
|
|
1245
|
-
const body = this.parseBlock()
|
|
1246
|
-
return this.withLoc({ kind: 'for_each', binding: varName, array: arrayOrStart, body }, forToken)
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
start = arrayOrStart
|
|
1250
|
-
// Consume the range_lit token which should be ".." or "..="
|
|
1251
|
-
if (this.check('range_lit')) {
|
|
1252
|
-
const rangeOp = this.advance()
|
|
1253
|
-
inclusive = rangeOp.value.includes('=')
|
|
1254
|
-
// If the range_lit captured digits after .., use them as end
|
|
1255
|
-
const afterOp = rangeOp.value.replace(/^\.\.=?/, '')
|
|
1256
|
-
if (afterOp.length > 0) {
|
|
1257
|
-
end = this.withLoc({ kind: 'int_lit', value: parseInt(afterOp, 10) }, rangeOp)
|
|
1258
|
-
} else {
|
|
1259
|
-
end = this.parseExpr()
|
|
1260
|
-
}
|
|
1261
|
-
} else {
|
|
1262
|
-
this.error('Expected .. or ..= in for-range expression')
|
|
1263
|
-
start = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
|
|
1264
|
-
end = this.withLoc({ kind: 'int_lit', value: 0 }, this.peek())
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
const body = this.parseBlock()
|
|
1269
|
-
return this.withLoc({ kind: 'for_range', varName, start, end, inclusive, body }, forToken)
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
private parseForeachStmt(): Stmt {
|
|
1273
|
-
const foreachToken = this.expect('foreach')
|
|
1274
|
-
this.expect('(')
|
|
1275
|
-
const binding = this.expect('ident').value
|
|
1276
|
-
this.expect('in')
|
|
1277
|
-
const iterable = this.parseExpr()
|
|
1278
|
-
this.expect(')')
|
|
1279
|
-
|
|
1280
|
-
// Parse optional execute context modifiers (as, at, positioned, rotated, facing, etc.)
|
|
1281
|
-
let executeContext: string | undefined
|
|
1282
|
-
// Check for execute subcommand keywords
|
|
1283
|
-
const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align', 'on', 'summon']
|
|
1284
|
-
if (this.check('as') || this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
|
|
1285
|
-
// Collect everything until we hit '{'
|
|
1286
|
-
let context = ''
|
|
1287
|
-
while (!this.check('{') && !this.check('eof')) {
|
|
1288
|
-
context += this.advance().value + ' '
|
|
1289
|
-
}
|
|
1290
|
-
executeContext = context.trim()
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
const body = this.parseBlock()
|
|
1294
|
-
|
|
1295
|
-
return this.withLoc({ kind: 'foreach', binding, iterable, body, executeContext }, foreachToken)
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
private parseMatchPattern(): MatchPattern {
|
|
1299
|
-
// Wildcard: _
|
|
1300
|
-
if (this.check('ident') && this.peek().value === '_') {
|
|
1301
|
-
this.advance()
|
|
1302
|
-
return { kind: 'PatWild' }
|
|
1303
|
-
}
|
|
1304
|
-
// None
|
|
1305
|
-
if (this.check('ident') && this.peek().value === 'None') {
|
|
1306
|
-
this.advance()
|
|
1307
|
-
return { kind: 'PatNone' }
|
|
1308
|
-
}
|
|
1309
|
-
// Some(x)
|
|
1310
|
-
if (this.check('ident') && this.peek().value === 'Some') {
|
|
1311
|
-
this.advance() // consume 'Some'
|
|
1312
|
-
this.expect('(')
|
|
1313
|
-
const binding = this.expect('ident').value
|
|
1314
|
-
this.expect(')')
|
|
1315
|
-
return { kind: 'PatSome', binding }
|
|
1316
|
-
}
|
|
1317
|
-
// Enum pattern: EnumName::Variant or EnumName::Variant(b1, b2, ...)
|
|
1318
|
-
if (this.check('ident') && this.peek(1).kind === '::') {
|
|
1319
|
-
const enumName = this.advance().value
|
|
1320
|
-
this.expect('::')
|
|
1321
|
-
const variant = this.expect('ident').value
|
|
1322
|
-
const bindings: string[] = []
|
|
1323
|
-
if (this.check('(')) {
|
|
1324
|
-
this.advance() // consume '('
|
|
1325
|
-
while (!this.check(')') && !this.check('eof')) {
|
|
1326
|
-
bindings.push(this.expect('ident').value)
|
|
1327
|
-
if (!this.match(',')) break
|
|
1328
|
-
}
|
|
1329
|
-
this.expect(')')
|
|
1330
|
-
}
|
|
1331
|
-
return { kind: 'PatEnum', enumName, variant, bindings }
|
|
1332
|
-
}
|
|
1333
|
-
// Integer literal
|
|
1334
|
-
if (this.check('int_lit')) {
|
|
1335
|
-
const tok = this.advance()
|
|
1336
|
-
return { kind: 'PatInt', value: parseInt(tok.value, 10) }
|
|
1337
|
-
}
|
|
1338
|
-
// Negative integer literal: -N
|
|
1339
|
-
if (this.check('-') && this.peek(1).kind === 'int_lit') {
|
|
1340
|
-
this.advance() // consume '-'
|
|
1341
|
-
const tok = this.advance()
|
|
1342
|
-
return { kind: 'PatInt', value: -parseInt(tok.value, 10) }
|
|
1343
|
-
}
|
|
1344
|
-
// Legacy: range_lit or any other expression (e.g. 0..59)
|
|
1345
|
-
const e = this.parseExpr()
|
|
1346
|
-
return { kind: 'PatExpr', expr: e }
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
private parseMatchStmt(): Stmt {
|
|
1350
|
-
const matchToken = this.expect('match')
|
|
1351
|
-
|
|
1352
|
-
// Support both `match (expr)` (legacy) and `match expr` (new syntax)
|
|
1353
|
-
let expr: Expr
|
|
1354
|
-
if (this.check('(')) {
|
|
1355
|
-
// Peek ahead — if it looks like `(expr)` followed by `{`, consume parens
|
|
1356
|
-
this.advance() // consume '('
|
|
1357
|
-
expr = this.parseExpr()
|
|
1358
|
-
this.expect(')')
|
|
1359
|
-
} else {
|
|
1360
|
-
expr = this.parseExpr()
|
|
1361
|
-
}
|
|
1362
|
-
this.expect('{')
|
|
1363
|
-
|
|
1364
|
-
const arms: Array<{ pattern: MatchPattern; body: Block }> = []
|
|
1365
|
-
while (!this.check('}') && !this.check('eof')) {
|
|
1366
|
-
const pattern = this.parseMatchPattern()
|
|
1367
|
-
this.expect('=>')
|
|
1368
|
-
const body = this.parseBlock()
|
|
1369
|
-
this.match(',') // optional trailing comma
|
|
1370
|
-
arms.push({ pattern, body })
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
this.expect('}')
|
|
1374
|
-
return this.withLoc({ kind: 'match', expr, arms }, matchToken)
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
private parseAsStmt(): Stmt {
|
|
1378
|
-
const asToken = this.expect('as')
|
|
1379
|
-
const as_sel = this.parseSelector()
|
|
1380
|
-
|
|
1381
|
-
// Check for combined as/at
|
|
1382
|
-
if (this.match('at')) {
|
|
1383
|
-
const at_sel = this.parseSelector()
|
|
1384
|
-
const body = this.parseBlock()
|
|
1385
|
-
return this.withLoc({ kind: 'as_at', as_sel, at_sel, body }, asToken)
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
const body = this.parseBlock()
|
|
1389
|
-
return this.withLoc({ kind: 'as_block', selector: as_sel, body }, asToken)
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
private parseAtStmt(): Stmt {
|
|
1393
|
-
const atToken = this.expect('at')
|
|
1394
|
-
const selector = this.parseSelector()
|
|
1395
|
-
const body = this.parseBlock()
|
|
1396
|
-
return this.withLoc({ kind: 'at_block', selector, body }, atToken)
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
private parseExecuteStmt(): Stmt {
|
|
1400
|
-
const executeToken = this.expect('execute')
|
|
1401
|
-
const subcommands: ExecuteSubcommand[] = []
|
|
1402
|
-
|
|
1403
|
-
// Parse subcommands until we hit 'run'
|
|
1404
|
-
while (!this.check('run') && !this.check('eof')) {
|
|
1405
|
-
if (this.match('as')) {
|
|
1406
|
-
const selector = this.parseSelector()
|
|
1407
|
-
subcommands.push({ kind: 'as', selector })
|
|
1408
|
-
} else if (this.match('at')) {
|
|
1409
|
-
const selector = this.parseSelector()
|
|
1410
|
-
subcommands.push({ kind: 'at', selector })
|
|
1411
|
-
} else if (this.checkIdent('positioned')) {
|
|
1412
|
-
this.advance()
|
|
1413
|
-
if (this.match('as')) {
|
|
1414
|
-
const selector = this.parseSelector()
|
|
1415
|
-
subcommands.push({ kind: 'positioned_as', selector })
|
|
1416
|
-
} else {
|
|
1417
|
-
const x = this.parseCoordToken()
|
|
1418
|
-
const y = this.parseCoordToken()
|
|
1419
|
-
const z = this.parseCoordToken()
|
|
1420
|
-
subcommands.push({ kind: 'positioned', x, y, z })
|
|
1421
|
-
}
|
|
1422
|
-
} else if (this.checkIdent('rotated')) {
|
|
1423
|
-
this.advance()
|
|
1424
|
-
if (this.match('as')) {
|
|
1425
|
-
const selector = this.parseSelector()
|
|
1426
|
-
subcommands.push({ kind: 'rotated_as', selector })
|
|
1427
|
-
} else {
|
|
1428
|
-
const yaw = this.parseCoordToken()
|
|
1429
|
-
const pitch = this.parseCoordToken()
|
|
1430
|
-
subcommands.push({ kind: 'rotated', yaw, pitch })
|
|
1431
|
-
}
|
|
1432
|
-
} else if (this.checkIdent('facing')) {
|
|
1433
|
-
this.advance()
|
|
1434
|
-
if (this.checkIdent('entity')) {
|
|
1435
|
-
this.advance()
|
|
1436
|
-
const selector = this.parseSelector()
|
|
1437
|
-
const anchor = this.checkIdent('eyes') || this.checkIdent('feet') ? this.advance().value as 'eyes' | 'feet' : 'feet'
|
|
1438
|
-
subcommands.push({ kind: 'facing_entity', selector, anchor })
|
|
1439
|
-
} else {
|
|
1440
|
-
const x = this.parseCoordToken()
|
|
1441
|
-
const y = this.parseCoordToken()
|
|
1442
|
-
const z = this.parseCoordToken()
|
|
1443
|
-
subcommands.push({ kind: 'facing', x, y, z })
|
|
1444
|
-
}
|
|
1445
|
-
} else if (this.checkIdent('anchored')) {
|
|
1446
|
-
this.advance()
|
|
1447
|
-
const anchor = this.advance().value as 'eyes' | 'feet'
|
|
1448
|
-
subcommands.push({ kind: 'anchored', anchor })
|
|
1449
|
-
} else if (this.checkIdent('align')) {
|
|
1450
|
-
this.advance()
|
|
1451
|
-
const axes = this.advance().value
|
|
1452
|
-
subcommands.push({ kind: 'align', axes })
|
|
1453
|
-
} else if (this.checkIdent('on')) {
|
|
1454
|
-
this.advance()
|
|
1455
|
-
const relation = this.advance().value
|
|
1456
|
-
subcommands.push({ kind: 'on', relation })
|
|
1457
|
-
} else if (this.checkIdent('summon')) {
|
|
1458
|
-
this.advance()
|
|
1459
|
-
const entity = this.advance().value
|
|
1460
|
-
subcommands.push({ kind: 'summon', entity })
|
|
1461
|
-
} else if (this.checkIdent('store')) {
|
|
1462
|
-
this.advance()
|
|
1463
|
-
const storeType = this.advance().value // 'result' or 'success'
|
|
1464
|
-
if (this.checkIdent('score')) {
|
|
1465
|
-
this.advance()
|
|
1466
|
-
const target = this.advance().value
|
|
1467
|
-
const targetObj = this.advance().value
|
|
1468
|
-
if (storeType === 'result') {
|
|
1469
|
-
subcommands.push({ kind: 'store_result', target, targetObj })
|
|
1470
|
-
} else {
|
|
1471
|
-
subcommands.push({ kind: 'store_success', target, targetObj })
|
|
1472
|
-
}
|
|
1473
|
-
} else {
|
|
1474
|
-
this.error('store currently only supports score target')
|
|
1475
|
-
}
|
|
1476
|
-
} else if (this.match('if')) {
|
|
1477
|
-
this.parseExecuteCondition(subcommands, 'if')
|
|
1478
|
-
} else if (this.match('unless')) {
|
|
1479
|
-
this.parseExecuteCondition(subcommands, 'unless')
|
|
1480
|
-
} else if (this.match('in')) {
|
|
1481
|
-
// Dimension can be namespaced: minecraft:the_nether
|
|
1482
|
-
let dim = this.advance().value
|
|
1483
|
-
if (this.match(':')) {
|
|
1484
|
-
dim += ':' + this.advance().value
|
|
1485
|
-
}
|
|
1486
|
-
subcommands.push({ kind: 'in', dimension: dim })
|
|
1487
|
-
} else {
|
|
1488
|
-
this.error(`Unexpected token in execute statement: ${this.peek().kind} (${this.peek().value})`)
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
this.expect('run')
|
|
1493
|
-
const body = this.parseBlock()
|
|
1494
|
-
|
|
1495
|
-
return this.withLoc({ kind: 'execute', subcommands, body }, executeToken)
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
private parseExecuteCondition(subcommands: ExecuteSubcommand[], type: 'if' | 'unless'): void {
|
|
1499
|
-
if (this.checkIdent('entity') || this.check('selector')) {
|
|
1500
|
-
if (this.checkIdent('entity')) this.advance()
|
|
1501
|
-
const selectorOrVar = this.parseSelectorOrVarSelector()
|
|
1502
|
-
subcommands.push({ kind: type === 'if' ? 'if_entity' : 'unless_entity', ...selectorOrVar })
|
|
1503
|
-
} else if (this.checkIdent('block')) {
|
|
1504
|
-
this.advance()
|
|
1505
|
-
const x = this.parseCoordToken()
|
|
1506
|
-
const y = this.parseCoordToken()
|
|
1507
|
-
const z = this.parseCoordToken()
|
|
1508
|
-
const block = this.parseBlockId()
|
|
1509
|
-
subcommands.push({ kind: type === 'if' ? 'if_block' : 'unless_block', pos: [x, y, z], block })
|
|
1510
|
-
} else if (this.checkIdent('score')) {
|
|
1511
|
-
this.advance()
|
|
1512
|
-
const target = this.advance().value
|
|
1513
|
-
const targetObj = this.advance().value
|
|
1514
|
-
// Check for range or comparison
|
|
1515
|
-
if (this.checkIdent('matches')) {
|
|
1516
|
-
this.advance()
|
|
1517
|
-
const range = this.advance().value
|
|
1518
|
-
subcommands.push({ kind: type === 'if' ? 'if_score_range' : 'unless_score_range', target, targetObj, range })
|
|
1519
|
-
} else {
|
|
1520
|
-
const op = this.advance().value // <, <=, =, >=, >
|
|
1521
|
-
const source = this.advance().value
|
|
1522
|
-
const sourceObj = this.advance().value
|
|
1523
|
-
subcommands.push({
|
|
1524
|
-
kind: type === 'if' ? 'if_score' : 'unless_score',
|
|
1525
|
-
target, targetObj, op, source, sourceObj
|
|
1526
|
-
})
|
|
1527
|
-
}
|
|
1528
|
-
} else {
|
|
1529
|
-
this.error(`Unknown condition type after ${type}`)
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
private parseCoordToken(): string {
|
|
1534
|
-
// Handle ~, ^, numbers, relative coords like ~5, ^-3
|
|
1535
|
-
const token = this.peek()
|
|
1536
|
-
if (token.kind === 'rel_coord' || token.kind === 'local_coord' ||
|
|
1537
|
-
token.kind === 'int_lit' || token.kind === 'float_lit' ||
|
|
1538
|
-
token.kind === '-' || token.kind === 'ident') {
|
|
1539
|
-
return this.advance().value
|
|
1540
|
-
}
|
|
1541
|
-
this.error(`Expected coordinate, got ${token.kind}`)
|
|
1542
|
-
return '~'
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
private parseBlockId(): string {
|
|
1546
|
-
// Parse block ID like minecraft:stone or stone
|
|
1547
|
-
let id = this.advance().value
|
|
1548
|
-
if (this.match(':')) {
|
|
1549
|
-
id += ':' + this.advance().value
|
|
1550
|
-
}
|
|
1551
|
-
// Handle block states [facing=north]
|
|
1552
|
-
if (this.check('[')) {
|
|
1553
|
-
id += this.advance().value // [
|
|
1554
|
-
while (!this.check(']') && !this.check('eof')) {
|
|
1555
|
-
id += this.advance().value
|
|
1556
|
-
}
|
|
1557
|
-
id += this.advance().value // ]
|
|
1558
|
-
}
|
|
1559
|
-
return id
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
private checkIdent(value: string): boolean {
|
|
1563
|
-
return this.check('ident') && this.peek().value === value
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
private parseExprStmt(): Stmt {
|
|
1567
|
-
const expr = this.parseExpr()
|
|
1568
|
-
this.match(';')
|
|
1569
|
-
const exprToken = this.getLocToken(expr) ?? this.peek()
|
|
1570
|
-
return this.withLoc({ kind: 'expr', expr }, exprToken)
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// -------------------------------------------------------------------------
|
|
1574
|
-
// Expressions (Precedence Climbing)
|
|
1575
|
-
// -------------------------------------------------------------------------
|
|
1576
|
-
|
|
1577
|
-
private parseExpr(): Expr {
|
|
1578
|
-
return this.parseAssignment()
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
private parseAssignment(): Expr {
|
|
1582
|
-
const left = this.parseBinaryExpr(1)
|
|
1583
|
-
|
|
1584
|
-
// Check for assignment
|
|
1585
|
-
const token = this.peek()
|
|
1586
|
-
if (token.kind === '=' || token.kind === '+=' || token.kind === '-=' ||
|
|
1587
|
-
token.kind === '*=' || token.kind === '/=' || token.kind === '%=') {
|
|
1588
|
-
const op = this.advance().kind as AssignOp
|
|
1589
|
-
|
|
1590
|
-
if (left.kind === 'ident') {
|
|
1591
|
-
const value = this.parseAssignment()
|
|
1592
|
-
return this.withLoc({ kind: 'assign', target: left.name, op, value }, this.getLocToken(left) ?? token)
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
// Member assignment: p.x = 10, p.x += 5
|
|
1596
|
-
if (left.kind === 'member') {
|
|
1597
|
-
const value = this.parseAssignment()
|
|
1598
|
-
return this.withLoc(
|
|
1599
|
-
{ kind: 'member_assign', obj: left.obj, field: left.field, op, value },
|
|
1600
|
-
this.getLocToken(left) ?? token
|
|
1601
|
-
)
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
// Index assignment: arr[0] = val, arr[i] = val
|
|
1605
|
-
if (left.kind === 'index') {
|
|
1606
|
-
const value = this.parseAssignment()
|
|
1607
|
-
return this.withLoc(
|
|
1608
|
-
{ kind: 'index_assign', obj: left.obj, index: left.index, op, value },
|
|
1609
|
-
this.getLocToken(left) ?? token
|
|
1610
|
-
)
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
return left
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
private parseBinaryExpr(minPrec: number): Expr {
|
|
1618
|
-
let left = this.parseUnaryExpr()
|
|
1619
|
-
|
|
1620
|
-
while (true) {
|
|
1621
|
-
const op = this.peek().kind
|
|
1622
|
-
if (!BINARY_OPS.has(op)) break
|
|
1623
|
-
|
|
1624
|
-
const prec = PRECEDENCE[op]
|
|
1625
|
-
if (prec < minPrec) break
|
|
1626
|
-
|
|
1627
|
-
const opToken = this.advance()
|
|
1628
|
-
if (op === 'is') {
|
|
1629
|
-
const entityType = this.parseEntityTypeName()
|
|
1630
|
-
left = this.withLoc(
|
|
1631
|
-
{ kind: 'is_check', expr: left, entityType },
|
|
1632
|
-
this.getLocToken(left) ?? opToken
|
|
1633
|
-
)
|
|
1634
|
-
continue
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
const right = this.parseBinaryExpr(prec + 1) // left associative
|
|
1638
|
-
left = this.withLoc(
|
|
1639
|
-
{ kind: 'binary', op: op as BinOp | CmpOp | '&&' | '||', left, right },
|
|
1640
|
-
this.getLocToken(left) ?? opToken
|
|
1641
|
-
)
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
return left
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
private parseUnaryExpr(): Expr {
|
|
1648
|
-
if (this.match('!')) {
|
|
1649
|
-
const bangToken = this.tokens[this.pos - 1]
|
|
1650
|
-
const operand = this.parseUnaryExpr()
|
|
1651
|
-
return this.withLoc({ kind: 'unary', op: '!', operand }, bangToken)
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
if (this.check('-') && !this.isSubtraction()) {
|
|
1655
|
-
const minusToken = this.advance()
|
|
1656
|
-
const operand = this.parseUnaryExpr()
|
|
1657
|
-
return this.withLoc({ kind: 'unary', op: '-', operand }, minusToken)
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
return this.parsePostfixExpr()
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
private parseEntityTypeName(): EntityTypeName {
|
|
1664
|
-
const token = this.expect('ident')
|
|
1665
|
-
if (ENTITY_TYPE_NAMES.has(token.value as EntityTypeName)) {
|
|
1666
|
-
return token.value as EntityTypeName
|
|
1667
|
-
}
|
|
1668
|
-
this.error(`Unknown entity type '${token.value}'`)
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
private isSubtraction(): boolean {
|
|
1672
|
-
// Check if this minus is binary (subtraction) by looking at previous token
|
|
1673
|
-
// If previous was a value (literal, ident, ), ]) it's subtraction
|
|
1674
|
-
if (this.pos === 0) return false
|
|
1675
|
-
const prev = this.tokens[this.pos - 1]
|
|
1676
|
-
return ['int_lit', 'float_lit', 'ident', ')', ']'].includes(prev.kind)
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
/**
|
|
1680
|
-
* Try to parse `<Type, ...>` as explicit generic type arguments.
|
|
1681
|
-
* Returns the parsed type list if successful, null if this looks like a comparison.
|
|
1682
|
-
* Does NOT consume any tokens if it returns null.
|
|
1683
|
-
*/
|
|
1684
|
-
private tryParseTypeArgs(): import('../ast/types').TypeNode[] | null {
|
|
1685
|
-
const saved = this.pos
|
|
1686
|
-
this.advance() // consume '<'
|
|
1687
|
-
const typeArgs: import('../ast/types').TypeNode[] = []
|
|
1688
|
-
try {
|
|
1689
|
-
do {
|
|
1690
|
-
typeArgs.push(this.parseType())
|
|
1691
|
-
} while (this.match(','))
|
|
1692
|
-
if (!this.check('>')) {
|
|
1693
|
-
this.pos = saved
|
|
1694
|
-
return null
|
|
1695
|
-
}
|
|
1696
|
-
this.advance() // consume '>'
|
|
1697
|
-
return typeArgs
|
|
1698
|
-
} catch {
|
|
1699
|
-
this.pos = saved
|
|
1700
|
-
return null
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
private parsePostfixExpr(): Expr {
|
|
1705
|
-
let expr = this.parsePrimaryExpr()
|
|
1706
|
-
|
|
1707
|
-
while (true) {
|
|
1708
|
-
// Generic call: ident<Type>(args) — check before regular '(' handling
|
|
1709
|
-
if (expr.kind === 'ident' && this.check('<')) {
|
|
1710
|
-
const typeArgs = this.tryParseTypeArgs()
|
|
1711
|
-
if (typeArgs !== null && this.check('(')) {
|
|
1712
|
-
const openParenToken = this.peek()
|
|
1713
|
-
this.advance() // consume '('
|
|
1714
|
-
const args = this.parseArgs()
|
|
1715
|
-
this.expect(')')
|
|
1716
|
-
expr = this.withLoc(
|
|
1717
|
-
{ kind: 'call', fn: expr.name, args, typeArgs },
|
|
1718
|
-
this.getLocToken(expr) ?? openParenToken
|
|
1719
|
-
)
|
|
1720
|
-
continue
|
|
1721
|
-
}
|
|
1722
|
-
// Not a generic call — fall through to normal expression handling
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
// Function call
|
|
1726
|
-
if (this.match('(')) {
|
|
1727
|
-
const openParenToken = this.tokens[this.pos - 1]
|
|
1728
|
-
if (expr.kind === 'ident') {
|
|
1729
|
-
const args = this.parseArgs()
|
|
1730
|
-
this.expect(')')
|
|
1731
|
-
expr = this.withLoc({ kind: 'call', fn: expr.name, args }, this.getLocToken(expr) ?? openParenToken)
|
|
1732
|
-
continue
|
|
1733
|
-
}
|
|
1734
|
-
// Member call: entity.tag("name") → __entity_tag(entity, "name")
|
|
1735
|
-
// Also handle arr.push(val) and arr.length
|
|
1736
|
-
if (expr.kind === 'member') {
|
|
1737
|
-
// Option.unwrap_or(default) → unwrap_or AST node
|
|
1738
|
-
if (expr.field === 'unwrap_or') {
|
|
1739
|
-
const defaultExpr = this.parseExpr()
|
|
1740
|
-
this.expect(')')
|
|
1741
|
-
expr = this.withLoc(
|
|
1742
|
-
{ kind: 'unwrap_or', opt: expr.obj, default_: defaultExpr },
|
|
1743
|
-
this.getLocToken(expr) ?? openParenToken
|
|
1744
|
-
)
|
|
1745
|
-
continue
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
const methodMap: Record<string, string> = {
|
|
1749
|
-
'tag': '__entity_tag',
|
|
1750
|
-
'untag': '__entity_untag',
|
|
1751
|
-
'has_tag': '__entity_has_tag',
|
|
1752
|
-
'push': '__array_push',
|
|
1753
|
-
'pop': '__array_pop',
|
|
1754
|
-
'add': 'set_add',
|
|
1755
|
-
'contains': 'set_contains',
|
|
1756
|
-
'remove': 'set_remove',
|
|
1757
|
-
'clear': 'set_clear',
|
|
1758
|
-
}
|
|
1759
|
-
const internalFn = methodMap[expr.field]
|
|
1760
|
-
if (internalFn) {
|
|
1761
|
-
const args = this.parseArgs()
|
|
1762
|
-
this.expect(')')
|
|
1763
|
-
expr = this.withLoc(
|
|
1764
|
-
{ kind: 'call', fn: internalFn, args: [expr.obj, ...args] },
|
|
1765
|
-
this.getLocToken(expr) ?? openParenToken
|
|
1766
|
-
)
|
|
1767
|
-
continue
|
|
1768
|
-
}
|
|
1769
|
-
// Generic method sugar: obj.method(args) → method(obj, args)
|
|
1770
|
-
const args = this.parseArgs()
|
|
1771
|
-
this.expect(')')
|
|
1772
|
-
expr = this.withLoc(
|
|
1773
|
-
{ kind: 'call', fn: expr.field, args: [expr.obj, ...args] },
|
|
1774
|
-
this.getLocToken(expr) ?? openParenToken
|
|
1775
|
-
)
|
|
1776
|
-
continue
|
|
1777
|
-
}
|
|
1778
|
-
const args = this.parseArgs()
|
|
1779
|
-
this.expect(')')
|
|
1780
|
-
expr = this.withLoc(
|
|
1781
|
-
{ kind: 'invoke', callee: expr, args },
|
|
1782
|
-
this.getLocToken(expr) ?? openParenToken
|
|
1783
|
-
)
|
|
1784
|
-
continue
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// Array index access: arr[0]
|
|
1788
|
-
if (this.match('[')) {
|
|
1789
|
-
const index = this.parseExpr()
|
|
1790
|
-
this.expect(']')
|
|
1791
|
-
expr = this.withLoc(
|
|
1792
|
-
{ kind: 'index', obj: expr, index },
|
|
1793
|
-
this.getLocToken(expr) ?? this.tokens[this.pos - 1]
|
|
1794
|
-
)
|
|
1795
|
-
continue
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// Member access
|
|
1799
|
-
if (this.match('.')) {
|
|
1800
|
-
const field = this.expect('ident').value
|
|
1801
|
-
expr = this.withLoc(
|
|
1802
|
-
{ kind: 'member', obj: expr, field },
|
|
1803
|
-
this.getLocToken(expr) ?? this.tokens[this.pos - 1]
|
|
1804
|
-
)
|
|
1805
|
-
continue
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
// Type cast: expr as Type
|
|
1809
|
-
// Only parse 'as' as a cast when followed by a type token (not a selector like @a)
|
|
1810
|
-
if (this.check('as') && this.isTypeCastAs()) {
|
|
1811
|
-
const asToken = this.advance() // consume 'as'
|
|
1812
|
-
const targetType = this.parseType()
|
|
1813
|
-
expr = this.withLoc(
|
|
1814
|
-
{ kind: 'type_cast', expr, targetType },
|
|
1815
|
-
this.getLocToken(expr) ?? asToken
|
|
1816
|
-
)
|
|
1817
|
-
continue
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
break
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
return expr
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
/** Returns true if the current 'as' token is a type cast (not a context block) */
|
|
1827
|
-
private isTypeCastAs(): boolean {
|
|
1828
|
-
// Look ahead past 'as' to see if the next token looks like a type
|
|
1829
|
-
const next = this.tokens[this.pos + 1]
|
|
1830
|
-
if (!next) return false
|
|
1831
|
-
const typeStartTokens = new Set(['int', 'bool', 'float', 'fixed', 'string', 'void', 'BlockPos', '('])
|
|
1832
|
-
if (typeStartTokens.has(next.kind)) return true
|
|
1833
|
-
if (next.kind === 'ident' && (
|
|
1834
|
-
next.value === 'double' || next.value === 'byte' || next.value === 'short' ||
|
|
1835
|
-
next.value === 'long' || next.value === 'selector' || next.value === 'Option'
|
|
1836
|
-
)) return true
|
|
1837
|
-
return false
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
private parseArgs(): Expr[] {
|
|
1841
|
-
const args: Expr[] = []
|
|
1842
|
-
|
|
1843
|
-
if (!this.check(')')) {
|
|
1844
|
-
do {
|
|
1845
|
-
args.push(this.parseExpr())
|
|
1846
|
-
} while (this.match(','))
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
return args
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
private parsePrimaryExpr(): Expr {
|
|
1853
|
-
const token = this.peek()
|
|
1854
|
-
|
|
1855
|
-
if (token.kind === 'ident' && this.peek(1).kind === '::') {
|
|
1856
|
-
const typeToken = this.advance()
|
|
1857
|
-
this.expect('::')
|
|
1858
|
-
const memberToken = this.expect('ident')
|
|
1859
|
-
if (this.check('(')) {
|
|
1860
|
-
// Peek inside: if first non-'(' token is `ident :` it's enum construction with named args.
|
|
1861
|
-
// We only treat it as enum_construct when there are actual named args (not empty parens),
|
|
1862
|
-
// because empty `()` is ambiguous and most commonly means a static method call.
|
|
1863
|
-
const isNamedArgs = this.peek(1).kind === 'ident' && this.peek(2).kind === ':'
|
|
1864
|
-
if (isNamedArgs) {
|
|
1865
|
-
// Enum variant construction: EnumName::Variant(field: expr, ...)
|
|
1866
|
-
this.advance() // consume '('
|
|
1867
|
-
const args: { name: string; value: Expr }[] = []
|
|
1868
|
-
while (!this.check(')') && !this.check('eof')) {
|
|
1869
|
-
const fieldName = this.expect('ident').value
|
|
1870
|
-
this.expect(':')
|
|
1871
|
-
const value = this.parseExpr()
|
|
1872
|
-
args.push({ name: fieldName, value })
|
|
1873
|
-
if (!this.match(',')) break
|
|
1874
|
-
}
|
|
1875
|
-
this.expect(')')
|
|
1876
|
-
return this.withLoc({ kind: 'enum_construct', enumName: typeToken.value, variant: memberToken.value, args }, typeToken)
|
|
1877
|
-
}
|
|
1878
|
-
// Static method call: Type::method(args)
|
|
1879
|
-
this.advance() // consume '('
|
|
1880
|
-
const args = this.parseArgs()
|
|
1881
|
-
this.expect(')')
|
|
1882
|
-
return this.withLoc({ kind: 'static_call', type: typeToken.value, method: memberToken.value, args }, typeToken)
|
|
1883
|
-
}
|
|
1884
|
-
// Enum variant access: Enum::Variant
|
|
1885
|
-
return this.withLoc({ kind: 'path_expr', enumName: typeToken.value, variant: memberToken.value }, typeToken)
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
if (token.kind === 'ident' && this.peek(1).kind === '=>') {
|
|
1889
|
-
return this.parseSingleParamLambda()
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
// Integer literal
|
|
1893
|
-
if (token.kind === 'int_lit') {
|
|
1894
|
-
this.advance()
|
|
1895
|
-
return this.withLoc({ kind: 'int_lit', value: parseInt(token.value, 10) }, token)
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
// Float literal
|
|
1899
|
-
if (token.kind === 'float_lit') {
|
|
1900
|
-
this.advance()
|
|
1901
|
-
return this.withLoc({ kind: 'float_lit', value: parseFloat(token.value) }, token)
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
// Relative coordinate: ~ ~5 ~-3 ~0.5
|
|
1905
|
-
if (token.kind === 'rel_coord') {
|
|
1906
|
-
this.advance()
|
|
1907
|
-
return this.withLoc({ kind: 'rel_coord', value: token.value }, token)
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
// Local coordinate: ^ ^5 ^-3 ^0.5
|
|
1911
|
-
if (token.kind === 'local_coord') {
|
|
1912
|
-
this.advance()
|
|
1913
|
-
return this.withLoc({ kind: 'local_coord', value: token.value }, token)
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
// NBT suffix literals
|
|
1917
|
-
if (token.kind === 'byte_lit') {
|
|
1918
|
-
this.advance()
|
|
1919
|
-
return this.withLoc({ kind: 'byte_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
|
|
1920
|
-
}
|
|
1921
|
-
if (token.kind === 'short_lit') {
|
|
1922
|
-
this.advance()
|
|
1923
|
-
return this.withLoc({ kind: 'short_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
|
|
1924
|
-
}
|
|
1925
|
-
if (token.kind === 'long_lit') {
|
|
1926
|
-
this.advance()
|
|
1927
|
-
return this.withLoc({ kind: 'long_lit', value: parseInt(token.value.slice(0, -1), 10) }, token)
|
|
1928
|
-
}
|
|
1929
|
-
if (token.kind === 'double_lit') {
|
|
1930
|
-
this.advance()
|
|
1931
|
-
return this.withLoc({ kind: 'double_lit', value: parseFloat(token.value.slice(0, -1)) }, token)
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
// String literal
|
|
1935
|
-
if (token.kind === 'string_lit') {
|
|
1936
|
-
this.advance()
|
|
1937
|
-
return this.parseStringExpr(token)
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
if (token.kind === 'f_string') {
|
|
1941
|
-
this.advance()
|
|
1942
|
-
return this.parseFStringExpr(token)
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
// MC name literal: #health → mc_name node (value = "health", without #)
|
|
1946
|
-
if (token.kind === 'mc_name') {
|
|
1947
|
-
this.advance()
|
|
1948
|
-
return this.withLoc({ kind: 'mc_name', value: token.value.slice(1) }, token)
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
// Boolean literal
|
|
1952
|
-
if (token.kind === 'true') {
|
|
1953
|
-
this.advance()
|
|
1954
|
-
return this.withLoc({ kind: 'bool_lit', value: true }, token)
|
|
1955
|
-
}
|
|
1956
|
-
if (token.kind === 'false') {
|
|
1957
|
-
this.advance()
|
|
1958
|
-
return this.withLoc({ kind: 'bool_lit', value: false }, token)
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
// Range literal
|
|
1962
|
-
if (token.kind === 'range_lit') {
|
|
1963
|
-
this.advance()
|
|
1964
|
-
return this.withLoc({ kind: 'range_lit', range: this.parseRangeValue(token.value) }, token)
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
// Selector
|
|
1968
|
-
if (token.kind === 'selector') {
|
|
1969
|
-
this.advance()
|
|
1970
|
-
return this.withLoc({
|
|
1971
|
-
kind: 'selector',
|
|
1972
|
-
raw: token.value,
|
|
1973
|
-
isSingle: computeIsSingle(token.value),
|
|
1974
|
-
sel: this.parseSelectorValue(token.value),
|
|
1975
|
-
}, token)
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
// Named struct literal: TypeName { field: value, ... }
|
|
1979
|
-
// Require at least one field (ident + :) to avoid ambiguity with blocks.
|
|
1980
|
-
if (token.kind === 'ident' && this.peek(1).kind === '{' &&
|
|
1981
|
-
this.peek(2).kind === 'ident' && this.peek(3).kind === ':') {
|
|
1982
|
-
this.advance() // consume type name (used only for disambiguation, dropped from AST)
|
|
1983
|
-
return this.parseStructLit()
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
// Some(expr) — Option constructor
|
|
1987
|
-
if (token.kind === 'ident' && token.value === 'Some' && this.peek(1).kind === '(') {
|
|
1988
|
-
this.advance() // consume 'Some'
|
|
1989
|
-
this.advance() // consume '('
|
|
1990
|
-
const value = this.parseExpr()
|
|
1991
|
-
this.expect(')')
|
|
1992
|
-
return this.withLoc({ kind: 'some_lit', value }, token)
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
// None — Option empty constructor
|
|
1996
|
-
if (token.kind === 'ident' && token.value === 'None') {
|
|
1997
|
-
this.advance()
|
|
1998
|
-
return this.withLoc({ kind: 'none_lit' }, token)
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
// Identifier
|
|
2002
|
-
if (token.kind === 'ident') {
|
|
2003
|
-
this.advance()
|
|
2004
|
-
return this.withLoc({ kind: 'ident', name: token.value }, token)
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
// Grouped expression or tuple literal
|
|
2008
|
-
if (token.kind === '(') {
|
|
2009
|
-
if (this.isBlockPosLiteral()) {
|
|
2010
|
-
return this.parseBlockPos()
|
|
2011
|
-
}
|
|
2012
|
-
if (this.isLambdaStart()) {
|
|
2013
|
-
return this.parseLambdaExpr()
|
|
2014
|
-
}
|
|
2015
|
-
this.advance()
|
|
2016
|
-
const first = this.parseExpr()
|
|
2017
|
-
// If followed by a comma, it's a tuple literal
|
|
2018
|
-
if (this.match(',')) {
|
|
2019
|
-
const elements: Expr[] = [first]
|
|
2020
|
-
if (!this.check(')')) {
|
|
2021
|
-
do {
|
|
2022
|
-
elements.push(this.parseExpr())
|
|
2023
|
-
} while (this.match(','))
|
|
2024
|
-
}
|
|
2025
|
-
this.expect(')')
|
|
2026
|
-
return this.withLoc({ kind: 'tuple_lit', elements }, token)
|
|
2027
|
-
}
|
|
2028
|
-
this.expect(')')
|
|
2029
|
-
return first
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
// Struct literal or block: { x: 10, y: 20 }
|
|
2033
|
-
if (token.kind === '{') {
|
|
2034
|
-
return this.parseStructLit()
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
// Array literal: [1, 2, 3] or []
|
|
2038
|
-
if (token.kind === '[') {
|
|
2039
|
-
return this.parseArrayLit()
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
this.error(`Unexpected token '${token.kind}'`)
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
private parseLiteralExpr(): LiteralExpr {
|
|
2046
|
-
// Support negative literals: -5, -3.14
|
|
2047
|
-
if (this.check('-')) {
|
|
2048
|
-
this.advance()
|
|
2049
|
-
const token = this.peek()
|
|
2050
|
-
if (token.kind === 'int_lit') {
|
|
2051
|
-
this.advance()
|
|
2052
|
-
return this.withLoc({ kind: 'int_lit', value: -Number(token.value) }, token)
|
|
2053
|
-
}
|
|
2054
|
-
if (token.kind === 'float_lit') {
|
|
2055
|
-
this.advance()
|
|
2056
|
-
return this.withLoc({ kind: 'float_lit', value: -Number(token.value) }, token)
|
|
2057
|
-
}
|
|
2058
|
-
this.error('Expected number after unary -')
|
|
2059
|
-
}
|
|
2060
|
-
const expr = this.parsePrimaryExpr()
|
|
2061
|
-
if (
|
|
2062
|
-
expr.kind === 'int_lit' ||
|
|
2063
|
-
expr.kind === 'float_lit' ||
|
|
2064
|
-
expr.kind === 'bool_lit' ||
|
|
2065
|
-
expr.kind === 'str_lit'
|
|
2066
|
-
) {
|
|
2067
|
-
return expr
|
|
2068
|
-
}
|
|
2069
|
-
this.error('Const value must be a literal')
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
private parseSingleParamLambda(): Expr {
|
|
2073
|
-
const paramToken = this.expect('ident')
|
|
2074
|
-
const params: LambdaParam[] = [{ name: paramToken.value }]
|
|
2075
|
-
this.expect('=>')
|
|
2076
|
-
return this.finishLambdaExpr(params, paramToken)
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
private parseLambdaExpr(): Expr {
|
|
2080
|
-
const openParenToken = this.expect('(')
|
|
2081
|
-
const params: LambdaParam[] = []
|
|
2082
|
-
|
|
2083
|
-
if (!this.check(')')) {
|
|
2084
|
-
do {
|
|
2085
|
-
const name = this.expect('ident').value
|
|
2086
|
-
let type: TypeNode | undefined
|
|
2087
|
-
if (this.match(':')) {
|
|
2088
|
-
type = this.parseType()
|
|
2089
|
-
}
|
|
2090
|
-
params.push({ name, type })
|
|
2091
|
-
} while (this.match(','))
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
this.expect(')')
|
|
2095
|
-
let returnType: TypeNode | undefined
|
|
2096
|
-
if (this.match('->')) {
|
|
2097
|
-
returnType = this.parseType()
|
|
2098
|
-
}
|
|
2099
|
-
this.expect('=>')
|
|
2100
|
-
return this.finishLambdaExpr(params, openParenToken, returnType)
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
private finishLambdaExpr(params: LambdaParam[], token: Token, returnType?: TypeNode): Expr {
|
|
2104
|
-
const body = this.check('{') ? this.parseBlock() : this.parseExpr()
|
|
2105
|
-
return this.withLoc({ kind: 'lambda', params, returnType, body }, token)
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
private parseStringExpr(token: Token): Expr {
|
|
2109
|
-
if (!token.value.includes('${')) {
|
|
2110
|
-
return this.withLoc({ kind: 'str_lit', value: token.value }, token)
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
const parts: Array<string | Expr> = []
|
|
2114
|
-
let current = ''
|
|
2115
|
-
let index = 0
|
|
2116
|
-
|
|
2117
|
-
while (index < token.value.length) {
|
|
2118
|
-
if (token.value[index] === '$' && token.value[index + 1] === '{') {
|
|
2119
|
-
if (current) {
|
|
2120
|
-
parts.push(current)
|
|
2121
|
-
current = ''
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
index += 2
|
|
2125
|
-
let depth = 1
|
|
2126
|
-
let exprSource = ''
|
|
2127
|
-
let inString = false
|
|
2128
|
-
|
|
2129
|
-
while (index < token.value.length && depth > 0) {
|
|
2130
|
-
const char = token.value[index]
|
|
2131
|
-
|
|
2132
|
-
if (char === '"' && token.value[index - 1] !== '\\') {
|
|
2133
|
-
inString = !inString
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
if (!inString) {
|
|
2137
|
-
if (char === '{') {
|
|
2138
|
-
depth++
|
|
2139
|
-
} else if (char === '}') {
|
|
2140
|
-
depth--
|
|
2141
|
-
if (depth === 0) {
|
|
2142
|
-
index++
|
|
2143
|
-
break
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
if (depth > 0) {
|
|
2149
|
-
exprSource += char
|
|
2150
|
-
}
|
|
2151
|
-
index++
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
if (depth !== 0) {
|
|
2155
|
-
this.error('Unterminated string interpolation')
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
parts.push(this.parseEmbeddedExpr(exprSource))
|
|
2159
|
-
continue
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
current += token.value[index]
|
|
2163
|
-
index++
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
if (current) {
|
|
2167
|
-
parts.push(current)
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
return this.withLoc({ kind: 'str_interp', parts }, token)
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
private parseFStringExpr(token: Token): Expr {
|
|
2174
|
-
const parts: Array<{ kind: 'text'; value: string } | { kind: 'expr'; expr: Expr }> = []
|
|
2175
|
-
let current = ''
|
|
2176
|
-
let index = 0
|
|
2177
|
-
|
|
2178
|
-
while (index < token.value.length) {
|
|
2179
|
-
if (token.value[index] === '{') {
|
|
2180
|
-
if (current) {
|
|
2181
|
-
parts.push({ kind: 'text', value: current })
|
|
2182
|
-
current = ''
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
index++
|
|
2186
|
-
let depth = 1
|
|
2187
|
-
let exprSource = ''
|
|
2188
|
-
let inString = false
|
|
2189
|
-
|
|
2190
|
-
while (index < token.value.length && depth > 0) {
|
|
2191
|
-
const char = token.value[index]
|
|
2192
|
-
|
|
2193
|
-
if (char === '"' && token.value[index - 1] !== '\\') {
|
|
2194
|
-
inString = !inString
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
if (!inString) {
|
|
2198
|
-
if (char === '{') {
|
|
2199
|
-
depth++
|
|
2200
|
-
} else if (char === '}') {
|
|
2201
|
-
depth--
|
|
2202
|
-
if (depth === 0) {
|
|
2203
|
-
index++
|
|
2204
|
-
break
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
if (depth > 0) {
|
|
2210
|
-
exprSource += char
|
|
2211
|
-
}
|
|
2212
|
-
index++
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
if (depth !== 0) {
|
|
2216
|
-
this.error('Unterminated f-string interpolation')
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
parts.push({ kind: 'expr', expr: this.parseEmbeddedExpr(exprSource) })
|
|
2220
|
-
continue
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
current += token.value[index]
|
|
2224
|
-
index++
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
if (current) {
|
|
2228
|
-
parts.push({ kind: 'text', value: current })
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
return this.withLoc({ kind: 'f_string', parts }, token)
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
private parseEmbeddedExpr(source: string): Expr {
|
|
2235
|
-
const tokens = new Lexer(source, this.filePath).tokenize()
|
|
2236
|
-
const parser = new Parser(tokens, source, this.filePath)
|
|
2237
|
-
const expr = parser.parseExpr()
|
|
2238
|
-
|
|
2239
|
-
if (!parser.check('eof')) {
|
|
2240
|
-
parser.error(`Unexpected token '${parser.peek().kind}' in string interpolation`)
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
return expr
|
|
2244
|
-
}
|
|
2245
|
-
|
|
2246
|
-
private parseStructLit(): Expr {
|
|
2247
|
-
const braceToken = this.expect('{')
|
|
2248
|
-
const fields: { name: string; value: Expr }[] = []
|
|
2249
|
-
|
|
2250
|
-
if (!this.check('}')) {
|
|
2251
|
-
do {
|
|
2252
|
-
const name = this.expect('ident').value
|
|
2253
|
-
this.expect(':')
|
|
2254
|
-
const value = this.parseExpr()
|
|
2255
|
-
fields.push({ name, value })
|
|
2256
|
-
} while (this.match(','))
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
this.expect('}')
|
|
2260
|
-
return this.withLoc({ kind: 'struct_lit', fields }, braceToken)
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
private parseArrayLit(): Expr {
|
|
2264
|
-
const bracketToken = this.expect('[')
|
|
2265
|
-
const elements: Expr[] = []
|
|
2266
|
-
|
|
2267
|
-
if (!this.check(']')) {
|
|
2268
|
-
do {
|
|
2269
|
-
elements.push(this.parseExpr())
|
|
2270
|
-
} while (this.match(','))
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
this.expect(']')
|
|
2274
|
-
return this.withLoc({ kind: 'array_lit', elements }, bracketToken)
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
private isLambdaStart(): boolean {
|
|
2278
|
-
if (!this.check('(')) return false
|
|
2279
|
-
|
|
2280
|
-
let offset = 1
|
|
2281
|
-
if (this.peek(offset).kind !== ')') {
|
|
2282
|
-
while (true) {
|
|
2283
|
-
if (this.peek(offset).kind !== 'ident') {
|
|
2284
|
-
return false
|
|
2285
|
-
}
|
|
2286
|
-
offset += 1
|
|
2287
|
-
|
|
2288
|
-
if (this.peek(offset).kind === ':') {
|
|
2289
|
-
offset += 1
|
|
2290
|
-
const consumed = this.typeTokenLength(offset)
|
|
2291
|
-
if (consumed === 0) {
|
|
2292
|
-
return false
|
|
2293
|
-
}
|
|
2294
|
-
offset += consumed
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
if (this.peek(offset).kind === ',') {
|
|
2298
|
-
offset += 1
|
|
2299
|
-
continue
|
|
2300
|
-
}
|
|
2301
|
-
break
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
if (this.peek(offset).kind !== ')') {
|
|
2306
|
-
return false
|
|
2307
|
-
}
|
|
2308
|
-
offset += 1
|
|
2309
|
-
|
|
2310
|
-
if (this.peek(offset).kind === '=>') {
|
|
2311
|
-
return true
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
if (this.peek(offset).kind === '->') {
|
|
2315
|
-
offset += 1
|
|
2316
|
-
const consumed = this.typeTokenLength(offset)
|
|
2317
|
-
if (consumed === 0) {
|
|
2318
|
-
return false
|
|
2319
|
-
}
|
|
2320
|
-
offset += consumed
|
|
2321
|
-
return this.peek(offset).kind === '=>'
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
return false
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
private typeTokenLength(offset: number): number {
|
|
2328
|
-
const token = this.peek(offset)
|
|
2329
|
-
|
|
2330
|
-
if (token.kind === '(') {
|
|
2331
|
-
let inner = offset + 1
|
|
2332
|
-
if (this.peek(inner).kind !== ')') {
|
|
2333
|
-
while (true) {
|
|
2334
|
-
const consumed = this.typeTokenLength(inner)
|
|
2335
|
-
if (consumed === 0) {
|
|
2336
|
-
return 0
|
|
2337
|
-
}
|
|
2338
|
-
inner += consumed
|
|
2339
|
-
if (this.peek(inner).kind === ',') {
|
|
2340
|
-
inner += 1
|
|
2341
|
-
continue
|
|
2342
|
-
}
|
|
2343
|
-
break
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
if (this.peek(inner).kind !== ')') {
|
|
2348
|
-
return 0
|
|
2349
|
-
}
|
|
2350
|
-
inner += 1
|
|
2351
|
-
|
|
2352
|
-
if (this.peek(inner).kind !== '->') {
|
|
2353
|
-
return 0
|
|
2354
|
-
}
|
|
2355
|
-
inner += 1
|
|
2356
|
-
const returnLen = this.typeTokenLength(inner)
|
|
2357
|
-
return returnLen === 0 ? 0 : inner + returnLen - offset
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
const isNamedType =
|
|
2361
|
-
token.kind === 'int' ||
|
|
2362
|
-
token.kind === 'bool' ||
|
|
2363
|
-
token.kind === 'float' ||
|
|
2364
|
-
token.kind === 'fixed' ||
|
|
2365
|
-
token.kind === 'string' ||
|
|
2366
|
-
token.kind === 'void' ||
|
|
2367
|
-
token.kind === 'BlockPos' ||
|
|
2368
|
-
token.kind === 'ident'
|
|
2369
|
-
if (!isNamedType) {
|
|
2370
|
-
return 0
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
let length = 1
|
|
2374
|
-
while (this.peek(offset + length).kind === '[' && this.peek(offset + length + 1).kind === ']') {
|
|
2375
|
-
length += 2
|
|
2376
|
-
}
|
|
2377
|
-
return length
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
private isBlockPosLiteral(): boolean {
|
|
2381
|
-
if (!this.check('(')) return false
|
|
2382
|
-
|
|
2383
|
-
let offset = 1
|
|
2384
|
-
for (let i = 0; i < 3; i++) {
|
|
2385
|
-
const consumed = this.coordComponentTokenLength(offset)
|
|
2386
|
-
if (consumed === 0) return false
|
|
2387
|
-
offset += consumed
|
|
2388
|
-
|
|
2389
|
-
if (i < 2) {
|
|
2390
|
-
if (this.peek(offset).kind !== ',') return false
|
|
2391
|
-
offset += 1
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
return this.peek(offset).kind === ')'
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
private coordComponentTokenLength(offset: number): number {
|
|
2399
|
-
const token = this.peek(offset)
|
|
2400
|
-
|
|
2401
|
-
if (token.kind === 'int_lit') {
|
|
2402
|
-
return 1
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
if (token.kind === '-') {
|
|
2406
|
-
return this.peek(offset + 1).kind === 'int_lit' ? 2 : 0
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
// rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) are single tokens now
|
|
2410
|
-
if (token.kind === 'rel_coord' || token.kind === 'local_coord') {
|
|
2411
|
-
return 1
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
return 0
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
private parseBlockPos(): BlockPosExpr {
|
|
2418
|
-
const openParenToken = this.expect('(')
|
|
2419
|
-
const x = this.parseCoordComponent()
|
|
2420
|
-
this.expect(',')
|
|
2421
|
-
const y = this.parseCoordComponent()
|
|
2422
|
-
this.expect(',')
|
|
2423
|
-
const z = this.parseCoordComponent()
|
|
2424
|
-
this.expect(')')
|
|
2425
|
-
return this.withLoc({ kind: 'blockpos', x, y, z }, openParenToken)
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
private parseCoordComponent(): CoordComponent {
|
|
2429
|
-
const token = this.peek()
|
|
2430
|
-
|
|
2431
|
-
// Handle rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) tokens
|
|
2432
|
-
if (token.kind === 'rel_coord') {
|
|
2433
|
-
this.advance()
|
|
2434
|
-
// Parse the offset from the token value (e.g., "~5" -> 5, "~" -> 0, "~-3" -> -3)
|
|
2435
|
-
const offset = this.parseCoordOffsetFromValue(token.value.slice(1))
|
|
2436
|
-
return { kind: 'relative', offset }
|
|
2437
|
-
}
|
|
2438
|
-
|
|
2439
|
-
if (token.kind === 'local_coord') {
|
|
2440
|
-
this.advance()
|
|
2441
|
-
const offset = this.parseCoordOffsetFromValue(token.value.slice(1))
|
|
2442
|
-
return { kind: 'local', offset }
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
return { kind: 'absolute', value: this.parseSignedCoordOffset(true) }
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
private parseCoordOffsetFromValue(value: string): number {
|
|
2449
|
-
if (value === '' || value === undefined) return 0
|
|
2450
|
-
return parseFloat(value)
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
private parseSignedCoordOffset(requireValue = false): number {
|
|
2454
|
-
let sign = 1
|
|
2455
|
-
if (this.match('-')) {
|
|
2456
|
-
sign = -1
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
if (this.check('int_lit')) {
|
|
2460
|
-
return sign * parseInt(this.advance().value, 10)
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
if (requireValue) {
|
|
2464
|
-
this.error('Expected integer coordinate component')
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
return 0
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
// -------------------------------------------------------------------------
|
|
2471
|
-
// Selector Parsing
|
|
2472
|
-
// -------------------------------------------------------------------------
|
|
2473
|
-
|
|
2474
|
-
private parseSelector(): EntitySelector {
|
|
2475
|
-
const token = this.expect('selector')
|
|
2476
|
-
return this.parseSelectorValue(token.value)
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
// Parse either a selector (@a[...]) or a variable with filters (p[...])
|
|
2480
|
-
// Returns { selector } for selectors or { varName, filters } for variables
|
|
2481
|
-
private parseSelectorOrVarSelector(): { selector?: EntitySelector, varName?: string, filters?: SelectorFilter } {
|
|
2482
|
-
if (this.check('selector')) {
|
|
2483
|
-
return { selector: this.parseSelector() }
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
// Must be an identifier (variable) possibly with filters
|
|
2487
|
-
const varToken = this.expect('ident')
|
|
2488
|
-
const varName = varToken.value
|
|
2489
|
-
|
|
2490
|
-
// Check for optional filters [...]
|
|
2491
|
-
if (this.check('[')) {
|
|
2492
|
-
this.advance() // consume '['
|
|
2493
|
-
// Collect everything until ']'
|
|
2494
|
-
let filterStr = ''
|
|
2495
|
-
let depth = 1
|
|
2496
|
-
while (depth > 0 && !this.check('eof')) {
|
|
2497
|
-
if (this.check('[')) depth++
|
|
2498
|
-
else if (this.check(']')) depth--
|
|
2499
|
-
if (depth > 0) {
|
|
2500
|
-
filterStr += this.peek().value ?? this.peek().kind
|
|
2501
|
-
this.advance()
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
this.expect(']')
|
|
2505
|
-
const filters = this.parseSelectorFilters(filterStr)
|
|
2506
|
-
return { varName, filters }
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
return { varName }
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
private parseSelectorValue(value: string): EntitySelector {
|
|
2513
|
-
// Parse @e[type=zombie, distance=..5]
|
|
2514
|
-
const bracketIndex = value.indexOf('[')
|
|
2515
|
-
if (bracketIndex === -1) {
|
|
2516
|
-
return { kind: value as SelectorKind }
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
const kind = value.slice(0, bracketIndex) as SelectorKind
|
|
2520
|
-
const paramsStr = value.slice(bracketIndex + 1, -1) // Remove [ and ]
|
|
2521
|
-
const filters = this.parseSelectorFilters(paramsStr)
|
|
2522
|
-
|
|
2523
|
-
return { kind, filters }
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
private parseSelectorFilters(paramsStr: string): SelectorFilter {
|
|
2527
|
-
const filters: SelectorFilter = {}
|
|
2528
|
-
const parts = this.splitSelectorParams(paramsStr)
|
|
2529
|
-
|
|
2530
|
-
for (const part of parts) {
|
|
2531
|
-
const eqIndex = part.indexOf('=')
|
|
2532
|
-
if (eqIndex === -1) continue
|
|
2533
|
-
|
|
2534
|
-
const key = part.slice(0, eqIndex).trim()
|
|
2535
|
-
const val = part.slice(eqIndex + 1).trim()
|
|
2536
|
-
|
|
2537
|
-
switch (key) {
|
|
2538
|
-
case 'type':
|
|
2539
|
-
filters.type = val
|
|
2540
|
-
break
|
|
2541
|
-
case 'distance':
|
|
2542
|
-
filters.distance = this.parseRangeValue(val)
|
|
2543
|
-
break
|
|
2544
|
-
case 'tag':
|
|
2545
|
-
if (val.startsWith('!')) {
|
|
2546
|
-
filters.notTag = filters.notTag ?? []
|
|
2547
|
-
filters.notTag.push(val.slice(1))
|
|
2548
|
-
} else {
|
|
2549
|
-
filters.tag = filters.tag ?? []
|
|
2550
|
-
filters.tag.push(val)
|
|
2551
|
-
}
|
|
2552
|
-
break
|
|
2553
|
-
case 'limit':
|
|
2554
|
-
filters.limit = parseInt(val, 10)
|
|
2555
|
-
break
|
|
2556
|
-
case 'sort':
|
|
2557
|
-
filters.sort = val as SelectorFilter['sort']
|
|
2558
|
-
break
|
|
2559
|
-
case 'nbt':
|
|
2560
|
-
filters.nbt = val
|
|
2561
|
-
break
|
|
2562
|
-
case 'gamemode':
|
|
2563
|
-
filters.gamemode = val
|
|
2564
|
-
break
|
|
2565
|
-
case 'scores':
|
|
2566
|
-
filters.scores = this.parseScoresFilter(val)
|
|
2567
|
-
break
|
|
2568
|
-
case 'x':
|
|
2569
|
-
filters.x = this.parseRangeValue(val)
|
|
2570
|
-
break
|
|
2571
|
-
case 'y':
|
|
2572
|
-
filters.y = this.parseRangeValue(val)
|
|
2573
|
-
break
|
|
2574
|
-
case 'z':
|
|
2575
|
-
filters.z = this.parseRangeValue(val)
|
|
2576
|
-
break
|
|
2577
|
-
case 'x_rotation':
|
|
2578
|
-
filters.x_rotation = this.parseRangeValue(val)
|
|
2579
|
-
break
|
|
2580
|
-
case 'y_rotation':
|
|
2581
|
-
filters.y_rotation = this.parseRangeValue(val)
|
|
2582
|
-
break
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
return filters
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
private splitSelectorParams(str: string): string[] {
|
|
2590
|
-
const parts: string[] = []
|
|
2591
|
-
let current = ''
|
|
2592
|
-
let depth = 0
|
|
2593
|
-
|
|
2594
|
-
for (const char of str) {
|
|
2595
|
-
if (char === '{' || char === '[') depth++
|
|
2596
|
-
else if (char === '}' || char === ']') depth--
|
|
2597
|
-
else if (char === ',' && depth === 0) {
|
|
2598
|
-
parts.push(current.trim())
|
|
2599
|
-
current = ''
|
|
2600
|
-
continue
|
|
2601
|
-
}
|
|
2602
|
-
current += char
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
if (current.trim()) {
|
|
2606
|
-
parts.push(current.trim())
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
return parts
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
|
-
private parseScoresFilter(val: string): Record<string, RangeExpr> {
|
|
2613
|
-
// Parse {kills=1.., deaths=..5}
|
|
2614
|
-
const scores: Record<string, RangeExpr> = {}
|
|
2615
|
-
const inner = val.slice(1, -1) // Remove { and }
|
|
2616
|
-
const parts = inner.split(',')
|
|
2617
|
-
|
|
2618
|
-
for (const part of parts) {
|
|
2619
|
-
const [name, range] = part.split('=').map(s => s.trim())
|
|
2620
|
-
scores[name] = this.parseRangeValue(range)
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
return scores
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
private parseRangeValue(value: string): RangeExpr {
|
|
2627
|
-
// ..5 → { max: 5 }
|
|
2628
|
-
// ..=5 → { max: 5 }
|
|
2629
|
-
// 1.. → { min: 1 }
|
|
2630
|
-
// 1..= → { min: 1 } (open-ended inclusive, end parsed separately)
|
|
2631
|
-
// 1..10 → { min: 1, max: 10 }
|
|
2632
|
-
// 1..=10 → { min: 1, max: 10 }
|
|
2633
|
-
// 5 → { min: 5, max: 5 } (exact match)
|
|
2634
|
-
|
|
2635
|
-
if (value.startsWith('..=')) {
|
|
2636
|
-
const rest = value.slice(3)
|
|
2637
|
-
if (!rest) return {} // open upper bound, no max
|
|
2638
|
-
const max = parseInt(rest, 10)
|
|
2639
|
-
return { max }
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
if (value.startsWith('..')) {
|
|
2643
|
-
const rest = value.slice(2)
|
|
2644
|
-
if (!rest) return {} // open upper bound, no max
|
|
2645
|
-
const max = parseInt(rest, 10)
|
|
2646
|
-
return { max }
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
const inclIdx = value.indexOf('..=')
|
|
2650
|
-
if (inclIdx !== -1) {
|
|
2651
|
-
const min = parseInt(value.slice(0, inclIdx), 10)
|
|
2652
|
-
const rest = value.slice(inclIdx + 3)
|
|
2653
|
-
if (!rest) return { min } // open-ended inclusive
|
|
2654
|
-
const max = parseInt(rest, 10)
|
|
2655
|
-
return { min, max }
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
const dotIndex = value.indexOf('..')
|
|
2659
|
-
if (dotIndex !== -1) {
|
|
2660
|
-
const min = parseInt(value.slice(0, dotIndex), 10)
|
|
2661
|
-
const rest = value.slice(dotIndex + 2)
|
|
2662
|
-
if (!rest) return { min } // open-ended
|
|
2663
|
-
const max = parseInt(rest, 10)
|
|
2664
|
-
return { min, max }
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
// Exact value
|
|
2668
|
-
const val = parseInt(value, 10)
|
|
2669
|
-
return { min: val, max: val }
|
|
2670
|
-
}
|
|
2671
130
|
}
|