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
|
@@ -766,17 +766,70 @@ function findFnDeclLine(document: vscode.TextDocument, name: string): number | n
|
|
|
766
766
|
function findFnSignature(document: vscode.TextDocument, name: string): string | null {
|
|
767
767
|
const text = document.getText()
|
|
768
768
|
// Match: fn name(params) or fn name(params) -> Type
|
|
769
|
-
const re = new RegExp(`\\bfn\\s+${escapeRe(name)}\\s*\\(([^)]*)\\)(?:\\s*->\\s*([A-Za-z_][A-Za-z0-9_\\[\\]]*))
|
|
769
|
+
const re = new RegExp(`\\bfn\\s+${escapeRe(name)}\\s*\\(([^)]*)\\)(?:\\s*->\\s*([A-Za-z_][A-Za-z0-9_\\[\\]]*))?\\s*\\{`, 'm')
|
|
770
770
|
const match = re.exec(text)
|
|
771
771
|
if (!match) return null
|
|
772
772
|
const params = match[1].trim()
|
|
773
|
-
|
|
773
|
+
let returnType = match[2]
|
|
774
|
+
|
|
775
|
+
// If no explicit return type, try to infer from return statements
|
|
776
|
+
if (!returnType) {
|
|
777
|
+
returnType = inferReturnType(text, match.index + match[0].length)
|
|
778
|
+
}
|
|
779
|
+
|
|
774
780
|
if (returnType) {
|
|
775
781
|
return `fn ${name}(${params}) -> ${returnType}`
|
|
776
782
|
}
|
|
777
783
|
return `fn ${name}(${params})`
|
|
778
784
|
}
|
|
779
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Infer return type by looking at return statements in function body
|
|
788
|
+
*/
|
|
789
|
+
function inferReturnType(text: string, bodyStart: number): string | null {
|
|
790
|
+
// Find the matching closing brace
|
|
791
|
+
let braceCount = 1
|
|
792
|
+
let pos = bodyStart
|
|
793
|
+
while (pos < text.length && braceCount > 0) {
|
|
794
|
+
if (text[pos] === '{') braceCount++
|
|
795
|
+
else if (text[pos] === '}') braceCount--
|
|
796
|
+
pos++
|
|
797
|
+
}
|
|
798
|
+
const body = text.slice(bodyStart, pos - 1)
|
|
799
|
+
|
|
800
|
+
// Look for return statements
|
|
801
|
+
const returnMatch = body.match(/\breturn\s+(.+?);/)
|
|
802
|
+
if (!returnMatch) return null
|
|
803
|
+
|
|
804
|
+
const returnExpr = returnMatch[1].trim()
|
|
805
|
+
|
|
806
|
+
// Infer type from expression
|
|
807
|
+
if (/^\d+$/.test(returnExpr)) return 'int'
|
|
808
|
+
if (/^\d+\.\d+$/.test(returnExpr)) return 'float'
|
|
809
|
+
if (/^\d+[bB]$/.test(returnExpr)) return 'byte'
|
|
810
|
+
if (/^\d+[sS]$/.test(returnExpr)) return 'short'
|
|
811
|
+
if (/^\d+[lL]$/.test(returnExpr)) return 'long'
|
|
812
|
+
if (/^\d+(\.\d+)?[dD]$/.test(returnExpr)) return 'double'
|
|
813
|
+
if (/^".*"$/.test(returnExpr)) return 'string'
|
|
814
|
+
if (/^(true|false)$/.test(returnExpr)) return 'bool'
|
|
815
|
+
if (/^@[aeprs]/.test(returnExpr)) return 'selector'
|
|
816
|
+
if (/^\{/.test(returnExpr)) return 'struct'
|
|
817
|
+
if (/^\[/.test(returnExpr)) return 'array'
|
|
818
|
+
|
|
819
|
+
// Check for known builtin return types
|
|
820
|
+
const callMatch = returnExpr.match(/^(\w+)\s*\(/)
|
|
821
|
+
if (callMatch) {
|
|
822
|
+
const fnName = callMatch[1]
|
|
823
|
+
// Common builtins that return int
|
|
824
|
+
if (['scoreboard_get', 'score', 'random', 'random_native', 'str_len', 'len', 'data_get', 'bossbar_get_value', 'set_contains'].includes(fnName)) {
|
|
825
|
+
return 'int'
|
|
826
|
+
}
|
|
827
|
+
if (fnName === 'set_new') return 'string'
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return null
|
|
831
|
+
}
|
|
832
|
+
|
|
780
833
|
function escapeRe(s: string): string {
|
|
781
834
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
782
835
|
}
|
|
@@ -77,6 +77,38 @@ function isStructLiteralField(doc: vscode.TextDocument, position: vscode.Positio
|
|
|
77
77
|
return null
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Check if cursor is on a member access field: expr.field
|
|
82
|
+
* Returns the struct type name if found, null otherwise
|
|
83
|
+
*/
|
|
84
|
+
function isMemberAccessField(doc: vscode.TextDocument, position: vscode.Position, word: string): string | null {
|
|
85
|
+
const line = doc.lineAt(position.line).text
|
|
86
|
+
const wordStart = position.character
|
|
87
|
+
|
|
88
|
+
// Check if word is preceded by '.'
|
|
89
|
+
const beforeWord = line.slice(0, wordStart)
|
|
90
|
+
if (!beforeWord.endsWith('.')) return null
|
|
91
|
+
|
|
92
|
+
// Find the variable before the dot
|
|
93
|
+
const varMatch = beforeWord.match(/(\w+)\s*\.$/)
|
|
94
|
+
if (!varMatch) return null
|
|
95
|
+
const varName = varMatch[1]
|
|
96
|
+
|
|
97
|
+
// Find the variable's type declaration
|
|
98
|
+
const text = doc.getText()
|
|
99
|
+
// Look for: let varName: TypeName or fn param varName: TypeName
|
|
100
|
+
const typeRe = new RegExp(`\\b(?:let|const)\\s+${varName}\\s*:\\s*(\\w+)`, 'm')
|
|
101
|
+
const typeMatch = text.match(typeRe)
|
|
102
|
+
if (typeMatch) return typeMatch[1]
|
|
103
|
+
|
|
104
|
+
// Also check function parameters: fn xxx(varName: TypeName)
|
|
105
|
+
const paramRe = new RegExp(`\\((?:[^)]*,\\s*)?${varName}\\s*:\\s*(\\w+)`, 'm')
|
|
106
|
+
const paramMatch = text.match(paramRe)
|
|
107
|
+
if (paramMatch) return paramMatch[1]
|
|
108
|
+
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
function findAllOccurrences(doc: vscode.TextDocument, word: string): vscode.Location[] {
|
|
81
113
|
const text = doc.getText()
|
|
82
114
|
const re = new RegExp(`\\b${escapeRegex(word)}\\b`, 'g')
|
|
@@ -119,6 +151,16 @@ export function registerSymbolProviders(context: vscode.ExtensionContext): void
|
|
|
119
151
|
}
|
|
120
152
|
}
|
|
121
153
|
|
|
154
|
+
// Check if this is a member access: expr.field
|
|
155
|
+
const memberAccess = isMemberAccessField(doc, position, word)
|
|
156
|
+
if (memberAccess) {
|
|
157
|
+
const structFields = findStructFields(doc)
|
|
158
|
+
const field = structFields.find(f => f.structName === memberAccess && f.fieldName === word)
|
|
159
|
+
if (field) {
|
|
160
|
+
return new vscode.Location(doc.uri, field.fieldRange)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
const decls = findDeclarations(doc)
|
|
123
165
|
const decl = decls.find(d => d.name === word)
|
|
124
166
|
if (!decl) return null
|
package/package.json
CHANGED
|
@@ -10,11 +10,11 @@ describe('CLI API', () => {
|
|
|
10
10
|
describe('imports', () => {
|
|
11
11
|
it('compiles a file with imported helpers', () => {
|
|
12
12
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-imports-'))
|
|
13
|
-
const libPath = path.join(tempDir, 'lib.
|
|
14
|
-
const mainPath = path.join(tempDir, 'main.
|
|
13
|
+
const libPath = path.join(tempDir, 'lib.mcrs')
|
|
14
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
15
15
|
|
|
16
16
|
fs.writeFileSync(libPath, 'fn double(x: int) -> int { return x + x; }\n')
|
|
17
|
-
fs.writeFileSync(mainPath, 'import "./lib.
|
|
17
|
+
fs.writeFileSync(mainPath, 'import "./lib.mcrs"\n\nfn main() { let value: int = double(2); }\n')
|
|
18
18
|
|
|
19
19
|
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
20
20
|
const result = compile(source, { namespace: 'imports', filePath: mainPath })
|
|
@@ -26,13 +26,13 @@ describe('CLI API', () => {
|
|
|
26
26
|
|
|
27
27
|
it('deduplicates circular imports', () => {
|
|
28
28
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-circular-'))
|
|
29
|
-
const aPath = path.join(tempDir, 'a.
|
|
30
|
-
const bPath = path.join(tempDir, 'b.
|
|
31
|
-
const mainPath = path.join(tempDir, 'main.
|
|
29
|
+
const aPath = path.join(tempDir, 'a.mcrs')
|
|
30
|
+
const bPath = path.join(tempDir, 'b.mcrs')
|
|
31
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
32
32
|
|
|
33
|
-
fs.writeFileSync(aPath, 'import "./b.
|
|
34
|
-
fs.writeFileSync(bPath, 'import "./a.
|
|
35
|
-
fs.writeFileSync(mainPath, 'import "./a.
|
|
33
|
+
fs.writeFileSync(aPath, 'import "./b.mcrs"\n\nfn from_a() -> int { return 1; }\n')
|
|
34
|
+
fs.writeFileSync(bPath, 'import "./a.mcrs"\n\nfn from_b() -> int { return from_a(); }\n')
|
|
35
|
+
fs.writeFileSync(mainPath, 'import "./a.mcrs"\n\nfn main() { let value: int = from_b(); }\n')
|
|
36
36
|
|
|
37
37
|
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
38
38
|
const result = compile(source, { namespace: 'circular', filePath: mainPath })
|
|
@@ -91,7 +91,7 @@ fn build() {
|
|
|
91
91
|
describe('--stats flag', () => {
|
|
92
92
|
it('prints optimizer statistics', () => {
|
|
93
93
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stats-'))
|
|
94
|
-
const inputPath = path.join(tempDir, 'input.
|
|
94
|
+
const inputPath = path.join(tempDir, 'input.mcrs')
|
|
95
95
|
const outputDir = path.join(tempDir, 'out')
|
|
96
96
|
|
|
97
97
|
fs.writeFileSync(inputPath, 'fn build() { setblock((0, 64, 0), "minecraft:stone"); setblock((1, 64, 0), "minecraft:stone"); }')
|
|
@@ -11,7 +11,7 @@ describe('generateDatapack', () => {
|
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
it('generates __load.mcfunction with objective setup', () => {
|
|
14
|
-
const mod: IRModule = { namespace: 'mypack', functions: [], globals: ['counter'] }
|
|
14
|
+
const mod: IRModule = { namespace: 'mypack', functions: [], globals: [{ name: 'counter', init: 0 }] }
|
|
15
15
|
const files = generateDatapack(mod)
|
|
16
16
|
const load = files.find(f => f.path.includes('__load.mcfunction'))
|
|
17
17
|
expect(load?.content).toContain('scoreboard objectives add rs dummy')
|
|
@@ -33,11 +33,11 @@ describe('DiagnosticError', () => {
|
|
|
33
33
|
const error = new DiagnosticError(
|
|
34
34
|
'TypeError',
|
|
35
35
|
'Unknown function: foo',
|
|
36
|
-
{ file: 'test.
|
|
36
|
+
{ file: 'test.mcrs', line: 1, col: 9 },
|
|
37
37
|
source.split('\n')
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
expect(formatError(error, source)).toContain('Error in test.
|
|
40
|
+
expect(formatError(error, source)).toContain('Error in test.mcrs at line 1, col 9:')
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
43
|
|
|
@@ -66,11 +66,11 @@ describe('DiagnosticError', () => {
|
|
|
66
66
|
const error = new DiagnosticError(
|
|
67
67
|
'LexError',
|
|
68
68
|
'Unexpected character',
|
|
69
|
-
{ file: 'test.
|
|
69
|
+
{ file: 'test.mcrs', line: 1, col: 1 },
|
|
70
70
|
['@@@']
|
|
71
71
|
)
|
|
72
72
|
const formatted = error.format()
|
|
73
|
-
expect(formatted).toContain('test.
|
|
73
|
+
expect(formatted).toContain('test.mcrs:')
|
|
74
74
|
expect(formatted).toContain('[LexError]')
|
|
75
75
|
})
|
|
76
76
|
|
|
@@ -153,7 +153,7 @@ describe('compile function', () => {
|
|
|
153
153
|
})
|
|
154
154
|
|
|
155
155
|
it('includes file path in error', () => {
|
|
156
|
-
const result = compile('fn main() { }', { filePath: 'test.
|
|
156
|
+
const result = compile('fn main() { }', { filePath: 'test.mcrs' })
|
|
157
157
|
// This is valid, but test that filePath is passed through
|
|
158
158
|
expect(result.success).toBe(true)
|
|
159
159
|
})
|
|
@@ -340,7 +340,7 @@ fn test() {
|
|
|
340
340
|
expect(fn).toContain('bossbar set ns:health visible true')
|
|
341
341
|
expect(fn).toContain('bossbar set ns:health players @a')
|
|
342
342
|
expect(fn).toContain('bossbar remove ns:health')
|
|
343
|
-
expect(fn).toMatch(/execute store result score \$
|
|
343
|
+
expect(fn).toMatch(/execute store result score \$_\d+ rs run bossbar get ns:health value/)
|
|
344
344
|
})
|
|
345
345
|
|
|
346
346
|
it('compiles team builtins', () => {
|
|
@@ -651,21 +651,21 @@ fn double_score() -> int {
|
|
|
651
651
|
const source = 'fn test() { let x: int = random(1, 10); }'
|
|
652
652
|
const files = compile(source)
|
|
653
653
|
const fn = getFunction(files, 'test')
|
|
654
|
-
expect(fn).toContain('scoreboard players random $
|
|
654
|
+
expect(fn).toContain('scoreboard players random $_0 rs 1 10')
|
|
655
655
|
})
|
|
656
656
|
|
|
657
657
|
it('compiles random_native()', () => {
|
|
658
658
|
const source = 'fn test() { let x: int = random_native(1, 6); }'
|
|
659
659
|
const files = compile(source)
|
|
660
660
|
const fn = getFunction(files, 'test')
|
|
661
|
-
expect(fn).toContain('execute store result score $
|
|
661
|
+
expect(fn).toContain('execute store result score $_0 rs run random value 1 6')
|
|
662
662
|
})
|
|
663
663
|
|
|
664
664
|
it('compiles random_native() with zero min', () => {
|
|
665
665
|
const source = 'fn test() { let x: int = random_native(0, 100); }'
|
|
666
666
|
const files = compile(source)
|
|
667
667
|
const fn = getFunction(files, 'test')
|
|
668
|
-
expect(fn).toContain('execute store result score $
|
|
668
|
+
expect(fn).toContain('execute store result score $_0 rs run random value 0 100')
|
|
669
669
|
})
|
|
670
670
|
|
|
671
671
|
it('compiles random_sequence()', () => {
|
|
@@ -1188,7 +1188,7 @@ fn handle_claim() {
|
|
|
1188
1188
|
})
|
|
1189
1189
|
})
|
|
1190
1190
|
|
|
1191
|
-
describe('Real program: zombie_game.
|
|
1191
|
+
describe('Real program: zombie_game.mcrs', () => {
|
|
1192
1192
|
const source = `
|
|
1193
1193
|
// A zombie survival game logic
|
|
1194
1194
|
// Kills nearby zombies and tracks score
|
|
@@ -1719,3 +1719,132 @@ describe('NBT parameters', () => {
|
|
|
1719
1719
|
expect(fn).toContain('{Unbreakable:1b}')
|
|
1720
1720
|
})
|
|
1721
1721
|
})
|
|
1722
|
+
|
|
1723
|
+
// ---------------------------------------------------------------------------
|
|
1724
|
+
// Set Operations
|
|
1725
|
+
// ---------------------------------------------------------------------------
|
|
1726
|
+
|
|
1727
|
+
describe('Set operations', () => {
|
|
1728
|
+
it('creates a new set', () => {
|
|
1729
|
+
const src = `fn test() { let s = set_new(); }`
|
|
1730
|
+
const files = compile(src, 'settest')
|
|
1731
|
+
const fn = getFunction(files, 'test')
|
|
1732
|
+
expect(fn).toContain('data modify storage rs:sets __set_0 set value []')
|
|
1733
|
+
})
|
|
1734
|
+
|
|
1735
|
+
it('adds to a set with uniqueness check', () => {
|
|
1736
|
+
const src = `fn test() { let s = set_new(); set_add(s, "apple"); }`
|
|
1737
|
+
const files = compile(src, 'setadd')
|
|
1738
|
+
const fn = getFunction(files, 'test')
|
|
1739
|
+
expect(fn).toContain('execute unless data storage rs:sets __set_0[{value:apple}] run data modify storage rs:sets __set_0 append value {value:apple}')
|
|
1740
|
+
})
|
|
1741
|
+
|
|
1742
|
+
it('checks set membership', () => {
|
|
1743
|
+
const src = `fn test() { let s = set_new(); set_add(s, "x"); let has = set_contains(s, "x"); }`
|
|
1744
|
+
const files = compile(src, 'setcontains')
|
|
1745
|
+
const fn = getFunction(files, 'test')
|
|
1746
|
+
expect(fn).toContain('if data storage rs:sets __set_0[{value:x}]')
|
|
1747
|
+
})
|
|
1748
|
+
|
|
1749
|
+
it('removes from a set', () => {
|
|
1750
|
+
const src = `fn test() { let s = set_new(); set_add(s, "y"); set_remove(s, "y"); }`
|
|
1751
|
+
const files = compile(src, 'setremove')
|
|
1752
|
+
const fn = getFunction(files, 'test')
|
|
1753
|
+
expect(fn).toContain('data remove storage rs:sets __set_0[{value:y}]')
|
|
1754
|
+
})
|
|
1755
|
+
|
|
1756
|
+
it('clears a set', () => {
|
|
1757
|
+
const src = `fn test() { let s = set_new(); set_clear(s); }`
|
|
1758
|
+
const files = compile(src, 'setclear')
|
|
1759
|
+
const fn = getFunction(files, 'test')
|
|
1760
|
+
expect(fn).toContain('data modify storage rs:sets __set_0 set value []')
|
|
1761
|
+
})
|
|
1762
|
+
})
|
|
1763
|
+
|
|
1764
|
+
// ---------------------------------------------------------------------------
|
|
1765
|
+
// Method Syntax Sugar
|
|
1766
|
+
// ---------------------------------------------------------------------------
|
|
1767
|
+
|
|
1768
|
+
describe('Method syntax sugar', () => {
|
|
1769
|
+
it('transforms obj.method() to method(obj)', () => {
|
|
1770
|
+
const src = `fn test() { let s = set_new(); s.clear(); }`
|
|
1771
|
+
const files = compile(src, 'method1')
|
|
1772
|
+
const fn = getFunction(files, 'test')
|
|
1773
|
+
expect(fn).toContain('data modify storage rs:sets __set_0 set value []')
|
|
1774
|
+
})
|
|
1775
|
+
|
|
1776
|
+
it('transforms obj.method(arg) to method(obj, arg)', () => {
|
|
1777
|
+
const src = `fn test() { let s = set_new(); s.add("apple"); }`
|
|
1778
|
+
const files = compile(src, 'method2')
|
|
1779
|
+
const fn = getFunction(files, 'test')
|
|
1780
|
+
expect(fn).toContain('data modify storage rs:sets __set_0 append value {value:apple}')
|
|
1781
|
+
})
|
|
1782
|
+
|
|
1783
|
+
it('transforms obj.method(arg) with contains', () => {
|
|
1784
|
+
const src = `fn test() { let s = set_new(); s.add("x"); let r = s.contains("x"); }`
|
|
1785
|
+
const files = compile(src, 'method3')
|
|
1786
|
+
const fn = getFunction(files, 'test')
|
|
1787
|
+
expect(fn).toBeDefined()
|
|
1788
|
+
})
|
|
1789
|
+
|
|
1790
|
+
it('works with multiple args', () => {
|
|
1791
|
+
const src = `fn test() { let s = set_new(); s.add("a"); s.add("b"); s.remove("a"); }`
|
|
1792
|
+
const files = compile(src, 'method4')
|
|
1793
|
+
const fn = getFunction(files, 'test')
|
|
1794
|
+
expect(fn).toContain('data remove storage rs:sets __set_0[{value:a}]')
|
|
1795
|
+
})
|
|
1796
|
+
})
|
|
1797
|
+
|
|
1798
|
+
describe('Global variables', () => {
|
|
1799
|
+
it('initializes global in __load', () => {
|
|
1800
|
+
const src = `let x: int = 42;\nfn test() { say("hi"); }`
|
|
1801
|
+
const files = compile(src, 'globaltest')
|
|
1802
|
+
const load = getFunction(files, '__load')
|
|
1803
|
+
expect(load).toContain('scoreboard players set $x rs 42')
|
|
1804
|
+
})
|
|
1805
|
+
|
|
1806
|
+
it('reads and writes global in function', () => {
|
|
1807
|
+
const src = `let count: int = 0;\nfn inc() { count = count + 1; }`
|
|
1808
|
+
const files = compile(src, 'globalrw')
|
|
1809
|
+
const fn = getFunction(files, 'inc')
|
|
1810
|
+
expect(fn).toBeDefined()
|
|
1811
|
+
// Global should be initialized in __load
|
|
1812
|
+
const load = getFunction(files, '__load')
|
|
1813
|
+
expect(load).toContain('scoreboard players set $count rs 0')
|
|
1814
|
+
})
|
|
1815
|
+
|
|
1816
|
+
it('const cannot be reassigned', () => {
|
|
1817
|
+
const src = `const X: int = 5;\nfn bad() { X = 10; }`
|
|
1818
|
+
expect(() => compile(src, 'constbad')).toThrow()
|
|
1819
|
+
})
|
|
1820
|
+
})
|
|
1821
|
+
|
|
1822
|
+
describe('@load decorator', () => {
|
|
1823
|
+
it('calls @load function from __load.mcfunction', () => {
|
|
1824
|
+
const src = `@load fn init() { say("Datapack loaded!"); }`
|
|
1825
|
+
const files = compile(src, 'loadtest')
|
|
1826
|
+
const load = getFunction(files, '__load')
|
|
1827
|
+
expect(load).toContain('function loadtest:init')
|
|
1828
|
+
})
|
|
1829
|
+
|
|
1830
|
+
it('calls multiple @load functions in order', () => {
|
|
1831
|
+
const src = `
|
|
1832
|
+
@load fn setup() { say("setup"); }
|
|
1833
|
+
@load fn init() { say("init"); }
|
|
1834
|
+
`
|
|
1835
|
+
const files = compile(src, 'loadtest')
|
|
1836
|
+
const load = getFunction(files, '__load')!
|
|
1837
|
+
const setupIdx = load.indexOf('function loadtest:setup')
|
|
1838
|
+
const initIdx = load.indexOf('function loadtest:init')
|
|
1839
|
+
expect(setupIdx).toBeGreaterThan(-1)
|
|
1840
|
+
expect(initIdx).toBeGreaterThan(-1)
|
|
1841
|
+
expect(setupIdx).toBeLessThan(initIdx)
|
|
1842
|
+
})
|
|
1843
|
+
|
|
1844
|
+
it('generates the @load function body normally', () => {
|
|
1845
|
+
const src = `@load fn init() { say("hi"); }`
|
|
1846
|
+
const files = compile(src, 'loadtest')
|
|
1847
|
+
const fn = getFunction(files, 'init')
|
|
1848
|
+
expect(fn).toContain('say hi')
|
|
1849
|
+
})
|
|
1850
|
+
})
|
|
@@ -635,7 +635,7 @@ fn test() {
|
|
|
635
635
|
expect(rawCmds).toContain('bossbar set ns:health visible true')
|
|
636
636
|
expect(rawCmds).toContain('bossbar set ns:health players @a')
|
|
637
637
|
expect(rawCmds).toContain('bossbar remove ns:health')
|
|
638
|
-
expect(rawCmds.some(cmd => /^execute store result score \$
|
|
638
|
+
expect(rawCmds.some(cmd => /^execute store result score \$_\d+ rs run bossbar get ns:health value$/.test(cmd))).toBe(true)
|
|
639
639
|
})
|
|
640
640
|
|
|
641
641
|
it('lowers team management builtins', () => {
|
|
@@ -665,14 +665,14 @@ fn test() {
|
|
|
665
665
|
const ir = compile('fn test() { let x: int = random(1, 100); }')
|
|
666
666
|
const fn = getFunction(ir, 'test')!
|
|
667
667
|
const rawCmds = getRawCommands(fn)
|
|
668
|
-
expect(rawCmds).toContain('scoreboard players random $
|
|
668
|
+
expect(rawCmds).toContain('scoreboard players random $_0 rs 1 100')
|
|
669
669
|
})
|
|
670
670
|
|
|
671
671
|
it('lowers random_native()', () => {
|
|
672
672
|
const ir = compile('fn test() { let x: int = random_native(1, 6); }')
|
|
673
673
|
const fn = getFunction(ir, 'test')!
|
|
674
674
|
const rawCmds = getRawCommands(fn)
|
|
675
|
-
expect(rawCmds).toContain('execute store result score $
|
|
675
|
+
expect(rawCmds).toContain('execute store result score $_0 rs run random value 1 6')
|
|
676
676
|
})
|
|
677
677
|
|
|
678
678
|
it('lowers random_sequence()', () => {
|
|
@@ -959,4 +959,49 @@ fn count_down() {
|
|
|
959
959
|
expect(bodyBlock).toBeDefined()
|
|
960
960
|
})
|
|
961
961
|
})
|
|
962
|
+
|
|
963
|
+
describe('Global variables', () => {
|
|
964
|
+
it('registers global in IR globals with init value', () => {
|
|
965
|
+
const ir = compile('let x: int = 42;\nfn test() { say("hi"); }')
|
|
966
|
+
expect(ir.globals).toContainEqual({ name: '$x', init: 42 })
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
it('reads global variable in function body', () => {
|
|
970
|
+
const ir = compile('let count: int = 0;\nfn test() { let y: int = count; }')
|
|
971
|
+
const fn = getFunction(ir, 'test')!
|
|
972
|
+
const instrs = getInstructions(fn)
|
|
973
|
+
expect(instrs.some(i =>
|
|
974
|
+
i.op === 'assign' && i.dst === '$y' && (i.src as any).kind === 'var' && (i.src as any).name === '$count'
|
|
975
|
+
)).toBe(true)
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
it('writes global variable in function body', () => {
|
|
979
|
+
const ir = compile('let count: int = 0;\nfn inc() { count = 5; }')
|
|
980
|
+
const fn = getFunction(ir, 'inc')!
|
|
981
|
+
const instrs = getInstructions(fn)
|
|
982
|
+
expect(instrs.some(i =>
|
|
983
|
+
i.op === 'assign' && i.dst === '$count' && (i.src as any).kind === 'const' && (i.src as any).value === 5
|
|
984
|
+
)).toBe(true)
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
it('compound assignment on global variable', () => {
|
|
988
|
+
const ir = compile('let count: int = 0;\nfn inc() { count += 1; }')
|
|
989
|
+
const fn = getFunction(ir, 'inc')!
|
|
990
|
+
const instrs = getInstructions(fn)
|
|
991
|
+
expect(instrs.some(i =>
|
|
992
|
+
i.op === 'binop' && (i.lhs as any).name === '$count' && i.bop === '+' && (i.rhs as any).value === 1
|
|
993
|
+
)).toBe(true)
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
it('const cannot be reassigned', () => {
|
|
997
|
+
const src = 'const X: int = 5;\nfn bad() { X = 10; }'
|
|
998
|
+
expect(() => compile(src)).toThrow(/Cannot assign to constant/)
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
it('multiple globals with different init values', () => {
|
|
1002
|
+
const ir = compile('let a: int = 10;\nlet b: int = 20;\nfn test() { a = b; }')
|
|
1003
|
+
expect(ir.globals).toContainEqual({ name: '$a', init: 10 })
|
|
1004
|
+
expect(ir.globals).toContainEqual({ name: '$b', init: 20 })
|
|
1005
|
+
})
|
|
1006
|
+
})
|
|
962
1007
|
})
|