Skip to content

diag: probe giac::gen layout to investigate Giac.jl#22 (DO NOT MERGE)#5

Closed
s-celles wants to merge 15 commits into
mainfrom
diagnostic/probe-windows-gen-layout
Closed

diag: probe giac::gen layout to investigate Giac.jl#22 (DO NOT MERGE)#5
s-celles wants to merge 15 commits into
mainfrom
diagnostic/probe-windows-gen-layout

Conversation

@s-celles
Copy link
Copy Markdown
Owner

@s-celles s-celles commented May 13, 2026

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 _REAL when used through Giac.jl on Windows.

What's in here

  • diagnostics/probe_gen_layout.cpp — C++ probe that measures giac::gen struct layout (sizeof, alignof, field offsets, byte patterns) AND runs live evaluations (evalf(pi, 50) etc.) to inspect the resulting type tags.
  • Two CI steps (Linux/macOS + Windows) that compile the probe twice (default + -DGIAC_TYPE_ON_8BITS) and run it.
  • .gitignore allowlist entry for diagnostics/.
  • 14 iterations of CI debugging to overcome Windows-specific obstacles (MSYS2 GCC 15.2 vs GIAC_jll GCC 8 ABI mismatch, PATH pollution from Julia DLLs, missing libintl-8.dll, MSYS2 launcher swallowing stdout/stderr).

Key findings

The probe ran successfully on all three OSes after enough iteration. The cross-OS data matrix:

OS / Compiler Default mode (bitfield type:5) -DGIAC_TYPE_ON_8BITS
Linux GCC 12 (dyn link) evalf(pi,50)_REAL SEGFAULT (ABI break with GIAC_jll)
macOS clang (dyn link) _REAL _REAL ✓ (clang's bitfield ≡ 8-bit)
Windows GCC 15.2 (static link) _REAL _REAL

Conclusion: when libgiac and consumer are compiled with the same compiler, both modes produce correct _REAL tagging. The Giac.jl#22 Windows bug is therefore a cross-compiler bitfield interpretation mismatch between GIAC_jll (BinaryBuilder GCC 8) and libgiac_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 hold type.

Fix: #define GIAC_TYPE_ON_8BITS in the s-celles/giac fork makes type a plain unsigned 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

  • This PR closes for reference; the diagnostic branch stays available.
  • Follow-up is a Yggdrasil PR setting GIAC_TYPE_ON_8BITS as default in the s-celles/giac fork (separate PR in that repo) and bumping GIAC_jll + libgiac_julia_jll patch versions.
  • Once those JLLs propagate, Giac.jl#22's string-length workaround can be removed.

(Branch and probe kept for archaeological value; not for merging into main.)

s-celles added 15 commits May 13, 2026 16:31
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.
@s-celles
Copy link
Copy Markdown
Owner Author

Closing for reference per the diagnostic plan — value extracted, follow-up is the giac fork patch (separate PR).

@s-celles s-celles closed this May 13, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant