Skip to main content

r/interpreter/builtins/
coercion.rs

1//! Coercion builtins: `as.integer`, `as.double`, `as.character`, etc.
2//!
3//! Each function coerces an R value to a specific type, following R's
4//! standard coercion rules.
5
6use crate::interpreter::value::*;
7use indexmap::IndexMap;
8use minir_macros::builtin;
9
10/// Coerce an object to double (numeric).
11///
12/// Also aliased as `as.double`.
13///
14/// @param x object to coerce
15/// @return double vector
16#[builtin(min_args = 1, names = ["as.double"])]
17fn builtin_as_numeric(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
18    match args.first() {
19        Some(RValue::Vector(v)) => Ok(RValue::vec(Vector::Double(v.to_doubles().into()))),
20        Some(RValue::Null) => Ok(RValue::vec(Vector::Double(vec![].into()))),
21        _ => Ok(RValue::vec(Vector::Double(vec![None].into()))),
22    }
23}
24
25/// Coerce an object to integer.
26///
27/// Doubles are truncated toward zero.
28///
29/// @param x object to coerce
30/// @return integer vector
31#[builtin(min_args = 1)]
32fn builtin_as_integer(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
33    match args.first() {
34        Some(RValue::Vector(v)) => Ok(RValue::vec(Vector::Integer(v.to_integers().into()))),
35        Some(RValue::Null) => Ok(RValue::vec(Vector::Integer(vec![].into()))),
36        _ => Ok(RValue::vec(Vector::Integer(vec![None].into()))),
37    }
38}
39
40/// Coerce an object to character (string).
41///
42/// For factors, maps integer codes back to level labels rather than
43/// stringifying the codes.
44///
45/// @param x object to coerce
46/// @return character vector
47#[builtin(min_args = 1)]
48fn builtin_as_character(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
49    match args.first() {
50        Some(RValue::Vector(v)) => {
51            // Check if the vector is a factor — if so, map codes to level labels
52            if is_factor(v) {
53                return factor_to_character(v);
54            }
55            Ok(RValue::vec(Vector::Character(v.to_characters().into())))
56        }
57        Some(RValue::Null) => Ok(RValue::vec(Vector::Character(vec![].into()))),
58        _ => Ok(RValue::vec(Vector::Character(vec![None].into()))),
59    }
60}
61
62/// Check whether an `RVector` has class "factor".
63fn is_factor(v: &RVector) -> bool {
64    if let Some(RValue::Vector(cls_vec)) = v.get_attr("class") {
65        if let Vector::Character(cls) = &cls_vec.inner {
66            return cls.iter().any(|c| c.as_deref() == Some("factor"));
67        }
68    }
69    false
70}
71
72/// Convert a factor (integer codes + levels attr) to a character vector of labels.
73fn factor_to_character(v: &RVector) -> Result<RValue, RError> {
74    let levels: Vec<Option<String>> = match v.get_attr("levels") {
75        Some(RValue::Vector(lv)) => match &lv.inner {
76            Vector::Character(c) => c.to_vec(),
77            _ => vec![],
78        },
79        _ => vec![],
80    };
81
82    let codes = v.inner.to_integers();
83    let labels: Vec<Option<String>> = codes
84        .iter()
85        .map(|code| {
86            code.and_then(|i| {
87                let idx = usize::try_from(i).ok()?.checked_sub(1)?;
88                levels.get(idx).cloned().flatten()
89            })
90        })
91        .collect();
92
93    Ok(RValue::vec(Vector::Character(labels.into())))
94}
95
96/// Coerce an object to logical.
97///
98/// @param x object to coerce
99/// @return logical vector
100#[builtin(min_args = 1)]
101fn builtin_as_logical(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
102    match args.first() {
103        Some(RValue::Vector(v)) => Ok(RValue::vec(Vector::Logical(v.to_logicals().into()))),
104        Some(RValue::Null) => Ok(RValue::vec(Vector::Logical(vec![].into()))),
105        _ => Ok(RValue::vec(Vector::Logical(vec![None].into()))),
106    }
107}
108
109/// Coerce an object to a list.
110///
111/// Atomic vectors are split into single-element list entries.
112///
113/// @param x object to coerce
114/// @return list
115#[builtin(min_args = 1)]
116fn builtin_as_list(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
117    match args.first() {
118        Some(RValue::List(l)) => Ok(RValue::List(l.clone())),
119        Some(RValue::Vector(v)) => {
120            let values: Vec<(Option<String>, RValue)> = match &v.inner {
121                Vector::Raw(vals) => vals
122                    .iter()
123                    .map(|&x| (None, RValue::vec(Vector::Raw(vec![x]))))
124                    .collect(),
125                Vector::Double(vals) => vals
126                    .iter_opt()
127                    .map(|x| (None, RValue::vec(Vector::Double(vec![x].into()))))
128                    .collect(),
129                Vector::Integer(vals) => vals
130                    .iter_opt()
131                    .map(|x| (None, RValue::vec(Vector::Integer(vec![x].into()))))
132                    .collect(),
133                Vector::Logical(vals) => vals
134                    .iter()
135                    .map(|x| (None, RValue::vec(Vector::Logical(vec![*x].into()))))
136                    .collect(),
137                Vector::Complex(vals) => vals
138                    .iter()
139                    .map(|x| (None, RValue::vec(Vector::Complex(vec![*x].into()))))
140                    .collect(),
141                Vector::Character(vals) => vals
142                    .iter()
143                    .map(|x| (None, RValue::vec(Vector::Character(vec![x.clone()].into()))))
144                    .collect(),
145            };
146            Ok(RValue::List(RList::new(values)))
147        }
148        Some(RValue::Null) => Ok(RValue::List(RList::new(vec![]))),
149        _ => Ok(RValue::List(RList::new(vec![]))),
150    }
151}
152
153/// Coerce an object to a matrix.
154///
155/// For vectors, creates a single-column matrix. For data frames, converts
156/// all columns to a common type and combines into a matrix.
157///
158/// @param x object to coerce
159/// @return matrix (vector with dim attribute)
160/// @namespace base
161#[builtin(name = "as.matrix", min_args = 1)]
162fn builtin_as_matrix(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
163    match args.first() {
164        Some(RValue::Vector(v)) => {
165            let len = v.len();
166            let mut rv = v.clone();
167            rv.set_attr(
168                "dim".to_string(),
169                RValue::vec(Vector::Integer(
170                    vec![Some(i64::try_from(len)?), Some(1)].into(),
171                )),
172            );
173            Ok(RValue::Vector(rv))
174        }
175        Some(RValue::List(list)) => {
176            // Data frame → matrix: convert all columns to double, combine column-major
177            let ncol = list.values.len();
178            if ncol == 0 {
179                return Ok(RValue::vec(Vector::Double(vec![].into())));
180            }
181            let nrow = list
182                .values
183                .first()
184                .map(|(_, v)| match v {
185                    RValue::Vector(rv) => rv.len(),
186                    _ => 1,
187                })
188                .unwrap_or(0);
189
190            let mut data: Vec<Option<f64>> = Vec::with_capacity(nrow * ncol);
191            for (_, val) in &list.values {
192                if let Some(v) = val.as_vector() {
193                    let doubles = v.to_doubles();
194                    data.extend(&doubles);
195                } else {
196                    for _ in 0..nrow {
197                        data.push(None);
198                    }
199                }
200            }
201            let mut rv = RVector::from(Vector::Double(data.into()));
202            rv.set_attr(
203                "dim".to_string(),
204                RValue::vec(Vector::Integer(
205                    vec![Some(i64::try_from(nrow)?), Some(i64::try_from(ncol)?)].into(),
206                )),
207            );
208            // Copy column names as dimnames
209            let col_names: Vec<Option<String>> =
210                list.values.iter().map(|(n, _)| n.clone()).collect();
211            if col_names.iter().any(|n| n.is_some()) {
212                let dimnames = RValue::List(RList::new(vec![
213                    (None, RValue::Null),
214                    (None, RValue::vec(Vector::Character(col_names.into()))),
215                ]));
216                rv.set_attr("dimnames".to_string(), dimnames);
217            }
218            Ok(RValue::Vector(rv))
219        }
220        Some(RValue::Null) => Ok(RValue::vec(Vector::Double(vec![].into()))),
221        _ => Ok(RValue::vec(Vector::Double(vec![None].into()))),
222    }
223}
224
225/// Coerce an object to a data frame.
226///
227/// @param x object to coerce
228/// @param row.names optional row names
229/// @return data.frame
230/// @namespace base
231#[builtin(name = "as.data.frame", min_args = 1)]
232fn builtin_as_data_frame(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
233    match args.first() {
234        Some(RValue::List(list)) => {
235            // Already a list — add data.frame class if not present
236            let mut list = list.clone();
237            let mut attrs = *list.attrs.take().unwrap_or_default();
238            attrs.insert(
239                "class".to_string(),
240                RValue::vec(Vector::Character(
241                    vec![Some("data.frame".to_string())].into(),
242                )),
243            );
244            // Add row.names if missing
245            if !attrs.contains_key("row.names") {
246                let nrow = list
247                    .values
248                    .first()
249                    .map(|(_, v)| match v {
250                        RValue::Vector(rv) => rv.len(),
251                        _ => 1,
252                    })
253                    .unwrap_or(0);
254                attrs.insert(
255                    "row.names".to_string(),
256                    RValue::vec(Vector::Integer(
257                        (1..=nrow as i64).map(Some).collect::<Vec<_>>().into(),
258                    )),
259                );
260            }
261            list.attrs = Some(Box::new(attrs));
262            Ok(RValue::List(list))
263        }
264        Some(RValue::Vector(v)) => {
265            // Single vector → single-column data frame
266            let col_name = named
267                .iter()
268                .find(|(n, _)| n == "col.names")
269                .and_then(|(_, v)| v.as_vector()?.as_character_scalar())
270                .unwrap_or_else(|| "V1".to_string());
271            let nrow = v.len();
272            let mut list = RList::new(vec![(Some(col_name.clone()), RValue::Vector(v.clone()))]);
273            let mut attrs: IndexMap<String, RValue> = IndexMap::new();
274            attrs.insert(
275                "class".to_string(),
276                RValue::vec(Vector::Character(
277                    vec![Some("data.frame".to_string())].into(),
278                )),
279            );
280            attrs.insert(
281                "names".to_string(),
282                RValue::vec(Vector::Character(vec![Some(col_name)].into())),
283            );
284            attrs.insert(
285                "row.names".to_string(),
286                RValue::vec(Vector::Integer(
287                    (1..=nrow as i64).map(Some).collect::<Vec<_>>().into(),
288                )),
289            );
290            list.attrs = Some(Box::new(attrs));
291            Ok(RValue::List(list))
292        }
293        Some(RValue::Null) => {
294            let list = RList::new(vec![]);
295            let mut attrs: IndexMap<String, RValue> = IndexMap::new();
296            attrs.insert(
297                "class".to_string(),
298                RValue::vec(Vector::Character(
299                    vec![Some("data.frame".to_string())].into(),
300                )),
301            );
302            attrs.insert(
303                "row.names".to_string(),
304                RValue::vec(Vector::Integer(vec![].into())),
305            );
306            let mut list = list;
307            list.attrs = Some(Box::new(attrs));
308            Ok(RValue::List(list))
309        }
310        _ => Err(RError::new(
311            RErrorKind::Argument,
312            "cannot coerce to data.frame".to_string(),
313        )),
314    }
315}
316
317/// Coerce an object to a factor.
318///
319/// @param x object to coerce
320/// @return factor
321/// @namespace base
322#[builtin(name = "as.factor", min_args = 1)]
323fn builtin_as_factor(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
324    // Convert to character first, then create a factor
325    let chars = match args.first() {
326        Some(RValue::Vector(v)) => {
327            if is_factor(v) {
328                return Ok(RValue::Vector(v.clone()));
329            }
330            v.to_characters()
331        }
332        _ => vec![],
333    };
334
335    // Get unique levels
336    let mut levels: Vec<String> = Vec::new();
337    for s in chars.iter().flatten() {
338        if !levels.contains(s) {
339            levels.push(s.clone());
340        }
341    }
342    levels.sort();
343
344    // Map values to integer codes
345    let codes: Vec<Option<i64>> = chars
346        .iter()
347        .map(|c| {
348            c.as_ref()
349                .and_then(|s| levels.iter().position(|l| l == s).map(|i| (i + 1) as i64))
350        })
351        .collect();
352
353    let mut rv = RVector::from(Vector::Integer(codes.into()));
354    rv.set_attr(
355        "levels".to_string(),
356        RValue::vec(Vector::Character(
357            levels.into_iter().map(Some).collect::<Vec<_>>().into(),
358        )),
359    );
360    rv.set_attr(
361        "class".to_string(),
362        RValue::vec(Vector::Character(vec![Some("factor".to_string())].into())),
363    );
364    Ok(RValue::Vector(rv))
365}
366
367/// Coerce a string to a symbol/name.
368///
369/// @param x character scalar
370/// @return name (Language wrapping Expr::Symbol)
371/// @namespace base
372#[builtin(name = "as.name", min_args = 1, names = ["as.symbol"])]
373fn builtin_as_name(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
374    let name = match args.first() {
375        Some(RValue::Vector(v)) => v.as_character_scalar().unwrap_or_default(),
376        _ => String::new(),
377    };
378    Ok(RValue::Language(Language::new(
379        crate::parser::ast::Expr::Symbol(name),
380    )))
381}
382
383/// Convert a list to a call object (Language).
384///
385/// `as.call(list(f, a, b))` creates the call `f(a, b)`.
386/// The first element is the function, remaining are arguments.
387///
388/// @param x a list
389/// @return a call (Language) object
390/// @namespace base
391#[builtin(name = "as.call", min_args = 1)]
392fn builtin_as_call(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
393    use crate::parser::ast::{Arg, Expr};
394    let list = match args.first() {
395        Some(RValue::List(l)) => l,
396        _ => {
397            return Err(RError::new(
398                RErrorKind::Argument,
399                "as.call: argument must be a list".to_string(),
400            ))
401        }
402    };
403    if list.values.is_empty() {
404        return Err(RError::new(
405            RErrorKind::Argument,
406            "as.call: list must have at least one element (the function)".to_string(),
407        ));
408    }
409
410    // First element is the function
411    let (_, func_val) = &list.values[0];
412    let func_expr = rvalue_to_expr(func_val);
413
414    // Remaining elements are arguments
415    let call_args: Vec<Arg> = list.values[1..]
416        .iter()
417        .map(|(name, val)| Arg {
418            name: name.clone(),
419            value: Some(rvalue_to_expr(val)),
420        })
421        .collect();
422
423    let call = Expr::Call {
424        func: Box::new(func_expr),
425        args: call_args,
426        span: None,
427    };
428    Ok(RValue::Language(Language::new(call)))
429}
430
431/// Convert an RValue to an Expr for constructing Language objects.
432fn rvalue_to_expr(val: &RValue) -> crate::parser::ast::Expr {
433    use crate::parser::ast::Expr;
434    match val {
435        RValue::Null => Expr::Null,
436        RValue::Vector(rv) => match &rv.inner {
437            Vector::Logical(l) if l.len() == 1 => match l[0] {
438                Some(true) => Expr::Bool(true),
439                Some(false) => Expr::Bool(false),
440                None => Expr::Na(crate::parser::ast::NaType::Logical),
441            },
442            Vector::Integer(i) if i.len() == 1 => match i.get_opt(0) {
443                Some(v) => Expr::Integer(v),
444                None => Expr::Na(crate::parser::ast::NaType::Integer),
445            },
446            Vector::Double(d) if d.len() == 1 => match d.get_opt(0) {
447                Some(v) => Expr::Double(v),
448                None => Expr::Na(crate::parser::ast::NaType::Real),
449            },
450            Vector::Character(c) if c.len() == 1 => match &c[0] {
451                Some(s) => Expr::String(s.clone()),
452                None => Expr::Na(crate::parser::ast::NaType::Character),
453            },
454            _ => Expr::Null,
455        },
456        RValue::Language(lang) => lang.inner.as_ref().clone(),
457        RValue::Function(_) => Expr::Symbol("<function>".to_string()),
458        _ => Expr::Null,
459    }
460}
461
462/// Convert a list to a pairlist. In miniR, pairlists are represented as lists.
463///
464/// @param x a list or other object
465/// @return a pairlist (represented as a list in miniR)
466/// @namespace base
467#[builtin(name = "as.pairlist", min_args = 1)]
468fn builtin_as_pairlist(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
469    // In miniR, pairlists are just lists
470    match args.first() {
471        Some(RValue::List(l)) => Ok(RValue::List(l.clone())),
472        Some(RValue::Null) => Ok(RValue::Null),
473        Some(other) => Ok(other.clone()),
474        None => Ok(RValue::Null),
475    }
476}
477
478/// Look up a function by name or return it if already a function.
479///
480/// @param FUN function or character string naming a function
481/// @return the function
482/// @namespace base
483#[builtin(name = "match.fun", min_args = 1)]
484fn builtin_match_fun(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
485    match args.first() {
486        Some(RValue::Function(_)) => Ok(args[0].clone()),
487        Some(RValue::Vector(v)) => {
488            let name = v.as_character_scalar().ok_or_else(|| {
489                RError::new(
490                    RErrorKind::Argument,
491                    "'FUN' must be a function or a character string".to_string(),
492                )
493            })?;
494            // Can't do environment lookup from a plain builtin — return an error
495            // suggesting the user pass the function directly
496            Err(RError::new(
497                RErrorKind::Other,
498                format!(
499                    "match.fun cannot resolve '{}' — pass the function directly",
500                    name
501                ),
502            ))
503        }
504        _ => Err(RError::new(
505            RErrorKind::Argument,
506            "'FUN' must be a function or a character string".to_string(),
507        )),
508    }
509}
510
511/// Coerce an object to a vector, stripping all attributes.
512///
513/// @param x object to coerce
514/// @return the object with all attributes removed
515#[builtin(min_args = 1)]
516fn builtin_as_vector(args: &[RValue], _: &[(String, RValue)]) -> Result<RValue, RError> {
517    match args.first() {
518        Some(RValue::Vector(v)) => {
519            let mut v = v.clone();
520            v.attrs = None;
521            Ok(RValue::Vector(v))
522        }
523        Some(RValue::List(items)) => {
524            let mut items = items.clone();
525            items.attrs = None;
526            Ok(RValue::List(items))
527        }
528        Some(RValue::Null) => Ok(RValue::Null),
529        _ => Ok(args.first().cloned().unwrap_or(RValue::Null)),
530    }
531}