Skip to main content

depyler_core/rust_gen/
numpy_gen.rs

1//! NumPy to Trueno codegen mapping.
2//!
3//! Maps NumPy API calls to trueno (SIMD-accelerated tensor library).
4//!
5//! # Mappings
6//!
7//! | NumPy | trueno |
8//! |-------|--------|
9//! | `np.array([1.0, 2.0])` | `Vector::from_slice(&[1.0f32, 2.0])` |
10//! | `np.dot(a, b)` | `a.dot(&b)?` |
11//! | `np.sum(a)` | `a.sum()?` |
12//! | `np.mean(a)` | `a.mean()?` |
13//! | `np.sqrt(a)` | `a.sqrt()?` |
14//!
15//! Created: 2025-11-27 (Phase 3 - NumPy→Trueno)
16
17use proc_macro2::TokenStream;
18use quote::quote;
19
20/// Represents a recognized numpy function call
21#[derive(Debug, Clone)]
22pub enum NumpyCall {
23    /// np.array([...]) - create vector from list
24    Array { elements: Vec<TokenStream> },
25    /// np.dot(a, b) - dot product
26    Dot { a: TokenStream, b: TokenStream },
27    /// np.sum(a) - sum all elements
28    Sum { arr: TokenStream },
29    /// np.mean(a) - mean of all elements
30    Mean { arr: TokenStream },
31    /// np.sqrt(a) - element-wise sqrt
32    Sqrt { arr: TokenStream },
33    /// np.abs(a) - element-wise abs
34    Abs { arr: TokenStream },
35    /// np.min(a) - minimum element
36    Min { arr: TokenStream },
37    /// np.max(a) - maximum element
38    Max { arr: TokenStream },
39    /// np.exp(a) - element-wise exp
40    Exp { arr: TokenStream },
41    /// np.log(a) - element-wise log
42    Log { arr: TokenStream },
43    /// np.sin(a) - element-wise sin
44    Sin { arr: TokenStream },
45    /// np.cos(a) - element-wise cos
46    Cos { arr: TokenStream },
47    /// np.clip(a, min, max) - clip values
48    Clip {
49        arr: TokenStream,
50        min: TokenStream,
51        max: TokenStream,
52    },
53    /// np.argmax(a) - index of max element
54    ArgMax { arr: TokenStream },
55    /// np.argmin(a) - index of min element
56    ArgMin { arr: TokenStream },
57    /// np.std(a) - standard deviation
58    Std { arr: TokenStream },
59    /// np.var(a) - variance
60    Var { arr: TokenStream },
61    /// np.zeros(n) - array of zeros
62    Zeros { size: TokenStream },
63    /// np.ones(n) - array of ones
64    Ones { size: TokenStream },
65    /// np.norm(a) or np.linalg.norm(a) - L2 norm
66    Norm { arr: TokenStream },
67}
68
69/// Generate trueno code for a numpy call.
70///
71/// # Returns
72///
73/// TokenStream containing the trueno equivalent code.
74pub fn generate_trueno_code(call: &NumpyCall) -> TokenStream {
75    match call {
76        NumpyCall::Array { elements } => {
77            quote! {
78                Vector::from_slice(&[#(#elements as f32),*])
79            }
80        }
81        NumpyCall::Dot { a, b } => {
82            quote! {
83                #a.dot(&#b).expect("numpy operation failed")
84            }
85        }
86        NumpyCall::Sum { arr } => {
87            quote! {
88                #arr.sum().expect("numpy operation failed")
89            }
90        }
91        NumpyCall::Mean { arr } => {
92            quote! {
93                #arr.mean().expect("numpy operation failed")
94            }
95        }
96        NumpyCall::Sqrt { arr } => {
97            quote! {
98                #arr.sqrt().expect("numpy operation failed")
99            }
100        }
101        NumpyCall::Abs { arr } => {
102            quote! {
103                #arr.abs().expect("numpy operation failed")
104            }
105        }
106        NumpyCall::Min { arr } => {
107            quote! {
108                #arr.min().expect("numpy operation failed")
109            }
110        }
111        NumpyCall::Max { arr } => {
112            quote! {
113                #arr.max().expect("numpy operation failed")
114            }
115        }
116        NumpyCall::Exp { arr } => {
117            quote! {
118                #arr.exp().expect("numpy operation failed")
119            }
120        }
121        NumpyCall::Log { arr } => {
122            quote! {
123                #arr.ln().expect("numpy operation failed")
124            }
125        }
126        NumpyCall::Sin { arr } => {
127            quote! {
128                #arr.sin().expect("numpy operation failed")
129            }
130        }
131        NumpyCall::Cos { arr } => {
132            quote! {
133                #arr.cos().expect("numpy operation failed")
134            }
135        }
136        NumpyCall::Clip { arr, min, max } => {
137            quote! {
138                #arr.clamp(#min, #max).expect("numpy operation failed")
139            }
140        }
141        NumpyCall::ArgMax { arr } => {
142            quote! {
143                #arr.argmax().expect("numpy operation failed")
144            }
145        }
146        NumpyCall::ArgMin { arr } => {
147            quote! {
148                #arr.argmin().expect("numpy operation failed")
149            }
150        }
151        NumpyCall::Std { arr } => {
152            // trueno uses stddev(), not std()
153            quote! {
154                #arr.stddev().expect("numpy operation failed")
155            }
156        }
157        NumpyCall::Var { arr } => {
158            quote! {
159                #arr.variance().expect("numpy operation failed")
160            }
161        }
162        NumpyCall::Zeros { size } => {
163            quote! {
164                Vector::zeros(#size)
165            }
166        }
167        NumpyCall::Ones { size } => {
168            quote! {
169                Vector::ones(#size)
170            }
171        }
172        NumpyCall::Norm { arr } => {
173            // DEPYLER-0583: trueno uses norm_l2() for L2 (Euclidean) norm
174            // which is np.linalg.norm() default behavior
175            // DEPYLER-0667: Wrap arg in parens for correct precedence
176            quote! {
177                (#arr).norm_l2().expect("numpy operation failed")
178            }
179        }
180    }
181}
182
183/// Check if a module name is numpy or np alias.
184pub fn is_numpy_module(module: &str) -> bool {
185    module == "numpy" || module == "np"
186}
187
188/// Parse a numpy function name and return the corresponding NumpyCall variant name.
189///
190/// Returns None if not a recognized numpy function.
191pub fn parse_numpy_function(func_name: &str) -> Option<&'static str> {
192    match func_name {
193        "array" => Some("Array"),
194        "dot" => Some("Dot"),
195        "sum" => Some("Sum"),
196        "mean" => Some("Mean"),
197        "sqrt" => Some("Sqrt"),
198        "abs" => Some("Abs"),
199        "min" | "amin" => Some("Min"),
200        "max" | "amax" => Some("Max"),
201        "exp" => Some("Exp"),
202        "log" => Some("Log"),
203        "sin" => Some("Sin"),
204        "cos" => Some("Cos"),
205        "clip" => Some("Clip"),
206        "argmax" => Some("ArgMax"),
207        "argmin" => Some("ArgMin"),
208        "std" => Some("Std"),
209        "var" => Some("Var"),
210        "zeros" => Some("Zeros"),
211        "ones" => Some("Ones"),
212        "norm" => Some("Norm"),
213        _ => None,
214    }
215}
216
217/// Get the trueno use statement needed for numpy code.
218pub fn trueno_use_statement() -> TokenStream {
219    quote! {
220        use trueno::Vector;
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_generate_array() {
230        let elements = vec![quote!(1.0), quote!(2.0), quote!(3.0)];
231        let call = NumpyCall::Array { elements };
232        let code = generate_trueno_code(&call);
233        let code_str = code.to_string();
234
235        assert!(
236            code_str.contains("Vector :: from_slice"),
237            "Should generate Vector::from_slice: {}",
238            code_str
239        );
240        assert!(
241            code_str.contains("1.0") && code_str.contains("2.0") && code_str.contains("3.0"),
242            "Should contain elements: {}",
243            code_str
244        );
245    }
246
247    #[test]
248    fn test_generate_dot() {
249        let call = NumpyCall::Dot {
250            a: quote!(a),
251            b: quote!(b),
252        };
253        let code = generate_trueno_code(&call);
254        let code_str = code.to_string();
255
256        assert!(
257            code_str.contains("dot"),
258            "Should generate dot call: {}",
259            code_str
260        );
261        assert!(
262            code_str.contains("unwrap") || code_str.contains("expect"),
263            "Should handle result: {}",
264            code_str
265        );
266    }
267
268    #[test]
269    fn test_generate_sum() {
270        let call = NumpyCall::Sum { arr: quote!(arr) };
271        let code = generate_trueno_code(&call);
272        let code_str = code.to_string();
273
274        assert!(
275            code_str.contains("sum"),
276            "Should generate sum call: {}",
277            code_str
278        );
279    }
280
281    #[test]
282    fn test_generate_mean() {
283        let call = NumpyCall::Mean { arr: quote!(arr) };
284        let code = generate_trueno_code(&call);
285        let code_str = code.to_string();
286
287        assert!(
288            code_str.contains("mean"),
289            "Should generate mean call: {}",
290            code_str
291        );
292    }
293
294    #[test]
295    fn test_generate_sqrt() {
296        let call = NumpyCall::Sqrt { arr: quote!(arr) };
297        let code = generate_trueno_code(&call);
298        let code_str = code.to_string();
299
300        assert!(
301            code_str.contains("sqrt"),
302            "Should generate sqrt call: {}",
303            code_str
304        );
305    }
306
307    #[test]
308    fn test_generate_zeros() {
309        let call = NumpyCall::Zeros { size: quote!(10) };
310        let code = generate_trueno_code(&call);
311        let code_str = code.to_string();
312
313        assert!(
314            code_str.contains("zeros"),
315            "Should generate zeros call: {}",
316            code_str
317        );
318    }
319
320    #[test]
321    fn test_generate_ones() {
322        let call = NumpyCall::Ones { size: quote!(10) };
323        let code = generate_trueno_code(&call);
324        let code_str = code.to_string();
325
326        assert!(
327            code_str.contains("ones"),
328            "Should generate ones call: {}",
329            code_str
330        );
331    }
332
333    #[test]
334    fn test_generate_clip() {
335        let call = NumpyCall::Clip {
336            arr: quote!(arr),
337            min: quote!(0.0),
338            max: quote!(1.0),
339        };
340        let code = generate_trueno_code(&call);
341        let code_str = code.to_string();
342
343        assert!(
344            code_str.contains("clamp"),
345            "Should generate clamp call: {}",
346            code_str
347        );
348    }
349
350    #[test]
351    fn test_is_numpy_module() {
352        assert!(is_numpy_module("numpy"));
353        assert!(is_numpy_module("np"));
354        assert!(!is_numpy_module("math"));
355        assert!(!is_numpy_module("random"));
356    }
357
358    #[test]
359    fn test_parse_numpy_function() {
360        assert_eq!(parse_numpy_function("array"), Some("Array"));
361        assert_eq!(parse_numpy_function("dot"), Some("Dot"));
362        assert_eq!(parse_numpy_function("sum"), Some("Sum"));
363        assert_eq!(parse_numpy_function("mean"), Some("Mean"));
364        assert_eq!(parse_numpy_function("sqrt"), Some("Sqrt"));
365        assert_eq!(parse_numpy_function("min"), Some("Min"));
366        assert_eq!(parse_numpy_function("amin"), Some("Min"));
367        assert_eq!(parse_numpy_function("max"), Some("Max"));
368        assert_eq!(parse_numpy_function("amax"), Some("Max"));
369        assert_eq!(parse_numpy_function("unknown"), None);
370    }
371
372    #[test]
373    fn test_trueno_use_statement() {
374        let stmt = trueno_use_statement();
375        let stmt_str = stmt.to_string();
376
377        assert!(
378            stmt_str.contains("trueno"),
379            "Should use trueno: {}",
380            stmt_str
381        );
382        assert!(
383            stmt_str.contains("Vector"),
384            "Should import Vector: {}",
385            stmt_str
386        );
387    }
388
389    #[test]
390    fn test_generate_norm() {
391        let call = NumpyCall::Norm { arr: quote!(v) };
392        let code = generate_trueno_code(&call);
393        let code_str = code.to_string();
394
395        assert!(
396            code_str.contains("norm"),
397            "Should generate norm call: {}",
398            code_str
399        );
400    }
401
402    #[test]
403    fn test_generate_argmax() {
404        let call = NumpyCall::ArgMax { arr: quote!(arr) };
405        let code = generate_trueno_code(&call);
406        let code_str = code.to_string();
407
408        assert!(
409            code_str.contains("argmax"),
410            "Should generate argmax call: {}",
411            code_str
412        );
413    }
414
415    #[test]
416    fn test_generate_std() {
417        let call = NumpyCall::Std { arr: quote!(arr) };
418        let code = generate_trueno_code(&call);
419        let code_str = code.to_string();
420
421        // trueno uses stddev(), not std()
422        assert!(
423            code_str.contains("stddev"),
424            "Should generate stddev call (trueno API): {}",
425            code_str
426        );
427    }
428
429    #[test]
430    fn test_generate_var() {
431        let call = NumpyCall::Var { arr: quote!(arr) };
432        let code = generate_trueno_code(&call);
433        let code_str = code.to_string();
434
435        assert!(
436            code_str.contains("variance"),
437            "Should generate variance call: {}",
438            code_str
439        );
440    }
441
442    #[test]
443    fn test_generate_abs() {
444        let call = NumpyCall::Abs { arr: quote!(arr) };
445        let code = generate_trueno_code(&call);
446        let code_str = code.to_string();
447        assert!(code_str.contains("abs"));
448        assert!(code_str.contains("expect"));
449    }
450
451    #[test]
452    fn test_generate_min() {
453        let call = NumpyCall::Min { arr: quote!(arr) };
454        let code = generate_trueno_code(&call);
455        assert!(code.to_string().contains("min"));
456    }
457
458    #[test]
459    fn test_generate_max() {
460        let call = NumpyCall::Max { arr: quote!(arr) };
461        let code = generate_trueno_code(&call);
462        assert!(code.to_string().contains("max"));
463    }
464
465    #[test]
466    fn test_generate_exp() {
467        let call = NumpyCall::Exp { arr: quote!(arr) };
468        let code = generate_trueno_code(&call);
469        assert!(code.to_string().contains("exp"));
470    }
471
472    #[test]
473    fn test_generate_log() {
474        let call = NumpyCall::Log { arr: quote!(arr) };
475        let code = generate_trueno_code(&call);
476        assert!(code.to_string().contains("ln"));
477    }
478
479    #[test]
480    fn test_generate_sin() {
481        let call = NumpyCall::Sin { arr: quote!(arr) };
482        let code = generate_trueno_code(&call);
483        assert!(code.to_string().contains("sin"));
484    }
485
486    #[test]
487    fn test_generate_cos() {
488        let call = NumpyCall::Cos { arr: quote!(arr) };
489        let code = generate_trueno_code(&call);
490        assert!(code.to_string().contains("cos"));
491    }
492
493    #[test]
494    fn test_generate_argmin() {
495        let call = NumpyCall::ArgMin { arr: quote!(arr) };
496        let code = generate_trueno_code(&call);
497        assert!(code.to_string().contains("argmin"));
498    }
499
500    #[test]
501    fn test_parse_numpy_function_exhaustive() {
502        assert_eq!(parse_numpy_function("abs"), Some("Abs"));
503        assert_eq!(parse_numpy_function("exp"), Some("Exp"));
504        assert_eq!(parse_numpy_function("log"), Some("Log"));
505        assert_eq!(parse_numpy_function("sin"), Some("Sin"));
506        assert_eq!(parse_numpy_function("cos"), Some("Cos"));
507        assert_eq!(parse_numpy_function("clip"), Some("Clip"));
508        assert_eq!(parse_numpy_function("argmax"), Some("ArgMax"));
509        assert_eq!(parse_numpy_function("argmin"), Some("ArgMin"));
510        assert_eq!(parse_numpy_function("std"), Some("Std"));
511        assert_eq!(parse_numpy_function("var"), Some("Var"));
512        assert_eq!(parse_numpy_function("zeros"), Some("Zeros"));
513        assert_eq!(parse_numpy_function("ones"), Some("Ones"));
514        assert_eq!(parse_numpy_function("norm"), Some("Norm"));
515    }
516
517    #[test]
518    fn test_is_numpy_module_edge_cases() {
519        assert!(!is_numpy_module(""));
520        assert!(!is_numpy_module("Numpy"));
521        assert!(!is_numpy_module("NP"));
522        assert!(!is_numpy_module("scipy"));
523    }
524
525    #[test]
526    fn test_generate_norm_uses_norm_l2() {
527        let call = NumpyCall::Norm {
528            arr: quote!(a + b),
529        };
530        let code = generate_trueno_code(&call);
531        assert!(code.to_string().contains("norm_l2"));
532    }
533
534    #[test]
535    fn test_generate_array_empty() {
536        let call = NumpyCall::Array { elements: vec![] };
537        let code = generate_trueno_code(&call);
538        assert!(code.to_string().contains("Vector :: from_slice"));
539    }
540
541    #[test]
542    fn test_generate_dot_with_expressions() {
543        let call = NumpyCall::Dot {
544            a: quote!(vec1),
545            b: quote!(vec2),
546        };
547        let code = generate_trueno_code(&call);
548        let s = code.to_string();
549        assert!(s.contains("vec1"));
550        assert!(s.contains("vec2"));
551    }
552
553    #[test]
554    fn test_numpy_call_debug() {
555        let call = NumpyCall::Sum { arr: quote!(x) };
556        let debug = format!("{:?}", call);
557        assert!(debug.contains("Sum"));
558    }
559
560    #[test]
561    fn test_numpy_call_clone() {
562        let call = NumpyCall::Zeros { size: quote!(10) };
563        let cloned = call.clone();
564        let code1 = generate_trueno_code(&call).to_string();
565        let code2 = generate_trueno_code(&cloned).to_string();
566        assert_eq!(code1, code2);
567    }
568}