redscript-mc 1.2.25 → 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 (57) hide show
  1. package/dist/__tests__/cli.test.js +1 -1
  2. package/dist/__tests__/codegen.test.js +12 -6
  3. package/dist/__tests__/e2e.test.js +6 -6
  4. package/dist/__tests__/lowering.test.js +8 -8
  5. package/dist/__tests__/optimizer.test.js +31 -0
  6. package/dist/__tests__/stdlib-advanced.test.d.ts +4 -0
  7. package/dist/__tests__/stdlib-advanced.test.js +264 -0
  8. package/dist/__tests__/stdlib-math.test.d.ts +7 -0
  9. package/dist/__tests__/stdlib-math.test.js +352 -0
  10. package/dist/__tests__/stdlib-vec.test.d.ts +4 -0
  11. package/dist/__tests__/stdlib-vec.test.js +264 -0
  12. package/dist/ast/types.d.ts +17 -1
  13. package/dist/codegen/mcfunction/index.js +154 -18
  14. package/dist/codegen/var-allocator.d.ts +17 -0
  15. package/dist/codegen/var-allocator.js +26 -0
  16. package/dist/compile.d.ts +14 -0
  17. package/dist/compile.js +62 -5
  18. package/dist/index.js +20 -1
  19. package/dist/ir/types.d.ts +4 -0
  20. package/dist/lexer/index.d.ts +1 -1
  21. package/dist/lexer/index.js +1 -0
  22. package/dist/lowering/index.d.ts +5 -0
  23. package/dist/lowering/index.js +83 -10
  24. package/dist/optimizer/dce.js +21 -5
  25. package/dist/optimizer/passes.js +18 -6
  26. package/dist/optimizer/structure.js +7 -0
  27. package/dist/parser/index.d.ts +5 -0
  28. package/dist/parser/index.js +43 -2
  29. package/dist/runtime/index.d.ts +6 -0
  30. package/dist/runtime/index.js +109 -9
  31. package/editors/vscode/package-lock.json +3 -3
  32. package/editors/vscode/package.json +1 -1
  33. package/package.json +1 -1
  34. package/src/__tests__/cli.test.ts +1 -1
  35. package/src/__tests__/codegen.test.ts +12 -6
  36. package/src/__tests__/e2e.test.ts +6 -6
  37. package/src/__tests__/lowering.test.ts +8 -8
  38. package/src/__tests__/optimizer.test.ts +33 -0
  39. package/src/__tests__/stdlib-advanced.test.ts +259 -0
  40. package/src/__tests__/stdlib-math.test.ts +374 -0
  41. package/src/__tests__/stdlib-vec.test.ts +259 -0
  42. package/src/ast/types.ts +11 -1
  43. package/src/codegen/mcfunction/index.ts +143 -19
  44. package/src/codegen/var-allocator.ts +29 -0
  45. package/src/compile.ts +72 -5
  46. package/src/index.ts +21 -1
  47. package/src/ir/types.ts +2 -0
  48. package/src/lexer/index.ts +2 -1
  49. package/src/lowering/index.ts +96 -10
  50. package/src/optimizer/dce.ts +22 -5
  51. package/src/optimizer/passes.ts +18 -5
  52. package/src/optimizer/structure.ts +6 -1
  53. package/src/parser/index.ts +47 -2
  54. package/src/runtime/index.ts +108 -10
  55. package/src/stdlib/advanced.mcrs +249 -0
  56. package/src/stdlib/math.mcrs +259 -19
  57. package/src/stdlib/vec.mcrs +246 -0
@@ -0,0 +1,374 @@
1
+ /**
2
+ * stdlib/math.mcrs — Runtime behavioural tests
3
+ *
4
+ * Each test compiles the math stdlib together with a small driver function,
5
+ * runs it through MCRuntime, and checks scoreboard values.
6
+ */
7
+
8
+ import * as fs from 'fs'
9
+ import * as path from 'path'
10
+ import { compile } from '../compile'
11
+ import { MCRuntime } from '../runtime'
12
+
13
+ const MATH_SRC = fs.readFileSync(
14
+ path.join(__dirname, '../../src/stdlib/math.mcrs'),
15
+ 'utf-8'
16
+ )
17
+
18
+ function run(driver: string): MCRuntime {
19
+ // Use librarySources so math functions are only compiled when actually called
20
+ const result = compile(driver, { namespace: 'mathtest', librarySources: [MATH_SRC] })
21
+ if (!result.success) throw new Error(result.error?.message ?? 'compile failed')
22
+ const runtime = new MCRuntime('mathtest')
23
+ for (const file of result.files ?? []) {
24
+ if (!file.path.endsWith('.mcfunction')) continue
25
+ const match = file.path.match(/data\/([^/]+)\/function\/(.+)\.mcfunction$/)
26
+ if (!match) continue
27
+ runtime.loadFunction(`${match[1]}:${match[2]}`, file.content.split('\n'))
28
+ }
29
+ runtime.load()
30
+ return runtime
31
+ }
32
+
33
+ function scoreOf(rt: MCRuntime, key: string): number {
34
+ return rt.getScore('out', `mathtest.${key}`)
35
+ }
36
+
37
+ // ─── abs ─────────────────────────────────────────────────────────────────────
38
+
39
+ describe('abs', () => {
40
+ it('abs of positive', () => {
41
+ const rt = run(`fn test() { scoreboard_set("out", "r", abs(42)); }`)
42
+ rt.execFunction('test')
43
+ expect(scoreOf(rt, 'r')).toBe(42)
44
+ })
45
+
46
+ it('abs of negative', () => {
47
+ const rt = run(`fn test() { scoreboard_set("out", "r", abs(-7)); }`)
48
+ rt.execFunction('test')
49
+ expect(scoreOf(rt, 'r')).toBe(7)
50
+ })
51
+
52
+ it('abs of zero', () => {
53
+ const rt = run(`fn test() { scoreboard_set("out", "r", abs(0)); }`)
54
+ rt.execFunction('test')
55
+ expect(scoreOf(rt, 'r')).toBe(0)
56
+ })
57
+ })
58
+
59
+ // ─── sign ────────────────────────────────────────────────────────────────────
60
+
61
+ describe('sign', () => {
62
+ it.each([
63
+ [5, 1],
64
+ [-3, -1],
65
+ [0, 0],
66
+ ])('sign(%d) == %d', (x, expected) => {
67
+ const rt = run(`fn test() { scoreboard_set("out", "r", sign(${x})); }`)
68
+ rt.execFunction('test')
69
+ expect(scoreOf(rt, 'r')).toBe(expected)
70
+ })
71
+ })
72
+
73
+ // ─── min / max ───────────────────────────────────────────────────────────────
74
+
75
+ describe('min', () => {
76
+ it.each([
77
+ [3, 7, 3],
78
+ [7, 3, 3],
79
+ [5, 5, 5],
80
+ [-2, 0, -2],
81
+ ])('min(%d, %d) == %d', (a, b, expected) => {
82
+ const rt = run(`fn test() { scoreboard_set("out", "r", min(${a}, ${b})); }`)
83
+ rt.execFunction('test')
84
+ expect(scoreOf(rt, 'r')).toBe(expected)
85
+ })
86
+ })
87
+
88
+ describe('max', () => {
89
+ it.each([
90
+ [3, 7, 7],
91
+ [7, 3, 7],
92
+ [5, 5, 5],
93
+ [-2, 0, 0],
94
+ ])('max(%d, %d) == %d', (a, b, expected) => {
95
+ const rt = run(`fn test() { scoreboard_set("out", "r", max(${a}, ${b})); }`)
96
+ rt.execFunction('test')
97
+ expect(scoreOf(rt, 'r')).toBe(expected)
98
+ })
99
+ })
100
+
101
+ // ─── clamp ───────────────────────────────────────────────────────────────────
102
+
103
+ describe('clamp', () => {
104
+ it.each([
105
+ [5, 0, 10, 5], // in range
106
+ [-5, 0, 10, 0], // below lo
107
+ [15, 0, 10, 10], // above hi
108
+ [0, 0, 10, 0], // at lo
109
+ [10, 0, 10, 10], // at hi
110
+ ])('clamp(%d, %d, %d) == %d', (x, lo, hi, expected) => {
111
+ const rt = run(`fn test() { scoreboard_set("out", "r", clamp(${x}, ${lo}, ${hi})); }`)
112
+ rt.execFunction('test')
113
+ expect(scoreOf(rt, 'r')).toBe(expected)
114
+ })
115
+ })
116
+
117
+ // ─── lerp ────────────────────────────────────────────────────────────────────
118
+
119
+ describe('lerp', () => {
120
+ it.each([
121
+ [0, 1000, 0, 0], // t=0 → a
122
+ [0, 1000, 1000, 1000], // t=1000 → b
123
+ [0, 1000, 500, 500], // t=0.5 → midpoint
124
+ [100, 200, 750, 175], // 100 + (200-100)*0.75 = 175
125
+ [0, 100, 333, 33], // integer division truncation
126
+ ])('lerp(%d, %d, %d) == %d', (a, b, t, expected) => {
127
+ const rt = run(`fn test() { scoreboard_set("out", "r", lerp(${a}, ${b}, ${t})); }`)
128
+ rt.execFunction('test')
129
+ expect(scoreOf(rt, 'r')).toBe(expected)
130
+ })
131
+ })
132
+
133
+ // ─── isqrt ───────────────────────────────────────────────────────────────────
134
+
135
+ describe('isqrt', () => {
136
+ it.each([
137
+ [0, 0],
138
+ [1, 1],
139
+ [4, 2],
140
+ [9, 3],
141
+ [10, 3], // floor
142
+ [16, 4],
143
+ [24, 4], // floor
144
+ [25, 5],
145
+ [100, 10],
146
+ [1000000, 1000],
147
+ ])('isqrt(%d) == %d', (n, expected) => {
148
+ const rt = run(`fn test() { scoreboard_set("out", "r", isqrt(${n})); }`)
149
+ rt.execFunction('test')
150
+ expect(scoreOf(rt, 'r')).toBe(expected)
151
+ })
152
+ })
153
+
154
+ // ─── sqrt_fixed ──────────────────────────────────────────────────────────────
155
+
156
+ describe('sqrt_fixed (scale=1000)', () => {
157
+ it.each([
158
+ [1000, 1000], // sqrt(1.0) = 1.0
159
+ [4000, 2000], // sqrt(4.0) = 2.0
160
+ [2000, 1414], // sqrt(2.0) ≈ 1.414 (truncated)
161
+ [9000, 3000], // sqrt(9.0) = 3.0
162
+ ])('sqrt_fixed(%d) ≈ %d', (x, expected) => {
163
+ const rt = run(`fn test() { scoreboard_set("out", "r", sqrt_fixed(${x})); }`)
164
+ rt.execFunction('test')
165
+ // Allow ±1 from integer truncation
166
+ expect(Math.abs(scoreOf(rt, 'r') - expected)).toBeLessThanOrEqual(1)
167
+ })
168
+ })
169
+
170
+ // ─── pow_int ─────────────────────────────────────────────────────────────────
171
+
172
+ describe('pow_int', () => {
173
+ it.each([
174
+ [2, 0, 1],
175
+ [2, 1, 2],
176
+ [2, 10, 1024],
177
+ [3, 3, 27],
178
+ [5, 4, 625],
179
+ [10, 5, 100000],
180
+ [7, 0, 1],
181
+ [1, 100, 1],
182
+ ])('pow_int(%d, %d) == %d', (base, exp, expected) => {
183
+ const rt = run(`fn test() { scoreboard_set("out", "r", pow_int(${base}, ${exp})); }`)
184
+ rt.execFunction('test')
185
+ expect(scoreOf(rt, 'r')).toBe(expected)
186
+ })
187
+ })
188
+
189
+ // ─── gcd ─────────────────────────────────────────────────────────────────────
190
+
191
+ describe('gcd', () => {
192
+ it.each([
193
+ [12, 8, 4],
194
+ [7, 5, 1],
195
+ [100, 25, 25],
196
+ [0, 5, 5],
197
+ [5, 0, 5],
198
+ [12, 12, 12],
199
+ [-12, 8, 4], // abs handled internally
200
+ ])('gcd(%d, %d) == %d', (a, b, expected) => {
201
+ const rt = run(`fn test() { scoreboard_set("out", "r", gcd(${a}, ${b})); }`)
202
+ rt.execFunction('test')
203
+ expect(scoreOf(rt, 'r')).toBe(expected)
204
+ })
205
+ })
206
+
207
+ // ─── Phase 4: Number theory & utilities ──────────────────────────────────────
208
+
209
+ describe('lcm', () => {
210
+ it.each([
211
+ [4, 6, 12],
212
+ [3, 5, 15],
213
+ [0, 5, 0],
214
+ [12, 12, 12],
215
+ [7, 1, 7],
216
+ ])('lcm(%d, %d) == %d', (a, b, expected) => {
217
+ const rt = run(`fn test() { scoreboard_set("out", "r", lcm(${a}, ${b})); }`)
218
+ rt.execFunction('test')
219
+ expect(scoreOf(rt, 'r')).toBe(expected)
220
+ })
221
+ })
222
+
223
+ describe('map', () => {
224
+ it.each([
225
+ [5, 0, 10, 0, 100, 50],
226
+ [0, 0, 10, 0, 100, 0],
227
+ [10, 0, 10, 0, 100, 100],
228
+ [1, 0, 10, 100, 200, 110],
229
+ [5, 0, 10, -100, 100, 0],
230
+ ])('map(%d, %d, %d, %d, %d) == %d', (x, il, ih, ol, oh, expected) => {
231
+ const rt = run(`fn test() { scoreboard_set("out", "r", map(${x}, ${il}, ${ih}, ${ol}, ${oh})); }`)
232
+ rt.execFunction('test')
233
+ expect(scoreOf(rt, 'r')).toBe(expected)
234
+ })
235
+ })
236
+
237
+ describe('ceil_div', () => {
238
+ it.each([
239
+ [7, 3, 3],
240
+ [6, 3, 2],
241
+ [9, 3, 3],
242
+ [1, 5, 1],
243
+ [10, 10, 1],
244
+ ])('ceil_div(%d, %d) == %d', (a, b, expected) => {
245
+ const rt = run(`fn test() { scoreboard_set("out", "r", ceil_div(${a}, ${b})); }`)
246
+ rt.execFunction('test')
247
+ expect(scoreOf(rt, 'r')).toBe(expected)
248
+ })
249
+ })
250
+
251
+ describe('log2_int', () => {
252
+ it.each([
253
+ [1, 0],
254
+ [2, 1],
255
+ [4, 2],
256
+ [8, 3],
257
+ [7, 2],
258
+ [1024, 10],
259
+ [0, -1],
260
+ ])('log2_int(%d) == %d', (n, expected) => {
261
+ const rt = run(`fn test() { scoreboard_set("out", "r", log2_int(${n})); }`)
262
+ rt.execFunction('test')
263
+ expect(scoreOf(rt, 'r')).toBe(expected)
264
+ })
265
+ })
266
+
267
+ // ─── Phase 3: Trigonometry ────────────────────────────────────────────────────
268
+ // MCRuntime doesn't support real NBT storage macro functions (data get storage
269
+ // path[$(i)]) — those require Minecraft 1.20.2+.
270
+ // We test what we can: compile-only + sin table initialisation check,
271
+ // and verify sin_fixed output for key angles where the MCRuntime
272
+ // can simulate the scoreboard value after we manually stub the lookup.
273
+
274
+ describe('sin table init', () => {
275
+ it('_math_init in __load when sin_fixed is called (via librarySources)', () => {
276
+ const mathSrc = require('fs').readFileSync(require('path').join(__dirname, '../../src/stdlib/math.mcrs'), 'utf-8')
277
+ const result = require('../compile').compile(
278
+ 'fn test() { scoreboard_set("out", "r", sin_fixed(30)); }',
279
+ { namespace: 'mathtest', librarySources: [mathSrc] }
280
+ )
281
+ expect(result.success).toBe(true)
282
+ const hasSinTable = result.files?.some((f: any) =>
283
+ f.content?.includes('data modify storage math:tables sin set value')
284
+ )
285
+ expect(hasSinTable).toBe(true)
286
+ })
287
+
288
+ it('_math_init NOT in output when sin_fixed is not used (library DCE)', () => {
289
+ const mathSrc = require('fs').readFileSync(require('path').join(__dirname, '../../src/stdlib/math.mcrs'), 'utf-8')
290
+ const result = require('../compile').compile(
291
+ 'fn test() { scoreboard_set("out", "r", abs(-5)); }',
292
+ { namespace: 'mathtest', librarySources: [mathSrc] }
293
+ )
294
+ expect(result.success).toBe(true)
295
+ const hasSinTable = result.files?.some((f: any) =>
296
+ f.content?.includes('data modify storage math:tables sin set value')
297
+ )
298
+ expect(hasSinTable).toBe(false)
299
+ })
300
+ })
301
+
302
+ describe('sin_fixed compile check', () => {
303
+ it('sin_fixed compiles without errors', () => {
304
+ const mathSrc = require('fs').readFileSync(require('path').join(__dirname, '../../src/stdlib/math.mcrs'), 'utf-8')
305
+ const result = require('../compile').compile(
306
+ 'fn test() { scoreboard_set("out", "r", sin_fixed(30)); }',
307
+ { namespace: 'mathtest', librarySources: [mathSrc] }
308
+ )
309
+ expect(result.success).toBe(true)
310
+ })
311
+
312
+ it('cos_fixed compiles without errors', () => {
313
+ const mathSrc = require('fs').readFileSync(require('path').join(__dirname, '../../src/stdlib/math.mcrs'), 'utf-8')
314
+ const result = require('../compile').compile(
315
+ 'fn test() { scoreboard_set("out", "r", cos_fixed(0)); }',
316
+ { namespace: 'mathtest', librarySources: [mathSrc] }
317
+ )
318
+ expect(result.success).toBe(true)
319
+ })
320
+ })
321
+
322
+ // ─── Phase 5: Vectors, directions & easing ───────────────────────────────────
323
+
324
+ describe('mulfix / divfix', () => {
325
+ it.each([
326
+ [500, 707, 353], // 0.5 × 0.707 ≈ 0.353
327
+ [1000, 1000, 1000],
328
+ [1000, 500, 500],
329
+ [0, 999, 0],
330
+ ])('mulfix(%d, %d) == %d', (a, b, expected) => {
331
+ const rt = run(`fn test() { scoreboard_set("out", "r", mulfix(${a}, ${b})); }`)
332
+ rt.execFunction('test')
333
+ expect(scoreOf(rt, 'r')).toBe(expected)
334
+ })
335
+
336
+ it.each([
337
+ [1, 3, 333],
338
+ [1, 2, 500],
339
+ [2, 1, 2000],
340
+ [0, 5, 0],
341
+ ])('divfix(%d, %d) == %d', (a, b, expected) => {
342
+ const rt = run(`fn test() { scoreboard_set("out", "r", divfix(${a}, ${b})); }`)
343
+ rt.execFunction('test')
344
+ expect(scoreOf(rt, 'r')).toBe(expected)
345
+ })
346
+ })
347
+
348
+ // dot2d, cross2d, length2d_fixed, manhattan, chebyshev, atan2_fixed
349
+ // have moved to vec.mcrs — tested in stdlib-vec.test.ts
350
+
351
+ describe('smoothstep', () => {
352
+ it.each([
353
+ [0, 0],
354
+ [100, 1000],
355
+ [50, 500], // midpoint: 3×0.5²−2×0.5³ = 0.5 → 500
356
+ ])('smoothstep(0,100,%d) == %d', (x, expected) => {
357
+ const rt = run(`fn test() { scoreboard_set("out", "r", smoothstep(0, 100, ${x})); }`)
358
+ rt.execFunction('test')
359
+ expect(scoreOf(rt, 'r')).toBe(expected)
360
+ })
361
+
362
+ it('smoothstep is monotonically increasing', () => {
363
+ let prev = -1
364
+ for (const x of [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) {
365
+ const rt = run(`fn test() { scoreboard_set("out", "r", smoothstep(0, 100, ${x})); }`)
366
+ rt.execFunction('test')
367
+ const v = scoreOf(rt, 'r')
368
+ expect(v).toBeGreaterThanOrEqual(prev)
369
+ prev = v
370
+ }
371
+ })
372
+ })
373
+
374
+ // atan2_fixed / _atan_init tests moved to stdlib-vec.test.ts
@@ -0,0 +1,259 @@
1
+ /**
2
+ * stdlib/vec.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 VEC_SRC = fs.readFileSync(path.join(__dirname, '../../src/stdlib/vec.mcrs'), 'utf-8')
12
+
13
+ function run(driver: string): MCRuntime {
14
+ const result = compile(driver, {
15
+ namespace: 'vectest',
16
+ librarySources: [MATH_SRC, VEC_SRC],
17
+ })
18
+ if (!result.success) throw new Error(result.error?.message ?? 'compile failed')
19
+ const rt = new MCRuntime('vectest')
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', `vectest.${key}`)
32
+ }
33
+
34
+ // ─── 2D basic ────────────────────────────────────────────────────────────────
35
+
36
+ describe('dot2d', () => {
37
+ it('dot2d(3,4,3,4) == 25', () => {
38
+ const rt = run(`fn test() { scoreboard_set("out", "r", dot2d(3,4,3,4)); }`)
39
+ rt.execFunction('test')
40
+ expect(scoreOf(rt, 'r')).toBe(25)
41
+ })
42
+ it('perpendicular == 0', () => {
43
+ const rt = run(`fn test() { scoreboard_set("out", "r", dot2d(1,0,0,1)); }`)
44
+ rt.execFunction('test')
45
+ expect(scoreOf(rt, 'r')).toBe(0)
46
+ })
47
+ })
48
+
49
+ describe('cross2d', () => {
50
+ it('cross2d(1,0,0,1) == 1', () => {
51
+ const rt = run(`fn test() { scoreboard_set("out", "r", cross2d(1,0,0,1)); }`)
52
+ rt.execFunction('test')
53
+ expect(scoreOf(rt, 'r')).toBe(1)
54
+ })
55
+ it('cross2d parallel == 0', () => {
56
+ const rt = run(`fn test() { scoreboard_set("out", "r", cross2d(3,0,6,0)); }`)
57
+ rt.execFunction('test')
58
+ expect(scoreOf(rt, 'r')).toBe(0)
59
+ })
60
+ })
61
+
62
+ describe('length2d_fixed', () => {
63
+ it.each([
64
+ [3, 4, 5000],
65
+ [0, 5, 5000],
66
+ [5, 0, 5000],
67
+ [1, 1, 1414],
68
+ ])('length2d_fixed(%d,%d) == %d', (x, y, expected) => {
69
+ const rt = run(`fn test() { scoreboard_set("out", "r", length2d_fixed(${x},${y})); }`)
70
+ rt.execFunction('test')
71
+ expect(scoreOf(rt, 'r')).toBe(expected)
72
+ })
73
+ })
74
+
75
+ describe('distance2d_fixed', () => {
76
+ it('distance2d_fixed(0,0,3,4) == 5000', () => {
77
+ const rt = run(`fn test() { scoreboard_set("out", "r", distance2d_fixed(0,0,3,4)); }`)
78
+ rt.execFunction('test')
79
+ expect(scoreOf(rt, 'r')).toBe(5000)
80
+ })
81
+ it('distance2d_fixed(p,p) == 0', () => {
82
+ const rt = run(`fn test() { scoreboard_set("out", "r", distance2d_fixed(5,7,5,7)); }`)
83
+ rt.execFunction('test')
84
+ expect(scoreOf(rt, 'r')).toBe(0)
85
+ })
86
+ })
87
+
88
+ describe('manhattan', () => {
89
+ it.each([
90
+ [0,0,3,4, 7],
91
+ [0,0,0,5, 5],
92
+ [1,1,1,1, 0],
93
+ ])('manhattan(%d,%d,%d,%d) == %d', (x1,y1,x2,y2,e) => {
94
+ const rt = run(`fn test() { scoreboard_set("out", "r", manhattan(${x1},${y1},${x2},${y2})); }`)
95
+ rt.execFunction('test')
96
+ expect(scoreOf(rt, 'r')).toBe(e)
97
+ })
98
+ })
99
+
100
+ describe('chebyshev', () => {
101
+ it.each([
102
+ [0,0,3,4, 4],
103
+ [0,0,4,3, 4],
104
+ [0,0,5,5, 5],
105
+ ])('chebyshev(%d,%d,%d,%d) == %d', (x1,y1,x2,y2,e) => {
106
+ const rt = run(`fn test() { scoreboard_set("out", "r", chebyshev(${x1},${y1},${x2},${y2})); }`)
107
+ rt.execFunction('test')
108
+ expect(scoreOf(rt, 'r')).toBe(e)
109
+ })
110
+ })
111
+
112
+ describe('normalize2d', () => {
113
+ it('normalize2d_x(3,4) == 600', () => {
114
+ const rt = run(`fn test() { scoreboard_set("out", "r", normalize2d_x(3,4)); }`)
115
+ rt.execFunction('test')
116
+ expect(scoreOf(rt, 'r')).toBe(600)
117
+ })
118
+ it('normalize2d_y(3,4) == 800', () => {
119
+ const rt = run(`fn test() { scoreboard_set("out", "r", normalize2d_y(3,4)); }`)
120
+ rt.execFunction('test')
121
+ expect(scoreOf(rt, 'r')).toBe(800)
122
+ })
123
+ it('zero vector → 0', () => {
124
+ const rt = run(`fn test() { scoreboard_set("out", "r", normalize2d_x(0,0)); }`)
125
+ rt.execFunction('test')
126
+ expect(scoreOf(rt, 'r')).toBe(0)
127
+ })
128
+ })
129
+
130
+ describe('lerp2d', () => {
131
+ it('lerp2d_x midpoint', () => {
132
+ const rt = run(`fn test() { scoreboard_set("out", "r", lerp2d_x(0,0,100,200,500)); }`)
133
+ rt.execFunction('test')
134
+ expect(scoreOf(rt, 'r')).toBe(50)
135
+ })
136
+ it('lerp2d_y midpoint', () => {
137
+ const rt = run(`fn test() { scoreboard_set("out", "r", lerp2d_y(0,0,100,200,500)); }`)
138
+ rt.execFunction('test')
139
+ expect(scoreOf(rt, 'r')).toBe(100)
140
+ })
141
+ })
142
+
143
+ // ─── 2D direction ────────────────────────────────────────────────────────────
144
+
145
+ describe('atan2_fixed', () => {
146
+ it.each([
147
+ [0, 1, 0],
148
+ [1, 0, 90],
149
+ [0, -1, 180],
150
+ [-1, 0, 270],
151
+ [1, 1, 45],
152
+ ])('atan2_fixed(%d,%d) == %d', (y, x, expected) => {
153
+ const rt = run(`fn test() { scoreboard_set("out", "r", atan2_fixed(${y},${x})); }`)
154
+ rt.execFunction('test')
155
+ expect(scoreOf(rt, 'r')).toBe(expected)
156
+ })
157
+ })
158
+
159
+ describe('rotate2d', () => {
160
+ it('rotate 90°: (1000,0) → x≈0', () => {
161
+ const rt = run(`fn test() { scoreboard_set("out", "r", rotate2d_x(1000,0,90)); }`)
162
+ rt.execFunction('test')
163
+ expect(Math.abs(scoreOf(rt, 'r'))).toBeLessThan(5) // ≈0, allow rounding
164
+ })
165
+ it('rotate 90°: (1000,0) → y≈1000', () => {
166
+ const rt = run(`fn test() { scoreboard_set("out", "r", rotate2d_y(1000,0,90)); }`)
167
+ rt.execFunction('test')
168
+ expect(scoreOf(rt, 'r')).toBe(1000)
169
+ })
170
+ it('rotate 0°: no change', () => {
171
+ const rt = run(`fn test() { scoreboard_set("out", "r", rotate2d_x(700,0,0)); }`)
172
+ rt.execFunction('test')
173
+ expect(scoreOf(rt, 'r')).toBe(700)
174
+ })
175
+ })
176
+
177
+ // ─── 3D geometry ─────────────────────────────────────────────────────────────
178
+
179
+ describe('dot3d', () => {
180
+ it('dot3d(1,0,0,1,0,0) == 1', () => {
181
+ const rt = run(`fn test() { scoreboard_set("out", "r", dot3d(1,0,0,1,0,0)); }`)
182
+ rt.execFunction('test')
183
+ expect(scoreOf(rt, 'r')).toBe(1)
184
+ })
185
+ it('perpendicular == 0', () => {
186
+ const rt = run(`fn test() { scoreboard_set("out", "r", dot3d(1,0,0,0,1,0)); }`)
187
+ rt.execFunction('test')
188
+ expect(scoreOf(rt, 'r')).toBe(0)
189
+ })
190
+ })
191
+
192
+ describe('cross3d', () => {
193
+ // (1,0,0) × (0,1,0) = (0,0,1)
194
+ it('cross3d_z(1,0,0,0,1,0) == 1', () => {
195
+ const rt = run(`fn test() { scoreboard_set("out", "r", cross3d_z(1,0,0,0,1,0)); }`)
196
+ rt.execFunction('test')
197
+ expect(scoreOf(rt, 'r')).toBe(1)
198
+ })
199
+ it('cross3d_x(1,0,0,0,1,0) == 0', () => {
200
+ const rt = run(`fn test() { scoreboard_set("out", "r", cross3d_x(1,0,0,0,1,0)); }`)
201
+ rt.execFunction('test')
202
+ expect(scoreOf(rt, 'r')).toBe(0)
203
+ })
204
+ })
205
+
206
+ describe('length3d_fixed', () => {
207
+ it('length3d_fixed(1,1,1) == 1732', () => { // √3 × 1000 ≈ 1732
208
+ const rt = run(`fn test() { scoreboard_set("out", "r", length3d_fixed(1,1,1)); }`)
209
+ rt.execFunction('test')
210
+ expect(scoreOf(rt, 'r')).toBe(1732)
211
+ })
212
+ it('length3d_fixed(3,4,0) == 5000', () => {
213
+ const rt = run(`fn test() { scoreboard_set("out", "r", length3d_fixed(3,4,0)); }`)
214
+ rt.execFunction('test')
215
+ expect(scoreOf(rt, 'r')).toBe(5000)
216
+ })
217
+ })
218
+
219
+ describe('manhattan3d / chebyshev3d', () => {
220
+ it('manhattan3d(0,0,0,1,2,3) == 6', () => {
221
+ const rt = run(`fn test() { scoreboard_set("out", "r", manhattan3d(0,0,0,1,2,3)); }`)
222
+ rt.execFunction('test')
223
+ expect(scoreOf(rt, 'r')).toBe(6)
224
+ })
225
+ it('chebyshev3d(0,0,0,3,1,2) == 3', () => {
226
+ const rt = run(`fn test() { scoreboard_set("out", "r", chebyshev3d(0,0,0,3,1,2)); }`)
227
+ rt.execFunction('test')
228
+ expect(scoreOf(rt, 'r')).toBe(3)
229
+ })
230
+ })
231
+
232
+ // ─── library DCE check ────────────────────────────────────────────────────────
233
+
234
+ describe('library DCE: vec.mcrs', () => {
235
+ it('only dot2d compiled when only dot2d called', () => {
236
+ const result = require('../compile').compile(
237
+ 'fn test() { scoreboard_set("out", "r", dot2d(1,0,0,1)); }',
238
+ { namespace: 'vectest', librarySources: [MATH_SRC, VEC_SRC] }
239
+ )
240
+ expect(result.success).toBe(true)
241
+ // atan2_fixed not called → no tan table in __load
242
+ const hasTanTable = result.files?.some((f: any) =>
243
+ f.content?.includes('data modify storage math:tables tan set value')
244
+ )
245
+ expect(hasTanTable).toBe(false)
246
+ })
247
+
248
+ it('_atan_init in __load when atan2_fixed is called', () => {
249
+ const result = require('../compile').compile(
250
+ 'fn test() { scoreboard_set("out", "r", atan2_fixed(1,0)); }',
251
+ { namespace: 'vectest', librarySources: [MATH_SRC, VEC_SRC] }
252
+ )
253
+ expect(result.success).toBe(true)
254
+ const hasTanTable = result.files?.some((f: any) =>
255
+ f.content?.includes('data modify storage math:tables tan set value')
256
+ )
257
+ expect(hasTanTable).toBe(true)
258
+ })
259
+ })
package/src/ast/types.ts CHANGED
@@ -235,7 +235,7 @@ export type Block = Stmt[]
235
235
  // ---------------------------------------------------------------------------
236
236
 
237
237
  export interface Decorator {
238
- name: 'tick' | 'load' | 'on' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
238
+ name: 'tick' | 'load' | 'on' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team' | 'keep' | 'require_on_load'
239
239
  args?: {
240
240
  rate?: number
241
241
  eventType?: string
@@ -244,6 +244,8 @@ export interface Decorator {
244
244
  item?: string
245
245
  team?: string
246
246
  }
247
+ /** Raw positional arguments (used by @requires and future generic decorators). */
248
+ rawArgs?: Array<{ kind: 'string'; value: string } | { kind: 'number'; value: number }>
247
249
  }
248
250
 
249
251
  // ---------------------------------------------------------------------------
@@ -257,6 +259,10 @@ export interface Param {
257
259
  }
258
260
 
259
261
  export interface FnDecl {
262
+ /** Set when this function was parsed from a `module library;` source.
263
+ * Library functions are NOT MC entry points — DCE only keeps them if they
264
+ * are reachable from a non-library (user) entry point. */
265
+ isLibraryFn?: boolean
260
266
  name: string
261
267
  params: Param[]
262
268
  returnType: TypeNode
@@ -326,4 +332,8 @@ export interface Program {
326
332
  implBlocks: ImplBlock[]
327
333
  enums: EnumDecl[]
328
334
  consts: ConstDecl[]
335
+ /** True when the source file declares `module library;`.
336
+ * Library-mode: all functions are DCE-eligible by default — none are treated
337
+ * as MC entry points unless they carry @tick / @load / @on / @keep etc. */
338
+ isLibrary?: boolean
329
339
  }