use crate::kernel::expr::{ExprData, PredicateKind};
use crate::kernel::{ExprId, ExprPool};
use rug::{Integer, Rational};
const PREC_QUANT: i32 = 2;
const PREC_OR: i32 = 4;
const PREC_AND: i32 = 6;
const PREC_CMP: i32 = 8;
const PREC_NOT: i32 = 9;
const PREC_ADD: i32 = 10;
const PREC_MUL: i32 = 20;
const PREC_NEG: i32 = 25;
const PREC_POW: i32 = 30;
const PREC_ATOM: i32 = 100;
const PREC_BIGOP: i32 = PREC_ADD;
const PREC_BIGOP_BODY: i32 = PREC_MUL;
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 literal_prec(rendered: &str) -> i32 {
if rendered.starts_with('-') {
PREC_NEG
} else {
PREC_ATOM
}
}
fn float_prec(rendered: &str) -> i32 {
if rendered.contains(['e', 'E']) {
PREC_MUL.min(literal_prec(rendered))
} else {
literal_prec(rendered)
}
}
fn collapse_runs(args: &[ExprId]) -> Vec<(ExprId, i64)> {
let mut out: Vec<(ExprId, i64)> = Vec::with_capacity(args.len());
for &id in args {
match out.last_mut() {
Some((prev, n)) if *prev == id => *n += 1,
_ => out.push((id, 1)),
}
}
out
}
fn rational_as_integer(r: &crate::kernel::expr::BigRat) -> Option<String> {
if *r.0.denom() == 1 {
Some(r.0.numer().to_string())
} else {
None
}
}
fn unicode_frac(num: &Integer, den: &Integer) -> String {
let small = num.to_i64().zip(den.to_i64());
match small.unwrap_or((0, 0)) {
(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 split_coefficient(args: &[ExprId], pool: &ExprPool) -> (i32, Integer, Integer, Vec<ExprId>) {
let mut coeff = Rational::from(1);
let mut others: Vec<ExprId> = Vec::new();
for &child in args {
pool.with(child, |data| match data {
ExprData::Integer(n) => coeff *= &n.0,
ExprData::Rational(r) => coeff *= &r.0,
_ => others.push(child),
});
}
let sign = if coeff < 0 { -1 } else { 1 };
let (numer, denom) = coeff.into_numer_denom();
(sign, numer.abs(), denom, others)
}
fn negative_exponent_abs(exp: ExprId, mult: i64, pool: &ExprPool) -> Option<Integer> {
match pool.get(exp) {
ExprData::Integer(n) if n.0 < 0 => Some(Integer::from(-&n.0) * mult),
_ => None,
}
}
fn latex_frac(num: &str, den: &str) -> String {
format!(r"\frac{{{num}}}{{{den}}}")
}
fn latex_sup(base_tex: &str, exp_tex: &str) -> String {
if exp_tex.chars().count() == 1 {
format!("{base_tex}^{exp_tex}")
} else {
format!("{base_tex}^{{{exp_tex}}}")
}
}
fn latex_float(s: &str) -> (String, i32) {
match s.split_once(['e', 'E']) {
Some((mantissa, exponent)) if !exponent.is_empty() => {
let exponent = exponent.strip_prefix('+').unwrap_or(exponent);
(
format!(r"{mantissa} \times 10^{{{exponent}}}"),
PREC_MUL.min(literal_prec(mantissa)),
)
}
_ => (s.to_string(), literal_prec(s)),
}
}
fn latex_factor(id: ExprId, pool: &ExprPool) -> String {
let (s, prec) = latex_r(id, pool);
if prec < PREC_MUL || s.starts_with('-') {
format!(r"\left({s}\right)")
} else {
s
}
}
fn latex_join_factors(parts: &[String]) -> String {
let mut out = String::new();
for (i, part) in parts.iter().enumerate() {
if i > 0 {
if part.starts_with(|c: char| c.is_ascii_digit() || c == '.') {
out.push_str(r" \cdot ");
} else {
out.push(' ');
}
}
out.push_str(part);
}
out
}
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('_') {
let base_tex = greek_latex(base)
.map(str::to_string)
.unwrap_or_else(|| base.to_string());
return format!("{{{base_tex}}}_{{{sub}}}");
}
if let Some(i) = name.find(|c: char| c.is_ascii_digit()).filter(|&i| {
i > 0
&& name[..i].chars().all(|c| c.is_ascii_alphabetic())
&& name[i..].chars().all(|c| c.is_ascii_digit())
}) {
let base = &name[..i];
let sub = &name[i..];
let base_tex = greek_latex(base)
.map(str::to_string)
.unwrap_or_else(|| base.to_string());
return format!("{{{base_tex}}}_{{{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) => {
if n.0 < 0 {
(-1, n.0.as_abs().to_string())
} else {
(1, n.0.to_string())
}
}
ExprData::Rational(r) => {
let num = r.0.numer();
let den = r.0.denom();
let s = match rational_as_integer(r) {
Some(n) => n.trim_start_matches('-').to_string(),
None => 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);
match s.strip_prefix('-') {
Some(rest) if matches!(data, ExprData::Float(_)) => (-1, rest.to_string()),
_ => (1, s),
}
}
})
}
fn latex_signed_mul(args: &[ExprId], pool: &ExprPool) -> (i32, String) {
let (sign, numer_i, denom_i, others) = split_coefficient(args, pool);
let mut num_parts: Vec<String> = Vec::new();
let mut den_parts: Vec<String> = Vec::new();
for (child, mult) in collapse_runs(&others) {
let pushed = pool.with(child, |data| {
if let ExprData::Pow { base, exp } = data {
if let Some(exp_abs) = negative_exponent_abs(*exp, mult, pool) {
if exp_abs == 1 {
den_parts.push(latex_factor(*base, pool));
} else {
let base_tex = latex_wrap(*base, pool, PREC_POW + 1);
den_parts.push(latex_sup(&base_tex, &exp_abs.to_string()));
}
return true;
}
}
false
});
if !pushed {
if mult > 1 {
let base_tex = latex_wrap(child, pool, PREC_POW + 1);
num_parts.push(latex_sup(&base_tex, &mult.to_string()));
} else {
num_parts.push(latex_factor(child, pool));
}
}
}
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 {
latex_join_factors(&num_parts)
};
let den_str = latex_join_factors(&den_parts);
return (sign, latex_frac(&num_str, &den_str));
}
(sign, latex_join_factors(&num_parts))
}
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, den) = (r.0.numer(), r.0.denom());
if *num == 1 && *den >= 2 {
let (base_tex, _) = latex_r(base, pool);
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 == -1 {
let base_tex = latex_factor(base, pool);
return latex_frac("1", &base_tex);
}
}
let base_tex = latex_wrap(base, pool, PREC_POW + 1);
let (exp_tex, _) = latex_r(exp, pool);
latex_sup(&base_tex, &exp_tex)
}
fn latex_func(name: &str, args: &[ExprId], pool: &ExprPool) -> String {
if !crate::kernel::func_arity_ok(name, args.len()) {
return latex_func_generic(name, args, pool);
}
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);
latex_sup("e", &inner)
}
"EllipticK" => {
let (m, _) = latex_r(args[0], pool);
format!(r"K\!\left({m}\right)")
}
"EllipticE" if args.len() == 1 => {
let (m, _) = latex_r(args[0], pool);
format!(r"E\!\left({m}\right)")
}
"EllipticE" => {
let (phi, _) = latex_r(args[0], pool);
let (m, _) = latex_r(args[1], pool);
format!(r"E\!\left({phi}\,\middle|\,{m}\right)")
}
"EllipticF" => {
let (phi, _) = latex_r(args[0], pool);
let (m, _) = latex_r(args[1], pool);
format!(r"F\!\left({phi}\,\middle|\,{m}\right)")
}
"EllipticPi" => {
let (n, _) = latex_r(args[0], pool);
let (phi, _) = latex_r(args[1], pool);
let (m, _) = latex_r(args[2], pool);
format!(r"\Pi\!\left({n};{phi}\,\middle|\,{m}\right)")
}
_ => latex_func_generic(name, args, pool),
}
}
fn latex_func_generic(name: &str, args: &[ExprId], pool: &ExprPool) -> String {
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_prec(name: &str) -> i32 {
match name {
"exp" => PREC_POW,
_ => PREC_ATOM,
}
}
fn unicode_func_prec(name: &str) -> i32 {
match name {
"exp" | "sqrt" => PREC_POW,
_ => PREC_ATOM,
}
}
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(),
"asinh" => r"\operatorname{arsinh}".into(),
"acosh" => r"\operatorname{arcosh}".into(),
"atanh" => r"\operatorname{artanh}".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(),
"digamma" => r"\psi".into(),
"lambert_w" => r"W".into(),
"bessel_j0" => r"J_0".into(),
"bessel_j1" => r"J_1".into(),
"Ei" => r"\operatorname{Ei}".into(),
"li" => r"\operatorname{li}".into(),
"Si" => r"\operatorname{Si}".into(),
"Ci" => r"\operatorname{Ci}".into(),
"Shi" => r"\operatorname{Shi}".into(),
"Chi" => r"\operatorname{Chi}".into(),
"fresnels" => r"S".into(),
"fresnelc" => r"C".into(),
"trigamma" => r"\psi_1".into(),
"dilog" => r"\operatorname{Li}_2".into(),
other => format!(r"\operatorname{{{}}}", other.replace('_', r"\_")),
}
}
fn predicate_prec(kind: &PredicateKind) -> i32 {
match kind {
PredicateKind::True | PredicateKind::False => PREC_ATOM,
PredicateKind::Not => PREC_NOT,
PredicateKind::And => PREC_AND,
PredicateKind::Or => PREC_OR,
_ => PREC_CMP,
}
}
fn latex_predicate(kind: &PredicateKind, args: &[ExprId], pool: &ExprPool) -> String {
match kind {
PredicateKind::True => r"\top".into(),
PredicateKind::False => r"\bot".into(),
PredicateKind::Not if args.len() == 1 => {
let inner = latex_wrap(args[0], pool, PREC_NOT + 1);
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",
_ => r"\lnot",
};
let req = predicate_prec(kind);
let rendered: Vec<String> = args.iter().map(|&a| latex_wrap(a, pool, req)).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(), literal_prec(&n.0.to_string())),
ExprData::Rational(r) => {
let num = r.0.numer();
let den = r.0.denom();
if let Some(n) = rational_as_integer(r) {
let prec = literal_prec(&n);
return (n, prec);
}
let s = latex_frac(num.to_string().trim_start_matches('-'), &den.to_string());
if *num < 0 {
(format!("-{s}"), PREC_MUL)
} else {
(s, PREC_MUL)
}
}
ExprData::Float(f) => latex_float(&f.to_string()),
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), latex_func_prec(name)),
ExprData::Piecewise { branches, default } => {
(latex_piecewise(branches, *default, pool), PREC_ATOM)
}
ExprData::Predicate { kind, args } => {
(latex_predicate(kind, args, pool), predicate_prec(kind))
}
ExprData::Forall { var, body } => {
let (v, _) = latex_r(*var, pool);
let (b, _) = latex_r(*body, pool);
(format!(r"\forall {v} \, . \, {b}"), PREC_QUANT)
}
ExprData::Exists { var, body } => {
let (v, _) = latex_r(*var, pool);
let (b, _) = latex_r(*body, pool);
(format!(r"\exists {v} \, . \, {b}"), PREC_QUANT)
}
ExprData::BigO(arg) => {
let (a, _) = latex_r(*arg, pool);
(format!(r"\mathcal{{O}}\!\left({a}\right)"), PREC_ATOM)
}
ExprData::RootSum { poly, var, body } => {
let (p, _) = latex_r(*poly, pool);
let (v, _) = latex_r(*var, pool);
let b = latex_wrap(*body, pool, PREC_BIGOP_BODY);
(format!(r"\sum_{{{v} \, : \, {p} = 0}} {b}"), PREC_BIGOP)
}
})
}
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_sup(base_tex: &str, exp: &str) -> String {
match to_superscript(exp) {
Some(s) => format!("{base_tex}{s}"),
None => format!("{base_tex}^({exp})"),
}
}
fn unicode_factor(id: ExprId, pool: &ExprPool) -> String {
let (s, prec) = unicode_r(id, pool);
if prec < PREC_MUL || s.starts_with('-') {
format!("({s})")
} else {
s
}
}
fn unicode_signed(id: ExprId, pool: &ExprPool) -> (i32, String) {
pool.with(id, |data| match data {
ExprData::Integer(n) => {
if n.0 < 0 {
(-1, n.0.as_abs().to_string())
} else {
(1, n.0.to_string())
}
}
ExprData::Rational(r) => {
let (num, den) = (r.0.numer(), r.0.denom());
let abs = num.as_abs();
let s = match rational_as_integer(r) {
Some(_) => abs.to_string(),
None => unicode_frac(&abs, den),
};
if *num < 0 {
(-1, s)
} else {
(1, s)
}
}
ExprData::Mul(args) => unicode_signed_mul(args, pool),
_ => {
let (s, _) = unicode_r(id, pool);
match s.strip_prefix('-') {
Some(rest) if matches!(data, ExprData::Float(_)) => (-1, rest.to_string()),
_ => (1, s),
}
}
})
}
fn unicode_signed_mul(args: &[ExprId], pool: &ExprPool) -> (i32, String) {
let (sign, numer_i, denom_i, others) = split_coefficient(args, pool);
let mut num_parts: Vec<String> = Vec::new();
let mut den_parts: Vec<String> = Vec::new();
for (child, mult) in collapse_runs(&others) {
let pushed = pool.with(child, |data| {
if let ExprData::Pow { base, exp } = data {
if let Some(exp_abs) = negative_exponent_abs(*exp, mult, pool) {
let base_tex = unicode_wrap(*base, pool, PREC_POW + 1);
den_parts.push(if exp_abs == 1 {
base_tex
} else {
unicode_sup(&base_tex, &exp_abs.to_string())
});
return true;
}
}
false
});
if !pushed {
if mult > 1 {
let base_tex = unicode_wrap(child, pool, PREC_POW + 1);
num_parts.push(unicode_sup(&base_tex, &mult.to_string()));
} else {
num_parts.push(unicode_factor(child, pool));
}
}
}
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, den) = (r.0.numer(), r.0.denom());
if *num == 1 && *den >= 2 {
let base_tex = unicode_wrap(base, pool, PREC_POW + 1);
return match den.to_u8() {
Some(2) => format!("√{base_tex}"),
Some(3) => format!("∛{base_tex}"),
Some(4) => format!("∜{base_tex}"),
_ => to_superscript(&den.to_string())
.map(|s| format!("{s}√{base_tex}"))
.unwrap_or_else(|| format!("{base_tex}^(1/{den})")),
};
}
}
let base_tex = unicode_wrap(base, pool, PREC_POW + 1);
if let Some(v) = integral_exponent(exp, pool) {
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 integral_exponent(exp: ExprId, pool: &ExprPool) -> Option<Integer> {
match pool.get(exp) {
ExprData::Integer(n) => Some(n.0),
ExprData::Rational(r) if *r.0.denom() == 1 => Some(r.0.numer().clone()),
_ => None,
}
}
fn unicode_func(name: &str, args: &[ExprId], pool: &ExprPool) -> String {
if !crate::kernel::func_arity_ok(name, args.len()) {
let rendered: Vec<String> = args.iter().map(|&a| unicode_r(a, pool).0).collect();
return format!("{name}({})", rendered.join(", "));
}
match name {
"sqrt" => {
let inner = unicode_wrap(args[0], pool, PREC_POW + 1);
format!("√{inner}")
}
"abs" => {
let (inner, _) = unicode_r(args[0], pool);
if inner.contains('|') {
format!("|({inner})|")
} else {
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" => "Γ",
"digamma" => "ψ",
"trigamma" => "ψ₁",
"bessel_j0" => "J₀",
"bessel_j1" => "J₁",
"lambert_w" => "W",
"Ei" => "Ei",
"li" => "li",
"Si" => "Si",
"Ci" => "Ci",
"Shi" => "Shi",
"Chi" => "Chi",
"fresnels" => "S",
"fresnelc" => "C",
"dilog" => "Li₂",
"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 if args.len() == 1 => {
let inner = unicode_wrap(args[0], pool, PREC_NOT + 1);
format!("¬{inner}")
}
_ => {
let op = match kind {
PredicateKind::Lt => "<",
PredicateKind::Le => "≤",
PredicateKind::Gt => ">",
PredicateKind::Ge => "≥",
PredicateKind::Eq => "=",
PredicateKind::Ne => "≠",
PredicateKind::And => "∧",
PredicateKind::Or => "∨",
_ => "¬",
};
let req = predicate_prec(kind);
let rendered: Vec<String> = args.iter().map(|&a| unicode_wrap(a, pool, req)).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(), literal_prec(&n.0.to_string())),
ExprData::Rational(r) => {
let (num, den) = (r.0.numer(), r.0.denom());
let abs = num.as_abs();
let s = match rational_as_integer(r) {
Some(_) => abs.to_string(),
None => unicode_frac(&abs, den),
};
let prec = if s.contains('/') { PREC_MUL } else { PREC_ATOM };
if *num < 0 {
(format!("-{s}"), prec.min(PREC_NEG))
} else {
(s, prec)
}
}
ExprData::Float(f) => {
let s = f.to_string();
let prec = float_prec(&s);
(s, prec)
}
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), unicode_func_prec(name)),
ExprData::Piecewise { branches, default } => {
(unicode_piecewise(branches, *default, pool), PREC_ATOM)
}
ExprData::Predicate { kind, args } => {
(unicode_predicate(kind, args, pool), predicate_prec(kind))
}
ExprData::Forall { var, body } => {
let (v, _) = unicode_r(*var, pool);
let (b, _) = unicode_r(*body, pool);
(format!("∀{v}.{b}"), PREC_QUANT)
}
ExprData::Exists { var, body } => {
let (v, _) = unicode_r(*var, pool);
let (b, _) = unicode_r(*body, pool);
(format!("∃{v}.{b}"), PREC_QUANT)
}
ExprData::BigO(arg) => {
let (a, _) = unicode_r(*arg, pool);
(format!("O({a})"), PREC_ATOM)
}
ExprData::RootSum { poly, var, body } => {
let (p, _) = unicode_r(*poly, pool);
let (v, _) = unicode_r(*var, pool);
let b = unicode_wrap(*body, pool, PREC_BIGOP_BODY);
(format!("∑_{{{v}:{p}=0}} {b}"), PREC_BIGOP)
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
#[test]
fn negative_pow_base_is_parenthesised() {
let p = ExprPool::new();
let n = p.symbol("n", Domain::Real);
let pow_m1 = p.pow(p.integer(-1_i32), n);
assert_eq!(render_latex(pow_m1, &p), r"\left(-1\right)^n");
assert_eq!(render_unicode(pow_m1, &p), "(-1)^(n)");
let pow_m2 = p.pow(p.integer(-2_i32), n);
assert_eq!(render_latex(pow_m2, &p), r"\left(-2\right)^n");
assert_eq!(render_unicode(pow_m2, &p), "(-2)^(n)");
let pow_mhalf = p.pow(p.rational(-1, 2), n);
assert_eq!(render_latex(pow_mhalf, &p), r"\left(-\frac{1}{2}\right)^n");
assert_eq!(render_unicode(pow_mhalf, &p), "(-½)^(n)");
}
#[test]
fn fractional_pow_base_is_parenthesised() {
let p = ExprPool::new();
let n = p.symbol("n", Domain::Real);
let pow_half = p.pow(p.rational(1, 2), n);
assert_eq!(render_latex(pow_half, &p), r"\left(\frac{1}{2}\right)^n");
assert_eq!(render_unicode(pow_half, &p), "½^(n)");
let pow_3_7 = p.pow(p.rational(3, 7), n);
assert_eq!(render_latex(pow_3_7, &p), r"\left(\frac{3}{7}\right)^n");
assert_eq!(render_unicode(pow_3_7, &p), "(3/7)^(n)");
}
#[test]
fn negative_pow_base_inside_product() {
let p = ExprPool::new();
let n = p.symbol("n", Domain::Real);
let prod = p.mul(vec![p.integer(-16_i32), p.pow(p.integer(-2_i32), n)]);
assert_eq!(render_latex(prod, &p), r"-16 \left(-2\right)^n");
assert_eq!(render_unicode(prod, &p), "-16·(-2)^(n)");
}
#[test]
fn positive_pow_base_is_bare() {
let p = ExprPool::new();
let n = p.symbol("n", Domain::Real);
let x = p.symbol("x", Domain::Real);
let pow_2 = p.pow(p.integer(2_i32), n);
assert_eq!(render_latex(pow_2, &p), "2^n");
assert_eq!(render_unicode(pow_2, &p), "2^(n)");
let pow_x = p.pow(x, p.integer(2_i32));
assert_eq!(render_latex(pow_x, &p), "x^2");
assert_eq!(render_unicode(pow_x, &p), "x²");
}
#[test]
fn negative_coefficient_in_product_stays_bare() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let prod = p.mul(vec![p.integer(-2_i32), x]);
assert_eq!(render_latex(prod, &p), "-2 x");
assert_eq!(render_unicode(prod, &p), "-2·x");
}
#[test]
fn a_repeated_factor_prints_as_a_power() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
assert_eq!(render_latex(p.mul(vec![x, x]), &p), "x^2");
assert_eq!(render_unicode(p.mul(vec![x, x]), &p), "x²");
assert_eq!(render_latex(p.mul(vec![x, x, x]), &p), "x^3");
assert_eq!(render_latex(p.mul(vec![x, x, y]), &p), "x^2 y");
assert_eq!(render_latex(p.mul(vec![x, y]), &p), "x y");
let s = p.func("sin", vec![x]);
assert_eq!(
render_latex(p.mul(vec![s, s]), &p),
r"\sin\!\left(x\right)^2"
);
}
#[test]
fn run_collapsing_does_not_reorder_a_product() {
assert_eq!(
collapse_runs(&[ExprId(1), ExprId(2), ExprId(1)]),
vec![(ExprId(1), 1), (ExprId(2), 1), (ExprId(1), 1)]
);
assert_eq!(
collapse_runs(&[ExprId(1), ExprId(1), ExprId(2)]),
vec![(ExprId(1), 2), (ExprId(2), 1)]
);
}
#[test]
fn a_numeral_before_a_numeral_gets_a_cdot() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let n = p.symbol("n", Domain::Real);
let two_three_n = p.mul(vec![p.integer(2_i32), p.pow(p.integer(3_i32), n)]);
assert_eq!(render_latex(two_three_n, &p), r"2 \cdot 3^n");
assert_eq!(render_latex(p.mul(vec![p.integer(2_i32), x]), &p), "2 x");
}
#[test]
fn a_negative_float_factor_is_parenthesised() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let prod = p.mul(vec![x, p.float(-1.5_f64, 53)]);
assert!(
render_latex(prod, &p).contains(r"\left(-1.5"),
"{}",
render_latex(prod, &p)
);
assert!(render_unicode(prod, &p).contains("(-1.5"));
}
#[test]
fn a_float_in_exponent_notation_is_typeset_as_a_power_of_ten() {
let p = ExprPool::new();
let quarter = p.float(0.25_f64, 53);
assert_eq!(
render_latex(quarter, &p),
r"2.5000000000000000 \times 10^{-1}"
);
let n = p.symbol("n", Domain::Real);
assert_eq!(
render_latex(p.pow(quarter, n), &p),
r"\left(2.5000000000000000 \times 10^{-1}\right)^n"
);
assert_eq!(
render_unicode(p.pow(quarter, n), &p),
"(2.5000000000000000e-1)^(n)"
);
}
#[test]
fn a_reciprocal_factor_carries_no_exponent_of_one() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let one = p.integer(1_i32);
let denom = p.add(vec![one, x]);
let recip = p.mul(vec![one, p.pow(denom, p.integer(-1_i32))]);
assert_eq!(render_unicode(recip, &p), "1/(x + 1)");
assert_eq!(render_latex(recip, &p), r"\frac{1}{\left(x + 1\right)}");
let squared = p.mul(vec![one, p.pow(denom, p.integer(-2_i32))]);
assert_eq!(render_unicode(squared, &p), "1/(x + 1)²");
}
#[test]
fn connective_precedence_is_parenthesised() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let a = p.predicate(PredicateKind::Lt, vec![x, p.integer(0_i32)]);
let b = p.predicate(PredicateKind::Gt, vec![x, p.integer(1_i32)]);
let c = p.predicate(PredicateKind::Eq, vec![x, p.integer(2_i32)]);
let or_in_and = p.predicate(
PredicateKind::And,
vec![p.predicate(PredicateKind::Or, vec![a, b]), c],
);
assert_eq!(
render_latex(or_in_and, &p),
r"\left(x < 0 \lor x > 1\right) \land x = 2"
);
assert_eq!(render_unicode(or_in_and, &p), "(x < 0 ∨ x > 1) ∧ x = 2");
let and_in_or = p.predicate(
PredicateKind::Or,
vec![a, p.predicate(PredicateKind::And, vec![b, c])],
);
assert_eq!(render_latex(and_in_or, &p), r"x < 0 \lor x > 1 \land x = 2");
let not_and = p.predicate(
PredicateKind::Not,
vec![p.predicate(PredicateKind::And, vec![a, b])],
);
assert_eq!(
render_latex(not_and, &p),
r"\lnot \left(x < 0 \land x > 1\right)"
);
assert_eq!(render_unicode(not_and, &p), "¬(x < 0 ∧ x > 1)");
}
#[test]
fn a_quantifier_inside_a_connective_is_parenthesised() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let a = p.predicate(PredicateKind::Lt, vec![x, p.integer(0_i32)]);
let b = p.predicate(PredicateKind::Gt, vec![x, p.integer(1_i32)]);
let quantified = p.forall(x, a);
let conj = p.predicate(PredicateKind::And, vec![quantified, b]);
assert_eq!(
render_latex(conj, &p),
r"\left(\forall x \, . \, x < 0\right) \land x > 1"
);
assert_eq!(render_unicode(conj, &p), "(∀x.x < 0) ∧ x > 1");
}
#[test]
fn an_open_ended_function_rendering_is_grouped_as_a_power_base() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let two = p.integer(2_i32);
let e = p.func("exp", vec![x]);
assert_eq!(render_latex(p.pow(e, two), &p), r"\left(e^x\right)^2");
assert_eq!(render_unicode(p.pow(e, two), &p), "(e^(x))²");
let r = p.func("sqrt", vec![x]);
assert_eq!(render_unicode(p.pow(r, two), &p), "(√x)²");
let s = p.func("sin", vec![x]);
assert_eq!(render_latex(p.pow(s, two), &p), r"\sin\!\left(x\right)^2");
}
#[test]
fn an_unregistered_head_escapes_its_underscores() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
assert_eq!(
render_latex(p.func("my_func", vec![x]), &p),
r"\operatorname{my\_func}\!\left(x\right)"
);
assert_eq!(render_latex(p.symbol("u_0", Domain::Real), &p), "{u}_{0}");
}
#[test]
fn a_rational_with_denominator_one_prints_as_an_integer() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let two_over_one = p.rational(4, 2);
assert_eq!(render_latex(two_over_one, &p), "2");
assert_eq!(render_unicode(two_over_one, &p), "2");
assert_eq!(render_latex(p.pow(x, p.rational(1, 1)), &p), "x^1");
assert_eq!(render_unicode(p.pow(x, p.rational(1, 1)), &p), "x");
}
#[test]
fn a_root_sum_under_a_power_brackets_the_operator() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let c = p.symbol("c", Domain::Real);
let poly = p.add(vec![p.pow(c, p.integer(2_i32)), p.integer(-2_i32)]);
let log = p.func("log", vec![p.add(vec![x, c])]);
let body = p.mul(vec![c, log]);
let squared_sum = p.pow(p.root_sum(poly, c, body), p.integer(2_i32));
let sum_of_squares = p.root_sum(poly, c, p.mul(vec![c, p.pow(log, p.integer(2_i32))]));
assert_ne!(
render_latex(squared_sum, &p),
render_latex(sum_of_squares, &p)
);
assert_ne!(
render_unicode(squared_sum, &p),
render_unicode(sum_of_squares, &p)
);
assert!(render_latex(squared_sum, &p).starts_with(r"\left(\sum"));
assert!(render_unicode(squared_sum, &p).starts_with("(∑"));
}
#[test]
fn a_root_sum_brackets_a_body_that_binds_looser_than_a_product() {
let p = ExprPool::new();
let c = p.symbol("c", Domain::Real);
let poly = p.add(vec![p.pow(c, p.integer(2_i32)), p.integer(-2_i32)]);
let add_body = p.root_sum(poly, c, p.add(vec![c, p.integer(1_i32)]));
assert_eq!(
render_latex(add_body, &p),
r"\sum_{c \, : \, c^2 - 2 = 0} \left(c + 1\right)"
);
assert_eq!(render_unicode(add_body, &p), "∑_{c:c² - 2=0} (c + 1)");
let mul_body = p.root_sum(poly, c, p.mul(vec![c, p.integer(3_i32)]));
assert_eq!(render_unicode(mul_body, &p), "∑_{c:c² - 2=0} 3·c");
}
#[test]
fn a_root_sum_inside_a_product_is_bracketed() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let c = p.symbol("c", Domain::Real);
let poly = p.add(vec![p.pow(c, p.integer(2_i32)), p.integer(-2_i32)]);
let rs = p.root_sum(poly, c, c);
let tex = render_latex(p.mul(vec![rs, x]), &p);
assert!(tex.contains(r"\left(\sum"), "{tex}");
assert!(tex.contains(r"\right)"), "{tex}");
}
#[test]
fn a_unicode_piecewise_closes_its_brace() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let cond = p.predicate(PredicateKind::Lt, vec![x, p.integer(0_i32)]);
let pw = p.piecewise(
vec![(cond, p.add(vec![x, p.integer(1_i32)]))],
p.mul(vec![p.integer(2_i32), x]),
);
let block = render_unicode(pw, &p);
assert!(block.starts_with("{ "), "{block}");
assert!(block.ends_with(" }"), "{block}");
let squared = render_unicode(p.pow(pw, p.integer(2_i32)), &p);
assert!(squared.ends_with(" }²"), "{squared}");
}
#[test]
fn a_nested_absolute_value_is_grouped() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let inner = p.func("abs", vec![x]);
assert_eq!(render_unicode(inner, &p), "|x|");
assert_eq!(render_unicode(p.func("abs", vec![inner]), &p), "|(|x|)|");
}
fn pow10(e: u32) -> Integer {
use rug::ops::Pow;
Integer::from(10).pow(e)
}
#[test]
fn integers_past_i64_are_printed_exactly() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let e20 = pow10(20).to_string();
let sum = p.add(vec![x, p.integer(pow10(20))]);
assert_eq!(render_latex(sum, &p), format!("x + {e20}"));
assert_eq!(render_unicode(sum, &p), format!("x + {e20}"));
let diff = p.add(vec![x, p.integer(-pow10(20))]);
assert_eq!(render_latex(diff, &p), format!("x - {e20}"));
assert_eq!(render_unicode(diff, &p), format!("x - {e20}"));
let e30 = pow10(30).to_string();
let prod = p.mul(vec![p.integer(pow10(30)), x]);
assert_eq!(render_latex(prod, &p), format!("{e30} x"));
assert_eq!(render_unicode(prod, &p), format!("{e30}·x"));
}
#[test]
fn i64_min_coefficient_has_one_minus_sign() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let prod = p.mul(vec![p.integer(i64::MIN), x]);
assert_eq!(render_latex(prod, &p), "-9223372036854775808 x");
assert_eq!(render_unicode(prod, &p), "-9223372036854775808·x");
let sum = p.add(vec![x, p.integer(i64::MIN)]);
assert_eq!(render_unicode(sum, &p), "x - 9223372036854775808");
assert_eq!(render_latex(sum, &p), "x - 9223372036854775808");
}
#[test]
fn rationals_past_i64_are_printed_exactly() {
use rug::Complete;
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let f21 = Integer::factorial(21).complete();
let r = p.rational(1, f21.clone());
assert_eq!(render_unicode(r, &p), format!("1/{f21}"));
assert_eq!(render_latex(r, &p), format!("\\frac{{1}}{{{f21}}}"));
let r = p.rational(pow10(30), 7);
assert_eq!(render_unicode(r, &p), format!("{}/7", pow10(30)));
assert_eq!(render_latex(r, &p), format!("\\frac{{{}}}{{7}}", pow10(30)));
let r = p.rational(-pow10(30), 7);
assert_eq!(render_unicode(r, &p), format!("-{}/7", pow10(30)));
let two64 = Integer::from(1u128 << 64);
let over = p.mul(vec![p.rational(1, two64.clone()), x]);
assert_eq!(render_unicode(over, &p), format!("1/{two64}·x"));
assert_eq!(render_latex(over, &p), format!("\\frac{{1}}{{{two64}}} x"));
let term = p.add(vec![x, p.rational(-1, f21.clone())]);
assert_eq!(render_unicode(term, &p), format!("x - 1/{f21}"));
}
#[test]
fn exponents_past_i64_are_printed_exactly() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let e20 = pow10(20);
let root = p.pow(x, p.rational(1, e20.clone()));
assert_eq!(
render_unicode(root, &p),
format!("{}√x", to_superscript(&e20.to_string()).unwrap())
);
assert_eq!(render_latex(root, &p), format!("\\sqrt[{e20}]{{x}}"));
let inv = p.mul(vec![p.integer(3), p.pow(x, p.integer(-e20.clone()))]);
assert_eq!(render_latex(inv, &p), format!("\\frac{{3}}{{x^{{{e20}}}}}"));
assert_eq!(
render_unicode(inv, &p),
format!("3/x{}", to_superscript(&e20.to_string()).unwrap())
);
let big = p.pow(x, p.integer(e20.clone()));
assert_eq!(render_latex(big, &p), format!("x^{{{e20}}}"));
}
}