use std::cell::Cell;
use ocas_atom::{Atom, AtomArena, AtomNode, Symbol};
use super::node_count;
pub(crate) const MAX_RESIDUAL_STEPS: u32 = 8;
pub(crate) const MAX_RESIDUAL_DEPTH: u32 = 2;
pub(crate) const MAX_RESIDUAL_NODES: usize = 64;
thread_local! {
static STATE: Cell<(u32, u32)> = const { Cell::new((MAX_RESIDUAL_STEPS, 0)) };
}
pub(crate) fn reset() {
STATE.with(|state| state.set((MAX_RESIDUAL_STEPS, 0)));
}
pub(crate) fn contains_residue(expr: Atom<'_>) -> bool {
match expr.node() {
AtomNode::Fun(name, args) if name.as_str() == "Integral" && args.len() == 2 => {
if matches!(args[1].node(), AtomNode::Var(_)) {
return true;
}
args.iter().any(|a| contains_residue(*a))
}
AtomNode::Fun(_, args) | AtomNode::Add(args) | AtomNode::Mul(args) => {
args.iter().any(|a| contains_residue(*a))
}
AtomNode::Pow(base, exp) => contains_residue(*base) || contains_residue(*exp),
AtomNode::Num(_) | AtomNode::Var(_) => false,
}
}
pub(crate) fn is_resolvable_shape(expr: Atom<'_>) -> bool {
match expr.node() {
AtomNode::Num(_) | AtomNode::Var(_) => true,
AtomNode::Fun(_, _) => false,
AtomNode::Add(args) | AtomNode::Mul(args) => args.iter().all(|a| is_resolvable_shape(*a)),
AtomNode::Pow(base, exp) => {
matches!(exp.node(), AtomNode::Num(_))
&& is_resolvable_shape(*base)
&& is_resolvable_shape(*exp)
}
}
}
const MAX_RESIDUAL_VARS: usize = 2;
fn few_symbols(expr: Atom<'_>) -> bool {
let mut seen: Vec<Symbol> = Vec::with_capacity(MAX_RESIDUAL_VARS);
fn walk(expr: Atom<'_>, seen: &mut Vec<Symbol>) -> bool {
match expr.node() {
AtomNode::Num(_) => true,
AtomNode::Var(v) => {
if seen.contains(v) {
return true;
}
if seen.len() >= MAX_RESIDUAL_VARS {
return false;
}
seen.push(*v);
true
}
AtomNode::Fun(_, args) | AtomNode::Add(args) | AtomNode::Mul(args) => {
args.iter().all(|a| walk(*a, seen))
}
AtomNode::Pow(base, exp) => walk(*base, seen) && walk(*exp, seen),
}
}
walk(expr, &mut seen)
}
pub(crate) fn count_residues(expr: Atom<'_>) -> usize {
match expr.node() {
AtomNode::Fun(name, args) if name.as_str() == "Integral" && args.len() == 2 => {
let own = usize::from(matches!(args[1].node(), AtomNode::Var(_)));
own + args.iter().map(|a| count_residues(*a)).sum::<usize>()
}
AtomNode::Fun(_, args) | AtomNode::Add(args) | AtomNode::Mul(args) => {
args.iter().map(|a| count_residues(*a)).sum()
}
AtomNode::Pow(base, exp) => count_residues(*base) + count_residues(*exp),
AtomNode::Num(_) | AtomNode::Var(_) => 0,
}
}
pub(crate) fn resolve<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
rules_enabled: bool,
rule_depth: usize,
parts_depth: usize,
max_steps: u32,
node_ceiling: usize,
) -> Atom<'a> {
if !contains_residue(expr) {
return expr;
}
let (steps_left, depth) = STATE.with(|state| state.get());
if steps_left == 0 || depth >= MAX_RESIDUAL_DEPTH {
return expr;
}
let allowance = steps_left.min(max_steps);
STATE.with(|state| state.set((steps_left, depth + 1)));
if super::trace_enabled() {
eprintln!(
"[trace] resolve-entry residues={} steps={} depth={}",
count_residues(expr),
steps_left,
depth
);
}
let before = count_residues(expr);
let mut walker = ResolveWalk {
rules_enabled,
rule_depth,
parts_depth,
node_ceiling,
allowance,
used: 0,
};
let out = super::chain::with_resolve_scope(|| walker.walk(ctx, expr));
let used = walker.used;
STATE.with(|state| state.set((steps_left.saturating_sub(used), depth)));
if count_residues(out) < before {
out
} else {
expr
}
}
struct ResolveWalk {
rules_enabled: bool,
rule_depth: usize,
parts_depth: usize,
node_ceiling: usize,
allowance: u32,
used: u32,
}
impl ResolveWalk {
fn walk<'a>(&mut self, ctx: &'a AtomArena<'a>, expr: Atom<'a>) -> Atom<'a> {
match expr.node() {
AtomNode::Fun(name, args) if name.as_str() == "Integral" && args.len() == 2 => {
let integrand = args[0];
let AtomNode::Var(var) = args[1].node() else {
return expr;
};
if self.used >= self.allowance || node_count(integrand) > self.node_ceiling {
if super::trace_enabled() {
eprintln!(
"[trace] resolve-skip size nodes={} used={} allowance={}",
node_count(integrand),
self.used,
self.allowance
);
}
return expr;
}
if !is_resolvable_shape(integrand) || !few_symbols(integrand) {
if super::trace_enabled() {
eprintln!(
"[trace] resolve-skip shape rational={} symbols={} :: {integrand}",
is_resolvable_shape(integrand),
few_symbols(integrand)
);
}
return expr;
}
if super::trace_enabled() {
eprintln!(
"[trace] resolve-attempt {} :: {integrand}",
node_count(integrand)
);
}
self.used += 1;
let resolved = crate::integral::integrate_raw(
ctx,
integrand,
*var,
0,
self.rules_enabled,
self.rule_depth,
self.parts_depth,
);
if resolved == expr {
return expr;
}
resolved
}
AtomNode::Fun(name, args) => {
let rebuilt: Vec<Atom<'a>> = args.iter().map(|a| self.walk(ctx, *a)).collect();
let rebuilt = ctx.fun(name.as_str(), &rebuilt);
if rebuilt == expr { expr } else { rebuilt }
}
AtomNode::Add(args) => {
let rebuilt: Vec<Atom<'a>> = args.iter().map(|a| self.walk(ctx, *a)).collect();
let rebuilt = ctx.add(&rebuilt);
if rebuilt == expr { expr } else { rebuilt }
}
AtomNode::Mul(args) => {
let rebuilt: Vec<Atom<'a>> = args.iter().map(|a| self.walk(ctx, *a)).collect();
let rebuilt = ctx.mul(&rebuilt);
if rebuilt == expr { expr } else { rebuilt }
}
AtomNode::Pow(base, exp) => {
let b = self.walk(ctx, *base);
let e = self.walk(ctx, *exp);
let rebuilt = ctx.pow(b, e);
if rebuilt == expr { expr } else { rebuilt }
}
AtomNode::Num(_) | AtomNode::Var(_) => expr,
}
}
}
#[cfg(test)]
mod tests {
use ocas_core::arena::Arena;
use ocas_parse::parse;
use super::*;
fn prep<'a>(ctx: &'a AtomArena<'a>, s: &str) -> Atom<'a> {
ocas_atom::normalize::normalize(ctx, parse(ctx, s).expect("parse"))
}
#[test]
fn counts_residues_in_any_variable() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x_residue = prep(&ctx, "Integral(x^2, x)");
let y_residue = prep(&ctx, "Integral(x^2, y)");
assert_eq!(count_residues(x_residue), 1);
assert_eq!(count_residues(y_residue), 1);
assert!(contains_residue(x_residue));
assert!(contains_residue(y_residue));
let nested = prep(&ctx, "Integral(x^2, x) + (Integral(x, y) + 1)^2");
assert_eq!(count_residues(nested), 2);
assert!(!contains_residue(prep(&ctx, "x + 1")));
}
#[test]
fn resolves_a_solvable_residue() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
reset();
let expr = prep(&ctx, "Integral(x^2, x)");
let out = resolve(
&ctx,
expr,
true,
0,
0,
MAX_RESIDUAL_STEPS,
MAX_RESIDUAL_NODES,
);
assert!(!contains_residue(out));
}
#[test]
fn resolves_a_substitution_variable_residue() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
reset();
let expr = prep(&ctx, "atan(t) + Integral(1/(1 + t^2), t)");
let out = resolve(
&ctx,
expr,
true,
0,
0,
MAX_RESIDUAL_STEPS,
MAX_RESIDUAL_NODES,
);
assert!(!contains_residue(out), "unresolved: {out}");
}
#[test]
fn keeps_an_unsolvable_residue_unchanged() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
reset();
let expr = prep(&ctx, "Integral(foo(x), x)");
let out = resolve(
&ctx,
expr,
true,
0,
0,
MAX_RESIDUAL_STEPS,
MAX_RESIDUAL_NODES,
);
assert_eq!(out, expr);
}
#[test]
fn non_rational_residues_are_left_alone() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
reset();
let expr = prep(&ctx, "Integral(log(x)/(1 + x^2), x)");
let out = resolve(
&ctx,
expr,
true,
0,
0,
MAX_RESIDUAL_STEPS,
MAX_RESIDUAL_NODES,
);
assert_eq!(out, expr);
}
#[test]
fn budget_exhaustion_leaves_the_residue() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
reset();
STATE.with(|state| state.set((0, 0)));
let expr = prep(&ctx, "Integral(x^2, x)");
let out = resolve(
&ctx,
expr,
true,
0,
0,
MAX_RESIDUAL_STEPS,
MAX_RESIDUAL_NODES,
);
assert_eq!(out, expr);
reset();
}
#[test]
fn oversized_residues_are_left_alone() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
reset();
let expr = prep(&ctx, "Integral(x^2, x)");
let out = resolve(&ctx, expr, true, 0, 0, MAX_RESIDUAL_STEPS, 1);
assert_eq!(out, expr);
reset();
}
}