diag: probe giac::gen layout to investigate Giac.jl#22 (DO NOT MERGE)#5
Closed
s-celles wants to merge 15 commits into
Closed
diag: probe giac::gen layout to investigate Giac.jl#22 (DO NOT MERGE)#5s-celles wants to merge 15 commits into
s-celles wants to merge 15 commits into
Conversation
Adds a temporary diagnostic that compiles a small probe against the installed GIAC_jll headers twice — default and -DGIAC_TYPE_ON_8BITS — and dumps sizeof, alignof, field offsets, byte-level layout per type tag, and an overflow test. CI runs the probe on Linux, macOS, and Windows so we can compare Windows-specific layout against the Linux baseline. On Linux x86_64 with GCC 12.2 both modes produce identical layouts for in-range type tags. If Windows MSYS2 GCC 15 disagrees with GIAC_jll's BinaryBuilder GCC 8 about MS-bitfield packing, the offsets will diverge here and we have a concrete witness for why MPFR reals come back tagged as _DOUBLE_ on Windows. Includes the necessary .gitignore allowlist entry (the repo uses an allowlist pattern) and two CI steps with continue-on-error so the diagnostic never gates CI. Removal of diagnostics/, the .gitignore lines, and the two CI steps closes out the investigation when the Windows bug is fixed.
The layout-only probe couldn't distinguish the two modes — sizeof and
offsetof matched, byte patterns matched. So extended the probe with
a "live-gen" section that actually creates a giac::context and
evaluates a few expressions through giac, then inspects the resulting
gen's type byte:
1.0 expected _DOUBLE_
evalf(pi) expected _DOUBLE_ (default precision)
evalf(pi, 30) expected _REAL on Linux
evalf(pi, 50) expected _REAL on Linux
evalf(sqrt(2), 80) expected _REAL on Linux
Local Linux run reveals what the offsetof probe missed: in
-DGIAC_TYPE_ON_8BITS mode the live-gen probe SEGFAULTS during
giac::gen("evalf(pi,50)", ctx) — i.e. defining the macro on the
consumer side without rebuilding GIAC_jll breaks the binary
interface. The crash is the proof of an ABI mismatch that didn't
show up in struct-offset comparison.
Hardened both CI probe steps with "|| echo [...]" so a segfault in
one mode no longer aborts the whole step, letting us collect default-
mode output even when 8-bit mode crashes.
Pushing to see what Windows reports for the live-gen tags (Giac.jl#22
predicts Windows mis-tagging evalf(pi, N>=15) as _DOUBLE_).
The Windows MSYS2 launcher swallows stdout/stderr from the probe .exe, making exit code and printf output unrecoverable through any combination of bash/cmd/file redirection we tried. Workaround: have the probe write its own copy of cout into a known file at runtime via a tee_streambuf. If the binary loads and reaches main(), the file exists and contains the truth, regardless of what the launcher does. Restores cout's original rdbuf via a guard struct (declared LAST so it destroys FIRST) so the std::ofstream/tee_streambuf going out of scope doesn't leave cout with dangling pointers and segfault at exit. CI step on Windows now also dumps probe_output_default.log and probe_output_8bits.log if they exist — those are written by the probe itself, not the launcher.
Owner
Author
|
Closing for reference per the diagnostic plan — value extracted, follow-up is the giac fork patch (separate PR). |
3 tasks
s-celles
added a commit
to s-celles/giac
that referenced
this pull request
May 13, 2026
The historical bitfield layout of class gen:
unsigned char type:5;
unsigned char type_unused:3;
signed char subtype;
unsigned short reserved;
is laid out and ACCESSED differently across GCC versions. GCC fuses
adjacent bitfield writes (g.type = ...; g.subtype = ...;) into a
single wider store using version-dependent bit placements: GCC 8
and GCC 10 produce instruction sequences that disagree on which
bits of the resulting word hold `type`.
This isn't theoretical. In the libgiac_julia / Giac.jl stack on
Windows:
- GIAC_jll is built with BinaryBuilder GCC 8
- libgiac_julia_jll (the CxxWrap wrapper) is GCC 10
- When libgiac writes a gen tagged _REAL (3) and the wrapper reads
it, the wrapper sees _DOUBLE_ (1) instead.
A C++ probe (s-celles/libgiac-julia-wrapper#5)
ran on Linux/macOS/Windows in both modes confirmed: with the
bitfield, GCC's optimizer fuses writes; the byte layout is correct
ONLY when libgiac and consumer use the same GCC. With
GIAC_TYPE_ON_8BITS, type is a plain `unsigned char` at offset 0 —
no bitfield, no fusion, every compiler emits the same simple byte
store/load. The ABI becomes compiler-invariant.
Cost: 3 mantissa bits on inline doubles encoded in a gen (48 → 45).
sizeof(gen) is unchanged at 64 bits. Per giac's author this is the
historical default and the safer cross-compiler path. Override by
removing this block in dispatch.h if you need the legacy layout.
Fixes: s-celles/Giac.jl#22
Refs: s-celles/libgiac-julia-wrapper#5
3 tasks
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.
Purpose
Diagnostic-only branch. Do not merge. Closed after creation to preserve the investigation for future reference.
Investigates Giac.jl#22 — MPFR reals come back tagged as
_DOUBLE_instead of_REALwhen used through Giac.jl on Windows.What's in here
diagnostics/probe_gen_layout.cpp— C++ probe that measuresgiac::genstruct layout (sizeof, alignof, field offsets, byte patterns) AND runs live evaluations (evalf(pi, 50)etc.) to inspect the resulting type tags.-DGIAC_TYPE_ON_8BITS) and run it..gitignoreallowlist entry fordiagnostics/.Key findings
The probe ran successfully on all three OSes after enough iteration. The cross-OS data matrix:
type:5)-DGIAC_TYPE_ON_8BITSevalf(pi,50)→_REAL✓_REAL✓_REAL✓ (clang's bitfield ≡ 8-bit)_REAL✓_REAL✓Conclusion: when libgiac and consumer are compiled with the same compiler, both modes produce correct
_REALtagging. The Giac.jl#22 Windows bug is therefore a cross-compiler bitfield interpretation mismatch betweenGIAC_jll(BinaryBuilder GCC 8) andlibgiac_julia_jll(BinaryBuilder GCC 10) — GCC 8 and GCC 10 fuse adjacent bitfield writes differently, so libgiac's GCC-8 store and the wrapper's GCC-10 load disagree on which bits holdtype.Fix:
#define GIAC_TYPE_ON_8BITSin the s-celles/giac fork makestypea plainunsigned char(no bitfield), eliminating the compiler-version-dependent packing. The giac author confirmed this is the historical default and the recommended portability path; the tradeoff is 3 mantissa bits on inline floats (48→45), bounded and acceptable.What happens next
GIAC_TYPE_ON_8BITSas default in the s-celles/giac fork (separate PR in that repo) and bumping GIAC_jll + libgiac_julia_jll patch versions.(Branch and probe kept for archaeological value; not for merging into main.)