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.
Files changed (82) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +18 -2
  3. package/dist/src/__tests__/array-dynamic.test.d.ts +12 -0
  4. package/dist/src/__tests__/array-dynamic.test.js +131 -0
  5. package/dist/src/__tests__/array-write.test.d.ts +11 -0
  6. package/dist/src/__tests__/array-write.test.js +149 -0
  7. package/dist/src/__tests__/tuner/engine.test.d.ts +4 -0
  8. package/dist/src/__tests__/tuner/engine.test.js +232 -0
  9. package/dist/src/ast/types.d.ts +7 -0
  10. package/dist/src/emit/modules.js +5 -0
  11. package/dist/src/hir/lower.js +29 -0
  12. package/dist/src/hir/monomorphize.js +2 -0
  13. package/dist/src/hir/types.d.ts +9 -2
  14. package/dist/src/lir/lower.js +131 -0
  15. package/dist/src/mir/lower.js +73 -3
  16. package/dist/src/mir/macro.js +5 -0
  17. package/dist/src/mir/types.d.ts +12 -0
  18. package/dist/src/mir/verify.js +7 -0
  19. package/dist/src/optimizer/copy_prop.js +5 -0
  20. package/dist/src/optimizer/coroutine.js +12 -0
  21. package/dist/src/optimizer/dce.js +9 -0
  22. package/dist/src/optimizer/unroll.js +3 -0
  23. package/dist/src/parser/index.js +5 -0
  24. package/dist/src/tuner/adapters/ln-polynomial.d.ts +23 -0
  25. package/dist/src/tuner/adapters/ln-polynomial.js +142 -0
  26. package/dist/src/tuner/adapters/sqrt-newton.d.ts +28 -0
  27. package/dist/src/tuner/adapters/sqrt-newton.js +125 -0
  28. package/dist/src/tuner/cli.d.ts +5 -0
  29. package/dist/src/tuner/cli.js +168 -0
  30. package/dist/src/tuner/engine.d.ts +17 -0
  31. package/dist/src/tuner/engine.js +215 -0
  32. package/dist/src/tuner/metrics.d.ts +15 -0
  33. package/dist/src/tuner/metrics.js +51 -0
  34. package/dist/src/tuner/simulator.d.ts +35 -0
  35. package/dist/src/tuner/simulator.js +78 -0
  36. package/dist/src/tuner/types.d.ts +32 -0
  37. package/dist/src/tuner/types.js +6 -0
  38. package/dist/src/typechecker/index.js +5 -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/package.json +1 -1
  43. package/src/__tests__/array-dynamic.test.ts +147 -0
  44. package/src/__tests__/array-write.test.ts +169 -0
  45. package/src/__tests__/tuner/engine.test.ts +260 -0
  46. package/src/ast/types.ts +1 -0
  47. package/src/emit/modules.ts +5 -0
  48. package/src/hir/lower.ts +30 -0
  49. package/src/hir/monomorphize.ts +2 -0
  50. package/src/hir/types.ts +3 -1
  51. package/src/lir/lower.ts +151 -0
  52. package/src/mir/lower.ts +75 -3
  53. package/src/mir/macro.ts +5 -0
  54. package/src/mir/types.ts +2 -0
  55. package/src/mir/verify.ts +7 -0
  56. package/src/optimizer/copy_prop.ts +5 -0
  57. package/src/optimizer/coroutine.ts +9 -0
  58. package/src/optimizer/dce.ts +6 -0
  59. package/src/optimizer/unroll.ts +3 -0
  60. package/src/parser/index.ts +9 -0
  61. package/src/stdlib/bigint.mcrs +155 -192
  62. package/src/stdlib/bits.mcrs +158 -0
  63. package/src/stdlib/color.mcrs +160 -0
  64. package/src/stdlib/geometry.mcrs +124 -0
  65. package/src/stdlib/list.mcrs +96 -0
  66. package/src/stdlib/math.mcrs +227 -0
  67. package/src/stdlib/math_hp.mcrs +65 -0
  68. package/src/stdlib/random.mcrs +67 -0
  69. package/src/stdlib/signal.mcrs +112 -0
  70. package/src/stdlib/timer.mcrs +32 -0
  71. package/src/stdlib/vec.mcrs +27 -0
  72. package/src/tuner/adapters/ln-polynomial.ts +147 -0
  73. package/src/tuner/adapters/sqrt-newton.ts +135 -0
  74. package/src/tuner/cli.ts +158 -0
  75. package/src/tuner/engine.ts +272 -0
  76. package/src/tuner/metrics.ts +66 -0
  77. package/src/tuner/simulator.ts +69 -0
  78. package/src/tuner/types.ts +44 -0
  79. package/src/typechecker/index.ts +6 -0
  80. package/docs/ARCHITECTURE.zh.md +0 -1088
  81. package/docs/COMPILATION_STATS.md +0 -142
  82. 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 }
@@ -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
 
@@ -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 }