Skip to content

Linux/WASM: portable libm differs from Apple's pow/sin/cos/atan2 (2470/2589 e2e)

Kind
Port bug
Severity
medium
Engine
all
Category
platform
Cases
119
Seeds
·
Tracking
GitHub issuewon't fix
Labels
fidelityplatformwontfix

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

repro.gv
digraph G {
layout="sfdp"
beautify=false
N0 -> {N1; N2; N3; N4; N5; N6}
}
cmd.txt
cargo build --release -p graphviz-pure-cli --features graphviz-pure-core/portable-math # or any Linux/WASM build
GVBINDIR=$HOME/src/gv-ref/lutplugins dot -Tsvg repro.gv
graphviz-pure -Tsvg repro.gv
cases.txt
circo/wild_gv16_undirected_ngk10_4_793340
fdp/wild_gv16_graphs_b15_8c212e
fdp/wild_gv16_graphs_b51_33bcb0
fdp/wild_gv16_graphs_ldbxtried_9e83ae
main/gallery_beautify_1_1cfb90
sfdp/attr_beautify_tree
sfdp/attr_label_scheme_1
sfdp/attr_label_scheme_2
sfdp/attr_label_scheme_3
sfdp/attr_label_scheme_4
sfdp/attr_label_scheme_4_overlap
sfdp/attr_levels_0
sfdp/attr_levels_1
sfdp/attr_levels_2
sfdp/attr_normalize
sfdp/attr_overlap_false_grid
sfdp/attr_overlap_false_labels
sfdp/attr_quadtree_none
sfdp/attr_quadtree_normal
sfdp/attr_ratio_fill
sfdp/attr_repulsiveforce_2
sfdp/attr_repulsiveforce_2_grid
sfdp/attr_repulsiveforce_3
sfdp/attr_rotation_-45
sfdp/attr_rotation_30
sfdp/attr_rotation_90
sfdp/attr_size
sfdp/attr_start_7
sfdp/attr_start_random42
sfdp/fx_ER
sfdp/fx_Heawood
sfdp/fx_NaN
sfdp/fx_Petersen
sfdp/fx_abstract
sfdp/fx_alf
sfdp/fx_arrows
sfdp/fx_clust3
sfdp/fx_pack
sfdp/fx_record
sfdp/fx_unix
sfdp/fx_world
sfdp/gen_grid10x10
sfdp/gen_grid20x20
sfdp/gen_grid8x40
sfdp/gen_powerlaw1000
sfdp/gen_powerlaw300
sfdp/gen_rnd100
sfdp/gen_rnd2000
sfdp/gen_rnd300
sfdp/gen_rnd800
sfdp/gen_tree4_ternary
sfdp/gen_tree7
sfdp/gen_tree9
sfdp/hw_C60
sfdp/hw_K5
sfdp/hw_K8_box
sfdp/hw_disconnected
sfdp/hw_disconnected_pack20
sfdp/hw_disconnected_packarray
sfdp/hw_labels
sfdp/hw_loops
sfdp/hw_petersen
sfdp/hw_strict_multi
sfdp/hw_weights
sfdp/wild_gv16_directed_arrows_54d1cf
sfdp/wild_gv16_directed_crazy_982d4d
sfdp/wild_gv16_directed_oldarrows_f70da3
sfdp/wild_gv16_graphs_ER_1359e3
sfdp/wild_gv16_graphs_Heawood_771643
sfdp/wild_gv16_graphs_Petersen_8a008f
sfdp/wild_gv16_graphs_arrows_148b6b
sfdp/wild_gv16_graphs_arrowsize_46ccd8
sfdp/wild_gv16_graphs_b34_ca01c7
sfdp/wild_gv16_graphs_b56_36f937
sfdp/wild_gv16_graphs_b69_4b5b48
sfdp/wild_gv16_graphs_badvoro_2e8cb8
sfdp/wild_gv16_graphs_clover_ae8650
sfdp/wild_gv16_graphs_crazy_cfdc7a
sfdp/wild_gv16_graphs_d_3a4a40
sfdp/wild_gv16_graphs_dpd_223cb3
sfdp/wild_gv16_graphs_ldbxtried_9e83ae
sfdp/wild_gv16_graphs_newarrows_769825
sfdp/wild_gv16_graphs_ngk10_4_6aab96
sfdp/wild_gv16_graphs_p2_99819c
sfdp/wild_gv16_graphs_p4_849964
sfdp/wild_gv16_graphs_pack_1c2410
sfdp/wild_gv16_graphs_pm2way_85dfc3
sfdp/wild_gv16_graphs_pmpipe_2cd2b2
sfdp/wild_gv16_graphs_proc3d_a79c48
sfdp/wild_gv16_graphs_triedds_2395ab
sfdp/wild_gv16_graphs_viewport_651d5b
sfdp/wild_gv16_linux_x86_arrows_dot_1ce23f
sfdp/wild_gv16_linux_x86_edgeclip_dot_17e57b
sfdp/wild_gv16_linux_x86_html2_dot_3ee3fc
sfdp/wild_gv16_linux_x86_pack_neato1_0327de
sfdp/wild_gv16_linux_x86_pack_neato2_dc0918
sfdp/wild_gv16_linux_x86_pack_neato_c0395a
sfdp/wild_gv16_linux_x86_ports_dot_99eb47
sfdp/wild_gv16_linux_x86_rankdir_dot1_8f7d7d
sfdp/wild_gv16_linux_x86_rankdir_dot2_810f01
sfdp/wild_gv16_linux_x86_rankdir_dot_f06bd6
sfdp/wild_gv16_linux_x86_record_dot_dd3ea1
sfdp/wild_gv16_linux_x86_rowcolsep_dot1_2c201c
sfdp/wild_gv16_linux_x86_rowcolsep_dot_0ad7dd
sfdp/wild_gv16_nshare_html2_dot_7bea59
sfdp/wild_gv16_nshare_neatosplines_neato1_9c01b5
sfdp/wild_gv16_nshare_neatosplines_neato_7ac8ee
sfdp/wild_gv16_nshare_overlap_neato_afd9ce
sfdp/wild_gv16_nshare_pack_neato1_014679
sfdp/wild_gv16_nshare_pack_neato2_10690f
sfdp/wild_gv16_nshare_pack_neato_e0a2bb
sfdp/wild_gv16_nshare_ports_dot_25dc96
sfdp/wild_gv16_nshare_rankdir_dot1_228ae0
sfdp/wild_gv16_nshare_rankdir_dot2_920cb4
sfdp/wild_gv16_nshare_rankdir_dot_0e4e43
sfdp/wild_gv16_nshare_record_dot_f557a8
sfdp/wild_wild_graphs_arrows_a4826e
sfdp/wild_wild_graphs_arrowsize_ccef6e
sfdp/wild_wild_graphs_complex_edges_71191c

Files: cases.txt, cmd.txt, corpus.txt, repro.gv. Source: tests/fuzz-open/031-platform-libm-apple.

All open issues