Linux/WASM: portable libm differs from Apple's pow/sin/cos/atan2 (2470/2589 e2e)
On Linux and in the WASM build, the port uses portable math functions (graphviz_pure_core::math::portable), because macOS's libsystem_m, the libm that C Graphviz uses on macOS and that the goldens were made with, is closed source. Apple's pow, sin, cos and atan2 give a different last bit from both the musl-derived libm crate and correctly rounded implementations on some inputs. One bit is enough to move a force-directed layout. With Apple's libm the port matches C on all 2,589 gv-diff e2e outputs. With portable math it matches 2,470: 114 sfdp, 3 fdp, 1 circo and 1 main (gallery_beautify_1_1cfb90, an sfdp graph). The same files explain the portable layout ratchets: main 564/565, sfdp 183/297, fdp 382/386, circo 279/280. twopi's one extra portable miss, wild_gv16_graphs_arrows_148b6b, is one of the C-unstable arrows graphs. Linux aarch64, Linux x86_64 and wasm32 produce bit-identical output to each other (docs/porting/libm.md, "Linux and wasm32").
Reproducer
The smallest affected file is tests/wild/gallery_beautify_1_1cfb90.gv (the main corpus's only portable miss):
digraph G {
layout="sfdp"
beautify=false
N0 -> {N1; N2; N3; N4; N5; N6}
}
GVBINDIR=<pango-free plugins> dot -Tsvg repro.gv # macOS C, Apple libm
cargo build --release -p graphviz-pure-cli --features graphviz-pure-core/portable-math # or any Linux / WASM build
graphviz-pure -Tsvg repro.gv
C output vs port
The outputs are not attached here. The affected files and baselines are listed in corpus.txt: tests/golden/e2e-baseline.json vs tests/golden/e2e-baseline.portable.json, and tests/golden/<engine>-baseline.json vs <engine>-baseline.portable.json.
Root cause
All transcendental calls go through crates/graphviz-pure-core/src/math/mod.rs (PORTABLE at line 25). There, powf (line 71) uses a Rust port of CORE-MATH's correctly rounded pow (crates/graphviz-pure-core/src/math/portable/pow/), hypot uses a formula that matches Apple on every tested input, and atan2 (line 83), sin (line 95), cos (line 103) and the rest come from the libm crate. The C side is not Graphviz code: C calls the platform libm. Measured contributions, swapping one function at a time (docs/porting/libm.md, "Which function moves which corpus", measured before the CORE-MATH pow port): pow sfdp 127, neato 3, main 1; atan2 fdp 3; sin fdp 2; cos circo 1, fdp 3. The CORE-MATH pow port has since fixed neato and 13 sfdp files. The breakdown for the remaining 119 files has not been measured again per function, so this is one issue rather than one per function.
Why the port does not match: matching bit for bit would mean reproducing Apple's closed pow/sin/cos/atan2 from the binary (the published Libm Source/ARM/*.s files are empty). That raises a licensing question and has not been done. Correctly rounded sin/cos would be worse (circo 274/280), and correctly rounded atan2 gains nothing. The port is exact on macOS and deterministic across all other platforms. The portable baselines (*.portable.json) catch regressions there.
Cases
119 e2e outputs. The full list is in corpus.txt and cases.txt (sfdp 114, fdp 3, circo 1, main 1).
Files
tests/fuzz-open/031-platform-libm-apple
C and the port
No pair of C and port outputs in this reproducer.
Reproduce
digraph G { layout="sfdp" beautify=false
N0 -> {N1; N2; N3; N4; N5; N6}}cargo build --release -p graphviz-pure-cli --features graphviz-pure-core/portable-math # or any Linux/WASM buildGVBINDIR=$HOME/src/gv-ref/lutplugins dot -Tsvg repro.gvgraphviz-pure -Tsvg repro.gvcirco/wild_gv16_undirected_ngk10_4_793340fdp/wild_gv16_graphs_b15_8c212efdp/wild_gv16_graphs_b51_33bcb0fdp/wild_gv16_graphs_ldbxtried_9e83aemain/gallery_beautify_1_1cfb90sfdp/attr_beautify_treesfdp/attr_label_scheme_1sfdp/attr_label_scheme_2sfdp/attr_label_scheme_3sfdp/attr_label_scheme_4sfdp/attr_label_scheme_4_overlapsfdp/attr_levels_0sfdp/attr_levels_1sfdp/attr_levels_2sfdp/attr_normalizesfdp/attr_overlap_false_gridsfdp/attr_overlap_false_labelssfdp/attr_quadtree_nonesfdp/attr_quadtree_normalsfdp/attr_ratio_fillsfdp/attr_repulsiveforce_2sfdp/attr_repulsiveforce_2_gridsfdp/attr_repulsiveforce_3sfdp/attr_rotation_-45sfdp/attr_rotation_30sfdp/attr_rotation_90sfdp/attr_sizesfdp/attr_start_7sfdp/attr_start_random42sfdp/fx_ERsfdp/fx_Heawoodsfdp/fx_NaNsfdp/fx_Petersensfdp/fx_abstractsfdp/fx_alfsfdp/fx_arrowssfdp/fx_clust3sfdp/fx_packsfdp/fx_recordsfdp/fx_unixsfdp/fx_worldsfdp/gen_grid10x10sfdp/gen_grid20x20sfdp/gen_grid8x40sfdp/gen_powerlaw1000sfdp/gen_powerlaw300sfdp/gen_rnd100sfdp/gen_rnd2000sfdp/gen_rnd300sfdp/gen_rnd800sfdp/gen_tree4_ternarysfdp/gen_tree7sfdp/gen_tree9sfdp/hw_C60sfdp/hw_K5sfdp/hw_K8_boxsfdp/hw_disconnectedsfdp/hw_disconnected_pack20sfdp/hw_disconnected_packarraysfdp/hw_labelssfdp/hw_loopssfdp/hw_petersensfdp/hw_strict_multisfdp/hw_weightssfdp/wild_gv16_directed_arrows_54d1cfsfdp/wild_gv16_directed_crazy_982d4dsfdp/wild_gv16_directed_oldarrows_f70da3sfdp/wild_gv16_graphs_ER_1359e3sfdp/wild_gv16_graphs_Heawood_771643sfdp/wild_gv16_graphs_Petersen_8a008fsfdp/wild_gv16_graphs_arrows_148b6bsfdp/wild_gv16_graphs_arrowsize_46ccd8sfdp/wild_gv16_graphs_b34_ca01c7sfdp/wild_gv16_graphs_b56_36f937sfdp/wild_gv16_graphs_b69_4b5b48sfdp/wild_gv16_graphs_badvoro_2e8cb8sfdp/wild_gv16_graphs_clover_ae8650sfdp/wild_gv16_graphs_crazy_cfdc7asfdp/wild_gv16_graphs_d_3a4a40sfdp/wild_gv16_graphs_dpd_223cb3sfdp/wild_gv16_graphs_ldbxtried_9e83aesfdp/wild_gv16_graphs_newarrows_769825sfdp/wild_gv16_graphs_ngk10_4_6aab96sfdp/wild_gv16_graphs_p2_99819csfdp/wild_gv16_graphs_p4_849964sfdp/wild_gv16_graphs_pack_1c2410sfdp/wild_gv16_graphs_pm2way_85dfc3sfdp/wild_gv16_graphs_pmpipe_2cd2b2sfdp/wild_gv16_graphs_proc3d_a79c48sfdp/wild_gv16_graphs_triedds_2395absfdp/wild_gv16_graphs_viewport_651d5bsfdp/wild_gv16_linux_x86_arrows_dot_1ce23fsfdp/wild_gv16_linux_x86_edgeclip_dot_17e57bsfdp/wild_gv16_linux_x86_html2_dot_3ee3fcsfdp/wild_gv16_linux_x86_pack_neato1_0327desfdp/wild_gv16_linux_x86_pack_neato2_dc0918sfdp/wild_gv16_linux_x86_pack_neato_c0395asfdp/wild_gv16_linux_x86_ports_dot_99eb47sfdp/wild_gv16_linux_x86_rankdir_dot1_8f7d7dsfdp/wild_gv16_linux_x86_rankdir_dot2_810f01sfdp/wild_gv16_linux_x86_rankdir_dot_f06bd6sfdp/wild_gv16_linux_x86_record_dot_dd3ea1sfdp/wild_gv16_linux_x86_rowcolsep_dot1_2c201csfdp/wild_gv16_linux_x86_rowcolsep_dot_0ad7ddsfdp/wild_gv16_nshare_html2_dot_7bea59sfdp/wild_gv16_nshare_neatosplines_neato1_9c01b5sfdp/wild_gv16_nshare_neatosplines_neato_7ac8eesfdp/wild_gv16_nshare_overlap_neato_afd9cesfdp/wild_gv16_nshare_pack_neato1_014679sfdp/wild_gv16_nshare_pack_neato2_10690fsfdp/wild_gv16_nshare_pack_neato_e0a2bbsfdp/wild_gv16_nshare_ports_dot_25dc96sfdp/wild_gv16_nshare_rankdir_dot1_228ae0sfdp/wild_gv16_nshare_rankdir_dot2_920cb4sfdp/wild_gv16_nshare_rankdir_dot_0e4e43sfdp/wild_gv16_nshare_record_dot_f557a8sfdp/wild_wild_graphs_arrows_a4826esfdp/wild_wild_graphs_arrowsize_ccef6esfdp/wild_wild_graphs_complex_edges_71191cFiles: cases.txt, cmd.txt, corpus.txt, repro.gv. Source: tests/fuzz-open/031-platform-libm-apple.