Repository navigation
Conversation
Rational functions that need an algebraic extension for their partial fraction decomposition came back with floating point coefficients: integrating 1/(x^3 - 1) produced 0.5773502691896257... where (1//3)*sqrt(3) was expected. The Risch backend already computes its roots exactly, as algebraic numbers, and only the conversion back to Symbolics was inexact: `to_symb` called Julia's `sqrt` on a `Rational`, which returns a float and contaminates the whole expression by promotion. Keep the radical as an unevaluated symbolic `sqrt` term instead, with the largest perfect square factor pulled out, so that a coefficient of sqrt(12//49) reads (2//7)*sqrt(3). The trial division that finds that factor is bounded, so a large radicand cannot make it run away; a factor it misses only leaves a less tidy radical, never an inexact one. Purely imaginary roots stay exact the same way: sqrt(-12) now gives (0 + 2im)*sqrt(3) rather than 3.4641016151377544im. This fixes the Risch backend part of JuliaSymbolics#135. The two-argument `integrate` goes through the rule-based backend, whose RUBI rules evaluate their own literal `sqrt(3)` to a float; that is left for separate work, because `sqrt` also appears in rule patterns which the matcher compares by name. Assisted-by: AI
Building an exact constant is not specific to integration. It is proposed upstream in JuliaSymbolics/SymbolicUtils.jl#1081; if that lands, this helper and split_perfect_square should be dropped in favour of it. Assisted-by: AI
Takes RuleBased from 92 to 93 successes on `TEST_GROUP=difficult` with no
regressions, and leaves Risch untouched (57/77/40/3, as on main). The
`[Rule Based] Integration of 177 functions` testset now passes.
Integrating 1/(x^2 - 2) with the default two-argument `integrate` gave
-0.7071067811865475atanh(x / 1.4142135623730951)
and now gives
(-atanh(x / sqrt(2))) / sqrt(2)
Three pieces were needed together; none of them is sufficient alone.
**Rewrite `sqrt` on the replacement side of each rule.** Rules are a
`Pair{Expr, Expr}` and the replacement is `eval`ed once a rule fires, so a
literal `sqrt(3)` there is Julia's `sqrt` on an `Int` and returns a float.
The AST of `rule.second` is rewritten to `rubi_sqrt`, and `rt(u, 2)` routed
through it. Only `rule.second` is touched: `sqrt` also appears in match
sides, which `check_expr_r` compares by name (`rule.args[1] === :sqrt`), so
renaming it there would stop every integrand containing a square root from
matching.
**Let the sign predicates decide on a constant.** `pos`, `gt`, `ge`, `lt`
and `le` gave up as soon as an argument was symbolic, so an exact `sqrt(2)`
made `pos` fall through to its `return true` default and rules took the
branch for the wrong sign. `constant_value` evaluates an expression that
denotes a number, which keeps the form exact while restoring the decisions
taken when these constants were floats.
**Let the matcher bind a defslot to a product of constants.** A numeric
coefficient folds into a single factor while an exact radical does not:
`-1.4142*x` has arguments `[-1.4142, x]` but `-sqrt(2)*x` has
`[-1, x, sqrt(2)]`, so a pattern like `(~!b)*(~x)` failed on length alone
and `1/(1 - sqrt(2)*x + x^2)` matched no rule at all — which is why
`1/(x^4 + 1)` came back with an unresolved integral inside it. The retry
collapses the constant factors into one. It is only reached after the
equal-length match has failed, so it can create a match but never break
one.
Four expected values in the Apostol test file were floats, because
`sqrt(3)` there is Julia's `sqrt` on an `Int`, so a result that is now
exact could not compare equal. They are written with the exact value,
wrapped in `Num` so the harness comparison against 0 holds.
`test/difficult_baseline.jl` is tightened for the one entry this improves,
`(x^4)/(4 + 5x^2 + x^4)`, which now verifies symbolically. Entry 132 is
left at its tolerant value, which its comment explains is deliberate for
platform variation, and the Risch entries are left alone since they belong
to JuliaSymbolics#139.
Assisted-by: AI
Verdict: changes needed before merge (reviewed at Preserves exact radicals in rule replacements and extends coefficient matching, on top of the unmerged Risch prerequisite. Request changes because constant evaluation gives incorrect signs and single-file rule reloads undo exactness. Risk assessment
Local verification used Symbolics 7.41.1, SymbolicUtils 4.48.0, Nemo 0.56.1 and AbstractAlgebra 0.50.2:
The separately delegated clean-main investigation established an exact dependency boundary for entry 15: SymbolicUtils 4.46.6 (identical tree to the parent of Scope checks: no new dependencies, public names, skipped tests, or loosened tolerances. The new SymbolicUtils accesses are public in the resolved version. The public Findings
Push a fix and the PR is reviewed again automatically at the new head. 🤖 Posted by an AI agent — harness: Claude Code · model: claude-opus-5-5[1m] (fleet master); review by Codex CLI 0.157.1 / gpt-6-astra |
The canonical radical form relies on the `simplify` rule that reduces `sqrt` of a literal, added in SymbolicUtils PR #1081. That rule first ships in v4.46.8: it was merged as `ef47adc2`, while `Project.toml` still read 4.46.7, and the next commit released 4.46.8. The tree of tag `v4.46.7` does not contain it. The old `"4.27"` bound still allowed 4.46.7, where the radicand is left unreduced and `test_exact_algebraic_coefficients.jl` fails on `sqrt(3)`, which is what CI resolved and why it was red. Also state in the test file header what is asserted rather than what used to happen, and make the file Runic-clean (explicit returns, `1.0e-10`). Assisted-by: AI
Three things, all from the review of this PR. **Rebuild `rubi_sqrt` on the current JuliaSymbolics#136.** That PR no longer introduces `exact_sqrt`; it builds the radical as an unevaluated `term(sqrt, y)` put through `simplify`, with the perfect-square extraction living upstream in SymbolicUtils 4.46.8. This branch still called `exact_sqrt`, so it would not have compiled once JuliaSymbolics#136 landed. `rubi_sqrt` now uses the same construction as `to_symb(::QQBarFieldElem)`, and the Apostol expected values follow. **Decide the sign of a constant exactly.** `constant_value` folds the tree in floating point, so nearly equal radicals cancel and the predicates returned a definite, wrong answer: for `rubi_sqrt(10^16 + 1) - 10^8`, whose value is about 5.0e-9, `pos` was `false` and `neg` and `le(u, 0)` were `true`. `pos`, `gt`, `ge`, `lt` and `le` now go through `constant_sign` / `compare_constants`, which keep the cheap floating point verdict while it is safely away from zero and otherwise decide over the algebraic numbers through `algebraic_constant_value`, using the `QQBarField` this package already depends on. Nonreal values are filtered with `is_real` before any comparison, so this cannot reach the `DomainError` of issue JuliaSymbolics#13, and an expression outside the supported shapes leaves the predicate as undecided as it is for a symbolic argument. **Rewrite roots on every load path.** `load_rules` rewrote the replacement side of a rule while `reload_rules` installed it raw at its three call sites, so reloading a shipped rule file during development brought floating point roots back: `integrate(1/sqrt(2 + x^2), x)` went from `asinh(x / sqrt(2))` to `asinh(x / 1.4142135623730951)`. Both paths now go through one `exact_roots_in_rule` helper so they cannot drift apart again. `test/methods/rule_based/test_exact_roots.jl` covers all of it, and is discriminating: without the two fixes it reports 5 failures in the sign testset and 2 in the reload testset; with them, 37 passed. `TEST_GROUP=easy`: 278 passed, 1 broken (pre-existing), exit 0. `TEST_GROUP=difficult` could not be completed on this machine, so the baseline gate in CI is the check to read for corpus regressions. Assisted-by: AI
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #141 +/- ##
==========================================
+ Coverage 50.98% 53.02% +2.03%
==========================================
Files 23 23
Lines 4309 4330 +21
==========================================
+ Hits 2197 2296 +99
+ Misses 2112 2034 -78 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
CI on this branch ran for the first time and the Maxima `difficult` job failed
with
UndefVarError: `rubi_sqrt` not defined in `SymbolicIntegration`
`lib/SymbolicIntegrationMaxima/test/runtests.jl` reads the same
`test/test_files` corpus, and `lib/SymbolicIntegrationMaxima/Project.toml` has
no `[sources]` entry, so there `SymbolicIntegration` is the registered release
rather than this working copy: an internal helper of the local tree cannot be
referenced from those files. `exact_sqrt` had the same problem before it was
renamed, it simply never surfaced because no CI run had reached this branch.
The four expected values now use `sqrt(Symbolics.Num(n))`, which is public API
and builds the same object: `isequal(sqrt(Symbolics.Num(3)), rubi_sqrt(3))` is
true, so the comparison outcome for every affected entry is unchanged. Replaying
the harness comparison
`isequal(simplify(computed - expected; expand = true), 0)` on the three
non-elliptic entries gives code 0, as before.
`SymbolicIntegration.elliptic_f`, also referenced from that file, is untouched:
it is already there on main and resolves in the Maxima environment.
`TEST_GROUP=easy`: 278 passed, 1 broken, exit 0.
Assisted-by: AI
CI on this branch reports `sqrt(2 - x - x^2)/x^2` (entry 158) going from `[ fail?]` to verifying symbolically, on all nine `difficult` jobs: Julia 1.10, 1 and pre across ubuntu, macos and windows. The harness asks for the baseline to be tightened on the pull request that causes an improvement, so the RuleBased expectation for that entry moves from 1 to 0. The five Risch improvements the same run lists ([3], [26], [27], [49], [55]) are left alone: they show up on unrelated pull requests too, so they come from dependency drift rather than from this change and belong in a separate baseline refresh. Assisted-by: AI
6596c60 to
1953759
Compare
SymbolicUtils #1079, the issue this PR's note and its numeric residual check worked around, was fixed by SymbolicUtils #1108 and closed on 2026-10-01. The fix is in v4.49.0 and absent from v4.48.0, which the old `"4.46.8"` bound still allowed; verified on 4.49.0 that `simplify(term(sqrt, 2)^2)` gives `2` rather than `2.0`. The documented caveat was therefore wrong and is rewritten rather than dropped, because a weaker version of it survives for a different reason. Measured on 4.49.0, the symbolic residual `d/dx F - f` now cancels to zero for `1/(x^3 - 1)`, `1/(x^4 + 1)`, `1/(x^2 + 2)` and `x/(x^4 + 1)`, but not for `1/(x^2 - 2)` and `1/(x^2 - 3)`, where `simplify` stops at an uncancelled product of conjugate factors. That residual contains no floating point number and evaluates to about 1e-17 at several points, so it is a completeness limit of `simplify`, not a loss of exactness. The numeric residual check stays for that reason, and the test gains the assertion that 4.49 makes meaningful: the simplified residual must contain no floating point literal, which is exactly what #1079 used to break. `TEST_GROUP=easy`: 264 passed, 1 broken (pre-existing), exit 0, with SymbolicUtils 4.49.0 and Symbolics 7.42.0. The new test file is Runic-clean. Also merges current main, which brings in JuliaSymbolics#147. Assisted-by: AI
Review suggestion 2 on JuliaSymbolics#136: the first paragraph of the `!!! note` and the matching test comment described what `simplify` used to do, which is of no use to a reader of the current code. Both now state the requirement — SymbolicUtils >= 4.49, whose `simplify` leaves a radical exact — and why the bound is there. The second paragraph of the note, about checking residuals numerically, is unchanged: the reviewer confirmed it is accurate, and it documents a limitation that is still real. 43 assertions in `test_exact_algebraic_coefficients.jl`, all passing. Assisted-by: AI
…in-rules Brings JuliaSymbolics#136 up to date in this branch, and with it current main, which had made this pull request conflict: JuliaSymbolics#147 added `nonnegative_substitution`, `drop_abs_of_nonnegative` and their hook in `int_and_subst` to the same `rules_utility_functions.jl` that this branch rewrites. Only `test/runtests.jl` conflicted, where both sides add an include; both are kept. The source file merged cleanly, and all of the functions from both sides are present: `nonnegative_substitution` and `drop_abs_of_nonnegative` from JuliaSymbolics#147, `algebraic_constant_value`, `exact_sign`, `constant_sign`, `compare_constants`, `exact_roots_in_rule` and `rubi_sqrt` from this branch. The `SymbolicUtils = "4.49"` bound comes in from JuliaSymbolics#136. `TEST_GROUP=easy`: 301 passed, 1 broken (pre-existing), exit 0. Assisted-by: AI
CI reported one RuleBased regression on this head, `[158] sqrt(2 - x - x^2)/x^2`, expected 0 and got 1, on ubuntu, macos and windows alike. It is not a regression of the engine. The residual `r - ref` carries floats — `0.35355339059327373` and `2.8284271247461903`, that is `1/(2*sqrt(2))` and `2*sqrt(2)` — and they come from the reference value, not from the result: in this corpus `sqrt(2)` is Julia's `sqrt` on an `Int`. The `SymbolicUtils = "4.49"` bound that JuliaSymbolics#136 brings into this branch makes the integrator's own output exact, so an exact result now faces an inexact reference and `simplify` cannot cancel the difference. The entry verified on the earlier head only because both sides were inexact. This is the same defect this PR already fixes for four other Apostol entries, and 158 is a fifth that was missed. Its two bare `sqrt(2)` become `sqrt(Symbolics.Num(2))`, the public spelling used for the others — `Symbolics` is reachable from `lib/SymbolicIntegrationMaxima`, which reads the same corpus, whereas an internal helper would not be. Verified: the reference is now free of floats, `simplify(r - ref; expand = true)` is `0`, and the harness scores the entry 0 again, so the baseline stays `(0, 2)`. `TEST_GROUP=difficult`: exit 0, **no regressions**, RuleBased up from 93 to 94 successes, Risch unchanged at 58 with 0 errors. The improvements the run still lists are left alone: entry 132 keeps its tolerant value, which its own comment explains returns code 2 on Julia pre and Windows, and the five Risch entries belong to JuliaSymbolics#148. Assisted-by: AI
|
Updated for review, and rebuilt on the current #136 — this had gone The two P2 findings were addressed in
One thing the merge surfaced that is worth flagging, since it is the same class of defect this
Assisted by: AI |
Finishes #135: the rule-based half, which is what the default two-argument
integrateuses and what produced the output quoted in that issue.Important
Depends on #136. This branch merges it, so the diff here includes that PR's commits; review or merge #136 first. The dependency is
rubi_sqrt, which builds its radical with the samesimplify(term(sqrt, y))construction that #136 introduces into_symb(::QQBarFieldElem), and which therefore needs theSymbolicUtils = "4.46.8"compat bound #136 sets. Once #136 lands, this PR's own diff is the twosrc/methods/rule_based/files, its test file, the Apostol expected values and two baseline entries.Result
TEST_GROUP=difficult: RuleBased goes from 92 to 93 successes with no regressions, and the[Rule Based] Integration of 177 functionstestset now passes. Risch is untouched (57 succeeded / 77 failed / 40 maybe failed / 3 errored, the same asmain; those 3 errors are the pre-existing #139 failures).TEST_GROUP=easypasses.Three pieces, needed together
1. Rewrite
sqrton the replacement side of each rule. Rules are aPair{Expr, Expr}, andrule2.jlevals the replacement once a rule fires — so a literalsqrt(3)there is Julia'ssqrton anIntand returns a float. The AST ofrule.secondis rewritten torubi_sqrt, andrt(u, 2)routed through it.Only
rule.secondis rewritten, and that matters:sqrtalso appears in match sides such as1/sqrt((~a) + (~b)*(~x)^2), andcheck_expr_rcompares those by name (rule.args[1] === :sqrt,operation(data) === sqrt). Renamingsqrtacross the rule files would silently stop every integrand containing a square root from matching.sqrt(1 + x)and1/sqrt(x^2 + 1)still integrate correctly here.2. Let the sign predicates decide on a constant.
pos,gt,ge,ltandlegave up as soon as an argument was symbolic. An exactsqrt(2)is symbolic but still denotes a number, soposfell through to itsreturn truedefault and rules took the branch for the wrong sign:constant_valueevaluates an expression that denotes a number, which keeps the form exact while restoring the decisions that were taken when these constants were floats.pos(x)and friends are unchanged for genuinely symbolic arguments.3. Let the matcher bind a defslot to a product of constants. This was the real blocker. A numeric coefficient folds into a single factor; an exact radical does not:
So a pattern like
1/((~!a) + (~!b)*(~x) + (~!c)*(~x)^2)failed on length alone, and1/(1 - sqrt(2)*x + x^2)matched no rule at all — which is why1/(x^4 + 1)came back with an unresolved integral inside it. The retry collapses the constant factors into one, so a slot can bind the whole coefficient. It is reached only after the equal-length match has failed, so it can create a match but never break one — the same shape as the existing fallbacks incheck_expr_r.Test changes
Four expected values in
Apostol Problems.jlwere floats, becausesqrt(3)written there is Julia'ssqrton anInt. A result that is now exact could not compare equal to them, so they are written with the exact value, wrapped inNumso that the harness comparison against0holds. The integrands are untouched.test/difficult_baseline.jlis tightened for the one entry this improves,(x^4)/(4 + 5x^2 + x^4), which now verifies symbolically. Entry 132 is left at its tolerant value — its comment explains that is deliberate for platform variation — and the Risch entries are left alone, since they belong to #139.Worth knowing
Symbolics.simplifyfolds exact radicals back into floats inconsistently (SymbolicUtils#1079), to the point wheresimplify(a - b)can report a nonzero residual for two structurally identical expressions containingsqrt(2). That is why the expected values had to become exact rather than the comparison being loosened. Results here are exact in the cases above, but a few still mix an exact radical with a float elsewhere in the expression; that is bounded by the same upstream behaviour.Disclosure: this change was written with AI assistance, and tested locally before opening the PR.
TEST_GROUP=easyandTEST_GROUP=difficultwere run on this branch and against unmodifiedmainfor comparison, the per-integral baseline diff was inspected case by case, and the four expected-value changes were each verified by differentiating the new antiderivative and checking the residual numerically. Every figure quoted above is copied from an actual run.Changes since the first review
Rebuilt on the current #136
#136 no longer introduces
exact_sqrt: it builds the radical as an unevaluatedSymbolicUtils.term(sqrt, y)put throughsimplify, and the perfect-square extraction nowlives upstream in SymbolicUtils 4.46.8. This branch was still calling
exact_sqrt, so itwould not have compiled once #136 landed.
rubi_sqrtnow uses the same construction asto_symb(::QQBarFieldElem), and the four Apostol expected values referencerubi_sqrtinstead of the removed helper. Verified:
rubi_sqrt(12//49)gives(2//7)*sqrt(3),rubi_sqrt(4)gives2,rubi_sqrt(-2)gives(0+1im)*sqrt(2), and the integrands in thetable above still produce the exact forms.
Finding 1: the sign of a constant is now decided exactly
constant_valuefolds the tree in floating point, so nearly equal radicals cancel and thepredicates could take a definite, wrong decision. The reviewer's example, with
exact_sqrtreplaced by
rubi_sqrt:pos,gt,ge,ltandlenow go throughconstant_sign/compare_constants, whichkeep the floating point verdict while it is far enough from zero (
sqrt(eps(Float64)),relative to the operands for a comparison) and otherwise decide over the algebraic numbers
via
algebraic_constant_value, using theQQBarFieldthis package already depends on. Thatpath is exact, so the answer is certified rather than tolerance-based; it is also much more
expensive, which is why it is only reached when the cheap value is inconclusive.
algebraic_constant_valuecovers rational literals,+,-,*,/, integer powers andsquare roots, which is what the rule table produces. Anything outside that, or a nonreal
value, yields
nothingand leaves the predicate exactly as undecided as it is for agenuinely symbolic argument. Nonreal values are filtered with
is_realbefore anycomparison, so this cannot hit the
DomainErrorof #13.Finding 2:
reload_rulesno longer bypasses the rewriteload_rulesrewrote the replacement side whilereload_rulesinstalled it raw at its threecall sites, so reloading a shipped file during rule development brought floating point roots
back:
Both paths now go through one
exact_roots_in_rulehelper, so they cannot drift apart again.Finding 3: formatting
The new test file is Runic-clean (
--checkexits 0), and the lines this PR adds or touchesin
rules_utility_functions.jlandrules_loader.jlno longer appear in Runic's diff. Therest of those two files was already non-clean on
main, verified by running Runic againstmain's copies, and is left alone.Tests
test/methods/rule_based/test_exact_roots.jl, wired into theeasygroup, coversrubi_sqrton exact, negative and symbolic arguments; that the defaultintegratereturnsno floating point literal for four integrands; the cancellation case above in both
directions plus an exact zero; that ordinary constants keep their decisions; and exactness
before and after
reload_rules.Failing before, passing after: on this branch without the two fixes the file reports 5
failures in the sign testset and 2 in the reload testset; with them it is 37 passed, 0
failed.
TEST_GROUP=easy: 278 passed, 1 broken (pre-existing), exit 0. Julia 1.13.0, SymbolicUtils4.48.0, Symbolics 7.41.1, Nemo 0.56.1, AbstractAlgebra 0.50.2.
TEST_GROUP=difficultwas not re-run locally for this revision: the machine I am oncould not keep the run alive. It matters here, because widening the sign predicates can
change which rule branch fires anywhere in the corpus, so the baseline gate in CI is the
check to read before merging this. For the same reason, the "92 to 93 successes" figure in
the description above was measured on the pre-review revision, before
rubi_sqrtwasrebuilt on the new #136 and before the sign predicates changed; treat CI's numbers on this
head as authoritative.
Two red checks on this PR are not caused by it: the
difficultjobs fail on baseline entry15,
sin(sqrt(1+x))/sqrt(1+x), now filed as #145; and the Maximaqajobs fail to resolvebecause of the stale 0.2 bound fixed by #146.
Assisted by: AI
CI result on this head
TEST_GROUP=difficultis the check I could not complete locally, and CI answers it:main-like heads (measured on Stop calling the deprecated AbstractAlgebra zeros #138, which doesnot touch rule selection), 0 errored.
pre × ubuntu / macos / windows):
[15] sin(sqrt(1+x))/sqrt(1+x), which is [RuleBased] change of variables loses the domain of the substituted variable, breaking ∫sin(sqrt(1+x))/sqrt(1+x)dx #145 and failsthe same way on every other open pull request. There are no Risch regressions at all.
So widening the sign predicates did not change any rule decision for the worse anywhere in
the 177-integrand corpus.
[158] sqrt(2 - x - x^2)/x^2, consistent across all ninejobs, and
test/difficult_baseline.jlis tightened for it here as the harness asks.The five Risch improvements the run lists ([3], [26], [27], [49], [55]) also appear on
unrelated pull requests, so they come from dependency drift rather than this change; they
would be worth a separate baseline refresh, which would let
difficultgo green again once#145 is addressed.
The earlier revision of this branch also failed the two Maxima
difficultjobs withUndefVarError: rubi_sqrt not defined in SymbolicIntegration, because the sharedtest/test_filescorpus is read bylib/SymbolicIntegrationMaximatoo, whereSymbolicIntegrationis the registered release rather than this working copy. Fixed byspelling those expected values with the public
sqrt(Symbolics.Num(n)), which builds anisequalobject. Those two jobs pass now.State on head
4b7fe12Rebuilt on the current #136, which brings current main with it. That was needed twice over:
the pull request had gone
CONFLICTING, because #147 addednonnegative_substitution,drop_abs_of_nonnegativeand their hook inint_and_substto the samerules_utility_functions.jlthis branch rewrites. Onlytest/runtests.jlconflicted, whereboth sides add an include; both are kept.
One real finding came out of the merge. CI reported a RuleBased regression on
[158] sqrt(2 - x - x^2)/x^2, expected 0 and got 1, on all platforms. It is not a regressionof the engine: the residual carries
0.35355339059327373and2.8284271247461903, that is1/(2*sqrt(2))and2*sqrt(2), and they come from the reference value, wheresqrt(2)isJulia's
sqrton anInt. TheSymbolicUtils = "4.49"bound makes the integrator's outputexact, so an exact result now faces an inexact reference and
simplifycannot cancel thedifference. The entry verified on the earlier head only because both sides were inexact.
That is the same defect this pull request already fixes for four Apostol entries; 158 was a
fifth, missed. Its two bare
sqrt(2)are nowsqrt(Symbolics.Num(2)), the public spellingthe others use —
lib/SymbolicIntegrationMaximareads the same corpus and cannot seeinternal helpers.
Verification
TEST_GROUP=difficult, locally, exit 0 and no regressions: RuleBased up from 93 to 94successes, Risch unchanged at 58, 0 errored. Two improvements are deliberately left in place —
entry 132 keeps its tolerant value, which its own comment records as returning code 2 on
Julia pre and Windows, and the five Risch entries belong to #148.
TEST_GROUP=easy: 301 passed, 1 broken (pre-existing), exit 0.CI on this head: 33 pass, 2 fail —
difficult11/11,easy9/9,qa9/9, docs and spellcheck green. The two failures are the
SymbolicIntegrationMaximaqajobs dying atresolution on the stale 0.2 bound that #146 fixes; it has failed on
mainsince the 0.3.0release and nothing here touches it.