redscript-mc 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
- package/CHANGELOG.md +58 -0
- package/CONTRIBUTING.md +140 -0
- package/README.md +28 -19
- package/README.zh.md +28 -19
- package/dist/__tests__/cli.test.js +10 -10
- package/dist/__tests__/codegen.test.js +1 -1
- package/dist/__tests__/diagnostics.test.js +5 -5
- package/dist/__tests__/e2e.test.js +146 -5
- package/dist/__tests__/formatter.test.d.ts +1 -0
- package/dist/__tests__/formatter.test.js +40 -0
- package/dist/__tests__/lowering.test.js +36 -3
- package/dist/__tests__/mc-integration.test.js +255 -10
- package/dist/__tests__/mc-syntax.test.js +3 -3
- package/dist/__tests__/nbt.test.js +2 -2
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/runtime.test.js +1 -1
- package/dist/ast/types.d.ts +21 -3
- package/dist/cli.js +25 -7
- package/dist/codegen/mcfunction/index.d.ts +1 -1
- package/dist/codegen/mcfunction/index.js +8 -2
- package/dist/codegen/structure/index.js +7 -1
- package/dist/formatter/index.d.ts +1 -0
- package/dist/formatter/index.js +26 -0
- package/dist/ir/builder.d.ts +2 -1
- package/dist/ir/types.d.ts +7 -2
- package/dist/ir/types.js +1 -1
- package/dist/lowering/index.d.ts +2 -0
- package/dist/lowering/index.js +183 -8
- package/dist/mc-test/runner.d.ts +2 -2
- package/dist/mc-test/runner.js +3 -3
- package/dist/mc-test/setup.js +2 -2
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +75 -7
- package/docs/COMPILATION_STATS.md +24 -24
- package/docs/IMPLEMENTATION_GUIDE.md +1 -1
- package/docs/STRUCTURE_TARGET.md +1 -1
- package/editors/vscode/.vscodeignore +1 -0
- package/editors/vscode/icons/mcrs.svg +7 -0
- package/editors/vscode/icons/redscript-icons.json +10 -0
- package/editors/vscode/out/extension.js +152 -9
- package/editors/vscode/package.json +10 -3
- package/editors/vscode/src/hover.ts +55 -2
- package/editors/vscode/src/symbols.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +10 -10
- package/src/__tests__/codegen.test.ts +1 -1
- package/src/__tests__/diagnostics.test.ts +5 -5
- package/src/__tests__/e2e.test.ts +134 -5
- package/src/__tests__/lowering.test.ts +48 -3
- package/src/__tests__/mc-integration.test.ts +285 -10
- package/src/__tests__/mc-syntax.test.ts +3 -3
- package/src/__tests__/nbt.test.ts +2 -2
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/runtime.test.ts +1 -1
- package/src/ast/types.ts +20 -3
- package/src/cli.ts +10 -10
- package/src/codegen/mcfunction/index.ts +9 -2
- package/src/codegen/structure/index.ts +8 -1
- package/src/examples/capture_the_flag.mcrs +208 -0
- package/src/examples/{counter.rs → counter.mcrs} +1 -1
- package/src/examples/hunger_games.mcrs +301 -0
- package/src/examples/new_features_demo.mcrs +193 -0
- package/src/examples/parkour_race.mcrs +233 -0
- package/src/examples/rpg.mcrs +13 -0
- package/src/examples/{shop.rs → shop.mcrs} +1 -1
- package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
- package/src/examples/{turret.rs → turret.mcrs} +1 -1
- package/src/examples/zombie_survival.mcrs +314 -0
- package/src/ir/builder.ts +3 -1
- package/src/ir/types.ts +8 -2
- package/src/lowering/index.ts +156 -8
- package/src/mc-test/runner.ts +3 -3
- package/src/mc-test/setup.ts +2 -2
- package/src/parser/index.ts +81 -8
- package/src/stdlib/README.md +155 -147
- package/src/stdlib/bossbar.mcrs +68 -0
- package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
- package/src/stdlib/effects.mcrs +64 -0
- package/src/stdlib/interactions.mcrs +195 -0
- package/src/stdlib/inventory.mcrs +38 -0
- package/src/stdlib/mobs.mcrs +99 -0
- package/src/stdlib/particles.mcrs +52 -0
- package/src/stdlib/sets.mcrs +20 -0
- package/src/stdlib/spawn.mcrs +41 -0
- package/src/stdlib/teams.mcrs +68 -0
- package/src/stdlib/world.mcrs +92 -0
- package/src/examples/rpg.rs +0 -13
- package/src/stdlib/mobs.rs +0 -99
- /package/src/examples/{arena.rs → arena.mcrs} +0 -0
- /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
- /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
- /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
- /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
- /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
- /package/src/stdlib/{math.rs → math.mcrs} +0 -0
- /package/src/stdlib/{player.rs → player.mcrs} +0 -0
- /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
- /package/src/stdlib/{timer.rs → timer.mcrs} +0 -0
- /package/src/templates/{combat.rs → combat.mcrs} +0 -0
- /package/src/templates/{economy.rs → economy.mcrs} +0 -0
- /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
- /package/src/templates/{quest.rs → quest.mcrs} +0 -0
- /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
package/src/lowering/index.ts
CHANGED
|
@@ -10,9 +10,10 @@ import { buildModule } from '../ir/builder'
|
|
|
10
10
|
import type { IRFunction, IRModule, Operand, BinOp, CmpOp } from '../ir/types'
|
|
11
11
|
import { DiagnosticError } from '../diagnostics'
|
|
12
12
|
import type {
|
|
13
|
-
Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, Program, RangeExpr, Span, Stmt,
|
|
13
|
+
Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, Program, RangeExpr, Span, Stmt,
|
|
14
14
|
StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent
|
|
15
15
|
} from '../ast/types'
|
|
16
|
+
import type { GlobalVar } from '../ir/types'
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Builtin Functions
|
|
@@ -28,7 +29,8 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
|
|
|
28
29
|
announce: ([msg]) => `tellraw @a {"text":"${msg}"}`,
|
|
29
30
|
give: ([sel, item, count, nbt]) => nbt ? `give ${sel} ${item}${nbt} ${count ?? '1'}` : `give ${sel} ${item} ${count ?? '1'}`,
|
|
30
31
|
kill: ([sel]) => `kill ${sel ?? '@s'}`,
|
|
31
|
-
effect:
|
|
32
|
+
effect: ([sel, eff, dur, amp]) => `effect give ${sel} ${eff} ${dur ?? '30'} ${amp ?? '0'}`,
|
|
33
|
+
effect_clear: ([sel, eff]) => eff ? `effect clear ${sel} ${eff}` : `effect clear ${sel}`,
|
|
32
34
|
summon: ([type, x, y, z, nbt]) => {
|
|
33
35
|
const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
|
|
34
36
|
return nbt ? `summon ${type} ${pos} ${nbt}` : `summon ${type} ${pos}`
|
|
@@ -80,6 +82,7 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
|
|
|
80
82
|
team_leave: () => null, // Special handling
|
|
81
83
|
team_option: () => null, // Special handling
|
|
82
84
|
data_get: () => null, // Special handling (returns value from NBT)
|
|
85
|
+
data_merge: () => null, // Special handling (merge NBT)
|
|
83
86
|
set_new: () => null, // Special handling (returns set ID)
|
|
84
87
|
set_add: () => null, // Special handling
|
|
85
88
|
set_contains: () => null, // Special handling (returns 1/0)
|
|
@@ -154,7 +157,8 @@ function emitBlockPos(pos: BlockPosExpr): string {
|
|
|
154
157
|
export class Lowering {
|
|
155
158
|
private namespace: string
|
|
156
159
|
private functions: IRFunction[] = []
|
|
157
|
-
private globals:
|
|
160
|
+
private globals: GlobalVar[] = []
|
|
161
|
+
private globalNames: Map<string, { mutable: boolean }> = new Map()
|
|
158
162
|
private fnDecls: Map<string, FnDecl> = new Map()
|
|
159
163
|
private specializedFunctions: Map<string, string> = new Map()
|
|
160
164
|
private currentFn: string = ''
|
|
@@ -185,6 +189,7 @@ export class Lowering {
|
|
|
185
189
|
|
|
186
190
|
constructor(namespace: string) {
|
|
187
191
|
this.namespace = namespace
|
|
192
|
+
LoweringBuilder.resetTempCounter()
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
lower(program: Program): IRModule {
|
|
@@ -212,6 +217,14 @@ export class Lowering {
|
|
|
212
217
|
this.varTypes.set(constDecl.name, this.normalizeType(constDecl.type))
|
|
213
218
|
}
|
|
214
219
|
|
|
220
|
+
// Process global variable declarations (top-level let)
|
|
221
|
+
for (const g of program.globals ?? []) {
|
|
222
|
+
this.globalNames.set(g.name, { mutable: g.mutable })
|
|
223
|
+
this.varTypes.set(g.name, this.normalizeType(g.type))
|
|
224
|
+
const initValue = g.init.kind === 'int_lit' ? g.init.value : 0
|
|
225
|
+
this.globals.push({ name: `$${g.name}`, init: initValue })
|
|
226
|
+
}
|
|
227
|
+
|
|
215
228
|
for (const fn of program.declarations) {
|
|
216
229
|
this.fnDecls.set(fn.name, fn)
|
|
217
230
|
this.functionDefaults.set(fn.name, fn.params.map(param => param.default))
|
|
@@ -323,6 +336,11 @@ export class Lowering {
|
|
|
323
336
|
}
|
|
324
337
|
}
|
|
325
338
|
|
|
339
|
+
// Check for @load decorator
|
|
340
|
+
if (fn.decorators.some(d => d.name === 'load')) {
|
|
341
|
+
irFn.isLoadInit = true
|
|
342
|
+
}
|
|
343
|
+
|
|
326
344
|
// Handle tick rate counter if needed
|
|
327
345
|
if (tickRate && tickRate > 1) {
|
|
328
346
|
this.wrapWithTickRate(irFn, tickRate)
|
|
@@ -339,7 +357,7 @@ export class Lowering {
|
|
|
339
357
|
private wrapWithTickRate(fn: IRFunction, rate: number): void {
|
|
340
358
|
// Add tick counter logic to entry block
|
|
341
359
|
const counterVar = `$__tick_${fn.name}`
|
|
342
|
-
this.globals.push(counterVar)
|
|
360
|
+
this.globals.push({ name: counterVar, init: 0 })
|
|
343
361
|
|
|
344
362
|
// Prepend counter logic to entry block
|
|
345
363
|
const entry = fn.blocks[0]
|
|
@@ -443,6 +461,15 @@ export class Lowering {
|
|
|
443
461
|
}
|
|
444
462
|
|
|
445
463
|
private lowerLetStmt(stmt: Extract<Stmt, { kind: 'let' }>): void {
|
|
464
|
+
// Check for duplicate declaration of foreach binding
|
|
465
|
+
if (this.currentContext.binding === stmt.name) {
|
|
466
|
+
throw new DiagnosticError(
|
|
467
|
+
'LoweringError',
|
|
468
|
+
`Cannot redeclare foreach binding '${stmt.name}'`,
|
|
469
|
+
stmt.span ?? { line: 0, col: 0 }
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
446
473
|
const varName = `$${stmt.name}`
|
|
447
474
|
this.varMap.set(stmt.name, varName)
|
|
448
475
|
|
|
@@ -495,6 +522,14 @@ export class Lowering {
|
|
|
495
522
|
return
|
|
496
523
|
}
|
|
497
524
|
|
|
525
|
+
// Handle set_new returning a set ID string
|
|
526
|
+
if (stmt.init.kind === 'call' && stmt.init.fn === 'set_new') {
|
|
527
|
+
const setId = `__set_${this.foreachCounter++}`
|
|
528
|
+
this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
|
|
529
|
+
this.stringValues.set(stmt.name, setId)
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
498
533
|
// Handle spawn_object returning entity handle
|
|
499
534
|
if (stmt.init.kind === 'call' && stmt.init.fn === 'spawn_object') {
|
|
500
535
|
const value = this.lowerExpr(stmt.init)
|
|
@@ -933,10 +968,22 @@ export class Lowering {
|
|
|
933
968
|
parts.push(`at ${this.selectorToString(sub.selector)}`)
|
|
934
969
|
break
|
|
935
970
|
case 'if_entity':
|
|
936
|
-
|
|
971
|
+
if (sub.selector) {
|
|
972
|
+
parts.push(`if entity ${this.selectorToString(sub.selector)}`)
|
|
973
|
+
} else if (sub.varName) {
|
|
974
|
+
// Variable with filters - substitute with @s and apply filters
|
|
975
|
+
const sel: EntitySelector = { kind: '@s', filters: sub.filters }
|
|
976
|
+
parts.push(`if entity ${this.selectorToString(sel)}`)
|
|
977
|
+
}
|
|
937
978
|
break
|
|
938
979
|
case 'unless_entity':
|
|
939
|
-
|
|
980
|
+
if (sub.selector) {
|
|
981
|
+
parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
|
|
982
|
+
} else if (sub.varName) {
|
|
983
|
+
// Variable with filters - substitute with @s and apply filters
|
|
984
|
+
const sel: EntitySelector = { kind: '@s', filters: sub.filters }
|
|
985
|
+
parts.push(`unless entity ${this.selectorToString(sel)}`)
|
|
986
|
+
}
|
|
940
987
|
break
|
|
941
988
|
case 'in':
|
|
942
989
|
parts.push(`in ${sub.dimension}`)
|
|
@@ -1269,6 +1316,15 @@ export class Lowering {
|
|
|
1269
1316
|
}
|
|
1270
1317
|
|
|
1271
1318
|
private lowerAssignExpr(expr: Extract<Expr, { kind: 'assign' }>): Operand {
|
|
1319
|
+
// Check for const reassignment (both compile-time consts and immutable globals)
|
|
1320
|
+
if (this.constValues.has(expr.target)) {
|
|
1321
|
+
throw new DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 })
|
|
1322
|
+
}
|
|
1323
|
+
const globalInfo = this.globalNames.get(expr.target)
|
|
1324
|
+
if (globalInfo && !globalInfo.mutable) {
|
|
1325
|
+
throw new DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 })
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1272
1328
|
const blockPosValue = this.resolveBlockPosExpr(expr.value)
|
|
1273
1329
|
if (blockPosValue) {
|
|
1274
1330
|
this.blockPosVars.set(expr.target, blockPosValue)
|
|
@@ -1744,6 +1800,67 @@ export class Lowering {
|
|
|
1744
1800
|
return { kind: 'var', name: dst }
|
|
1745
1801
|
}
|
|
1746
1802
|
|
|
1803
|
+
// data_merge(target, nbt) — merge NBT into entity/block/storage
|
|
1804
|
+
// data_merge(@s, { Invisible: 1b, Silent: 1b })
|
|
1805
|
+
if (name === 'data_merge') {
|
|
1806
|
+
const target = args[0]
|
|
1807
|
+
const nbt = args[1]
|
|
1808
|
+
const nbtStr = this.exprToSnbt ? this.exprToSnbt(nbt) : this.exprToString(nbt)
|
|
1809
|
+
|
|
1810
|
+
// Check if target is a selector (entity) or string (block/storage)
|
|
1811
|
+
if (target.kind === 'selector') {
|
|
1812
|
+
const sel = this.exprToTargetString(target)
|
|
1813
|
+
this.builder.emitRaw(`data merge entity ${sel} ${nbtStr}`)
|
|
1814
|
+
} else {
|
|
1815
|
+
// Assume block position or storage
|
|
1816
|
+
const targetStr = this.exprToString(target)
|
|
1817
|
+
// If it looks like coordinates, use block; otherwise storage
|
|
1818
|
+
if (targetStr.match(/^~|^\d|^\^/)) {
|
|
1819
|
+
this.builder.emitRaw(`data merge block ${targetStr} ${nbtStr}`)
|
|
1820
|
+
} else {
|
|
1821
|
+
this.builder.emitRaw(`data merge storage ${targetStr} ${nbtStr}`)
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return { kind: 'const', value: 0 }
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Set data structure operations — unique collections via NBT storage
|
|
1828
|
+
// set_new is primarily handled in lowerLetStmt for proper string tracking.
|
|
1829
|
+
// This fallback handles standalone set_new() calls without assignment.
|
|
1830
|
+
if (name === 'set_new') {
|
|
1831
|
+
const setId = `__set_${this.foreachCounter++}`
|
|
1832
|
+
this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
|
|
1833
|
+
return { kind: 'const', value: 0 }
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
if (name === 'set_add') {
|
|
1837
|
+
const setId = this.exprToString(args[0])
|
|
1838
|
+
const value = this.exprToString(args[1])
|
|
1839
|
+
this.builder.emitRaw(`execute unless data storage rs:sets ${setId}[{value:${value}}] run data modify storage rs:sets ${setId} append value {value:${value}}`)
|
|
1840
|
+
return { kind: 'const', value: 0 }
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (name === 'set_contains') {
|
|
1844
|
+
const dst = this.builder.freshTemp()
|
|
1845
|
+
const setId = this.exprToString(args[0])
|
|
1846
|
+
const value = this.exprToString(args[1])
|
|
1847
|
+
this.builder.emitRaw(`execute store result score ${dst} rs if data storage rs:sets ${setId}[{value:${value}}]`)
|
|
1848
|
+
return { kind: 'var', name: dst }
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
if (name === 'set_remove') {
|
|
1852
|
+
const setId = this.exprToString(args[0])
|
|
1853
|
+
const value = this.exprToString(args[1])
|
|
1854
|
+
this.builder.emitRaw(`data remove storage rs:sets ${setId}[{value:${value}}]`)
|
|
1855
|
+
return { kind: 'const', value: 0 }
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
if (name === 'set_clear') {
|
|
1859
|
+
const setId = this.exprToString(args[0])
|
|
1860
|
+
this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
|
|
1861
|
+
return { kind: 'const', value: 0 }
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1747
1864
|
const coordCommand = this.lowerCoordinateBuiltin(name, args)
|
|
1748
1865
|
if (coordCommand) {
|
|
1749
1866
|
this.builder.emitRaw(coordCommand)
|
|
@@ -1943,6 +2060,17 @@ export class Lowering {
|
|
|
1943
2060
|
}
|
|
1944
2061
|
case 'selector':
|
|
1945
2062
|
return this.selectorToString(expr.sel)
|
|
2063
|
+
case 'unary':
|
|
2064
|
+
// Handle unary minus on literals directly
|
|
2065
|
+
if (expr.op === '-' && expr.operand.kind === 'int_lit') {
|
|
2066
|
+
return (-expr.operand.value).toString()
|
|
2067
|
+
}
|
|
2068
|
+
if (expr.op === '-' && expr.operand.kind === 'float_lit') {
|
|
2069
|
+
return Math.trunc(-expr.operand.value).toString()
|
|
2070
|
+
}
|
|
2071
|
+
// Fall through to default for complex cases
|
|
2072
|
+
const unaryOp = this.lowerExpr(expr)
|
|
2073
|
+
return this.operandToVar(unaryOp)
|
|
1946
2074
|
default:
|
|
1947
2075
|
// Complex expression - lower and return var name
|
|
1948
2076
|
const op = this.lowerExpr(expr)
|
|
@@ -2050,6 +2178,14 @@ export class Lowering {
|
|
|
2050
2178
|
return null
|
|
2051
2179
|
}
|
|
2052
2180
|
|
|
2181
|
+
if (name === 'summon') {
|
|
2182
|
+
if (args.length >= 2 && pos1) {
|
|
2183
|
+
const nbt = args[2] ? ` ${this.exprToString(args[2])}` : ''
|
|
2184
|
+
return `summon ${this.exprToString(args[0])} ${emitBlockPos(pos1)}${nbt}`
|
|
2185
|
+
}
|
|
2186
|
+
return null
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2053
2189
|
return null
|
|
2054
2190
|
}
|
|
2055
2191
|
|
|
@@ -2294,6 +2430,13 @@ export class Lowering {
|
|
|
2294
2430
|
}
|
|
2295
2431
|
if (filters.nbt) parts.push(`nbt=${filters.nbt}`)
|
|
2296
2432
|
if (filters.gamemode) parts.push(`gamemode=${filters.gamemode}`)
|
|
2433
|
+
// Position filters
|
|
2434
|
+
if (filters.x) parts.push(`x=${this.rangeToString(filters.x)}`)
|
|
2435
|
+
if (filters.y) parts.push(`y=${this.rangeToString(filters.y)}`)
|
|
2436
|
+
if (filters.z) parts.push(`z=${this.rangeToString(filters.z)}`)
|
|
2437
|
+
// Rotation filters
|
|
2438
|
+
if (filters.x_rotation) parts.push(`x_rotation=${this.rangeToString(filters.x_rotation)}`)
|
|
2439
|
+
if (filters.y_rotation) parts.push(`y_rotation=${this.rangeToString(filters.y_rotation)}`)
|
|
2297
2440
|
|
|
2298
2441
|
return this.finalizeSelector(parts.length ? `${kind}[${parts.join(',')}]` : kind)
|
|
2299
2442
|
}
|
|
@@ -2318,14 +2461,19 @@ export class Lowering {
|
|
|
2318
2461
|
// ---------------------------------------------------------------------------
|
|
2319
2462
|
|
|
2320
2463
|
class LoweringBuilder {
|
|
2321
|
-
private
|
|
2464
|
+
private static globalTempId = 0
|
|
2322
2465
|
private labelCount = 0
|
|
2323
2466
|
private blocks: any[] = []
|
|
2324
2467
|
private currentBlock: any = null
|
|
2325
2468
|
private locals = new Set<string>()
|
|
2326
2469
|
|
|
2470
|
+
/** Reset the global temp counter (call between compilations). */
|
|
2471
|
+
static resetTempCounter(): void {
|
|
2472
|
+
LoweringBuilder.globalTempId = 0
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2327
2475
|
freshTemp(): string {
|
|
2328
|
-
const name = `$
|
|
2476
|
+
const name = `$_${LoweringBuilder.globalTempId++}`
|
|
2329
2477
|
this.locals.add(name)
|
|
2330
2478
|
return name
|
|
2331
2479
|
}
|
package/src/mc-test/runner.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RedScript MC Integration Test Runner
|
|
3
3
|
*
|
|
4
|
-
* Compiles a .
|
|
4
|
+
* Compiles a .mcrs file, installs it to a running Paper server,
|
|
5
5
|
* runs test scenarios, and reports results.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* npx ts-node src/mc-test/runner.ts src/examples/counter.
|
|
8
|
+
* npx ts-node src/mc-test/runner.ts src/examples/counter.mcrs
|
|
9
9
|
*
|
|
10
10
|
* Requires:
|
|
11
11
|
* - Paper server running with TestHarnessPlugin
|
|
@@ -105,7 +105,7 @@ export async function runMCTests(
|
|
|
105
105
|
if (require.main === module) {
|
|
106
106
|
const sourceFile = process.argv[2]
|
|
107
107
|
if (!sourceFile) {
|
|
108
|
-
console.error('Usage: ts-node runner.ts <source.
|
|
108
|
+
console.error('Usage: ts-node runner.ts <source.mcrs>')
|
|
109
109
|
process.exit(1)
|
|
110
110
|
}
|
|
111
111
|
|
package/src/mc-test/setup.ts
CHANGED
|
@@ -36,11 +36,11 @@ function main() {
|
|
|
36
36
|
// Example files
|
|
37
37
|
const exampleNamespaces = ['counter', 'world_manager']
|
|
38
38
|
for (const ns of exampleNamespaces) {
|
|
39
|
-
const file = path.join(EXAMPLES_DIR, `${ns}.
|
|
39
|
+
const file = path.join(EXAMPLES_DIR, `${ns}.mcrs`)
|
|
40
40
|
if (fs.existsSync(file)) {
|
|
41
41
|
writeFixture(fs.readFileSync(file, 'utf-8'), ns)
|
|
42
42
|
} else {
|
|
43
|
-
console.log(` ⚠ ${ns}.
|
|
43
|
+
console.log(` ⚠ ${ns}.mcrs not found, skipping`)
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
package/src/parser/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { Lexer, type Token, type TokenKind } from '../lexer'
|
|
9
9
|
import type {
|
|
10
|
-
Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, LiteralExpr, Param,
|
|
10
|
+
Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, LiteralExpr, Param,
|
|
11
11
|
Program, RangeExpr, SelectorFilter, SelectorKind, Span, Stmt, TypeNode, AssignOp,
|
|
12
12
|
StructDecl, StructField, ExecuteSubcommand, EnumDecl, EnumVariant, BlockPosExpr,
|
|
13
13
|
CoordComponent, LambdaParam
|
|
@@ -132,6 +132,7 @@ export class Parser {
|
|
|
132
132
|
|
|
133
133
|
parse(defaultNamespace = 'redscript'): Program {
|
|
134
134
|
let namespace = defaultNamespace
|
|
135
|
+
const globals: GlobalDecl[] = []
|
|
135
136
|
const declarations: FnDecl[] = []
|
|
136
137
|
const structs: StructDecl[] = []
|
|
137
138
|
const enums: EnumDecl[] = []
|
|
@@ -147,7 +148,9 @@ export class Parser {
|
|
|
147
148
|
|
|
148
149
|
// Parse struct and function declarations
|
|
149
150
|
while (!this.check('eof')) {
|
|
150
|
-
if (this.check('
|
|
151
|
+
if (this.check('let')) {
|
|
152
|
+
globals.push(this.parseGlobalDecl(true))
|
|
153
|
+
} else if (this.check('struct')) {
|
|
151
154
|
structs.push(this.parseStructDecl())
|
|
152
155
|
} else if (this.check('enum')) {
|
|
153
156
|
enums.push(this.parseEnumDecl())
|
|
@@ -158,7 +161,7 @@ export class Parser {
|
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
163
|
|
|
161
|
-
return { namespace, declarations, structs, enums, consts }
|
|
164
|
+
return { namespace, globals, declarations, structs, enums, consts }
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
// -------------------------------------------------------------------------
|
|
@@ -227,6 +230,17 @@ export class Parser {
|
|
|
227
230
|
return this.withLoc({ name, type, value }, constToken)
|
|
228
231
|
}
|
|
229
232
|
|
|
233
|
+
private parseGlobalDecl(mutable: boolean): GlobalDecl {
|
|
234
|
+
const token = this.advance() // consume 'let'
|
|
235
|
+
const name = this.expect('ident').value
|
|
236
|
+
this.expect(':')
|
|
237
|
+
const type = this.parseType()
|
|
238
|
+
this.expect('=')
|
|
239
|
+
const init = this.parseExpr()
|
|
240
|
+
this.expect(';')
|
|
241
|
+
return this.withLoc({ kind: 'global', name, type, init, mutable }, token)
|
|
242
|
+
}
|
|
243
|
+
|
|
230
244
|
// -------------------------------------------------------------------------
|
|
231
245
|
// Function Declaration
|
|
232
246
|
// -------------------------------------------------------------------------
|
|
@@ -650,15 +664,15 @@ export class Parser {
|
|
|
650
664
|
if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
|
|
651
665
|
this.advance() // consume 'entity'
|
|
652
666
|
}
|
|
653
|
-
const
|
|
654
|
-
subcommands.push({ kind: 'if_entity',
|
|
667
|
+
const selectorOrVar = this.parseSelectorOrVarSelector()
|
|
668
|
+
subcommands.push({ kind: 'if_entity', ...selectorOrVar })
|
|
655
669
|
} else if (this.match('unless')) {
|
|
656
670
|
// Expect 'entity' keyword (as ident) or just parse selector directly
|
|
657
671
|
if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
|
|
658
672
|
this.advance() // consume 'entity'
|
|
659
673
|
}
|
|
660
|
-
const
|
|
661
|
-
subcommands.push({ kind: 'unless_entity',
|
|
674
|
+
const selectorOrVar = this.parseSelectorOrVarSelector()
|
|
675
|
+
subcommands.push({ kind: 'unless_entity', ...selectorOrVar })
|
|
662
676
|
} else if (this.match('in')) {
|
|
663
677
|
const dim = this.expect('ident').value
|
|
664
678
|
subcommands.push({ kind: 'in', dimension: dim })
|
|
@@ -782,6 +796,10 @@ export class Parser {
|
|
|
782
796
|
'has_tag': '__entity_has_tag',
|
|
783
797
|
'push': '__array_push',
|
|
784
798
|
'pop': '__array_pop',
|
|
799
|
+
'add': 'set_add',
|
|
800
|
+
'contains': 'set_contains',
|
|
801
|
+
'remove': 'set_remove',
|
|
802
|
+
'clear': 'set_clear',
|
|
785
803
|
}
|
|
786
804
|
const internalFn = methodMap[expr.field]
|
|
787
805
|
if (internalFn) {
|
|
@@ -793,7 +811,14 @@ export class Parser {
|
|
|
793
811
|
)
|
|
794
812
|
continue
|
|
795
813
|
}
|
|
796
|
-
|
|
814
|
+
// Generic method sugar: obj.method(args) → method(obj, args)
|
|
815
|
+
const args = this.parseArgs()
|
|
816
|
+
this.expect(')')
|
|
817
|
+
expr = this.withLoc(
|
|
818
|
+
{ kind: 'call', fn: expr.field, args: [expr.obj, ...args] },
|
|
819
|
+
this.getLocToken(expr) ?? openParenToken
|
|
820
|
+
)
|
|
821
|
+
continue
|
|
797
822
|
}
|
|
798
823
|
const args = this.parseArgs()
|
|
799
824
|
this.expect(')')
|
|
@@ -1308,6 +1333,39 @@ export class Parser {
|
|
|
1308
1333
|
return this.parseSelectorValue(token.value)
|
|
1309
1334
|
}
|
|
1310
1335
|
|
|
1336
|
+
// Parse either a selector (@a[...]) or a variable with filters (p[...])
|
|
1337
|
+
// Returns { selector } for selectors or { varName, filters } for variables
|
|
1338
|
+
private parseSelectorOrVarSelector(): { selector?: EntitySelector, varName?: string, filters?: SelectorFilter } {
|
|
1339
|
+
if (this.check('selector')) {
|
|
1340
|
+
return { selector: this.parseSelector() }
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Must be an identifier (variable) possibly with filters
|
|
1344
|
+
const varToken = this.expect('ident')
|
|
1345
|
+
const varName = varToken.value
|
|
1346
|
+
|
|
1347
|
+
// Check for optional filters [...]
|
|
1348
|
+
if (this.check('[')) {
|
|
1349
|
+
this.advance() // consume '['
|
|
1350
|
+
// Collect everything until ']'
|
|
1351
|
+
let filterStr = ''
|
|
1352
|
+
let depth = 1
|
|
1353
|
+
while (depth > 0 && !this.check('eof')) {
|
|
1354
|
+
if (this.check('[')) depth++
|
|
1355
|
+
else if (this.check(']')) depth--
|
|
1356
|
+
if (depth > 0) {
|
|
1357
|
+
filterStr += this.peek().value ?? this.peek().kind
|
|
1358
|
+
this.advance()
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
this.expect(']')
|
|
1362
|
+
const filters = this.parseSelectorFilters(filterStr)
|
|
1363
|
+
return { varName, filters }
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
return { varName }
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1311
1369
|
private parseSelectorValue(value: string): EntitySelector {
|
|
1312
1370
|
// Parse @e[type=zombie, distance=..5]
|
|
1313
1371
|
const bracketIndex = value.indexOf('[')
|
|
@@ -1364,6 +1422,21 @@ export class Parser {
|
|
|
1364
1422
|
case 'scores':
|
|
1365
1423
|
filters.scores = this.parseScoresFilter(val)
|
|
1366
1424
|
break
|
|
1425
|
+
case 'x':
|
|
1426
|
+
filters.x = this.parseRangeValue(val)
|
|
1427
|
+
break
|
|
1428
|
+
case 'y':
|
|
1429
|
+
filters.y = this.parseRangeValue(val)
|
|
1430
|
+
break
|
|
1431
|
+
case 'z':
|
|
1432
|
+
filters.z = this.parseRangeValue(val)
|
|
1433
|
+
break
|
|
1434
|
+
case 'x_rotation':
|
|
1435
|
+
filters.x_rotation = this.parseRangeValue(val)
|
|
1436
|
+
break
|
|
1437
|
+
case 'y_rotation':
|
|
1438
|
+
filters.y_rotation = this.parseRangeValue(val)
|
|
1439
|
+
break
|
|
1367
1440
|
}
|
|
1368
1441
|
}
|
|
1369
1442
|
|