1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//! Integration tests for numerical optimization rewrites in code generation.
//!
//! These tests verify that the codegen pipeline detects patterns like
//! `exp(x) - 1`, `ln(1 + x)`, `ln(x)/ln(2)`, and `2^x` and emits
//! numerically precise Rust intrinsics (`exp_m1`, `ln_1p`, `log2`, `exp2`).
use symplex::matrix::{CodegenOptions, MathBackend};
use symplex::prelude::*;
// ═══════════════════════════════════════════════════════════════════════════
// Pattern 1: exp(x) - 1 → exp_m1
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn codegen_expm1_pattern() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.exp() - 1;
let code = f.to_rust_fn("expm1_fn", &["x"]).unwrap();
assert!(
code.contains(".exp_m1()"),
"expected .exp_m1() for exp(x) - 1, got:\n{code}"
);
assert!(
!code.contains(".exp()"),
"should NOT contain bare .exp() when exp_m1 is used, got:\n{code}"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Pattern 2: ln(1 + x) → ln_1p
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn codegen_log1p_pattern() {
let ctx = Context::new();
let x = ctx.symbol("x");
let one = ctx.int(1);
let f = (&x + &one).ln();
let code = f.to_rust_fn("log1p_fn", &["x"]).unwrap();
assert!(
code.contains(".ln_1p()"),
"expected .ln_1p() for ln(1 + x), got:\n{code}"
);
assert!(
!code.contains(".ln()"),
"should NOT contain bare .ln() when ln_1p is used, got:\n{code}"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Pattern 3: ln(x) / ln(2) → log2
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn codegen_log2_pattern() {
let ctx = Context::new();
let x = ctx.symbol("x");
let two = ctx.int(2);
// x.log(&two) computes ln(x) / ln(2)
let f = x.log(&two);
let code = f.to_rust_fn("log2_fn", &["x"]).unwrap();
assert!(
code.contains(".log2()"),
"expected .log2() for ln(x)/ln(2), got:\n{code}"
);
assert!(
!code.contains(".ln()"),
"should NOT contain bare .ln() when log2 is used, got:\n{code}"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Pattern 4: 2^x → exp2
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn codegen_exp2_pattern() {
let ctx = Context::new();
let x = ctx.symbol("x");
let two = ctx.int(2);
let f = two.pow(&x);
let code = f.to_rust_fn("exp2_fn", &["x"]).unwrap();
assert!(
code.contains(".exp2()"),
"expected .exp2() for 2^x, got:\n{code}"
);
assert!(
!code.contains(".powf(") && !code.contains(".powi("),
"should NOT contain powf/powi when exp2 is used, got:\n{code}"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Negative tests — must NOT trigger false positives
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn codegen_no_false_positive_exp() {
// exp(x) - 2 should NOT use exp_m1
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.exp() - 2;
let code = f.to_rust_fn("no_expm1", &["x"]).unwrap();
assert!(
!code.contains("exp_m1"),
"exp(x) - 2 should NOT use exp_m1, got:\n{code}"
);
}
#[test]
fn codegen_no_false_positive_ln() {
// ln(2 + x) should NOT use ln_1p
let ctx = Context::new();
let x = ctx.symbol("x");
let two = ctx.int(2);
let f = (&x + &two).ln();
let code = f.to_rust_fn("no_log1p", &["x"]).unwrap();
assert!(
!code.contains("ln_1p"),
"ln(2 + x) should NOT use ln_1p, got:\n{code}"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Subexpression inside a larger expression
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn codegen_numopt_in_larger_expr() {
// sin(x) + (exp(x) - 1) should use exp_m1 for the subexpression
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.sin() + x.exp() - 1;
let code = f.to_rust_fn("mixed_fn", &["x"]).unwrap();
assert!(
code.contains("exp_m1"),
"expected exp_m1 for the exp(x)-1 subexpression in sin(x)+exp(x)-1, got:\n{code}"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Backend: Libm
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn codegen_libm_backend_expm1() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.exp() - 1;
let opts = CodegenOptions {
math_backend: MathBackend::Libm,
..Default::default()
};
let code = f
.to_rust_fn_with_options("libm_expm1", &["x"], &opts)
.unwrap();
assert!(
code.contains("libm::expm1("),
"expected libm::expm1 with Libm backend, got:\n{code}"
);
}