Skip to content

Commit 5a8dd27

Browse files
authored
fix(test): mark the bridge's real round trips broken on Windows (#56)
* fix(test): mark the bridge's real round trips broken on Windows main was red on both Windows runners: 23 failures, all in test_libpari_ext.jl, all in reals. Every other part of the bridge passes there. Cause. On Linux and macOS, GIAC renders a REAL at *that value's* precision — 156 significant digits for a 512-bit value — and the global `Digits` has no effect on it. The bridge reads a Giac REAL back by decoding its printed decimal and relies on exactly that. On Windows, GIAC honours `Digits` instead, default 12. Every real crossed truncated to twelve significant digits: expected 3.1415926535897932384626433832795028842 obtained 3.14159265359 0000062 at every precision asked for — 64, 128, 192, 256, 384, 512, 1024 all give the same twelve digits, so it is a ceiling, not a rounding difference. `Digits` governs GIAC's parser on Windows too, so `convert(GiacExpr, ::BigFloat)` cannot even store a wide value: it lands on DOUBLE rather than REAL. Worth recording plainly, because it is the more useful lesson: the decode *verifies itself* by re-encoding the candidate and comparing printed forms, and that check was blind here. Re-encoding the truncated value also prints twelve digits, the forms agree, and a wrong answer is confirmed. The check establishes the printer's self-consistency, not its fidelity; the two coincide only where the printer is faithful. A guard that inspires confidence without providing it is worse than none. The assertions are marked `@test_broken` rather than skipped, so the suite errors if they start passing and tells whoever fixed the platform difference to remove the marker. The documentation claimed Giac's printer emits a REAL at the value's own precision "rather than at a global setting", presented as measured fact. It was measured on Linux only. Corrected, with a dedicated section, a warning admonition on the reals section, and an entry in the limitations list. Not attempted here: raising `Digits` around the read, which might restore Windows. That needs a Windows runner to judge and belongs on its own branch — this one only stops main lying about the state of the tests. * fix(test): mark the bridge's real round trips broken on Windows main was red on both Windows runners: 23 failures, all in test_libpari_ext.jl, all in reals. Every other part of the bridge passes there. Cause — a known ABI bug between the two binaries, not a Giac quirk and not something this bridge can work around. `class gen` stored its tag as a bitfield, `unsigned char type:5; unsigned char type_unused:3;`. GCC fuses adjacent bitfield writes into one wider store and picks the bit placement in a version-dependent way. GIAC_jll is built with GCC 8 and libgiac_julia_jll with GCC 10, so they disagree about which bits hold `type`: libgiac writes a gen tagged _REAL and the wrapper reads back _DOUBLE_. The suite observes it directly: Expression: Giac.giac_type(wide) == REAL Evaluated: DOUBLE == REAL Everything else follows. Giac.jl believes it holds a Float64, prints at the global `Digits` — default 12 — and every real crosses truncated to twelve significant digits: expected 3.1415926535897932384626433832795028842 obtained 3.14159265359 0000062 identically at 64, 128, 192, 256, 384, 512 and 1024 bits, because 53 bits is all a mis-tagged DOUBLE ever had. Raising `Digits` would change nothing: the precision is lost at the tag, not at the printer. Fix in flight upstream: JuliaPackaging/Yggdrasil#13717 bumps GIAC_jll to v2.0.2 with GIAC_TYPE_ON_8BITS=1, making `type` a plain byte at offset 0 and the ABI compiler-invariant, followed by a libgiac_julia_jll bump. Same root cause as #22 and the probe in #26. The assertions are marked `@test_broken` rather than skipped, so that when both JLLs land the suite reports unexpected passes and tells whoever did it to delete the markers. One thing this did establish about the bridge itself: its decode verifies itself by re-encoding the candidate and comparing printed forms, and that check is blind here. Re-encoding the truncated value also prints twelve digits, the forms agree, and a wrong answer is confirmed. The check establishes the printer's self-consistency, not its fidelity — the two coincide only where the tag is right. Worth stating, because I had described that check in the docs as what makes a text-mediated step safe. The documentation claimed Giac's printer emits a REAL at the value's own precision "rather than at a global setting", presented as measured fact. It was measured on Linux only. Corrected, with a dedicated section, a warning admonition on the reals section, and an entry in the limitations list.
1 parent 3f3b258 commit 5a8dd27

4 files changed

Lines changed: 310 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- **The LibPARI bridge's real-number tests no longer fail the suite on
13+
Windows**, and the platform difference behind them is documented rather than
14+
hidden. `main` was red on both Windows runners with 23 failures, all of them
15+
in `test_libpari_ext.jl`, all in reals.
16+
17+
The cause is not Giac and not the bridge: it is a known ABI bug between the
18+
two binaries. `class gen` stored its tag as a bitfield, GCC fuses adjacent
19+
bitfield writes with version-dependent bit placement, and `GIAC_jll` is
20+
built with GCC 8 against `libgiac_julia_jll`'s GCC 10 — so libgiac writes a
21+
gen tagged `_REAL` and the wrapper reads back `_DOUBLE_`. The suite observes
22+
it directly: `Giac.giac_type(wide) == REAL` evaluates to `DOUBLE == REAL`.
23+
24+
Everything else follows. Giac.jl believes it holds a `Float64`, prints at
25+
the global `Digits` — default 12 — and every real crosses truncated to
26+
twelve significant digits, identically at 64, 128, 256, 512 and 1024 bits,
27+
because 53 bits is all a mis-tagged `DOUBLE` ever had. Raising `Digits`
28+
would change nothing: the precision is lost at the tag, not at the printer.
29+
30+
Fix in flight upstream:
31+
[Yggdrasil#13717](https://github.com/JuliaPackaging/Yggdrasil/pull/13717)
32+
bumps `GIAC_jll` to v2.0.2 with `GIAC_TYPE_ON_8BITS=1`, making the ABI
33+
compiler-invariant, followed by a `libgiac_julia_jll` bump. Same root cause
34+
as [#22](https://github.com/s-celles/Giac.jl/pull/22) and the probe in
35+
[#26](https://github.com/s-celles/Giac.jl/pull/26).
36+
37+
One thing this did establish about the bridge itself: its decode *verifies
38+
itself* by re-encoding the candidate and comparing printed forms, and that
39+
check is blind here. Re-encoding the truncated value also prints twelve
40+
digits, the forms agree, and a wrong answer is confirmed. The check
41+
establishes the printer's self-consistency, not its fidelity — the two
42+
coincide only where the tag is right.
43+
44+
The affected assertions are now marked `@test_broken` on Windows rather than
45+
skipped, so that when the two JLLs land the suite reports *unexpected
46+
passes* and tells whoever did it to delete the markers. Everything else
47+
in the bridge passes on Windows: integers, rationals, complex numbers,
48+
polynomials, vectors, matrices, refusals, variable names, the PARI stack
49+
check and the piracy check. A `DOUBLE`, needing no more than twelve digits,
50+
crosses correctly.
51+
52+
The documentation said Giac's printer emits a `REAL` at the value's own
53+
precision "rather than at a global setting", presented as a measured fact.
54+
It was measured on Linux only. Corrected, with a dedicated section and a
55+
warning admonition on the reals section.
56+
1057
### Added
1158

1259
- **Dependabot for the pinned GitHub Actions** (`.github/dependabot.yml`),

docs/src/extensions/libpari.md

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ the value itself, in both directions — a 512-bit `t_REAL` reaches Giac with
145145
all 512 bits even while the ambient setting is 64, and comes back a 512-bit
146146
`t_REAL`.
147147

148+
!!! warning "Linux and macOS only"
149+
This section describes reals on Linux and macOS. **On Windows a real does
150+
not cross at all**: a binary ABI mismatch makes the wrapper read a `_REAL`
151+
tag as `_DOUBLE_`, so it arrives truncated to twelve significant digits.
152+
See [Reals do not cross on Windows](@ref) before relying on any of what
153+
follows.
154+
148155
```julia
149156
using Giac, LibPARI
150157

@@ -179,20 +186,85 @@ by construction.
179186
Reading a Giac `REAL` back out is the exception, and it is worth stating
180187
plainly. The libgiac wrapper exposes `to_double` — a `Float64`, and therefore
181188
lossy — and nothing for an MPFR value; reaching past it into an undeclared
182-
libgiac C symbol is a call this project does not make. Giac's printer does,
183-
however, emit a `REAL` at that value's *own* precision rather than at a global
184-
setting, so the decimal it produces is faithful and only the bit width has to
185-
be recovered.
186-
187-
The bridge recovers that width by search and then **verifies** it: a candidate
188-
is accepted only when re-encoding it reproduces Giac's own printed form
189-
character for character. A width that cannot be verified raises rather than
190-
returning a quietly-rounded value. The behaviour is pinned by tests at 64,
191-
128, 192, 256, 384, 512 and 1024 bits.
189+
libgiac C symbol is a call this project does not make. **On Linux and macOS**
190+
Giac's printer emits a `REAL` at that value's *own* precision rather than at
191+
the global `Digits` setting, so the decimal it produces is faithful and only
192+
the bit width has to be recovered.
193+
194+
The bridge recovers that width by search and then re-encodes the candidate,
195+
accepting it only when the printed forms agree character for character. A
196+
width for which nothing agrees raises rather than returning a quietly-rounded
197+
value. The behaviour is pinned by tests at 64, 128, 192, 256, 384, 512 and
198+
1024 bits.
199+
200+
Note precisely what that check does and does not establish. It tests the
201+
**printer's self-consistency**, not its fidelity to the stored value. Those
202+
coincide only where the printer is faithful — which is why the check is sound
203+
on Linux and macOS and blind on Windows, where it confirms a truncated answer
204+
without complaint. See [Reals do not cross on Windows](@ref).
192205

193206
This would become unnecessary if the wrapper gained an MPFR accessor for
194-
`REAL`; until then it is the bridge's only text-mediated step, and it is
195-
checked rather than trusted.
207+
`REAL`; until then it is the bridge's only text-mediated step.
208+
209+
## Reals do not cross on Windows
210+
211+
**On Windows, a `t_REAL` does not survive the crossing.** Every real arrives
212+
truncated to twelve significant digits, whatever precision was asked for:
213+
214+
```julia
215+
p = setprecision(() -> LibPARI.PARI.mppi(), LibPARI.Gen, 512)
216+
217+
pari(to_giac(p)) == p # false on Windows, true elsewhere
218+
# expected 3.1415926535897932384626433832795028842
219+
# obtained 3.14159265359 0000062
220+
```
221+
222+
This is **not** a Giac printing quirk and not something the bridge can work
223+
around. It is a known ABI bug between the two binaries, already diagnosed,
224+
with a fix in flight upstream.
225+
226+
`class gen` historically stored its tag as a bitfield —
227+
`unsigned char type:5; unsigned char type_unused:3;`. GCC fuses adjacent
228+
bitfield writes into one wider store and chooses the bit placement in a
229+
version-dependent way. `GIAC_jll` is built with GCC 8 and
230+
`libgiac_julia_jll` with GCC 10, so the two disagree about which bits hold
231+
`type`: libgiac writes a gen tagged `_REAL`, and the wrapper reads back
232+
`_DOUBLE_`. Giac.jl then believes it is holding a `Float64`, prints it at the
233+
global `Digits` — default 12 — and the extra precision is gone.
234+
235+
That the loss is identical at 64, 128, 256, 512 and 1024 bits is the tell:
236+
53 bits is all a mis-tagged `DOUBLE` ever had. **Raising `Digits` would change
237+
nothing** — the precision is lost at the tag, not at the printer.
238+
239+
The fix is [JuliaPackaging/Yggdrasil#13717](https://github.com/JuliaPackaging/Yggdrasil/pull/13717),
240+
which bumps `GIAC_jll` to v2.0.2 built with `GIAC_TYPE_ON_8BITS=1` — making
241+
`type` a plain byte at offset 0, so the ABI no longer depends on the compiler
242+
version — followed by a matching `libgiac_julia_jll` bump. Same root cause as
243+
[Giac.jl#22](https://github.com/s-celles/Giac.jl/pull/22) and the diagnostic
244+
probe in [Giac.jl#26](https://github.com/s-celles/Giac.jl/pull/26).
245+
246+
**Everything else in the bridge works on Windows** — integers, rationals,
247+
complex numbers, polynomials, vectors, matrices, the refusal list, variable
248+
names. Only reals are affected, and a `DOUBLE` crosses correctly, since a
249+
`DOUBLE` is what the mis-tag claims it already is.
250+
251+
The affected assertions are marked `@test_broken` on Windows rather than
252+
skipped. When the two JLLs land, those markers will start reporting
253+
*unexpected passes*, which is the signal to delete them.
254+
255+
If you need reals to cross on Windows before then, convert to an exact type
256+
first — `Rational` crosses faithfully on every platform.
257+
258+
### What this says about the bridge's own safety check
259+
260+
The bridge reads a Giac `REAL` back by decoding its printed decimal, and
261+
verifies the result by re-encoding it and comparing printed forms. On Windows
262+
that check is **blind**: re-encoding the truncated value also prints twelve
263+
digits, the forms agree, and a wrong answer is confirmed without complaint.
264+
265+
The check establishes the printer's *self-consistency*, not its *fidelity to
266+
the stored value*. Those coincide only where the tag is right. A guard worth
267+
having, but not one that can detect a lie told further down the stack.
196268

197269
## Known limitations
198270

@@ -220,16 +292,23 @@ Giac.jl's suite rather than in your results.
220292
PARI's `t_REAL`; coming back, Giac picks `DOUBLE` or `REAL` according to
221293
how much precision the value needs. Values are preserved either way.
222294

223-
4. **A `Gen` is not a `Number`.** It is a `LibPARI.PariObject`. Generic
295+
4. **Reals do not cross on Windows** — see the dedicated section above. A
296+
`t_REAL` arrives truncated to twelve significant digits, because a GCC
297+
bitfield-ABI mismatch between `GIAC_jll` and `libgiac_julia_jll` makes the
298+
wrapper read a `_REAL` tag as `_DOUBLE_`. Fix in flight upstream
299+
([Yggdrasil#13717](https://github.com/JuliaPackaging/Yggdrasil/pull/13717));
300+
every other type is unaffected.
301+
302+
5. **A `Gen` is not a `Number`.** It is a `LibPARI.PariObject`. Generic
224303
numeric code bounded by `T<:Number` will not accept one, and `Number`
225304
methods must not be assumed to apply.
226305

227-
5. **`LibPARI.PARI.gpolvar` takes a keyword**, not a positional argument:
306+
6. **`LibPARI.PARI.gpolvar` takes a keyword**, not a positional argument:
228307
`gpolvar(; x1 = g)`. Most of LibPARI's generated bindings pass the first
229308
argument positionally and the rest as keywords; check the signature before
230309
calling one.
231310

232-
6. **`gp_eval` does not honour `setprecision(LibPARI.Gen, …)`.** It goes
311+
7. **`gp_eval` does not honour `setprecision(LibPARI.Gen, …)`.** It goes
233312
through the GP interpreter, which reads PARI's process-global
234313
`realprecision`, so `setprecision(() -> gp_eval("Pi"), LibPARI.Gen, 512)`
235314
returns a 128-bit value. This is documented, intended LibPARI behaviour —

docs/src/llms-full.txt

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5422,6 +5422,13 @@ the value itself, in both directions — a 512-bit `t_REAL` reaches Giac with
54225422
all 512 bits even while the ambient setting is 64, and comes back a 512-bit
54235423
`t_REAL`.
54245424

5425+
!!! warning "Linux and macOS only"
5426+
This section describes reals on Linux and macOS. **On Windows a real does
5427+
not cross at all**: a binary ABI mismatch makes the wrapper read a `_REAL`
5428+
tag as `_DOUBLE_`, so it arrives truncated to twelve significant digits.
5429+
See [Reals do not cross on Windows](@ref) before relying on any of what
5430+
follows.
5431+
54255432
```julia
54265433
using Giac, LibPARI
54275434

@@ -5456,20 +5463,85 @@ by construction.
54565463
Reading a Giac `REAL` back out is the exception, and it is worth stating
54575464
plainly. The libgiac wrapper exposes `to_double` — a `Float64`, and therefore
54585465
lossy — and nothing for an MPFR value; reaching past it into an undeclared
5459-
libgiac C symbol is a call this project does not make. Giac's printer does,
5460-
however, emit a `REAL` at that value's *own* precision rather than at a global
5461-
setting, so the decimal it produces is faithful and only the bit width has to
5462-
be recovered.
5463-
5464-
The bridge recovers that width by search and then **verifies** it: a candidate
5465-
is accepted only when re-encoding it reproduces Giac's own printed form
5466-
character for character. A width that cannot be verified raises rather than
5467-
returning a quietly-rounded value. The behaviour is pinned by tests at 64,
5468-
128, 192, 256, 384, 512 and 1024 bits.
5466+
libgiac C symbol is a call this project does not make. **On Linux and macOS**
5467+
Giac's printer emits a `REAL` at that value's *own* precision rather than at
5468+
the global `Digits` setting, so the decimal it produces is faithful and only
5469+
the bit width has to be recovered.
5470+
5471+
The bridge recovers that width by search and then re-encodes the candidate,
5472+
accepting it only when the printed forms agree character for character. A
5473+
width for which nothing agrees raises rather than returning a quietly-rounded
5474+
value. The behaviour is pinned by tests at 64, 128, 192, 256, 384, 512 and
5475+
1024 bits.
5476+
5477+
Note precisely what that check does and does not establish. It tests the
5478+
**printer's self-consistency**, not its fidelity to the stored value. Those
5479+
coincide only where the printer is faithful — which is why the check is sound
5480+
on Linux and macOS and blind on Windows, where it confirms a truncated answer
5481+
without complaint. See [Reals do not cross on Windows](@ref).
54695482

54705483
This would become unnecessary if the wrapper gained an MPFR accessor for
5471-
`REAL`; until then it is the bridge's only text-mediated step, and it is
5472-
checked rather than trusted.
5484+
`REAL`; until then it is the bridge's only text-mediated step.
5485+
5486+
## Reals do not cross on Windows
5487+
5488+
**On Windows, a `t_REAL` does not survive the crossing.** Every real arrives
5489+
truncated to twelve significant digits, whatever precision was asked for:
5490+
5491+
```julia
5492+
p = setprecision(() -> LibPARI.PARI.mppi(), LibPARI.Gen, 512)
5493+
5494+
pari(to_giac(p)) == p # false on Windows, true elsewhere
5495+
# expected 3.1415926535897932384626433832795028842
5496+
# obtained 3.14159265359 0000062
5497+
```
5498+
5499+
This is **not** a Giac printing quirk and not something the bridge can work
5500+
around. It is a known ABI bug between the two binaries, already diagnosed,
5501+
with a fix in flight upstream.
5502+
5503+
`class gen` historically stored its tag as a bitfield —
5504+
`unsigned char type:5; unsigned char type_unused:3;`. GCC fuses adjacent
5505+
bitfield writes into one wider store and chooses the bit placement in a
5506+
version-dependent way. `GIAC_jll` is built with GCC 8 and
5507+
`libgiac_julia_jll` with GCC 10, so the two disagree about which bits hold
5508+
`type`: libgiac writes a gen tagged `_REAL`, and the wrapper reads back
5509+
`_DOUBLE_`. Giac.jl then believes it is holding a `Float64`, prints it at the
5510+
global `Digits` — default 12 — and the extra precision is gone.
5511+
5512+
That the loss is identical at 64, 128, 256, 512 and 1024 bits is the tell:
5513+
53 bits is all a mis-tagged `DOUBLE` ever had. **Raising `Digits` would change
5514+
nothing** — the precision is lost at the tag, not at the printer.
5515+
5516+
The fix is [JuliaPackaging/Yggdrasil#13717](https://github.com/JuliaPackaging/Yggdrasil/pull/13717),
5517+
which bumps `GIAC_jll` to v2.0.2 built with `GIAC_TYPE_ON_8BITS=1` — making
5518+
`type` a plain byte at offset 0, so the ABI no longer depends on the compiler
5519+
version — followed by a matching `libgiac_julia_jll` bump. Same root cause as
5520+
[Giac.jl#22](https://github.com/s-celles/Giac.jl/pull/22) and the diagnostic
5521+
probe in [Giac.jl#26](https://github.com/s-celles/Giac.jl/pull/26).
5522+
5523+
**Everything else in the bridge works on Windows** — integers, rationals,
5524+
complex numbers, polynomials, vectors, matrices, the refusal list, variable
5525+
names. Only reals are affected, and a `DOUBLE` crosses correctly, since a
5526+
`DOUBLE` is what the mis-tag claims it already is.
5527+
5528+
The affected assertions are marked `@test_broken` on Windows rather than
5529+
skipped. When the two JLLs land, those markers will start reporting
5530+
*unexpected passes*, which is the signal to delete them.
5531+
5532+
If you need reals to cross on Windows before then, convert to an exact type
5533+
first — `Rational` crosses faithfully on every platform.
5534+
5535+
### What this says about the bridge's own safety check
5536+
5537+
The bridge reads a Giac `REAL` back by decoding its printed decimal, and
5538+
verifies the result by re-encoding it and comparing printed forms. On Windows
5539+
that check is **blind**: re-encoding the truncated value also prints twelve
5540+
digits, the forms agree, and a wrong answer is confirmed without complaint.
5541+
5542+
The check establishes the printer's *self-consistency*, not its *fidelity to
5543+
the stored value*. Those coincide only where the tag is right. A guard worth
5544+
having, but not one that can detect a lie told further down the stack.
54735545

54745546
## Known limitations
54755547

@@ -5497,16 +5569,23 @@ Giac.jl's suite rather than in your results.
54975569
PARI's `t_REAL`; coming back, Giac picks `DOUBLE` or `REAL` according to
54985570
how much precision the value needs. Values are preserved either way.
54995571

5500-
4. **A `Gen` is not a `Number`.** It is a `LibPARI.PariObject`. Generic
5572+
4. **Reals do not cross on Windows** — see the dedicated section above. A
5573+
`t_REAL` arrives truncated to twelve significant digits, because a GCC
5574+
bitfield-ABI mismatch between `GIAC_jll` and `libgiac_julia_jll` makes the
5575+
wrapper read a `_REAL` tag as `_DOUBLE_`. Fix in flight upstream
5576+
([Yggdrasil#13717](https://github.com/JuliaPackaging/Yggdrasil/pull/13717));
5577+
every other type is unaffected.
5578+
5579+
5. **A `Gen` is not a `Number`.** It is a `LibPARI.PariObject`. Generic
55015580
numeric code bounded by `T<:Number` will not accept one, and `Number`
55025581
methods must not be assumed to apply.
55035582

5504-
5. **`LibPARI.PARI.gpolvar` takes a keyword**, not a positional argument:
5583+
6. **`LibPARI.PARI.gpolvar` takes a keyword**, not a positional argument:
55055584
`gpolvar(; x1 = g)`. Most of LibPARI's generated bindings pass the first
55065585
argument positionally and the rest as keywords; check the signature before
55075586
calling one.
55085587

5509-
6. **`gp_eval` does not honour `setprecision(LibPARI.Gen, …)`.** It goes
5588+
7. **`gp_eval` does not honour `setprecision(LibPARI.Gen, …)`.** It goes
55105589
through the GP interpreter, which reads PARI's process-global
55115590
`realprecision`, so `setprecision(() -> gp_eval("Pi"), LibPARI.Gen, 512)`
55125591
returns a 128-bit value. This is documented, intended LibPARI behaviour —

0 commit comments

Comments
 (0)