redscript-mc 1.2.24 → 1.2.26

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 (58) hide show
  1. package/.github/workflows/publish-extension-on-ci.yml +1 -0
  2. package/dist/__tests__/cli.test.js +1 -1
  3. package/dist/__tests__/codegen.test.js +12 -6
  4. package/dist/__tests__/e2e.test.js +6 -6
  5. package/dist/__tests__/lowering.test.js +8 -8
  6. package/dist/__tests__/optimizer.test.js +31 -0
  7. package/dist/__tests__/stdlib-advanced.test.d.ts +4 -0
  8. package/dist/__tests__/stdlib-advanced.test.js +264 -0
  9. package/dist/__tests__/stdlib-math.test.d.ts +7 -0
  10. package/dist/__tests__/stdlib-math.test.js +352 -0
  11. package/dist/__tests__/stdlib-vec.test.d.ts +4 -0
  12. package/dist/__tests__/stdlib-vec.test.js +264 -0
  13. package/dist/ast/types.d.ts +17 -1
  14. package/dist/codegen/mcfunction/index.js +159 -18
  15. package/dist/codegen/var-allocator.d.ts +17 -0
  16. package/dist/codegen/var-allocator.js +33 -3
  17. package/dist/compile.d.ts +14 -0
  18. package/dist/compile.js +62 -5
  19. package/dist/index.js +20 -1
  20. package/dist/ir/types.d.ts +4 -0
  21. package/dist/lexer/index.d.ts +1 -1
  22. package/dist/lexer/index.js +1 -0
  23. package/dist/lowering/index.d.ts +5 -0
  24. package/dist/lowering/index.js +83 -10
  25. package/dist/optimizer/dce.js +21 -5
  26. package/dist/optimizer/passes.js +18 -6
  27. package/dist/optimizer/structure.js +7 -0
  28. package/dist/parser/index.d.ts +5 -0
  29. package/dist/parser/index.js +43 -2
  30. package/dist/runtime/index.d.ts +6 -0
  31. package/dist/runtime/index.js +109 -9
  32. package/editors/vscode/package-lock.json +3 -3
  33. package/editors/vscode/package.json +1 -1
  34. package/package.json +1 -1
  35. package/src/__tests__/cli.test.ts +1 -1
  36. package/src/__tests__/codegen.test.ts +12 -6
  37. package/src/__tests__/e2e.test.ts +6 -6
  38. package/src/__tests__/lowering.test.ts +8 -8
  39. package/src/__tests__/optimizer.test.ts +33 -0
  40. package/src/__tests__/stdlib-advanced.test.ts +259 -0
  41. package/src/__tests__/stdlib-math.test.ts +374 -0
  42. package/src/__tests__/stdlib-vec.test.ts +259 -0
  43. package/src/ast/types.ts +11 -1
  44. package/src/codegen/mcfunction/index.ts +148 -19
  45. package/src/codegen/var-allocator.ts +36 -3
  46. package/src/compile.ts +72 -5
  47. package/src/index.ts +21 -1
  48. package/src/ir/types.ts +2 -0
  49. package/src/lexer/index.ts +2 -1
  50. package/src/lowering/index.ts +96 -10
  51. package/src/optimizer/dce.ts +22 -5
  52. package/src/optimizer/passes.ts +18 -5
  53. package/src/optimizer/structure.ts +6 -1
  54. package/src/parser/index.ts +47 -2
  55. package/src/runtime/index.ts +108 -10
  56. package/src/stdlib/advanced.mcrs +249 -0
  57. package/src/stdlib/math.mcrs +259 -19
  58. package/src/stdlib/vec.mcrs +246 -0
@@ -19,26 +19,32 @@ describe('generateDatapack', () => {
19
19
  })
20
20
 
21
21
  it('generates function file for simple add(a, b)', () => {
22
+ // IR now uses { kind: 'param', index: i } for param-copy instructions,
23
+ // matching what the lowering emits. Pass mangle:false so we can check
24
+ // readable names without worrying about sequential mangled names.
22
25
  const mod: IRModule = {
23
26
  namespace: 'mypack',
24
27
  globals: [],
25
28
  functions: [{
26
29
  name: 'add',
27
- params: ['a', 'b'],
28
- locals: ['a', 'b', 'result'],
30
+ params: ['$a', '$b'],
31
+ locals: ['$a', '$b', '$result'],
29
32
  blocks: [{
30
33
  label: 'entry',
31
34
  instrs: [
32
- { op: 'binop', dst: 'result', lhs: { kind: 'var', name: 'a' }, bop: '+', rhs: { kind: 'var', name: 'b' } },
35
+ // param-copy instructions (what the lowering now emits)
36
+ { op: 'assign', dst: '$a', src: { kind: 'param', index: 0 } },
37
+ { op: 'assign', dst: '$b', src: { kind: 'param', index: 1 } },
38
+ { op: 'binop', dst: '$result', lhs: { kind: 'var', name: '$a' }, bop: '+', rhs: { kind: 'var', name: '$b' } },
33
39
  ],
34
- term: { op: 'return', value: { kind: 'var', name: 'result' } },
40
+ term: { op: 'return', value: { kind: 'var', name: '$result' } },
35
41
  }],
36
42
  }],
37
43
  }
38
- const files = generateDatapack(mod)
44
+ const files = generateDatapackWithStats(mod, { mangle: false }).files
39
45
  const fn = files.find(f => f.path.includes('add.mcfunction'))
40
46
  expect(fn).toBeDefined()
41
- // Should have param setup
47
+ // param setup emitted from the IR
42
48
  expect(fn!.content).toContain('scoreboard players operation $a rs = $p0 rs')
43
49
  expect(fn!.content).toContain('scoreboard players operation $b rs = $p1 rs')
44
50
  // Should have add operation
@@ -124,7 +124,7 @@ fn main() {
124
124
  `)
125
125
  const mainFn = getFunction(files, 'main')
126
126
  expect(mainFn).toBeDefined()
127
- expect(mainFn).toContain('scoreboard players set $hp rs 105')
127
+ expect(mainFn).toContain('scoreboard players set $main_hp rs 105')
128
128
  expect(mainFn).toContain('Arena Battle')
129
129
  })
130
130
  })
@@ -1852,7 +1852,7 @@ describe('for-range loop', () => {
1852
1852
  const src = `fn test() { for i in 0..5 { say("hi"); } }`
1853
1853
  const files = compile(src, 'forloop')
1854
1854
  const fn = getFunction(files, 'test')
1855
- expect(fn).toContain('scoreboard players set $i rs 0')
1855
+ expect(fn).toContain('scoreboard players set $test_i rs 0')
1856
1856
  })
1857
1857
 
1858
1858
  it('generates loop sub-function with increment and condition', () => {
@@ -1861,17 +1861,17 @@ describe('for-range loop', () => {
1861
1861
  const subFn = files.find(f => f.path.includes('__for_0'))
1862
1862
  expect(subFn).toBeDefined()
1863
1863
  expect(subFn?.content).toContain('say hi')
1864
- expect(subFn?.content).toContain('scoreboard players add $i rs 1')
1865
- expect(subFn?.content).toContain('execute if score $i rs matches ..4 run function forloop:test/__for_0')
1864
+ expect(subFn?.content).toContain('scoreboard players add $test_i rs 1')
1865
+ expect(subFn?.content).toContain('execute if score $test_i rs matches ..4 run function forloop:test/__for_0')
1866
1866
  })
1867
1867
 
1868
1868
  it('supports non-zero start', () => {
1869
1869
  const src = `fn test() { for x in 3..8 { say("loop"); } }`
1870
1870
  const files = compile(src, 'forloop2')
1871
1871
  const fn = getFunction(files, 'test')
1872
- expect(fn).toContain('scoreboard players set $x rs 3')
1872
+ expect(fn).toContain('scoreboard players set $test_x rs 3')
1873
1873
  const subFn = files.find(f => f.path.includes('__for_0'))
1874
- expect(subFn?.content).toContain('execute if score $x rs matches ..7 run function forloop2:test/__for_0')
1874
+ expect(subFn?.content).toContain('execute if score $test_x rs matches ..7 run function forloop2:test/__for_0')
1875
1875
  })
1876
1876
  })
1877
1877
 
@@ -50,7 +50,7 @@ describe('Lowering', () => {
50
50
  const fn = getFunction(ir, 'foo')!
51
51
  const instrs = getInstructions(fn)
52
52
  expect(instrs.some(i =>
53
- i.op === 'assign' && i.dst === '$x' && (i.src as any).name === '$p0'
53
+ i.op === 'assign' && i.dst === '$foo_x' && (i.src as any).kind === 'param' && (i.src as any).index === 0
54
54
  )).toBe(true)
55
55
  })
56
56
 
@@ -139,7 +139,7 @@ fn foo() {
139
139
  const fn = getFunction(ir, 'foo')!
140
140
  const instrs = getInstructions(fn)
141
141
  expect(instrs.some(i =>
142
- i.op === 'assign' && i.dst === '$x' && (i.src as any).kind === 'const' && (i.src as any).value === 100
142
+ i.op === 'assign' && i.dst === '$foo_x' && (i.src as any).kind === 'const' && (i.src as any).value === 100
143
143
  )).toBe(true)
144
144
  expect(ir.globals).not.toContain('$MAX_HP')
145
145
  })
@@ -149,7 +149,7 @@ fn foo() {
149
149
  const fn = getFunction(ir, 'foo')!
150
150
  const instrs = getInstructions(fn)
151
151
  expect(instrs.some(i =>
152
- i.op === 'assign' && i.dst === '$x' && (i.src as any).value === 42
152
+ i.op === 'assign' && i.dst === '$foo_x' && (i.src as any).value === 42
153
153
  )).toBe(true)
154
154
  })
155
155
 
@@ -579,16 +579,16 @@ fn choose(dir: Direction) {
579
579
  const ir = compile('fn test() { let score: int = 7; say("You have ${score} points"); }')
580
580
  const fn = getFunction(ir, 'test')!
581
581
  const rawCmds = getRawCommands(fn)
582
- expect(rawCmds).toContain('tellraw @a ["",{"text":"You have "},{"score":{"name":"$score","objective":"rs"}},{"text":" points"}]')
582
+ expect(rawCmds).toContain('tellraw @a ["",{"text":"You have "},{"score":{"name":"$test_score","objective":"rs"}},{"text":" points"}]')
583
583
  })
584
584
 
585
585
  it('lowers f-string output builtins to tellraw/title JSON components', () => {
586
586
  const ir = compile('fn test() { let score: int = 7; say(f"Score: {score}"); tellraw(@a, f"Score: {score}"); actionbar(@s, f"Score: {score}"); title(@s, f"Score: {score}"); }')
587
587
  const fn = getFunction(ir, 'test')!
588
588
  const rawCmds = getRawCommands(fn)
589
- expect(rawCmds).toContain('tellraw @a ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]')
590
- expect(rawCmds).toContain('title @s actionbar ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]')
591
- expect(rawCmds).toContain('title @s title ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]')
589
+ expect(rawCmds).toContain('tellraw @a ["",{"text":"Score: "},{"score":{"name":"$test_score","objective":"rs"}}]')
590
+ expect(rawCmds).toContain('title @s actionbar ["",{"text":"Score: "},{"score":{"name":"$test_score","objective":"rs"}}]')
591
+ expect(rawCmds).toContain('title @s title ["",{"text":"Score: "},{"score":{"name":"$test_score","objective":"rs"}}]')
592
592
  })
593
593
 
594
594
  it('lowers summon()', () => {
@@ -1149,7 +1149,7 @@ fn count_down() {
1149
1149
  const fn = getFunction(ir, 'test')!
1150
1150
  const instrs = getInstructions(fn)
1151
1151
  expect(instrs.some(i =>
1152
- i.op === 'assign' && i.dst === '$y' && (i.src as any).kind === 'var' && (i.src as any).name === '$count'
1152
+ i.op === 'assign' && i.dst === '$test_y' && (i.src as any).kind === 'var' && (i.src as any).name === '$count'
1153
1153
  )).toBe(true)
1154
1154
  })
1155
1155
 
@@ -106,6 +106,39 @@ describe('deadCodeElimination', () => {
106
106
  })
107
107
  })
108
108
 
109
+ describe('copyPropagation – stale alias invalidation', () => {
110
+ it('does not propagate $tmp = $y after $y is overwritten (swap pattern)', () => {
111
+ // Simulates: let tmp = y; y = x % y; x = tmp
112
+ // The copy $tmp = $y must be invalidated when $y is reassigned.
113
+ // Before fix: x = tmp was propagated to x = y (new y, wrong value).
114
+ const fn = makeFn([
115
+ { op: 'assign', dst: '$tmp', src: { kind: 'var', name: '$y' } }, // tmp = y
116
+ { op: 'binop', dst: '$r', lhs: { kind: 'var', name: '$x' }, bop: '%', rhs: { kind: 'var', name: '$y' } }, // r = x%y
117
+ { op: 'assign', dst: '$y', src: { kind: 'var', name: '$r' } }, // y = r ← stale: tmp still points to OLD y
118
+ { op: 'assign', dst: '$x', src: { kind: 'var', name: '$tmp' } }, // x = tmp (should NOT be x = y)
119
+ ])
120
+ const opt = copyPropagation(fn)
121
+ const instrs = opt.blocks[0].instrs
122
+ const xAssign = instrs.find((i: any) => i.dst === '$x') as any
123
+ // x = tmp must NOT be optimised to x = $y (stale) or x = $r (new y).
124
+ // It should stay as x = $tmp (the original copy).
125
+ expect(xAssign.src).toEqual({ kind: 'var', name: '$tmp' })
126
+ })
127
+
128
+ it('still propagates simple non-conflicting copies', () => {
129
+ // a = 5; b = a; c = b → after propagation b and c should both be const 5
130
+ const fn = makeFn([
131
+ { op: 'assign', dst: '$a', src: { kind: 'const', value: 5 } },
132
+ { op: 'assign', dst: '$b', src: { kind: 'var', name: '$a' } },
133
+ { op: 'assign', dst: '$c', src: { kind: 'var', name: '$b' } },
134
+ ])
135
+ const opt = copyPropagation(fn)
136
+ const instrs = opt.blocks[0].instrs
137
+ const cAssign = instrs.find((i: any) => i.dst === '$c') as any
138
+ expect(cAssign.src).toEqual({ kind: 'const', value: 5 })
139
+ })
140
+ })
141
+
109
142
  describe('optimize pipeline', () => {
110
143
  it('combines all passes', () => {
111
144
  // t0 = 2 + 3 (→ constant fold → t0 = 5)
@@ -0,0 +1,259 @@
1
+ /**
2
+ * stdlib/advanced.mcrs — runtime behavioural tests
3
+ */
4
+
5
+ import * as fs from 'fs'
6
+ import * as path from 'path'
7
+ import { compile } from '../compile'
8
+ import { MCRuntime } from '../runtime'
9
+
10
+ const MATH_SRC = fs.readFileSync(path.join(__dirname, '../../src/stdlib/math.mcrs'), 'utf-8')
11
+ const ADV_SRC = fs.readFileSync(path.join(__dirname, '../../src/stdlib/advanced.mcrs'), 'utf-8')
12
+
13
+ function run(driver: string): MCRuntime {
14
+ const result = compile(driver, {
15
+ namespace: 'advtest',
16
+ librarySources: [MATH_SRC, ADV_SRC],
17
+ })
18
+ if (!result.success) throw new Error(result.error?.message ?? 'compile failed')
19
+ const rt = new MCRuntime('advtest')
20
+ for (const file of result.files ?? []) {
21
+ if (!file.path.endsWith('.mcfunction')) continue
22
+ const match = file.path.match(/data\/([^/]+)\/function\/(.+)\.mcfunction$/)
23
+ if (!match) continue
24
+ rt.loadFunction(`${match[1]}:${match[2]}`, file.content.split('\n'))
25
+ }
26
+ rt.load()
27
+ return rt
28
+ }
29
+
30
+ function scoreOf(rt: MCRuntime, key: string): number {
31
+ return rt.getScore('out', `advtest.${key}`)
32
+ }
33
+
34
+ // ─── fib ─────────────────────────────────────────────────────────────────────
35
+
36
+ describe('fib', () => {
37
+ it.each([
38
+ [0, 0],
39
+ [1, 1],
40
+ [2, 1],
41
+ [5, 5],
42
+ [10, 55],
43
+ [20, 6765],
44
+ ])('fib(%d) == %d', (n, expected) => {
45
+ const rt = run(`fn test() { scoreboard_set("out", "r", fib(${n})); }`)
46
+ rt.execFunction('test')
47
+ expect(scoreOf(rt, 'r')).toBe(expected)
48
+ })
49
+ })
50
+
51
+ // ─── is_prime ─────────────────────────────────────────────────────────────────
52
+
53
+ describe('is_prime', () => {
54
+ it.each([
55
+ [0, 0], [1, 0], [2, 1], [3, 1], [4, 0],
56
+ [7, 1], [9, 0], [11, 1], [97, 1], [100, 0],
57
+ ])('is_prime(%d) == %d', (n, expected) => {
58
+ const rt = run(`fn test() { scoreboard_set("out", "r", is_prime(${n})); }`)
59
+ rt.execFunction('test')
60
+ expect(scoreOf(rt, 'r')).toBe(expected)
61
+ })
62
+ })
63
+
64
+ // ─── collatz_steps ───────────────────────────────────────────────────────────
65
+
66
+ describe('collatz_steps', () => {
67
+ it.each([
68
+ [1, 0],
69
+ [2, 1],
70
+ [4, 2],
71
+ [6, 8],
72
+ [27, 111],
73
+ ])('collatz_steps(%d) == %d', (n, expected) => {
74
+ const rt = run(`fn test() { scoreboard_set("out", "r", collatz_steps(${n})); }`)
75
+ rt.execFunction('test')
76
+ expect(scoreOf(rt, 'r')).toBe(expected)
77
+ })
78
+ })
79
+
80
+ // ─── digit helpers ───────────────────────────────────────────────────────────
81
+
82
+ describe('digit_sum', () => {
83
+ it.each([
84
+ [0, 0], [1, 1], [9, 9], [123, 6], [999, 27], [-42, 6],
85
+ ])('digit_sum(%d) == %d', (n, expected) => {
86
+ const rt = run(`fn test() { scoreboard_set("out", "r", digit_sum(${n})); }`)
87
+ rt.execFunction('test')
88
+ expect(scoreOf(rt, 'r')).toBe(expected)
89
+ })
90
+ })
91
+
92
+ describe('count_digits', () => {
93
+ it.each([
94
+ [0, 1], [9, 1], [10, 2], [100, 3], [9999, 4],
95
+ ])('count_digits(%d) == %d', (n, expected) => {
96
+ const rt = run(`fn test() { scoreboard_set("out", "r", count_digits(${n})); }`)
97
+ rt.execFunction('test')
98
+ expect(scoreOf(rt, 'r')).toBe(expected)
99
+ })
100
+ })
101
+
102
+ describe('reverse_int', () => {
103
+ it.each([
104
+ [12345, 54321],
105
+ [100, 1],
106
+ [7, 7],
107
+ ])('reverse_int(%d) == %d', (n, expected) => {
108
+ const rt = run(`fn test() { scoreboard_set("out", "r", reverse_int(${n})); }`)
109
+ rt.execFunction('test')
110
+ expect(scoreOf(rt, 'r')).toBe(expected)
111
+ })
112
+ })
113
+
114
+ // ─── mod_pow ─────────────────────────────────────────────────────────────────
115
+
116
+ describe('mod_pow', () => {
117
+ it.each([
118
+ [2, 10, 1000, 24], // 2^10 = 1024, 1024 mod 1000 = 24
119
+ [3, 4, 100, 81], // 3^4 = 81
120
+ [2, 0, 10, 1], // any^0 = 1
121
+ [5, 1, 100, 5],
122
+ [7, 3, 13, 343 % 13], // 343 mod 13 = 5
123
+ ])('mod_pow(%d,%d,%d) == %d', (b, e, m, expected) => {
124
+ const rt = run(`fn test() { scoreboard_set("out", "r", mod_pow(${b},${e},${m})); }`)
125
+ rt.execFunction('test')
126
+ expect(scoreOf(rt, 'r')).toBe(expected)
127
+ })
128
+ })
129
+
130
+ // ─── hash_int ────────────────────────────────────────────────────────────────
131
+
132
+ describe('hash_int', () => {
133
+ it('is deterministic', () => {
134
+ const rt1 = run(`fn test() { scoreboard_set("out", "r", hash_int(42)); }`)
135
+ rt1.execFunction('test')
136
+ const rt2 = run(`fn test() { scoreboard_set("out", "r", hash_int(42)); }`)
137
+ rt2.execFunction('test')
138
+ expect(scoreOf(rt1, 'r')).toBe(scoreOf(rt2, 'r'))
139
+ })
140
+
141
+ it('different inputs → different outputs', () => {
142
+ const rt = run(`fn test() {
143
+ scoreboard_set("out", "a", hash_int(0));
144
+ scoreboard_set("out", "b", hash_int(1));
145
+ scoreboard_set("out", "c", hash_int(1000));
146
+ }`)
147
+ rt.execFunction('test')
148
+ const a = scoreOf(rt, 'a')
149
+ const b = scoreOf(rt, 'b')
150
+ const c = scoreOf(rt, 'c')
151
+ expect(a).not.toBe(b)
152
+ expect(b).not.toBe(c)
153
+ })
154
+
155
+ it('output is non-negative', () => {
156
+ for (const n of [-1000, -1, 0, 1, 999, 46340]) {
157
+ const rt = run(`fn test() { scoreboard_set("out", "r", hash_int(${n})); }`)
158
+ rt.execFunction('test')
159
+ expect(scoreOf(rt, 'r')).toBeGreaterThanOrEqual(0)
160
+ }
161
+ })
162
+ })
163
+
164
+ // ─── noise1d ─────────────────────────────────────────────────────────────────
165
+
166
+ describe('noise1d', () => {
167
+ it('output in [0, 999]', () => {
168
+ for (const x of [0, 100, 500, 999, 1000, 2000]) {
169
+ const rt = run(`fn test() { scoreboard_set("out", "r", noise1d(${x})); }`)
170
+ rt.execFunction('test')
171
+ const v = scoreOf(rt, 'r')
172
+ expect(v).toBeGreaterThanOrEqual(0)
173
+ expect(v).toBeLessThan(1000)
174
+ }
175
+ })
176
+
177
+ it('is deterministic', () => {
178
+ const rt1 = run(`fn test() { scoreboard_set("out", "r", noise1d(1234)); }`)
179
+ rt1.execFunction('test')
180
+ const rt2 = run(`fn test() { scoreboard_set("out", "r", noise1d(1234)); }`)
181
+ rt2.execFunction('test')
182
+ expect(scoreOf(rt1, 'r')).toBe(scoreOf(rt2, 'r'))
183
+ })
184
+ })
185
+
186
+ // ─── bezier ──────────────────────────────────────────────────────────────────
187
+
188
+ describe('bezier_quad', () => {
189
+ it.each([
190
+ [0, 500, 1000, 0, 0], // t=0: start
191
+ [0, 500, 1000, 1000, 1000], // t=1000: end
192
+ [0, 1000, 0, 500, 500], // arch midpoint
193
+ ])('bezier_quad(%d,%d,%d,t=%d) == %d', (p0, p1, p2, t, expected) => {
194
+ const rt = run(`fn test() { scoreboard_set("out", "r", bezier_quad(${p0},${p1},${p2},${t})); }`)
195
+ rt.execFunction('test')
196
+ expect(scoreOf(rt, 'r')).toBe(expected)
197
+ })
198
+ })
199
+
200
+ describe('bezier_cubic', () => {
201
+ it('t=0: start', () => {
202
+ const rt = run(`fn test() { scoreboard_set("out", "r", bezier_cubic(0,333,667,1000,0)); }`)
203
+ rt.execFunction('test')
204
+ expect(scoreOf(rt, 'r')).toBe(0)
205
+ })
206
+ it('t=1000: end', () => {
207
+ const rt = run(`fn test() { scoreboard_set("out", "r", bezier_cubic(0,333,667,1000,1000)); }`)
208
+ rt.execFunction('test')
209
+ expect(scoreOf(rt, 'r')).toBe(1000)
210
+ })
211
+ })
212
+
213
+ // ─── mandelbrot_iter ─────────────────────────────────────────────────────────
214
+
215
+ describe('mandelbrot_iter', () => {
216
+ // c = 0 + 0i → always in set (z always 0)
217
+ it('origin (0,0) stays bounded for max_iter=20', () => {
218
+ const rt = run(`fn test() { scoreboard_set("out", "r", mandelbrot_iter(0,0,20)); }`)
219
+ rt.execFunction('test')
220
+ expect(scoreOf(rt, 'r')).toBe(20)
221
+ })
222
+
223
+ // c = 2 + 0i → escapes immediately (|z1| = 2, |z2| = 6 > 2)
224
+ it('c=(2000,0) escapes quickly', () => {
225
+ const rt = run(`fn test() { scoreboard_set("out", "r", mandelbrot_iter(2000,0,50)); }`)
226
+ rt.execFunction('test')
227
+ expect(scoreOf(rt, 'r')).toBeLessThan(5)
228
+ })
229
+
230
+ // c = -1 + 0i → stays bounded (period-2 cycle: 0 → -1 → 0 → ...)
231
+ it('c=(-1000,0) is in the set', () => {
232
+ const rt = run(`fn test() { scoreboard_set("out", "r", mandelbrot_iter(-1000,0,50)); }`)
233
+ rt.execFunction('test')
234
+ expect(scoreOf(rt, 'r')).toBe(50)
235
+ })
236
+
237
+ // c = 0.5 + 0.5i → escapes
238
+ it('c=(500,500) escapes before max_iter=100', () => {
239
+ const rt = run(`fn test() { scoreboard_set("out", "r", mandelbrot_iter(500,500,100)); }`)
240
+ rt.execFunction('test')
241
+ expect(scoreOf(rt, 'r')).toBeLessThan(100)
242
+ })
243
+ })
244
+
245
+ // ─── julia_iter ──────────────────────────────────────────────────────────────
246
+
247
+ describe('julia_iter', () => {
248
+ it('z0=(0,0) with c=(0,0) stays bounded', () => {
249
+ const rt = run(`fn test() { scoreboard_set("out", "r", julia_iter(0,0,0,0,20)); }`)
250
+ rt.execFunction('test')
251
+ expect(scoreOf(rt, 'r')).toBe(20)
252
+ })
253
+
254
+ it('z0=(3000,0) escapes immediately', () => {
255
+ const rt = run(`fn test() { scoreboard_set("out", "r", julia_iter(3000,0,0,0,20)); }`)
256
+ rt.execFunction('test')
257
+ expect(scoreOf(rt, 'r')).toBe(0)
258
+ })
259
+ })