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.
- package/dist/__tests__/cli.test.js +1 -1
- package/dist/__tests__/codegen.test.js +12 -6
- package/dist/__tests__/e2e.test.js +6 -6
- package/dist/__tests__/lowering.test.js +8 -8
- package/dist/__tests__/optimizer.test.js +31 -0
- package/dist/__tests__/stdlib-advanced.test.d.ts +4 -0
- package/dist/__tests__/stdlib-advanced.test.js +264 -0
- package/dist/__tests__/stdlib-math.test.d.ts +7 -0
- package/dist/__tests__/stdlib-math.test.js +352 -0
- package/dist/__tests__/stdlib-vec.test.d.ts +4 -0
- package/dist/__tests__/stdlib-vec.test.js +264 -0
- package/dist/ast/types.d.ts +17 -1
- package/dist/codegen/mcfunction/index.js +154 -18
- package/dist/codegen/var-allocator.d.ts +17 -0
- package/dist/codegen/var-allocator.js +26 -0
- package/dist/compile.d.ts +14 -0
- package/dist/compile.js +62 -5
- package/dist/index.js +20 -1
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +1 -0
- package/dist/lowering/index.d.ts +5 -0
- package/dist/lowering/index.js +83 -10
- package/dist/optimizer/dce.js +21 -5
- package/dist/optimizer/passes.js +18 -6
- package/dist/optimizer/structure.js +7 -0
- package/dist/parser/index.d.ts +5 -0
- package/dist/parser/index.js +43 -2
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +109 -9
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +1 -1
- package/src/__tests__/codegen.test.ts +12 -6
- package/src/__tests__/e2e.test.ts +6 -6
- package/src/__tests__/lowering.test.ts +8 -8
- package/src/__tests__/optimizer.test.ts +33 -0
- package/src/__tests__/stdlib-advanced.test.ts +259 -0
- package/src/__tests__/stdlib-math.test.ts +374 -0
- package/src/__tests__/stdlib-vec.test.ts +259 -0
- package/src/ast/types.ts +11 -1
- package/src/codegen/mcfunction/index.ts +143 -19
- package/src/codegen/var-allocator.ts +29 -0
- package/src/compile.ts +72 -5
- package/src/index.ts +21 -1
- package/src/ir/types.ts +2 -0
- package/src/lexer/index.ts +2 -1
- package/src/lowering/index.ts +96 -10
- package/src/optimizer/dce.ts +22 -5
- package/src/optimizer/passes.ts +18 -5
- package/src/optimizer/structure.ts +6 -1
- package/src/parser/index.ts +47 -2
- package/src/runtime/index.ts +108 -10
- package/src/stdlib/advanced.mcrs +249 -0
- package/src/stdlib/math.mcrs +259 -19
- 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
|
}
|