Precision
PARI computes with reals to a working precision. LibPARI states that precision in bits everywhere on its own surface, and converts only at the ccall boundary.
The units
PARI's own units are easy to misread, so they are pinned here against the headers shipped with PARI_jll (PARI 2.17):
- A prototype
porbargument is a bit count.pariinl.hdefinesprec2nbits(long x) { return x; }— the value is already in bits. - PARI allocates a
t_REALto a whole number of machine words, so a request is rounded up:nbits2prec(x) = ceil(x / 64) * 64on a 64-bit platform.
LibPARI.nbits2prec exposes that rounding:
| requested bits | allocated bits |
|---|---|
| 1 | 64 |
| 4 | 64 |
| 64 | 64 |
| 65 | 128 |
| 128 | 128 |
| 200 | 256 |
Before version 0.18.0 every generated binding defaulted to prec = 4, with a comment describing it as "word precision". It was in fact a request for 4 bits, which PARI rounded up to its 64-bit minimum — not a deliberate choice. Bindings now default to LibPARI's working precision.
The default
LibPARI.default_precision is 128 bits, which is GP's own realbitprecision default (about 38 decimal digits — the width PARI prints by default). Matching GP means the same expression gives the same answer in gp and in LibPARI.
Tracking Julia's precision(BigFloat) (256 bits) was the alternative. It was rejected: it would make LibPARI disagree with the library it wraps.
Setting it
setprecision(LibPARI.Gen, 256) do
LibPARI.PARI.mppi() # computed to 256 bits
endScopes nest and unwind, including when the body throws:
setprecision(LibPARI.Gen, 256) do
setprecision(LibPARI.Gen, 512) do
precision(LibPARI.Gen) # 512
end
precision(LibPARI.Gen) # back to 256
endprecision(LibPARI.Gen) reads the value in force; precision(g::LibPARI.Gen) reads the accuracy of a real value, and raises ArgumentError for an exact one — test with LibPARI.isexact.
What the scope reaches, and what it does not
follows setprecision(Gen, …) | |
|---|---|
generated bindings taking p or b | yes — it is their keyword default |
Gen^Gen and the hand-written numeric API | yes |
conversions into and out of a Gen | value-preserving, so unaffected |
gp_eval | no — see below |
| a task spawned inside the scope | no — see below |
gp_eval follows PARI's process-global realprecision, not the scope. The GP interpreter reads its own global default, which is shared by every thread in the process; LibPARI does not write it behind your back. Set it from GP itself if you need to:
LibPARI.gp_eval("default(realbitprecision, 512)")The scope is task-local. It nests and unwinds correctly, and it does cross the hop onto LibPARI's PARI worker tasks — the precision is read in the calling task and carried into the closure that gets marshalled. But a task you spawn inside the scope does not inherit it:
setprecision(LibPARI.Gen, 512) do
fetch(Threads.@spawn precision(LibPARI.Gen)) # the default, not 512
endThis is a limitation of the Julia 1.10 LTS floor, which has no Base.ScopedValues; a ScopedValue would be inherited. Set the precision inside the spawned task, or revisit when the compat floor moves to 1.11.
Precision and conversion
Gen(x) preserves x exactly, whatever its width:
x = big"1.00000000000000000000000000000001"
BigFloat(LibPARI.Gen(x)) == x # trueA wide float is decomposed into its exact integer significand and binary exponent and rebuilt at a precision that holds every bit — nothing passes through Cdouble. An IEEEFloat takes PARI's dbltor, which is exact.
Coming back, BigFloat(g) is exact by default: the result carries PARI's own mantissa, at whatever precision that takes, which may exceed the ambient precision(BigFloat). Pass the keyword to round once to a chosen width instead:
BigFloat(g; precision = 256)Inf, -Inf and NaN raise InexactError: PARI's t_INFINITY exists but does not take part in general arithmetic, so mapping onto it would produce values that fail later, far from the conversion.
Series precision
A series precision (prototype code P) is a number of terms, not a bit count. It has its own default and is deliberately outside the precision scope.
Reference
LibPARI.Gen — Method
Gen(x::AbstractFloat) -> Gen
Convert a Julia floating-point number to a Gen (a PARI t_REAL), preserving the value exactly.
An IEEEFloat (Float16, Float32, Float64) goes through PARI's dbltor, which is exact. Any wider float — a BigFloat, or a third-party AbstractFloat — is decomposed into its exact integer significand and binary exponent and rebuilt at a precision that holds every bit, so nothing passes through Cdouble (REQ-PREC-08).
Inf, -Inf and NaN raise InexactError: PARI's t_INFINITY does not take part in general arithmetic (REQ-PROM-10).
Examples
julia> using LibPARI
julia> BigFloat(LibPARI.Gen(big"1.00000000000000000000000000000001")) ==
big"1.00000000000000000000000000000001"
trueBase.MPFR.setprecision — Method
setprecision(
f::Function,
_::Type{Gen},
bits::Integer
) -> Any
Run f with LibPARI's working precision set to bits, then restore it.
Scopes nest, and unwind even when f throws. The setting is task-local: the calls f makes are marshalled onto LibPARI's PARI worker tasks, and the precision is read in the calling task and carried across that boundary — but a task spawned inside the scope does not inherit it (Julia 1.10 has no ScopedValues).
The scope governs the calls LibPARI itself makes. It does not reach into the GP interpreter: gp_eval follows PARI's process-global realprecision.
Examples
julia> using LibPARI
julia> setprecision(LibPARI.Gen, 256) do
precision(LibPARI.Gen)
end
256Base.precision — Method
precision(g::Gen) -> Int64
The accuracy of a real Gen, in bits.
PARI stores a t_REAL to a whole number of machine words, so the answer is a multiple of the word size. An exact Gen has no finite precision and raises ArgumentError rather than returning an invented number — test with isexact first.
Examples
julia> using LibPARI
julia> precision(LibPARI.Gen(1.5)) >= 64
trueBase.precision — Method
precision(_::Type{Gen}) -> Int64
The working precision in force for the current task, in bits.
Equal to default_precision outside any setprecision(Gen, bits) scope.
Examples
julia> using LibPARI
julia> precision(LibPARI.Gen) == LibPARI.default_precision()
trueLibPARI.default_precision — Method
default_precision() -> Int64
The working precision LibPARI supplies to PARI, in bits, when a caller does not give one.
Defaults to 128 bits — GP's own realbitprecision default — so that an expression evaluates to the same thing in gp and in LibPARI. Change it for a dynamic extent with setprecision(Gen, bits).
Examples
julia> using LibPARI
julia> LibPARI.default_precision()
128LibPARI.isexact — Method
isexact(g::Gen) -> Bool
Whether g is an exact PARI value — an integer, a rational, or any other type PARI stores without a working precision.
Examples
julia> using LibPARI
julia> LibPARI.isexact(LibPARI.Gen(42))
trueLibPARI.nbits2prec — Method
nbits2prec(bits::Integer) -> Int64
Round a request of bits up to the precision PARI will actually allocate — a whole number of machine words.
This mirrors PARI's own nbits2prec.
Examples
julia> using LibPARI
julia> LibPARI.nbits2prec(65)
128LibPARI.set_global_precision! — Method
set_global_precision!(bits::Integer) -> Int64
Set PARI's process-global working precision, in bits — the default the GP interpreter reads.
This is the setting gp_eval follows; the setprecision(Gen, bits) scope deliberately does not touch it. Unlike that scope it is global and shared by every thread and task in the process, so prefer the scope unless you specifically need to change what GP itself does.
Returns the requested bits.
This replaces the 47 zero-argument sd_* bindings the generator used to emit for PARI's Class: default records. Those called C functions declared (const char *, long) with no arguments set, which is undefined behaviour; they are no longer generated (REQ-PREC-13).
Examples
julia> using LibPARI
julia> old = LibPARI.set_global_precision!(256);
julia> precision(LibPARI.gp_eval("Pi")) >= 256
true
julia> LibPARI.set_global_precision!(old); # always restore: it is global