redscript-mc 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +66 -21
  3. package/README.zh.md +61 -61
  4. package/dist/src/__tests__/e2e/basic.test.js +25 -0
  5. package/dist/src/__tests__/e2e/coroutine.test.js +22 -0
  6. package/dist/src/__tests__/mc-integration.test.js +25 -13
  7. package/dist/src/__tests__/schedule.test.js +105 -0
  8. package/dist/src/__tests__/tuner/engine.test.d.ts +4 -0
  9. package/dist/src/__tests__/tuner/engine.test.js +232 -0
  10. package/dist/src/__tests__/typechecker.test.js +63 -0
  11. package/dist/src/emit/compile.js +1 -0
  12. package/dist/src/emit/index.js +3 -1
  13. package/dist/src/lir/lower.js +26 -0
  14. package/dist/src/mir/lower.js +341 -12
  15. package/dist/src/mir/types.d.ts +10 -0
  16. package/dist/src/optimizer/copy_prop.js +4 -0
  17. package/dist/src/optimizer/coroutine.d.ts +2 -0
  18. package/dist/src/optimizer/coroutine.js +33 -1
  19. package/dist/src/optimizer/dce.js +7 -1
  20. package/dist/src/optimizer/lir/const_imm.js +1 -1
  21. package/dist/src/optimizer/lir/dead_slot.js +1 -1
  22. package/dist/src/tuner/adapters/ln-polynomial.d.ts +23 -0
  23. package/dist/src/tuner/adapters/ln-polynomial.js +142 -0
  24. package/dist/src/tuner/adapters/sqrt-newton.d.ts +28 -0
  25. package/dist/src/tuner/adapters/sqrt-newton.js +125 -0
  26. package/dist/src/tuner/cli.d.ts +5 -0
  27. package/dist/src/tuner/cli.js +168 -0
  28. package/dist/src/tuner/engine.d.ts +17 -0
  29. package/dist/src/tuner/engine.js +215 -0
  30. package/dist/src/tuner/metrics.d.ts +15 -0
  31. package/dist/src/tuner/metrics.js +51 -0
  32. package/dist/src/tuner/simulator.d.ts +35 -0
  33. package/dist/src/tuner/simulator.js +78 -0
  34. package/dist/src/tuner/types.d.ts +32 -0
  35. package/dist/src/tuner/types.js +6 -0
  36. package/dist/src/typechecker/index.d.ts +2 -0
  37. package/dist/src/typechecker/index.js +29 -0
  38. package/docs/ROADMAP.md +35 -0
  39. package/docs/STDLIB_ROADMAP.md +142 -0
  40. package/editors/vscode/package-lock.json +3 -3
  41. package/editors/vscode/package.json +1 -1
  42. package/examples/coroutine-demo.mcrs +11 -10
  43. package/jest.config.js +19 -0
  44. package/package.json +1 -1
  45. package/src/__tests__/e2e/basic.test.ts +27 -0
  46. package/src/__tests__/e2e/coroutine.test.ts +23 -0
  47. package/src/__tests__/fixtures/array-test.mcrs +21 -22
  48. package/src/__tests__/fixtures/counter.mcrs +17 -0
  49. package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
  50. package/src/__tests__/mc-integration.test.ts +25 -13
  51. package/src/__tests__/schedule.test.ts +112 -0
  52. package/src/__tests__/tuner/engine.test.ts +260 -0
  53. package/src/__tests__/typechecker.test.ts +68 -0
  54. package/src/emit/compile.ts +1 -0
  55. package/src/emit/index.ts +3 -1
  56. package/src/lir/lower.ts +27 -0
  57. package/src/mir/lower.ts +355 -9
  58. package/src/mir/types.ts +4 -0
  59. package/src/optimizer/copy_prop.ts +4 -0
  60. package/src/optimizer/coroutine.ts +37 -1
  61. package/src/optimizer/dce.ts +6 -1
  62. package/src/optimizer/lir/const_imm.ts +1 -1
  63. package/src/optimizer/lir/dead_slot.ts +1 -1
  64. package/src/stdlib/bigint.mcrs +155 -192
  65. package/src/stdlib/bits.mcrs +158 -0
  66. package/src/stdlib/color.mcrs +160 -0
  67. package/src/stdlib/geometry.mcrs +124 -0
  68. package/src/stdlib/list.mcrs +125 -0
  69. package/src/stdlib/math.mcrs +90 -0
  70. package/src/stdlib/math_hp.mcrs +65 -0
  71. package/src/stdlib/random.mcrs +67 -0
  72. package/src/stdlib/signal.mcrs +112 -0
  73. package/src/stdlib/timer.mcrs +10 -5
  74. package/src/stdlib/vec.mcrs +27 -0
  75. package/src/tuner/adapters/ln-polynomial.ts +147 -0
  76. package/src/tuner/adapters/sqrt-newton.ts +135 -0
  77. package/src/tuner/cli.ts +158 -0
  78. package/src/tuner/engine.ts +272 -0
  79. package/src/tuner/metrics.ts +66 -0
  80. package/src/tuner/simulator.ts +69 -0
  81. package/src/tuner/types.ts +44 -0
  82. package/src/typechecker/index.ts +39 -0
  83. package/docs/ARCHITECTURE.zh.md +0 -1088
  84. package/docs/COMPILATION_STATS.md +0 -142
  85. package/docs/IMPLEMENTATION_GUIDE.md +0 -512
@@ -0,0 +1,112 @@
1
+ // signal.mcrs — Statistical distributions and probability helpers.
2
+ //
3
+ // All functions use integer arithmetic with ×10000 scale where noted.
4
+ // For game logic: loot tables, mob spawn weights, particle density, etc.
5
+ //
6
+ // Requires: import "stdlib/random" for seed-based RNG
7
+
8
+ module library;
9
+
10
+ // ─── Uniform distribution ────────────────────────────────────────────────────
11
+
12
+ // uniform_int(seed, lo, hi): integer in [lo, hi] inclusive
13
+ // Returns new seed for chaining: use the result as next seed input.
14
+ fn uniform_int(seed: int, lo: int, hi: int): int {
15
+ let r: int = seed * 1664525 + 1013904223;
16
+ if (r < 0) { r = 0 - r; }
17
+ return lo + r % (hi - lo + 1);
18
+ }
19
+
20
+ // uniform_frac(seed): uniform fraction in [0, 10000] (×10000 scale)
21
+ fn uniform_frac(seed: int): int {
22
+ let r: int = seed * 1664525 + 1013904223;
23
+ if (r < 0) { r = 0 - r; }
24
+ return r % 10001;
25
+ }
26
+
27
+ // ─── Normal (Gaussian) approximation ─────────────────────────────────────────
28
+ //
29
+ // Method: Sum of 12 uniform [0,1] random values → subtract 6 → scale.
30
+ // Result approximates N(0,1). Good enough for game use.
31
+ // (Box-Muller requires sqrt/ln; this is cheaper and sufficient.)
32
+ //
33
+ // normal_approx12(seed): approximate N(0, 1) × 10000, range ≈ [-60000, 60000]
34
+ // Each call consumes the seed 12 times (chains internally).
35
+ fn normal_approx12(seed: int): int {
36
+ let s: int = seed;
37
+ let sum: int = 0;
38
+ // 12 uniform samples [0, 10000]
39
+ let r: int = 0;
40
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
41
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
42
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
43
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
44
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
45
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
46
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
47
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
48
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
49
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
50
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
51
+ s = s * 1664525 + 1013904223; r = s; if (r < 0) { r = 0 - r; } sum = sum + r % 10001;
52
+ // sum in [0, 120000], mean = 60000, σ ≈ 10000
53
+ return sum - 60000;
54
+ }
55
+
56
+ // ─── Exponential distribution ─────────────────────────────────────────────────
57
+ //
58
+ // Method: -ln(U) / λ where U is uniform [0,1].
59
+ // Approximation: use ln from stdlib/math.
60
+ //
61
+ // exp_dist_approx(seed, lambda_fx): exponential variate × 10000
62
+ // lambda_fx: rate parameter × 10000 (e.g. 10000 = rate 1.0)
63
+ // Returns value in [0, ∞) × 10000, but caps at 100000 (10.0) for MC sanity.
64
+ // Note: requires import "stdlib/math" for ln function.
65
+ fn exp_dist_approx(seed: int, lambda_fx: int): int {
66
+ let r: int = seed * 1664525 + 1013904223;
67
+ if (r < 0) { r = 0 - r; }
68
+ let u: int = 100 + r % 9901; // uniform in [100, 10000] (avoid ln(0))
69
+ // -ln(u/10000) × 10000 / lambda_fx
70
+ // ln(u) where u is ×10000 → use existing ln(u) which returns ×10000
71
+ // -ln(u/10000) = -(ln(u) - ln(10000)) = ln(10000) - ln(u)
72
+ // ln(10000) × 10000 ≈ 92103
73
+ let ln_u: int = ln(u); // requires stdlib/math
74
+ let neg_ln: int = 92103 - ln_u;
75
+ if (neg_ln < 0) { neg_ln = 0; }
76
+ let result: int = neg_ln * 10000 / lambda_fx;
77
+ if (result > 100000) { result = 100000; }
78
+ return result;
79
+ }
80
+
81
+ // ─── Bernoulli trial ──────────────────────────────────────────────────────────
82
+
83
+ // bernoulli(seed, p_fx): 1 with probability p/10000, else 0.
84
+ // p_fx: probability × 10000 (e.g. 5000 = 50%, 1000 = 10%)
85
+ fn bernoulli(seed: int, p_fx: int): int {
86
+ let r: int = seed * 1664525 + 1013904223;
87
+ if (r < 0) { r = 0 - r; }
88
+ if (r % 10000 < p_fx) { return 1; }
89
+ return 0;
90
+ }
91
+
92
+ // ─── Weighted choice ──────────────────────────────────────────────────────────
93
+
94
+ // weighted2(seed, w0, w1): choose 0 or 1 with weights w0, w1
95
+ fn weighted2(seed: int, w0: int, w1: int): int {
96
+ let total: int = w0 + w1;
97
+ let r: int = seed * 1664525 + 1013904223;
98
+ if (r < 0) { r = 0 - r; }
99
+ if (r % total < w0) { return 0; }
100
+ return 1;
101
+ }
102
+
103
+ // weighted3(seed, w0, w1, w2): choose 0, 1, or 2 with given weights
104
+ fn weighted3(seed: int, w0: int, w1: int, w2: int): int {
105
+ let total: int = w0 + w1 + w2;
106
+ let r: int = seed * 1664525 + 1013904223;
107
+ if (r < 0) { r = 0 - r; }
108
+ let v: int = r % total;
109
+ if (v < w0) { return 0; }
110
+ if (v < w0 + w1) { return 1; }
111
+ return 2;
112
+ }
@@ -1,11 +1,16 @@
1
1
  // Timer utilities with an OOP-style API.
2
2
  //
3
- // Timer state is stored in scoreboard-backed runtime state. Because RedScript
4
- // does not yet support dynamic scoreboard player/objective names for impl
5
- // methods, this API currently uses a single shared runtime slot.
3
+ // Each Timer::new() call is statically allocated a unique compile-time ID
4
+ // (0, 1, 2, ...). The compiler intercepts Timer method calls and inlines
5
+ // them as direct scoreboard operations on per-instance slots:
6
+ // __timer_N_ticks, __timer_N_active (stored in the namespace objective)
6
7
  //
7
- // The `_id` field is reserved for a future runtime-backed instance identifier.
8
- // Today it remains `0`, while persistence is shared across Timer values.
8
+ // Restriction: Timer::new() must be called at the top level of a function,
9
+ // not inside loops or if/else bodies (compile error enforced by TypeChecker).
10
+ //
11
+ // This file provides the struct definition and method stubs. The actual
12
+ // scoreboard operations are generated by the MIR lowering pass and never
13
+ // call these method bodies directly when the _id is statically known.
9
14
 
10
15
  struct Timer {
11
16
  _id: int,
@@ -244,3 +244,30 @@ fn chebyshev3d(x1: int, y1: int, z1: int, x2: int, y2: int, z2: int) -> int {
244
244
  if (dz > m) { m = dz; }
245
245
  return m;
246
246
  }
247
+
248
+ // ─── 2D vector arithmetic ─────────────────────────────────────────────────────
249
+
250
+ // add2d / sub2d / scale2d: component-wise ops. Return x or y component.
251
+ fn add2d_x(ax: int, bx: int): int { return ax + bx; }
252
+ fn add2d_y(ay: int, by: int): int { return ay + by; }
253
+ fn sub2d_x(ax: int, bx: int): int { return ax - bx; }
254
+ fn sub2d_y(ay: int, by: int): int { return ay - by; }
255
+ fn scale2d_x(x: int, s: int): int { return x * s / 1000; }
256
+ fn scale2d_y(y: int, s: int): int { return y * s / 1000; }
257
+ fn neg2d_x(x: int): int { return 0 - x; }
258
+ fn neg2d_y(y: int): int { return 0 - y; }
259
+
260
+ // ─── 3D vector arithmetic ─────────────────────────────────────────────────────
261
+
262
+ fn add3d_x(ax: int, bx: int): int { return ax + bx; }
263
+ fn add3d_y(ay: int, by: int): int { return ay + by; }
264
+ fn add3d_z(az: int, bz: int): int { return az + bz; }
265
+ fn sub3d_x(ax: int, bx: int): int { return ax - bx; }
266
+ fn sub3d_y(ay: int, by: int): int { return ay - by; }
267
+ fn sub3d_z(az: int, bz: int): int { return az - bz; }
268
+ fn scale3d_x(x: int, s: int): int { return x * s / 1000; }
269
+ fn scale3d_y(y: int, s: int): int { return y * s / 1000; }
270
+ fn scale3d_z(z: int, s: int): int { return z * s / 1000; }
271
+ fn neg3d_x(x: int): int { return 0 - x; }
272
+ fn neg3d_y(y: int): int { return 0 - y; }
273
+ fn neg3d_z(z: int): int { return 0 - z; }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * ln(x) polynomial approximation adapter — atanh series form.
3
+ *
4
+ * Algorithm:
5
+ * Input x: fixed-point integer (×10000), e.g. 10000 = 1.0
6
+ * 1. Range reduction: find k s.t. xr ∈ [10000, 20000)
7
+ * 2. s = (xr - 10000) * 10000 / (xr + 10000) → s ∈ [0, 3333]
8
+ * 3. ln(xr/10000) ≈ A1*s/SCALE + A3*s³/SCALE² + A5*s⁵/SCALE³
9
+ * (coefficients absorb the factor of 2; theoretical: A1=20000, A3=6667, A5=4000)
10
+ * 4. ln(x/10000) = k * LN2 + ln(xr/10000)
11
+ *
12
+ * Intermediate overflow analysis (s ≤ 3333, SCALE = 10000):
13
+ * s² = s*s ≤ 11M — fits int32 (max ~2.1B)
14
+ * s2 = s²/SCALE ≤ 1111
15
+ * s3 = s*s2 ≤ 3.7M — fits int32
16
+ * s5 = s3*s2 ≤ 4.1M — fits int32
17
+ * A1*s ≤ 22000*3333 ≤ 73M — fits int32
18
+ * A3*s3 ≤ 7000*3703 ≤ 26M — fits int32
19
+ * A5*s5 ≤ 5000*4115 ≤ 21M — fits int32
20
+ */
21
+
22
+ import { TunerAdapter, ParamSpec, ResultMeta } from '../types';
23
+ import { i32 } from '../simulator';
24
+
25
+ const SCALE = 10000;
26
+ const LN2_SCALED = 6931; // ln(2) * 10000
27
+
28
+ export const defaultParams: Record<string, number> = {
29
+ A1: 20000,
30
+ A3: 6667,
31
+ A5: 4000,
32
+ };
33
+
34
+ export const lnPolynomialAdapter: TunerAdapter = {
35
+ name: 'ln-polynomial',
36
+ description: 'ln(x) atanh series approximation using fixed-point int32 arithmetic',
37
+
38
+ params: [
39
+ { name: 'A1', range: [18000, 22000], integer: true } as ParamSpec,
40
+ { name: 'A3', range: [5000, 9000], integer: true } as ParamSpec,
41
+ { name: 'A5', range: [2000, 6000], integer: true } as ParamSpec,
42
+ ],
43
+
44
+ simulate(input: number, params: Record<string, number>): number {
45
+ const A1 = i32(params['A1']);
46
+ const A3 = i32(params['A3']);
47
+ const A5 = i32(params['A5']);
48
+
49
+ let x = i32(input);
50
+ if (x <= 0) return -2147483648;
51
+
52
+ // Step 1: range reduction to [10000, 20000)
53
+ let k = 0;
54
+ while (x < 10000) { x = i32(x * 2); k--; }
55
+ while (x >= 20000) { x = i32(x / 2); k++; }
56
+
57
+ // Step 2: s = (xr - SCALE) * SCALE / (xr + SCALE)
58
+ const num = i32(i32(x - SCALE) * SCALE);
59
+ const den = i32(x + SCALE);
60
+ if (den === 0) return Infinity as unknown as number;
61
+ const s = i32(num / den); // s ∈ [0, 3333]
62
+
63
+ // Step 3: atanh series (overflow-safe, each power divided by SCALE)
64
+ const s2 = i32(i32(s * s) / SCALE); // s²/SCALE ∈ [0, 1111]
65
+ const s3 = i32(i32(s * s2) / SCALE); // s³/SCALE² ∈ [0, 370]
66
+ const s5 = i32(i32(s3 * s2) / SCALE); // s⁵/SCALE⁴ ∈ [0, 41]
67
+
68
+ const lnxr = i32(
69
+ i32(A1 * s / SCALE) +
70
+ i32(A3 * s3 / SCALE) +
71
+ i32(A5 * s5 / SCALE)
72
+ );
73
+
74
+ // Step 4: ln(x) = k*ln(2) + ln(xr)
75
+ return i32(i32(k * LN2_SCALED) + lnxr);
76
+ },
77
+
78
+ reference(input: number): number {
79
+ if (input <= 0) return -Infinity;
80
+ return Math.log(input / SCALE) * SCALE;
81
+ },
82
+
83
+ sampleInputs(): number[] {
84
+ const inputs: number[] = [];
85
+ // logarithmic sampling from 0.01 to 100 (×10000: 100 to 1_000_000)
86
+ const logMin = Math.log10(100);
87
+ const logMax = Math.log10(1_000_000);
88
+ const steps = 200;
89
+ for (let i = 0; i <= steps; i++) {
90
+ const v = logMin + (i / steps) * (logMax - logMin);
91
+ inputs.push(Math.round(Math.pow(10, v)));
92
+ }
93
+ return inputs;
94
+ },
95
+
96
+ generateCode(params: Record<string, number>, meta: ResultMeta): string {
97
+ const A1 = Math.round(params['A1']);
98
+ const A3 = Math.round(params['A3']);
99
+ const A5 = Math.round(params['A5']);
100
+ return `// AUTO-GENERATED by redscript tune — DO NOT EDIT
101
+ // Adapter: ln-polynomial | Date: ${meta.tuneDate}
102
+ // max_error: ${meta.maxError.toFixed(6)} | mae: ${meta.mae.toFixed(6)} | estimated_cmds: ${meta.estimatedCmds}
103
+ // Run \`redscript tune --adapter ln-polynomial\` to regenerate
104
+ //
105
+ // atanh series coefficients (×10000 scale):
106
+ // A1 = ${A1} (theoretical 2×10000 = 20000)
107
+ // A3 = ${A3} (theoretical 2/3×10000 = 6667)
108
+ // A5 = ${A5} (theoretical 2/5×10000 = 4000)
109
+
110
+ fn ln(x: int): int {
111
+ // Input x: fixed-point ×10000; returns ln(x/10000)×10000
112
+ // Valid range: x ∈ [100, 1000000] (0.01 ~ 100.0)
113
+ // max_error: ${meta.maxError.toFixed(6)} (in ×10000 units, i.e. ${(meta.maxError / SCALE).toFixed(8)} in real units)
114
+
115
+ let scale: int = ${SCALE};
116
+ let ln2: int = ${LN2_SCALED};
117
+ let A1: int = ${A1};
118
+ let A3: int = ${A3};
119
+ let A5: int = ${A5};
120
+
121
+ // Step 1: range reduction — bring x into [scale, 2*scale)
122
+ let xr: int = x;
123
+ let k: int = 0;
124
+ while (xr < scale) {
125
+ xr = xr * 2;
126
+ k = k - 1;
127
+ }
128
+ while (xr >= scale * 2) {
129
+ xr = xr / 2;
130
+ k = k + 1;
131
+ }
132
+
133
+ // Step 2: s = (xr - scale) * scale / (xr + scale)
134
+ let s: int = (xr - scale) * scale / (xr + scale);
135
+
136
+ // Step 3: atanh series ln(xr/scale) ≈ A1*s + A3*s³ + A5*s⁵ (all /scale)
137
+ let s2: int = s * s / scale;
138
+ let s3: int = s * s2;
139
+ let s5: int = s3 * s2;
140
+ let lnxr: int = A1 * s / scale + A3 * s3 / scale + A5 * s5 / scale;
141
+
142
+ // Step 4: ln(x) = k*ln(2) + ln(xr/scale)
143
+ return k * ln2 + lnxr;
144
+ }
145
+ `;
146
+ },
147
+ };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * sqrt(x) Newton's method adapter — fixed-point ×10000 scale.
3
+ *
4
+ * Algorithm:
5
+ * Input x: fixed-point integer (×10000), e.g. 10000 = 1.0
6
+ * Target: return floor(sqrt(x/10000) * 10000)
7
+ *
8
+ * 1. If x <= 0, return 0
9
+ * 2. Initial guess: g = x >> INIT_SHIFT (default: x/2)
10
+ * 3. Newton iteration: g = (g + x * 10000 / g) / 2, repeated N times
11
+ * Overflow-safe: x * 10000 / g → (x * 100) / (g / 100)
12
+ * 4. Return g
13
+ *
14
+ * Valid range: x ∈ [1, 1_000_000] (real range 0.0001 to 100.0)
15
+ * Larger x needs more iterations; beyond 1M, convergence is not guaranteed
16
+ * within N=12 iterations from an x/2 initial guess.
17
+ *
18
+ * Overflow analysis (x ≤ 1_000_000, SCALE = 10000):
19
+ * x * 100 ≤ 100_000_000 — fits int32 (max ~2.1B) ✓
20
+ * g / 100 is never 0 for g ≥ 100 (guard included) ✓
21
+ *
22
+ * Parameters:
23
+ * N: iteration count, range [4, 12], integer
24
+ * INIT_SHIFT: initial guess right shift, range [0, 3], integer (1 → x/2)
25
+ */
26
+
27
+ import { TunerAdapter, ParamSpec, ResultMeta } from '../types';
28
+ import { i32 } from '../simulator';
29
+
30
+ const SCALE = 10000;
31
+
32
+ export const defaultParams: Record<string, number> = {
33
+ N: 8,
34
+ INIT_SHIFT: 1,
35
+ };
36
+
37
+ export const sqrtNewtonAdapter: TunerAdapter = {
38
+ name: 'sqrt-newton',
39
+ description: 'sqrt(x) Newton iteration using fixed-point int32 arithmetic (×10000 scale)',
40
+
41
+ params: [
42
+ { name: 'N', range: [4, 12], integer: true } as ParamSpec,
43
+ { name: 'INIT_SHIFT', range: [0, 3], integer: true } as ParamSpec,
44
+ ],
45
+
46
+ simulate(input: number, params: Record<string, number>): number {
47
+ const N = Math.round(params['N']!);
48
+ const INIT_SHIFT = Math.round(params['INIT_SHIFT']!);
49
+
50
+ let x = i32(input);
51
+ if (x <= 0) return 0;
52
+
53
+ // Initial guess: x >> INIT_SHIFT
54
+ let g = i32(x >> INIT_SHIFT);
55
+ if (g <= 0) g = 1; // guard against zero/negative guess
56
+
57
+ // Newton iterations: g = (g + x * 10000 / g) / 2
58
+ // Overflow-safe for x ≤ 1_000_000: split x*10000/g as (x*100)/(g/100)
59
+ for (let iter = 0; iter < N; iter++) {
60
+ const gDiv = i32(g / 100);
61
+ if (gDiv <= 0) break; // guard: g is too small, stop
62
+ const xdivg = i32(i32(x * 100) / gDiv); // ≈ x * 10000 / g
63
+ g = i32(i32(g + xdivg) / 2);
64
+ if (g <= 0) { g = 1; break; }
65
+ }
66
+
67
+ return g;
68
+ },
69
+
70
+ reference(input: number): number {
71
+ if (input <= 0) return 0;
72
+ return Math.floor(Math.sqrt(input / SCALE) * SCALE);
73
+ },
74
+
75
+ sampleInputs(): number[] {
76
+ const inputs: number[] = [];
77
+
78
+ // Logarithmic sampling from 0.01 to 100.0 (×10000: 100 to 1_000_000)
79
+ // Lower bound 100 avoids g/100=0 in overflow-safe Newton step.
80
+ const logMin = Math.log10(100);
81
+ const logMax = Math.log10(1_000_000);
82
+ const steps = 200;
83
+ for (let i = 0; i <= steps; i++) {
84
+ const v = logMin + (i / steps) * (logMax - logMin);
85
+ const x = Math.round(Math.pow(10, v));
86
+ inputs.push(x);
87
+ }
88
+
89
+ // Exact perfect squares (k=1..10 → x up to 100 * 10000 = 1_000_000)
90
+ for (let k = 1; k <= 10; k++) {
91
+ inputs.push(k * k * SCALE); // sqrt should be exactly k * SCALE
92
+ }
93
+
94
+ return inputs;
95
+ },
96
+
97
+ generateCode(params: Record<string, number>, meta: ResultMeta): string {
98
+ const N = Math.round(params['N']!);
99
+ const INIT_SHIFT = Math.round(params['INIT_SHIFT']!);
100
+
101
+ const iters = Array.from({ length: N }, (_, i) =>
102
+ ` g = (g + x * 100 / (g / 100)) / 2; // iteration ${i + 1}`
103
+ ).join('\n');
104
+
105
+ return `// AUTO-GENERATED by redscript tune — DO NOT EDIT
106
+ // Adapter: sqrt-newton | Date: ${meta.tuneDate}
107
+ // max_error: ${meta.maxError.toFixed(6)} | mae: ${meta.mae.toFixed(6)} | estimated_cmds: ${meta.estimatedCmds}
108
+ // Run \`redscript tune --adapter sqrt-newton\` to regenerate
109
+ //
110
+ // Parameters:
111
+ // N = ${N} (Newton iteration count)
112
+ // INIT_SHIFT = ${INIT_SHIFT} (initial guess = x >> ${INIT_SHIFT}, i.e. x / ${1 << INIT_SHIFT})
113
+
114
+ fn sqrt_fx(x: int): int {
115
+ // Input x: fixed-point ×10000, returns floor(sqrt(x/10000)*10000)
116
+ // Valid range: x ∈ [0, 1000000] (real range 0.0 to 100.0)
117
+ // max_error: ${meta.maxError.toFixed(6)} (in ×10000 units)
118
+ //
119
+ // Overflow-safe: x*10000/g computed as (x*100)/(g/100), valid for x ≤ 1_000_000
120
+
121
+ if (x <= 0) { return 0; }
122
+
123
+ // Initial guess: x / ${1 << INIT_SHIFT}
124
+ let g: int = x / ${1 << INIT_SHIFT};
125
+ if (g <= 0) { g = 1; }
126
+
127
+ // Newton iterations: g = (g + x * 10000 / g) / 2
128
+ // Split to avoid overflow: (x * 100) / (g / 100) ≈ x * 10000 / g
129
+ ${iters}
130
+
131
+ return g;
132
+ }
133
+ `;
134
+ },
135
+ };
@@ -0,0 +1,158 @@
1
+ /**
2
+ * CLI entry point for `redscript tune`.
3
+ * Usage: redscript tune --adapter <name> [--budget N] [--out path]
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { search, searchSA } from './engine';
9
+ import { lnPolynomialAdapter } from './adapters/ln-polynomial';
10
+ import { sqrtNewtonAdapter } from './adapters/sqrt-newton';
11
+ import { TunerAdapter, ResultMeta } from './types';
12
+
13
+ const ADAPTERS: Record<string, TunerAdapter> = {
14
+ 'ln-polynomial': lnPolynomialAdapter,
15
+ 'sqrt-newton': sqrtNewtonAdapter,
16
+ };
17
+
18
+ function printUsage(): void {
19
+ console.log(`Usage: redscript tune --adapter <name> [--budget N] [--out path]
20
+
21
+ Available adapters:
22
+ ${Object.entries(ADAPTERS)
23
+ .map(([name, a]) => ` ${name.padEnd(20)} ${a.description}`)
24
+ .join('\n')}
25
+
26
+ Options:
27
+ --adapter <name> Adapter to use (required)
28
+ --budget <N> Max optimizer iterations (default: 10000)
29
+ --out <path> Output .mcrs file path (optional)
30
+ `);
31
+ }
32
+
33
+ function parseArgs(args: string[]): {
34
+ adapter?: string;
35
+ budget: number;
36
+ out?: string;
37
+ strategy: 'nm' | 'sa';
38
+ } {
39
+ const result: { adapter?: string; budget: number; out?: string; strategy: 'nm' | 'sa' } = { budget: 10000, strategy: 'nm' };
40
+
41
+ for (let i = 0; i < args.length; i++) {
42
+ if (args[i] === '--adapter' && args[i + 1]) {
43
+ result.adapter = args[++i] as string;
44
+ } else if (args[i] === '--budget' && args[i + 1]) {
45
+ result.budget = parseInt(args[++i]!, 10);
46
+ } else if (args[i] === '--out' && args[i + 1]) {
47
+ result.out = args[++i] as string;
48
+ } else if (args[i] === '--strategy' && args[i + 1]) {
49
+ const s = args[++i];
50
+ if (s === 'nm' || s === 'sa') result.strategy = s;
51
+ }
52
+ }
53
+
54
+ return result;
55
+ }
56
+
57
+ function renderProgressBar(fraction: number, width = 30): string {
58
+ const filled = Math.round(fraction * width);
59
+ const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
60
+ return `[${bar}]`;
61
+ }
62
+
63
+ async function main(): Promise<void> {
64
+ // Skip 'node', 'ts-node', 'cli.ts' etc from argv
65
+ const rawArgs = process.argv.slice(2);
66
+
67
+ // Support `redscript tune` prefix (first arg might be 'tune')
68
+ const args = rawArgs[0] === 'tune' ? rawArgs.slice(1) : rawArgs;
69
+
70
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
71
+ printUsage();
72
+ process.exit(0);
73
+ }
74
+
75
+ const { adapter: adapterName, budget, out, strategy } = parseArgs(args);
76
+
77
+ if (!adapterName) {
78
+ console.error('Error: --adapter is required');
79
+ printUsage();
80
+ process.exit(1);
81
+ }
82
+
83
+ const adapter = ADAPTERS[adapterName];
84
+ if (!adapter) {
85
+ console.error(`Error: unknown adapter "${adapterName}"`);
86
+ console.error(`Available: ${Object.keys(ADAPTERS).join(', ')}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ console.log(`\nredscript tune — ${adapter.name}`);
91
+ console.log(`Description: ${adapter.description}`);
92
+ console.log(`Strategy: ${strategy === 'sa' ? 'Simulated Annealing' : 'Nelder-Mead'}`);
93
+ console.log(`Budget: ${budget} iterations`);
94
+ console.log(`Parameters: ${adapter.params.map(p => p.name).join(', ')}\n`);
95
+
96
+ let lastProgress = 0;
97
+ const startTime = Date.now();
98
+
99
+ const searchFn = strategy === 'sa' ? searchSA : search;
100
+ const result = searchFn(adapter, budget, (iteration, bestError) => {
101
+ const fraction = iteration / budget;
102
+ const bar = renderProgressBar(fraction);
103
+ const errorStr = isFinite(bestError) ? bestError.toFixed(6) : 'Inf';
104
+ process.stdout.write(
105
+ `\r ${bar} ${(fraction * 100).toFixed(1)}% iter=${iteration} best_max_error=${errorStr} `
106
+ );
107
+ lastProgress = iteration;
108
+ });
109
+
110
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
111
+ process.stdout.write('\n');
112
+
113
+ console.log(`\n${'─'.repeat(60)}`);
114
+ console.log(`Optimization complete in ${elapsed}s`);
115
+ console.log(`Budget used: ${result.budgetUsed}/${budget} iterations`);
116
+ console.log(`\nResults:`);
117
+ console.log(` max_error : ${result.maxError.toFixed(8)}`);
118
+ console.log(` mae : ${result.mae.toFixed(8)}`);
119
+ console.log(` rmse : ${result.rmse.toFixed(8)}`);
120
+ console.log(`\nBest parameters:`);
121
+ for (const [k, v] of Object.entries(result.params)) {
122
+ console.log(` ${k.padEnd(12)} = ${v}`);
123
+ }
124
+
125
+ // Estimate command count (rough: ~4 cmds per param + 10 overhead)
126
+ const estimatedCmds = adapter.params.length * 4 + 15;
127
+
128
+ const meta: ResultMeta = {
129
+ maxError: result.maxError,
130
+ mae: result.mae,
131
+ rmse: result.rmse,
132
+ estimatedCmds,
133
+ tuneDate: new Date().toISOString().split('T')[0],
134
+ budgetUsed: result.budgetUsed,
135
+ };
136
+
137
+ const code = adapter.generateCode(result.params, meta);
138
+
139
+ if (out) {
140
+ const outPath = path.resolve(out);
141
+ const dir = path.dirname(outPath);
142
+ if (!fs.existsSync(dir)) {
143
+ fs.mkdirSync(dir, { recursive: true });
144
+ }
145
+ fs.writeFileSync(outPath, code, 'utf8');
146
+ console.log(`\nWrote: ${outPath}`);
147
+ } else {
148
+ console.log(`\n${'─'.repeat(60)}`);
149
+ console.log('Generated code:');
150
+ console.log('─'.repeat(60));
151
+ console.log(code);
152
+ }
153
+ }
154
+
155
+ main().catch(err => {
156
+ console.error(err);
157
+ process.exit(1);
158
+ });