///|
/// - Does: Decomposes one expression into a symbolic numerator and denominator pair.
/// - Input: Any `Expr`.
/// - Returns: `(Expr, Expr)` where the original expression is represented as `num * den**-1`.
/// - Limits: Keeps symbolic factors unevaluated when no simpler exact split is available.
pub fn fraction(expr : Expr) -> (Expr, Expr) {
  split_fraction_simple(expr)
}

///|
/// - Does: Extracts the symbolic numerator of one expression.
/// - Input: Any `Expr`.
/// - Returns: One `Expr`.
/// - Limits: Uses the same structural split as `fraction`, so unevaluated factors can remain in the result.
pub fn numer(expr : Expr) -> Expr {
  let (n, _) = fraction(expr)
  n
}

///|
/// - Does: Extracts the symbolic denominator of one expression.
/// - Input: Any `Expr`.
/// - Returns: One `Expr`.
/// - Limits: Uses the same structural split as `fraction`, so unevaluated factors can remain in the result.
pub fn denom(expr : Expr) -> Expr {
  let (_, d) = fraction(expr)
  d
}

///|
/// - Does: Groups additive terms by powers of one target symbol.
/// - Input: One expression and one target `Expr`, typically a symbol.
/// - Returns: One rewritten `Expr`.
/// - Limits: Non-symbol targets are left unchanged instead of raising an error.
pub fn collect(expr : Expr, sym : Expr) -> Expr {
  match sym {
    Expr::Symbol(_) => collect_one_symbol(expr, sym)
    _ => expr
  }
}

///|
/// - Does: Applies `collect` recursively through the expression tree.
/// - Input: One expression and one target `Expr`, typically a symbol.
/// - Returns: One rewritten `Expr`.
/// - Limits: Uses the same symbol-only front door as `collect`, so unsupported targets are propagated unchanged.
pub fn rcollect(expr : Expr, sym : Expr) -> Expr {
  let rewritten = @symcore.map_children(expr, child => rcollect(child, sym))
  collect(rewritten, sym)
}

///|
/// - Does: Factors a shared rational constant out of additive terms.
/// - Input: Any `Expr`.
/// - Returns: One rewritten `Expr`.
/// - Limits: Only exact rational additive coefficients are combined; non-additive inputs are returned unchanged.
pub fn collect_const(expr : Expr) -> Expr {
  match expr {
    Expr::Add(args) => {
      if args.is_empty() {
        return expr
      }
      let terms : Array[(@symnum.BigRational, Expr)] = Array::new()
      for arg in args {
        terms.push(split_coeff_mul(arg))
      }
      let g = rational_gcd(terms.map(t => t.0))
      if g.is_zero() || g.is_one() {
        return expr
      }
      let scaled : Array[Expr] = Array::new()
      for pair in terms {
        let coeff = pair.0
        let rest = pair.1
        let new_coeff = coeff.div_r(g) catch { _ => return expr }
        let scaled_term = if rest == int(1) {
          @symcore.Expr::Number(new_coeff)
        } else if new_coeff.is_one() {
          rest
        } else {
          @symcore.mul([@symcore.Expr::Number(new_coeff), rest])
        }
        scaled.push(scaled_term)
      }
      @symcore.mul([@symcore.Expr::Number(g), @symcore.add(scaled)])
    }
    _ => expr
  }
}

///|
/// - Does: Collapses nested powers when algebraic exponent rules allow it.
/// - Input: Any `Expr` plus optional `max_passes`.
/// - Returns: One rewritten `Expr`.
/// - Limits: Stops after a bounded number of passes and leaves unsupported power patterns unchanged.
pub fn powdenest(expr : Expr, max_passes? : Int = 8) -> Expr {
  fixpoint_advanced(expr, rewrite_powdenest, max_passes~)
}

///|
/// - Does: Switches between exponential and trigonometric forms to shorten an expression.
/// - Input: Any `Expr` plus optional `max_passes`.
/// - Returns: One rewritten `Expr`.
/// - Limits: Only built-in rewrite families are tried, so expressions outside that surface are returned as-is.
pub fn exptrigsimp(expr : Expr, max_passes? : Int = 8) -> Expr {
  fixpoint_advanced(expr, rewrite_exptrigsimp, max_passes~)
}

///|
/// - Does: Applies Gamma-function simplifications and related exact identities.
/// - Input: Any `Expr` plus optional `max_passes`.
/// - Returns: One rewritten `Expr`.
/// - Limits: Only supported Gamma/combinatorial identities are used; unsupported forms are left unchanged.
pub fn gammasimp(expr : Expr, max_passes? : Int = 8) -> Expr {
  fixpoint_advanced(expr, rewrite_gammasimp, max_passes~)
}

///|
/// - Does: Combines logarithmic sums and powers into more compact logarithmic forms.
/// - Input: Any `Expr` plus optional `max_passes`.
/// - Returns: One rewritten `Expr`.
/// - Limits: Respects only the implemented algebraic identities and does not infer extra assumptions.
pub fn logcombine(expr : Expr, max_passes? : Int = 8) -> Expr {
  fixpoint_advanced(expr, rewrite_logcombine, max_passes~)
}

///|
/// - Does: Separates multiplicative factors by symbolic variable sets when possible.
/// - Input: Any `Expr` plus optional `force`.
/// - Returns: One rewritten `Expr`.
/// - Limits: Unsupported couplings remain unevaluated; `force` only widens rewrite attempts and does not guarantee separation.
pub fn separatevars(expr : Expr, force? : Bool = false) -> Expr {
  rewrite_bottom_up_advanced(expr, child => rewrite_separatevars(child, force~))
}

///|
/// - Does: Replaces symbols with positivity-assumed stand-ins and records how to restore the originals.
/// - Input: Any `Expr`.
/// - Returns: `(Expr, Map[String, Expr])` where the map restores the temporary symbols.
/// - Limits: Uses simple name-based stand-ins and does not preserve richer assumption metadata.
pub fn posify(expr : Expr) -> (Expr, Map[String, Expr]) {
  let symbols : Map[String, Bool] = {}
  collect_symbols(expr, symbols)
  let env : Map[String, Expr] = {}
  let restore : Map[String, Expr] = {}
  for name, _ in symbols {
    let new_name = "_\{name}"
    env[name] = Expr::Symbol(new_name)
    restore[new_name] = Expr::Symbol(name)
  }
  (@symcore.subst(expr, env), restore)
}

///|
/// - Does: Computes the hypergeometric term ratio `f(k + 1) / f(k)` when the index shift is supported.
/// - Input: One expression `f` and one index expression `k`.
/// - Returns: One `Expr`.
/// - Limits: Non-symbol indices return `0` instead of raising, and unsupported terms can stay unsimplified.
pub fn hypersimp(f : Expr, k : Expr) -> Expr {
  match k {
    Expr::Symbol(name) => {
      let env : Map[String, Expr] = {}
      env[name] = @symcore.add([k, int(1)])
      let fk1 = @symcore.subst(f, env)
      combsimp(simplify(@symcore.mul([fk1, @symcore.pow(f, int(-1))])))
    }
    _ => int(0)
  }
}

///|
/// - Does: Checks whether two sequences are hyper-similar in one index.
/// - Input: Two expressions plus one index expression `k`.
/// - Returns: `Bool`.
/// - Limits: Non-symbol indices return `false`, and the decision is limited to the implemented rational-form check.
pub fn hypersimilar(f : Expr, g : Expr, k : Expr) -> Bool {
  match k {
    Expr::Symbol(name) => {
      let rf = hypersimp(f, k)
      let rg = hypersimp(g, k)
      let ratio = combsimp(
        simplify(@symcore.mul([rf, @symcore.pow(rg, int(-1))])),
      )
      is_rational_form(ratio, name) && !contains_function(ratio)
    }
    _ => false
  }
}

///|
fn split_fraction_simple(expr : Expr) -> (Expr, Expr) {
  match @symcore.normalize_legacy_expr(expr) {
    Expr::Number(n) =>
      if n.is_integral() {
        (Expr::Number(n), int(1))
      } else {
        (
          @symcore.Expr::Number(@symnum.BigRational::from_bigint(n.numerator())),
          @symcore.Expr::Number(
            @symnum.BigRational::from_bigint(n.denominator()),
          ),
        )
      }
    Expr::Add(args) => {
      if args.is_empty() {
        return (int(0), int(1))
      }
      let numerators : Array[Expr] = []
      let denominators : Array[Expr] = []
      for arg in args {
        let (num, den) = split_fraction_simple(arg)
        numerators.push(num)
        denominators.push(den)
      }
      let same_denom = {
        let first = denominators[0]
        let mut all_same = true
        for i in 1..
      if exp.is_integral() {
        let (num0, den0) = split_fraction_simple(base)
        let power = exp.numerator().to_int()
        if power == 0 {
          return (int(1), int(1))
        }
        let (num, den, abs_power) = if power < 0 {
          (den0, num0, -power)
        } else {
          (num0, den0, power)
        }
        let out_num = if @symcore.compare_expr(num, int(1)) == 0 {
          int(1)
        } else {
          @symcore.normalize_legacy_expr(@symcore.pow(num, int(abs_power)))
        }
        let out_den = if @symcore.compare_expr(den, int(1)) == 0 {
          int(1)
        } else {
          @symcore.normalize_legacy_expr(@symcore.pow(den, int(abs_power)))
        }
        (out_num, out_den)
      } else {
        (Expr::Pow(base, Expr::Number(exp)), int(1))
      }
    Expr::Mul(args) => {
      let num_factors : Array[Expr] = Array::new()
      let den_factors : Array[Expr] = Array::new()
      for arg in args {
        let (n, d) = split_fraction_simple(arg)
        if n != int(1) {
          num_factors.push(n)
        }
        if d != int(1) {
          den_factors.push(d)
        }
      }
      (@symcore.mul(num_factors), @symcore.mul(den_factors))
    }
    other => (other, int(1))
  }
}

///|
fn collect_one_symbol(expr : Expr, sym : Expr) -> Expr {
  match expr {
    Expr::Add(args) => {
      let bins : Map[Int, Expr] = {}
      let leftover : Array[Expr] = Array::new()
      for arg in args {
        match decompose_sym_power(arg, sym) {
          Some((exp, coeff)) =>
            match bins.get(exp) {
              Some(prev) => bins[exp] = @symcore.add([prev, coeff])
              None => bins[exp] = coeff
            }
          None => leftover.push(arg)
        }
      }
      let exps : Array[Int] = Array::new()
      for exp, _ in bins {
        exps.push(exp)
      }
      exps.sort()
      for i in 0.. expr
  }
}

///|
fn decompose_sym_power(term : Expr, sym : Expr) -> (Int, Expr)? {
  if term == sym {
    return Some((1, int(1)))
  }
  match term {
    Expr::Pow(base, Expr::Number(e)) if base == sym =>
      if e.is_integral() && e.numerator().to_int() >= 0 {
        Some((e.numerator().to_int(), int(1)))
      } else {
        None
      }
    Expr::Mul(args) => {
      let mut exp = 0
      let coeff_factors : Array[Expr] = Array::new()
      for arg in args {
        if arg == sym {
          exp = exp + 1
          continue
        }
        match arg {
          Expr::Pow(base, Expr::Number(e)) if base == sym => {
            if !e.is_integral() || e.numerator().to_int() < 0 {
              return None
            }
            exp = exp + e.numerator().to_int()
          }
          _ => coeff_factors.push(arg)
        }
      }
      Some((exp, @symcore.mul(coeff_factors)))
    }
    _ => Some((0, term))
  }
}

///|
fn split_coeff_mul(term : Expr) -> (@symnum.BigRational, Expr) {
  match term {
    Expr::Number(n) => (n, int(1))
    Expr::Mul(args) => {
      let mut coeff = @symnum.BigRational::one()
      let rest : Array[Expr] = Array::new()
      for arg in args {
        match arg {
          Expr::Number(n) => coeff = coeff.mul_r(n)
          _ => rest.push(arg)
        }
      }
      (coeff, @symcore.mul(rest))
    }
    _ => (@symnum.BigRational::one(), term)
  }
}

///|
fn rational_gcd(values : Array[@symnum.BigRational]) -> @symnum.BigRational {
  if values.is_empty() {
    return @symnum.BigRational::one()
  }
  let mut ng = abs_big(values[0].numerator())
  let mut dl = values[0].denominator()
  for i in 1.. @symnum.BigRational::one()
  }
}

///|
fn abs_big(x : BigInt) -> BigInt {
  if x.op_lt(BigInt::from_int(0)) {
    x.neg()
  } else {
    x
  }
}

///|
fn fixpoint_advanced(
  expr : Expr,
  rule : (Expr) -> Expr,
  max_passes? : Int = 8,
) -> Expr {
  let passes = if max_passes <= 0 { 1 } else { max_passes }
  let mut cur = expr
  for _ in 0.. Expr) -> Expr {
  let rewritten = @symcore.map_children(expr, child => {
    rewrite_bottom_up_advanced(child, rule)
  })
  rule(rewritten)
}

///|
fn rewrite_powdenest(expr : Expr) -> Expr {
  match expr {
    Expr::Pow(Expr::Pow(base, a), b) => @symcore.pow(base, @symcore.mul([a, b]))
    _ =>
      match named_unary_application(expr) {
        Some((name, inner_expr)) =>
          match named_unary_application(inner_expr) {
            Some((inner, x)) if name == "exp" && inner == "log" => x
            _ =>
              match inner_expr {
                Expr::Mul(args) if args.length() == 2 => {
                  let mut log_arg : Expr? = None
                  let mut other : Expr? = None
                  for arg in args {
                    match named_unary_application(arg) {
                      Some((inner, x)) if name == "exp" && inner == "log" =>
                        if log_arg is None {
                          log_arg = Some(x)
                        } else {
                          return expr
                        }
                      _ =>
                        if other is None {
                          other = Some(arg)
                        } else {
                          return expr
                        }
                    }
                  }
                  match (log_arg, other) {
                    (Some(x), Some(a)) => @symcore.pow(x, a)
                    _ => expr
                  }
                }
                _ => expr
              }
          }
        None => expr
      }
  }
}

///|
fn rewrite_exptrigsimp(expr : Expr) -> Expr {
  match expr {
    _ =>
      match named_unary_application(expr) {
        Some((name, inner_expr)) =>
          match named_unary_application(inner_expr) {
            Some((inner, x)) if name == "exp" && inner == "log" => x
            _ => expr
          }
        None =>
          match expr {
            Expr::Add([a, b]) =>
              match parse_exp_i_pair(a, b) {
                Some((x, sign)) =>
                  if sign == 1 {
                    @symcore.mul([int(2), @symcore.function("cos", [x])])
                  } else {
                    @symcore.mul([
                      int(2),
                      @symcore.Expr::NumberSymbol(
                        @symcore.NumberSymbolKind::ImaginaryUnit,
                      ),
                      @symcore.function("sin", [x]),
                    ])
                  }
                None =>
                  match parse_cos_i_sin_pair(a, b) {
                    Some((x, sign)) =>
                      if sign == 1 {
                        @symcore.function("exp", [
                          @symcore.mul([
                            @symcore.Expr::NumberSymbol(
                              @symcore.NumberSymbolKind::ImaginaryUnit,
                            ),
                            x,
                          ]),
                        ])
                      } else {
                        @symcore.function("exp", [
                          @symcore.mul([
                            int(-1),
                            @symcore.Expr::NumberSymbol(
                              @symcore.NumberSymbolKind::ImaginaryUnit,
                            ),
                            x,
                          ]),
                        ])
                      }
                    None => expr
                  }
              }
            _ => expr
          }
      }
  }
}

///|
fn parse_exp_i_pair(a : Expr, b : Expr) -> (Expr, Int)? {
  match (exp_i_arg(a), exp_minus_i_arg(b)) {
    (Some(x), Some(y)) if x == y => Some((x, 1))
    _ =>
      match (exp_i_arg(a), neg_exp_minus_i_arg(b)) {
        (Some(x), Some(y)) if x == y => Some((x, -1))
        _ =>
          match (exp_i_arg(b), exp_minus_i_arg(a)) {
            (Some(x), Some(y)) if x == y => Some((x, 1))
            _ =>
              match (exp_i_arg(b), neg_exp_minus_i_arg(a)) {
                (Some(x), Some(y)) if x == y => Some((x, -1))
                _ => None
              }
          }
      }
  }
}

///|
fn exp_i_arg(expr : Expr) -> Expr? {
  match named_unary_application(expr) {
    Some((name, Expr::Mul(args))) if name == "exp" && args.length() == 2 => {
      let mut saw_i = false
      let mut other : Expr? = None
      for arg in args {
        if arg ==
          @symcore.Expr::NumberSymbol(@symcore.NumberSymbolKind::ImaginaryUnit) {
          saw_i = true
        } else if other is None {
          other = Some(arg)
        } else {
          return None
        }
      }
      if saw_i {
        other
      } else {
        None
      }
    }
    _ => None
  }
}

///|
fn exp_minus_i_arg(expr : Expr) -> Expr? {
  match named_unary_application(expr) {
    Some((name, Expr::Mul(args))) if name == "exp" =>
      if args.length() == 3 {
        let mut saw_neg_one = false
        let mut saw_i = false
        let mut other : Expr? = None
        for arg in args {
          match arg {
            Expr::Number(c) if c.compare(@symnum.BigRational::from_int(-1)) == 0 =>
              saw_neg_one = true
            _ if arg ==
              @symcore.Expr::NumberSymbol(
                @symcore.NumberSymbolKind::ImaginaryUnit,
              ) => saw_i = true
            _ => if other is None { other = Some(arg) } else { return None }
          }
        }
        if saw_neg_one && saw_i {
          other
        } else {
          None
        }
      } else {
        None
      }
    _ => None
  }
}

///|
fn neg_exp_minus_i_arg(expr : Expr) -> Expr? {
  match expr {
    Expr::Mul(args) if args.length() == 2 => {
      let mut saw_neg_one = false
      let mut other : Expr? = None
      for arg in args {
        match arg {
          Expr::Number(c) if c.compare(@symnum.BigRational::from_int(-1)) == 0 =>
            saw_neg_one = true
          _ => if other is None { other = Some(arg) } else { return None }
        }
      }
      if saw_neg_one {
        match other {
          Some(t) => exp_minus_i_arg(t)
          None => None
        }
      } else {
        None
      }
    }
    _ => None
  }
}

///|
fn parse_cos_i_sin_pair(a : Expr, b : Expr) -> (Expr, Int)? {
  match (cos_arg(a), i_sin_arg(b)) {
    (Some(x), Some((y, sign))) if x == y => Some((x, sign))
    _ =>
      match (cos_arg(b), i_sin_arg(a)) {
        (Some(x), Some((y, sign))) if x == y => Some((x, sign))
        _ => None
      }
  }
}

///|
fn cos_arg(expr : Expr) -> Expr? {
  unary_application_arg(expr, "cos")
}

///|
fn i_sin_arg(expr : Expr) -> (Expr, Int)? {
  match expr {
    Expr::Mul(args) if args.length() == 2 => {
      let mut saw_i = false
      let mut sin_arg : Expr? = None
      for arg in args {
        if arg ==
          @symcore.Expr::NumberSymbol(@symcore.NumberSymbolKind::ImaginaryUnit) {
          saw_i = true
        } else {
          match unary_application_arg(arg, "sin") {
            Some(x) => sin_arg = Some(x)
            None => return None
          }
        }
      }
      if saw_i {
        match sin_arg {
          Some(x) => Some((x, 1))
          None => None
        }
      } else {
        None
      }
    }
    Expr::Mul(args) if args.length() == 3 => {
      let mut sign = 1
      let mut saw_i = false
      let mut sin_arg : Expr? = None
      for arg in args {
        match arg {
          Expr::Number(c) =>
            if c.compare(@symnum.BigRational::from_int(-1)) == 0 {
              sign = -1
            } else {
              return None
            }
          _ if arg ==
            @symcore.Expr::NumberSymbol(
              @symcore.NumberSymbolKind::ImaginaryUnit,
            ) => saw_i = true
          _ =>
            match unary_application_arg(arg, "sin") {
              Some(x) => sin_arg = Some(x)
              None => return None
            }
        }
      }
      if saw_i {
        match sin_arg {
          Some(x) => Some((x, sign))
          None => None
        }
      } else {
        None
      }
    }
    _ => None
  }
}

///|
fn rewrite_gammasimp(expr : Expr) -> Expr {
  match expr {
    _ =>
      match named_unary_application(expr) {
        Some((name, Expr::Add(args))) if name == "gamma" && args.length() == 2 => {
          let mut shifted : Expr? = None
          let mut offset_one = false
          for arg in args {
            match arg {
              Expr::Number(n) if n.is_one() => offset_one = true
              _ =>
                if shifted is None {
                  shifted = Some(arg)
                } else {
                  return expr
                }
            }
          }
          if offset_one {
            match shifted {
              Some(x) => @symcore.mul([x, @symcore.function("gamma", [x])])
              None => expr
            }
          } else {
            expr
          }
        }
        Some((name, Expr::Number(n))) if name == "gamma" =>
          if n.is_integral() && n.numerator().to_int() > 0 {
            @symcore.function("factorial", [int(n.numerator().to_int() - 1)])
          } else {
            expr
          }
        _ =>
          match expr {
            Expr::Mul(args) => simplify_gamma_ratio_mul(args)
            _ => expr
          }
      }
  }
}

///|
fn simplify_gamma_ratio_mul(args : Array[Expr]) -> Expr {
  let numer_gamma : Array[Expr] = Array::new()
  let denom_gamma : Array[Expr] = Array::new()
  let others : Array[Expr] = Array::new()
  for arg in args {
    match gamma_numerator_arg(arg) {
      Some(value) => numer_gamma.push(value)
      None =>
        match gamma_denominator_arg(arg) {
          Some(value) => denom_gamma.push(value)
          None => others.push(arg)
        }
    }
  }
  if numer_gamma.is_empty() || denom_gamma.is_empty() {
    return combsimp(@symcore.mul(args))
  }

  let used_den : Array[Bool] = Array::make(denom_gamma.length(), false)
  let out = others.map(x => x)
  for num_arg in numer_gamma {
    let mut matched = false
    for j in 0.. {
          out.push(rising_product_from(den_arg, steps))
          used_den[j] = true
          matched = true
          break
        }
        None =>
          match gamma_integer_shift(den_arg, num_arg) {
            Some(steps) => {
              out.push(
                @symcore.pow(rising_product_from(num_arg, steps), int(-1)),
              )
              used_den[j] = true
              matched = true
              break
            }
            None => ()
          }
      }
    }
    if !matched {
      out.push(@symcore.function("gamma", [num_arg]))
    }
  }

  for j in 0.. Expr? {
  unary_application_arg(expr, "gamma")
}

///|
fn gamma_denominator_arg(expr : Expr) -> Expr? {
  match pow_named_unary_application(expr) {
    Some((name, arg, Expr::Number(exp))) =>
      if name == "gamma" && exp.is_integral() && exp.numerator().to_int() == -1 {
        Some(arg)
      } else {
        None
      }
    _ => None
  }
}

///|
fn gamma_integer_shift(target : Expr, base : Expr) -> Int? {
  let (target_norm, target_shift) = split_expr_integer_shift(target)
  let (base_norm, base_shift) = split_expr_integer_shift(base)
  if target_norm != base_norm {
    return None
  }
  let diff = target_shift - base_shift
  if diff > 0 {
    Some(diff)
  } else {
    None
  }
}

///|
fn split_expr_integer_shift(expr : Expr) -> (Expr, Int) {
  match expr {
    Expr::Add(args) => {
      let mut shift = 0
      let rest : Array[Expr] = Array::new()
      for arg in args {
        match arg {
          Expr::Number(n) if n.is_integral() => shift += n.numerator().to_int()
          _ => rest.push(arg)
        }
      }
      (if rest.is_empty() { int(0) } else { @symcore.add(rest) }, shift)
    }
    Expr::Number(n) if n.is_integral() => (int(0), n.numerator().to_int())
    _ => (expr, 0)
  }
}

///|
fn rising_product_from(start : Expr, steps : Int) -> Expr {
  if steps <= 0 {
    return int(1)
  }
  let factors : Array[Expr] = Array::new()
  for i in 0.. Expr {
  if offset == 0 {
    expr
  } else {
    @symcore.add([expr, int(offset)])
  }
}

///|
fn rewrite_logcombine(expr : Expr) -> Expr {
  match expr {
    Expr::Add(args) => {
      let log_pows : Array[Expr] = Array::new()
      let others : Array[Expr] = Array::new()
      for arg in args {
        match parse_coeff_log(arg) {
          Some((coeff, base)) =>
            log_pows.push(@symcore.pow(base, @symcore.Expr::Number(coeff)))
          None => others.push(arg)
        }
      }
      if log_pows.is_empty() {
        expr
      } else {
        let combined_arg = simplify_rational_structure(@symcore.mul(log_pows))
        let combined = @symcore.function("log", [combined_arg])
        let merged : Array[Expr] = [combined]
        for item in others {
          merged.push(item)
        }
        @symcore.add(merged)
      }
    }
    _ => expr
  }
}

///|
fn parse_coeff_log(term : Expr) -> (@symnum.BigRational, Expr)? {
  match term {
    _ if unary_application_arg(term, "log") is Some(arg) =>
      Some((@symnum.BigRational::one(), arg))
    Expr::Mul(args) if args.length() == 2 => {
      let mut coeff : @symnum.BigRational? = None
      let mut log_arg : Expr? = None
      for arg in args {
        match arg {
          Expr::Number(c) => coeff = Some(c)
          _ =>
            match unary_application_arg(arg, "log") {
              Some(inner) => log_arg = Some(inner)
              None => return None
            }
        }
      }
      match (coeff, log_arg) {
        (Some(c), Some(arg)) => Some((c, arg))
        _ => None
      }
    }
    _ => None
  }
}

///|
fn rewrite_separatevars(expr : Expr, force? : Bool = false) -> Expr {
  match expr {
    Expr::Pow(Expr::Mul(factors), exp) =>
      match exp {
        Expr::Number(n) =>
          if force || n.is_integral() {
            @symcore.mul(factors.map(f => @symcore.pow(f, exp)))
          } else {
            expr
          }
        _ => expr
      }
    _ => expr
  }
}

///|
fn collect_symbols(expr : Expr, out : Map[String, Bool]) -> Unit {
  match expr {
    Expr::Symbol(name) => out[name] = true
    Expr::NumberSymbol(_) => ()
    _ =>
      for child in @symcore.children(expr) {
        collect_symbols(child, out)
      }
  }
}

///|
fn is_rational_form(expr : Expr, k_name : String) -> Bool {
  ignore(k_name)
  match expr {
    Expr::Number(_)
    | Expr::Float(_)
    | Expr::ComplexFloat(_)
    | Expr::NumberSymbol(_)
    | Expr::Boolean(_) => true
    Expr::Symbol(_)
    | Expr::Dummy(_, _)
    | Expr::Wild(_, _, _)
    | Expr::FunctionHead(_) => true
    Expr::UndefinedFunction(_) => false
    Expr::Apply(_, args) => {
      for arg in args {
        if !is_rational_form(arg, k_name) {
          return false
        }
      }
      false
    }
    Expr::Add(args) | Expr::Mul(args) => {
      for arg in args {
        if !is_rational_form(arg, k_name) {
          return false
        }
      }
      true
    }
    Expr::Pow(base, Expr::Number(e)) =>
      e.is_integral() && is_rational_form(base, k_name)
    Expr::Mod(lhs, rhs) =>
      is_rational_form(lhs, k_name) && is_rational_form(rhs, k_name)
    Expr::Pow(_, _) => false
    Expr::Tuple(_)
    | Expr::Dict(_)
    | Expr::Relational(_, _, _)
    | Expr::Derivative(_, _)
    | Expr::Subs(_, _, _)
    | Expr::Lambda(_, _) => false
    _ if @symcore.application_parts(expr) is Some(_) => false
    _ => false
  }
}

///|
fn contains_function(expr : Expr) -> Bool {
  match expr {
    _ if @symcore.application_parts(expr) is Some(_) => true
    Expr::Derivative(_, _) | Expr::Subs(_, _, _) | Expr::Lambda(_, _) => true
    _ => {
      for child in @symcore.children(expr) {
        if contains_function(child) {
          return true
        }
      }
      false
    }
  }
}