///|
/// - Does: Applies one callback to nodes at a fixed depth in the expression tree.
/// - Input: One `Expr`, one callback, and optional `level`.
/// - Returns: One rewritten `Expr`.
/// - Limits: Negative levels are clamped to `0`, and atoms are left untouched below that point.
pub fn apply_at_level(
  expr : Expr,
  func : (Expr) -> Expr,
  level? : Int = 0,
) -> Expr {
  let depth = if level < 0 { 0 } else { level }
  use_at(expr, func, depth)
}

///|
fn use_at(expr : Expr, func : (Expr) -> Expr, depth : Int) -> Expr {
  if depth <= 0 {
    return func(expr)
  }
  if is_atom_traversal(expr) {
    return expr
  }
  @symcore.map_children(expr, child => use_at(child, func, depth - 1))
}

///|
fn is_atom_traversal(expr : Expr) -> Bool {
  match expr {
    Expr::Number(_) | Expr::Symbol(_) => true
    _ => false
  }
}