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.
Files changed (106) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
  4. package/CHANGELOG.md +58 -0
  5. package/CONTRIBUTING.md +140 -0
  6. package/README.md +28 -19
  7. package/README.zh.md +28 -19
  8. package/dist/__tests__/cli.test.js +10 -10
  9. package/dist/__tests__/codegen.test.js +1 -1
  10. package/dist/__tests__/diagnostics.test.js +5 -5
  11. package/dist/__tests__/e2e.test.js +146 -5
  12. package/dist/__tests__/formatter.test.d.ts +1 -0
  13. package/dist/__tests__/formatter.test.js +40 -0
  14. package/dist/__tests__/lowering.test.js +36 -3
  15. package/dist/__tests__/mc-integration.test.js +255 -10
  16. package/dist/__tests__/mc-syntax.test.js +3 -3
  17. package/dist/__tests__/nbt.test.js +2 -2
  18. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  19. package/dist/__tests__/runtime.test.js +1 -1
  20. package/dist/ast/types.d.ts +21 -3
  21. package/dist/cli.js +25 -7
  22. package/dist/codegen/mcfunction/index.d.ts +1 -1
  23. package/dist/codegen/mcfunction/index.js +8 -2
  24. package/dist/codegen/structure/index.js +7 -1
  25. package/dist/formatter/index.d.ts +1 -0
  26. package/dist/formatter/index.js +26 -0
  27. package/dist/ir/builder.d.ts +2 -1
  28. package/dist/ir/types.d.ts +7 -2
  29. package/dist/ir/types.js +1 -1
  30. package/dist/lowering/index.d.ts +2 -0
  31. package/dist/lowering/index.js +183 -8
  32. package/dist/mc-test/runner.d.ts +2 -2
  33. package/dist/mc-test/runner.js +3 -3
  34. package/dist/mc-test/setup.js +2 -2
  35. package/dist/parser/index.d.ts +2 -0
  36. package/dist/parser/index.js +75 -7
  37. package/docs/COMPILATION_STATS.md +24 -24
  38. package/docs/IMPLEMENTATION_GUIDE.md +1 -1
  39. package/docs/STRUCTURE_TARGET.md +1 -1
  40. package/editors/vscode/.vscodeignore +1 -0
  41. package/editors/vscode/icons/mcrs.svg +7 -0
  42. package/editors/vscode/icons/redscript-icons.json +10 -0
  43. package/editors/vscode/out/extension.js +152 -9
  44. package/editors/vscode/package.json +10 -3
  45. package/editors/vscode/src/hover.ts +55 -2
  46. package/editors/vscode/src/symbols.ts +42 -0
  47. package/package.json +1 -1
  48. package/src/__tests__/cli.test.ts +10 -10
  49. package/src/__tests__/codegen.test.ts +1 -1
  50. package/src/__tests__/diagnostics.test.ts +5 -5
  51. package/src/__tests__/e2e.test.ts +134 -5
  52. package/src/__tests__/lowering.test.ts +48 -3
  53. package/src/__tests__/mc-integration.test.ts +285 -10
  54. package/src/__tests__/mc-syntax.test.ts +3 -3
  55. package/src/__tests__/nbt.test.ts +2 -2
  56. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  57. package/src/__tests__/runtime.test.ts +1 -1
  58. package/src/ast/types.ts +20 -3
  59. package/src/cli.ts +10 -10
  60. package/src/codegen/mcfunction/index.ts +9 -2
  61. package/src/codegen/structure/index.ts +8 -1
  62. package/src/examples/capture_the_flag.mcrs +208 -0
  63. package/src/examples/{counter.rs → counter.mcrs} +1 -1
  64. package/src/examples/hunger_games.mcrs +301 -0
  65. package/src/examples/new_features_demo.mcrs +193 -0
  66. package/src/examples/parkour_race.mcrs +233 -0
  67. package/src/examples/rpg.mcrs +13 -0
  68. package/src/examples/{shop.rs → shop.mcrs} +1 -1
  69. package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
  70. package/src/examples/{turret.rs → turret.mcrs} +1 -1
  71. package/src/examples/zombie_survival.mcrs +314 -0
  72. package/src/ir/builder.ts +3 -1
  73. package/src/ir/types.ts +8 -2
  74. package/src/lowering/index.ts +156 -8
  75. package/src/mc-test/runner.ts +3 -3
  76. package/src/mc-test/setup.ts +2 -2
  77. package/src/parser/index.ts +81 -8
  78. package/src/stdlib/README.md +155 -147
  79. package/src/stdlib/bossbar.mcrs +68 -0
  80. package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
  81. package/src/stdlib/effects.mcrs +64 -0
  82. package/src/stdlib/interactions.mcrs +195 -0
  83. package/src/stdlib/inventory.mcrs +38 -0
  84. package/src/stdlib/mobs.mcrs +99 -0
  85. package/src/stdlib/particles.mcrs +52 -0
  86. package/src/stdlib/sets.mcrs +20 -0
  87. package/src/stdlib/spawn.mcrs +41 -0
  88. package/src/stdlib/teams.mcrs +68 -0
  89. package/src/stdlib/world.mcrs +92 -0
  90. package/src/examples/rpg.rs +0 -13
  91. package/src/stdlib/mobs.rs +0 -99
  92. /package/src/examples/{arena.rs → arena.mcrs} +0 -0
  93. /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
  94. /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
  95. /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
  96. /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
  97. /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
  98. /package/src/stdlib/{math.rs → math.mcrs} +0 -0
  99. /package/src/stdlib/{player.rs → player.mcrs} +0 -0
  100. /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
  101. /package/src/stdlib/{timer.rs → timer.mcrs} +0 -0
  102. /package/src/templates/{combat.rs → combat.mcrs} +0 -0
  103. /package/src/templates/{economy.rs → economy.mcrs} +0 -0
  104. /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
  105. /package/src/templates/{quest.rs → quest.mcrs} +0 -0
  106. /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_\\[\\]]*))?`, 'm')
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
- const returnType = match[2]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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.rs')
14
- const mainPath = path.join(tempDir, 'main.rs')
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.rs"\n\nfn main() { let value: int = double(2); }\n')
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.rs')
30
- const bPath = path.join(tempDir, 'b.rs')
31
- const mainPath = path.join(tempDir, 'main.rs')
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.rs"\n\nfn from_a() -> int { return 1; }\n')
34
- fs.writeFileSync(bPath, 'import "./a.rs"\n\nfn from_b() -> int { return from_a(); }\n')
35
- fs.writeFileSync(mainPath, 'import "./a.rs"\n\nfn main() { let value: int = from_b(); }\n')
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.rs')
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.rs', line: 1, col: 9 },
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.rs at line 1, col 9:')
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.rs', line: 1, col: 1 },
69
+ { file: 'test.mcrs', line: 1, col: 1 },
70
70
  ['@@@']
71
71
  )
72
72
  const formatted = error.format()
73
- expect(formatted).toContain('test.rs:')
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.rs' })
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 \$t\d+ rs run bossbar get ns:health value/)
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 $t0 rs 1 10')
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 $t0 rs run random value 1 6')
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 $t0 rs run random value 0 100')
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.rs', () => {
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 \$t\d+ rs run bossbar get ns:health value$/.test(cmd))).toBe(true)
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 $t0 rs 1 100')
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 $t0 rs run random value 1 6')
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
  })