Skip to content

feat(libc): add exact long double and BSD math exports - #6345

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
hypermoist:feat/libc-exact-math
Oct 19, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
hypermoist:feat/libc-exact-math

Conversation

@hypermoist

Copy link
Copy Markdown
Contributor

What

Adds 40 libc math exports that AnyPS5 did not have, in a new src/MathExact.cpp. Every one is matched exactly against the console:

  • long double: copysignl, fdiml, fmodl, remainderl, remquol, logbl, ilogbl, rintl, roundl, lroundl, lrintl, llrintl, sqrtl, modfl, nanl, scalblnl, fmal
  • double/float: rint(f), lrint(f), llrint(f), drem(f), scalbln(f), scalb(f), finite(f)
  • classification: __fpclassifyl, __isinfl, __isnanl, __isnormall, _LDtest, _Dclass, _FDclass, _LDclass, _LDsign

Console behaviour that is reproduced (found by running the console binary side by side, see Tested):

  • fdiml, roundl, modfl, and fmal with a NaN z, return a signalling NaN operand unquieted; rint/rintf quiet it.
  • nanl ignores its argument and always returns the default quiet NaN.
  • fmodl sets EDOM whenever x is infinite or NaN, y is zero or y is NaN (NaN inputs included); remquol, remainderl and drem(f) set it only for real domain errors. scalb(f) never touches errno.
  • remquol reports the quotient modulo 8 with its sign (glibc can report ±8 where the console reports 0).
  • ilogbl(NaN) is INT_MAX; scalblnl truncates its exponent to int (scalblnl(1, 2^32 + 1) == 2).
  • fmal is FreeBSD's: x*y + z when x or y is zero or non-finite, x*y when z is zero, z when only z is non-finite, and x*y when z is more than 128 binades below the product (so a midpoint product rounds to even instead of using z's sign).
  • __fpclassifyl returns the Dinkumware codes (-2 denormal, -1 finite, 0 zero, 1 infinite, 2 NaN), like _LDtest; they differ only on pseudo-denormals, which _LDtest treats as finite. _LDsign returns the raw sign bit (-32768).

Two known gaps are recorded in docs/dev/debt/libc.md: remquol leaves *quo unchanged when the result is NaN (the console stores a value that depends on earlier x87 state), and non-canonical 80-bit encodings passed to the arithmetic functions follow the host x87.

Tested

Linux (CachyOS, GCC Release build, plus a clang -Wall -Wextra syntax check): new guest_math_exact test and the full ctest suite pass. tools/check_conventions.py passes; tools/pr_overlap.py --branch reports no line overlap.

Differential test against the console: the PPSA08427 libc.prx was mapped into a Linux process (its __error import bound to the test's errno) and all 40 functions were run side by side with this implementation on 200,000 random inputs each (random exponents and mantissas, signalling and quiet NaNs with payloads, infinities, zeros, subnormals, huge long exponents). Results agree bit for bit, and errno agrees, everywhere except the remquol NaN quotient noted above. All 40 NIDs are also exported by a newer libc.prx build (J03848509).

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q8tinbP1MUpUaDsbNWnGnu

copysignl, fdiml, fmodl, remainderl, remquol, logbl, ilogbl, rintl, roundl,
lroundl, lrintl, llrintl, sqrtl, modfl, nanl, scalblnl, fmal, rint(f),
lrint(f), llrint(f), drem(f), scalbln(f), scalb(f), finite(f),
__fpclassifyl, __isinfl, __isnanl, __isnormall, _LDtest, _Dclass, _FDclass,
_LDclass and _LDsign, matching the libc.prx bundled with PPSA08427 bit for
bit, including NaN propagation, errno and FreeBSD fmal special cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8tinbP1MUpUaDsbNWnGnu
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Progress report (9f5d4bd → 1181068)

📈 System libraries: 91.8% (+0.08%, +40 implemented functions)

✅ 40 implemented
Library Name
libc _Dclass_nid_postfix
libc _FDclass_nid_postfix
libc _LDclass_nid_postfix
libc _LDsign_nid_postfix
libc _LDtest_nid_postfix
libc __fpclassifyl_nid_postfix
libc __isinfl_nid_postfix
libc __isnanl_nid_postfix
libc __isnormall_nid_postfix
libc copysignl_nid_postfix
libc drem_nid_postfix
libc dremf_nid_postfix
libc fdiml_nid_postfix
libc finite_nid_postfix
libc finitef_nid_postfix
libc fmal_nid_postfix
libc fmodl_nid_postfix
libc ilogbl_nid_postfix
libc llrint_nid_postfix
libc llrintf_nid_postfix
libc llrintl_nid_postfix
libc logbl_nid_postfix
libc lrint_nid_postfix
libc lrintf_nid_postfix
libc lrintl_nid_postfix
libc lroundl_nid_postfix
libc modfl_nid_postfix
libc nanl_nid_postfix
libc remainderl_nid_postfix
libc remquol_nid_postfix
libc rint_nid_postfix
libc rintf_nid_postfix
libc rintl_nid_postfix
libc roundl_nid_postfix
libc scalb_nid_postfix
libc scalbf_nid_postfix
libc scalbln_nid_postfix
libc scalblnf_nid_postfix
libc scalblnl_nid_postfix
libc sqrtl_nid_postfix

This was referenced Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 7545 docs/dev/debt/libc.md L64

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 3987 docs/dev/debt/libc.md
PR 5021 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 5303 docs/dev/debt/libc.md
PR 6044 core/libs/prx/libc/CMakeLists.txt
PR 6226 docs/dev/debt/libc.md
PR 6469 docs/dev/debt/libc.md
PR 7039 docs/dev/debt/libc.md
PR 7081 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 7084 core/libs/prx/libc/CMakeLists.txt
PR 7201 core/libs/prx/libc/CMakeLists.txt
PR 7332 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md
PR 7715 core/libs/prx/libc/CMakeLists.txt
docs/dev/debt/libc.md

@DavidSamir

Copy link
Copy Markdown
Contributor

Title evidence for this PR: Spider-Man: Edge of Time (PPSA99300), added to the homebrew catalog today, stops on llrint on main.

Windows 11 10.0.26300, WinLibs GCC 15.2.0 r7, Release. main 0afa6a6 alone, then with this PR's commit 4f608a8 cherry-picked onto it (4 files, no conflicts); the catalog release converted with relinker --windows and run for 60 s.

main: the title exits after 1 s with 0xC0000139 (entry point not found):

FAIL: unresolved ELF import -431A-YBAks from libSceLibcInternal.prx

-431A-YBAks is llrint (tools/nid_names.py --nid=-431A-YBAks).

With this PR it starts, runs its ReXGlue startup and exits by itself with code 0 after reporting the files it needs from the user:

[Spider-Man Edge Of Time] startup: app: paths
[Spider-Man Edge Of Time] startup: fatal: reeot - game files missing: No Default.xex in /app0/gamedata. Install the game with reeot on a PC, then copy its game folder into the title's gamedata ...
[Spider-Man Edge Of Time] startup: main: returning success
[Spider-Man Edge Of Time] rex_ps5_app_main returned 0

That message is the title's own check for the game data it does not ship, so this PR is all it needs to reach it.

AI-assisted: yes

@jotitadev jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On macOS, drem, dremf and remainderl don't set EDOM. The other paths that the description says set errno (fmodl, remquol) set it explicitly. These three return std::remainder directly, so they rely on the host libm setting errno. glibc does that. Apple's libm never sets errno: math_errhandling is MATH_ERREXCEPT only.

Snippet, macOS 27.2, Apple clang (Xcode 27), M2 Max:

volatile double one = 1.0, zero = 0.0; volatile long double onel = 1.0L, zerol = 0.0L;
errno = 0; std::remainder(one, zero);   // errno
errno = 0; std::remainder(onel, zerol); // errno
== x86_64 (Rosetta)
math_errhandling=2 (MATH_ERRNO=1)
remainder(1,0)=nan errno=0 (EDOM=33)
remainderf(1,0)=nan errno=0
remainderl(1,0)=nan errno=0
== arm64
(same: errno=0 for all three)

So on a macOS host a guest gets errno == 0 where the console sets EDOM. CheckErrno in GuestMathExact.cpp would also abort at Require(std::isnan(drem_nid_postfix(1.0, 0.0)) && errno == EDOM). The fix would be the same explicit check that remquol already uses (!isnan(x) && !isnan(y) && (isinf(x) || y == 0) → errno = EDOM) in remainderl, drem and dremf. That makes them independent of the host libm.

Not tested: I did not build this PR. libc doesn't build on macOS from main, so this is the host-libm behaviour checked with a standalone snippet. Head 4f608a8.

Written with AI assistance (Claude Code). I ran the check above on my machine.

@nicolasfalesy

Copy link
Copy Markdown
Contributor

A title that reaches this: Spider-Man: Edge of Time (PPSA99300) from the homebrew catalog, on Linux (Intel Arc, Mesa ANV).

  • main 92e5041: stops at load with ./app.elf: symbol lookup error: ./app.elf: undefined symbol: -431A-YBAks (llrint, libc).
  • main + this PR: it loads and runs to its own check, startup: fatal: reeot - game files missing: No Default.xex in /app0/gamedata, then exits with status 0 (no game data was installed for the test).

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough differential testing. One thing to fix before merge, already reported by @jotitadev:

  • drem, dremf and remainderl (MathExact.cpp:83-85, :179-185) return std::remainder directly, so errno == EDOM for x infinite or y == 0 depends on the host libm. glibc sets it; Apple's libm (and likely MinGW's) does not, so a guest gets errno == 0 where the console sets EDOM, and CheckErrno (GuestMathExact.cpp:71) aborts on those hosts. Use the same explicit check as remquol (!isnan(x) && !isnan(y) && (isinf(x) || y == 0) → errno = EDOM) in all three, and add remainderl and dremf cases to the test.

@cadenasynyster

Copy link
Copy Markdown
Contributor

Windows results for this head (4f608a8), merged onto current main without conflicts. Windows 11, WinLibs GCC 15.2.0 r7 (x86_64-ucrt-posix-seh), Release.

  1. guest_math_exact aborts on Windows, 3 of 3 runs (exit code 0xc0000409, SIGABRT inside the test). It is not CheckErrno. I ran a local copy of the test where Require prints its line number and carries on, so every failing check shows: exactly one fails, GuestMathExact.cpp:77 in CheckConsoleQuirks, the pair scalblnl(1.0L, 0x100000001L) == 2.0L && scalblnl(1.0L, LONG_MAX) == 0.5L. I did not isolate which half. The LONG_MAX half cannot hold on Windows, since long is 32 bits there: a standalone program prints sizeof(long) = 4, LONG_MAX = 2147483647, and std::scalbln(1.0L, LONG_MAX) = inf. On the console (and Linux), truncating LONG_MAX to int gives -1, hence 0.5. Everything else in the test passes on Windows, including the drem errno row (CheckErrno, line 71). The check probably needs an exponent that does not depend on the width of the host long.

  2. errno of the host remainder family, for the review point on drem, dremf and remainderl. A standalone program calling std::remainder, std::fmod and std::remquo (not this PR's code), same toolchain:

  • float and double: errno = EDOM (33) for (1,0) and for (inf,1); errno stays 0 for NaN arguments. So drem and dremf get EDOM from the host here.
  • long double: errno stays 0 for every case, including (1,0) and (inf,1), for remainder, fmod and remquo.
    So on Windows/MinGW only remainderl has the gap; "likely MinGW's" holds for long double only. Going by this, a remainderl row in CheckErrno would abort on Windows without the explicit check (not run).

Not run: Linux and macOS, and the fix itself.

AI-assisted: yes

drem, dremf, remainderl and remquol now set EDOM themselves for an
infinite x or a zero y, as fmodl does. Apple's libm never sets errno and
MinGW skips it for long double, so the host could not be relied on.

Guest long is 64 bits, but the host long is 32 bits on Windows. The
lrint, lround and scalbln exports now take and return std::int64_t,
round through llrint and llround, and clamp scalbln exponents to the int
range, where the result is already infinite or zero. The test uses
INT64_MAX instead of LONG_MAX and covers remainderl, dremf and a 2^40
lrint result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adriadam10
adriadam10 merged commit 9817d53 into boykopovar:main Oct 19, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants