use std::fmt::Write as _;
use crate::rindex::rds::{Rkind, Robj};
const MAX_DEPTH: usize = 64;
pub fn deparse(obj: &Robj) -> Option<String> {
if matches!(obj.kind, Rkind::Missing) {
return None;
}
let mut out = String::new();
write_obj(&mut out, obj, 0);
Some(out)
}
fn write_obj(out: &mut String, obj: &Robj, depth: usize) {
if depth > MAX_DEPTH {
out.push_str("NULL");
return;
}
match &obj.kind {
Rkind::Null => out.push_str("NULL"),
Rkind::Missing => {}
Rkind::Logical(v) => write_vec(out, v, "logical(0)", |out, x| {
out.push_str(match x {
Some(true) => "TRUE",
Some(false) => "FALSE",
None => "NA",
});
}),
Rkind::Int(v) => write_vec(out, v, "integer(0)", |out, x| match x {
Some(i) => {
let _ = write!(out, "{i}L");
}
None => out.push_str("NA_integer_"),
}),
Rkind::Real(v) => write_vec(out, v, "numeric(0)", |out, x| write_real(out, *x)),
Rkind::Str(v) => write_vec(out, v, "character(0)", |out, x| match x {
Some(s) => write_string_lit(out, s),
None => out.push_str("NA_character_"),
}),
Rkind::Symbol(s) => write_name(out, s),
Rkind::List(v) => write_list(out, obj, v, depth),
Rkind::Pairlist(items) => write_call(out, items, depth),
Rkind::Closure { .. } | Rkind::Builtin | Rkind::Env | Rkind::Opaque => out.push_str("NULL"),
}
}
fn write_vec<T>(out: &mut String, v: &[T], empty: &str, one: impl Fn(&mut String, &T)) {
match v {
[] => out.push_str(empty),
[x] => one(out, x),
_ => {
out.push_str("c(");
for (i, x) in v.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
one(out, x);
}
out.push(')');
}
}
}
fn write_real(out: &mut String, x: f64) {
if x.is_nan() {
out.push_str("NaN");
} else if x.is_infinite() {
out.push_str(if x < 0.0 { "-Inf" } else { "Inf" });
} else if x == x.trunc() && x.abs() < 1e15 {
let i = x as i64;
let _ = write!(out, "{i}");
} else {
let _ = write!(out, "{x}");
}
}
fn write_string_lit(out: &mut String, s: &str) {
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
_ => out.push(c),
}
}
out.push('"');
}
fn write_name(out: &mut String, name: &str) {
if is_syntactic(name) {
out.push_str(name);
} else {
out.push('`');
out.push_str(name);
out.push('`');
}
}
fn is_syntactic(name: &str) -> bool {
if name.is_empty() {
return true;
}
let mut chars = name.chars();
let first = chars.next().unwrap();
if !(first.is_ascii_alphabetic() || first == '.') {
return false;
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
{
return false;
}
!is_reserved(name)
}
fn is_reserved(name: &str) -> bool {
matches!(
name,
"if" | "else"
| "repeat"
| "while"
| "function"
| "for"
| "in"
| "next"
| "break"
| "TRUE"
| "FALSE"
| "NULL"
| "Inf"
| "NaN"
| "NA"
| "NA_integer_"
| "NA_real_"
| "NA_character_"
| "NA_complex_"
)
}
fn write_list(out: &mut String, obj: &Robj, v: &[Robj], depth: usize) {
out.push_str("list(");
let names = obj.names();
for (i, el) in v.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
if let Some(Some(nm)) = names.as_ref().and_then(|ns| ns.get(i))
&& !nm.is_empty()
{
write_name(out, nm);
out.push_str(" = ");
}
write_obj(out, el, depth + 1);
}
out.push(')');
}
fn write_call(out: &mut String, items: &[crate::rindex::rds::PairlistItem], depth: usize) {
let Some((head, args)) = items.split_first() else {
out.push_str("NULL");
return;
};
if let Rkind::Symbol(op) = &head.value.kind
&& write_operator(out, op, args, depth)
{
return;
}
write_head(out, &head.value, depth);
out.push('(');
write_args(out, args, depth);
out.push(')');
}
type Item = crate::rindex::rds::PairlistItem;
fn write_operator(out: &mut String, op: &str, args: &[Item], depth: usize) -> bool {
match op {
"$" | "@" if args.len() == 2 => {
write_operand(out, &args[0].value, depth);
out.push_str(op);
write_obj(out, &args[1].value, depth + 1);
true
}
"::" | ":::" if args.len() == 2 => {
write_obj(out, &args[0].value, depth + 1);
out.push_str(op);
write_obj(out, &args[1].value, depth + 1);
true
}
"[" if !args.is_empty() => {
write_operand(out, &args[0].value, depth);
out.push('[');
write_args(out, &args[1..], depth);
out.push(']');
true
}
"[[" if !args.is_empty() => {
write_operand(out, &args[0].value, depth);
out.push_str("[[");
write_args(out, &args[1..], depth);
out.push_str("]]");
true
}
"(" if args.len() == 1 => {
out.push('(');
write_obj(out, &args[0].value, depth + 1);
out.push(')');
true
}
"{" => {
out.push_str("{ ");
for (i, a) in args.iter().enumerate() {
if i > 0 {
out.push_str("; ");
}
write_obj(out, &a.value, depth + 1);
}
out.push_str(" }");
true
}
"if" if args.len() >= 2 => {
out.push_str("if (");
write_obj(out, &args[0].value, depth + 1);
out.push_str(") ");
write_obj(out, &args[1].value, depth + 1);
if let Some(else_branch) = args.get(2) {
out.push_str(" else ");
write_obj(out, &else_branch.value, depth + 1);
}
true
}
"for" if args.len() == 3 => {
out.push_str("for (");
write_obj(out, &args[0].value, depth + 1);
out.push_str(" in ");
write_obj(out, &args[1].value, depth + 1);
out.push_str(") ");
write_obj(out, &args[2].value, depth + 1);
true
}
"while" if args.len() == 2 => {
out.push_str("while (");
write_obj(out, &args[0].value, depth + 1);
out.push_str(") ");
write_obj(out, &args[1].value, depth + 1);
true
}
"function" if args.len() >= 2 => {
out.push_str("function(");
write_formals(out, &args[0].value, depth);
out.push_str(") ");
write_obj(out, &args[1].value, depth + 1);
true
}
"!" if args.len() == 1 => {
out.push('!');
write_operand(out, &args[0].value, depth);
true
}
"-" | "+" if args.len() == 1 => {
out.push_str(op);
write_operand(out, &args[0].value, depth);
true
}
"^" | ":" if args.len() == 2 => {
write_operand(out, &args[0].value, depth);
out.push_str(op);
write_operand(out, &args[1].value, depth);
true
}
_ if args.len() == 2 && is_binary_spaced(op) => {
write_operand(out, &args[0].value, depth);
out.push(' ');
out.push_str(op);
out.push(' ');
write_operand(out, &args[1].value, depth);
true
}
_ => false,
}
}
fn is_binary_spaced(op: &str) -> bool {
matches!(
op,
"+" | "-"
| "*"
| "/"
| "%%"
| "%/%"
| "=="
| "!="
| "<"
| ">"
| "<="
| ">="
| "&"
| "&&"
| "|"
| "||"
| "<-"
| "<<-"
| "="
| "->"
| "->>"
| "~"
) || (op.starts_with('%') && op.ends_with('%') && op.len() >= 2)
}
fn write_operand(out: &mut String, obj: &Robj, depth: usize) {
if needs_parens(obj) {
out.push('(');
write_obj(out, obj, depth + 1);
out.push(')');
} else {
write_obj(out, obj, depth + 1);
}
}
fn needs_parens(obj: &Robj) -> bool {
let Rkind::Pairlist(items) = &obj.kind else {
return false;
};
let Some(head) = items.first() else {
return false;
};
let Rkind::Symbol(op) = &head.value.kind else {
return false;
};
let args = items.len() - 1;
match op.as_str() {
"!" | "-" | "+" if args == 1 => true,
"^" | ":" if args == 2 => true,
_ => args == 2 && is_binary_spaced(op),
}
}
fn write_head(out: &mut String, head: &Robj, depth: usize) {
match &head.kind {
Rkind::Symbol(s) => write_name(out, s),
_ => write_obj(out, head, depth + 1),
}
}
fn write_args(out: &mut String, args: &[Item], depth: usize) {
for (i, a) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
if let Some(tag) = &a.tag {
write_name(out, tag);
out.push_str(" = ");
}
write_obj(out, &a.value, depth + 1);
}
}
fn write_formals(out: &mut String, obj: &Robj, depth: usize) {
let Rkind::Pairlist(items) = &obj.kind else {
return;
};
for (i, it) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_name(out, it.tag.as_deref().unwrap_or(""));
if !matches!(it.value.kind, Rkind::Missing) {
out.push_str(" = ");
write_obj(out, &it.value, depth + 1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rindex::rds::PairlistItem;
use smol_str::SmolStr;
fn bare(kind: Rkind) -> Robj {
Robj {
kind,
attr: Vec::new(),
}
}
fn sym(name: &str) -> Robj {
bare(Rkind::Symbol(SmolStr::new(name)))
}
fn call(parts: Vec<(Option<&str>, Robj)>) -> Robj {
bare(Rkind::Pairlist(
parts
.into_iter()
.map(|(tag, value)| PairlistItem {
tag: tag.map(SmolStr::new),
value,
})
.collect(),
))
}
fn d(obj: &Robj) -> Option<String> {
deparse(obj)
}
#[test]
fn scalars_and_na() {
assert_eq!(d(&bare(Rkind::Missing)), None);
assert_eq!(d(&bare(Rkind::Null)).as_deref(), Some("NULL"));
assert_eq!(
d(&bare(Rkind::Logical(vec![Some(true)]))).as_deref(),
Some("TRUE")
);
assert_eq!(d(&bare(Rkind::Logical(vec![None]))).as_deref(), Some("NA"));
assert_eq!(d(&bare(Rkind::Int(vec![Some(1)]))).as_deref(), Some("1L"));
assert_eq!(
d(&bare(Rkind::Int(vec![None]))).as_deref(),
Some("NA_integer_")
);
assert_eq!(d(&bare(Rkind::Real(vec![1.0]))).as_deref(), Some("1"));
assert_eq!(d(&bare(Rkind::Real(vec![2.5]))).as_deref(), Some("2.5"));
assert_eq!(
d(&bare(Rkind::Str(vec![None]))).as_deref(),
Some("NA_character_")
);
}
#[test]
fn string_escaping() {
let s = bare(Rkind::Str(vec![Some("a\"b\\c".to_string())]));
assert_eq!(d(&s).as_deref(), Some(r#""a\"b\\c""#));
}
#[test]
fn vectors_use_c() {
let v = bare(Rkind::Real(vec![1.0, 2.0]));
assert_eq!(d(&v).as_deref(), Some("c(1, 2)"));
let iv = bare(Rkind::Int(vec![Some(1), Some(2)]));
assert_eq!(d(&iv).as_deref(), Some("c(1L, 2L)"));
}
#[test]
fn symbols_and_backticks() {
assert_eq!(d(&sym("foo")).as_deref(), Some("foo"));
assert_eq!(d(&sym("a b")).as_deref(), Some("`a b`"));
}
#[test]
fn generic_call_with_named_arg() {
let c = call(vec![
(None, sym("c")),
(None, bare(Rkind::Real(vec![1.0]))),
(None, bare(Rkind::Real(vec![2.0]))),
]);
assert_eq!(d(&c).as_deref(), Some("c(1, 2)"));
let f = call(vec![
(None, sym("f")),
(None, sym("x")),
(Some("y"), bare(Rkind::Int(vec![Some(1)]))),
]);
assert_eq!(d(&f).as_deref(), Some("f(x, y = 1L)"));
let g = call(vec![
(None, sym("getOption")),
(None, bare(Rkind::Str(vec![Some("foo".to_string())]))),
]);
assert_eq!(d(&g).as_deref(), Some(r#"getOption("foo")"#));
}
#[test]
fn operators() {
let neg = call(vec![(None, sym("-")), (None, bare(Rkind::Real(vec![1.0])))]);
assert_eq!(d(&neg).as_deref(), Some("-1"));
let add = call(vec![
(None, sym("+")),
(None, sym("x")),
(None, bare(Rkind::Real(vec![1.0]))),
]);
assert_eq!(d(&add).as_deref(), Some("x + 1"));
let dollar = call(vec![(None, sym("$")), (None, sym("a")), (None, sym("b"))]);
assert_eq!(d(&dollar).as_deref(), Some("a$b"));
let seq = call(vec![
(None, sym(":")),
(None, bare(Rkind::Int(vec![Some(1)]))),
(None, bare(Rkind::Int(vec![Some(10)]))),
]);
assert_eq!(d(&seq).as_deref(), Some("1L:10L"));
let inop = call(vec![
(None, sym("%in%")),
(None, sym("x")),
(None, sym("y")),
]);
assert_eq!(d(&inop).as_deref(), Some("x %in% y"));
}
#[test]
fn parenthesizes_nested_operators() {
let inner = call(vec![(None, sym("+")), (None, sym("a")), (None, sym("b"))]);
let outer = call(vec![(None, sym("*")), (None, inner), (None, sym("c"))]);
assert_eq!(d(&outer).as_deref(), Some("(a + b) * c"));
}
#[test]
fn pkg_qualified_call() {
let head = call(vec![
(None, sym("::")),
(None, sym("stats")),
(None, sym("rnorm")),
]);
let c = call(vec![(None, head), (None, sym("n"))]);
assert_eq!(d(&c).as_deref(), Some("stats::rnorm(n)"));
}
}