Skip to content

Latest commit

 

History

History
2307 lines (1834 loc) · 86.5 KB

File metadata and controls

2307 lines (1834 loc) · 86.5 KB
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

Fortran Wrapper Reference

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.

Contents

Building And Importing A Wrapper

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_abi

Add 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.5

Native 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.

Wrapper Build Mechanism

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 solver

PRIK 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.json

Edited .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",
)

Native Build Plan In Build Results

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_dirs and include_dirs: directories needed while compiling the generated bridge; and
  • link_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)  # object

Example 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)  # object

Example 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.

How Support Claims Are Established

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 And Lifetime

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.

Ownership Vocabulary

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.

Ownership Invariants

The wrapper enforces these invariants:

  1. Exactly one owner destroys each owned native allocation.
  2. A Python-owned copy is independent of later native mutation.
  3. A borrowed child or view keeps a Python wrapper owner alive when that owner contains the referenced storage.
  4. Keeping the Python owner alive does not protect a view from native reallocation or deallocation performed by another native call.
  5. A pointer component does not imply ownership of its target.
  6. Missing owner, lifetime, release, shape, dtype, contiguity, mutability, or aliasing facts produce a blocker.

Destruction Rules

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.

Borrowed Handle And View Example

type :: buffer
  real(8), allocatable :: values(:)
end type buffer
b = 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.0

If a later method reallocates values, an older borrowed view is not automatically invalidated. Use .copy() before that operation when Python needs an independent lifetime.

Policy Overrides In Semantic .pyi Files

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.

Scalar Calls And Verified Baseline

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 square
print(square(3.0))  # 9.0

Python 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:

  • Float64 accepts a scalar value. If a writable native reference is projected back with Returns["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, pass value.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.

Generic Procedure Interfaces

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 norm
norm(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.

Defined Operators And Assignment

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 interface
c = a + b
c = 2.0 + a

a.assign(b)             # invokes Fortran assignment(=)
a = a.assign(b)         # also valid; assign returns the same wrapped object

Python = 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.

Output Arguments And Multiple Results

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 bounds
smallest, 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.

Caller-Provided Array Outputs

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 fill
values = 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).

Allocatable Outputs

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_values
values = 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())  # None

Failure to allocate owned descriptor storage after Fortran produced a result raises MemoryError; it is not confused with an unallocated descriptor.

Tuple Ordering

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 analyze
value, 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 Docstrings

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 Arguments

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 step
step(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.

Assumed-Size And Lower Bounds

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 shift
values = np.zeros(4, dtype=np.float64)
shift(4, values)
print(values)  # [1. 1. 1. 1.]

Assumed Rank

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 bump
vector = 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.

Derived Types Across Procedure Boundaries

Scalar Arguments And Results

  • 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 intent follows the conservative intent(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_point
p = point(x=1.0, y=2.0)
move_point(p, 3.0, 4.0)
print(p.x, p.y)  # 4.0 6.0

Nested Components

A 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 particle
particle = make_particle()
origin = particle.origin
del particle

origin.x = 4.0          # valid: origin retains the parent owner

Private 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.

Inheritance And Polymorphism

type :: shape
contains
  procedure :: area => shape_area
end type shape

type, extends(shape) :: circle
  real(8) :: radius
contains
  procedure :: area => circle_area
end type circle
c = circle(radius=2.0)
assert isinstance(c, shape)
print(c.area())  # 12.566370614359172

A 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_area
print_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.

Constructors, Initialization, And Finalizers

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 settings
defaulted = 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.

Edited Constructor Contracts

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.

Finalization

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.

Module Variables, Constants, Saved State, And Common Blocks

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 state
print(counter)  # 0
counter = np.int32(4)
advance()
print(counter)  # 5

print(max_count)  # 100

Parameters 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 view

Pointer-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_shared
write_shared(np.int32(17))
print(read_shared())  # 17

prik adds no independent lock for module or object state. Concurrency rules are covered in Runtime Errors, The GIL, OpenMP, And Concurrency.

Fortran Enums

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 enum

The generated semantic stub preserves the values:

from prik.contracts import Final, Int32

red: Final[Int32] = 1
blue: Final[Int32] = 2
invalid: Final[Int32] = -1

The underlying bind(C) integer representation is retained as metadata. The underlying procedure and field surface remains the resolved integer dtype.

Character Arguments, Results, And Fields

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.

Input, Output, And Replacement

subroutine edit_name(name)
  character(len=8), intent(inout) :: name

  name(1:1) = "X"
end subroutine edit_name
original = "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.

Length, Encoding, And NUL Rules

character(len=8) function label()
  label = "ready"
end function label
print(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.

Scalar Types And Kind Coverage

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_api
result = 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.

Derived-Type Layout And Interoperability

Multiple Sources And Build Modes

python3 -m prik \
  solver.f90 \
  diagnostics.f90 \
  --out-dir build
import 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 Stub Output

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/solver
contracts/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 contracts
contracts/
├── __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_api

Example 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_api

Example 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_value

This 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.

Editable Makefile

python3 -m prik generate --makefile mesh.f90 solver.f90 --out-dir build
make -f build/Makefile.prik -j4 PRIK_FFLAGS=-O3 PRIK_CFLAGS=-O3

For 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.json

Visibility, Naming, And The Python Surface

Only 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.

Name Normalization

The same normalization applies to module members, types, methods, fields, and keyword arguments:

  1. Fortran identifiers are lowercased because Fortran lookup is case-insensitive.
  2. A Python keyword gains one trailing underscore, so class becomes class_.
  3. Invalid identifier characters become underscores, and a leading underscore is added when the first character would otherwise be invalid.
  4. 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_entry

Python 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.

Collisions

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.

Immediate Python Callbacks

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 apply
result = apply(lambda value: 3.0 * value, np.float64(2.5))
print(result)  # 7.5

The 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.

Callback Values

  • 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 transform
values = np.ones(3, dtype=np.float64, order="F")

def double(array):
    array *= 2.0

transform(double, values)
print(values)  # [2. 2. 2.]

GIL, Threads, And Exceptions

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.

Runtime Errors, The GIL, OpenMP, And Concurrency

Wrapper Errors And Fortran Errors

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) otherwise

The 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.

GIL Policy

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

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=-fopenmp
values = np.arange(1, 33, dtype=np.float64)
print(parallel_sum(values))  # 528.0

prik 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.

Not Handled Or Not Yet Settled

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.

Pointer Views And Reassociation

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.

Advanced Multi-Source Integration

The basic caller-ordered multi-source build is supported, but prik does not yet:

  • resolve every renamed or only import 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.

Persistent Callbacks And Procedure Pointers

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.

Other Explicit Blockers

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.

Troubleshooting

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.