redscript-mc 2.2.1 → 2.4.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.
- package/CHANGELOG.md +31 -0
- package/README.md +18 -2
- package/dist/src/__tests__/array-dynamic.test.d.ts +12 -0
- package/dist/src/__tests__/array-dynamic.test.js +131 -0
- package/dist/src/__tests__/array-write.test.d.ts +11 -0
- package/dist/src/__tests__/array-write.test.js +149 -0
- package/dist/src/__tests__/tuner/engine.test.d.ts +4 -0
- package/dist/src/__tests__/tuner/engine.test.js +232 -0
- package/dist/src/ast/types.d.ts +7 -0
- package/dist/src/emit/modules.js +5 -0
- package/dist/src/hir/lower.js +29 -0
- package/dist/src/hir/monomorphize.js +2 -0
- package/dist/src/hir/types.d.ts +9 -2
- package/dist/src/lir/lower.js +131 -0
- package/dist/src/mir/lower.js +73 -3
- package/dist/src/mir/macro.js +5 -0
- package/dist/src/mir/types.d.ts +12 -0
- package/dist/src/mir/verify.js +7 -0
- package/dist/src/optimizer/copy_prop.js +5 -0
- package/dist/src/optimizer/coroutine.js +12 -0
- package/dist/src/optimizer/dce.js +9 -0
- package/dist/src/optimizer/unroll.js +3 -0
- package/dist/src/parser/index.js +5 -0
- package/dist/src/tuner/adapters/ln-polynomial.d.ts +23 -0
- package/dist/src/tuner/adapters/ln-polynomial.js +142 -0
- package/dist/src/tuner/adapters/sqrt-newton.d.ts +28 -0
- package/dist/src/tuner/adapters/sqrt-newton.js +125 -0
- package/dist/src/tuner/cli.d.ts +5 -0
- package/dist/src/tuner/cli.js +168 -0
- package/dist/src/tuner/engine.d.ts +17 -0
- package/dist/src/tuner/engine.js +215 -0
- package/dist/src/tuner/metrics.d.ts +15 -0
- package/dist/src/tuner/metrics.js +51 -0
- package/dist/src/tuner/simulator.d.ts +35 -0
- package/dist/src/tuner/simulator.js +78 -0
- package/dist/src/tuner/types.d.ts +32 -0
- package/dist/src/tuner/types.js +6 -0
- package/dist/src/typechecker/index.js +5 -0
- package/docs/STDLIB_ROADMAP.md +142 -0
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/array-dynamic.test.ts +147 -0
- package/src/__tests__/array-write.test.ts +169 -0
- package/src/__tests__/tuner/engine.test.ts +260 -0
- package/src/ast/types.ts +1 -0
- package/src/emit/modules.ts +5 -0
- package/src/hir/lower.ts +30 -0
- package/src/hir/monomorphize.ts +2 -0
- package/src/hir/types.ts +3 -1
- package/src/lir/lower.ts +151 -0
- package/src/mir/lower.ts +75 -3
- package/src/mir/macro.ts +5 -0
- package/src/mir/types.ts +2 -0
- package/src/mir/verify.ts +7 -0
- package/src/optimizer/copy_prop.ts +5 -0
- package/src/optimizer/coroutine.ts +9 -0
- package/src/optimizer/dce.ts +6 -0
- package/src/optimizer/unroll.ts +3 -0
- package/src/parser/index.ts +9 -0
- package/src/stdlib/bigint.mcrs +155 -192
- package/src/stdlib/bits.mcrs +158 -0
- package/src/stdlib/color.mcrs +160 -0
- package/src/stdlib/geometry.mcrs +124 -0
- package/src/stdlib/list.mcrs +96 -0
- package/src/stdlib/math.mcrs +227 -0
- package/src/stdlib/math_hp.mcrs +65 -0
- package/src/stdlib/random.mcrs +67 -0
- package/src/stdlib/signal.mcrs +112 -0
- package/src/stdlib/timer.mcrs +32 -0
- package/src/stdlib/vec.mcrs +27 -0
- package/src/tuner/adapters/ln-polynomial.ts +147 -0
- package/src/tuner/adapters/sqrt-newton.ts +135 -0
- package/src/tuner/cli.ts +158 -0
- package/src/tuner/engine.ts +272 -0
- package/src/tuner/metrics.ts +66 -0
- package/src/tuner/simulator.ts +69 -0
- package/src/tuner/types.ts +44 -0
- package/src/typechecker/index.ts +6 -0
- package/docs/ARCHITECTURE.zh.md +0 -1088
- package/docs/COMPILATION_STATS.md +0 -142
- package/docs/IMPLEMENTATION_GUIDE.md +0 -512
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for dynamic array index read: arr[i] where i is a variable.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - MIR: nbt_read_dynamic instruction is emitted instead of falling back to
|
|
6
|
+
* copy(obj) (which returned the array length, not the value)
|
|
7
|
+
* - LIR/Emit: generates a macro helper function and calls it with
|
|
8
|
+
* `function ns:__dyn_idx_... with storage rs:macro_args`
|
|
9
|
+
* - The generated .mcfunction contains 'with storage' (function macro call)
|
|
10
|
+
* - The helper function contains the $return macro line
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { compile } from '../emit/compile'
|
|
14
|
+
|
|
15
|
+
// Helper: find file in compiled output by path substring
|
|
16
|
+
function getFile(files: { path: string; content: string }[], pathSubstr: string): string | undefined {
|
|
17
|
+
const f = files.find(f => f.path.includes(pathSubstr))
|
|
18
|
+
return f?.content
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Helper: get the content of the function file for `fnName` in namespace
|
|
22
|
+
function getFunctionBody(files: { path: string; content: string }[], fnName: string, ns = 'test'): string {
|
|
23
|
+
const content = getFile(files, `${fnName}.mcfunction`)
|
|
24
|
+
if (!content) {
|
|
25
|
+
// list files for debug
|
|
26
|
+
const paths = files.map(f => f.path).join('\n')
|
|
27
|
+
throw new Error(`Function '${fnName}' not found in output. Files:\n${paths}`)
|
|
28
|
+
}
|
|
29
|
+
return content
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('Dynamic array index read: arr[i]', () => {
|
|
33
|
+
const src = `
|
|
34
|
+
fn test() {
|
|
35
|
+
let nums: int[] = [10, 20, 30, 40, 50];
|
|
36
|
+
let i: int = 2;
|
|
37
|
+
i = i + 1;
|
|
38
|
+
let v: int = nums[i];
|
|
39
|
+
scoreboard_set("#out", "test", v);
|
|
40
|
+
}
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
let files: { path: string; content: string }[]
|
|
44
|
+
beforeAll(() => {
|
|
45
|
+
const result = compile(src, { namespace: 'test' })
|
|
46
|
+
files = result.files
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('compiles without error', () => {
|
|
50
|
+
// beforeAll would have thrown if compilation failed
|
|
51
|
+
expect(files.length).toBeGreaterThan(0)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('test function contains "with storage" (macro call)', () => {
|
|
55
|
+
const body = getFunctionBody(files, 'test')
|
|
56
|
+
expect(body).toContain('with storage')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('test function does NOT contain fallback "scoreboard players set #out test 5"', () => {
|
|
60
|
+
// Old fallback would copy the array length (5 elements) as the result
|
|
61
|
+
const body = getFunctionBody(files, 'test')
|
|
62
|
+
expect(body).not.toContain('scoreboard players set #out test 5')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('a macro helper function is generated for the array', () => {
|
|
66
|
+
// Should have a function file matching __dyn_idx_
|
|
67
|
+
const helperFile = files.find(f => f.path.includes('__dyn_idx_'))
|
|
68
|
+
expect(helperFile).toBeDefined()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('macro helper function contains $return run data get', () => {
|
|
72
|
+
const helperFile = files.find(f => f.path.includes('__dyn_idx_'))
|
|
73
|
+
expect(helperFile).toBeDefined()
|
|
74
|
+
expect(helperFile!.content).toContain('$return run data get')
|
|
75
|
+
expect(helperFile!.content).toContain('$(arr_idx)')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('macro helper function references the correct array path (nums)', () => {
|
|
79
|
+
const helperFile = files.find(f => f.path.includes('__dyn_idx_'))
|
|
80
|
+
expect(helperFile).toBeDefined()
|
|
81
|
+
expect(helperFile!.content).toContain('nums[$(arr_idx)]')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('test function stores index to rs:macro_args', () => {
|
|
85
|
+
const body = getFunctionBody(files, 'test')
|
|
86
|
+
// Should store the index value into rs:macro_args arr_idx
|
|
87
|
+
expect(body).toContain('rs:macro_args')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('Dynamic array index: constant index still uses direct nbt_read', () => {
|
|
92
|
+
const src = `
|
|
93
|
+
fn test_const() {
|
|
94
|
+
let nums: int[] = [10, 20, 30];
|
|
95
|
+
let v: int = nums[1];
|
|
96
|
+
scoreboard_set("#out", "test", v);
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
|
|
100
|
+
let files: { path: string; content: string }[]
|
|
101
|
+
beforeAll(() => {
|
|
102
|
+
const result = compile(src, { namespace: 'test' })
|
|
103
|
+
files = result.files
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('constant index does NOT generate macro call (uses direct data get)', () => {
|
|
107
|
+
const body = getFunctionBody(files, 'test_const')
|
|
108
|
+
// Direct nbt_read emits store_nbt_to_score → execute store result score ... run data get ...
|
|
109
|
+
// without 'with storage'
|
|
110
|
+
expect(body).not.toContain('with storage')
|
|
111
|
+
expect(body).toContain('data get storage')
|
|
112
|
+
expect(body).toContain('nums[1]')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('Dynamic array index: multiple arrays, separate helpers', () => {
|
|
117
|
+
const src = `
|
|
118
|
+
fn test_multi() {
|
|
119
|
+
let a: int[] = [1, 2, 3];
|
|
120
|
+
let b: int[] = [10, 20, 30];
|
|
121
|
+
let i: int = 1;
|
|
122
|
+
i = i + 0;
|
|
123
|
+
let va: int = a[i];
|
|
124
|
+
let vb: int = b[i];
|
|
125
|
+
scoreboard_set("#va", "test", va);
|
|
126
|
+
scoreboard_set("#vb", "test", vb);
|
|
127
|
+
}
|
|
128
|
+
`
|
|
129
|
+
|
|
130
|
+
let files: { path: string; content: string }[]
|
|
131
|
+
beforeAll(() => {
|
|
132
|
+
const result = compile(src, { namespace: 'test' })
|
|
133
|
+
files = result.files
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('two separate macro helpers are generated for arrays a and b', () => {
|
|
137
|
+
const helperFiles = files.filter(f => f.path.includes('__dyn_idx_'))
|
|
138
|
+
expect(helperFiles.length).toBe(2)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('each helper references its respective array path', () => {
|
|
142
|
+
const helperFiles = files.filter(f => f.path.includes('__dyn_idx_'))
|
|
143
|
+
const contents = helperFiles.map(f => f.content).join('\n')
|
|
144
|
+
expect(contents).toContain('a[$(arr_idx)]')
|
|
145
|
+
expect(contents).toContain('b[$(arr_idx)]')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for array index write: arr[i] = val (constant and dynamic index).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Parser: arr[i] = val parses as index_assign (no "Expected ';'" error)
|
|
6
|
+
* - MIR: constant index → nbt_write, dynamic index → nbt_write_dynamic
|
|
7
|
+
* - LIR/Emit: constant index uses store_score_to_nbt to path[N]
|
|
8
|
+
* dynamic index generates a macro helper function for write
|
|
9
|
+
* - Compound assignments: arr[i] += 5 desugars to read + write
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { compile } from '../emit/compile'
|
|
13
|
+
|
|
14
|
+
// Helper: find file in compiled output by path substring
|
|
15
|
+
function getFile(files: { path: string; content: string }[], pathSubstr: string): string | undefined {
|
|
16
|
+
const f = files.find(f => f.path.includes(pathSubstr))
|
|
17
|
+
return f?.content
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Helper: get the content of the function file for `fnName` in namespace
|
|
21
|
+
function getFunctionBody(files: { path: string; content: string }[], fnName: string, ns = 'test'): string {
|
|
22
|
+
const content = getFile(files, `${fnName}.mcfunction`)
|
|
23
|
+
if (!content) {
|
|
24
|
+
const paths = files.map(f => f.path).join('\n')
|
|
25
|
+
throw new Error(`Function '${fnName}' not found in output. Files:\n${paths}`)
|
|
26
|
+
}
|
|
27
|
+
return content
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Constant index write: arr[1] = 99
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
describe('Constant index write: arr[1] = 99', () => {
|
|
34
|
+
const src = `
|
|
35
|
+
fn test() {
|
|
36
|
+
let nums: int[] = [10, 20, 30];
|
|
37
|
+
nums[1] = 99;
|
|
38
|
+
scoreboard_set("#out", "test", nums[1]);
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
let files: { path: string; content: string }[]
|
|
43
|
+
beforeAll(() => {
|
|
44
|
+
const result = compile(src, { namespace: 'test' })
|
|
45
|
+
files = result.files
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('compiles without error', () => {
|
|
49
|
+
expect(files.length).toBeGreaterThan(0)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('test function contains nbt store to array path [1] (constant write)', () => {
|
|
53
|
+
const body = getFunctionBody(files, 'test')
|
|
54
|
+
// Should write to path like "nums[1]" via execute store result storage
|
|
55
|
+
expect(body).toMatch(/nums\[1\]/)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('test function reads back from nums[1] after writing', () => {
|
|
59
|
+
const body = getFunctionBody(files, 'test')
|
|
60
|
+
// Should also read nums[1] for scoreboard_set
|
|
61
|
+
expect(body).toMatch(/nums\[1\]/)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Dynamic index write: arr[i] = 99
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
describe('Dynamic index write: arr[i] = 99', () => {
|
|
69
|
+
const src = `
|
|
70
|
+
fn test() {
|
|
71
|
+
let nums: int[] = [10, 20, 30];
|
|
72
|
+
let i: int = 1;
|
|
73
|
+
nums[i] = 99;
|
|
74
|
+
scoreboard_set("#out", "test", nums[i]);
|
|
75
|
+
}
|
|
76
|
+
`
|
|
77
|
+
|
|
78
|
+
let files: { path: string; content: string }[]
|
|
79
|
+
beforeAll(() => {
|
|
80
|
+
const result = compile(src, { namespace: 'test' })
|
|
81
|
+
files = result.files
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('compiles without error', () => {
|
|
85
|
+
expect(files.length).toBeGreaterThan(0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('test function contains "with storage" (macro call for write)', () => {
|
|
89
|
+
const body = getFunctionBody(files, 'test')
|
|
90
|
+
expect(body).toContain('with storage')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('a __dyn_wrt_ helper function is generated', () => {
|
|
94
|
+
const helperFile = files.find(f => f.path.includes('__dyn_wrt_'))
|
|
95
|
+
expect(helperFile).toBeDefined()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('the write helper contains a macro line with arr_idx and arr_val', () => {
|
|
99
|
+
const helperFile = files.find(f => f.path.includes('__dyn_wrt_'))
|
|
100
|
+
expect(helperFile).toBeDefined()
|
|
101
|
+
expect(helperFile!.content).toContain('$(arr_idx)')
|
|
102
|
+
expect(helperFile!.content).toContain('$(arr_val)')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('the write helper uses data modify set value', () => {
|
|
106
|
+
const helperFile = files.find(f => f.path.includes('__dyn_wrt_'))
|
|
107
|
+
expect(helperFile!.content).toContain('data modify storage')
|
|
108
|
+
expect(helperFile!.content).toContain('set value')
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Compound assignment: arr[i] += 5
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
describe('Compound index assignment: arr[i] += 5', () => {
|
|
116
|
+
const src = `
|
|
117
|
+
fn test() {
|
|
118
|
+
let nums: int[] = [10, 20, 30];
|
|
119
|
+
let i: int = 0;
|
|
120
|
+
nums[i] += 5;
|
|
121
|
+
scoreboard_set("#out", "test", nums[i]);
|
|
122
|
+
}
|
|
123
|
+
`
|
|
124
|
+
|
|
125
|
+
let files: { path: string; content: string }[]
|
|
126
|
+
beforeAll(() => {
|
|
127
|
+
const result = compile(src, { namespace: 'test' })
|
|
128
|
+
files = result.files
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('compiles without error', () => {
|
|
132
|
+
expect(files.length).toBeGreaterThan(0)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('compound assignment generates both read and write macro calls', () => {
|
|
136
|
+
const body = getFunctionBody(files, 'test')
|
|
137
|
+
// Should call with storage at least twice (read for += and write + scoreboard_set read)
|
|
138
|
+
const matches = (body.match(/with storage/g) || []).length
|
|
139
|
+
expect(matches).toBeGreaterThanOrEqual(1)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Constant compound assignment: arr[0] += 5
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
describe('Constant compound index assignment: arr[0] += 5', () => {
|
|
147
|
+
const src = `
|
|
148
|
+
fn test() {
|
|
149
|
+
let nums: int[] = [10, 20, 30];
|
|
150
|
+
nums[0] += 5;
|
|
151
|
+
scoreboard_set("#out", "test", nums[0]);
|
|
152
|
+
}
|
|
153
|
+
`
|
|
154
|
+
|
|
155
|
+
let files: { path: string; content: string }[]
|
|
156
|
+
beforeAll(() => {
|
|
157
|
+
const result = compile(src, { namespace: 'test' })
|
|
158
|
+
files = result.files
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('compiles without error', () => {
|
|
162
|
+
expect(files.length).toBeGreaterThan(0)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('test function contains array path [0] for read and write', () => {
|
|
166
|
+
const body = getFunctionBody(files, 'test')
|
|
167
|
+
expect(body).toMatch(/nums\[0\]/)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the redscript tuner engine, simulator, and ln-polynomial adapter.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { search, searchSA } from '../../tuner/engine';
|
|
6
|
+
import { i32, fixedMul, isOverflow } from '../../tuner/simulator';
|
|
7
|
+
import { evaluate } from '../../tuner/metrics';
|
|
8
|
+
import { lnPolynomialAdapter, defaultParams as lnDefaultParams } from '../../tuner/adapters/ln-polynomial';
|
|
9
|
+
import { sqrtNewtonAdapter, defaultParams as sqrtDefaultParams } from '../../tuner/adapters/sqrt-newton';
|
|
10
|
+
import { TunerAdapter, ParamSpec } from '../../tuner/types';
|
|
11
|
+
|
|
12
|
+
// ─── simulator tests ──────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
describe('simulator', () => {
|
|
15
|
+
test('i32 truncates to int32', () => {
|
|
16
|
+
expect(i32(3.7)).toBe(3);
|
|
17
|
+
expect(i32(-3.7)).toBe(-3);
|
|
18
|
+
expect(i32(2147483648)).toBe(-2147483648); // overflow wraps
|
|
19
|
+
expect(i32(0)).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('fixedMul basic', () => {
|
|
23
|
+
// 10000 * 10000 / 10000 = 10000
|
|
24
|
+
expect(fixedMul(10000, 10000, 10000)).toBe(10000);
|
|
25
|
+
// 5000 * 2 / 10000 = 1
|
|
26
|
+
expect(fixedMul(5000, 2, 10000)).toBe(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('fixedMul returns Infinity on overflow', () => {
|
|
30
|
+
expect(fixedMul(2147483647, 2147483647, 1)).toBe(Infinity);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('isOverflow detects out-of-range', () => {
|
|
34
|
+
expect(isOverflow(2147483648)).toBe(true);
|
|
35
|
+
expect(isOverflow(-2147483649)).toBe(true);
|
|
36
|
+
expect(isOverflow(Infinity)).toBe(true);
|
|
37
|
+
expect(isOverflow(NaN)).toBe(true);
|
|
38
|
+
expect(isOverflow(0)).toBe(false);
|
|
39
|
+
expect(isOverflow(2147483647)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ─── Nelder-Mead convergence test ────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe('Nelder-Mead engine', () => {
|
|
46
|
+
test('converges to minimum of (x-3)^2', () => {
|
|
47
|
+
// Simple 1D minimization: minimize (x-3)^2
|
|
48
|
+
const mockAdapter: TunerAdapter = {
|
|
49
|
+
name: 'test-quadratic',
|
|
50
|
+
description: 'Minimize (x-3)^2',
|
|
51
|
+
params: [
|
|
52
|
+
{ name: 'x', range: [-10, 10], integer: false } as ParamSpec,
|
|
53
|
+
],
|
|
54
|
+
simulate(input: number, params: Record<string, number>): number {
|
|
55
|
+
// Return the residual as a scaled integer
|
|
56
|
+
const x = params['x'];
|
|
57
|
+
return Math.round(x * 10000);
|
|
58
|
+
},
|
|
59
|
+
reference(_input: number): number {
|
|
60
|
+
// Target: x = 3 → value 30000
|
|
61
|
+
return 30000;
|
|
62
|
+
},
|
|
63
|
+
sampleInputs(): number[] {
|
|
64
|
+
return [1]; // single input, target value is 3.0 (×10000 = 30000)
|
|
65
|
+
},
|
|
66
|
+
generateCode(params: Record<string, number>): string {
|
|
67
|
+
return `// x = ${params['x']}`;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = search(mockAdapter, 5000);
|
|
72
|
+
// Should converge close to x=3
|
|
73
|
+
expect(result.params['x']).toBeCloseTo(3.0, 1);
|
|
74
|
+
expect(result.maxError).toBeLessThan(0.1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('handles integer constraints', () => {
|
|
78
|
+
const mockAdapter: TunerAdapter = {
|
|
79
|
+
name: 'test-integer',
|
|
80
|
+
description: 'Integer parameter test',
|
|
81
|
+
params: [
|
|
82
|
+
{ name: 'n', range: [0, 10], integer: true } as ParamSpec,
|
|
83
|
+
],
|
|
84
|
+
simulate(input: number, params: Record<string, number>): number {
|
|
85
|
+
// Should snap to integer 7
|
|
86
|
+
return Math.round(params['n'] * 10000);
|
|
87
|
+
},
|
|
88
|
+
reference(_input: number): number {
|
|
89
|
+
return 70000; // 7.0 × 10000
|
|
90
|
+
},
|
|
91
|
+
sampleInputs(): number[] {
|
|
92
|
+
return [1];
|
|
93
|
+
},
|
|
94
|
+
generateCode(): string {
|
|
95
|
+
return '';
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const result = search(mockAdapter, 2000);
|
|
100
|
+
// Should find n close to 7
|
|
101
|
+
expect(Math.round(result.params['n'])).toBe(7);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('i32 overflow penalization', () => {
|
|
105
|
+
const mockAdapter: TunerAdapter = {
|
|
106
|
+
name: 'test-overflow',
|
|
107
|
+
description: 'Test overflow penalization',
|
|
108
|
+
params: [
|
|
109
|
+
{ name: 'scale', range: [1, 1000], integer: true } as ParamSpec,
|
|
110
|
+
],
|
|
111
|
+
simulate(_input: number, params: Record<string, number>): number {
|
|
112
|
+
// Always overflow for any scale >= 500
|
|
113
|
+
if (params['scale'] >= 500) return Infinity;
|
|
114
|
+
return params['scale'] * 10000;
|
|
115
|
+
},
|
|
116
|
+
reference(_input: number): number {
|
|
117
|
+
return 2000000; // target: scale=200 → 2000000
|
|
118
|
+
},
|
|
119
|
+
sampleInputs(): number[] {
|
|
120
|
+
return [1];
|
|
121
|
+
},
|
|
122
|
+
generateCode(): string {
|
|
123
|
+
return '';
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const { maxError, mae, rmse } = evaluate(mockAdapter, { scale: 2147483647 });
|
|
128
|
+
expect(maxError).toBe(Infinity);
|
|
129
|
+
expect(mae).toBe(Infinity);
|
|
130
|
+
expect(rmse).toBe(Infinity);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ─── ln-polynomial adapter tests ─────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
describe('ln-polynomial adapter', () => {
|
|
137
|
+
const defaultParams = lnDefaultParams; // { A1: 20000, A3: 6667, A5: 4000 }
|
|
138
|
+
|
|
139
|
+
test('sample inputs cover the valid range', () => {
|
|
140
|
+
const inputs = lnPolynomialAdapter.sampleInputs();
|
|
141
|
+
expect(inputs.length).toBeGreaterThan(50);
|
|
142
|
+
// All inputs should be positive
|
|
143
|
+
expect(inputs.every(x => x > 0)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('reference matches Math.log', () => {
|
|
147
|
+
const SCALE = 10000;
|
|
148
|
+
// ln(1.0) = 0
|
|
149
|
+
expect(lnPolynomialAdapter.reference(SCALE)).toBeCloseTo(0, 5);
|
|
150
|
+
// ln(2.0) ≈ 0.6931 → 6931
|
|
151
|
+
expect(lnPolynomialAdapter.reference(2 * SCALE)).toBeCloseTo(6931.47, 0);
|
|
152
|
+
// ln(0.5) ≈ -0.6931 → -6931
|
|
153
|
+
expect(lnPolynomialAdapter.reference(5000)).toBeCloseTo(-6931.47, 0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('simulate produces reasonable output for x=1 (no error)', () => {
|
|
157
|
+
const result = lnPolynomialAdapter.simulate(10000, defaultParams);
|
|
158
|
+
// ln(1.0) = 0; allow some approximation error
|
|
159
|
+
expect(Math.abs(result)).toBeLessThan(500); // within 0.05
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('simulate returns Infinity for invalid input', () => {
|
|
163
|
+
const result = lnPolynomialAdapter.simulate(0, defaultParams);
|
|
164
|
+
expect(result).toBeLessThan(0); // negative sentinel or -MAX_INT
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('max_error < 0.001 with default atanh coefficients', () => {
|
|
168
|
+
const metrics = evaluate(lnPolynomialAdapter, defaultParams);
|
|
169
|
+
expect(metrics.maxError).toBeLessThan(0.001);
|
|
170
|
+
}, 10000);
|
|
171
|
+
|
|
172
|
+
test('search improves over default params', () => {
|
|
173
|
+
// Run a short search and confirm it doesn't get worse
|
|
174
|
+
const baseMetrics = evaluate(lnPolynomialAdapter, defaultParams);
|
|
175
|
+
const result = search(lnPolynomialAdapter, 500); // short budget for test speed
|
|
176
|
+
// Either same or better
|
|
177
|
+
expect(result.maxError).toBeLessThanOrEqual(baseMetrics.maxError * 2);
|
|
178
|
+
expect(result.maxError).toBeLessThan(0.01);
|
|
179
|
+
}, 30000);
|
|
180
|
+
|
|
181
|
+
test('generateCode produces valid output', () => {
|
|
182
|
+
const meta = {
|
|
183
|
+
maxError: 0.00003,
|
|
184
|
+
mae: 0.000012,
|
|
185
|
+
rmse: 0.000015,
|
|
186
|
+
estimatedCmds: 38,
|
|
187
|
+
tuneDate: '2026-03-17',
|
|
188
|
+
budgetUsed: 5000,
|
|
189
|
+
};
|
|
190
|
+
const code = lnPolynomialAdapter.generateCode(defaultParams, meta);
|
|
191
|
+
expect(code).toContain('AUTO-GENERATED');
|
|
192
|
+
expect(code).toContain('ln-polynomial');
|
|
193
|
+
expect(code).toContain('fn ln');
|
|
194
|
+
expect(code).toContain('A1');
|
|
195
|
+
expect(code).toContain('A3');
|
|
196
|
+
expect(code).toContain('A5');
|
|
197
|
+
expect(code).toContain('2026-03-17');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('searchSA achieves max_error < 0.001 on ln-polynomial', () => {
|
|
201
|
+
const result = searchSA(lnPolynomialAdapter, 3000);
|
|
202
|
+
expect(result.maxError).toBeLessThan(0.001);
|
|
203
|
+
}, 30000);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─── sqrt-newton adapter tests ────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
describe('sqrt-newton adapter', () => {
|
|
209
|
+
test('simulate(10000, defaultParams) ≈ 10000 (sqrt(1.0)=1.0)', () => {
|
|
210
|
+
const result = sqrtNewtonAdapter.simulate(10000, sqrtDefaultParams);
|
|
211
|
+
// sqrt(1.0) * 10000 = 10000
|
|
212
|
+
expect(Math.abs(result - 10000)).toBeLessThan(10);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('simulate(40000, defaultParams) ≈ 20000 (sqrt(4.0)=2.0)', () => {
|
|
216
|
+
const result = sqrtNewtonAdapter.simulate(40000, sqrtDefaultParams);
|
|
217
|
+
// sqrt(4.0) * 10000 = 20000
|
|
218
|
+
expect(Math.abs(result - 20000)).toBeLessThan(10);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('simulate(0) returns 0', () => {
|
|
222
|
+
expect(sqrtNewtonAdapter.simulate(0, sqrtDefaultParams)).toBe(0);
|
|
223
|
+
expect(sqrtNewtonAdapter.simulate(-1, sqrtDefaultParams)).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('simulate(250000, defaultParams) ≈ 50000 (sqrt(25.0)=5.0)', () => {
|
|
227
|
+
const result = sqrtNewtonAdapter.simulate(250000, sqrtDefaultParams);
|
|
228
|
+
expect(Math.abs(result - 50000)).toBeLessThan(10);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('sample inputs are all positive', () => {
|
|
232
|
+
const inputs = sqrtNewtonAdapter.sampleInputs();
|
|
233
|
+
expect(inputs.length).toBeGreaterThan(50);
|
|
234
|
+
expect(inputs.every(x => x > 0)).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('reference matches Math.sqrt', () => {
|
|
238
|
+
const SCALE = 10000;
|
|
239
|
+
expect(sqrtNewtonAdapter.reference(SCALE)).toBe(SCALE); // sqrt(1.0)
|
|
240
|
+
expect(sqrtNewtonAdapter.reference(4 * SCALE)).toBe(2 * SCALE); // sqrt(4.0)
|
|
241
|
+
expect(sqrtNewtonAdapter.reference(9 * SCALE)).toBe(3 * SCALE); // sqrt(9.0)
|
|
242
|
+
expect(sqrtNewtonAdapter.reference(0)).toBe(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('generateCode contains fn sqrt_fx', () => {
|
|
246
|
+
const meta = {
|
|
247
|
+
maxError: 1.5,
|
|
248
|
+
mae: 0.5,
|
|
249
|
+
rmse: 0.8,
|
|
250
|
+
estimatedCmds: 30,
|
|
251
|
+
tuneDate: '2026-03-17',
|
|
252
|
+
budgetUsed: 3000,
|
|
253
|
+
};
|
|
254
|
+
const code = sqrtNewtonAdapter.generateCode(sqrtDefaultParams, meta);
|
|
255
|
+
expect(code).toContain('AUTO-GENERATED');
|
|
256
|
+
expect(code).toContain('sqrt-newton');
|
|
257
|
+
expect(code).toContain('fn sqrt_fx');
|
|
258
|
+
expect(code).toContain('2026-03-17');
|
|
259
|
+
});
|
|
260
|
+
});
|
package/src/ast/types.ts
CHANGED
|
@@ -171,6 +171,7 @@ export type Expr =
|
|
|
171
171
|
| { kind: 'struct_lit'; fields: { name: string; value: Expr }[]; span?: Span }
|
|
172
172
|
| { kind: 'member_assign'; obj: Expr; field: string; op: AssignOp; value: Expr; span?: Span }
|
|
173
173
|
| { kind: 'index'; obj: Expr; index: Expr; span?: Span }
|
|
174
|
+
| { kind: 'index_assign'; obj: Expr; index: Expr; op: AssignOp; value: Expr; span?: Span }
|
|
174
175
|
| { kind: 'array_lit'; elements: Expr[]; span?: Span }
|
|
175
176
|
| { kind: 'static_call'; type: string; method: string; args: Expr[]; span?: Span }
|
|
176
177
|
| { kind: 'path_expr'; enumName: string; variant: string; span?: Span }
|
package/src/emit/modules.ts
CHANGED
|
@@ -560,6 +560,11 @@ function rewriteExpr(expr: Expr, symbolMap: Map<string, string>): void {
|
|
|
560
560
|
rewriteExpr(expr.obj, symbolMap)
|
|
561
561
|
rewriteExpr(expr.index, symbolMap)
|
|
562
562
|
break
|
|
563
|
+
case 'index_assign':
|
|
564
|
+
rewriteExpr(expr.obj, symbolMap)
|
|
565
|
+
rewriteExpr(expr.index, symbolMap)
|
|
566
|
+
rewriteExpr(expr.value, symbolMap)
|
|
567
|
+
break
|
|
563
568
|
case 'array_lit':
|
|
564
569
|
for (const el of expr.elements) rewriteExpr(el, symbolMap)
|
|
565
570
|
break
|
package/src/hir/lower.ts
CHANGED
|
@@ -409,6 +409,36 @@ function lowerExpr(expr: Expr): HIRExpr {
|
|
|
409
409
|
case 'index':
|
|
410
410
|
return { kind: 'index', obj: lowerExpr(expr.obj), index: lowerExpr(expr.index), span: expr.span }
|
|
411
411
|
|
|
412
|
+
// --- Desugaring: compound index_assign → plain index_assign ---
|
|
413
|
+
case 'index_assign':
|
|
414
|
+
if (expr.op !== '=') {
|
|
415
|
+
const binOp = COMPOUND_TO_BINOP[expr.op]
|
|
416
|
+
const obj = lowerExpr(expr.obj)
|
|
417
|
+
const index = lowerExpr(expr.index)
|
|
418
|
+
return {
|
|
419
|
+
kind: 'index_assign',
|
|
420
|
+
obj,
|
|
421
|
+
index,
|
|
422
|
+
op: '=' as const,
|
|
423
|
+
value: {
|
|
424
|
+
kind: 'binary',
|
|
425
|
+
op: binOp as any,
|
|
426
|
+
left: { kind: 'index', obj, index },
|
|
427
|
+
right: lowerExpr(expr.value),
|
|
428
|
+
span: expr.span,
|
|
429
|
+
},
|
|
430
|
+
span: expr.span,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
kind: 'index_assign',
|
|
435
|
+
obj: lowerExpr(expr.obj),
|
|
436
|
+
index: lowerExpr(expr.index),
|
|
437
|
+
op: expr.op,
|
|
438
|
+
value: lowerExpr(expr.value),
|
|
439
|
+
span: expr.span,
|
|
440
|
+
}
|
|
441
|
+
|
|
412
442
|
case 'call':
|
|
413
443
|
return { kind: 'call', fn: expr.fn, args: expr.args.map(lowerExpr), typeArgs: expr.typeArgs, span: expr.span }
|
|
414
444
|
|
package/src/hir/monomorphize.ts
CHANGED
|
@@ -312,6 +312,8 @@ class Monomorphizer {
|
|
|
312
312
|
return { ...expr, value: this.rewriteExpr(expr.value, ctx) }
|
|
313
313
|
case 'member_assign':
|
|
314
314
|
return { ...expr, obj: this.rewriteExpr(expr.obj, ctx), value: this.rewriteExpr(expr.value, ctx) }
|
|
315
|
+
case 'index_assign':
|
|
316
|
+
return { ...expr, obj: this.rewriteExpr(expr.obj, ctx), index: this.rewriteExpr(expr.index, ctx), value: this.rewriteExpr(expr.value, ctx) }
|
|
315
317
|
case 'member':
|
|
316
318
|
return { ...expr, obj: this.rewriteExpr(expr.obj, ctx) }
|
|
317
319
|
case 'index':
|
package/src/hir/types.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
EntityTypeName,
|
|
25
25
|
LambdaParam,
|
|
26
26
|
} from '../ast/types'
|
|
27
|
-
import type { BinOp, CmpOp } from '../ast/types'
|
|
27
|
+
import type { BinOp, CmpOp, AssignOp } from '../ast/types'
|
|
28
28
|
|
|
29
29
|
// Re-export types that HIR shares with AST unchanged
|
|
30
30
|
export type {
|
|
@@ -40,6 +40,7 @@ export type {
|
|
|
40
40
|
LambdaParam,
|
|
41
41
|
BinOp,
|
|
42
42
|
CmpOp,
|
|
43
|
+
AssignOp,
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
// ---------------------------------------------------------------------------
|
|
@@ -77,6 +78,7 @@ export type HIRExpr =
|
|
|
77
78
|
// Assignment — only plain '=' (compound ops desugared)
|
|
78
79
|
| { kind: 'assign'; target: string; value: HIRExpr; span?: Span }
|
|
79
80
|
| { kind: 'member_assign'; obj: HIRExpr; field: string; value: HIRExpr; span?: Span }
|
|
81
|
+
| { kind: 'index_assign'; obj: HIRExpr; index: HIRExpr; op: AssignOp; value: HIRExpr; span?: Span }
|
|
80
82
|
// Access
|
|
81
83
|
| { kind: 'member'; obj: HIRExpr; field: string; span?: Span }
|
|
82
84
|
| { kind: 'index'; obj: HIRExpr; index: HIRExpr; span?: Span }
|