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,67 @@
|
|
|
1
|
+
// random.mcrs — Pseudo-random number generators for RedScript datapacks.
|
|
2
|
+
//
|
|
3
|
+
// LCG (Linear Congruential Generator):
|
|
4
|
+
// Simple, fast, sufficient for most game use-cases.
|
|
5
|
+
// Parameters: a=1664525, c=1013904223 (Numerical Recipes)
|
|
6
|
+
// Modulus: 2^32 via int32 overflow (free in MC scoreboard arithmetic).
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// let seed: int = 12345; // any non-zero starting value
|
|
10
|
+
// seed = next_lcg(seed); // advance state
|
|
11
|
+
// let roll: int = random_range(seed, 0, 100);
|
|
12
|
+
|
|
13
|
+
module library;
|
|
14
|
+
|
|
15
|
+
// next_lcg(seed): advance LCG state, return next pseudo-random int32.
|
|
16
|
+
// The returned value is also the new seed for the next call.
|
|
17
|
+
fn next_lcg(seed: int): int {
|
|
18
|
+
return seed * 1664525 + 1013904223;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// random_range(seed, lo, hi): return integer in [lo, hi).
|
|
22
|
+
// seed should already be the output of next_lcg.
|
|
23
|
+
fn random_range(seed: int, lo: int, hi: int): int {
|
|
24
|
+
let r: int = seed;
|
|
25
|
+
if (r < 0) { r = 0 - r; }
|
|
26
|
+
return lo + r % (hi - lo);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// random_bool(seed): return 0 or 1 with equal probability.
|
|
30
|
+
fn random_bool(seed: int): int {
|
|
31
|
+
let r: int = seed;
|
|
32
|
+
if (r < 0) { r = 0 - r; }
|
|
33
|
+
return r % 2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── PCG (Permuted Congruential Generator) ───────────────────────────────────
|
|
37
|
+
// Better statistical quality than LCG — passes more randomness tests.
|
|
38
|
+
// Requires stdlib/bits for bit_xor, bit_shr.
|
|
39
|
+
//
|
|
40
|
+
// PCG-XSH-RR (32-bit output from 64-bit state is approximated here with two
|
|
41
|
+
// int32 words; this is a simplified PCG32 variant that fits MC scoreboard).
|
|
42
|
+
//
|
|
43
|
+
// Reference: https://www.pcg-random.org/
|
|
44
|
+
//
|
|
45
|
+
// Usage: maintain two state variables (hi, lo); update both each call.
|
|
46
|
+
|
|
47
|
+
// pcg_next_lo(state_lo, state_hi): advance PCG state (low word).
|
|
48
|
+
// Returns new low word. Use with pcg_next_hi.
|
|
49
|
+
fn pcg_next_lo(state_lo: int): int {
|
|
50
|
+
return state_lo * 747796405 + 2891336453;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// pcg_next_hi(state_lo, state_hi): advance PCG state (high word).
|
|
54
|
+
fn pcg_next_hi(state_hi: int, state_lo: int): int {
|
|
55
|
+
return state_hi * 747796405 + state_lo;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// pcg_output(state_lo): extract output value from PCG low word.
|
|
59
|
+
// Applies XSH-RR output permutation (simplified to XOR-shift).
|
|
60
|
+
fn pcg_output(state_lo: int): int {
|
|
61
|
+
let xsh: int = state_lo;
|
|
62
|
+
// xsh = ((state >> 18) ^ state) >> 27 (simplified: use single xor-shift)
|
|
63
|
+
let shifted: int = xsh / 262144; // >> 18
|
|
64
|
+
let xored: int = xsh / 32 + shifted; // approximation of XSH
|
|
65
|
+
if (xored < 0) { xored = 0 - xored; }
|
|
66
|
+
return xored;
|
|
67
|
+
}
|
|
@@ -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
|
+
}
|
package/src/stdlib/timer.mcrs
CHANGED
|
@@ -75,3 +75,35 @@ impl Timer {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
// ─── Tick-based stopwatch ─────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
// tick_to_seconds(ticks): convert MC ticks to seconds (20 ticks = 1 second)
|
|
82
|
+
fn tick_to_seconds(ticks: int): int {
|
|
83
|
+
return ticks / 20;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// tick_to_ms(ticks): convert ticks to milliseconds
|
|
87
|
+
fn tick_to_ms(ticks: int): int {
|
|
88
|
+
return ticks * 50;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// seconds_to_ticks(s): convert seconds to ticks
|
|
92
|
+
fn seconds_to_ticks(s: int): int {
|
|
93
|
+
return s * 20;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// format_time_s(ticks): ticks → seconds component (mod 60)
|
|
97
|
+
fn format_time_s(ticks: int): int {
|
|
98
|
+
return (ticks / 20) % 60;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// format_time_m(ticks): ticks → minutes component
|
|
102
|
+
fn format_time_m(ticks: int): int {
|
|
103
|
+
return (ticks / 1200) % 60;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// format_time_h(ticks): ticks → hours component
|
|
107
|
+
fn format_time_h(ticks: int): int {
|
|
108
|
+
return ticks / 72000;
|
|
109
|
+
}
|
package/src/stdlib/vec.mcrs
CHANGED
|
@@ -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
|
+
};
|
package/src/tuner/cli.ts
ADDED
|
@@ -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
|
+
});
|