use crate::kernel::expr::{ExprData, PredicateKind};
use crate::kernel::{ExprId, ExprPool};
const PREC_ADD: i32 = 10;
const PREC_MUL: i32 = 20;
const PREC_POW: i32 = 30;
const PREC_ATOM: i32 = 100;
pub fn render_latex(id: ExprId, pool: &ExprPool) -> String {
latex_r(id, pool).0
}
pub fn render_unicode(id: ExprId, pool: &ExprPool) -> String {
unicode_r(id, pool).0
}
fn greek_latex(name: &str) -> Option<&'static str> {
match name {
"alpha" => Some(r"\alpha"),
"beta" => Some(r"\beta"),
"gamma" => Some(r"\gamma"),
"delta" => Some(r"\delta"),
"epsilon" => Some(r"\epsilon"),
"zeta" => Some(r"\zeta"),
"eta" => Some(r"\eta"),
"theta" => Some(r"\theta"),
"iota" => Some(r"\iota"),
"kappa" => Some(r"\kappa"),
"lamda" | "lambda" => Some(r"\lambda"),
"mu" => Some(r"\mu"),
"nu" => Some(r"\nu"),
"xi" => Some(r"\xi"),
"pi" => Some(r"\pi"),
"rho" => Some(r"\rho"),
"sigma" => Some(r"\sigma"),
"tau" => Some(r"\tau"),
"upsilon" => Some(r"\upsilon"),
"phi" => Some(r"\phi"),
"chi" => Some(r"\chi"),
"psi" => Some(r"\psi"),
"omega" => Some(r"\omega"),
"Alpha" => Some(r"\Alpha"),
"Beta" => Some(r"\Beta"),
"Gamma" => Some(r"\Gamma"),
"Delta" => Some(r"\Delta"),
"Epsilon" => Some(r"\Epsilon"),
"Zeta" => Some(r"\Zeta"),
"Eta" => Some(r"\Eta"),
"Theta" => Some(r"\Theta"),
"Iota" => Some(r"\Iota"),
"Kappa" => Some(r"\Kappa"),
"Lambda" => Some(r"\Lambda"),
"Mu" => Some(r"\Mu"),
"Nu" => Some(r"\Nu"),
"Xi" => Some(r"\Xi"),
"Pi" => Some(r"\Pi"),
"Rho" => Some(r"\Rho"),
"Sigma" => Some(r"\Sigma"),
"Tau" => Some(r"\Tau"),
"Upsilon" => Some(r"\Upsilon"),
"Phi" => Some(r"\Phi"),
"Chi" => Some(r"\Chi"),
"Psi" => Some(r"\Psi"),
"Omega" => Some(r"\Omega"),
"inf" | "oo" => Some(r"\infty"),
_ => None,
}
}
fn greek_unicode(name: &str) -> Option<&'static str> {
match name {
"alpha" => Some("α"),
"beta" => Some("β"),
"gamma" => Some("γ"),
"delta" => Some("δ"),
"epsilon" => Some("ε"),
"zeta" => Some("ζ"),
"eta" => Some("η"),
"theta" => Some("θ"),
"iota" => Some("ι"),
"kappa" => Some("κ"),
"lamda" | "lambda" => Some("λ"),
"mu" => Some("μ"),
"nu" => Some("ν"),
"xi" => Some("ξ"),
"pi" => Some("π"),
"rho" => Some("ρ"),
"sigma" => Some("σ"),
"tau" => Some("τ"),
"upsilon" => Some("υ"),
"phi" => Some("φ"),
"chi" => Some("χ"),
"psi" => Some("ψ"),
"omega" => Some("ω"),
"Alpha" => Some("Α"),
"Beta" => Some("Β"),
"Gamma" => Some("Γ"),
"Delta" => Some("Δ"),
"Epsilon" => Some("Ε"),
"Zeta" => Some("Ζ"),
"Eta" => Some("Η"),
"Theta" => Some("Θ"),
"Iota" => Some("Ι"),
"Kappa" => Some("Κ"),
"Lambda" => Some("Λ"),
"Mu" => Some("Μ"),
"Nu" => Some("Ν"),
"Xi" => Some("Ξ"),
"Pi" => Some("Π"),
"Rho" => Some("Ρ"),
"Sigma" => Some("Σ"),
"Tau" => Some("Τ"),
"Upsilon" => Some("Υ"),
"Phi" => Some("Φ"),
"Chi" => Some("Χ"),
"Psi" => Some("Ψ"),
"Omega" => Some("Ω"),
"inf" | "oo" => Some("∞"),
_ => None,
}
}
fn to_superscript(s: &str) -> Option<String> {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
let sup = match ch {
'0' => '⁰',
'1' => '¹',
'2' => '²',
'3' => '³',
'4' => '⁴',
'5' => '⁵',
'6' => '⁶',
'7' => '⁷',
'8' => '⁸',
'9' => '⁹',
'+' => '⁺',
'-' => '⁻',
_ => return None,
};
out.push(sup);
}
Some(out)
}
fn unicode_frac(num: i64, den: i64) -> String {
match (num, den) {
(1, 2) => "½".into(),
(1, 3) => "⅓".into(),
(2, 3) => "⅔".into(),
(1, 4) => "¼".into(),
(3, 4) => "¾".into(),
(1, 5) => "⅕".into(),
(2, 5) => "⅖".into(),
(3, 5) => "⅗".into(),
(4, 5) => "⅘".into(),
(1, 6) => "⅙".into(),
(5, 6) => "⅚".into(),
(1, 7) => "⅐".into(),
(1, 8) => "⅛".into(),
(3, 8) => "⅜".into(),
(5, 8) => "⅝".into(),
(7, 8) => "⅞".into(),
(1, 9) => "⅑".into(),
(1, 10) => "⅒".into(),
_ => format!("{num}/{den}"),
}
}
fn latex_frac(num: &str, den: &str) -> String {
format!(r"\frac{{{num}}}{{{den}}}")
}
fn latex_symbol(name: &str) -> String {
if let Some(g) = greek_latex(name) {
return g.to_string();
}
if let Some((base, sub)) = name.split_once('_') {
return format!("{{{base}}}_{{{sub}}}");
}
name.to_string()
}
fn latex_wrap(id: ExprId, pool: &ExprPool, req_prec: i32) -> String {
let (s, prec) = latex_r(id, pool);
if prec < req_prec {
format!(r"\left({s}\right)")
} else {
s
}
}
fn latex_signed(id: ExprId, pool: &ExprPool) -> (i32, String) {
pool.with(id, |data| match data {
ExprData::Integer(n) => {
let v = n.0.to_i64().unwrap_or(0);
if v < 0 {
(-1, (-v).to_string())
} else {
(1, v.to_string())
}
}
ExprData::Rational(r) => {
let num = r.0.numer();
let den = r.0.denom();
let s = latex_frac(num.to_string().trim_start_matches('-'), &den.to_string());
if *num < 0 {
(-1, s)
} else {
(1, s)
}
}
ExprData::Mul(args) => latex_signed_mul(args, pool),
_ => {
let (s, _) = latex_r(id, pool);
(1, s)
}
})
}
fn latex_signed_mul(args: &[ExprId], pool: &ExprPool) -> (i32, String) {
let mut numer_i = 1i64;
let mut denom_i = 1i64;
let mut others: Vec<ExprId> = Vec::new();
for &child in args {
pool.with(child, |data| match data {
ExprData::Integer(n) => {
numer_i *= n.0.to_i64().unwrap_or(1);
}
ExprData::Rational(r) => {
numer_i *= r.0.numer().to_i64().unwrap_or(1);
denom_i *= r.0.denom().to_i64().unwrap_or(1);
}
_ => others.push(child),
});
}
let sign = if numer_i < 0 {
numer_i = -numer_i;
-1i32
} else {
1i32
};
let sign = if denom_i < 0 {
denom_i = -denom_i;
-sign
} else {
sign
};
let mut num_parts: Vec<String> = Vec::new();
let mut den_parts: Vec<String> = Vec::new();
for &child in &others {
let pushed = pool.with(child, |data| {
if let ExprData::Pow { base, exp } = data {
if let ExprData::Integer(n) = pool.get(*exp) {
let v = n.0.to_i64().unwrap_or(0);
if v < 0 {
let exp_abs = -v;
let base_tex = latex_wrap(*base, pool, PREC_POW + 1);
if exp_abs == 1 {
den_parts.push(base_tex);
} else {
den_parts.push(format!("{{{base_tex}}}^{{{exp_abs}}}"));
}
return true;
}
}
}
false
});
if !pushed {
num_parts.push(latex_wrap(child, pool, PREC_MUL));
}
}
if numer_i != 1 || denom_i != 1 {
let coeff = if denom_i != 1 {
latex_frac(&numer_i.to_string(), &denom_i.to_string())
} else {
numer_i.to_string()
};
num_parts.insert(0, coeff);
}
if num_parts.is_empty() && den_parts.is_empty() {
return (sign, "1".into());
}
if !den_parts.is_empty() {
let num_str = if num_parts.is_empty() {
"1".into()
} else {
num_parts.join(" ")
};
let den_str = den_parts.join(" ");
return (sign, latex_frac(&num_str, &den_str));
}
(sign, num_parts.join(" "))
}
fn latex_add(args: &[ExprId], pool: &ExprPool) -> String {
let mut parts: Vec<String> = Vec::new();
for &child in args {
let (sign, tex) = latex_signed(child, pool);
if parts.is_empty() {
if sign < 0 {
parts.push(format!("-{tex}"));
} else {
parts.push(tex);
}
} else if sign < 0 {
parts.push(format!(" - {tex}"));
} else {
parts.push(format!(" + {tex}"));
}
}
parts.concat()
}
fn latex_pow(base: ExprId, exp: ExprId, pool: &ExprPool) -> String {
if let ExprData::Rational(r) = pool.get(exp) {
let num = r.0.numer().to_i64().unwrap_or(0);
let den = r.0.denom().to_i64().unwrap_or(1);
if num == 1 && den >= 2 {
let base_tex = latex_wrap(base, pool, PREC_POW + 1);
return if den == 2 {
format!(r"\sqrt{{{base_tex}}}")
} else {
format!(r"\sqrt[{den}]{{{base_tex}}}")
};
}
}
if let ExprData::Integer(n) = pool.get(exp) {
if n.0.to_i64() == Some(-1) {
let base_tex = latex_wrap(base, pool, PREC_POW + 1);
return latex_frac("1", &base_tex);
}
}
let base_tex = latex_wrap(base, pool, PREC_POW);
let (exp_tex, _) = latex_r(exp, pool);
let exp_braced = if exp_tex.len() == 1 {
exp_tex
} else {
format!("{{{exp_tex}}}")
};
format!("{base_tex}^{exp_braced}")
}
fn latex_func(name: &str, args: &[ExprId], pool: &ExprPool) -> String {
match name {
"abs" => {
let (inner, _) = latex_r(args[0], pool);
format!(r"\left|{inner}\right|")
}
"floor" => {
let (inner, _) = latex_r(args[0], pool);
format!(r"\lfloor {inner} \rfloor")
}
"ceil" => {
let (inner, _) = latex_r(args[0], pool);
format!(r"\lceil {inner} \rceil")
}
"sqrt" => {
let (inner, _) = latex_r(args[0], pool);
format!(r"\sqrt{{{inner}}}")
}
"exp" => {
let (inner, _) = latex_r(args[0], pool);
let exp_braced = if inner.len() == 1 {
inner
} else {
format!("{{{inner}}}")
};
format!(r"e^{exp_braced}")
}
_ => {
let fn_latex = latex_func_name(name);
let rendered: Vec<String> = args.iter().map(|&a| latex_r(a, pool).0).collect();
format!(r"{fn_latex}\!\left({}\right)", rendered.join(", "))
}
}
}
fn latex_func_name(name: &str) -> String {
match name {
"sin" => r"\sin".into(),
"cos" => r"\cos".into(),
"tan" => r"\tan".into(),
"sinh" => r"\sinh".into(),
"cosh" => r"\cosh".into(),
"tanh" => r"\tanh".into(),
"asin" => r"\arcsin".into(),
"acos" => r"\arccos".into(),
"atan" => r"\arctan".into(),
"log" => r"\ln".into(),
"sign" => r"\operatorname{sign}".into(),
"round" => r"\operatorname{round}".into(),
"erf" => r"\operatorname{erf}".into(),
"erfc" => r"\operatorname{erfc}".into(),
"gamma" => r"\Gamma".into(),
other => format!(r"\operatorname{{{other}}}"),
}
}
fn latex_predicate(kind: &PredicateKind, args: &[ExprId], pool: &ExprPool) -> String {
match kind {
PredicateKind::True => r"\top".into(),
PredicateKind::False => r"\bot".into(),
PredicateKind::Not => {
let (inner, _) = latex_r(args[0], pool);
format!(r"\lnot {inner}")
}
_ => {
let op = match kind {
PredicateKind::Lt => "<",
PredicateKind::Le => r"\le",
PredicateKind::Gt => ">",
PredicateKind::Ge => r"\ge",
PredicateKind::Eq => "=",
PredicateKind::Ne => r"\ne",
PredicateKind::And => r"\land",
PredicateKind::Or => r"\lor",
_ => unreachable!(),
};
let rendered: Vec<String> = args.iter().map(|&a| latex_r(a, pool).0).collect();
rendered.join(&format!(" {op} "))
}
}
}
fn latex_piecewise(branches: &[(ExprId, ExprId)], default: ExprId, pool: &ExprPool) -> String {
let mut rows: Vec<String> = Vec::new();
for &(cond, val) in branches {
let (val_tex, _) = latex_r(val, pool);
let (cond_tex, _) = latex_r(cond, pool);
rows.push(format!(r"{val_tex} & \text{{if }} {cond_tex}"));
}
let (def_tex, _) = latex_r(default, pool);
rows.push(format!(r"{def_tex} & \text{{otherwise}}"));
format!(r"\begin{{cases}} {} \end{{cases}}", rows.join(r" \\ "))
}
fn latex_r(id: ExprId, pool: &ExprPool) -> (String, i32) {
pool.with(id, |data| match data {
ExprData::Symbol { name, .. } => (latex_symbol(name), PREC_ATOM),
ExprData::Integer(n) => (n.0.to_string(), PREC_ATOM),
ExprData::Rational(r) => {
let num = r.0.numer();
let den = r.0.denom();
let s = latex_frac(num.to_string().trim_start_matches('-'), &den.to_string());
if *num < 0 {
(format!("-{s}"), PREC_ATOM)
} else {
(s, PREC_ATOM)
}
}
ExprData::Float(f) => (f.inner.to_string(), PREC_ATOM),
ExprData::Add(args) => (latex_add(args, pool), PREC_ADD),
ExprData::Mul(args) => {
let (sign, tex) = latex_signed_mul(args, pool);
let s = if sign < 0 { format!("-{tex}") } else { tex };
(s, PREC_MUL)
}
ExprData::Pow { base, exp } => (latex_pow(*base, *exp, pool), PREC_POW),
ExprData::Func { name, args } => (latex_func(name, args, pool), PREC_ATOM),
ExprData::Piecewise { branches, default } => {
(latex_piecewise(branches, *default, pool), PREC_ATOM)
}
ExprData::Predicate { kind, args } => (latex_predicate(kind, args, pool), PREC_ADD),
ExprData::Forall { var, body } => {
let (v, _) = latex_r(*var, pool);
let (b, _) = latex_r(*body, pool);
(format!(r"\forall {v} \, . \, {b}"), PREC_ATOM)
}
ExprData::Exists { var, body } => {
let (v, _) = latex_r(*var, pool);
let (b, _) = latex_r(*body, pool);
(format!(r"\exists {v} \, . \, {b}"), PREC_ATOM)
}
ExprData::BigO(arg) => {
let (a, _) = latex_r(*arg, pool);
(format!(r"\mathcal{{O}}\!\left({a}\right)"), PREC_ATOM)
}
})
}
fn unicode_symbol(name: &str) -> String {
if let Some(g) = greek_unicode(name) {
return g.to_string();
}
name.to_string()
}
fn unicode_wrap(id: ExprId, pool: &ExprPool, req_prec: i32) -> String {
let (s, prec) = unicode_r(id, pool);
if prec < req_prec {
format!("({s})")
} else {
s
}
}
fn unicode_signed(id: ExprId, pool: &ExprPool) -> (i32, String) {
pool.with(id, |data| match data {
ExprData::Integer(n) => {
let v = n.0.to_i64().unwrap_or(0);
if v < 0 {
(-1, (-v).to_string())
} else {
(1, v.to_string())
}
}
ExprData::Rational(r) => {
let num = r.0.numer().to_i64().unwrap_or(0);
let den = r.0.denom().to_i64().unwrap_or(1);
if num < 0 {
(-1, unicode_frac(-num, den))
} else {
(1, unicode_frac(num, den))
}
}
ExprData::Mul(args) => unicode_signed_mul(args, pool),
_ => {
let (s, _) = unicode_r(id, pool);
(1, s)
}
})
}
fn unicode_signed_mul(args: &[ExprId], pool: &ExprPool) -> (i32, String) {
let mut numer_i = 1i64;
let mut denom_i = 1i64;
let mut others: Vec<ExprId> = Vec::new();
for &child in args {
pool.with(child, |data| match data {
ExprData::Integer(n) => {
numer_i *= n.0.to_i64().unwrap_or(1);
}
ExprData::Rational(r) => {
numer_i *= r.0.numer().to_i64().unwrap_or(1);
denom_i *= r.0.denom().to_i64().unwrap_or(1);
}
_ => others.push(child),
});
}
let sign = if numer_i < 0 {
numer_i = -numer_i;
-1i32
} else {
1i32
};
let sign = if denom_i < 0 {
denom_i = -denom_i;
-sign
} else {
sign
};
let mut num_parts: Vec<String> = Vec::new();
let mut den_parts: Vec<String> = Vec::new();
for &child in &others {
let pushed = pool.with(child, |data| {
if let ExprData::Pow { base, exp } = data {
if let ExprData::Integer(n) = pool.get(*exp) {
let v = n.0.to_i64().unwrap_or(0);
if v < 0 {
let exp_abs = -v;
let base_tex = unicode_wrap(*base, pool, PREC_POW + 1);
let sup = to_superscript(&exp_abs.to_string());
den_parts.push(match sup {
Some(s) => format!("{base_tex}{s}"),
None => format!("{base_tex}^{exp_abs}"),
});
return true;
}
}
}
false
});
if !pushed {
num_parts.push(unicode_wrap(child, pool, PREC_MUL));
}
}
if numer_i != 1 || denom_i != 1 {
let coeff = if denom_i != 1 {
unicode_frac(numer_i, denom_i)
} else {
numer_i.to_string()
};
num_parts.insert(0, coeff);
}
if num_parts.is_empty() && den_parts.is_empty() {
return (sign, "1".into());
}
if !den_parts.is_empty() {
let num_str = if num_parts.is_empty() {
"1".into()
} else {
num_parts.join("·")
};
let den_str = den_parts.join("·");
let s = if den_parts.len() > 1 {
format!("({num_str})/({den_str})")
} else {
format!("{num_str}/{den_str}")
};
return (sign, s);
}
(sign, num_parts.join("·"))
}
fn unicode_add(args: &[ExprId], pool: &ExprPool) -> String {
let mut parts: Vec<String> = Vec::new();
for &child in args {
let (sign, tex) = unicode_signed(child, pool);
if parts.is_empty() {
if sign < 0 {
parts.push(format!("-{tex}"));
} else {
parts.push(tex);
}
} else if sign < 0 {
parts.push(format!(" - {tex}"));
} else {
parts.push(format!(" + {tex}"));
}
}
parts.concat()
}
fn unicode_pow(base: ExprId, exp: ExprId, pool: &ExprPool) -> String {
if let ExprData::Rational(r) = pool.get(exp) {
let num = r.0.numer().to_i64().unwrap_or(0);
let den = r.0.denom().to_i64().unwrap_or(1);
if num == 1 {
let base_tex = unicode_wrap(base, pool, PREC_POW + 1);
return match den {
2 => format!("√{base_tex}"),
3 => format!("∛{base_tex}"),
4 => format!("∜{base_tex}"),
_ => {
let sup = to_superscript(&den.to_string())
.map(|s| format!("{}√{base_tex}", s))
.unwrap_or_else(|| format!("{base_tex}^(1/{den})"));
sup
}
};
}
}
let base_tex = unicode_wrap(base, pool, PREC_POW);
if let ExprData::Integer(n) = pool.get(exp) {
if let Some(v) = n.0.to_i64() {
if v == 1 {
return base_tex;
}
if let Some(sup) = to_superscript(&v.to_string()) {
return format!("{base_tex}{sup}");
}
}
}
let (exp_tex, _) = unicode_r(exp, pool);
format!("{base_tex}^({exp_tex})")
}
fn unicode_func(name: &str, args: &[ExprId], pool: &ExprPool) -> String {
match name {
"sqrt" => {
let inner = unicode_wrap(args[0], pool, PREC_POW + 1);
format!("√{inner}")
}
"abs" => {
let (inner, _) = unicode_r(args[0], pool);
format!("|{inner}|")
}
"floor" => {
let (inner, _) = unicode_r(args[0], pool);
format!("⌊{inner}⌋")
}
"ceil" => {
let (inner, _) = unicode_r(args[0], pool);
format!("⌈{inner}⌉")
}
"exp" => {
let (inner, _) = unicode_r(args[0], pool);
if let Some(sup) = to_superscript(&inner) {
format!("e{sup}")
} else {
format!("e^({inner})")
}
}
_ => {
let fn_name = match name {
"gamma" => "Γ",
"asin" => "arcsin",
"acos" => "arccos",
"atan" => "arctan",
"log" => "ln",
other => other,
};
let rendered: Vec<String> = args.iter().map(|&a| unicode_r(a, pool).0).collect();
format!("{fn_name}({})", rendered.join(", "))
}
}
}
fn unicode_predicate(kind: &PredicateKind, args: &[ExprId], pool: &ExprPool) -> String {
match kind {
PredicateKind::True => "⊤".into(),
PredicateKind::False => "⊥".into(),
PredicateKind::Not => {
let (inner, _) = unicode_r(args[0], pool);
format!("¬{inner}")
}
_ => {
let op = match kind {
PredicateKind::Lt => "<",
PredicateKind::Le => "≤",
PredicateKind::Gt => ">",
PredicateKind::Ge => "≥",
PredicateKind::Eq => "=",
PredicateKind::Ne => "≠",
PredicateKind::And => "∧",
PredicateKind::Or => "∨",
_ => unreachable!(),
};
let rendered: Vec<String> = args.iter().map(|&a| unicode_r(a, pool).0).collect();
rendered.join(&format!(" {op} "))
}
}
}
fn unicode_piecewise(branches: &[(ExprId, ExprId)], default: ExprId, pool: &ExprPool) -> String {
let mut rows: Vec<String> = Vec::new();
for &(cond, val) in branches {
let (val_tex, _) = unicode_r(val, pool);
let (cond_tex, _) = unicode_r(cond, pool);
rows.push(format!("{val_tex} if {cond_tex}"));
}
let (def_tex, _) = unicode_r(default, pool);
rows.push(format!("{def_tex} otherwise"));
format!("{{ {}", rows.join("\n "))
}
fn unicode_r(id: ExprId, pool: &ExprPool) -> (String, i32) {
pool.with(id, |data| match data {
ExprData::Symbol { name, .. } => (unicode_symbol(name), PREC_ATOM),
ExprData::Integer(n) => (n.0.to_string(), PREC_ATOM),
ExprData::Rational(r) => {
let num = r.0.numer().to_i64().unwrap_or(0);
let den = r.0.denom().to_i64().unwrap_or(1);
let s = unicode_frac(num.abs(), den);
if num < 0 {
(format!("-{s}"), PREC_ATOM)
} else {
(s, PREC_ATOM)
}
}
ExprData::Float(f) => (f.inner.to_string(), PREC_ATOM),
ExprData::Add(args) => (unicode_add(args, pool), PREC_ADD),
ExprData::Mul(args) => {
let (sign, tex) = unicode_signed_mul(args, pool);
let s = if sign < 0 { format!("-{tex}") } else { tex };
(s, PREC_MUL)
}
ExprData::Pow { base, exp } => (unicode_pow(*base, *exp, pool), PREC_POW),
ExprData::Func { name, args } => (unicode_func(name, args, pool), PREC_ATOM),
ExprData::Piecewise { branches, default } => {
(unicode_piecewise(branches, *default, pool), PREC_ATOM)
}
ExprData::Predicate { kind, args } => (unicode_predicate(kind, args, pool), PREC_ADD),
ExprData::Forall { var, body } => {
let (v, _) = unicode_r(*var, pool);
let (b, _) = unicode_r(*body, pool);
(format!("∀{v}.{b}"), PREC_ATOM)
}
ExprData::Exists { var, body } => {
let (v, _) = unicode_r(*var, pool);
let (b, _) = unicode_r(*body, pool);
(format!("∃{v}.{b}"), PREC_ATOM)
}
ExprData::BigO(arg) => {
let (a, _) = unicode_r(*arg, pool);
(format!("O({a})"), PREC_ATOM)
}
})
}