Skip to content

Gather a quotient of powers whose exponents a rewrite has already moved (#740) - #748

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
fix/gather-power-quotients
Aug 5, 2026
Merged

Gather a quotient of powers whose exponents a rewrite has already moved (#740)#748
Rafael-SOWNet merged 1 commit into
masterfrom
fix/gather-power-quotients

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Closes #740.

What was wrong

a^p / b^p is gathered into (a/b)^p, and stopped being gathered as soon as one of the bases was itself a power:

"(a ^ 2) ^ x / (b ^ 2) ^ x".Simplify()      // (a / b) ^ (2 * x)     ✅
"(a ^ 2) ^ x / b ^ x".Simplify()            // unchanged             ❌
"(a ^ 2 + 1) ^ x / (a ^ 2) ^ x".Simplify()  // unchanged             ❌

It is not the shape of the quotient but the order the rules run in. The rule that rewrites (b^c)^p as b^(c*p) applies to the child, on the way up, so by the time the pair of powers is looked at it has already happened:

  • where both bases are powers it moves both exponents together, the exponents still match, and the pair survives — which is why the first line works, and why this looked like a question of shape;
  • where one base is a power it moves one exponent and not the other, they stop matching, and there is nothing left to pair on.

What the fix does

Reads the pair back: a^p / b^(c*p) is (a / b^c)^p, and the same the other way up.

Restricted to a whole c, so b^c moves into the base and nothing gains a root it did not have. A fractional c would have to divide the exponent instead, turning (sqrt(x) + 1)^x / sqrt(x)^x into ((sqrt(x) + 1)^2 / x)^(x/2) — a gathered form bought with a squared numerator. That is a judgement about output rather than the gap this fixes, so the issue's second example is deliberately left as it stands, and pinned as such.

was is
(a^2 + 1)^x / (a^2)^x unchanged (1 + 1/a^2)^x
(x^3 + 1)^x / (x^3)^x unchanged (1 + 1/x^3)^x
(a^2)^x / b^x unchanged (a^2 / b)^x
x^(2a) / y^a unchanged (x^2 / y)^a
2^(2x) / 3^x unchanged (4/3)^x
x^4 / y^2 unchanged unchanged — a numeric exponent is not a product, so it matches nothing here

Why it is worth gathering

The limit machinery reads a 1^oo off a single power and cannot see one in a quotient, so the same function was answered or not according only to how it had been written:

was is
lim x->+oo (x^2 + 1)^x / (x^2)^x unevaluated after 5.5 s 1 in 31 ms
lim x->+oo (x^3 + 1)^x / (x^3)^x unevaluated 1
lim x->+oo ((x^2 + 1) / x^2)^x 1 (unchanged) 1

Measured

The tests pin the value as well as the shape — the rewrite is checked numerically at a point where every base is positive, which is where a^p / b^(c*p) = (a/b^c)^p holds without a branch argument — and the limits #739 fixed are pinned alongside, since they go through this same gathering.

🤖 Generated with Claude Code

…ed (#740)

`a^p / b^p` is gathered into `(a/b)^p`, and stopped being gathered as soon as
one of the bases was itself a power:

    (a^2)^x / (b^2)^x   gathers
    (a^2)^x / b^x       did not
    (a^2 + 1)^x / (a^2)^x  did not

It is not the shape of the quotient but the order the rules run in. The rule
that rewrites `(b^c)^p` as `b^(c*p)` applies to the *child*, on the way up, so
by the time the pair of powers is looked at it has already happened. Where both
bases are powers it moves both exponents together and the pair survives -- which
is why the first line above works, and why it looked like the shape mattered.
Where only one base is a power it moves one exponent and not the other, the
exponents stop matching, and there is nothing left to pair on.

So the pair is read back: `a^p / b^(c*p)` is `(a / b^c)^p`, and the same the
other way up. Restricted to a whole `c`, so that `b^c` moves into the base and
nothing gains a root it did not have -- a fractional `c` would have to divide
the exponent instead, turning `(sqrt(x) + 1)^x / sqrt(x)^x` into
`((sqrt(x) + 1)^2 / x)^(x/2)`, which buys a gathered form with a squared
numerator. That is a judgement about output rather than the gap this fixes, so
it is left as it stands and pinned as such.

Why it is worth gathering: the limit machinery reads a `1^oo` off a single
power and cannot see one in a quotient, so the same function was answered or
not according only to how it had been written.

    lim x->+oo (x^2 + 1)^x / (x^2)^x    unevaluated after 5.5 s  ->  1 in 31 ms
    lim x->+oo (x^3 + 1)^x / (x^3)^x    the same

A quotient of numeric powers is untouched, because a numeric exponent is not
written as a product and so matches nothing here: `x^4 / y^2` keeps its form.
`2^(2x) / 3^x` becoming `(4/3)^x` is the one visible widening beyond the issue.

Suite 4979 -> 4986 passed / 0 failed, F# 130/130, corpus 112/117 with 0 wrong
and every verdict and answer byte-identical. rootcheck is 595/596 on this
branch, its one incomplete case being #744, fixed in PR #745 rather than here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet merged commit bced7bf into master Aug 5, 2026
24 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/gather-power-quotients branch August 5, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Simplify does not gather a^p / b^p into (a/b)^p when a base is itself a power

1 participant