| title | Fortran Wrapper Reference |
|---|---|
| audience | users, advanced users |
| prerequisites | first wrapped module, NumPy basics |
| related | ../guide/index.md, pyi-contracts/index.md, ../language-support/index.md |
| status | maintained |
| publication | draft |
This reference describes the Python API generated by prik for Fortran code. It is the canonical contract for ownership, lifetime, naming, supported behavior, and current limitations.
The reference follows the wrapper by subject. Each subject includes a small example showing the Fortran interface and the corresponding Python use. Examples omit unrelated module scaffolding when that makes the contract easier to see.
This reference covers the implemented wrapper for Fortran source inputs.
- Foundations: building a wrapper, support boundaries, and ownership and lifetime
- Arrays and pointers: allocatables, pointers, array results, and NumPy argument contracts
- Objects and state: derived types, inheritance, constructors/finalizers, module state, and enums
- ABI and packaging: characters, scalar kinds, derived layout, and multi-source builds
- Python runtime: visibility and naming, callbacks, and errors/concurrency
- Not handled or not yet settled
- Procedures: scalars, generic interfaces, operators, outputs, and optional arguments
The direct wrapper path accepts fixed-form and free-form Fortran sources and requires a working GNU native toolchain, Python development headers, and NumPy development files. Recognizable Fortran sources default to a wrapper build.
Build the checked scalar example:
python3 -m prik tests/fortran/building_shared_library/end_to_end/fixtures/native/fruntime_abi_f90.f90 \
--out-dir build/fruntime_abiAdd the output directory to sys.path or run Python from a location where the
extension can be imported:
import sys
import numpy as np
sys.path.insert(0, "build/fruntime_abi")
import fruntime_abi_f90
result = fruntime_abi_f90.scale(np.float64(3.0), np.float64(2.5))
print(result) # 7.5Native scalar arguments use their exact NumPy dtype. prik rejects a Python
float where the generated contract requires numpy.float64; this avoids
implicit ABI-changing coercions.
One direct build executes this pipeline:
ordered Fortran source files
-> compiler preprocessing
-> Fortran parser project model
-> compiler-dependent kind and storage probes
-> semantic modules and completed policy
-> post-IR policy completion
-> ordered wrapper plan preserving native module namespaces and ABI slots
-> direct native-bridge and Python-binding lowering
-> compile and link one Python extension module
The generated bridge preserves native calling contracts while the Python binding validates arguments, manages wrapper-owned temporaries, calls native code, and projects results onto the documented Python API. Shared runtime support supplies array, error, allocation, and ownership helpers.
There is no separate codegen-AST conversion stage. Post-IR completion freezes object kind, storage, ownership, mutation, output projection, and native-call policy; wrapper planning orders those completed decisions, and the binding and bridge generators dispatch them directly into emitted source.
Typical generated artifacts are:
| Artifact | Purpose |
|---|---|
binding_support/ |
Header-only native binding support |
user and generated .o/.mod files |
Native build intermediates |
<module>.<extension-suffix>.so |
Importable extension on Linux |
Using prik does not impose prik's MIT License on user-supplied native sources
or on wrapper code derived from those inputs. Users may distribute generated
wrappers under terms of their choice. The native support files copied into
binding_support/ remain MIT-licensed; the copied directory includes the
license notice that must be retained when those files are redistributed.
The extension name comes from the first source filename. Contained Fortran
modules become child Python namespaces and standalone procedures remain at the
extension root. For example, solver.f90 containing module kernels exposes
solver.kernels, not a flattened solver surface. Multi-source builds preserve
one child per contained module and compile sources in caller-supplied order.
When a folder contains only standalone BLAS/LAPACK-style procedures,
--pyi --out contracts can generate one compact entry .pyi containing all
@external declarations while the native sources still compile and link as
separate artifacts.
Without --out-dir, prik writes generated artifacts, including the ABI-suffixed
extension, in a private __prik__ build directory in the current working
directory. A direct CLI build writes its stable <module>.so import alias in
the current working directory unless --out gives it an explicit path. Generated
wrapper sources remain build artifacts; users do not edit them to change the
Python API.
The semantic .pyi is the editable contract and wrapper-planning surface.
The supported edit workflow, including removal, addition, call projection,
ownership, and destruction, is explained in
Editing .pyi Contracts. The complete grammar
appears in the Semantic .pyi Format reference.
The normal CLI build is source-driven: recognizable Fortran sources build
wrappers without a stage flag and cannot be combined with --pyi. A semantic
.pyi entry contract also selects the wrapper stage automatically when its
native build artifacts, such as .o, .a, or .so inputs, are supplied. In
that mode the .pyi is the Python API source of truth; native source is not
reparsed during wrapper generation.
Both routes produce the same native extension build plan. In a source-driven
build, positional Fortran files are semantic inputs and, by default, native
compilation units. Add --no-compile-input-sources to use them only as semantic
inputs and link an already-built object, archive, shared library, or named
library instead. Explicit --native-fortran-sources remain hidden
implementation sources and are still compiled.
--native-compile-flags applies to native compilation when it is enabled and
always describes the compile model used for preprocessing and datatype
measurement. Objects, libraries, include directories, library directories,
and ordered link items may complete the implementation without changing the
parsed Python API. --wrapper-fortran-flags applies only to the generated
Fortran bridge; --wrapper-c-flags applies to the generated binding and
extension-link command.
For example, this command supplies every common build input shown by
python3 -m prik --help and produces the Python shared library under
build/solver. Here include/ contains source include files or module
interfaces, and openblas is a system library available to the linker:
python3 -m prik solver.f90 \
--out solver \
--out-dir build/solver \
--compiler gfortran \
-I include \
--native-compile-flags=-O3 \
--native-library openblas \
--verbose--compiler selects the input-language compiler used for preprocessing,
datatype measurement, native and generated-bridge compilation, and extension
linking. It also selects the matching C compiler profile for the generated
binding: gfortran uses gcc, ifx or ifort uses icx, flang uses
clang, nvfortran uses nvc, and pgfortran uses pgcc. prik fails when
the selected compiler family is unknown or the matching C compiler is
unavailable; it does not silently build a mixed-vendor wrapper. Python supplies
the binding headers and link metadata, while binding compiler flags come from
the selected vendor profile rather than Python's own build compiler. -I is
passed to preprocessing and to native, bridge, and binding compilation. The
library is kept separate from compiler flags because --native-library openblas must become -lopenblas on the final link command.
The maintained Linux alternate-toolchain smoke lanes pin Intel IFX/ICX
2026.1.1 and LLVM Flang/Clang 22.1.8. Flang preprocessing uses -P and keeps
the resulting marker-free source in memory. These versions are reproducible CI
pins rather than minimum-version promises; other versions remain supported
only when the same profile and strict smoke contracts pass.
The current .pyi build subset requires the contract filename stem to match
the native Fortran module name. Supply the native module file directory as an
include directory when the generated bridge contains use <module>:
python3 -m prik path/to/module.pyi \
--native-objects path/to/module.o \
-I path/to/mod-files \
--out-dir build/module--native-fortran-sources accepts one or more additional native implementation
sources that prik should compile without using them as semantic input.
--native-compile-flags applies to native source compile commands; its name is
language-neutral even though native source compilation is currently
Fortran-only. Group
dash-prefixed compiler flags with the equals form, such as
--native-compile-flags="-O3 -fopenmp". --native-objects accepts one or more
ordered object, static archive, or shared library paths. Named libraries use
--native-library NAME [NAME ...] and --native-library-dir DIR [DIR ...].
If you pass already-prefixed names, group them with the equals form, for example
--native-library="-lblas -llapack".
The latter is passed as both a link search path and a runtime search path. At
least one native implementation input is required.
Use --native-fortran-sources when prik should compile the implementation and
--native-objects when objects, archives, or shared libraries are already built.
To wrap an already-built library directly from its source directory, keep the sources as the semantic input and disable their automatic native compilation:
python3 -m prik path/to/fortran-sources \
--no-compile-input-sources \
--native-objects path/to/libsolver.so \
-I path/to/mod-files \
--out solverPRIK reads the directory recursively in deterministic path order, compiles only its generated bridge and binding, and links the supplied native library.
Semantic .pyi Makefile mode writes <out-dir>/prik-build.json first and then
generates <out-dir>/Makefile.prik from that manifest. The manifest can be
replayed directly:
python3 -m prik generate --makefile contracts/module.pyi \
--native-fortran-sources native/module.f90 \
--native-compile-flags="-O3 -fopenmp" \
--out-dir build/module
python3 -m prik --build-manifest build/module/prik-build.json
python3 -m prik generate --makefile --build-manifest build/module/prik-build.jsonEdited .pyi contracts may expose the native call shape directly. If every
native dummy argument stays visible in native order, no @native_call decorator
is required. Scalar intent(out) slots are caller-supplied mutable storage, so
pass a 0-D NumPy array with the declared dtype instead of expecting a projected
Python return. Fixed-length string identity calls can also return None; when
the caller passes an ordinary Python str, native in-place character mutation
is not observable in Python.
Edited contracts may also remove public declarations or mark declarations with
@private / private[...]; removed or private declarations are omitted from
the generated Python API while unaffected public declarations keep their runtime
behavior.
Misuse handling, diagnostic categories, and risky explicit-contract behavior
are covered in Editing .pyi Contracts and the
Semantic .pyi Format reference.
The Semantic .pyi Wrapper Checklist later records parity completion.
Use --verbose to execute a build while printing every exact, shell-escaped
compiler and linker command. It first announces binding, bridge, and header
source-text generation on separate lines without paths, because those files do
not exist yet. Each line is printed immediately before its separate lowering
and printing operation, followed by Timing: ... for that operation. It then announces each written artifact with its output path
(Write bridge source: ... and Write native support: ...), each native, bridge, and binding compilation with its
source and object path (Compile bridge source: source -> object), and the final
extension path before linking (Create shared library: ...). The exact
shell-escaped command follows each compilation or link announcement, so it can
be copied to reproduce that step. Verbose builds print elapsed time for policy
completion, each source-text generation, every compilation,
and linking, followed by total build time; writing generated files has no separate
timing. Use --makefile to generate an editable
Makefile.prik without compiling. These modes are mutually exclusive.
The equivalent Python entrypoint returns structured artifact paths:
from prik import build_fortran_extension
result = build_fortran_extension(
"tests/fortran/building_shared_library/end_to_end/fixtures/native/fruntime_abi_f90.f90",
output_dir="build/fruntime_abi",
)
print(result.module_name)
print(result.shared_library)The .pyi Python entrypoint accepts the same explicit native inputs. Use
native sources when prik should compile the implementation:
from prik import build_pyi_extension
result = build_pyi_extension(
"path/to/module.pyi",
native_fortran_sources=["path/to/module.f90"],
native_fortran_flags=["-O3"],
output_dir="build/module",
)Use native objects when the implementation was built elsewhere:
from prik import build_pyi_extension
result = build_pyi_extension(
"path/to/module.pyi",
native_objects=["path/to/module.o"],
native_include_dirs=["path/to/mod-files"],
output_dir="build/module",
)Every wrapper build returns a WrapperBuildResult with a structured
native_build_plan. This plan is separate from sources: sources records the
semantic inputs used to define the Python API, while native_build_plan records
the native implementation inputs used to compile and link the extension.
The plan records:
compilation_units: native sources that prik compiled and their produced objects;produced_objects: object files produced from those compilation units;prebuilt_artifacts: caller-supplied objects, archives, or shared libraries;module_dirsandinclude_dirs: directories needed while compiling the generated bridge; andlink_items: the ordered native implementation link items.
link_items is the order-sensitive representation. It can record objects,
static archives, direct shared-library paths, named -l libraries, and explicit
linker arguments without flattening them into one ambiguous string list.
The typed convenience options expose objects, archives, shared libraries,
named libraries, library directories, and include/module directories.
--native-link-item KIND:VALUE exposes the ordered form directly, including
explicit linker arguments, when caller order must be preserved.
Example 1: a source-driven build records native source compilation separately from generated wrapper files.
from prik import build_fortran_extension
result = build_fortran_extension("solver.f90", output_dir="build/solver")
plan = result.native_build_plan
print(plan.compilation_units[0].source)
print(plan.compilation_units[0].object_path)
print(plan.link_items[0].kind) # objectExample 2: a .pyi build from a native object keeps semantic contracts and
native artifacts separate.
from prik import build_pyi_extension
result = build_pyi_extension(
"contracts/solver/__init__.pyi",
native_objects=["build/solver.o"],
native_include_dirs=["build/mod"],
output_dir="build/solver",
)
print(result.sources[0]) # contracts/solver/__init__.pyi
print(result.native_build_plan.prebuilt_artifacts[0].kind) # objectExample 3: an object followed by a static archive remains ordered in the link plan.
from prik import build_pyi_extension
result = build_pyi_extension(
"contracts/api.pyi",
native_objects=["build/api.o", "vendor/libsupport.a"],
output_dir="build/api",
)
print([item.to_dict() for item in result.native_build_plan.link_items])
# [{'kind': 'object', 'path': 'build/api.o'},
# {'kind': 'archive', 'path': 'vendor/libsupport.a'}]Example 4: a direct shared-library path is distinct from a named library.
from prik import build_pyi_extension
result = build_pyi_extension(
"contracts/vendor_solver.pyi",
native_objects=["vendor/libsolver.so"],
native_library_dirs=["vendor"],
output_dir="build/vendor_solver",
)
artifact = result.native_build_plan.prebuilt_artifacts[0]
print(artifact.kind) # shared_library
print([str(path) for path in result.native_build_plan.library_dirs]) # ['vendor']Example 5: the ordered representation can express linker control arguments for CLI, manifest, and Makefile replay without pretending they are objects or libraries.
from pathlib import Path
from prik import NativeBuildPlan, NativeLinkItem
plan = NativeBuildPlan(
link_items=(
NativeLinkItem("object", Path("build/api.o")),
NativeLinkItem("linker_argument", "-Wl,--start-group"),
NativeLinkItem("archive", Path("vendor/liba.a")),
NativeLinkItem("archive", Path("vendor/libb.a")),
NativeLinkItem("linker_argument", "-Wl,--end-group"),
NativeLinkItem("named_library", "gfortran"),
)
)The Examples Cookbook later provides copy-paste recipes for direct CLI builds, Makefile generation, multi-source builds, and temporary-directory Python API builds.
A wrapper feature is considered supported only when all applicable layers agree:
- the Python-visible API, ownership, and limitations are documented;
- the parser and semantic IR preserve every source fact required by the wrapper;
- the default wrapper build emits a precise error when a declaration is unsupported or lacks policy;
- semantic lowering preserves the contract without reconstructing source text;
- the source formatter wraps generated free-form Fortran at syntax-safe token boundaries, including character-literal continuations that preserve their exact value, so every bridge line stays within the standard 132-column limit; generation fails before compilation when no safe continuation point exists;
- runtime behavior is covered by the project verification policy before it is presented as supported; and
- fixed-form and free-form behavior are both considered when the source feature exists in both forms.
This matters because a stable parser model is not the same thing as a safe Python runtime contract. When owner, lifetime, shape, ABI, or destruction is unclear, prik blocks generation instead of guessing.
Ownership determines whether Python sees a value, copy, descriptor handle, extracted view, or generated object; whether mutation reaches native storage; and which runtime destroys the storage.
The central rule is:
Ownership follows the native storage category, the known owner, and the transfer mode at the Python boundary. It is never inferred from Fortran syntax alone.
For example, both a non-optional allocatable output dummy and an allocatable
component use the Fortran allocatable attribute, but they have different
owners. The output dummy becomes an owned AllocatableArray whose finalizer
releases prik-owned descriptor storage. A component becomes a borrowed
AllocatableArray that retains its containing native wrapper.
| Term | Meaning | Typical example |
|---|---|---|
| Python-owned | Python or NumPy owns the value or data buffer and releases it normally. | Scalar results, strings, copy-return arrays, and explicitly copied pointer-scalar values. |
| Caller-owned | The caller supplied the Python object and retains ownership. | A NumPy array passed as intent(in), intent(out), or intent(inout). |
| Wrapper-owned | A Python object owns or controls native storage. | A wrapped derived-type result or owned allocatable result handle. |
| Native-owned | Fortran or an external library owns storage independently of Python. | A module allocatable array or external-library buffer. |
| Descriptor handle | Python carries native allocation or association state and a completed owner policy. | An allocatable or pointer module variable, field, argument, or supported result. |
| Borrowed view | Python references storage owned elsewhere and does not destroy it. | A NumPy array extracted from a borrowed descriptor handle. |
| Copy-return | Native output is copied into a new Python-owned value before return. | An ordinary array result or immutable replacement. |
Detached copy (snapshot_copy policy) |
Python receives a copy of current native state, not a live view. | Scalar pointer copied values or another explicit copy-result contract; native-array-handle to_numpy() never selects this behavior. |
| Call-local association | Native code may use Python storage only during the wrapped call. | Pointer intent(in) array arguments. |
| Blocked | Generation stops because a safe contract cannot be proven. | Pointer reassociation without owner and release policy. |
The wrapper enforces these invariants:
- Exactly one owner destroys each owned native allocation.
- A Python-owned copy is independent of later native mutation.
- A borrowed child or view keeps a Python wrapper owner alive when that owner contains the referenced storage.
- Keeping the Python owner alive does not protect a view from native reallocation or deallocation performed by another native call.
- A pointer component does not imply ownership of its target.
- Missing owner, lifetime, release, shape, dtype, contiguity, mutability, or aliasing facts produce a blocker.
| Value | Who destroys it | When |
|---|---|---|
| Python scalar or string | Python | When Python references are gone. |
| Copy-return or explicitly detached NumPy array | NumPy or its generated base capsule | When Python references are gone. |
| Caller-supplied NumPy array | The Python caller | According to normal Python lifetime. |
| Wrapper-owned derived instance | Generated wrapper deallocator | When the owning wrapper is collected. |
| Borrowed nested component | The parent wrapper | When parent and all borrowed children are gone. |
| Borrowed allocatable or pointer handle | The containing wrapper or native module | The handle does not release borrowed descriptor storage. |
| NumPy view extracted from a handle | The handle's completed owner policy | The view retains the handle, but can become stale after descriptor changes. |
| Owned allocatable result handle | Generated handle finalizer | On close() or when the handle is collected. |
| Pointer target | The explicit pointer policy's owner | Never inferred from the pointer declaration alone. |
| Call-local temporary | The generated bridge | Before the wrapped call returns. |
Users do not call a generated destroy() method for normal wrapper-owned
objects. Native allocation or deallocation routines that are part of the
Fortran API remain ordinary callable routines, but invoking one can invalidate
borrowed views.
type :: buffer
real(8), allocatable :: values(:)
end type bufferb = buffer()
b.allocate_values(3)
handle = b.values
assert handle.owner is b
view = handle.to_numpy()
view[0] = 9.0 # mutates b%values
independent = view.copy()
del b # handle and view retain the wrapper owner chain
print(view[0]) # 9.0If a later method reallocates values, an older borrowed view is not
automatically invalidated. Use .copy() before that operation when Python needs
an independent lifetime.
Ownership decisions are centralized in prik.semantics.ownership. Semantic
lowering and both bridge layers consume that resolved decision; low-level
printers do not invent ownership behavior.
An edited .pyi can provide ownership metadata:
from prik.contracts import Annotated, Destruction, Float64, Ownership, Pointer, Transfer
values: Annotated[
Pointer[Float64[:]],
Ownership("python"),
Transfer("snapshot_copy"),
Destruction("python_refcount"),
]Metadata describes policy; it does not create backend support. Pointer metadata, for example, must still provide the required shape, nullability, target owner, lifetime, and release facts. It can select implemented descriptor extraction or policy-gated operations, but it cannot extend a pointer target's lifetime or manufacture proof of target ownership.
The Semantic .pyi Format reference later gives canonical spellings and
examples for every Transfer(...) and Destruction(...) mode.
prik supports fixed-form and free-form single-source builds, scalar integer, real, complex, and logical calls, and common scalar results. Primitive scalar inputs are converted for one call; no persistent storage ownership crosses the boundary.
real(8) function square(x)
real(8), intent(in) :: x
square = x * x
end function squareprint(square(3.0)) # 9.0Python immutable scalars cannot expose native in-place mutation. Scalar
intent(out) values are hidden and returned as new Python values. Source-built
primitive scalar intent(inout) arguments remain visible inputs and are
returned as replacement values; the original Python scalar object is unchanged.
Without intent, prik conservatively uses this intent(inout) behavior. This
is common in legacy sources, but fixed-form and free-form code follow the same
rule.
Mutable semantics for strings use replacement projection as described below.
Editable semantic contracts distinguish three numeric scalar boundaries:
Float64accepts a scalar value. If a writable native reference is projected back withReturns["value", Float64], prik copies into call-local storage and returns the mutated replacement; the original Python scalar is unchanged.Float64[()]represents rank-zero NumPy storage. Arguments accept caller-owned 0-D arrays and pass their data address; results return Python-owned 0-D arrays instead of scalar values.Addr(Float64)accepts an integer raw address and forwards it without copying or owning the pointee. For a NumPy buffer, passvalue.ctypes.data.
storage = np.array(3.5, dtype=np.float64)
update_storage(storage)
raw_storage = np.array(4.5, dtype=np.float64)
update_raw(raw_storage.ctypes.data)Addr(Arg(i)) inside @native_call(...) is different from Addr(T): it tells
the wrapper to take the address of its converted call-local scalar. It does not
make the Python caller pass an address.
Named module interfaces and type-bound generics become one Python-visible callable backed by an overload set. Dispatch is exact by scalar or array dtype, rank, and generated extension class. Each target must resolve to a concrete procedure. Two Fortran specifics that collapse to the same Python signature are rejected deterministically during generation.
interface norm
module procedure norm_i32
module procedure norm_f64
module procedure norm_vec
end interface normnorm(np.int32(4))
norm(np.float64(4.0))
norm(np.array([3.0, 4.0], dtype=np.float64))The generated extension selects the concrete target by exact type and rank. A
value with no matching specific raises TypeError. The .pyi contains overload
declarations linked to their concrete native targets with prik's
@overload("specific_name") metadata.
For derived types, dispatch uses the generated wrapper class. Scalar polymorphic input dispatch over a known inheritance hierarchy is described in Inheritance And Polymorphism.
Intrinsic-style defined operators map to Python data-model slots when Python has equivalent syntax:
- arithmetic operators map to
__add__,__sub__,__mul__,__truediv__, and__pow__where signatures permit; - unary operators map to
__pos__and__neg__; - relational operators map to the corresponding comparison slots;
- reverse slots such as
__radd__are generated when operand order permits; and - safe in-place forms use slots such as
__iadd__.
interface operator(+)
module procedure add_vector
module procedure add_scalar_vector
end interface
interface assignment(=)
module procedure assign_vector
end interfacec = a + b
c = 2.0 + a
a.assign(b) # invokes Fortran assignment(=)
a = a.assign(b) # also valid; assign returns the same wrapped objectPython = only rebinds a Python name, so prik never pretends to intercept it.
Fortran defined assignment is exposed as the explicit mutating assign(...)
method, which returns the same object it mutated. Named Fortran operators such
as .cross. become documented methods such as cross(...) rather than
invented Python syntax. Unsupported operands raise deterministic Python errors
through the same overload dispatcher used by generic interfaces.
The Python signature distinguishes values produced by the wrapper from storage that the caller must supply.
Hidden Scalar Outputs
A non-allocatable scalar intent(out) dummy is hidden from the Python argument
list. The bridge allocates temporary native storage and returns the converted
value.
subroutine bounds(values, smallest, largest)
real(8), intent(in) :: values(:)
real(8), intent(out) :: smallest, largest
smallest = minval(values)
largest = maxval(values)
end subroutine boundssmallest, largest = bounds(values)Scalar character outputs follow the same hidden-output shape and return a new
str. Scalar derived-type objects use caller-provided mutable wrappers, as
described below.
A non-allocatable array intent(out) remains visible because the caller must
provide storage. The wrapper validates dtype, rank, shape, layout, alignment,
native byte order, and writeability. Fortran writes into the object and the same
object exposes the result. The wrapper returns None unless another output
requires a Python return value.
subroutine fill(values)
real(8), intent(out) :: values(:)
values = 1.0_8
end subroutine fillvalues = np.empty(4, dtype=np.float64)
fill(values)
print(values) # [1. 1. 1. 1.]The initial contents of an intent(out) array are ignored. An intent(inout)
array also remains visible and is mutated in place. Neither ordinary array is
duplicated in the return value. Array function results and hidden allocatable
outputs still return Python-visible objects because the caller did not provide
their storage. An array without intent follows the same conservative
in-place rule as intent(inout).
A non-optional allocatable array intent(out) dummy is hidden and returned as
an owned AllocatableArray. The generated binding transfers the native result
into persistent descriptor storage. Allocated and unallocated results both
return a present handle; allocation state is read through handle.allocated.
subroutine build_values(n, values)
integer, intent(in) :: n
real(8), allocatable, intent(out) :: values(:)
if (n <= 0) return
allocate(values(n))
values = 2.0_8
end subroutine build_valuesvalues = build_values(3)
print(values.allocated) # True
print(values.to_numpy()) # [2. 2. 2.]
missing = build_values(0)
print(missing.allocated) # False
print(missing.to_numpy()) # NoneFailure to allocate owned descriptor storage after Fortran produced a result
raises MemoryError; it is not confused with an unallocated descriptor.
When a function result and output dummies are returned together, tuple order is stable: function result first, followed by output dummies in Fortran argument order.
real(8) function analyze(x, status, message)
real(8), intent(in) :: x
integer, intent(out) :: status
character(len=32), intent(out) :: message
! ...
end function analyzevalue, status, message = analyze(2.0)Generated .pyi signatures and NumPy-style docstrings use the same projection.
Returns["name", T] is reserved for an explicit replacement projection that
also remains a Python-visible argument. Generated ordinary writable arrays use
in-place mutation without this projection. Hidden outputs use ordinary return
annotations; hidden allocatable array outputs use
Allocatable[T[...]] handles whose unallocated state remains inside the
handle.
Direct allocatable array function results are different: plain
Allocatable[T[...]] means the native function must return an allocated
descriptor, using a zero extent for empty data. Use
Annotated[Allocatable[T[...]], MaybeUnallocated] only for a direct function
result that may return an unallocated descriptor.
Generated modules, functions, classes, constructors, methods, overloads, and
properties expose compact NumPy-style docstrings derived from the same completed
wrapper plan as the executable code. help(module.function) therefore reports
the Python-visible signature rather than the native dummy list, including
hidden outputs, ordered tuple results, optional omission versus a present
None, constrained array shape and layout, handle ownership, and native-status
exceptions.
Module docstrings index their public attributes, functions, and classes. Class docstrings index the public constructor, fields, methods, and overloads; the individual constructor, method, overload, and property descriptors also carry focused docstrings. Private wrapper helper names and internal bridge roles are never shown. Module attributes are documented in the module docstring because Python extension modules do not provide portable per-attribute descriptor docstrings.
Optional scalars, arrays, strings, derived types, outputs, and inout arguments
preserve Fortran present(...) behavior. Required Python parameters are emitted
before optional parameters without changing native dummy positions.
subroutine step(dt, max_iter, tol)
real(8), intent(in) :: dt
integer, intent(in), optional :: max_iter
real(8), intent(in), optional :: tol
! ...
end subroutine stepstep(0.1)
step(0.1, tol=1.0e-8)
step(0.1, max_iter=None)For ordinary Python-visible optional inputs, omission and explicit None both
mean that no native actual argument is passed, so present(dummy) is false.
Optional scalar allocatable and pointer descriptors are the three-state
exception: omission means absent, explicit None means a present unallocated
or unassociated descriptor, and a concrete value means present storage.
Optional intent(out) and intent(inout) dummies remain visible so omission or
None makes native present(dummy) false. Optional scalar outputs use mutable
rank-zero storage such as Int32[()]. An optional allocatable or pointer array
dummy accepts the matching handle or None; a present handle contains its own
unallocated or unassociated state. Hidden scalar or derived-type Return(...)
outputs are different: the wrapper requests them with native temporary storage,
so they are present and returned on every call.
For an assumed-size dummy, Python supplies the actual array and therefore the
runtime storage extent. prik validates declared extents it can express, but it
does not infer the omitted final extent from unrelated companion arguments. The
caller must provide enough storage for the native routine.
Generated semantic .pyi contracts spell this final assumed-size dimension as
Flat, for example Float64[Flat] for real(8) :: values(*).
Float64[Flat] accepts any contiguous NumPy rank from 1 through 15 and passes
the element sequence as a rank-one native view. Multidimensional flat-edge forms
preserve the checked non-flat axes and collapse the remaining contiguous Python
axes into the flat native extent. Float64[rows, Flat] accepts a
Fortran-contiguous actual of rank 2 through 15, checks rows against the first
Python axis, and passes native extents [rows, product(rest)].
Source-generated contracts use the extents that Fortran can declare, such as
values(rows, *) becoming Float64[rows, Flat]. A handwritten
Float64[:, Flat] contract follows the same flattening rule but reads the
prefix extent from the Python actual. It is not a literal Fortran declaration:
values(:, *) is not legal Fortran assumed-size syntax.
Non-default lower bounds are preserved when computing shape constraints; they do not change Python's zero-based indexing.
subroutine shift(n, values)
integer, intent(in) :: n
real(8), intent(inout) :: values(0:n-1)
values = values + 1.0_8
end subroutine shiftvalues = np.zeros(4, dtype=np.float64)
shift(4, values)
print(values) # [1. 1. 1. 1.]Numeric dimension(..) dummies use a generated Fortran rank-dispatch bridge
for NumPy ranks 1 through 15. Each assumed-rank dummy in a call is dispatched at
its own runtime rank. Rank-0 scalars and ranks above 15 are rejected.
subroutine bump(values)
real(8), intent(inout), dimension(..) :: values
select rank (values)
rank (1)
values = values + 1.0_8
rank (2)
values = values + 2.0_8
end select
end subroutine bumpvector = np.zeros(3, dtype=np.float64, order="F")
matrix = np.zeros((2, 2), dtype=np.float64, order="F")
bump(vector)
bump(matrix)Assumed-type type(*), character arrays that cannot be represented as
fixed-width NumPy bytes storage, and derived-type arrays are blocked until their
descriptor, ABI, element construction, and ownership policies are defined.
intent(in)passes the existing native instance by address without transferring ownership;intent(inout)mutates that existing instance;intent(out)fills a caller-provided instance without returning it again;- a dummy without
intentfollows the conservativeintent(inout)rule; and - a function result is copied into a new wrapper-owned native instance before the Fortran temporary expires.
type :: point
real(8) :: x, y
end type point
subroutine move_point(p, dx, dy)
type(point), intent(inout) :: p
real(8), intent(in) :: dx, dy
p%x = p%x + dx
p%y = p%y + dy
end subroutine move_pointp = point(x=1.0, y=2.0)
move_point(p, 3.0, 4.0)
print(p.x, p.y) # 4.0 6.0A nested scalar derived-type component is a borrowed child wrapper. It keeps the parent alive and never destroys the component independently.
type :: particle
type(point) :: origin
real(8) :: mass
end type particleparticle = make_particle()
origin = particle.origin
del particle
origin.x = 4.0 # valid: origin retains the parent ownerPrivate components are omitted from Python descriptors. Allocatable fields use
Allocatable[T[...]] handles, and pointer-array fields use
Pointer[T[...]] handles. Each handle retains the containing wrapper for
descriptor access; that retention does not make the wrapper owner of a pointer
target. Arrays of derived types are blocked.
type :: shape
contains
procedure :: area => shape_area
end type shape
type, extends(shape) :: circle
real(8) :: radius
contains
procedure :: area => circle_area
end type circlec = circle(radius=2.0)
assert isinstance(c, shape)
print(c.area()) # 12.566370614359172A scalar class(base), intent(in) dummy dispatches over the closed set of
wrapped base and descendant classes. Descendants are checked before the base so
a circle selects the circle bridge rather than the general shape bridge.
subroutine print_area(item)
class(shape), intent(in) :: item
! ...
end subroutine print_areaprint_area(shape())
print_area(circle(radius=2.0))Polymorphic outputs, intent(inout), arrays, allocatable or pointer scalar
polymorphic values, and polymorphic function results are blocked. They need a
contract for dynamic type, allocation, replacement, and ownership. class(*)
is blocked with the assumed-type descriptor policy. Abstract types and deferred
bindings produce wrapper-planning errors rather than instantiable Python types.
Native allocation runs Fortran default component initialization. Unless an
edited .pyi chooses another constructor contract, prik generates a
keyword-only Python initializer for public rank-0 numeric, logical, and complex
components. Omitted keywords preserve the native initialized value. When a
generated class has fields but none are eligible constructor keywords, its
contract instead contains def __init__(self) -> None: ... so default
construction remains explicit.
type :: settings
integer :: iterations = 10
real(8) :: tolerance = 1.0e-6_8
contains
final :: finalize_settings
end type settingsdefaulted = settings()
custom = settings(iterations=np.int32(20), tolerance=np.float64(1.0e-8))Private components, arrays, allocatables, pointers, characters, and nested derived components are not automatic constructor keywords.
Removing either generated __init__ form from an edited .pyi suppresses
public construction; prik does not regenerate it. To use one concrete native
initializer, bind __init__ to its native name and place the new object with
Pass():
from prik.contracts import Addr, Arg, Float64, Int32, Pass, bind, native_call
class settings:
@bind("initialize_settings")
@native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))])
def __init__(self, iterations: Int32, tolerance: Float64) -> None: ...Exactly one Pass() identifies the allocated settings object. Its native
position may appear anywhere. Other settings arguments remain ordinary
Arg(...) inputs. The original module-level declaration may remain public or
be marked @private independently.
An edited contract can instead declare multiple __init__ overload links.
The wrapper allocates the ordinary Phase 8 native owner once, selects an exact
candidate from completed dtype/rank/class predicates, invokes that target, and
commits ownership only after it succeeds. Selection never calls candidates to
see which one works. Indistinguishable candidates fail during generation and a
runtime call with no match raises TypeError before native entry.
An owned wrapper invokes Fortran finalization exactly once through its generated
deallocation helper. Failed Python initialization still releases the native
instance allocated by tp_new. Borrowed child wrappers never finalize their
native component; the owner finalizes the containing object.
Final subroutines have no recoverable Python status channel during tp_dealloc.
A finalizer that executes stop, error stop, aborts, or otherwise terminates
native execution terminates the process.
Supported public scalar numeric, logical, and complex module variables are normal Python module attributes. Reading an attribute fetches current native storage; assigning to it writes through to the Fortran module variable. Generated native getter and setter bridge functions are implementation details and are not Python-callable procedures.
module state
integer :: counter = 0
integer, parameter :: max_count = 100
contains
subroutine advance()
counter = counter + 1
end subroutine advance
end module stateprint(counter) # 0
counter = np.int32(4)
advance()
print(counter) # 5
print(max_count) # 100Parameters become Final[...] constants and have no native setter. A literal
value is materialized directly by the binding. When a numeric initializer
remains a Fortran expression, a generated bridge getter reads the
compiler-evaluated parameter while the Python module is initialized. Rebinding
module.max_count only shadows the Python attribute and does not change native
Fortran state. Private variables are omitted.
Allocatable module arrays are attributes returning persistent
Allocatable[T[...]] handles:
allocate_values(3)
handle = values
print(handle.allocated) # True
view = handle.to_numpy()
view[0] = 5.0 # writes native module storage
independent = view.copy()
deallocate_values() # invalidates the native storage behind viewPointer-array module variables return Pointer[T[...]] handles with a default
conservative policy for association inspection and legal descriptor operations.
Extraction and ownership-changing operations remain policy-gated. Explicit
save on a public module variable does not change exposure because module
storage already has module lifetime. Procedure-local save variables remain
internal.
Common-block storage is never exported as Python variables or modeled by prik. Wrapped native procedures may read and write it normally:
subroutine write_shared(value)
integer, intent(in) :: value
integer :: shared
common /shared_block/ shared
shared = value
end subroutine write_sharedwrite_shared(np.int32(17))
print(read_shared()) # 17prik adds no independent lock for module or object state. Concurrency rules are covered in Runtime Errors, The GIL, OpenMP, And Concurrency.
enum, bind(C) enumerators become ordinary typed integer constants. prik does
not generate Python Enum or IntEnum classes. Procedure arguments, results,
fields, and variables that carry enumerator values remain ordinary integer
types.
enum, bind(C)
enumerator :: red = 1
enumerator :: blue
enumerator :: invalid = -1
end enumThe generated semantic stub preserves the values:
from prik.contracts import Final, Int32
red: Final[Int32] = 1
blue: Final[Int32] = 2
invalid: Final[Int32] = -1The underlying bind(C) integer representation is retained as metadata. The
underlying procedure and field surface remains the resolved integer dtype.
The public scalar character type is Python str. Native character storage is
copied at the boundary, so returned strings are Python-owned and never borrow a
Fortran character buffer.
subroutine edit_name(name)
character(len=8), intent(inout) :: name
name(1:1) = "X"
end subroutine edit_nameoriginal = "alpha "
replacement = edit_name(original)
print(repr(original)) # 'alpha ' (unchanged)
print(repr(replacement)) # 'Xlpha 'The wrapper copies the input into mutable native storage, calls Fortran, and
returns a new Python string. A hidden intent(out) string is returned like any
other scalar output.
character(len=8) function label()
label = "ready"
end function labelprint(repr(label())) # 'ready 'Character arrays use fixed-width NumPy bytes dtypes such as S5; the dtype
itemsize is the Fortran element length. Deferred-length allocatable character
arrays carry that length at runtime and return a fresh fixed-width bytes array.
Python Unicode arrays, object arrays, mutable scalar deferred-length character
storage, deferred-length character fields, and mutable character-buffer fields
remain blocked until an explicit field and encoding policy exists.
Wrapper builds use compiler probing rather than assuming that a Fortran kind
number equals a byte width. Character is not included in storage_size
probing: its semantic family is always String, and its element length is
tracked independently from the declaration or runtime descriptor.
The supported scalar storage subset is:
- signed integers corresponding to 8, 16, 32, and 64 bits;
- real values corresponding to 32 and 64 bits; and
- complex values corresponding to 64 and 128 total bits.
Direct Boolean function results use a normalized bridge ABI. The native result
is first stored as logical(c_bool), then the Fortran bridge returns its low
truth bit as integer(c_int8_t). The C binding explicitly converts that 0 or
1 value to bool. This prevents processor-specific noncanonical logical bit
patterns from being interpreted as C truth values while leaving native
Fortran logical evaluation unchanged.
Mutable ordinary Boolean array buffers use the same low-bit rule on writeback.
After the native call, the bridge normalizes every returned byte with
iand(value, 1_c_int8_t) before Python observes the NumPy buffer.
module kinds_api
use iso_fortran_env, only: int64, real64
contains
complex(real64) function combine(count, value)
integer(int64), intent(in) :: count
complex(real64), intent(in) :: value
combine = count * value
end function combine
end module kinds_apiresult = combine(np.int64(3), np.complex128(1.0 + 2.0j))
print(result) # (3+6j)Target mappings are validated before wrapper compilation. Real storage wider than 64 bits and complex storage wider than 128 bits are blocked rather than silently down-converted. Wider explicit logical kinds are blocked because they lack a portable Python/NumPy Boolean round-trip contract.
python3 -m prik \
solver.f90 \
diagnostics.f90 \
--out-dir buildimport solver
result = solver.solve(32)
solver.print_diagnostics(result)prik does not discover missing sources, infer a dependency graph, or reorder files. The caller or build system must provide all sources in compiler-valid order. Standalone external procedures from several files can be merged the same way.
Semantic .pyi output writes a contract package. With an explicit --out, the
requested directory is the package itself. The package contains one
__init__.pyi entry contract and one flat <fortran-module>.pyi leaf for each
native Fortran module from the ordered source inputs. prik does not add
per-source subdirectories or a synthetic combined_extensions/ directory.
When --out is omitted, prik prints the contract report. When --out is
present without a path, prik writes adjacent source-owned packages beside each
input source for inspection workflows. Use explicit --out PATH for
wrapper-contract builds and parity tests.
Example 1: one source containing one native module writes one package entry and one leaf directly under the requested directory.
python3 -m prik generate --pyi solver.f90 --out contracts/solvercontracts/solver/
├── __init__.pyi
└── solver_mod.pyi
Example 2: two ordered sources that each define two native modules write one combined package with five files total when no extra dependency stubs are needed.
python3 -m prik generate --pyi first_api.f90 second_api.f90 --out contractscontracts/
├── __init__.pyi
├── first_math.pyi
├── shared_types.pyi
├── second_math.pyi
└── box_ops.pyi
Example 3: the generated entry is the only semantic wrapper input. Native objects are separate build inputs and keep caller order.
python3 -m prik contracts/__init__.pyi \
--out first_api \
--native-objects native/first_api.o native/second_api.o \
-I native \
--out-dir build/first_apiExample 4: source and generated-contract parity builds use the same extension name and native module namespaces.
from prik import build_pyi_extension
result = build_pyi_extension(
"contracts/__init__.pyi",
native_objects=["native/first_api.o", "native/second_api.o"],
native_include_dirs=["native"],
output_name="first_api",
output_dir="build/first_api",
)
print(result.module_name) # first_apiExample 5: a modified entry may add documented Python export policy while preserving native module leaves.
# contracts/__init__.pyi
from . import first_math
from . import shared_types
from . import second_math
from . import box_ops
from .second_math import double_after_add as fused_valueThis keeps first_api.second_math.double_after_add(...) available and also
exports first_api.fused_value(...). The module leaves still define the native
module contracts; the entry only changes the Python-facing export tree.
python3 -m prik generate --makefile mesh.f90 solver.f90 --out-dir build
make -f build/Makefile.prik -j4 PRIK_FFLAGS=-O3 PRIK_CFLAGS=-O3For semantic .pyi builds, Makefile mode writes prik-build.json before
Makefile.prik and the Makefile is regenerated from that manifest:
python3 -m prik generate --makefile contracts/solver.pyi \
--native-fortran-sources native/solver.f90 \
--out-dir build/solver
python3 -m prik --build-manifest build/solver/prik-build.jsonOnly public Fortran procedures, generic interfaces, derived types, type-bound bindings, fields, and variables are exported. Private declarations remain implementation details. A public signature may not expose a private derived type.
The same normalization applies to module members, types, methods, fields, and keyword arguments:
- Fortran identifiers are lowercased because Fortran lookup is case-insensitive.
- A Python keyword gains one trailing underscore, so
classbecomesclass_. - Invalid identifier characters become underscores, and a leading underscore is added when the first character would otherwise be invalid.
- Module variables retain
<name>as Python attributes; generated native accessors remain internal. Parameters retain<name>as constants.
class_(np.int32(4)) # Python name
# native call uses native_class_entryPython escaping changes only the public Python surface. For example, a native
Fortran variable named lambda remains lambda in native code and is exposed
to Python as lambda_. Generated native symbols are checked against their own
target-language restrictions, not Python's keyword list.
Every normalized public name must be unique in its namespace. Module members share one namespace, each derived type has a field/method namespace, and each callable has a keyword-argument namespace.
Default mode appends deterministic numeric suffixes:
class_
class__2
class__3
Generated native symbols use the same readable duplicate convention, while a target-language reserved word gets an explicit wrapper suffix:
value # first generated symbol
value_2 # another generated symbol named value
module_prik # generated symbol whose original spelling is a native reserved word
module_prik_2 # collision with the escaped spelling
The generator does not invent semantic names for collisions: the source name and deterministic suffix make generated artifacts easy to trace and reproduce.
Generated helper names use an internal namespace, so a user procedure named
get_value does not collide with the internal accessor for a variable named
value.
With --strict-wrapper-names, prik applies no fixes. Any name requiring keyword
or identifier escaping, or any collision after normalization, raises a
generation error before native compilation.
prik supports dummy procedures invoked during the wrapped call. It resolves
local explicit interfaces and named abstract interfaces into named
@prototype declarations containing argument order, types, value/reference
transport, array ranks and shapes, derived-type references, and result type.
Source intent remains in the native interface when that interface must be
imported, but it is not repeated in the semantic prototype.
abstract interface
real(8) function scalar_callback(value)
real(8), intent(in) :: value
end function scalar_callback
end interface
real(8) function apply(callback, value)
procedure(scalar_callback) :: callback
real(8), intent(in) :: value
apply = callback(value)
end function applyresult = apply(lambda value: 3.0 * value, np.float64(2.5))
print(result) # 7.5The generated wrapper keeps a strong reference to the callback only until the native call returns. Nested callback-taking calls on the same entering Python thread are supported.
- primitive scalars use matching owned NumPy scalar values, regardless of whether their native callback ABI is a value or reference;
- arrays require exact dtype, rank, declared shape, alignment, and Fortran contiguity;
- derived values require the generated wrapper type;
- reference arrays, characters, and derived objects are handled permissively and written back before the adapter returns; scalar reference writeback is unsupported, so model that native output as the callback result; and
- primitive scalars may be retained safely. Temporary NumPy array views and borrowed derived wrappers passed to the callback are valid only during that callback invocation.
subroutine transform(callback, values)
interface
subroutine callback(values)
real(8), intent(inout) :: values(:)
end subroutine callback
end interface
procedure(callback) :: callback
real(8), intent(inout) :: values(:)
call callback(values)
end subroutine transformvalues = np.ones(3, dtype=np.float64, order="F")
def double(array):
array *= 2.0
transform(double, values)
print(values) # [2. 2. 2.]The callback trampoline acquires the GIL for Python invocation and releases the matching GIL state afterward. The callback must execute on the Python thread that entered the wrapped routine.
Stored callbacks, callback registration, optional dummy procedures, procedure pointers, and invocation after the wrapped call are not supported.
prik raises ordinary Python exceptions for wrapper-level failures such as wrong type, rank, shape, layout, unsupported argument mode, allocation failure, or failed conversion. It does not infer application-specific Fortran error conventions.
Without explicit metadata, status, info, and message outputs remain ordinary
outputs. Native stop or error stop can terminate the Python process.
An edited semantic .pyi can opt into status projection:
from prik.contracts import Float64, Int32, Returns, String, raises
@raises(status="status", message="message", success=0)
def solve(
x: Float64[:],
) -> tuple[Returns["status", Int32], Returns["message", String]]: ...solve(values) # returns None when status == 0
solve(bad_values) # raises RuntimeError(message) otherwiseThe status target must be a hidden scalar integer output. The optional message target must be a hidden string output. Annotated status and message values are consumed rather than returned. prik cannot recover from native termination, process abort, or a callback failure crossing a native callback boundary.
Module-variable and class-property accessors, constructors, destructors, and
other generated procedures keep the GIL automatically. An edited .pyi can
explicitly release it around one native procedure call:
from prik.contracts import Int32, nogil
@nogil
def update_disjoint_state(value: Int32) -> None: ...@nogil accepts no arguments. It releases the GIL only around the native
bridge call; conversion, writeback, cleanup, and exception projection keep it.
Use it only when the native call is safe while other Python threads execute.
For callback-taking calls, the callback trampoline reacquires the GIL during
Python execution.
Keeping the GIL serializes against ordinary Python threads in the same interpreter; it is not a lock against native threads, OpenMP workers, external libraries, or another interpreter.
OpenMP is an explicit build/runtime choice. Add @nogil when a callback-free
OpenMP procedure should run concurrently with Python threads. For GNU Fortran,
pass OpenMP flags to both compile and link steps:
python3 -m prik generate --makefile parallel_api.f90 --out-dir build
make -f build/Makefile.prik \
PRIK_FFLAGS=-fopenmp \
PRIK_LDFLAGS=-fopenmpvalues = np.arange(1, 33, dtype=np.float64)
print(parallel_sum(values)) # 528.0prik does not infer host-memory synchronization. Callers must protect arrays, module variables, object state, and aliases touched by concurrent Python calls, OpenMP workers, or external native code. Use native locks, Python locks around the whole call, disjoint storage, or the default held-GIL policy where its limited serialization scope is sufficient.
The verified compiler path includes GNU Fortran and debug/optimized ABI builds. Other compilers and platforms require their own ABI validation; support is not inferred from GNU results.
This chapter groups behavior for which implementation or policy is incomplete. These items are not enabled by parser support or by editing metadata unless the backend contract described here is also implemented.
Module and derived-field pointer handles can expose borrowed NumPy views when completed policy proves descriptor extraction, target owner, lifetime, shape, and mutability. Descriptor metadata supports contiguous and strided targets. The handle retains the descriptor owner, but prik cannot invalidate an existing NumPy view after native reassociation, nullification, owner destruction, or target reallocation. Discard old views after those operations.
Pointer-array results use wrapper-owned persistent descriptor storage without claiming ownership of the target. The native API must still provide a target whose lifetime outlives every use through the returned handle. Persistent reassociation and pointer-driven allocation, deallocation, or resize require explicit completed policy; wrapper planning blocks an unproved request instead of guessing ownership.
The basic caller-ordered multi-source build is supported, but prik does not yet:
- resolve every renamed or
onlyimport collision while merging wrapped modules; - expose submodule and separate-module procedures as additional public API; or
- discover or infer prebuilt Fortran module and library search paths.
Callers currently provide compilable source files in valid order and pass required module, include, library, and runtime-search paths explicitly. A separate build system remains responsible for source discovery, dependency resolution, and locating external artifacts.
Callbacks are call-scoped only. prik does not support:
- registration and unregistration of stored Python callbacks;
- persistent Python-reference ownership after the wrapped call;
- procedure-pointer association or null procedure pointers;
- optional dummy procedures; or
- later callback execution across threads, object destruction, or library shutdown.
These require a persistent handle with explicit owner, lifetime, thread, exception, unregistration, and destruction rules.
The following forms have stable wrapper-planning errors rather than unsafe partial wrappers:
| Subject | Blocked form | Missing contract |
|---|---|---|
| Allocatables | Passing a module allocatable scalar derived object to an allocatable dummy | The object address is not the concrete allocatable descriptor; use a wrapper-owned allocatable result holder. |
| Arrays | Assumed type type(*) |
Runtime dtype and descriptor policy. |
| Arrays | Character arrays not representable as fixed-width bytes dtype | Encoding, ABI, allocation, and ownership. |
| Arrays | Derived-type arrays | Element layout, construction, destruction, aliasing, and copy/view behavior. |
| Pointers | Scalar-derived pointer results without stable typed holder storage, expired-target results, and unproved reassociation or ownership-changing operations | Stable target lifetime, descriptor identity, typed holder storage, or explicit operation policy. |
| Polymorphism | Results, mutable dummies, arrays, allocatable/pointer scalars, class(*) |
Dynamic type, allocation, replacement, and ownership. |
| Constructors | Incomplete or indistinguishable constructor overload sets | Every candidate needs a complete exact runtime signature and compatible owner lifecycle. |
| Characters | Mutable scalar allocatable character dummies and deferred-length mutable fields | Allocation, encoding, replacement, and destruction. |
| Kinds | Real wider than 64 bits, complex wider than 128 bits, wider explicit logical storage | Portable NumPy round-trip without silent precision loss. |
| Callbacks | Stored, optional, cross-thread, or procedure-pointer callbacks | Persistent ownership, thread, exception, nullability, and teardown. |
If a documented wrapper behavior does not match the generated extension, first
compare the native source, generated .pyi, Python call, dtype, shape, and
ownership expectations. Use --verbose for build failures and reduce runtime
failures to the smallest source and call that still reproduces the mismatch.