Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/backend/transpiler/dataframe.rs
Line
Count
Source
1
//! `DataFrame` transpilation for Polars integration
2
3
#![allow(clippy::missing_errors_doc)]
4
#![allow(clippy::wildcard_imports)]
5
#![allow(clippy::doc_markdown)]
6
7
use super::*;
8
use crate::frontend::ast::{AggregateOp, DataFrameColumn, DataFrameOp, JoinType};
9
use anyhow::Result;
10
use proc_macro2::TokenStream;
11
use quote::{format_ident, quote};
12
13
impl Transpiler {
14
    /// Transpiles DataFrame literals (df![] syntax)
15
0
    pub fn transpile_dataframe(&self, columns: &[DataFrameColumn]) -> Result<TokenStream> {
16
0
        if columns.is_empty() {
17
            // Empty DataFrame
18
0
            return Ok(quote! {
19
0
                polars::prelude::DataFrame::empty()
20
0
            });
21
0
        }
22
23
0
        let mut series_tokens = Vec::new();
24
25
0
        for column in columns {
26
0
            let col_name = &column.name;
27
28
            // Transpile the column values
29
0
            let values_tokens = if column.values.is_empty() {
30
0
                quote! { vec![] }
31
            } else {
32
                // Collect all values into a vector
33
0
                let value_tokens: Result<Vec<_>> = column
34
0
                    .values
35
0
                    .iter()
36
0
                    .map(|v| self.transpile_expr(v))
37
0
                    .collect();
38
0
                let value_tokens = value_tokens?;
39
0
                quote! { vec![#(#value_tokens),*] }
40
            };
41
42
            // Create a Series from the values
43
0
            series_tokens.push(quote! {
44
                polars::prelude::Series::new(#col_name, #values_tokens)
45
            });
46
        }
47
48
        // Create DataFrame from series
49
0
        Ok(quote! {
50
            polars::prelude::DataFrame::new(vec![
51
                #(#series_tokens),*
52
            ]).unwrap()
53
        })
54
0
    }
55
56
    /// Transpiles DataFrame operations
57
0
    pub fn transpile_dataframe_operation(
58
0
        &self,
59
0
        df: &Expr,
60
0
        op: &DataFrameOp,
61
0
    ) -> Result<TokenStream> {
62
0
        let df_tokens = self.transpile_expr(df)?;
63
64
0
        match op {
65
0
            DataFrameOp::Select(columns) => {
66
0
                let col_tokens: Vec<TokenStream> =
67
0
                    columns.iter().map(|col| quote! { #col }).collect();
68
0
                Ok(quote! {
69
                    #df_tokens.select(&[#(#col_tokens),*]).unwrap()
70
                })
71
            }
72
0
            DataFrameOp::Filter(condition) => {
73
0
                let cond_tokens = self.transpile_expr(condition)?;
74
0
                Ok(quote! {
75
0
                    #df_tokens.filter(&#cond_tokens).unwrap()
76
0
                })
77
            }
78
0
            DataFrameOp::GroupBy(columns) => {
79
0
                let col_tokens: Vec<TokenStream> =
80
0
                    columns.iter().map(|col| quote! { #col }).collect();
81
0
                Ok(quote! {
82
                    #df_tokens.groupby(&[#(#col_tokens),*]).unwrap()
83
                })
84
            }
85
0
            DataFrameOp::Sort(columns) => {
86
                // Sort by multiple columns
87
0
                let col_tokens: Vec<TokenStream> =
88
0
                    columns.iter().map(|col| quote! { #col }).collect();
89
0
                Ok(quote! {
90
                    #df_tokens.sort(&[#(#col_tokens),*], false).unwrap()
91
                })
92
            }
93
0
            DataFrameOp::Join { other, on, how } => {
94
0
                let other_tokens = self.transpile_expr(other)?;
95
0
                let on_tokens: Vec<TokenStream> = on.iter().map(|col| quote! { #col }).collect();
96
97
0
                let join_type = match how {
98
0
                    JoinType::Left => quote! { polars::prelude::JoinType::Left },
99
0
                    JoinType::Right => quote! { polars::prelude::JoinType::Right },
100
0
                    JoinType::Inner => quote! { polars::prelude::JoinType::Inner },
101
0
                    JoinType::Outer => quote! { polars::prelude::JoinType::Outer },
102
                };
103
104
0
                Ok(quote! {
105
                    #df_tokens.join(
106
                        &#other_tokens,
107
                        &[#(#on_tokens),*],
108
                        &[#(#on_tokens),*],
109
                        #join_type
110
                    ).unwrap()
111
                })
112
            }
113
0
            DataFrameOp::Aggregate(agg_ops) => {
114
                // Convert AggregateOp to expressions
115
0
                let agg_exprs: Vec<TokenStream> = agg_ops
116
0
                    .iter()
117
0
                    .map(|op| match op {
118
0
                        AggregateOp::Sum(col) => quote! { col(#col).sum() },
119
0
                        AggregateOp::Mean(col) => quote! { col(#col).mean() },
120
0
                        AggregateOp::Min(col) => quote! { col(#col).min() },
121
0
                        AggregateOp::Max(col) => quote! { col(#col).max() },
122
0
                        AggregateOp::Count(col) => quote! { col(#col).count() },
123
0
                        AggregateOp::Std(col) => quote! { col(#col).std() },
124
0
                        AggregateOp::Var(col) => quote! { col(#col).var() },
125
0
                    })
126
0
                    .collect();
127
128
0
                Ok(quote! {
129
                    #df_tokens.agg(&[#(#agg_exprs),*]).unwrap()
130
                })
131
            }
132
0
            DataFrameOp::Limit(n) => Ok(quote! {
133
0
                #df_tokens.limit(#n)
134
0
            }),
135
0
            DataFrameOp::Head(n) => Ok(quote! {
136
0
                #df_tokens.head(Some(#n))
137
0
            }),
138
0
            DataFrameOp::Tail(n) => Ok(quote! {
139
0
                #df_tokens.tail(Some(#n))
140
0
            }),
141
        }
142
0
    }
143
144
    /// Transpiles DataFrame method calls (alternative to operation enum)
145
0
    pub fn transpile_dataframe_method(
146
0
        &self,
147
0
        df_expr: &Expr,
148
0
        method: &str,
149
0
        args: &[Expr],
150
0
    ) -> Result<TokenStream> {
151
0
        let df_tokens = self.transpile_expr(df_expr)?;
152
153
0
        let arg_tokens: Result<Vec<_>> = args.iter().map(|a| self.transpile_expr(a)).collect();
154
0
        let arg_tokens = arg_tokens?;
155
156
        // Map Ruchy DataFrame methods to correct Polars API
157
0
        match method {
158
            // DataFrame builder pattern methods (don't exist in Polars)
159
0
            "column" => {
160
                // .column() doesn't exist - this is part of builder pattern
161
                // For now, just pass through to show the issue
162
0
                if arg_tokens.len() == 2 {
163
0
                    let name = &arg_tokens[0];
164
0
                    let data = &arg_tokens[1];
165
0
                    Ok(quote! { #df_tokens.column(#name, #data) })
166
                } else {
167
0
                    Ok(quote! { #df_tokens.column(#(#arg_tokens),*) })
168
                }
169
            }
170
0
            "build" => {
171
                // .build() doesn't exist in Polars - just return the DataFrame
172
0
                Ok(quote! { #df_tokens })
173
            }
174
            
175
            // DataFrame inspection methods
176
0
            "rows" => {
177
                // Polars uses .height() not .rows()
178
0
                Ok(quote! { #df_tokens.height() })
179
            }
180
0
            "columns" => {
181
                // Polars uses .get_column_names() for column names
182
0
                Ok(quote! { #df_tokens.get_column_names() })
183
            }
184
0
            "get" => {
185
                // df.get("column") -> df.column("column") in Polars
186
0
                if arg_tokens.len() == 1 {
187
0
                    let col_name = &arg_tokens[0];
188
0
                    Ok(quote! { #df_tokens.column(#col_name) })
189
                } else {
190
0
                    Ok(quote! { #df_tokens.get(#(#arg_tokens),*) })
191
                }
192
            }
193
            
194
            // DataFrame operations
195
0
            "select" | "filter" | "sort" => {
196
                // These need lazy evaluation for proper chaining
197
0
                let method_ident = format_ident!("{}", method);
198
0
                Ok(quote! {
199
                    #df_tokens.lazy().#method_ident(#(#arg_tokens),*).collect().unwrap()
200
                })
201
            }
202
0
            "groupby" | "group_by" => {
203
                // Polars uses group_by
204
0
                Ok(quote! {
205
                    #df_tokens.group_by(#(#arg_tokens),*).unwrap()
206
                })
207
            }
208
0
            "agg" | "join" => {
209
0
                let method_ident = format_ident!("{}", method);
210
0
                Ok(quote! {
211
                    #df_tokens.#method_ident(#(#arg_tokens),*).unwrap()
212
                })
213
            }
214
            
215
            // Statistical methods
216
0
            "mean" | "std" | "min" | "max" | "sum" | "count" => {
217
                // These are aggregate functions
218
0
                let method_ident = format_ident!("{}", method);
219
0
                Ok(quote! {
220
0
                    #df_tokens.#method_ident()
221
0
                })
222
            }
223
            
224
            // Head/tail methods
225
0
            "head" | "tail" => {
226
0
                let method_ident = format_ident!("{}", method);
227
0
                if args.is_empty() {
228
0
                    Ok(quote! { #df_tokens.#method_ident(Some(5)) })
229
                } else {
230
0
                    Ok(quote! { #df_tokens.#method_ident(Some(#(#arg_tokens),*)) })
231
                }
232
            }
233
            _ => {
234
                // Default method call
235
0
                let method_ident = format_ident!("{}", method);
236
0
                Ok(quote! {
237
                    #df_tokens.#method_ident(#(#arg_tokens),*)
238
                })
239
            }
240
        }
241
0
    }
242
    
243
    /// Check if an expression is a DataFrame type
244
0
    pub fn is_dataframe_expr(&self, expr: &Expr) -> bool {
245
        use crate::frontend::ast::ExprKind;
246
        
247
0
        match &expr.kind {
248
            // Variable named "df" is likely a DataFrame
249
0
            ExprKind::Identifier(name) if name == "df" => true,
250
            
251
            // DataFrame constructor calls
252
0
            ExprKind::Call { func, .. } => {
253
0
                if let ExprKind::QualifiedName { module, name } = &func.kind {
254
0
                    module == "DataFrame" && (name == "new" || name == "from_csv" || name == "from_json" || name == "from_csv_string")
255
                } else {
256
0
                    false
257
                }
258
            }
259
            
260
            // Method calls that return DataFrames
261
0
            ExprKind::MethodCall { receiver, method, .. } => {
262
                // Check if it's a DataFrame method chain
263
0
                matches!(method.as_str(), 
264
0
                    "column" | "build" | "select" | "filter" | "sort" | 
265
0
                    "head" | "tail" | "drop_nulls" | "fill_null"
266
0
                ) || self.is_dataframe_expr(receiver)
267
            }
268
            
269
            // DataFrame literals
270
0
            ExprKind::DataFrame { .. } => true,
271
            
272
0
            _ => false,
273
        }
274
0
    }
275
}
276
277
#[cfg(test)]
278
mod tests {
279
    use super::*;
280
    use crate::frontend::ast::{Expr, ExprKind, Literal, Span};
281
    
282
    fn make_test_transpiler() -> Transpiler {
283
        Transpiler::new()
284
    }
285
    
286
    fn make_literal_expr(val: i64) -> Expr {
287
        Expr {
288
            kind: ExprKind::Literal(Literal::Integer(val)),
289
            span: Span::new(0, 10),
290
            attributes: vec![],
291
        }
292
    }
293
    
294
    #[test]
295
    fn test_empty_dataframe() {
296
        let transpiler = make_test_transpiler();
297
        let result = transpiler.transpile_dataframe(&[]).unwrap();
298
        let output = result.to_string();
299
        assert!(output.contains("DataFrame"));
300
        assert!(output.contains("empty"));
301
    }
302
    
303
    #[test]
304
    fn test_dataframe_with_columns() {
305
        let transpiler = make_test_transpiler();
306
        let columns = vec![
307
            DataFrameColumn {
308
                name: "col1".to_string(),
309
                values: vec![make_literal_expr(1), make_literal_expr(2)],
310
            },
311
            DataFrameColumn {
312
                name: "col2".to_string(),
313
                values: vec![make_literal_expr(3), make_literal_expr(4)],
314
            },
315
        ];
316
        
317
        let result = transpiler.transpile_dataframe(&columns).unwrap();
318
        let output = result.to_string();
319
        assert!(output.contains("DataFrame"));
320
        assert!(output.contains("Series"));
321
        assert!(output.contains("col1"));
322
        assert!(output.contains("col2"));
323
    }
324
    
325
    #[test]
326
    fn test_dataframe_select_operation() {
327
        let transpiler = make_test_transpiler();
328
        let df_expr = make_literal_expr(0); // Placeholder
329
        let op = DataFrameOp::Select(vec!["col1".to_string(), "col2".to_string()]);
330
        
331
        let result = transpiler.transpile_dataframe_operation(&df_expr, &op).unwrap();
332
        let output = result.to_string();
333
        assert!(output.contains("select"));
334
        assert!(output.contains("col1"));
335
        assert!(output.contains("col2"));
336
    }
337
    
338
    #[test]
339
    fn test_dataframe_filter_operation() {
340
        let transpiler = make_test_transpiler();
341
        let df_expr = make_literal_expr(0);
342
        let condition = make_literal_expr(1);
343
        let op = DataFrameOp::Filter(Box::new(condition));
344
        
345
        let result = transpiler.transpile_dataframe_operation(&df_expr, &op).unwrap();
346
        let output = result.to_string();
347
        assert!(output.contains("filter"));
348
    }
349
    
350
    #[test]
351
    fn test_dataframe_groupby_operation() {
352
        let transpiler = make_test_transpiler();
353
        let df_expr = make_literal_expr(0);
354
        let op = DataFrameOp::GroupBy(vec!["group_col".to_string()]);
355
        
356
        let result = transpiler.transpile_dataframe_operation(&df_expr, &op).unwrap();
357
        let output = result.to_string();
358
        assert!(output.contains("groupby"));
359
        assert!(output.contains("group_col"));
360
    }
361
    
362
    #[test]
363
    fn test_dataframe_sort_operation() {
364
        let transpiler = make_test_transpiler();
365
        let df_expr = make_literal_expr(0);
366
        let op = DataFrameOp::Sort(vec!["sort_col".to_string()]);
367
        
368
        let result = transpiler.transpile_dataframe_operation(&df_expr, &op).unwrap();
369
        let output = result.to_string();
370
        assert!(output.contains("sort"));
371
        assert!(output.contains("sort_col"));
372
    }
373
    
374
    #[test]
375
    fn test_dataframe_join_operations() {
376
        let transpiler = make_test_transpiler();
377
        let df_expr = make_literal_expr(0);
378
        let other_expr = make_literal_expr(1);
379
        
380
        let join_types = vec![
381
            (JoinType::Inner, "Inner"),
382
            (JoinType::Left, "Left"),
383
            (JoinType::Right, "Right"),
384
        ];
385
        
386
        for (join_type, expected) in join_types {
387
            let op = DataFrameOp::Join {
388
                other: Box::new(other_expr.clone()),
389
                on: vec!["id".to_string()],
390
                how: join_type,
391
            };
392
            
393
            let result = transpiler.transpile_dataframe_operation(&df_expr, &op).unwrap();
394
            let output = result.to_string();
395
            assert!(output.contains("join"));
396
            assert!(output.contains(expected));
397
        }
398
    }
399
    
400
    #[test]
401
    fn test_dataframe_aggregate_operations() {
402
        let transpiler = make_test_transpiler();
403
        let df_expr = make_literal_expr(0);
404
        
405
        let agg_ops = vec![
406
            AggregateOp::Mean("col1".to_string()),
407
            AggregateOp::Sum("col2".to_string()),
408
            AggregateOp::Min("col3".to_string()),
409
            AggregateOp::Max("col4".to_string()),
410
            AggregateOp::Count("col5".to_string()),
411
            AggregateOp::Std("col6".to_string()),
412
        ];
413
        
414
        let op = DataFrameOp::Aggregate(agg_ops);
415
        let result = transpiler.transpile_dataframe_operation(&df_expr, &op).unwrap();
416
        let output = result.to_string();
417
        // Check that it produces some output
418
        assert!(!output.is_empty());
419
    }
420
    
421
    #[test]
422
    fn test_dataframe_limit_operations() {
423
        let transpiler = make_test_transpiler();
424
        let df_expr = make_literal_expr(0);
425
        
426
        // Test Limit
427
        let op = DataFrameOp::Limit(10);
428
        let result = transpiler.transpile_dataframe_operation(&df_expr, &op).unwrap();
429
        let output = result.to_string();
430
        assert!(output.contains("limit"));
431
        
432
        // Test Head
433
        let op = DataFrameOp::Head(5);
434
        let result = transpiler.transpile_dataframe_operation(&df_expr, &op).unwrap();
435
        let output = result.to_string();
436
        assert!(output.contains("head"));
437
        
438
        // Test Tail
439
        let op = DataFrameOp::Tail(5);
440
        let result = transpiler.transpile_dataframe_operation(&df_expr, &op).unwrap();
441
        let output = result.to_string();
442
        assert!(output.contains("tail"));
443
    }
444
    
445
    #[test]
446
    fn test_dataframe_with_empty_column_values() {
447
        let transpiler = make_test_transpiler();
448
        let columns = vec![
449
            DataFrameColumn {
450
                name: "empty_col".to_string(),
451
                values: vec![],
452
            },
453
        ];
454
        
455
        let result = transpiler.transpile_dataframe(&columns).unwrap();
456
        let output = result.to_string();
457
        assert!(output.contains("Series"));
458
        assert!(output.contains("empty_col"));
459
        assert!(output.contains("vec"));
460
    }
461
}