Building a CAS in OCaml Part 5
2026-05-04 · 14 min
Part 4 can factor and cancel, and it still cannot differentiate sin(x). This part adds the calculus: a derivative table, Laurent series, limits read off a valuation, and symbolic integration, which is the first operation in the series that is not a decision procedure and has to admit it.
What Part 3 refused
Two refusals, both lifted here. The first by writing u^v as exp(v log u), the second by a table.
Part 3 diff sin(x), x -> no derivative rule for sindiff x^y, x -> power with a symbolic exponentPart 5 diff sin(x^2), x -> 2*x*cos(x^2)diff x^x, x -> x^x*(1 + log(x))
The table. Every rule is written with constructors Expr already has, so nothing introduces a function the simplifier has never seen.
let table (f : string) (args : Expr.t list) : Expr.t =let u = match args with [ a ] -> a | _ -> raise (Not_differentiable (f ^ ": arity")) inlet inv e = Expr.Pow (e, Expr.int (-1)) inlet sq e = Expr.Pow (e, Expr.int 2) inmatch f with| "exp" -> Expr.Fun ("exp", [ u ])| "log" -> inv u| "sin" -> Expr.Fun ("cos", [ u ])| "cos" -> Expr.Mul [ Expr.int (-1); Expr.Fun ("sin", [ u ]) ]| "tan" -> Expr.Add [ Expr.int 1; sq (Expr.Fun ("tan", [ u ])) ]| "sinh" -> Expr.Fun ("cosh", [ u ])| "cosh" -> Expr.Fun ("sinh", [ u ])| "tanh" -> Expr.Add [ Expr.int 1; Expr.Mul [ Expr.int (-1); sq (Expr.Fun ("tanh", [ u ])) ] ](* 1 / sqrt(1 - u^2) *)| "asin" -> Expr.Pow (Expr.Add [ Expr.int 1; Expr.Mul [ Expr.int (-1); sq u ] ], Expr.rat (-1) 2)| "acos" ->Expr.Mul[ Expr.int (-1);Expr.Pow (Expr.Add [ Expr.int 1; Expr.Mul [ Expr.int (-1); sq u ] ], Expr.rat (-1) 2) ]| "atan" -> inv (Expr.Add [ Expr.int 1; sq u ])| "sqrt" -> Expr.Mul [ Expr.rat 1 2; Expr.Pow (u, Expr.rat (-1) 2) ]| _ -> raise (Not_differentiable ("no derivative rule for " ^ f))
The general power rule
A constant exponent is the power rule. A symbolic one needs u^v = exp(v log u), and differentiating that gives
Reaching for it only when the exponent really is symbolic keeps the common case free of logarithms.
(* A constant exponent is the power rule and nothing more. *)| Expr.Pow (b, (Expr.Num n as ne)) ->Expr.Mul [ ne; Expr.Pow (b, Expr.Num (Rational.sub n Rational.one)); raw b v ](* u^v = exp(v log u), so the general rule isu^v * (v' log u + v u' / u). Reaching for it only when the exponentreally is symbolic keeps the common case free of logarithms. *)| Expr.Pow (b, x) ->let du = raw b v and dx = raw x v inExpr.Mul[ Expr.Pow (b, x);Expr.Add[ Expr.Mul [ dx; Expr.Fun ("log", [ b ]) ];Expr.Mul [ x; du; Expr.Pow (b, Expr.int (-1)) ] ] ]
Verbatim.
> diff x^x, xx^x*(1 + log(x))> diff a^x, xa^x*log(a)> diff exp(x)*log(x), xexp(x)*log(x) + exp(x)*x^(-1)> diff x^5, x, 360*x^2
Laurent series
A series here is a valuation and a coefficient array: the value is sum c_i x^(v+i). The valuation is what makes it Laurent rather than Taylor, and it is the whole reason limits work. sin(x)/x is not a power series at all, but it is a Laurent series with valuation 0, and its constant term is the limit.
Coefficients are rationals, so a coefficient that is zero is zero rather than 1e-17.
type t = {v : int; (* valuation: the exponent of the first term *)c : Rational.t array; (* c.(0) is the coefficient of x^v *)}let order (s : t) : int = Array.length s.c(* Drop leading zero coefficients, raising the valuation to match. Aseries that is entirely zero to the known order has no meaningfulvaluation, so it is reported as zero at the truncation point. *)let normalize (s : t) : t =let n = Array.length s.c inlet i = ref 0 inwhile !i < n && Rational.is_zero s.c.(!i) do incr i done;if !i = 0 then selse if !i = n then { v = s.v + n; c = [||] }else { v = s.v + !i; c = Array.sub s.c !i (n - !i) }
Two series are aligned over a common valuation before anything binary happens, and the known length shrinks to whatever both sides guarantee.
(* Re-express two series over a common valuation and length. *)let align (a : t) (b : t) : int * int * Rational.t array * Rational.t array =let v = min a.v b.v inlet n = min (a.v + order a) (b.v + order b) - v inlet n = max n 0 in(v, n,Array.init n (fun i -> coeff a (v + i)),Array.init n (fun i -> coeff b (v + i)))let add (a : t) (b : t) : t =let v, n, x, y = align a b innormalize { v; c = Array.init n (fun i -> Rational.add x.(i) y.(i)) }
The reciprocal falls out of a * a^-1 = 1 as a recurrence. The leading coefficient has to be nonzero, which normalize guarantees for anything that is not zero to the known order.
(* Reciprocal, by the recurrence that falls out of a * a^-1 = 1. Theleading coefficient must be nonzero, which normalize guarantees foranything that is not zero to the known order. *)let inv (a : t) : t =if is_zero a then raise Division_by_zero;let a = normalize a inlet n = order a inlet c = Array.make n Rational.zero inc.(0) <- Rational.inv a.c.(0);for i = 1 to n - 1 dolet acc = ref Rational.zero infor j = 1 to i doacc := Rational.add !acc (Rational.mul a.c.(j) c.(i - j))done;c.(i) <- Rational.neg (Rational.mul c.(0) !acc)done;{ v = -a.v; c }
Elementary functions, by recurrence
Defining exp by its Taylor coefficients would be a table; defining it by its differential equation makes composition free. For a series u with positive valuation, y = exp(u) satisfies y' = u' y, and matching coefficients gives
The positive-valuation requirement is not a limitation to work around: exp of a nonzero constant is not rational, so there is nothing to return.
let require_positive name (a : t) =let a = normalize a inif (not (is_zero a)) && a.v < 1 thenraise (Cannot_expand (name ^ ": needs a series with zero constant term"));alet exp_s (a : t) : t =let a = require_positive "exp" a inlet n = order a inif n = 0 then const Rational.one 0else beginlet y = Array.make n Rational.zero iny.(0) <- Rational.one;let da = diff a in(* n y_n = sum_{k=1..n} k a_k y_{n-k} *)for i = 1 to n - 1 dolet acc = ref Rational.zero infor k = 1 to i doacc := Rational.add !acc (Rational.mul (Rational.mul (Rational.of_int k) (coeff a k)) y.(i - k))done;ignore da;y.(i) <- Rational.div !acc (Rational.of_int i)done;{ v = 0; c = y }end
Checked against the textbook expansions. Compiled with ocamlopt 4.14.1.
exp(x) = 1 + x + 1/2*x^2 + 1/6*x^3 + 1/24*x^4 + 1/120*x^5 + 1/720*x^6 + 1/5040*x^7 + O(x^8)sin(x) = x - 1/6*x^3 + 1/120*x^5 - 1/5040*x^7 + O(x^9)cos(x) = 1 - 1/2*x^2 + 1/24*x^4 - 1/720*x^6 + O(x^8)log(1+x) = x - 1/2*x^2 + 1/3*x^3 - 1/4*x^4 + 1/5*x^5 - 1/6*x^6 + 1/7*x^7 - 1/8*x^8 + O(x^9)sqrt(1+x) = 1 + 1/2*x - 1/8*x^2 + 1/16*x^3 - 5/128*x^4 + 7/256*x^5 - 21/1024*x^6 + 33/2048*x^7 + O(x^8)tan-ish = x + 1/3*x^3 + 2/15*x^5 + 17/315*x^7 + O(x^9)exp(sin x) = 1 + x + 1/2*x^2 - 1/8*x^4 - 1/15*x^5 - 1/240*x^6 + 1/90*x^7 + O(x^8)
The identities are the better test, because a reference value and an implementation can be wrong together in a way an identity cannot.
(* The identities the expansions have to satisfy, which catch an errorthe reference values would not if both were wrong the same way. *)let test_identities () =let sin_ = Series.sin_s x and cos_ = Series.cos_s x and exp_ = Series.exp_s x incheck "sin^2 + cos^2 = 1"(Series.is_zero (Series.sub (Series.add (Series.mul sin_ sin_) (Series.mul cos_ cos_)) one));check "d/dx sin = cos" (Series.is_zero (Series.sub (Series.diff sin_) cos_));check "d/dx cos = -sin" (Series.is_zero (Series.add (Series.diff cos_) sin_));check "d/dx exp = exp" (Series.is_zero (Series.sub (Series.diff exp_) exp_));check "exp(x)*exp(-x) = 1"(Series.is_zero (Series.sub (Series.mul exp_ (Series.exp_s (Series.neg x))) one));check "log(1+x) then exp gives 1+x"(Series.is_zero (Series.sub (Series.exp_s (Series.log1p_s x)) (Series.add one x)));check "sqrt(1+x) squared is 1+x"(let q = Series.binomial_s x (r 1 2) n inSeries.is_zero (Series.sub (Series.mul q q) (Series.add one x)));check "integrating the derivative restores the series"(Series.is_zero (Series.sub (Series.integrate (Series.diff sin_)) sin_))
Where the known order goes
Accumulating into a zero series at valuation 0 silently throws away terms: alignment takes the minimum valuation, so adding a series at valuation 1 to zero at valuation 0 shortens the result by one. The first version of sin did exactly that, and sin(x)/x came back one term weaker than it should have been.
Starting from the first term rather than from zero is the whole fix.
let sin_s (a : t) : t =let a = require_positive "sin" a inlet n = order a inif is_zero a then aelse begin(* Accumulate starting from the first term rather than from a zeroseries at valuation 0: adding to zero would align the result downto valuation 0 and throw away known terms at the top. *)let a2 = mul a a inlet acc = ref a and term = ref a and k = ref 1 inlet fact = ref (Rational.of_int 6) inwhile 2 * !k + 1 <= n && not (is_zero !term) doterm := mul !term a2;let s = scale !term (Rational.inv !fact) inacc := (if !k mod 2 = 1 then sub !acc s else add !acc s);incr k;fact := Rational.mul !fact (Rational.of_int ((2 * !k) * ((2 * !k) + 1)))done;!accend
Before and after, on the same input.
before sin(x) = x - 1/6*x^3 + 1/120*x^5 - 1/5040*x^7 + O(x^8)sin(x)/x = 1 - 1/6*x^2 + 1/120*x^4 + O(x^7)after sin(x) = x - 1/6*x^3 + 1/120*x^5 - 1/5040*x^7 + O(x^9)sin(x)/x = 1 - 1/6*x^2 + 1/120*x^4 - 1/5040*x^6 + O(x^8)
Expanding an expression
The bridge from Expr to Series. A fractional power only has a Laurent expansion when the base is 1 plus something vanishing, which is why x^(1/2) is refused rather than approximated.
let rec expand (e : Expr.t) (v : string) (n : int) : Series.t =match e with| Expr.Num r -> Series.const r n| Expr.Sym s -> if s = v then Series.ident n else raise (Cannot_expand ("free symbol " ^ s))| Expr.Add xs -> List.fold_left (fun acc x -> Series.add acc (expand x v n)) (Series.zero n) xs| Expr.Mul xs ->List.fold_left (fun acc x -> Series.mul acc (expand x v n)) (Series.const Rational.one n) xs| Expr.Pow (b, Expr.Num r) when Rational.is_integer r -> (match Rational.to_int_opt r with| Some k -> Series.pow_int (expand b v n) k| None -> raise (Cannot_expand "exponent too large"))(* A fractional power only has a Laurent expansion when the base is1 + (something vanishing); x^(1/2) is not a Laurent series at all. *)| Expr.Pow (b, Expr.Num r) -> binomial_of (expand b v n) r n| Expr.Pow _ -> raise (Cannot_expand "symbolic exponent")| Expr.Fun (f, args) -> fn f (List.map (fun a -> expand a v n) args) n
And the function cases.
and fn (f : string) (args : Series.t list) (n : int) : Series.t =let one = Series.const Rational.one n inmatch (f, args) with| "exp", [ u ] -> Series.exp_s u| "log", [ u ] -> Series.log1p_s (unit_part u n)| "sin", [ u ] -> Series.sin_s u| "cos", [ u ] -> Series.cos_s u| "tan", [ u ] -> Series.div (Series.sin_s u) (Series.cos_s u)| "sqrt", [ u ] -> binomial_of u half n| "sinh", [ u ] ->Series.scale (Series.sub (Series.exp_s u) (Series.exp_s (Series.neg u))) half| "cosh", [ u ] ->Series.scale (Series.add (Series.exp_s u) (Series.exp_s (Series.neg u))) half| "tanh", [ u ] ->let e = Series.exp_s (Series.scale u (Rational.of_int 2)) inSeries.div (Series.sub e one) (Series.add e one)(* atan u = integral of u' / (1 + u^2) *)| "atan", [ u ] -> Series.integrate (Series.div (Series.diff u) (Series.add one (Series.mul u u)))| _ -> raise (Cannot_expand ("no expansion for " ^ f))
Limits
Once the expansion exists the limit is not a computation, it is a look at the valuation.
Three cases, and nothing else.
(* Read the limit off the valuation: a positive valuation means everyterm vanishes, zero means the constant term is the answer, and anegative one means it blows up with the sign of the leadingcoefficient. *)let of_series (s : Series.t) : result =let s = Series.normalize s inif Series.is_zero s then Finite Expr.num_zeroelse if s.Series.v > 0 then Finite Expr.num_zeroelse if s.Series.v = 0 then Finite (Expr.Num (Series.coeff s 0))else if Rational.sign (Series.coeff s s.Series.v) > 0 then PosInfelse NegInf
A leading coefficient that cancels at one truncation may be nonzero at the next, so the order is increased rather than trusted.
(* Try increasing orders: a leading coefficient that cancels to zero atone truncation may be nonzero at the next, and only a series that isgenuinely zero stays zero. *)let with_orders (f : int -> 'a) : 'a =let rec go = function| [] -> raise (Cannot_expand "no order sufficed")| n :: rest -> ( try f n with Cannot_expand _ when rest <> [] -> go rest)ingo [ 10; 20; 32 ]
x -> infinity is the same machinery under x = 1/t.
(* x -> infinity becomes t -> 0 under x = 1/t. *)let at_infinity (e : Expr.t) (v : string) : result =with_orders (fun n ->let sub = Expr.substitute v (Expr.Pow (Expr.Sym v, Expr.int (-1))) (Expr.simplify e) inof_series (expand sub v n))
Verbatim.
> limit sin(x)/x, x, 01> limit (1 - cos(x))/x^2, x, 01/2> limit (exp(x) - 1 - x)/x^2, x, 01/2> limit 1/x^2, x, 0+infinity> limit (2*x^2 + 3)/(x^2 - 1), x, oo2> limit atan(1/x), x, oo0
This is not Gruntz. There is no most-rapidly-varying subexpression analysis, so a limit whose answer is invisible in any finite truncation, anything needing an exponential tower, is refused rather than guessed at.
Integration, and what is decidable
Differentiation is total and integration is not, which changes what the code can promise.
differentiation total every elementary function has anelementary derivativeintegration partial exp(x^2) has no elementary antiderivative,and deciding that in general is the Rischalgorithm, which is not hererational functions decidable every one has an elementary antiderivative,and it is a rational part plus logarithmsand arctangents. That part is a decisionprocedure and is implemented in full.
Rational functions
Partial fractions from Part 4 reduce the problem to one term at a time. A linear factor gives a logarithm or a power; an irreducible quadratic gives a logarithm and an arctangent.
One term. The numerator is split into a multiple of the denominator's derivative plus a constant, so the first piece integrates by the power rule and only the constant needs the recurrence.
(* One partial-fraction term: numerator / g^j with deg numerator < deg g. *)let term_integral (ctx : Algebra.ctx) (num : Poly.t) (g : Poly.t) (j : int) (v : string) : Expr.t =let x = Expr.Sym v inlet dg = Poly.degree_in g v inlet rat p = match Poly.to_rational p with Some r -> r | None -> raise (Cannot_integrate "non-constant coefficient") inif dg = 1 then begin(* g = a x + b, numerator is a constant k. *)let a = rat (Poly.coeff_in g v 1) and b = rat (Poly.coeff_in g v 0) inlet k = rat num inlet ge = Expr.Add [ Expr.Mul [ Expr.Num a; x ]; Expr.Num b ] inif j = 1 then Expr.simplify (Expr.Mul [ Expr.Num (Rational.div k a); Expr.Fun ("log", [ ge ]) ])elselet c = Rational.div k (Rational.mul a (Rational.of_int (1 - j))) inExpr.simplify (Expr.Mul [ Expr.Num c; Expr.Pow (ge, Expr.int (1 - j)) ])endelse if dg = 2 then beginlet a = rat (Poly.coeff_in g v 2) and b = rat (Poly.coeff_in g v 1) and c = rat (Poly.coeff_in g v 0) inlet p = rat (Poly.coeff_in num v 1) and q = rat (Poly.coeff_in num v 0) in(* Split the numerator into a multiple of g' plus a constant, so thefirst piece integrates by the power rule and only the constantneeds the recurrence. *)let alpha = Rational.div p (Rational.mul (Rational.of_int 2) a) inlet beta = Rational.sub q (Rational.mul alpha b) inlet ge = Algebra.of_poly ctx g inlet first =if Rational.is_zero alpha then Expr.num_zeroelse if j = 1 then Expr.Mul [ Expr.Num alpha; Expr.Fun ("log", [ ge ]) ]elseExpr.Mul[ Expr.Num (Rational.div alpha (Rational.of_int (1 - j))); Expr.Pow (ge, Expr.int (1 - j)) ]inlet second =if Rational.is_zero beta then Expr.num_zeroelse Expr.Mul [ Expr.Num beta; inv_quadratic a b c j v ]inExpr.simplify (Expr.Add [ first; second ])endelse raise (Cannot_integrate "irreducible factor of degree 3 or more")
The recurrence for the constant part is the standard reduction
bottoming out at j = 1, which is the arctangent when D > 0 and a pair of logarithms when it is negative. Both cases occur: x^2 - 2 is irreducible over the rationals and still has real roots.
Which is why the discriminant is tested rather than assumed.
let rec inv_quadratic (a : Rational.t) (b : Rational.t) (c : Rational.t) (j : int) (v : string) : Expr.t =let x = Expr.Sym v inlet q = Expr.Add [ Expr.Mul [ Expr.Num a; Expr.Pow (x, Expr.int 2) ]; Expr.Mul [ Expr.Num b; x ]; Expr.Num c ] inlet d = Rational.sub (Rational.mul (Rational.of_int 4) (Rational.mul a c)) (Rational.mul b b) inif j = 1 then beginif Rational.is_zero d then raise (Cannot_integrate "degenerate quadratic");if Rational.sign d > 0 then begin(* 2/sqrt(D) * atan((2ax + b)/sqrt(D)) *)let s = sqrt_expr d inExpr.simplify(Expr.Mul[ Expr.int 2; Expr.Pow (s, Expr.int (-1));Expr.Fun ("atan",[ Expr.Mul[ Expr.Add [ Expr.Mul [ Expr.Num (Rational.mul (Rational.of_int 2) a); x ]; Expr.Num b ];Expr.Pow (s, Expr.int (-1)) ] ]) ])endelse begin(* Real distinct roots: 1/(a(x-r1)(x-r2)) splits into two logs. *)let s = sqrt_expr (Rational.neg d) inlet two_a = Expr.Num (Rational.mul (Rational.of_int 2) a) inlet u = Expr.Add [ Expr.Mul [ two_a; x ]; Expr.Num b ] inExpr.simplify(Expr.Mul[ Expr.Pow (s, Expr.int (-1));Expr.Add[ Expr.Fun ("log", [ Expr.Add [ u; Expr.Mul [ Expr.int (-1); s ] ] ]);Expr.Mul [ Expr.int (-1); Expr.Fun ("log", [ Expr.Add [ u; s ] ]) ] ] ])endendelse beginif Rational.is_zero d then raise (Cannot_integrate "degenerate quadratic");let jm = Rational.of_int (j - 1) inlet denom = Rational.mul jm d inlet first =Expr.Mul[ Expr.Add [ Expr.Mul [ Expr.Num (Rational.mul (Rational.of_int 2) a); x ]; Expr.Num b ];Expr.Pow (Expr.Num denom, Expr.int (-1));Expr.Pow (q, Expr.int (-(j - 1))) ]inlet coeff =Rational.div (Rational.mul (Rational.of_int (2 * ((2 * j) - 3))) a) denominExpr.simplify (Expr.Add [ first; Expr.Mul [ Expr.Num coeff; inv_quadratic a b c (j - 1) v ] ])end
The kernel trap
The polynomial layer turns anything non-polynomial into an opaque variable, which is what makes expand work on sin(x)*(x+1)^2. In an integrator it is a bug waiting to happen, and it happened.
What the first version returned. It is not a bad answer, it is a wrong one.
> integrate x*exp(x), x1/2*exp(x)*x^2 <- wrongexp(x) became an opaque variable, the integrator saw a constanttimes x, and integrated it as one. The real answer needsintegration by parts, which is not implemented.
The guard. A kernel that mentions the variable of integration is not a constant, and refusing is the only correct response.
(* The polynomial layer turns anything non-polynomial into an opaquevariable. That is exactly wrong here if the kernel depends on thevariable of integration: exp(x) would be carried along as a constantand x*exp(x) would integrate to x^2 exp(x)/2. Refuse instead. *)let check_kernels (ctx : Algebra.ctx) (p : Poly.t) (v : string) : unit =List.iter(fun name ->if name <> v thenmatch List.assoc_opt name ctx.Algebra.kernels with| Some k when List.mem v (Expr.free_symbols k) ->raise (Cannot_integrate ("cannot integrate in terms of " ^ Expr.to_string k))| _ -> ())(Poly.vars_of p)
Verification
Every integral here is checked by differentiating it. That is a real test rather than a restatement: differentiation lives in another module, uses another method, and is decidable where integration is not.
The property.
(* The check that makes the whole thing testable: differentiate theanswer and compare with the integrand. *)let verify (e : Expr.t) (v : string) : bool =match integrate e v with| exception Cannot_integrate _ -> false| anti ->let d = Deriv.diff anti v inExpr.compare_expr (Algebra.together (Expr.Add [ d; Expr.Mul [ Expr.int (-1); e ] ])) Expr.num_zero = 0
Every rational function in the suite, verified this way. Verbatim.
integrand antiderivative checkx^2 1/3*x^3 verified1/x log(x) verified1/(x^2 - 1) -1/2*log(1 + x) + 1/2*log(-1 + x) verified1/(x^2 + 1) atan(x) verified1/(x^3 + x) log(x) - 1/2*log(1 + x^2) verified1/((x - 1)^2*(x + 2)) -1/3*(-1 + x)^(-1) - 1/9*log(-1 + x)+ 1/9*log(2 + x) verified1/(x^2 + x + 1) 2*atan(3^(-1/2)*(1 + 2*x))*3^(-1/2) verified1/(x^2 + 1)^2 1/2*x*(1 + x^2)^(-1) + 1/2*atan(x) verifiedexp(2*x + 1) 1/2*exp(1 + 2*x) verifiedlog(x) -x + x*log(x) verified
What is refused, and why that is the feature
Three refusals, all honest.
> integrate x*exp(x), xcannot integrate: cannot integrate in terms of exp(x)> integrate sin(x^2), xcannot integrate: function of a non-linear argument> integrate tan(x), xcannot integrate: no table entry for tan
The last one is deliberate and is worth explaining. -log(cos x) is the right answer, and it was in the table until it failed its own verification: nothing in this system knows that tan = sin/cos, so the check could not confirm it. An entry whose correctness the system cannot demonstrate does not belong in the table.
Tests
The randomized one: build a rational function with a planted denominator, integrate it, differentiate the result, and demand zero.
(* Randomized: build a rational function with a planted denominator anddemand that it integrates and differentiates back. *)let test_random_rational () =Random.init 5150;let x = Expr.Sym "x" infor _ = 1 to 60 dolet lin () = Expr.Add [ x; Expr.int (Random.int 7 - 3) ] inlet quad () = Expr.Add [ Expr.Pow (x, Expr.int 2); Expr.int (1 + Random.int 4) ] inlet den =match Random.int 3 with| 0 -> Expr.Mul [ lin (); lin () ]| 1 -> Expr.Mul [ lin (); quad () ]| _ -> quad ()inlet num = Expr.Add [ Expr.Mul [ Expr.int (1 + Random.int 4); x ]; Expr.int (Random.int 5) ] inlet f = Expr.simplify (Expr.Mul [ num; Expr.Pow (den, Expr.int (-1)) ]) inmatch Integrate.integrate f "x" with| exception Integrate.Cannot_integrate _ -> ()| exception Division_by_zero -> ()| anti ->let back = Algebra.together (Expr.Add [ Deriv.diff anti "x"; Expr.Mul [ Expr.int (-1); f ] ]) incheck "a random rational function integrates back"(Expr.compare_expr back Expr.num_zero = 0)done
What is deliberately not here
Three gaps, in order of how much they hurt.
Risch the decision procedure for elementary integration.Without it, integrate refuses rather than provingthat exp(x^2) has no elementary antiderivative.integration by parts and substitution. x*exp(x) is refused, not solved.Gruntz limits are read off a series, so anything needingan exponential tower is out of reach.
Download
The snapshot at the end of this part. New in Part 5: deriv.ml, series.ml, limit.ml, integrate.ml, and the tests test_deriv.ml, test_series.ml, test_limit.ml, test_integrate.ml. Unchanged from Part 4: bigint.ml, rational.ml, expr.ml, fp.ml, upoly.ml, poly.ml, factor.ml, ratfun.ml, parser.ml, and main.ml.