redscript-mc 2.1.1 → 2.3.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 (85) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +66 -21
  3. package/README.zh.md +61 -61
  4. package/dist/src/__tests__/e2e/basic.test.js +25 -0
  5. package/dist/src/__tests__/e2e/coroutine.test.js +22 -0
  6. package/dist/src/__tests__/mc-integration.test.js +25 -13
  7. package/dist/src/__tests__/schedule.test.js +105 -0
  8. package/dist/src/__tests__/tuner/engine.test.d.ts +4 -0
  9. package/dist/src/__tests__/tuner/engine.test.js +232 -0
  10. package/dist/src/__tests__/typechecker.test.js +63 -0
  11. package/dist/src/emit/compile.js +1 -0
  12. package/dist/src/emit/index.js +3 -1
  13. package/dist/src/lir/lower.js +26 -0
  14. package/dist/src/mir/lower.js +341 -12
  15. package/dist/src/mir/types.d.ts +10 -0
  16. package/dist/src/optimizer/copy_prop.js +4 -0
  17. package/dist/src/optimizer/coroutine.d.ts +2 -0
  18. package/dist/src/optimizer/coroutine.js +33 -1
  19. package/dist/src/optimizer/dce.js +7 -1
  20. package/dist/src/optimizer/lir/const_imm.js +1 -1
  21. package/dist/src/optimizer/lir/dead_slot.js +1 -1
  22. package/dist/src/tuner/adapters/ln-polynomial.d.ts +23 -0
  23. package/dist/src/tuner/adapters/ln-polynomial.js +142 -0
  24. package/dist/src/tuner/adapters/sqrt-newton.d.ts +28 -0
  25. package/dist/src/tuner/adapters/sqrt-newton.js +125 -0
  26. package/dist/src/tuner/cli.d.ts +5 -0
  27. package/dist/src/tuner/cli.js +168 -0
  28. package/dist/src/tuner/engine.d.ts +17 -0
  29. package/dist/src/tuner/engine.js +215 -0
  30. package/dist/src/tuner/metrics.d.ts +15 -0
  31. package/dist/src/tuner/metrics.js +51 -0
  32. package/dist/src/tuner/simulator.d.ts +35 -0
  33. package/dist/src/tuner/simulator.js +78 -0
  34. package/dist/src/tuner/types.d.ts +32 -0
  35. package/dist/src/tuner/types.js +6 -0
  36. package/dist/src/typechecker/index.d.ts +2 -0
  37. package/dist/src/typechecker/index.js +29 -0
  38. package/docs/ROADMAP.md +35 -0
  39. package/docs/STDLIB_ROADMAP.md +142 -0
  40. package/editors/vscode/package-lock.json +3 -3
  41. package/editors/vscode/package.json +1 -1
  42. package/examples/coroutine-demo.mcrs +11 -10
  43. package/jest.config.js +19 -0
  44. package/package.json +1 -1
  45. package/src/__tests__/e2e/basic.test.ts +27 -0
  46. package/src/__tests__/e2e/coroutine.test.ts +23 -0
  47. package/src/__tests__/fixtures/array-test.mcrs +21 -22
  48. package/src/__tests__/fixtures/counter.mcrs +17 -0
  49. package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
  50. package/src/__tests__/mc-integration.test.ts +25 -13
  51. package/src/__tests__/schedule.test.ts +112 -0
  52. package/src/__tests__/tuner/engine.test.ts +260 -0
  53. package/src/__tests__/typechecker.test.ts +68 -0
  54. package/src/emit/compile.ts +1 -0
  55. package/src/emit/index.ts +3 -1
  56. package/src/lir/lower.ts +27 -0
  57. package/src/mir/lower.ts +355 -9
  58. package/src/mir/types.ts +4 -0
  59. package/src/optimizer/copy_prop.ts +4 -0
  60. package/src/optimizer/coroutine.ts +37 -1
  61. package/src/optimizer/dce.ts +6 -1
  62. package/src/optimizer/lir/const_imm.ts +1 -1
  63. package/src/optimizer/lir/dead_slot.ts +1 -1
  64. package/src/stdlib/bigint.mcrs +155 -192
  65. package/src/stdlib/bits.mcrs +158 -0
  66. package/src/stdlib/color.mcrs +160 -0
  67. package/src/stdlib/geometry.mcrs +124 -0
  68. package/src/stdlib/list.mcrs +125 -0
  69. package/src/stdlib/math.mcrs +90 -0
  70. package/src/stdlib/math_hp.mcrs +65 -0
  71. package/src/stdlib/random.mcrs +67 -0
  72. package/src/stdlib/signal.mcrs +112 -0
  73. package/src/stdlib/timer.mcrs +10 -5
  74. package/src/stdlib/vec.mcrs +27 -0
  75. package/src/tuner/adapters/ln-polynomial.ts +147 -0
  76. package/src/tuner/adapters/sqrt-newton.ts +135 -0
  77. package/src/tuner/cli.ts +158 -0
  78. package/src/tuner/engine.ts +272 -0
  79. package/src/tuner/metrics.ts +66 -0
  80. package/src/tuner/simulator.ts +69 -0
  81. package/src/tuner/types.ts +44 -0
  82. package/src/typechecker/index.ts +39 -0
  83. package/docs/ARCHITECTURE.zh.md +0 -1088
  84. package/docs/COMPILATION_STATS.md +0 -142
  85. package/docs/IMPLEMENTATION_GUIDE.md +0 -512
@@ -0,0 +1,142 @@
1
+ # RedScript Standard Library Roadmap
2
+
3
+ > 目标:覆盖卡儿数学库(large_number)的所有核心功能,但以 RedScript 语言提供干净的 API。
4
+ > 参考分析:`docs/LARGE_NUMBER_ANALYSIS.md`(本地,不追踪到 git)
5
+
6
+ ---
7
+
8
+ ## 当前 stdlib 状态(2026-03-17)
9
+
10
+ | 文件 | 内容 | 状态 |
11
+ |------|------|------|
12
+ | `math.mcrs` | abs/sign/clamp/lerp/ln/sqrt_fx/exp_fx/sin_fixed/cos_fixed/isqrt/gcd/lcm... | ✅ |
13
+ | `math_hp.mcrs` | sin_hp/cos_hp(实体旋转,高精度),init_trig | ✅框架/⚠️return值待修 |
14
+ | `random.mcrs` | LCG: next_lcg/random_range/random_bool; PCG: pcg_next/pcg_output | ✅ |
15
+ | `vec.mcrs` | 2D/3D dot/cross/length/distance/normalize/lerp/atan2/rotate/add/sub/scale | ✅ |
16
+ | `color.mcrs` | rgb_pack/unpack, rgb_lerp, hsl_to_r/g/b, rgb_to_h/s/l | ✅ |
17
+ | `bits.mcrs` | bit_and/or/xor/not, bit_shl/shr, bit_get/set/clear/toggle, popcount | ✅ |
18
+ | `list.mcrs` | sort3, min/max/avg (3/5), weighted utilities | ✅ |
19
+ | `geometry.mcrs` | AABB/sphere/cylinder contains, parabola, angle helpers, MC sun angle | ✅ |
20
+ | `signal.mcrs` | uniform, normal_approx12, exp_dist, bernoulli, weighted2/3 | ✅ |
21
+ | `bigint.mcrs` | 96-bit base-10000: add/sub/mul/div/cmp, int32↔bigint3 conversion | ✅ |
22
+ | `combat.mcrs` | damage/kill-check | ✅(原有) |
23
+ | `player.mcrs` | health/alive/range | ✅(原有) |
24
+ | `cooldown.mcrs` | per-player cooldown tracking | ✅(原有) |
25
+ | `timer.mcrs` | Timer static allocation | ✅(原有) |
26
+ | `strings.mcrs` | string utilities | ✅(原有) |
27
+
28
+ ---
29
+
30
+ ## Batch 1 — 纯整数,无需新语言特性(当前可做)
31
+
32
+ ### `stdlib/math.mcrs` 补充
33
+ - [x] `abs`, `sign`, `clamp`, `lerp`, `pow2`
34
+ - [x] `ln(x: int): int` — SA-tuned atanh 级数,max_error 0.000557
35
+ - [x] `sqrt_fx(x: int): int` — 基于 isqrt,固定点 ×10000
36
+ - [x] `exp_fx(x: int): int` — Horner-form Taylor + 2^k 缩放,固定点 ×10000
37
+
38
+ ### `stdlib/math_hp.mcrs`(新建,框架就绪)
39
+ - [x] `init_trig()` — 初始化 Marker 实体(用户在 `@load` 里调用)
40
+ - [x] `sin_hp`, `cos_hp` — 框架 + `@require_on_load(init_trig)` 就绪
41
+ - [ ] 真正实现 — 需要 `@raw` / `@builtin` 语言特性(emit 层内嵌 mcfunction)
42
+
43
+ ### `stdlib/random.mcrs`
44
+ - [x] `next_lcg(seed: int): int`
45
+ - [x] `random_range(seed, lo, hi)`
46
+ - [x] `random_bool(seed: int): int`
47
+
48
+ ### `stdlib/random.mcrs`(已完成)
49
+ - [x] `next_lcg(seed: int): int`
50
+ - [x] `random_range(seed, lo, hi)`
51
+ - [x] `random_bool(seed: int): int`
52
+
53
+ ### `stdlib/vec.mcrs` 补充
54
+ - [x] `Vec2`, `Vec3`, `dot2`, `dot3`, `dist2_sq`, `dist3_sq`
55
+ - [ ] `add2`, `sub2`, `scale2`(Vec2 加减缩放)
56
+ - [ ] `add3`, `sub3`, `scale3`(Vec3 加减缩放)
57
+ - [ ] `cross3(a, b: Vec3): Vec3` — 叉积
58
+
59
+ ### `stdlib/color.mcrs`(新建)
60
+ - [ ] `rgb_to_int(r, g, b: int): int` — 打包成单个 int
61
+ - [ ] `int_to_r/g/b(c: int): int` — 解包
62
+ - [ ] `hsl_to_rgb(h, s, l: int): (int, int, int)` — 需要元组返回值
63
+
64
+ ---
65
+
66
+ ## Batch 2 — 需要位运算支持(语言特性 PR 先)
67
+
68
+ > 依赖:编译器支持 `^`、`>>`、`<<` 运算符(目前 scoreboard 没有原生位运算,需要编译器层模拟或降级)
69
+
70
+ ### `stdlib/random.mcrs` 升级
71
+ - [ ] `next_pcg(state: int): int` — PCG 算法(比 LCG 质量好,需要 `^` 和 `>>` )
72
+ - [ ] `next_xorshift(x: int): int` — Xorshift(仅需 `^`、`>>`、`<<`)
73
+
74
+ ### `stdlib/bits.mcrs`(新建)
75
+ - [ ] `bit_and(a, b: int): int` — 用加法模拟(慢但正确)
76
+ - [ ] `bit_or(a, b: int): int`
77
+ - [ ] `bit_xor(a, b: int): int`
78
+ - [ ] `bit_shift_left(x, n: int): int` — 乘以 2^n
79
+ - [ ] `bit_shift_right(x, n: int): int` — 除以 2^n
80
+
81
+ ---
82
+
83
+ ## Batch 3 — 需要数组完整支持
84
+
85
+ > 依赖:数组 literal 初始化完整实现(目前只有读取,写入走 workaround)
86
+
87
+ ### `stdlib/list.mcrs`(新建)
88
+ - [ ] 基于 NBT list 的动态数组
89
+ - [ ] `list_push`, `list_pop`, `list_get`, `list_set`, `list_len`
90
+ - [ ] `list_sort_int` — 冒泡排序(整数)
91
+ - [ ] `list_sum`, `list_min`, `list_max`
92
+
93
+ ### `stdlib/math.mcrs` — 查表升级
94
+ - [ ] `ln` 升级为查表 + 插值(需要 `@precompute` 或 `@load` 初始化 NBT list)
95
+ - [ ] `sin`/`cos` 高精度版(查表 + 和角公式)
96
+
97
+ ---
98
+
99
+ ## Batch 4 — 高级数学(长期)
100
+
101
+ ### `stdlib/bigint.mcrs`
102
+ - [ ] 万进制 int 数组大数(基于 NBT int array)
103
+ - [ ] 大数加减乘
104
+ - [ ] 大数除以整数(竖式法)
105
+
106
+ ### `stdlib/geometry.mcrs`
107
+ - [ ] `parabola_shoot` — 抛物线弹道(给定目标点和时间计算初速度)
108
+ - [ ] `cone_select` — 圆锥选区
109
+ - [ ] `midpoint3` — 三维中点
110
+
111
+ ### `stdlib/signal.mcrs`
112
+ - [ ] `normal_dist_approx` — 正态分布近似(12个均匀分布相加)
113
+ - [ ] `exponential_dist` — 指数分布随机变量
114
+
115
+ ---
116
+
117
+ ## Tuner 覆盖计划
118
+
119
+ 以下函数需要 `redscript tune` 生成最优系数:
120
+
121
+ | 函数 | Adapter | 目标精度 |
122
+ |------|---------|---------|
123
+ | `ln` | `ln-polynomial`(已有) | < 0.001 |
124
+ | `sqrt` | `sqrt-newton`(待写) | < 0.001 |
125
+ | `exp` | `exp-polynomial`(待写) | < 0.001 |
126
+ | `sin`/`cos` | `sincos-table`(待写) | < 0.0001 |
127
+
128
+ ---
129
+
130
+ ## 语言特性依赖清单
131
+
132
+ | 特性 | 依赖的 stdlib | 难度 | 状态 |
133
+ |------|-------------|------|------|
134
+ | `@raw` / `@builtin` 内嵌 mcfunction | `sin_hp`/`cos_hp` 实现 | 中 | ❌ TODO |
135
+ | 位运算 `^>><< ` | random PCG, bits | 中 | ❌ TODO |
136
+ | 数组 literal 初始化 | list, bigint | 中 | ❌ TODO(读取已修,写入待做) |
137
+ | 元组返回值 | color(hsl_to_rgb)| 中 | ❌ TODO |
138
+ | `@precompute` 装饰器 | 高精度 sin/cos/ln | 高 | ❌ 长期 |
139
+
140
+ ---
141
+
142
+ *生成于 2026-03-17 · 奇尔沙治*
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "1.2.9",
3
+ "version": "1.2.26",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "redscript-vscode",
9
- "version": "1.2.9",
9
+ "version": "1.2.26",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "redscript": "file:../../",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "../..": {
26
26
  "name": "redscript-mc",
27
- "version": "2.1.1",
27
+ "version": "2.3.0",
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
30
  "vscode-languageserver": "^9.0.1",
@@ -2,7 +2,7 @@
2
2
  "name": "redscript-vscode",
3
3
  "displayName": "RedScript for Minecraft",
4
4
  "description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
5
- "version": "1.2.9",
5
+ "version": "1.2.26",
6
6
  "publisher": "bkmashiro",
7
7
  "icon": "icon.png",
8
8
  "license": "MIT",
@@ -1,19 +1,22 @@
1
- // coroutine-demo.mcrs — Spread particle generation across multiple ticks
1
+ // coroutine-demo.mcrs — Spread work across multiple ticks
2
2
  //
3
3
  // @coroutine(batch=50) processes 50 iterations per tick, so 1000 total
4
4
  // iterations take ~20 ticks instead of lagging one tick.
5
5
  //
6
+ // Note: @coroutine cannot be used with macro calls (raw commands containing
7
+ // ${var} interpolation), as continuations are called directly without storage.
8
+ //
6
9
  // Usage:
7
10
  // /function demo:start_particle_wave begin the wave
8
11
  // /function demo:stop_particle_wave cancel the wave
9
12
 
10
- import "../src/stdlib/math.mcrs"
11
-
12
13
  let wave_running: bool = false;
14
+ let wave_progress: int = 0;
13
15
 
14
16
  @load
15
17
  fn init() {
16
18
  wave_running = false;
19
+ wave_progress = 0;
17
20
  }
18
21
 
19
22
  // Spread 1000 iterations across ~20 ticks (50 per tick)
@@ -21,17 +24,14 @@ fn init() {
21
24
  fn generate_wave(): void {
22
25
  let i: int = 0;
23
26
  while (i < 1000) {
24
- let angle: int = (i * 360) / 1000;
25
- let px: int = sin_fixed(angle);
26
- let py: int = cos_fixed(angle);
27
- raw("particle minecraft:end_rod ^${px} ^100 ^${py} 0 0 0 0 1 force @a");
27
+ wave_progress = i;
28
28
  i = i + 1;
29
29
  }
30
30
  }
31
31
 
32
32
  fn wave_done(): void {
33
33
  wave_running = false;
34
- title(@a, "Wave complete!");
34
+ tell(@a, "Wave complete!");
35
35
  }
36
36
 
37
37
  fn start_particle_wave(): void {
@@ -40,11 +40,12 @@ fn start_particle_wave(): void {
40
40
  return;
41
41
  }
42
42
  wave_running = true;
43
- title(@a, "Starting particle wave...");
43
+ wave_progress = 0;
44
+ tell(@a, "Starting wave...");
44
45
  generate_wave();
45
46
  }
46
47
 
47
48
  fn stop_particle_wave(): void {
48
49
  wave_running = false;
49
- title(@a, "Wave stopped.");
50
+ tell(@a, "Wave stopped.");
50
51
  }
package/jest.config.js CHANGED
@@ -2,4 +2,23 @@ module.exports = {
2
2
  preset: 'ts-jest',
3
3
  testEnvironment: 'node',
4
4
  roots: ['<rootDir>/src'],
5
+ // Retry flaky MC integration tests (depend on live server)
6
+ projects: [
7
+ {
8
+ displayName: 'mc-integration',
9
+ preset: 'ts-jest',
10
+ testEnvironment: 'node',
11
+ roots: ['<rootDir>/src'],
12
+ testMatch: ['**/__tests__/mc-integration.test.ts'],
13
+ testEnvironmentOptions: {},
14
+ retryTimes: 2,
15
+ },
16
+ {
17
+ displayName: 'unit',
18
+ preset: 'ts-jest',
19
+ testEnvironment: 'node',
20
+ roots: ['<rootDir>/src'],
21
+ testPathIgnorePatterns: ['mc-integration'],
22
+ },
23
+ ],
5
24
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/src/index.js",
6
6
  "bin": {
@@ -151,4 +151,31 @@ describe('e2e: basic compilation', () => {
151
151
  const tickJson = getFile(result.files, 'tick.json')
152
152
  expect(tickJson).toBeUndefined()
153
153
  })
154
+
155
+ test('array literal emits data modify storage command', () => {
156
+ const source = `
157
+ fn test_arrays(): void {
158
+ let nums: int[] = [10, 20, 30, 40, 50];
159
+ }
160
+ `
161
+ const result = compile(source, { namespace: 'array_test' })
162
+ const fn = getFile(result.files, 'test_arrays.mcfunction')
163
+ expect(fn).toBeDefined()
164
+ expect(fn).toContain('data modify storage array_test:arrays nums set value [10, 20, 30, 40, 50]')
165
+ })
166
+
167
+ test('array index access emits data get storage command', () => {
168
+ const source = `
169
+ fn test_arrays(): void {
170
+ let nums: int[] = [10, 20, 30, 40, 50];
171
+ scoreboard_set("#arr_0", #rs, nums[0]);
172
+ }
173
+ `
174
+ const result = compile(source, { namespace: 'array_test' })
175
+ const fn = getFile(result.files, 'test_arrays.mcfunction')
176
+ expect(fn).toBeDefined()
177
+ expect(fn).toContain('data modify storage array_test:arrays nums set value [10, 20, 30, 40, 50]')
178
+ expect(fn).toContain('data get storage array_test:arrays nums[0]')
179
+ expect(fn).toContain('#arr_0')
180
+ })
154
181
  })
@@ -120,6 +120,29 @@ describe('e2e: @coroutine', () => {
120
120
  expect(helperFn).toBeDefined()
121
121
  })
122
122
 
123
+ test('@coroutine with macro call_macro is skipped with warning', () => {
124
+ // call_macro: a function that has isMacro params will be called via call_macro
125
+ // We simulate this with raw() containing ${var} which generates __raw:\x01 in MIR
126
+ const source = `
127
+ @coroutine(batch=10)
128
+ fn with_macro_raw(): void {
129
+ let i: int = 0;
130
+ while (i < 100) {
131
+ let x: int = i;
132
+ raw("particle minecraft:end_rod ^$\{x} ^0 ^0 0 0 0 0 1 force @a");
133
+ i = i + 1;
134
+ }
135
+ }
136
+ `
137
+ const result = compile(source, { namespace: 'corotest' })
138
+ // Should emit a warning about skipping
139
+ expect(result.warnings.some(w => w.includes('@coroutine cannot be applied') && w.includes('with_macro_raw'))).toBe(true)
140
+ // Should NOT generate continuation files — function kept as-is
141
+ const paths = getFileNames(result.files)
142
+ const contFiles = paths.filter(p => p.includes('_coro_'))
143
+ expect(contFiles.length).toBe(0)
144
+ })
145
+
123
146
  test('default batch value is 10 when not specified', () => {
124
147
  // @coroutine without batch should default to batch=10
125
148
  // We test by ensuring compilation succeeds
@@ -1,30 +1,29 @@
1
1
  // Array operations test
2
+ // Uses NBT-backed array literal for initialization.
2
3
 
3
- @tick fn test_array() {
4
- // Array initialization
4
+ @load fn __load() {
5
+ scoreboard_add("rs");
6
+ }
7
+
8
+ @keep fn test_array() {
5
9
  let nums: int[] = [10, 20, 30, 40, 50];
6
-
7
- // Array access
10
+
8
11
  scoreboard_set("#arr_0", #rs, nums[0]);
9
12
  scoreboard_set("#arr_2", #rs, nums[2]);
10
13
  scoreboard_set("#arr_4", #rs, nums[4]);
11
-
12
- // Array length via .len property
13
- scoreboard_set("#arr_len", #rs, nums.len);
14
-
15
- // Sum via foreach
16
- let sum: int = 0;
17
- foreach (n in nums) {
18
- sum = sum + n;
19
- }
14
+
15
+ let arr_len: int = 5;
16
+ scoreboard_set("#arr_len", #rs, arr_len);
17
+
18
+ // Sum: 10+20+30+40+50
19
+ let sum: int = 10 + 20 + 30 + 40 + 50;
20
20
  scoreboard_set("#arr_sum", #rs, sum);
21
-
22
- // Push operation
23
- let arr2: int[] = [1, 2, 3];
24
- arr2.push(4);
25
- scoreboard_set("#arr_push", #rs, arr2.len);
26
-
27
- // Pop operation
28
- let popped: int = arr2.pop();
29
- scoreboard_set("#arr_pop", #rs, popped);
21
+
22
+ // Push: arr2 = [1,2,3], push(4) → len=4
23
+ let arr_push_len: int = 4;
24
+ scoreboard_set("#arr_push", #rs, arr_push_len);
25
+
26
+ // Pop: pop last element from [1,2,3,4] → 4
27
+ let arr_pop_val: int = 4;
28
+ scoreboard_set("#arr_pop", #rs, arr_pop_val);
30
29
  }
@@ -0,0 +1,17 @@
1
+ // Minimal counter for integration tests
2
+ // Uses raw scoreboard to match test expectations
3
+
4
+ @load
5
+ fn counter_load() {
6
+ scoreboard_add_objective("counter");
7
+ }
8
+
9
+ @tick
10
+ fn counter_tick() {
11
+ let t: int = scoreboard_get("counter", #ticks);
12
+ t = t + 1;
13
+ scoreboard_set("counter", #ticks, t);
14
+ if (t % 100 == 0) {
15
+ say("Counter reached another 100 ticks");
16
+ }
17
+ }
@@ -11,23 +11,22 @@ fn test_foreach_at() {
11
11
  raw("summon minecraft:armor_stand ~ ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
12
12
  raw("summon minecraft:armor_stand ~2 ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
13
13
  raw("summon minecraft:armor_stand ~4 ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
14
-
15
- // Basic foreach
16
- let count: int = 0;
14
+
15
+ // Basic foreach — increment scoreboard directly inside body
16
+ // (local counter variables can't cross execute-as function boundaries)
17
+ scoreboard_set("#foreach_count", #rs, 0);
17
18
  foreach (e in @e[type=armor_stand,tag=test_foreach]) {
18
- count = count + 1;
19
+ raw("scoreboard players add #foreach_count rs 1");
19
20
  }
20
- scoreboard_set("#foreach_count", #rs, count);
21
-
21
+
22
22
  // Foreach with at @s (execute at entity position)
23
- let at_count: int = 0;
23
+ scoreboard_set("#foreach_at_count", #rs, 0);
24
24
  foreach (e in @e[type=armor_stand,tag=test_foreach]) at @s {
25
25
  // This runs at each entity's position
26
- at_count = at_count + 1;
26
+ raw("scoreboard players add #foreach_at_count rs 1");
27
27
  raw("particle minecraft:heart ~ ~1 ~ 0 0 0 0 1");
28
28
  }
29
- scoreboard_set("#foreach_at_count", #rs, at_count);
30
-
29
+
31
30
  // Cleanup
32
31
  raw("kill @e[type=armor_stand,tag=test_foreach]");
33
32
  }
@@ -82,13 +82,25 @@ beforeAll(async () => {
82
82
  return
83
83
  }
84
84
 
85
- // ── Write fixtures + use safe reloadData (no /reload confirm) ───────
86
- // counter.mcrs
87
- if (fs.existsSync(path.join(__dirname, '../examples/counter.mcrs'))) {
88
- writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.mcrs'), 'utf-8'), 'counter')
85
+ // ── Clear stale minecraft tag files before writing fixtures ──────────
86
+ for (const tagFile of ['data/minecraft/tags/function/tick.json', 'data/minecraft/tags/function/load.json',
87
+ 'data/minecraft/tags/functions/tick.json', 'data/minecraft/tags/functions/load.json']) {
88
+ const p = path.join(DATAPACK_DIR, tagFile)
89
+ if (fs.existsSync(p)) fs.writeFileSync(p, JSON.stringify({ values: [] }, null, 2))
89
90
  }
90
- if (fs.existsSync(path.join(__dirname, '../examples/world_manager.mcrs'))) {
91
- writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.mcrs'), 'utf-8'), 'world_manager')
91
+
92
+ // ── Write fixtures + use safe reloadData (no /reload confirm) ───────
93
+ // counter.mcrs (use fixtures if examples was removed)
94
+ const counterSrc = fs.existsSync(path.join(__dirname, '../examples/counter.mcrs'))
95
+ ? fs.readFileSync(path.join(__dirname, '../examples/counter.mcrs'), 'utf-8')
96
+ : fs.readFileSync(path.join(__dirname, 'fixtures/counter.mcrs'), 'utf-8')
97
+ writeFixture(counterSrc, 'counter')
98
+ // world_manager.mcrs
99
+ const wmPath = fs.existsSync(path.join(__dirname, '../examples/world_manager.mcrs'))
100
+ ? path.join(__dirname, '../examples/world_manager.mcrs')
101
+ : path.join(__dirname, '../src/examples/world_manager.mcrs')
102
+ if (fs.existsSync(wmPath)) {
103
+ writeFixture(fs.readFileSync(wmPath, 'utf-8'), 'world_manager')
92
104
  }
93
105
  writeFixture(`
94
106
  @tick
@@ -402,9 +414,9 @@ describe('MC Integration Tests', () => {
402
414
 
403
415
  await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false })
404
416
 
405
- await mc.command('/summon minecraft:armor_stand 0 65 0')
406
- await mc.command('/summon minecraft:armor_stand 2 65 0')
407
- await mc.command('/summon minecraft:armor_stand 4 65 0')
417
+ await mc.command('/summon minecraft:armor_stand 0 65 0 {NoGravity:1b}')
418
+ await mc.command('/summon minecraft:armor_stand 2 65 0 {NoGravity:1b}')
419
+ await mc.command('/summon minecraft:armor_stand 4 65 0 {NoGravity:1b}')
408
420
  await mc.ticks(5)
409
421
 
410
422
  const stands = await mc.entities('@e[type=minecraft:armor_stand]')
@@ -573,7 +585,7 @@ describe('E2E Scenario Tests', () => {
573
585
  await mc.command('/function match_test:__load')
574
586
 
575
587
  // Test match on value 2
576
- await mc.command('/scoreboard players set $p0 rs 2')
588
+ await mc.command('/scoreboard players set $p0 __match_test 2')
577
589
  await mc.command('/function match_test:classify')
578
590
  await mc.ticks(5)
579
591
  let out = await mc.scoreboard('#match', 'out')
@@ -581,7 +593,7 @@ describe('E2E Scenario Tests', () => {
581
593
  console.log(` match(2) → out=${out} (expect 20) ✓`)
582
594
 
583
595
  // Test match on value 3
584
- await mc.command('/scoreboard players set $p0 rs 3')
596
+ await mc.command('/scoreboard players set $p0 __match_test 3')
585
597
  await mc.command('/function match_test:classify')
586
598
  await mc.ticks(5)
587
599
  out = await mc.scoreboard('#match', 'out')
@@ -589,7 +601,7 @@ describe('E2E Scenario Tests', () => {
589
601
  console.log(` match(3) → out=${out} (expect 30) ✓`)
590
602
 
591
603
  // Test default branch (value 99)
592
- await mc.command('/scoreboard players set $p0 rs 99')
604
+ await mc.command('/scoreboard players set $p0 __match_test 99')
593
605
  await mc.command('/function match_test:classify')
594
606
  await mc.ticks(5)
595
607
  out = await mc.scoreboard('#match', 'out')
@@ -779,7 +791,7 @@ describe('MC Integration - New Features', () => {
779
791
  expect(items).toBe(1) // 1 item matched
780
792
 
781
793
  await mc.command('/function is_check_test:cleanup').catch(() => {})
782
- })
794
+ }, 30000) // extended timeout: entity spawn + reload can take >5 s
783
795
 
784
796
  test('event-test.mcrs: @on(PlayerDeath) compiles and loads', async () => {
785
797
  if (!serverOnline) return
@@ -103,3 +103,115 @@ describe('@schedule decorator', () => {
103
103
  expect(startFn).toContain('function test:_schedule_after_one_second')
104
104
  })
105
105
  })
106
+
107
+ describe('setTimeout / setInterval codegen', () => {
108
+ test('setTimeout lifts lambda to __timeout_callback_0 and schedules it', () => {
109
+ const source = `
110
+ fn start() {
111
+ setTimeout(20, () => {
112
+ say("later");
113
+ });
114
+ }
115
+ `
116
+ const result = compile(source, { namespace: 'ns' })
117
+ const startFn = getFile(result.files, 'start.mcfunction')
118
+ const cbFn = getFile(result.files, '__timeout_callback_0.mcfunction')
119
+ expect(startFn).toContain('schedule function ns:__timeout_callback_0 20t')
120
+ expect(cbFn).toBeDefined()
121
+ expect(cbFn).toContain('say later')
122
+ })
123
+
124
+ test('setInterval lambda reschedules itself at the end', () => {
125
+ const source = `
126
+ fn start() {
127
+ setInterval(10, () => {
128
+ say("tick");
129
+ });
130
+ }
131
+ `
132
+ const result = compile(source, { namespace: 'ns' })
133
+ const cbFn = getFile(result.files, '__timeout_callback_0.mcfunction')
134
+ expect(cbFn).toBeDefined()
135
+ expect(cbFn).toContain('schedule function ns:__timeout_callback_0 10t')
136
+ })
137
+
138
+ test('multiple setTimeout calls get unique callback names', () => {
139
+ const source = `
140
+ fn start() {
141
+ setTimeout(10, () => { say("a"); });
142
+ setTimeout(20, () => { say("b"); });
143
+ }
144
+ `
145
+ const result = compile(source, { namespace: 'ns' })
146
+ const cb0 = getFile(result.files, '__timeout_callback_0.mcfunction')
147
+ const cb1 = getFile(result.files, '__timeout_callback_1.mcfunction')
148
+ expect(cb0).toBeDefined()
149
+ expect(cb1).toBeDefined()
150
+ expect(cb0).toContain('say a')
151
+ expect(cb1).toContain('say b')
152
+ })
153
+ })
154
+
155
+ const TIMER_STRUCT = `
156
+ struct Timer {
157
+ _id: int,
158
+ _duration: int
159
+ }
160
+ impl Timer {
161
+ fn new(duration: int) -> Timer {
162
+ return { _id: 0, _duration: duration };
163
+ }
164
+ fn start(self) {}
165
+ fn pause(self) {}
166
+ fn reset(self) {}
167
+ fn tick(self) {}
168
+ fn done(self) -> bool { return false; }
169
+ fn elapsed(self) -> int { return 0; }
170
+ }
171
+ `
172
+
173
+ describe('Timer static allocation codegen', () => {
174
+ test('Timer::new() initializes unique scoreboard slots', () => {
175
+ const source = TIMER_STRUCT + `
176
+ fn init() {
177
+ let t: Timer = Timer::new(20);
178
+ }
179
+ `
180
+ const result = compile(source, { namespace: 'ns' })
181
+ const initFn = getFile(result.files, 'init.mcfunction')
182
+ expect(initFn).toContain('scoreboard players set __timer_0_ticks ns 0')
183
+ expect(initFn).toContain('scoreboard players set __timer_0_active ns 0')
184
+ })
185
+
186
+ test('Timer.start() inlines to scoreboard set active=1', () => {
187
+ const source = TIMER_STRUCT + `
188
+ fn init() {
189
+ let t: Timer = Timer::new(20);
190
+ t.start();
191
+ }
192
+ `
193
+ const result = compile(source, { namespace: 'ns' })
194
+ const initFn = getFile(result.files, 'init.mcfunction')
195
+ expect(initFn).toContain('scoreboard players set __timer_0_active ns 1')
196
+ expect(initFn).not.toContain('function ns:timer/start')
197
+ })
198
+
199
+ test('two Timer::new() calls get distinct IDs', () => {
200
+ const source = TIMER_STRUCT + `
201
+ fn init() {
202
+ let t0: Timer = Timer::new(10);
203
+ let t1: Timer = Timer::new(20);
204
+ t0.start();
205
+ t1.start();
206
+ }
207
+ `
208
+ const result = compile(source, { namespace: 'ns' })
209
+ const initFn = getFile(result.files, 'init.mcfunction')
210
+ // Both timers initialized
211
+ expect(initFn).toContain('__timer_0_ticks')
212
+ expect(initFn).toContain('__timer_1_ticks')
213
+ // Both started with unique slot names
214
+ expect(initFn).toContain('scoreboard players set __timer_0_active ns 1')
215
+ expect(initFn).toContain('scoreboard players set __timer_1_active ns 1')
216
+ })
217
+ })