Fix cube roots: real odd roots of negative bases - #82
Merged
Conversation
Math.pow(-8, 1/3) and the GLSL pow() builtin both return NaN, so any negative base with a fractional exponent rendered/evaluated as undefined even for real odd roots like cube roots. Graphing calculators (Desmos, etc.) instead find the exponent's rational form p/q in lowest terms via a tolerance search over small denominators, and return a real result when q is odd (sign * |a|^b, sign negative iff p is odd). Even roots of negative numbers (e.g. (-4)^(1/2)) stay undefined. Implemented in both the CPU evaluator (realPow() in lib/expr.ts) and the GLSL shader codegen (eq_pow() in lib/glsl.ts) so hover values, root-finding, and the rendered curve all agree. The two are documented as needing to stay in sync since GLSL can't share TS source. Fixes #15
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
equation | 8ebdf76 | Commit Preview URL Branch Preview URL |
Aug 03 2026, 11:52 AM |
There was a problem hiding this comment.
Pull request overview
Adds real odd-root support for negative bases in CPU and GLSL evaluation.
Changes:
- Introduces
realPow()with rational-exponent detection. - Updates GLSL power handling.
- Adds evaluator tests for odd and even roots.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
lib/expr.ts |
Adds CPU-side real-power evaluation. |
lib/glsl.ts |
Adds equivalent shader-side handling. |
lib/expr.test.ts |
Tests negative-base fractional powers. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| case '*': return a * b; | ||
| case '/': return a / b; | ||
| case '^': return Math.pow(a, b); | ||
| case '^': return realPow(a, b); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #15.
(-8)^(1/3)(and any negative base with a fractional exponent) previously evaluated toNaNeverywhere — both in the CPU evaluator (Math.pow(-8, 1/3)isNaNin JS) and in the rendered curve (GLSL'spow()is undefined for negative bases). The only existing special case was for exponents that are (numerically) exact integers.This matches how graphing calculators like Desmos handle it: for a negative base
aand exponentb, findb's rational formp/qin lowest terms via a tolerance search over small denominators. Ifqis odd, the root is real:sign * |a|^b, wheresignis negative iff the reduced numeratorpis odd. Ifqis even (an even root, e.g.(-4)^(1/2)), or no small-denominator match is found (an irrational-looking exponent), the result staysNaN, same as before.Changes
lib/expr.ts: addedrealPow(a, b), used byevaluate()'s^case instead of a bareMath.pow. Handles the CPU-side path (hover values, root-finding, etc).lib/glsl.ts: rewrote theeq_pow()GLSL helper with the same rational-exponent algorithm (GLSL has no big-int/rational type, so it's implemented with a fixed denominator searchq = 1..12and the existingeq_gcdhelper instead of a real gcd loop). Used for rendering curves/surfaces.lib/expr.test.ts: added adescribe('negative base with fractional exponent (real odd roots)')block covering cube roots,p/qwith even numerator, negative exponents, even roots staying undefined, and that a typed decimal like0.33333is not snapped to1/3.Both implementations use the same tolerance (
1e-6) and max denominator (12), and are commented as needing to stay in sync since GLSL source text and TS can't share code directly.Examples verified
8^(1/3)2(unaffected, positive base)(-8)^(1/3)-2(-8)^(2/3)4(-1)^(1/3)-1(-8)^(-1/3)-0.5(-4)^(1/2)NaN(even root, correctly undefined)(-8)^(1/4)NaN(q=4 even)(-8)^(0.33333)NaN(deliberately not snapped — see tolerance note below)(-2)^3,(-2)^2-8,4(existing integer-exponent behavior unchanged)On the tolerance choice:
1e-6is tight enough that an exponent entered as an actual fraction (1/3parses to0.3333333333333333, ~1e-16 from the true rational) always snaps, while a typed decimal approximation like0.33333(~3.3e-6 away from1/3) is left undefined rather than silently guessed at. This is documented in a comment onrealPow().Test plan
npx vitest run— all 495 tests pass (including the new cases)npx tsc --noEmitandnpx tsc -p web --noEmit— cleannpx tsc -p worker --noEmithas pre-existing errors in this sandbox unrelated to this change (missingwrangler types-generated ambient globals — confirmed identical errors on a clean checkout withwranglerunavailable in this environment)🤖 Generated with Claude Code