Data Types¶
PRIK resolves Fortran types using the selected compiler, then generates an
explicit semantic contract (.pyi). Inspect that contract before calling the
wrapper because kind numbers are compiler-dependent.
Example¶
The source, generated contract, and Python call describe the same API. The observable values remain below the three views.
Fortran source¶
Create numeric_types.f90:
module numeric_types
use iso_c_binding, only: c_long_double, c_long_double_complex
use iso_fortran_env, only: int32, real64
implicit none
contains
integer(int32) function add_one(value) result(output)
integer(int32), intent(in) :: value
output = value + 1
end function add_one
real(real64) function double(value) result(output)
real(real64), intent(in) :: value
output = 2.0_real64 * value
end function double
real(c_long_double) function double_extended(value) result(output)
real(c_long_double), intent(in) :: value
output = 2.0_c_long_double * value
end function double_extended
complex(real64) function conjugate_value(value) result(output)
complex(real64), intent(in) :: value
output = conjg(value)
end function conjugate_value
complex(c_long_double_complex) function conjugate_extended(value) result(output)
complex(c_long_double_complex), intent(in) :: value
output = conjg(value)
end function conjugate_extended
logical(kind=1) function invert(flag) result(output)
logical(kind=1), intent(in) :: flag
output = .not. flag
end function invert
end module numeric_types
Build it:
python3 -m prik numeric_types.f90 --out-dir build/numeric-types
Generated Contract¶
On a GNU/Linux x86-64 target where long double uses x87 extended precision,
the generated numeric_types.pyi is:
from prik.contracts import Addr, Arg, Bool8, Complex128, Complex256, Float128, Float64, Int32, native_call
@native_call([Addr(Arg(0))])
def add_one(
value: Int32
) -> Int32: ...
@native_call([Addr(Arg(0))])
def double(
value: Float64
) -> Float64: ...
@native_call([Addr(Arg(0))])
def double_extended(
value: Float128
) -> Float128: ...
@native_call([Addr(Arg(0))])
def conjugate_value(
value: Complex128
) -> Complex128: ...
@native_call([Addr(Arg(0))])
def conjugate_extended(
value: Complex256
) -> Complex256: ...
@native_call([Addr(Arg(0))])
def invert(
flag: Bool8
) -> Bool8: ...
Generate it:
python3 -m prik generate --pyi numeric_types.f90
Bool8 is the result of probing logical(kind=1) with the selected compiler.
Calling from Python¶
import sys
import numpy as np
sys.path.insert(0, "build/numeric-types")
from numeric_types.numeric_types import (
add_one,
conjugate_extended,
conjugate_value,
double,
double_extended,
invert,
)
print(add_one(np.int32(4))) # 5
print(double(np.float64(1.5))) # 3.0
# np.float64 cannot hold this value; np.longdouble keeps it.
print(double_extended(np.longdouble("1.0000000000000000001")))
print(conjugate_value(np.complex128(1.0 + 2.0j))) # (1-2j)
print(conjugate_extended(np.clongdouble(1.0 + 2.0j))) # (1-2j)
print(invert(True)) # False
Result on that same target:
5
3.0
2.0000000000000000002
(1-2j)
(1-2j)
False
This is a target snapshot, not a portable promise that the extended routines
always use Float128, Complex256, or extra precision. On a target where C
long double has the same mantissa width as double, PRIK selects
Float64/Complex128 and the corresponding NumPy dtypes instead. The mapping
table below explains the target-mantissa rule.
Scalar Type Mapping¶
| Fortran Type | Semantic Type | Scalar Input | Direct Scalar Result |
|---|---|---|---|
integer(1) |
Int8 |
np.int8 |
np.int8 |
integer(2) |
Int16 |
np.int16 |
np.int16 |
integer(4) / int32 |
Int32 |
np.int32 |
np.int32 |
integer(8) / int64 |
Int64 |
np.int64 |
np.int64 |
real(4) |
Float32 |
np.float32 |
np.float32 |
real(8) / real64 |
Float64 |
np.float64 |
np.float64 |
real(c_long_double) — real(10) on x86-64 |
Float128 |
np.longdouble |
np.longdouble |
complex(4) |
Complex64 |
np.complex64 |
np.complex64 |
complex(8) |
Complex128 |
np.complex128 |
np.complex128 |
complex(c_long_double_complex) — complex(10) on x86-64 |
Complex256 |
np.clongdouble |
np.clongdouble |
logical |
Bool8-Bool64 |
bool or np.bool_ |
bool |
character |
String / String[n] |
Depends on the string boundary | Depends on the string boundary |
| Derived Type | Generated Class | Instance of that class | Instance of that class |
Float128 and Complex256 mean the target's long double, not a fixed
128-bit format. On x86-64 that is x87 extended precision, so real(10) and
complex(10) map to it and real(16) does not; on a target whose long
double is IEEE quad, real(16) maps to it instead. PRIK decides from the
mantissa width the compiler reports, never from storage size — see
Unsupported Widths And Forms.
Boolean contract names describe native storage. Scalars cross as Python
bool. Arrays are aliased element by element: one-byte logical arrays use
numpy.bool_, and wider kinds use the integer dtype of matching width.
| Semantic Contract | Native Logical Storage Represented | Scalar Input | Direct Result | Array Storage |
|---|---|---|---|---|
Bool |
8 bits; portable default, equivalent to Bool8 |
bool or np.bool_ |
bool |
dtype=np.bool_ |
Bool8 |
8 bits | bool or np.bool_ |
bool |
dtype=np.bool_ |
Bool16 |
16 bits | bool or np.bool_ |
bool |
dtype=np.int16 |
Bool32 |
32 bits | bool or np.bool_ |
bool |
dtype=np.int32 |
Bool64 |
64 bits | bool or np.bool_ |
bool |
dtype=np.int64 |
Generated contracts select a numbered name after probing the chosen compiler.
A logical(c_bool) array uses numpy.bool_. Wider logical kinds use the
integer dtype of matching width and can be read as Boolean values with
.astype(bool):
flags = mod.flags # dtype bool for logical(c_bool)
wide = mod.wide # dtype int32 for a 32-bit logical
wide.astype(bool) # array([True, False, True])
wide[0] = 0 # visible to Fortran
Write only 0 or 1 into an integer-typed logical array. Other integer values
are not portable Fortran logical representations.
Compiler options for interoperable logicals¶
PRIK selects interoperable logical storage for Intel and PGI/NVIDIA compilers.
When linking prebuilt Intel objects compiled without that setting, pass
--no-standard-logicals so their module symbols remain compatible:
python3 -m prik lib.f90 --compiler ifx --no-standard-logicals
The equivalent Python build option is standard_logicals=False.
Runtime Default Constructors¶
Concrete primitive contracts can create their matching NumPy scalar with its zero value:
import prik.contracts as xc
count = xc.Int32() # np.int32(0)
weight = xc.Float64() # np.float64(0.0)
flag = xc.Bool() # np.bool_(False)
This applies to Boolean, fixed-width numeric, and SizeT contracts. It does
not apply to target-resolved Int, UInt, or CEnum, or to Byte, Char,
String, and Void, because those names do not define one portable NumPy
scalar representation by themselves. A scalar being constructible does not
mean every wrapper backend supports that native type; the generated contract
and feature matrix remain authoritative.
Array annotations are not array factories: Float64[:]() is invalid. Create
ordinary arrays with NumPy. Allocatable and pointer descriptor handles have
their own default constructors, described in their later user-guide pages.
Important Rules¶
- Use exact NumPy scalar dtypes (
np.float64,np.int32, etc.) for numeric scalar arguments and expect the matching NumPy scalar result. Boolean arguments acceptboolornp.bool_, and Boolean scalar results are Pythonboolvalues. - Plain Python
floatandintvalues raiseTypeErrorfor numeric scalar arguments. - PRIK resolves kinds using the selected compiler (
gfortranby default). - Inspect the contract with
generate --pyiwhenever you change compiler flags or architecture.
Values And Native Storage¶
A bare primitive type represents a Python-visible scalar:
def double(value: Float64) -> Float64: ...
The wrapper requires a numpy.float64 input and returns a numpy.float64.
Other primitive result types follow the mapping table above.
T[()] represents rank-zero NumPy storage: arguments accept a 0-D NumPy
array, and results return a 0-D NumPy array. Raw integer addresses are an
advanced boundary covered later in the guide. A bare numeric T result is the
NumPy scalar listed in the mapping table; Boolean scalar results are Python
bool values.
Unsupported Widths And Forms¶
Float128 and Complex256 name the target's long double, which NumPy
exposes as longdouble and clongdouble. Storage size alone cannot identify
that format: on x86-64 both x87 extended precision and IEEE binary128 occupy
128 bits and differ only in mantissa width.
PRIK therefore compares the compiler-measured mantissa against the target's
long double rather than trusting the declaration. On a target whose long
double is x87 extended precision this accepts C long double and Fortran
real(10), and refuses real(16) with a diagnostic naming both widths --
rather than narrowing it silently. On a target whose long double is IEEE
quad, the same rule accepts real(16).
Next¶
- Continue with Arrays for rank, shape, strides, and contiguity.
- Then read Strings for immutable values and mutable character storage.
- Wrapping Derived Types