Skip to content

Latest commit

 

History

History
300 lines (229 loc) · 10.3 KB

File metadata and controls

300 lines (229 loc) · 10.3 KB
title Data Types
description How PRIK maps Fortran types to Python, NumPy dtypes, and semantic contracts
audience users
prerequisites common beginner workflow
related arrays.md, strings.md, wrapping-derived-types.md
status maintained
publication reviewed

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 Generated contract Python usage

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, not different Python dtypes:

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.bool_
Bool32 32 bits bool or np.bool_ bool dtype=np.bool_
Bool64 64 bits bool or np.bool_ bool dtype=np.bool_

Generated contracts select a numbered name after probing the chosen compiler. Callers never pass integer arrays for wider logical storage: the wrapper adapts the one-byte NumPy Boolean representation at the native boundary.


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 accept bool or np.bool_, and Boolean scalar results are Python bool values.
  • Plain Python float and int values raise TypeError for numeric scalar arguments.
  • PRIK resolves kinds using the selected compiler (gfortran by default).
  • Inspect the contract with generate --pyi whenever 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