Skip to main content

r/interpreter/builtins/
toml.rs

1//! TOML builtins — `read.toml()`, `write.toml()`, `toml_parse()`, and `toml_serialize()`
2//! for reading, writing, and converting between R values and TOML.
3
4use std::collections::HashSet;
5
6use super::CallArgs;
7use crate::interpreter::value::*;
8use minir_macros::builtin;
9
10// region: toml_parse
11
12/// Parse a TOML string into an R value.
13///
14/// Conversion rules:
15/// - TOML table -> named list
16/// - TOML array of tables (homogeneous keys) -> data.frame
17/// - TOML array of scalars -> atomic vector
18/// - TOML array of mixed types -> list
19/// - TOML string -> character
20/// - TOML integer -> integer
21/// - TOML float -> double
22/// - TOML boolean -> logical
23/// - TOML datetime -> character (ISO 8601 string)
24///
25/// @param text character scalar: TOML string to parse
26/// @return R value corresponding to the TOML structure
27#[builtin(name = "toml_parse", min_args = 1, namespace = "utils")]
28fn builtin_toml_parse(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
29    let call_args = CallArgs::new(args, named);
30    let text = call_args.string("text", 0)?;
31
32    let doc: toml_edit::DocumentMut = text.parse().map_err(|e: toml_edit::TomlError| {
33        RError::new(RErrorKind::Other, format!("TOML parse error: {e}"))
34    })?;
35
36    table_to_rvalue(doc.as_table())
37}
38
39// endregion
40
41// region: toml_serialize
42
43/// Convert an R value to a TOML string.
44///
45/// Conversion rules:
46/// - Named list -> TOML table
47/// - Atomic vector of length 1 -> TOML scalar
48/// - Atomic vector of length > 1 -> TOML array
49/// - NULL -> omitted
50///
51/// @param x R value to convert (typically a named list)
52/// @return character scalar containing the TOML string
53#[builtin(name = "toml_serialize", min_args = 1, namespace = "utils")]
54fn builtin_toml_serialize(args: &[RValue], _named: &[(String, RValue)]) -> Result<RValue, RError> {
55    let value = args
56        .first()
57        .ok_or_else(|| RError::new(RErrorKind::Argument, "argument 'x' is missing".to_string()))?;
58
59    let RValue::List(list) = value else {
60        return Err(RError::new(
61            RErrorKind::Type,
62            "toml_serialize() requires a named list as input".to_string(),
63        ));
64    };
65
66    let table = rlist_to_table(list)?;
67    let mut doc = toml_edit::DocumentMut::new();
68    // Copy all items from our table into the document
69    for (key, item) in table.iter() {
70        doc.insert(key, item.clone());
71    }
72
73    Ok(RValue::vec(Vector::Character(
74        vec![Some(doc.to_string())].into(),
75    )))
76}
77
78// endregion
79
80// region: read.toml
81
82/// Read a TOML file and return its contents as an R named list.
83///
84/// @param file character scalar: path to the TOML file
85/// @return named list representing the TOML document
86#[builtin(name = "read.toml", min_args = 1, namespace = "utils")]
87fn builtin_read_toml(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
88    let call_args = CallArgs::new(args, named);
89    let path = call_args.string("file", 0)?;
90
91    let content = std::fs::read_to_string(&path).map_err(|e| {
92        RError::new(
93            RErrorKind::Other,
94            format!("cannot open file '{}': {}", path, e),
95        )
96    })?;
97
98    let doc: toml_edit::DocumentMut = content.parse().map_err(|e: toml_edit::TomlError| {
99        RError::new(
100            RErrorKind::Other,
101            format!("TOML parse error in '{}': {}", path, e),
102        )
103    })?;
104
105    table_to_rvalue(doc.as_table())
106}
107
108// endregion
109
110// region: write.toml
111
112/// Write an R named list as a TOML file.
113///
114/// @param x named list to serialize
115/// @param file character scalar: path to the output TOML file
116/// @return NULL (invisibly)
117#[builtin(name = "write.toml", min_args = 2, namespace = "utils")]
118fn builtin_write_toml(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
119    let call_args = CallArgs::new(args, named);
120
121    let value = args
122        .first()
123        .ok_or_else(|| RError::new(RErrorKind::Argument, "argument 'x' is missing".to_string()))?;
124    let path = call_args.string("file", 1)?;
125
126    let RValue::List(list) = value else {
127        return Err(RError::new(
128            RErrorKind::Type,
129            "write.toml() requires a named list as input".to_string(),
130        ));
131    };
132
133    let table = rlist_to_table(list)?;
134    let mut doc = toml_edit::DocumentMut::new();
135    for (key, item) in table.iter() {
136        doc.insert(key, item.clone());
137    }
138
139    std::fs::write(&path, doc.to_string()).map_err(|e| {
140        RError::new(
141            RErrorKind::Other,
142            format!("cannot write to file '{}': {}", path, e),
143        )
144    })?;
145
146    Ok(RValue::Null)
147}
148
149// endregion
150
151// region: TOML -> RValue conversion
152
153/// Convert a TOML table to an R named list.
154fn table_to_rvalue(table: &toml_edit::Table) -> Result<RValue, RError> {
155    let mut entries: Vec<(Option<String>, RValue)> = Vec::with_capacity(table.len());
156    for (key, item) in table.iter() {
157        entries.push((Some(key.to_string()), item_to_rvalue(item)?));
158    }
159    Ok(RValue::List(RList::new(entries)))
160}
161
162/// Convert a TOML `Item` to an `RValue`.
163fn item_to_rvalue(item: &toml_edit::Item) -> Result<RValue, RError> {
164    match item {
165        toml_edit::Item::None => Ok(RValue::Null),
166        toml_edit::Item::Value(v) => value_to_rvalue(v),
167        toml_edit::Item::Table(t) => table_to_rvalue(t),
168        toml_edit::Item::ArrayOfTables(aot) => array_of_tables_to_rvalue(aot),
169    }
170}
171
172/// Convert a TOML `Value` to an `RValue`.
173fn value_to_rvalue(value: &toml_edit::Value) -> Result<RValue, RError> {
174    match value {
175        toml_edit::Value::String(s) => Ok(RValue::vec(Vector::Character(
176            vec![Some(s.value().to_string())].into(),
177        ))),
178        toml_edit::Value::Integer(i) => {
179            Ok(RValue::vec(Vector::Integer(vec![Some(*i.value())].into())))
180        }
181        toml_edit::Value::Float(f) => {
182            Ok(RValue::vec(Vector::Double(vec![Some(*f.value())].into())))
183        }
184        toml_edit::Value::Boolean(b) => {
185            Ok(RValue::vec(Vector::Logical(vec![Some(*b.value())].into())))
186        }
187        toml_edit::Value::Datetime(dt) => {
188            // Convert TOML datetime to its string representation
189            Ok(RValue::vec(Vector::Character(
190                vec![Some(dt.value().to_string())].into(),
191            )))
192        }
193        toml_edit::Value::Array(arr) => toml_array_to_rvalue(arr),
194        toml_edit::Value::InlineTable(t) => inline_table_to_rvalue(t),
195    }
196}
197
198/// Convert a TOML inline table to an R named list.
199fn inline_table_to_rvalue(table: &toml_edit::InlineTable) -> Result<RValue, RError> {
200    let mut entries: Vec<(Option<String>, RValue)> = Vec::with_capacity(table.len());
201    for (key, value) in table.iter() {
202        entries.push((Some(key.to_string()), value_to_rvalue(value)?));
203    }
204    Ok(RValue::List(RList::new(entries)))
205}
206
207/// Convert a TOML array to an R value.
208///
209/// If all elements are scalars of the same type, produce an atomic vector.
210/// If all elements are tables with the same keys, produce a data.frame.
211/// Otherwise, produce a list.
212fn toml_array_to_rvalue(arr: &toml_edit::Array) -> Result<RValue, RError> {
213    let items: Vec<&toml_edit::Value> = arr.iter().collect();
214
215    if items.is_empty() {
216        return Ok(RValue::List(RList::new(vec![])));
217    }
218
219    // Try homogeneous scalar array -> atomic vector
220    if let Some(vec) = try_toml_array_as_vector(&items) {
221        return Ok(vec);
222    }
223
224    // Try array of inline tables with same keys -> data.frame
225    if let Some(df) = try_toml_array_as_dataframe(&items)? {
226        return Ok(df);
227    }
228
229    // Fallback: heterogeneous list
230    let elements: Result<Vec<(Option<String>, RValue)>, RError> = items
231        .iter()
232        .map(|v| Ok((None, value_to_rvalue(v)?)))
233        .collect();
234    Ok(RValue::List(RList::new(elements?)))
235}
236
237/// Try to convert a TOML array of scalars into an atomic vector.
238/// Returns `None` if the array contains non-scalar values or mixed types.
239fn try_toml_array_as_vector(items: &[&toml_edit::Value]) -> Option<RValue> {
240    // Check what scalar types are present
241    let mut has_string = false;
242    let mut has_int = false;
243    let mut has_float = false;
244    let mut has_bool = false;
245    let mut has_datetime = false;
246    let mut has_non_scalar = false;
247
248    for item in items {
249        match item {
250            toml_edit::Value::String(_) => has_string = true,
251            toml_edit::Value::Integer(_) => has_int = true,
252            toml_edit::Value::Float(_) => has_float = true,
253            toml_edit::Value::Boolean(_) => has_bool = true,
254            toml_edit::Value::Datetime(_) => has_datetime = true,
255            toml_edit::Value::Array(_) | toml_edit::Value::InlineTable(_) => {
256                has_non_scalar = true;
257            }
258        }
259    }
260
261    if has_non_scalar {
262        return None;
263    }
264
265    // Datetimes go to character
266    if has_datetime {
267        let result: Vec<Option<String>> = items
268            .iter()
269            .map(|v| match v {
270                toml_edit::Value::Datetime(dt) => Some(dt.value().to_string()),
271                toml_edit::Value::String(s) => Some(s.value().to_string()),
272                _ => Some(format!("{}", v)),
273            })
274            .collect();
275        return Some(RValue::vec(Vector::Character(result.into())));
276    }
277
278    // Strings dominate
279    if has_string {
280        let result: Vec<Option<String>> = items
281            .iter()
282            .map(|v| match v {
283                toml_edit::Value::String(s) => Some(s.value().to_string()),
284                toml_edit::Value::Boolean(b) => {
285                    Some(if *b.value() { "TRUE" } else { "FALSE" }.to_string())
286                }
287                toml_edit::Value::Integer(i) => Some(i.value().to_string()),
288                toml_edit::Value::Float(f) => Some(f.value().to_string()),
289                _ => None,
290            })
291            .collect();
292        return Some(RValue::vec(Vector::Character(result.into())));
293    }
294
295    // Pure booleans (no numbers)
296    if has_bool && !has_int && !has_float {
297        let result: Vec<Option<bool>> = items
298            .iter()
299            .map(|v| match v {
300                toml_edit::Value::Boolean(b) => Some(*b.value()),
301                _ => None,
302            })
303            .collect();
304        return Some(RValue::vec(Vector::Logical(result.into())));
305    }
306
307    // Has floats -> all numeric becomes double
308    if has_float {
309        let result: Vec<Option<f64>> = items
310            .iter()
311            .map(|v| match v {
312                toml_edit::Value::Float(f) => Some(*f.value()),
313                toml_edit::Value::Integer(i) => {
314                    // Safe: i64 always fits in f64 (may lose precision for very large values)
315                    #[allow(clippy::cast_precision_loss)]
316                    Some(*i.value() as f64)
317                }
318                toml_edit::Value::Boolean(b) => Some(if *b.value() { 1.0 } else { 0.0 }),
319                _ => None,
320            })
321            .collect();
322        return Some(RValue::vec(Vector::Double(result.into())));
323    }
324
325    // Pure integers
326    if has_int {
327        let result: Vec<Option<i64>> = items
328            .iter()
329            .map(|v| match v {
330                toml_edit::Value::Integer(i) => Some(*i.value()),
331                toml_edit::Value::Boolean(b) => Some(i64::from(*b.value())),
332                _ => None,
333            })
334            .collect();
335        return Some(RValue::vec(Vector::Integer(result.into())));
336    }
337
338    // Pure booleans with numbers -> integer
339    if has_bool {
340        let result: Vec<Option<i64>> = items
341            .iter()
342            .map(|v| match v {
343                toml_edit::Value::Boolean(b) => Some(i64::from(*b.value())),
344                _ => None,
345            })
346            .collect();
347        return Some(RValue::vec(Vector::Integer(result.into())));
348    }
349
350    None
351}
352
353/// Try to convert a TOML array of inline tables into a data.frame.
354/// Returns `None` if the elements are not all inline tables with the same keys.
355fn try_toml_array_as_dataframe(items: &[&toml_edit::Value]) -> Result<Option<RValue>, RError> {
356    // Check all elements are inline tables
357    let mut tables: Vec<&toml_edit::InlineTable> = Vec::with_capacity(items.len());
358    for item in items {
359        if let toml_edit::Value::InlineTable(t) = item {
360            tables.push(t);
361        } else {
362            return Ok(None);
363        }
364    }
365
366    if tables.is_empty() {
367        return Ok(None);
368    }
369
370    // Check all tables have the same keys
371    let first_keys: HashSet<&str> = tables[0].iter().map(|(k, _)| k).collect();
372    for table in &tables[1..] {
373        let keys: HashSet<&str> = table.iter().map(|(k, _)| k).collect();
374        if keys != first_keys {
375            return Ok(None);
376        }
377    }
378
379    // Collect column names in order from first table
380    let col_names: Vec<String> = tables[0].iter().map(|(k, _)| k.to_string()).collect();
381    let nrows = tables.len();
382
383    // Build each column as an R vector
384    let mut list_cols: Vec<(Option<String>, RValue)> = Vec::new();
385    for col_name in &col_names {
386        let vals: Vec<&toml_edit::Value> = tables
387            .iter()
388            .map(|t| {
389                t.get(col_name.as_str())
390                    .expect("key verified to exist in all tables")
391            })
392            .collect();
393        let col_value = coerce_toml_column(&vals)?;
394        list_cols.push((Some(col_name.clone()), col_value));
395    }
396
397    let mut list = RList::new(list_cols);
398    list.set_attr(
399        "class".to_string(),
400        RValue::vec(Vector::Character(
401            vec![Some("data.frame".to_string())].into(),
402        )),
403    );
404    list.set_attr(
405        "names".to_string(),
406        RValue::vec(Vector::Character(
407            col_names.into_iter().map(Some).collect::<Vec<_>>().into(),
408        )),
409    );
410    let row_names: Vec<Option<i64>> = (1..=i64::try_from(nrows)?).map(Some).collect();
411    list.set_attr(
412        "row.names".to_string(),
413        RValue::vec(Vector::Integer(row_names.into())),
414    );
415
416    Ok(Some(RValue::List(list)))
417}
418
419/// Convert an array of tables (TOML `[[section]]`) to an R value.
420///
421/// If all tables have the same keys, produce a data.frame.
422/// Otherwise, produce a list.
423fn array_of_tables_to_rvalue(aot: &toml_edit::ArrayOfTables) -> Result<RValue, RError> {
424    let tables: Vec<&toml_edit::Table> = aot.iter().collect();
425
426    if tables.is_empty() {
427        return Ok(RValue::List(RList::new(vec![])));
428    }
429
430    // Check if all tables have the same keys -> data.frame
431    let first_keys: HashSet<&str> = tables[0].iter().map(|(k, _)| k).collect();
432    let all_same_keys = tables[1..]
433        .iter()
434        .all(|t| t.iter().map(|(k, _)| k).collect::<HashSet<_>>() == first_keys);
435
436    if all_same_keys && !first_keys.is_empty() {
437        let col_names: Vec<String> = tables[0].iter().map(|(k, _)| k.to_string()).collect();
438        let nrows = tables.len();
439
440        let mut list_cols: Vec<(Option<String>, RValue)> = Vec::new();
441        for col_name in &col_names {
442            let vals: Vec<&toml_edit::Item> = tables
443                .iter()
444                .map(|t| {
445                    t.get(col_name.as_str())
446                        .expect("key verified to exist in all tables")
447                })
448                .collect();
449            let col_value = coerce_toml_item_column(&vals)?;
450            list_cols.push((Some(col_name.clone()), col_value));
451        }
452
453        let mut list = RList::new(list_cols);
454        list.set_attr(
455            "class".to_string(),
456            RValue::vec(Vector::Character(
457                vec![Some("data.frame".to_string())].into(),
458            )),
459        );
460        list.set_attr(
461            "names".to_string(),
462            RValue::vec(Vector::Character(
463                col_names.into_iter().map(Some).collect::<Vec<_>>().into(),
464            )),
465        );
466        let row_names: Vec<Option<i64>> = (1..=i64::try_from(nrows)?).map(Some).collect();
467        list.set_attr(
468            "row.names".to_string(),
469            RValue::vec(Vector::Integer(row_names.into())),
470        );
471
472        return Ok(RValue::List(list));
473    }
474
475    // Fallback: list of named lists
476    let elements: Result<Vec<(Option<String>, RValue)>, RError> = tables
477        .iter()
478        .map(|t| Ok((None, table_to_rvalue(t)?)))
479        .collect();
480    Ok(RValue::List(RList::new(elements?)))
481}
482
483/// Coerce a column of TOML values to an R vector (for data.frame construction).
484fn coerce_toml_column(vals: &[&toml_edit::Value]) -> Result<RValue, RError> {
485    let mut has_string = false;
486    let mut has_int = false;
487    let mut has_float = false;
488    let mut has_bool = false;
489    let mut has_datetime = false;
490    let mut has_complex = false; // arrays/tables
491
492    for v in vals {
493        match v {
494            toml_edit::Value::String(_) => has_string = true,
495            toml_edit::Value::Integer(_) => has_int = true,
496            toml_edit::Value::Float(_) => has_float = true,
497            toml_edit::Value::Boolean(_) => has_bool = true,
498            toml_edit::Value::Datetime(_) => has_datetime = true,
499            _ => has_complex = true,
500        }
501    }
502
503    // If complex values present, fall back to list column
504    if has_complex {
505        let elements: Result<Vec<(Option<String>, RValue)>, RError> = vals
506            .iter()
507            .map(|v| Ok((None, value_to_rvalue(v)?)))
508            .collect();
509        return Ok(RValue::List(RList::new(elements?)));
510    }
511
512    // Datetimes and strings -> character
513    if has_string || has_datetime {
514        let result: Vec<Option<String>> = vals
515            .iter()
516            .map(|v| match v {
517                toml_edit::Value::String(s) => Some(s.value().to_string()),
518                toml_edit::Value::Datetime(dt) => Some(dt.value().to_string()),
519                toml_edit::Value::Boolean(b) => {
520                    Some(if *b.value() { "TRUE" } else { "FALSE" }.to_string())
521                }
522                toml_edit::Value::Integer(i) => Some(i.value().to_string()),
523                toml_edit::Value::Float(f) => Some(f.value().to_string()),
524                _ => None,
525            })
526            .collect();
527        return Ok(RValue::vec(Vector::Character(result.into())));
528    }
529
530    // Pure booleans
531    if has_bool && !has_int && !has_float {
532        let result: Vec<Option<bool>> = vals
533            .iter()
534            .map(|v| match v {
535                toml_edit::Value::Boolean(b) => Some(*b.value()),
536                _ => None,
537            })
538            .collect();
539        return Ok(RValue::vec(Vector::Logical(result.into())));
540    }
541
542    // Has floats -> double
543    if has_float {
544        let result: Vec<Option<f64>> = vals
545            .iter()
546            .map(|v| match v {
547                toml_edit::Value::Float(f) => Some(*f.value()),
548                toml_edit::Value::Integer(i) =>
549                {
550                    #[allow(clippy::cast_precision_loss)]
551                    Some(*i.value() as f64)
552                }
553                toml_edit::Value::Boolean(b) => Some(if *b.value() { 1.0 } else { 0.0 }),
554                _ => None,
555            })
556            .collect();
557        return Ok(RValue::vec(Vector::Double(result.into())));
558    }
559
560    // Pure integers
561    if has_int {
562        let result: Vec<Option<i64>> = vals
563            .iter()
564            .map(|v| match v {
565                toml_edit::Value::Integer(i) => Some(*i.value()),
566                toml_edit::Value::Boolean(b) => Some(i64::from(*b.value())),
567                _ => None,
568            })
569            .collect();
570        return Ok(RValue::vec(Vector::Integer(result.into())));
571    }
572
573    // Pure booleans with numbers
574    if has_bool {
575        let result: Vec<Option<i64>> = vals
576            .iter()
577            .map(|v| match v {
578                toml_edit::Value::Boolean(b) => Some(i64::from(*b.value())),
579                _ => None,
580            })
581            .collect();
582        return Ok(RValue::vec(Vector::Integer(result.into())));
583    }
584
585    // All empty? Return empty logical
586    Ok(RValue::vec(Vector::Logical(
587        Vec::<Option<bool>>::new().into(),
588    )))
589}
590
591/// Coerce a column of TOML Items to an R vector (for array-of-tables data.frame).
592fn coerce_toml_item_column(vals: &[&toml_edit::Item]) -> Result<RValue, RError> {
593    // Extract Values from Items, falling back to list for non-Value items
594    let mut values: Vec<&toml_edit::Value> = Vec::with_capacity(vals.len());
595    let mut has_non_value = false;
596
597    for item in vals {
598        if let Some(v) = item.as_value() {
599            values.push(v);
600        } else {
601            has_non_value = true;
602            break;
603        }
604    }
605
606    if has_non_value {
607        // Fall back to list of items
608        let elements: Result<Vec<(Option<String>, RValue)>, RError> = vals
609            .iter()
610            .map(|item| Ok((None, item_to_rvalue(item)?)))
611            .collect();
612        return Ok(RValue::List(RList::new(elements?)));
613    }
614
615    coerce_toml_column(&values)
616}
617
618// endregion
619
620// region: RValue -> TOML conversion
621
622/// Convert an R named list to a TOML `Table`.
623fn rlist_to_table(list: &RList) -> Result<toml_edit::Table, RError> {
624    let mut table = toml_edit::Table::new();
625
626    for (name, value) in &list.values {
627        let key = name.as_ref().ok_or_else(|| {
628            RError::new(
629                RErrorKind::Type,
630                "TOML requires all list elements to be named".to_string(),
631            )
632        })?;
633
634        match value {
635            RValue::Null => {
636                // TOML has no null — skip null entries
637            }
638            RValue::Vector(rv) => {
639                table.insert(key, toml_edit::Item::Value(vector_to_toml(&rv.inner)?));
640            }
641            RValue::List(inner_list) => {
642                // Check if this is a nested table or should be an inline table
643                if is_simple_list(inner_list) {
644                    table.insert(
645                        key,
646                        toml_edit::Item::Value(toml_edit::Value::InlineTable(
647                            rlist_to_inline_table(inner_list)?,
648                        )),
649                    );
650                } else {
651                    let subtable = rlist_to_table(inner_list)?;
652                    table.insert(key, toml_edit::Item::Table(subtable));
653                }
654            }
655            RValue::Function(_) => {
656                return Err(RError::new(
657                    RErrorKind::Type,
658                    format!("cannot convert function to TOML (key '{key}')"),
659                ));
660            }
661            RValue::Environment(_) => {
662                return Err(RError::new(
663                    RErrorKind::Type,
664                    format!("cannot convert environment to TOML (key '{key}')"),
665                ));
666            }
667            RValue::Language(_) => {
668                return Err(RError::new(
669                    RErrorKind::Type,
670                    format!("cannot convert language object to TOML (key '{key}')"),
671                ));
672            }
673            RValue::Promise(_) => {
674                return Err(RError::new(
675                    RErrorKind::Type,
676                    format!("cannot convert promise to TOML (key '{key}') — force it first"),
677                ));
678            }
679        }
680    }
681
682    Ok(table)
683}
684
685/// Check if a list is "simple" (all scalar values, no nested lists/tables).
686/// Simple lists become inline tables, complex ones become regular tables.
687fn is_simple_list(list: &RList) -> bool {
688    list.values.iter().all(|(_, v)| match v {
689        RValue::Vector(rv) => rv.inner.len() <= 1,
690        RValue::Null => true,
691        _ => false,
692    })
693}
694
695/// Convert an R named list to a TOML inline table.
696fn rlist_to_inline_table(list: &RList) -> Result<toml_edit::InlineTable, RError> {
697    let mut table = toml_edit::InlineTable::new();
698
699    for (name, value) in &list.values {
700        let key = name.as_ref().ok_or_else(|| {
701            RError::new(
702                RErrorKind::Type,
703                "TOML requires all list elements to be named".to_string(),
704            )
705        })?;
706
707        match value {
708            RValue::Null => {}
709            RValue::Vector(rv) => {
710                table.insert(key, vector_to_toml(&rv.inner)?);
711            }
712            RValue::List(inner) => {
713                table.insert(
714                    key,
715                    toml_edit::Value::InlineTable(rlist_to_inline_table(inner)?),
716                );
717            }
718            _ => {
719                return Err(RError::new(
720                    RErrorKind::Type,
721                    format!("cannot convert {} to TOML (key '{key}')", value.type_name()),
722                ));
723            }
724        }
725    }
726
727    Ok(table)
728}
729
730/// Convert an R atomic vector to a TOML `Value`.
731/// Scalars (length 1) become TOML scalars; longer vectors become TOML arrays.
732fn vector_to_toml(vec: &Vector) -> Result<toml_edit::Value, RError> {
733    match vec {
734        Vector::Logical(v) => {
735            if v.len() == 1 {
736                match v[0] {
737                    Some(b) => Ok(toml_edit::Value::from(b)),
738                    None => Err(RError::new(
739                        RErrorKind::Type,
740                        "TOML does not support NA values".to_string(),
741                    )),
742                }
743            } else {
744                let mut arr = toml_edit::Array::new();
745                for item in v.iter() {
746                    match item {
747                        Some(b) => arr.push_formatted(toml_edit::Value::from(*b)),
748                        None => {
749                            return Err(RError::new(
750                                RErrorKind::Type,
751                                "TOML does not support NA values".to_string(),
752                            ))
753                        }
754                    }
755                }
756                Ok(toml_edit::Value::Array(arr))
757            }
758        }
759        Vector::Integer(v) => {
760            if v.len() == 1 {
761                match v.get_opt(0) {
762                    Some(i) => Ok(toml_edit::Value::from(i)),
763                    None => Err(RError::new(
764                        RErrorKind::Type,
765                        "TOML does not support NA values".to_string(),
766                    )),
767                }
768            } else {
769                let mut arr = toml_edit::Array::new();
770                for item in v.iter_opt() {
771                    match item {
772                        Some(i) => arr.push_formatted(toml_edit::Value::from(i)),
773                        None => {
774                            return Err(RError::new(
775                                RErrorKind::Type,
776                                "TOML does not support NA values".to_string(),
777                            ))
778                        }
779                    }
780                }
781                Ok(toml_edit::Value::Array(arr))
782            }
783        }
784        Vector::Double(v) => {
785            if v.len() == 1 {
786                match v.get_opt(0) {
787                    Some(f) => double_to_toml(f),
788                    None => Err(RError::new(
789                        RErrorKind::Type,
790                        "TOML does not support NA values".to_string(),
791                    )),
792                }
793            } else {
794                let mut arr = toml_edit::Array::new();
795                for item in v.iter_opt() {
796                    match item {
797                        Some(f) => arr.push_formatted(double_to_toml(f)?),
798                        None => {
799                            return Err(RError::new(
800                                RErrorKind::Type,
801                                "TOML does not support NA values".to_string(),
802                            ))
803                        }
804                    }
805                }
806                Ok(toml_edit::Value::Array(arr))
807            }
808        }
809        Vector::Character(v) => {
810            if v.len() == 1 {
811                match &v[0] {
812                    Some(s) => Ok(toml_edit::Value::from(s.as_str())),
813                    None => Err(RError::new(
814                        RErrorKind::Type,
815                        "TOML does not support NA values".to_string(),
816                    )),
817                }
818            } else {
819                let mut arr = toml_edit::Array::new();
820                for item in v.iter() {
821                    match item {
822                        Some(s) => arr.push_formatted(toml_edit::Value::from(s.as_str())),
823                        None => {
824                            return Err(RError::new(
825                                RErrorKind::Type,
826                                "TOML does not support NA values".to_string(),
827                            ))
828                        }
829                    }
830                }
831                Ok(toml_edit::Value::Array(arr))
832            }
833        }
834        Vector::Complex(_) => Err(RError::new(
835            RErrorKind::Type,
836            "cannot convert complex numbers to TOML".to_string(),
837        )),
838        Vector::Raw(_) => Err(RError::new(
839            RErrorKind::Type,
840            "cannot convert raw bytes to TOML".to_string(),
841        )),
842    }
843}
844
845/// Convert an f64 to a TOML value, handling special float values.
846fn double_to_toml(f: f64) -> Result<toml_edit::Value, RError> {
847    if f.is_nan() {
848        Ok(toml_edit::Value::from(f64::NAN))
849    } else if f.is_infinite() {
850        if f.is_sign_positive() {
851            Ok(toml_edit::Value::from(f64::INFINITY))
852        } else {
853            Ok(toml_edit::Value::from(f64::NEG_INFINITY))
854        }
855    } else {
856        Ok(toml_edit::Value::from(f))
857    }
858}
859
860// endregion