| title | Semantic .pyi Format |
|---|---|
| audience | users, advanced users, developers |
| prerequisites | semantic IR reference, wrapper build workflow |
| related | index.md, semantic-ir.md |
| status | maintained |
| publication | draft |
For the supported edit workflow and runtime consequences of changing a
contract, including ownership and destruction examples, see
Editing .pyi contracts.
The normal wrapper workflow accepts recognizable Fortran source without a
stage flag. A .pyi-driven wrapper workflow is also available for the
implemented subset: pass the semantic .pyi file as the wrapper input and
provide native object, archive, shared-library, module, include, and link
inputs with the native artifact flags. The .pyi input selects the wrapper
stage automatically and remains the source of truth for the Python API; native
source is not reparsed to reconstruct the contract.
The implemented subset and remaining parity limits are stated in this reference and summarized later in Language Support.
Status terms used below:
- Generated: emitted today by
--pyiorwrapper_codegen.printers.pyi_printer. - Loaded: accepted today by
prik.parsers.pyiand converted back to semantic IR. - Planning: can be lowered by the implemented wrapper planner once its semantic policy is complete.
- Build input: accepted by the
.pyiwrapper build for the implemented subset when the required native artifacts are supplied. - Roadmap: design direction, not implemented wrapper behavior.
Parser-related pull requests that change prik/parsers/pyi/ or its focused
loading tests should update this reference when the documented behavior
changes.
Every semantic .pyi control name is imported from prik.contracts. This
single namespace also re-exports the typing forms used by the contract:
from prik.contracts import Addr, Arg, Final, Flat, Float64, Int32, native_call
max_colors: Final[Int32] = 256
@native_call([Addr(Arg(0))])
def inspect(values: Float64[Flat]) -> None: ...The loader follows each import binding, including arbitrary as aliases,
instead of matching the final spelling. For example, importing
Flat as LayoutFlat makes LayoutFlat the contract marker. An unimported
Flat, Arg, or Float64 is therefore a user symbol. When a user declaration
has the same name, generated contracts alias only the imported control name:
from prik.contracts import Final, Flat as LayoutFlat, Float64, Int32
Flat: Final[Int32] = 10
values: Float64[LayoutFlat]The alias spelling in generated files is an implementation detail; edited
contracts may use any non-conflicting local alias imported from
prik.contracts.
Bare-name compatibility is not part of the format. Contract files must import every prik or re-exported typing form they use.
The user-facing Fortran, semantic .pyi, Python, and NumPy mapping is documented
in Data Types. The underlying semantic model is
documented in Semantic IR reference.
Semantic .pyi files are ordinary Python syntax, so a user can write many
things that are syntactically valid but not meaningful to prik. The wrapper
build accepts only the documented semantic subset. Unsupported syntax, unknown
metadata, missing native facts, contradictory projection metadata, and unsupported
runtime policy must never be ignored and must never trigger a hidden fallback to
native-source parsing.
Failures should happen at the earliest layer that has enough information:
- Stub-shape errors fail while loading the
.pyi: unsupported decorators, ordinary function bodies, untyped parameters, invalidAnnotatedmetadata, unknown semantic types, missing relative imports, import cycles, or conflicting exports. - Structural contract errors fail during semantic validation:
incomplete
@native_callmappings, duplicate native argument positions, missing or incompatible@bind/@overloadtargets, public declarations that expose private types, or native-placement facts that contradict the contract file shape. - Unsupported policy fails during wrapper planning or lowering: ownership, lifetime, replacement, pointer reassociation, callback lifetime, coercion, or allocation behavior that prik cannot yet express safely.
- Native artifact mismatches fail during compile, link, import, or runtime
execution. prik can validate the
.pyicontract structure; it cannot prove that an arbitrary caller-supplied object, archive, or shared library implements the declared ABI.
Actionable diagnostics should name the contract path when available, the declaration or import being processed, the invalid fact, and the expected documented form. When prik can continue only by guessing, it should report an error instead of guessing.
When a .pyi file is converted from disk, syntax diagnostics use Python's
filename field and semantic pipeline diagnostics prefix the message with the
contract path. Inline helper calls such as pyi_text_to_semantic_module(...)
do not invent a path; pass a filename= when inline syntax diagnostics need
source provenance.
Some edited contracts intentionally request a lower-level native identity call.
Those are not misuse if they are structurally complete, but the Python behavior
is exactly the behavior declared in the .pyi. For example, an identity
fixed-length String[n] writable argument can return None; if the caller
passed an ordinary Python str, native mutation happened in temporary native
storage and is not observable in Python. To request Python-visible replacement
behavior, write a projected return contract such as
Returns["name", String[n]].
Future unsafe, coercion, or copy/readback modes must be explicit .pyi metadata.
prik must not infer them from malformed syntax or from a declaration that merely
looks risky.
Immutable marks a Python-visible value as replace-only: native code may write a
temporary representation, but the caller's Python object must not be mutated in
place. Transfer("borrowed_view") requests no-copy shared storage. Combining
Immutable with a writable borrowed view is contradictory and fails while
loading the .pyi contract:
from prik.contracts import Annotated, Float64, Immutable, Transfer
def normalize(
values: Annotated[Float64[:], Immutable, Transfer("borrowed_view")]
) -> None: ...The diagnostic tells the user to choose one contract: remove Immutable for an
in-place no-copy view, or keep Immutable and use a projected replacement return
such as Returns["values", Float64[:]].
Immutable is a post-IR policy input, not a bridge heuristic. For writable
native storage, policy completion must choose either copy-in/copy-out replacement
or an explicit call-local copy whose native mutation is discarded. A replacement
requires a projected return such as Returns["values", Float64[:]]; the bridge
and binding then emit the already-selected action without reconsidering the
datatype, mutability, ownership, or storage mode. Unsupported combinations block
before wrapper planning and direct lowering.
@native_call(...) and Returns[...] describe projection and native placement;
they do not ask the backend to rediscover conversion policy. After .pyi
loading, policy completion records two barrier actions for each argument. The
Python barrier defines how the generated Python extension consumes the Python object
(T, T[()], arrays, strings, raw addresses, or wrapper instances). The native
barrier defines how the bridge passes the extracted value onward (value,
call-local address, caller storage address, raw address, array descriptor, or
wrapper address). Code generation dispatches from those recorded actions and
fails closed for missing handlers.
Loaded files support imports, classes, enums, variables and stub functions:
from prik.contracts import Addr, Arg, Final, Float64, Int32, native_call
from types_mod import particle
answer: Final[Int32]
class particle:
id: Int32
mass: Float64
@native_call([Addr(Arg(0)), Arg(1)])
def scale(
n: Int32,
values: Float64[n],
) -> None: ...Function and method bodies must be .... Positional-only, keyword-only,
*args, **kwargs, untyped parameters and ordinary Python statements are not
part of the semantic format. The generated keyword-only derived-type
constructor described below is the only keyword-only exception.
Wrapper commands accept exactly one entry .pyi. Relative imports from that
entry recursively discover the remaining contract files and reconcile imported
type references across the discovered project. Low-level semantic loading may
still operate on the resulting file set internally; users do not pass that set
as separate wrapper inputs.
Fortran use statements at module scope may still generate flattened .pyi
imports when that is unambiguous:
module physics
use types_mod, only: particle
contains
subroutine move(p)
type(particle), intent(inout) :: p
end subroutine move
end module physicsfrom types_mod import particle
def move(p: particle) -> None: ...A procedure-local use that appears in the public API is represented by the
imported module namespace instead. The public datatype spelling is the origin
module plus the original exported type name:
module physics
contains
subroutine move(p)
use types_mod, only: local_particle => particle
type(local_particle), intent(inout) :: p
end subroutine move
end module physicsfrom . import types_mod
def move(p: types_mod.particle) -> None: ...The Fortran rename local_particle is a procedure-local convenience and is not
preserved as the public datatype spelling. Two modules that both export
state therefore remain distinct as a_types.state and b_types.state.
Generated procedure-local namespace imports are not aliased; if the generated
module already has a public declaration or another import named a_types,
contract generation fails instead of inventing a replacement alias.
Wrapper generation must distinguish immutable native structure from editable
Python export policy. Module .pyi files describe where native declarations
actually live. A root export contract describes where those declarations appear
in Python. Export policy must never rewrite native module membership or ABI
facts.
Every contained Fortran module is emitted as a leaf named after that module:
solver_mod.pyi
The leaf filename is the native module identity. Renaming the leaf changes the
module selected by generated bridge code. Module procedures need no placement
or kind decorator: a declaration returning None is a subroutine; an
unprojected return is a function result; returns named by @native_call are
native output arguments.
The ordinary module-procedure form is intentionally small when the Python signature already describes the native argument order:
from prik.contracts import Float64
def update(value: Float64[()]) -> None: ...Only standalone procedures carry @external:
from prik.contracts import Float64, external
@external
def update(value: Float64[()]) -> None: ...@bind("native_name") remains necessary only when the Python declaration name
differs from the native symbol. @native_call remains necessary only when the
Python signature hides, inserts, or reorders native arguments. Pass() marks
the implicit class instance. This is a method receiver or a newly allocated
constructor object.
Ordinary semantic types are the native type contract. Int32, Float64,
Addr, scalar storage rank (), array rank, shape, and focused metadata such
as Allocatable are not duplicated with source-language spellings.
Write the Python boundary shape directly as T, T[()], T[:], Addr(T),
or WrappedType.
Final[T] remains the module-variable and constant spelling. @native_type(...)
is emitted only when a derived type has irreducible attributes or finalizers.
These facts are structurally validated before .pyi wrapper code generation.
They are declarations about the supplied native artifacts, not binary
introspection: prik cannot prove that an arbitrary opaque binary actually uses
the declared ABI.
One Fortran module maps to one .pyi file named for that module. A procedure
declared without @external in that module contract is contained in the native
Fortran module:
from prik.contracts import Float64
# module1.pyi
def update(value: Float64[()]) -> None: ...The generated Fortran bridge imports the procedure from its retained native scope, conceptually:
use module1, only: updateThe contract must retain the native module name even when Python export policy
later aliases or hides update. A modified module .pyi cannot move the
procedure to another module or reinterpret it as standalone.
A procedure outside every Fortran module is marked explicitly with
@external:
from prik.contracts import Float64, Int32, external
# externals/dgesv.pyi
@external
def dgesv(a: Float64[:, :], b: Float64[:, :]) -> Int32: ...@external is immutable native-placement metadata. The bridge calls the
external procedure without a use <module> statement. Classic
implicit-interface-compatible procedures use a compact external declaration;
features that require an explicit interface retain one. The procedure needs no
Fortran .mod file, but its defining object, archive, or shared library must be
supplied to the link.
Python-visible renaming is separate from placement. @bind retains the native
Fortran procedure name while the declaration uses a wrapper name:
from prik.contracts import Float64, Int32, bind, external
@external
@bind("dgesv")
def solve(a: Float64[:, :], b: Float64[:, :]) -> Int32: ...Here the bridge calls the external native procedure dgesv; the root export
contract may expose the wrapper declaration as solve. @bind does not turn a
module procedure into an external procedure and @external does not rename a
symbol.
Every generated standalone declaration must carry @external. Handwritten
contracts must do the same. Missing or contradictory placement metadata must
fail during .pyi validation or wrapper planning, before bridge emission or native
compilation.
For Fortran --pyi --out PATH, PATH is the generated contract package
directory. The package entry is PATH/__init__.pyi. Native Fortran module
contracts are flat leaves named <fortran-module>.pyi directly under PATH;
the generator does not add per-source directories.
| Native input shape | Generated contract shape |
|---|---|
| One source containing one module | __init__.pyi plus one <module>.pyi leaf |
| One source containing several modules | __init__.pyi plus one flat leaf per native module |
| Several ordered sources containing modules | one combined package with one __init__.pyi and one flat leaf per native module across all sources |
| One fixed- or free-form source containing only standalone procedures | one __init__.pyi entry with @external on every procedure |
| Several standalone-procedure sources, such as BLAS/LAPACK | one compact __init__.pyi entry containing all generated @external declarations |
| Mixed modules and standalone procedures | one entry contract containing standalone declarations and importing module leaves |
For example, explicit output for basic_subroutine.f90 containing module m1
emits:
contracts/basic_subroutine/
├── __init__.pyi # entry contract: from . import m1
└── m1.pyi # declarations for native module m1
The entry file is the only wrapper input. It recursively discovers its native leaves:
python3 -m prik contracts/basic_subroutine/__init__.pyi \
--native-objects basic_subroutine.oFor __init__.pyi, the package directory name supplies the extension name
unless wrapper --out NAME is provided. The runtime follows the entry's import
policy: from . import m1 exposes basic_subroutine.m1, while
from .m1 import * explicitly flattens m1 into the extension root.
Passing a leaf such as m1.pyi directly builds a flat extension named m1
unless --out NAME overrides it, so the same declaration is exposed as
m1.update(...) instead of under a package child namespace.
A mixed source keeps standalone procedures in the entry contract and marks each
one with @external:
from prik.contracts import Float64, external
from . import m1
@external
def func(value: Float64[()]) -> None: ...This exposes basic_subroutine.func and basic_subroutine.m1.add1. The
standalone marker remains necessary because the bridge must distinguish an
external call from use m1, only: add1.
For several ordered sources, the requested output directory is still the package itself. If two sources each define two modules, then:
python3 -m prik generate --pyi first_api.f90 second_api.f90 --out contractsemits exactly this shape when no extra dependency stubs are needed:
contracts/
├── __init__.pyi
├── first_math.pyi
├── shared_types.pyi
├── second_math.pyi
└── box_ops.pyi
The entry imports module leaves in source order. Native source order and native
link order remain build-plan facts; the .pyi package records the Python API
and native module topology.
For a BLAS/LAPACK-style folder containing only standalone procedures, generated
output stays compact. Even when the native implementation remains split across
several source files, explicit --pyi --out contracts emits one entry
contract:
contracts/
└── __init__.pyi # @external dgesv, @external dgetrf, @external dgetrs
The entry is still the sole wrapper input. The native build plan remains
separate: each original Fortran source may compile to its own object, or the
procedures may come from one archive or shared library. This compact generated
shape applies only to standalone @external procedures. If a bundle also
contains native modules, those modules still generate one flat module leaf per
native module and the entry imports those leaves.
For legacy BLAS/LAPACK-style assumed-size arrays such as DX(*), generated
contracts use Flat:
from prik.contracts import Addr, Flat, Float64, Int32, external
@external
def DAXPY(
N: Addr(Int32),
DA: Addr(Float64),
DX: Float64[Flat],
INCX: Addr(Int32),
DY: Float64[Flat],
INCY: Addr(Int32),
) -> None: ...Float64[Flat] maps to rank-one assumed-size storage such as real :: a(*).
The Python argument may be any contiguous NumPy array with rank 1 through 15;
the wrapper passes the contiguous element sequence as a rank-one native view.
Float64[3, Flat] maps to real :: a(3, *), and
Float64[3, 4, Flat] maps to real :: a(3, 4, *). Those multidimensional
forms are flat-edge contracts: the wrapper validates the fixed prefix axes, then
collapses all remaining contiguous Python axes into the final native assumed-size
extent. Float64[:, Flat] follows the same rule but reads the prefix extent
from the Python actual. Because real :: a(:, *) is not a legal Fortran
assumed-size declaration, source-generated contracts use declared prefix
extents such as Float64[n, Flat]; edited contracts may use : when the Python
actual should provide that prefix extent.
Semantic contracts do not map to native artifacts by filename. prik must never
assume that name.pyi is implemented by name.o:
- one entry
.pyimay require several objects and libraries; - several imported
.pyifiles may be implemented by one object or archive; - one shared library may implement an entire BLAS/LAPACK contract bundle; and
- module files, objects, archives, shared libraries, and transitive libraries may come from different directories or build systems.
Native inputs form one extension-level link plan. The generated bridge creates
native symbol uses from the immutable .pyi binding metadata, and the linker
resolves those symbols from caller-supplied artifacts. The .pyi filename is
never used to guess an object, archive, or shared-library name.
Build results expose that plan as WrapperBuildResult.native_build_plan, not
as a flattened string list. sources records the semantic entry contract and
its recursively imported .pyi files. The native plan separately records
compiled native source units, produced objects, prebuilt objects/archives/shared
libraries, module/include directories, library directories, and ordered
link_items. Link items can represent object, archive, shared_library,
named_library, and linker_argument entries, so the model can preserve order
without pretending every item is the same kind of input.
The current .pyi build subset accepts direct artifact paths through
--native-objects:
--native-objects build/module1.o build/module2.o \
/opt/vendor/lib/libsupport.a \
/opt/vendor/lib/libsolver.soNamed libraries use linker-style names and directories:
--native-library lapack blas \
--native-library-dir /opt/vendor/libThis requests -llapack -lblas, adds the directory to the link search path, and
adds the supported runtime search path for the produced extension. A direct
shared-library path and a named -l library are alternate ways to identify a
shared dependency; neither is inferred from .pyi.
Fortran module procedures additionally need their compiler-produced .mod
files while the generated bridge is compiled:
-I build/modArchives do not normally contain .mod files, so module directories remain
separate inputs. Standalone @external procedures require no .mod file because
the semantic contract supplies either their implicit external declaration or
their required explicit interface.
Required link cases are:
| Case | Native inputs |
|---|---|
| One contract, one object | one .o plus module directory when applicable |
| One contract, several dependencies | ordered objects/archives/shared libraries and named libraries |
| Imported contracts, separate objects | all required .o files in dependency-safe link order |
| Imported contracts, one archive | one .a; no contract-to-member mapping is inferred |
| Vendor shared implementation | direct .so path or --native-library NAME plus search directory |
| Mixed implementation | objects, archives, direct shared libraries, and named libraries in one ordered plan |
| Module procedures | native artifacts plus every required .mod search directory |
| Standalone procedures | native artifacts only; declaration mode comes from the completed @external contract |
Static link order is semantically significant: dependent objects precede the archives or libraries that satisfy them, and dependent libraries precede their providers. Cyclic static archives may require linker grouping or repeated archives. The completed build interface must preserve caller order across all native item kinds and provide an explicit ordered linker-argument mechanism for groups, whole-archive policy, and platform-specific flags. The maintained GNU/Linux runtime evidence covers mixed objects, archives, direct shared libraries, named libraries, transitive providers, and explicit archive groups for cyclic dependencies. These cases prove that the ordered plan is preserved; they do not imply that every linker accepts the same platform-specific control arguments.
Directly linked objects and static archives must be position-independent when
the platform requires PIC. All artifacts must match the active compiler ABI,
architecture, Fortran kind/layout assumptions, and name-mangling convention.
Missing symbols, duplicate strong definitions, incompatible files, unavailable
dependent shared libraries, and missing .mod files must produce actionable
build or import diagnostics rather than triggering a source fallback.
For multi-file contract projects, one entry file defines the export contract. Native module boundaries remain preserved by default:
from . import module1 as module1
from . import module2 as module2With extension name library, this exposes
library.module1.update and library.module2.update. Identically named members
in different native modules do not collide.
Aliases change only the Python export tree. They never change native placement:
from . import module1 as solver
from .module2 import update as update_secondThis exposes library.solver and library.update_second, not
library.module1 or library.update. The bridge still imports native module
module1 and still calls native procedure module2.update.
An alias creates another public Python binding to the same native declaration;
it does not promise Python object identity between exported names. For module
variables, every exported name routes to the same native storage, so writes
through one name are visible through the others, but each read may return a new
Python object. For functions, each exported name calls the same native
procedure; introspection such as __name__ and repr() may report the public
alias name.
Standalone procedures are explicitly re-exported at the extension root:
from .externals.dgesv import dgesv as dgesv
from .externals.dgetrf import dgetrf as dgetrfThis exposes library.dgesv and library.dgetrf. Duplicate root names are an
error unless the root contract resolves them through an explicit alias or hides
one declaration.
Users may replace the generated export policy without changing leaf native contracts. Selective aliasing is unambiguous:
from .module1 import update as update_first
from .module2 import update as update_secondExplicit wildcard imports request flattening:
from .module1 import *
from .module2 import *Wildcard import order must not silently resolve collisions. If both modules
export update, semantic validation requires explicit aliases or exclusions.
Every .pyi wrapper build takes exactly one entry contract. A module leaf may
itself be the entry; in that case its declarations appear at the extension root.
A multi-module project uses an entry containing relative imports so each native
module remains a distinct child namespace unless explicitly re-exported.
An arbitrary root file is allowed and uses normal stub import syntax without a
.pyi suffix:
# api.pyi
from .module1 import *
from .module2 import *The entry filename chooses the compiled extension and shared-library name by
default. For __init__.pyi, the resolved containing directory name is used;
calling prik as either foo/__init__.pyi or __init__.pyi from inside foo/
therefore selects foo. Wrapper --out NAME
overrides that inference and controls the extension filename,
PyInit_<name> symbol, and Python import name.
Target CLI shapes are:
python3 -m prik contracts/library/__init__.pyi \
--out library \
--native-objects native.apython3 -m prik api.pyi \
--out library \
--native-library native \
--native-library-dir /path/to/libsFor a single standalone fragment, no __init__.pyi is required:
python3 -m prik dgesv.pyi \
--out lapack_dgesv \
--native-objects dgesv.oThese commands treat native artifacts as link inputs only. They do not permit fallback parsing of unavailable Fortran source. The entry recursively resolves its relative imports; imported contracts must not also appear as positional arguments.
prik parses the entry as a restricted semantic stub; it does not execute Python
code. Every relative import is resolved recursively to a sibling .pyi or a
package __init__.pyi, producing the complete transitive contract graph before
wrapper planning or code generation. Files that both declare native objects and import
other contracts contribute both roles.
The resolver preserves normal explicit export choices:
from . import m1 as m2
from .m1 import func as f
from .m1 import *The first form creates child namespace m2, the second exports only f, and
the third explicitly flattens all public names. Repeating the same export is
idempotent, and the same declaration may be exported under its original name and
one or more aliases when each export is requested explicitly. These aliases
share the same native target or storage, but is identity between Python
attributes is not part of the contract. Missing relative imports,
relative-import cycles, and conflicting exports fail before code generation and
identify the participating contract paths.
For wrapper builds, the entry export policy also defines the generated Python extension binding surface. Declarations in imported leaf files that are not reachable from that policy do not get standalone public wrapper bindings; they remain native contract facts only when an exported declaration depends on them.
Absolute support imports such as from prik.contracts import prototype or
from types import SimpleNamespace may support annotation parsing, but they are
not contract graph edges and never create runtime exports. Generated references
to declarations in another contract package file use relative imports.
Source-driven wrapping applies the same export construction internally. A
source foo.f90 containing module m1 therefore exposes foo.m1, while
standalone procedures remain directly below foo; source and generated-contract
builds must not disagree about namespace placement.
| Family | Names |
|---|---|
| Booleans and generic values | Bool, Any |
| Signed integers | Int, Int8, Int16, Int32, Int64 |
| Unsigned integers | UInt8, UInt16, UInt32, UInt64, SizeT |
| Reals | Float32, Float64, Float128 |
| Complex | Complex64, Complex128, Complex256 |
| Text | String |
| User types | class names and imported type names |
| Named callable prototypes | @prototype function declarations referenced by name |
| Prototype primitive reference | Addr(T) inside a @prototype declaration |
| Prototype non-primitive value override | Value(T) inside a @prototype declaration |
Semantic .pyi annotations describe two related but separate boundaries:
- the Python boundary: what the caller passes to the generated wrapper;
- the native boundary: how prik lowers that value into the native call.
Some types have one normal native representation. Array storage, scalar storage,
and scalar character values are always lowered as storage addresses. Bare numeric
scalars have two native representations: value by default, or address of
call-local storage when @native_call([Addr(Arg(i))]) asks for that projection.
Use @native_call only when the native argument order, hidden outputs, inserted
arguments, or scalar by-address projection differs from the default lowering.
| Contract | Python boundary | Default native boundary |
|---|---|---|
Float64 |
np.float64(...) |
scalar value |
@native_call([Addr(Arg(i))]) with Float64 |
np.float64(...) |
address of prik's call-local native scalar slot |
Float64[()] |
rank-zero NumPy array with dtype np.float64 |
storage address |
Float64[n], Float64[:], Float64[:, :] |
NumPy array storage | data address |
String[n] |
Python str whose encoded length is exactly n |
address of prik's call-local fixed-width character storage |
String[n][:], String[:][:] |
NumPy bytes array storage | character array descriptor/data contract |
String[n][()] |
rank-zero NumPy bytes array with dtype S<n> |
fixed-width character storage copied back into the NumPy array when native code mutates it |
Addr(Float64), Addr(Float64[n]), Addr(String[n]) |
integer raw address such as array.ctypes.data or a ctypes buffer address |
that raw address |
WrappedType |
generated wrapper instance | wrapped object's native handle/address |
Arg(i) in @native_call means "use argument i's default native
representation." For arrays, scalar storage, strings, and raw-address arguments,
that representation is already address/storage based. Addr(Arg(i)) is reserved
for bare numeric scalar values that would otherwise be passed by value.
Return(...) entries always name hidden writable native output storage. The
wrapper passes that storage by address because a native output argument cannot
be written by value; Addr(Return(...)) is redundant and invalid. Do not use
Return(...) for optional native outputs when the caller must control
present(...); keep the output visible instead, using T[()] for scalar
storage and visible optional array storage for arrays.
Bare scalar types are direct values:
from prik.contracts import Float64
def dot(a: Float64, b: Float64) -> Float64: ...T[()] represents rank-zero NumPy storage. For arguments, the caller passes an
addressable rank-0 NumPy array with the declared dtype; prik validates the
object and uses its data storage for the native call. For direct or projected
results, prik returns a rank-0 NumPy array instead of collapsing the contract to
a scalar value:
from prik.contracts import Float64, Int32
def update_storage(value: Float64[()]) -> None: ...
def inspect_storage(value: Int32[()]) -> None: ...
def current_storage() -> Float64[()]: ...The caller writes:
value = np.array(3.0, dtype=np.float64)
update_storage(value)Array annotations such as T[n], T[:], and T[:, :] represent
caller-provided NumPy storage. Character arrays use the same second-axis shape
rule: String[8][:] is an array of fixed-length strings, while String[:][:]
is an array whose element length is not fixed in the public contract. The native
boundary is always the array data address; @native_call([Addr(Arg(i))]) is
redundant for these arguments.
Array dimensions in the public type are bridge extents, not native lower and
upper bounds. For example, a native dimension 0:LDB-1 has public extent
LDB, so an assumed-size contract is written T[LDB, Flat]. The bridge may
construct its local view with any lower bound while passing the same base
address and extent; the compiled native procedure applies its own declared
bounds. Native lower bounds, upper bounds, and source-dimension spellings are
not part of the semantic .pyi format.
String[n] represents a Python str at the Python boundary. Its encoded byte
length must be exactly n; prik does not pad or truncate the public value. prik
converts it to call-local fixed-width character storage and passes that storage
address to native code. If a returned Returns["name", String[n]] item is
present, native mutation is copied back into a replacement Python str;
otherwise the mutation is discarded.
String[n][()] represents caller-provided mutable scalar character storage. The
caller passes a rank-zero NumPy fixed-width bytes array:
from prik.contracts import String
def rewrite_label(label: String[8][()]) -> None: ...
label = np.array("abcdefgh", dtype="S8")
rewrite_label(label)The public object is NumPy bytes storage, so reads such as label[()] produce a
bytes value. The dtype itemsize must match n; Python Unicode arrays and object
arrays are rejected.
Type-level Addr(T) represents an integer raw address supplied by the Python
caller. It is valid only for a primitive scalar pointee, a fixed-length
String[n], or an array whose rank and every extent are resolved by literals
or visible scalar arguments. It is an advanced unsafe contract: prik casts the
address according to the declared pointee type, but it cannot prove the address
lifetime, true dtype, alignment, length, or ownership. The pointer value itself
does not carry string length or array shape. Integer zero becomes a null
pointer without a conversion error, negative integers are forwarded through
the platform pointer conversion, and an out-of-range integer raises
OverflowError. prik likewise resolves raw-array extent expressions without a
positivity check. Calling native code with an invalid address or pointee shape
is the caller's responsibility and may crash the process:
from prik.contracts import Addr, Float64, Int32, String
def update_raw(value: Addr(Float64)) -> None: ...
def inspect_raw(value: Addr(Int32)) -> None: ...
def raw_values(n: Int32, values: Addr(Float64[n])) -> None: ...
def raw_label(label: Addr(String[8])) -> None: ...The caller passes an address value, usually from NumPy:
value = np.array(3.0, dtype=np.float64)
update_raw(value.ctypes.data)Addr(WrappedType), Addr(String), and unresolved array forms such as
Addr(Float64[:]) are invalid Python-visible raw-address contracts. Wrapped
classes use WrappedType at the Python boundary, and their default Arg(i)
representation already supplies the wrapped native handle/address. Post-IR
policy completion rejects these invalid forms before wrapper planning or wrapper
lowering. Addr(...) remains a Python-visible raw-address contract and is not a
Fortran callback argument wrapper.
There is no type-level read-only wrapper. T[()], arrays, raw addresses, and
wrapped objects are storage boundaries. Source-language read/write facts may
guide policy while converting source, but generated and edited .pyi contracts
do not store argument direction. Projected replacement and output behavior is
expressed with Returns[...]; ownership and transfer behavior is expressed
with explicit policy metadata. When a .pyi contract is loaded directly,
ordinary visible array and raw address storage is treated as writable caller
storage. Pointer array storage remains in the supported input subset unless
projected output policy is added.
Pointer depth is explicit for low-level pointer graphs:
from prik.contracts import Addr, Int8, OpaqueHandle
handle: Addr[2](OpaqueHandle)
argv: Addr[3](Int8)Addr[1](T) is invalid; use Addr(T).
Array storage uses NumPy-style subscriptions:
Dimension entries have the following meaning:
| Form | Meaning |
|---|---|
: |
unconstrained extent for that axis |
n, 3, n + 1 |
required extent expression |
lower:upper |
range-like storage expression |
:: |
axis accepts runtime stride |
0:n: |
range plus stride-aware axis |
Flat |
edge-position flat contiguous storage dimension |
... |
rank-polymorphic storage |
Generated .pyi prints :: and bounded forms such as 0:n: for stride-aware
axes. Edited contracts may still use the explicit ::Strided and
0:n:Strided spellings; they load to the same semantic array contract.
Wrapper planning uses the structured array contract to distinguish layout dimensions
from extent expressions. Names such as Strided and Flat remain ordinary
symbols when they occur in native extent expressions. Called Fortran shape
intrinsics such as size(v) are recognized only after visible symbols are
resolved; the referenced value v must still be visible in the interface.
The Python argument may provide more storage than the declared explicit dimensions describe, but the wrapper passes it to native code without a stride descriptor. Non-contiguous arrays in the required native layout must be rejected or copied into a contiguous temporary.
Qualified names such as foo.bar are not accepted as dimension expressions.
Use local constants or generated Final[...] names for shape symbols.
Annotated[...] carries storage and call-boundary metadata. It does not carry
source-language argument direction or per-call value/reference selection. The
Fortran semantic pipeline supplies ORDER_F as the default multidimensional
layout. Generated contracts omit that default. Write explicit layout metadata
only when the Python-visible storage deliberately differs from that Fortran
representation, such as a row-major input accepted by a Fortran wrapper.
Native call transport belongs to @native_call: a wrapped derived
object uses its normal reference handoff with Arg(i) and exact typed value
handoff with Value(Arg(i)). The Python API accepts the same opaque wrapper
object in both cases; the generated Fortran bridge performs the typed call, and
the binding never exposes or guesses aggregate layout.
from prik.contracts import Annotated, COPY_F, Float64, ORDER_C
def fill(
a: Float64[:, :],
c_input: Annotated[Float64[:, :], ORDER_C, COPY_F],
out: Float64[()],
) -> None: ...Generated canonical metadata:
| Metadata | Meaning |
|---|---|
COPY_F |
accept the declared C-contiguous Python layout, create an F-contiguous temporary with the same logical axes, and copy back after visible native mutation |
PointerAssociation("runtime") |
pointer association is a runtime state rather than a declaration-time constant |
SourceName("native-name") |
source name cannot be represented directly as the Python target name |
Aliased |
native storage may be exposed across the Python boundary as an alias |
Immutable |
Python-visible value must not be mutated in place; this is a use-site boundary policy rather than an intrinsic datatype property, and writable native calls require a completed copy-in/copy-out replacement policy or an explicit call-local discarded-mutation policy |
Polymorphic |
an ordinary derived argument is a native polymorphic class(T) dummy; the passed-object dummy of a type-bound procedure omits this metadata because the binding already proves it |
| `Ownership("python" | "native" |
| `Transfer("copy_return" | "snapshot_copy" |
| `Destruction("python_refcount" | "wrapper_dealloc" |
PointerPolicy(...) |
complete pointer policy: nullable, transfer, target_owner, lifetime, deallocation, shape_source, contiguity, reassociation, aliasing, and mutability |
Loaded compatibility metadata:
| Metadata | Meaning |
|---|---|
Contiguous |
source provenance says the array is contiguous |
ArrayCategory("...") |
source array category provenance |
FortranAllocatable |
older scalar character allocatable metadata; generated contracts use Allocatable[String] |
Without COPY_F, ORDER_C is zero-copy and native Fortran observes the
reversed-axis storage view. With COPY_F, the binding performs both copy-in and
any required copy-out; the bridge receives an ordinary F-order buffer and does
not know that a representation conversion occurred. COPY_F is initially
limited to required, concrete-rank, dense numeric ndarray arguments. It does
not apply to Flat, assumed-rank or strided arrays, optional arrays, character
arrays, native descriptor arguments, or handle actuals.
Semantic .pyi types never repeat the native procedure's intent. Native
source conversion may use the source declaration once to propose default
Python argument/result positions. The emitted Python signature, Returns[...]
items, and ordered @native_call mapping are the editable, authoritative
contract; users may retain the native positions or choose a different Python
projection. Wrapper policy is completed from that contract and does not retain
or re-infer native intent.
Bridge entry dummies use the permissive omitted-intent default. The binding
may therefore use mutable call-local storage and perform completed copy-back
even when the native procedure has a more restrictive dummy. The compiled
native procedure's own explicit interface remains authoritative when the
bridge calls it. When an argument is projected with Returns, Python receives
the original C-order object after copy-back.
Persistent native descriptors use wrapper type syntax instead of descriptor
metadata inside Annotated[...]:
from prik.contracts import Allocatable, Float64, Int32, Pointer
scratch: Allocatable[Float64]
current: Pointer[Int32]
values: Allocatable[Float64[:]]
target: Pointer[Float64[:]]Allocatable[T] means a persistent native allocatable scalar descriptor and
Pointer[T] means a persistent native pointer scalar descriptor. Use those
type wrappers for module variables and derived-type fields.
Allocatable[T[...]] means a Python handle to a native allocatable array
descriptor. Pointer[T[...]] means a Python handle to native pointer
association state. Both are handles, not NumPy arrays. Extra metadata wraps the
handle, for example Annotated[Allocatable[Float64[:]], Aliased] or
Annotated[Pointer[Float64[:]], PointerAssociation("runtime")].
At runtime, the same annotation can create a present empty descriptor handle:
Allocatable[Float64[:]]() starts unallocated, while
Pointer[Float64[:]]() starts unassociated. The element annotation and array
rank are required. Ordinary array annotations such as Float64[:] and scalar
descriptor annotations such as Allocatable[Float64] are not constructors.
Allocatable[T[...]] | None and Pointer[T[...]] | None are valid only on
optional callable arguments, where None or omission maps to native
present(...) false. Module variables, derived-type fields, and function results
use the handle type without | None; unallocated or unassociated state lives
inside the present handle.
For this optional-descriptor ABI, the binding always supplies the bridge with a valid standard descriptor. For an omitted Python value it establishes a local unallocated or unassociated placeholder descriptor, while a separate completed presence action selects a native call that omits the native dummy. The bridge never forwards or inspects placeholder storage as a present native argument.
Procedure boundaries keep the Python value type in the annotation and put the
native descriptor conversion in @native_call. Both scalar descriptor kinds
are nullable: Python passes T | None, where None creates a present but
unallocated allocatable descriptor or a present but unassociated pointer
descriptor for the call. An unallocated or unassociated projected result returns
None.
from prik.contracts import Allocatable, Arg, Float64, Pointer, Return, Returns, native_call
@native_call(
[
Allocatable(Arg(0)),
Allocatable(Return("normalized", 0)),
Pointer(Return("selected", 2)),
],
)
def normalize(
value: Float64 | None,
) -> tuple[
Float64 | None,
Returns["value", Float64] | None,
Returns["selected", Float64] | None,
]: ...Allocatable(Arg(i)) and Pointer(Arg(i)) initialize an intent(in) or
intent(inout) descriptor from Python argument i. For intent(inout), a
matching Returns["name", T] | None item requests readback from the same native
dummy. Allocatable(Return("name", j)) and Pointer(Return("name", j))
create hidden intent(out) descriptor dummies projected into Python result slot
j. The result=... keyword describes the single native Fortran function
result; its nested Return(j) selects that result's position among all Python
results. Other Python results come from projected intent(out) and
intent(inout) dummies.
Use hidden descriptor outputs for nullable rank-zero results that must preserve
None: Allocatable(Return("name", j)) or Pointer(Return("name", j)).
Direct rank-zero allocatable function results are blocked because the bridge
cannot safely preserve the unallocated function-result state across supported
Fortran compilers.
Direct allocatable array function results use wrapper-owned descriptor handles
and preserve allocated, zero-sized, and unallocated state for rank one and
higher.
Descriptor projection uses calls, not type subscriptions. Write
Allocatable(Arg(0)), not Allocatable[Arg(0)]. This is distinct from
Addr(Arg(0)): a scalar allocatable or pointer is a native descriptor, not just
the address of a scalar slot.
Defaulted scalar descriptor arguments carry native optional-dummy absence:
@native_call([Allocatable(Arg(0))])
def update(value: Float64 | None = ...) -> None: ...The native optional-dummy behavior has three states: update() means native
present(value) is false, update(None) means the dummy is present with an
unallocated or unassociated descriptor, and update(1.0) means the dummy is
present with a value. This scalar rule is separate from array handles:
Allocatable[T[...]] | None and Pointer[T[...]] | None are only the optional
absent-handle form, while unallocated or unassociated array state remains inside
a present handle.
Reads of scalar descriptor variables or derived-type components are different
because they expose persistent native state. Attribute reads copy the current
scalar value into Python and return None when the allocatable is unallocated
or the pointer is unassociated. No scalar handle object is exposed, so these
values do not provide .to_numpy(), .view(), .get(), .value,
allocation, deallocation, nullification, or resize APIs.
Plain nullable Python projections are separate from native descriptors:
from prik.contracts import Float64
maybe_value: Float64 | NoneFloat64 | None does not imply a native allocatable or pointer descriptor.
For array descriptors, use Allocatable[T[...]] and Pointer[T[...]].
Annotated[T[...], Allocatable] and Annotated[T[...], Pointer] are not
active public descriptor spellings. Derived module objects remain live objects;
there is no public whole-object snapshot annotation.
Ownership metadata is consumed by the centralized wrapper ownership policy. These annotations are the editable contract for how a value crosses the Python boundary and who eventually releases the storage:
Ownership("...")says who owns the value or native storage.Transfer("...")says how that value crosses the Python/native boundary.Destruction("...")says where the owned storage is released.
Transfer(...) is intentionally the canonical spelling for borrowed views. A
borrowed view is one transfer mode among copies, call-local temporaries, in-place
mutation, wrapper-owned instances, and explicit blockers. Grouping them under
Transfer(...) keeps mutually exclusive boundary behaviors visible in the same
place instead of hiding them behind unrelated helper names.
These annotations are policy requests, not permission to skip validation. The backend still verifies that the requested owner, transfer mode, lifetime, shape, and destruction policy are implemented for the object kind and native context. Unsupported or contradictory policy must fail before bridge lowering instead of falling back to source-derived behavior.
Transfer modes:
| Transfer mode | Meaning | Usual destruction policy | Example |
|---|---|---|---|
Transfer("by_value") |
A scalar value crosses as a Python value; no shared native storage is exposed. | Destruction("python_refcount") for the returned Python object. |
def count() -> Annotated[Int32, Ownership("python"), Transfer("by_value"), Destruction("python_refcount")]: ... |
Transfer("call_local") |
The wrapper creates or associates storage only for one native call. Python does not receive persistent native storage. | Destruction("call_local") for bridge temporaries, or Destruction("none") when no generated storage is owned. |
def use_value(value: Annotated[Float64, Ownership("temporary"), Transfer("call_local"), Destruction("call_local")]) -> None: ... |
Transfer("in_place") |
Native code writes through caller-provided mutable Python storage. The same Python object observes the mutation. | Destruction("caller"); prik must not free caller storage. |
def scale(values: Annotated[Float64[:], Ownership("caller"), Transfer("in_place"), Destruction("caller")]) -> None: ... |
Transfer("copy_return") |
Native output is copied or read back into a fresh Python-visible return value. The original Python object is not mutated unless separately declared. | Destruction("python_refcount") after Python owns the copy. |
def read_values() -> Annotated[Float64[:], Ownership("python"), Transfer("copy_return"), Destruction("python_refcount")]: ... |
Transfer("snapshot_copy") |
Python receives a detached copy of current native state. Later native changes do not update it, and Python writes do not mutate native storage. This transfer name does not by itself make a returned NumPy array read-only. | Destruction("python_refcount") for the detached copy. |
def read_values() -> Annotated[Float64[:], Ownership("python"), Transfer("snapshot_copy"), Destruction("python_refcount")]: ... |
Transfer("borrowed_view") |
Python receives a no-copy view of storage owned somewhere else. Writes may mutate that storage when the value is mutable and the backend supports writable views. | Usually Destruction("native_owner") or Destruction("wrapper_dealloc"); Python does not free the borrowed target. |
module_values: Annotated[Allocatable[Float64[:]], Aliased, Ownership("native"), Transfer("borrowed_view"), Destruction("native_owner")] |
Transfer("wrapper_instance") |
Python receives a wrapper object that owns or controls a native instance. | Destruction("wrapper_dealloc"). |
def make_state() -> Annotated[state, Ownership("wrapper"), Transfer("wrapper_instance"), Destruction("wrapper_dealloc")]: ... |
Transfer("blocked") |
The contract intentionally has no safe lowering with the current policy facts. Wrapper generation must stop. | Destruction("blocked"). |
def reassociate(values: Annotated[Pointer[Float64[:]], Ownership("unknown"), Transfer("blocked"), Destruction("blocked")]) -> None: ... |
Destruction policies:
| Destruction policy | Where storage is released |
|---|---|
Destruction("python_refcount") |
Python, NumPy, or a generated base capsule releases the Python-owned copy when references are gone. |
Destruction("wrapper_dealloc") |
The generated wrapper deallocator releases the native instance or storage owned by that wrapper. |
Destruction("native_owner") |
Native module state or an external native owner releases the storage; Python only borrows it. |
Destruction("caller") |
The Python caller owns the object passed into the wrapper; prik may mutate it but must not destroy it. |
Destruction("call_local") |
The generated bridge releases the temporary before the wrapped call returns. |
Destruction("none") |
No persistent owned storage is created by prik for this boundary value. This is not a claim that no native storage exists. |
Destruction("blocked") |
Release ownership is unknown, contradictory, or unimplemented, so wrapper generation must stop. |
Contradictions are contract errors, not implementation choices. For example,
Immutable says the Python-visible value must not be mutated in place, while
Transfer("borrowed_view") says Python sees shared no-copy storage. Combining
them for a writable native argument is invalid because the wrapper cannot both
preserve immutability and expose a writable shared view. The user must choose one
contract: remove Immutable for in-place borrowed mutation, or keep Immutable
and request an explicit replacement return such as Returns["values", Float64[:]].
PointerPolicy is keyword-only and requires all ten keys. Its string values are
preserved verbatim so project-specific owner and release names can be expressed;
the backend still validates whether the requested transfer and destruction path
are implemented.
For native pointer-array handles, associate(other) and nullify() are always
available. allocate(shape) is permitted only when reassociation is
allocate, allocate_resize, reallocate, or reassociate_allocate.
deallocate() is permitted only when deallocation is
deallocate, deallocate_resize, owner_deallocate,
unsafe_deallocate, or wrapper_dealloc.
Use unsafe_deallocate only when the contract intentionally makes the caller
responsible for requesting deallocation without prik-proven target ownership.
resize(shape) is permitted only when the policy opts into resize through both
reassociation and deallocation, using resize, allocate_resize, or
deallocate_resize as appropriate. Other values keep those operations absent
from the completed handle policy.
Pointer-array extraction policy is selected from completed descriptor and
layout facts before wrapper lowering. A contiguous target selects
contiguous_view. A strided or otherwise general target selects
descriptor_view, which requires standard descriptor interop support. Copy-
oriented PointerPolicy(...) values may retain unrelated call/ownership
meaning, but they do not request a copied to_numpy() result. If those facts
cannot support a live view, extraction is unsupported rather than copied. When
generated descriptor interop supplies decoded descriptor fields as mappings or
field-record objects, the shared runtime can build live NumPy views for positive
or negative descriptor stride multipliers.
from prik.contracts import Annotated, Float64, Pointer, PointerPolicy
value: Annotated[
Pointer[Float64[:]],
PointerPolicy(
nullable=True,
transfer="call_local",
target_owner="module",
lifetime="module",
deallocation="never",
shape_source="pointer_bounds",
contiguity="contiguous",
reassociation="never",
aliasing="borrowed",
mutability="view",
),
]For module and derived-field pointer-array handles, a completed contiguous
policy enables contiguous_view extraction and checks the target's current
contiguity before reading it. General strided extraction still requires the
descriptor-view path. Pointer-array function results and nonoptional
intent(out) outputs use wrapper-owned standard pointer descriptors. Their
handles release descriptor storage on close() or finalization without
implicitly deallocating the target.
Derived module objects use the normal generated class in both plain and
Aliased declarations:
from prik.contracts import Aliased, Allocatable, Annotated, Float64
class box:
values: Allocatable[Float64[:]]
live_current: Annotated[box, Aliased]
plain_current: boxBoth reads return live native-owned objects. Aliased remains a
language-neutral addressability fact: it permits an address-backed borrow, but
does not make a native-array handle copy. A plain derived module declaration
uses typed module-specific bridge operations and must not be lowered by
fabricating a native address.
Final[T] is the only public constant spelling. Do not use
Annotated[T, Constant] or T[Constant].
Constants use Final[T]. Literal values are optional unless the value is needed
as a compile-time expression or enumerator initializer:
from prik.contracts import Final, Int32
nmax: Final[Int32]
answer: Final[Int32] = 42Fortran enumerators are plain integer constants. Do not declare or expect
Python Enum/IntEnum classes or semantic enum datatypes:
from prik.contracts import Final, Int
STATUS_OK: Final[Int] = 0
STATUS_RETRY: Final[Int] = STATUS_OK + 1The listed names are documentation and convenience constants. Procedure arguments and returns that use native enum types are emitted as the underlying integer type.
Fortran derived types and ordinary semantic classes use normal class syntax:
from prik.contracts import Float64, Int32
class particle:
id: Int32
position: Float64[3]from prik.contracts import CStruct, CUnion, Float64, Int32, Opaque, UInt32
class packet(CStruct):
tag: UInt32
class scalar(CUnion):
i: Int32
x: Float64
class context(CStruct, Opaque):
pass| Marker | Meaning |
|---|---|
Opaque |
type identity is known, but fields/layout are intentionally hidden |
from prik.contracts import Annotated, CAnonymous, CAnonymousMember, CStruct, CUnion, Float32, Int
class flags(CStruct):
class anonymous_union_0_type(CUnion, CAnonymous):
integer: Int
real: Float32
_anonymous_union_0: Annotated[anonymous_union_0_type, CAnonymousMember]
tag: IntExternal opaque types can live in separate owner stubs:
from prik.contracts import Opaque
# types_mod.pyi
class particle(Opaque):
pass
# physics.pyi
from types_mod import particle
def move(p: particle) -> None: ...If the owner stub is later edited to include fields, the import is reconciled as a wrapped external type without changing the importing file.
Generated Fortran stubs present the documented Python call while retaining the
exact native argument topology. An identity call needs no @native_call.
Whenever the Python signature hides, inserts, or reorders a native argument,
the generated declaration includes @native_call.
Edited contracts may also choose the identity native call directly. When every
native dummy argument remains visible in native order, output scalar dummies are
ordinary writable arguments instead of projected Python returns, and the
declaration does not need @native_call. Callers pass mutable storage, such as
a 0-D NumPy array with the declared dtype, for scalar output slots.
If a caller chooses identity form for a fixed-length String[n] argument while
passing an ordinary Python str, any native mutation is made to a temporary
native buffer and is not observable after a None return.
Fortran scalar dummy arguments are represented with explicit value, storage, or address contracts:
| Source dummy shape | Generated semantic form |
|---|---|
no value, read-only reference |
visible T plus @native_call([Addr(Arg(i))]) |
no value, output reference |
T[()] for identity storage, or a projected Returns["name", T] |
no value, writable reference |
T[()] for caller storage, or visible T plus projected Returns["name", T] |
value |
direct T |
| function result | direct return annotation |
Visible non-allocatable array output buffers are compact by default. They are
still passed to native code as writable arrays and projected with
Returns["name", T]; the native procedure owns the source-level
discard-initial-value semantics.
Loaded return forms:
from prik.contracts import Float64, Int32, Returns
def f() -> None: ...
def g(x: Float64) -> Float64: ...
def split(x: Float64) -> tuple[Float64, Int32]: ...
def projected(x: Float64) -> Returns["x", Float64]: ...
def maybe_projected(x: Float64) -> Returns["x", Float64] | None: ...Returns["name", T] records an output value associated with an argument name.
Use Returns["name", T] | None when that named output can be absent. Plain
tuple return components after the first are converted to generated output arguments.
When the name matches an existing Python-visible argument, the argument remains
an input and the return item represents replacement-style writable-reference
behavior for immutable public values such as Python str.
With an explicit @native_call, a matching Returns["name", T] item
automatically assigns that visible Arg(i) its Python result position; it does
not require a duplicate Return(...) entry. The first ordinary return item is
the native function result. A native output dummy with no visible Python
argument must instead appear explicitly as Return("name", position) in the
native argument list. The list itself is exhaustive: once @native_call is
present, every native dummy position must have exactly one entry in native
order; native arguments are never inferred from leftovers.
For bare numeric scalar values, Addr(Arg(i)) means prik first converts the
Python argument to its native scalar representation and then passes the address
of that native slot. It does not mean the user passed a reference.
from prik.contracts import Addr, Arg, Float64, Returns, native_call
@native_call([Addr(Arg(0))])
def read_ref(x: Float64) -> None: ...
@native_call([Addr(Arg(0))])
def update_value(x: Float64) -> Returns["x", Float64]: ...read_ref creates call-local native scalar storage initialized from x and
passes its address with no readback. update_value creates mutable native scalar
storage initialized from x, passes its address, then returns the updated value.
The caller writes x = update_value(x).
When an existing native signature has writable scalar-reference behavior but no projected replacement result, use scalar storage to expose an addressable Python object:
from prik.contracts import Float64
def legacy_update(x: Float64[()]) -> None: ...The wrapper uses the caller-supplied rank-zero storage, so native mutation is observable through that storage after the call.
Inside @native_call, Addr wraps a projection rather than a type.
Addr(Arg(i)) is valid only for a primitive scalar Python value whose native
parameter requires the address of call-local scalar storage. Do not use it for
T[()], arrays, strings, wrapped objects, or raw Addr(...) arguments. Their
default Arg(i) representation is already the native storage, handle, or raw
address representation. Address projections of Return(...) and Work(...)
are also rejected; native outputs and workspaces already name their storage.
Value(Arg(i)) is the inverse override for an exact rank-zero monomorphic
wrapped derived object. Plain Arg(i) passes that object by reference;
Value(Arg(i)) asks the typed bridge to pass the exact native object by value.
Primitive scalars already use value passing with plain Arg(i), while arrays,
strings, raw addresses, and descriptor handles keep their normal storage ABI and
do not accept Value(...). The foreign binding boundary still carries an opaque object
address: Value(...) records the native Fortran dummy contract, and the Fortran
compiler performs any required copy when the typed bridge makes the call. It
never asks the binding to pass aggregate bytes through the foreign ABI.
from prik.contracts import Returns, String
def string_inout(label: String[8]) -> Returns["label", String[8]]: ...The caller passes a Python str; prik creates fixed-width character storage,
passes its address, and returns a replacement string because the signature asks
for one.
For example, a native subroutine ordered as (a, status, b) with hidden scalar
status output is represented as:
from prik.contracts import Addr, Arg, Float64, Int32, Return, native_call
@native_call([Addr(Arg(0)), Return("status", 0), Addr(Arg(1))])
def solve(
a: Float64,
b: Float64,
) -> Int32: ...@native_call preserves native argument order. The return annotation preserves
Python result order and the hidden output's native name and type. A function
result is Python result slot zero; projected output arguments follow it in
native argument order. Each Return(...) entry is hidden writable storage passed
to the native procedure by address. The optional result= keyword records a
native scalar descriptor function result, for example
result=Allocatable(Return(0)) or result=Pointer(Return(0)).
For nullable rank-zero values, prefer a hidden output dummy:
Allocatable(Return("name", 0)) or Pointer(Return("name", 0)). Direct
rank-zero allocatable function results are rejected when the wrapper would need
to preserve an unallocated result as Python None.
Direct allocatable array function results use wrapper-owned descriptor handles
and are supported for matrices and higher-rank arrays.
For descriptor projections, Pointer(Arg(i)) without a matching
Returns["name", T] creates a permissive call-local pointer adapter and
discards any native reassociation after the call. Adding the matching projected
return requests association writeback instead; scalar-derived values then
require persistent pointer storage. This choice belongs to the Python contract,
not native intent.
The same native routine can be edited into an identity call without projection:
from prik.contracts import Addr, Float64, Int32
def solve(
a: Addr(Float64),
status: Int32[()],
b: Addr(Float64),
) -> None: ...This form exposes the native argument order directly. Python callers allocate
status as a rank-0 NumPy array and inspect it after the call; prik does not
synthesize a return value for that output slot. The raw Addr(Float64)
arguments require callers to pass addresses directly; use visible T
plus @native_call([Addr(Arg(i))]) when callers should pass ordinary scalar
values instead.
Class methods use the same stub form. An untyped leading self is allowed in a
method and is not treated as a native argument.
A declared module procedure may also be projected as an instance method.
Pass() places self in its native argument list. The module function and
method remain independent Python exports, even when they have the same name.
Matching names select the same native procedure without @bind. A different
Python method name requires @bind("native_name").
The prik semantic .pyi format uses @overload("specific_name") to link one
Python-visible declaration to an ordinary concrete procedure declaration. This
decorator is prik metadata; it is not typing.overload and must not be imported
from typing.
from prik.contracts import Addr, Arg, Float64, Int32, Pass, bind, native_call, overload, private
@private
@native_call([Addr(Arg(0))])
def convert_integer(value: Int32) -> Int32: ...
@private
@native_call([Addr(Arg(0))])
def convert_real(value: Float64) -> Float64: ...
@bind("convert")
@overload("convert_integer")
def convert(value: Int32) -> Int32: ...
@bind("convert")
@overload("convert_real")
def convert(value: Float64) -> Float64: ...
class accumulator:
@bind("add")
@overload("accumulator_add_integer")
def add(self, value: Int32) -> None: ...
@bind("add")
@overload("accumulator_add_real")
def add(self, value: Float64) -> None: ...Concrete specifics that remain in a stub are ordinary functions with their
native names. Ordinary source-private Fortran declarations are not emitted as
standalone generated .pyi items. A private overload specific may remain only
when it is needed to resolve a public overload declaration from the standalone
.pyi. @private is reserved for a user-imposed contract on a declaration
that is otherwise part of the wrapper input.
@native_call is not emitted merely to restate an unchanged native function
name.
An overload declaration is a Python dispatch link. It must not also carry
@native_call; the linked concrete procedure owns argument reordering,
Pass(), hidden values, and projected returns. An overload-level @bind
changes only the final native call target.
The same rule applies to class overloads. Their concrete link may describe a
private specific while @bind("public_generic") selects the callable
type-bound generic.
The loader resolves only the decorator string. It never guesses a target by signature. The target must exist exactly once, each target may occur only once in one overload set, and the public declaration must agree with the concrete call signature and return type. Missing, duplicate, ambiguous, and incompatible links are deterministic errors.
Without overload-level @bind, a module candidate calls the linked procedure's
resolved native name. With @bind, it calls the named native symbol instead.
This is required when a public generic is the only native entry point for a
private specific:
from prik.contracts import Int32, bind, overload, private
@private
def convert_integer(value: Int32) -> Int32: ...
@bind("convert")
@overload("convert_integer")
def convert_number(value: Int32) -> Int32: ...@private controls Python visibility only. For edited standalone contracts,
prik cannot infer whether the linked native procedure is accessible. A direct
call to a Fortran-private specific therefore fails during the native build.
Python method names recover the native generic for ordinary operators. When two distinct Fortran generics share one Python method, the decorator also carries the otherwise unrecoverable operator spelling:
from prik.contracts import Bool, overload
@overload("equivalent_values", generic="operator(.eqv.)")
def __eq__(self, other: value) -> Bool: ...For class methods, generic= is restricted to a compatible operator or
assignment generic. It is emitted for .eqv. and .neqv., which would
otherwise be indistinguishable from operator(==) and operator(/=).
Each specific keeps its declared Python call shape. The generated dispatcher
normalizes positional and keyword arguments against each candidate, then uses
the candidate's exact typed predicates. A call that matches no specific raises
TypeError; duplicate runtime dtype/rank/class signatures are a deterministic
generation error.
Defined operators use the same explicit link. The concrete function keeps its full Fortran operand list, while the class declaration describes the Python method call:
from prik.contracts import Addr, Arg, Float64, Pass, native_call, overload, private
@private
@native_call([Arg(0), Addr(Arg(1))])
def add_vector_real(left: vector, right: Float64) -> vector: ...
@private
@native_call([Addr(Arg(0)), Arg(1)])
def add_real_vector(left: Float64, right: vector) -> vector: ...
class vector:
@overload("add_vector_real")
def __add__(self, right: Float64) -> vector: ...
@overload("add_real_vector")
def __radd__(self, left: Float64) -> vector: ...Operand positions are fixed:
| Python method | Native operands |
|---|---|
| non-reflected binary method | self is operand 1; other is operand 2 |
| reflected binary method | other is operand 1; self is operand 2 |
| unary method | self is the only operand |
| comparison method | self is the Python left operand; reflected comparison metadata restores native order |
Mappings:
| Fortran generic | Python methods |
|---|---|
binary operator(+) |
__add__, __radd__ |
unary operator(+) |
__pos__ |
binary operator(-) |
__sub__, __rsub__ |
unary operator(-) |
__neg__ |
operator(*), operator(/), operator(**) |
__mul__/__rmul__, __truediv__/__rtruediv__, __pow__/__rpow__ |
operator(==), operator(/=) |
__eq__, __ne__ |
operator(<), operator(<=), operator(>), operator(>=) |
__lt__, __le__, __gt__, __ge__ with reflected comparison routing |
operator(.and.), operator(.or.), operator(.not.) |
__and__/__rand__, __or__/__ror__, __invert__ |
operator(.eqv.), operator(.neqv.) |
__eq__, __ne__ |
prik does not infer in-place methods such as __iadd__. Python's fallback
therefore applies: an expression such as value += other may replace the
Python reference with the ordinary operator result rather than invoking
Fortran defined assignment.
A named operator .custom. is exposed as operator_custom(self, other). If
the wrapped class is native operand 2, the method is
r_operator_custom(self, other). These are normal methods because Python has
no syntax or data-model slot for arbitrary Fortran operator names.
Python assignment cannot be intercepted. Fortran assignment(=) is exposed as
explicit mutation:
from prik.contracts import Addr, Arg, Float64, Pass, Returns, native_call, overload, private
@private
@native_call([Arg(0), Addr(Arg(1))])
def assign_vector_real(
left: vector,
right: Float64,
) -> Returns["left", vector]: ...
class vector:
@overload("assign_vector_real")
def assign(self, right: Float64) -> vector: ...lhs.assign(rhs) invokes native lhs = rhs, mutates the existing wrapped
object, preserves Python object identity, and returns the same object. Both
lhs.assign(rhs) and lhs = lhs.assign(rhs) are therefore valid. Assigning an
object to itself is a no-op that returns the existing object. A supported
specific must be a two-argument subroutine whose wrapped derived-type LHS is
writable and whose RHS is read-only. Unsafe or unsupported forms are wrapper-planning
blockers.
Supported Fortran allocatable module arrays and derived-type array fields are
exposed as Allocatable[T[...]] handles. The handle carries allocation state
and descriptor operations. It is not a NumPy array, and unallocated state lives
inside the handle.
h.to_numpy() returns None when the descriptor is unallocated. Otherwise it
returns a live NumPy view over the current native storage and never an automatic
detached copy. For derived-type fields, the extracted view retains the field
handle and the field handle retains the containing Python wrapper. For module
variables, the handle retains the generated module owner while the native
module controls allocation.
Existing views are not invalidated, detached, or tracked. If a wrapped Fortran procedure reallocates or deallocates native storage while Python still holds an old view, that old view is stale; reading or writing it is unsupported and may crash the process. Users who need independent lifetime must copy explicitly:
x = obj.values.to_numpy() # live view or None
y = None if x is None else x.copy()
obj.reset_values() # may invalidate x; y remains validDerived-type allocatable fields remain fields in .pyi:
from prik.contracts import Allocatable, Float64
class buffer:
values: Allocatable[Float64[:]]Python cannot directly replace or reallocate such fields. Assigning a new array
to the field raises AttributeError; explicit wrapped Fortran procedures must
perform allocation, reallocation, and deallocation.
Fortran classes with public rank-0 numeric, logical, or complex components emit a generated keyword-only constructor in generated stubs. Every constructor keyword is optional: omitted components keep the native allocation state, including any Fortran default component initializer.
from prik.contracts import Float64, Int32
class state:
def __init__(
self,
*,
id: Int32 = 7,
scale: Float64 = 2.5
) -> None: ...
id: Int32 = 7
scale: Float64 = 2.5If a generated class has fields but none can be constructor keywords, the stub
emits the self-only form def __init__(self) -> None: .... This declaration
keeps native default construction explicit in the editable contract; arrays,
allocatables, pointers, characters, and derived-type fields still do not become
constructor arguments.
An edited stub controls whether that generated constructor remains part of the
Python surface. If either generated __init__ form is removed, wrapper
generation must not recreate it. A class left without any __init__ has no
public Python constructor; native allocation remains an internal wrapper
operation only.
An edited stub may replace the generated field-keyword constructor with one
native initializer. @bind("native_name") selects the procedure. Exactly one
Pass() in @native_call(...) selects the newly allocated object. Its native
position is explicit, so other arguments of the same derived type remain
unambiguous. The selected native dummy must accept the constructed type. The
original module-level declaration may remain public or be marked @private.
It may also be removed when only construction should expose the initializer.
The __init__ declaration remains sufficient to generate the native call.
from prik.contracts import Addr, Arg, Float64, Int32, Pass, bind, native_call
class state:
@bind("init_state")
@native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))])
def __init__(
self,
seed: Int32,
scale: Float64 = ...
) -> None: ...
id: Int32 = 7
scale: Float64 = 2.5The generated keyword-only shape remains reserved: if undecorated __init__
keeps the self, *, ... form and every keyword has a default, the loader treats
it as the generated field constructor metadata. Constructor overload
declarations replace runtime field initialization with an exact constructor
overload set linked to concrete same-class targets. The direct wrapper allocates
one native owner, dispatches without candidate trial calls, and releases an
uncommitted owner on failure. Missing, incompatible, or indistinguishable
candidates are rejected before source emission.
A constructor overload may also route through a public type-bound generic:
class accumulator:
@bind("add")
@overload("accumulator_add_integer")
def __init__(self, value: Int32) -> None: ...The wrapper allocates self first. The overload link supplies the concrete
argument contract; @bind("add") supplies the native call target.
Module variables are declarations in the semantic contract. Allocatable array
module variables expose handles; unallocated state is represented by the handle,
not by making the module attribute None:
from prik.contracts import Aliased, Allocatable, Annotated, Float64
module_values: Annotated[Allocatable[Float64[:]], Aliased]
plain_values: Allocatable[Float64[:]]Both declarations expose a stable native-owned handle whose to_numpy() call
returns a current live view or None. Aliased does not select view versus
copy extraction. It remains a language-neutral fact that native storage may be
externally aliased or addressed. Fortran source declarations with target are
printed as Aliased because they supply that native fact.
Aliased also records addressability used by borrowed access to an existing
derived-type module object. Plain derived module objects remain live but use a
different bridge mechanism:
from prik.contracts import Aliased, Allocatable, Annotated, Float64
class box:
values: Allocatable[Float64[:]]
live_current: Annotated[box, Aliased]
plain_current: boxThe annotation belongs to the module variable, not to box. An prik-created
box() is addressable because its generated constructor allocates
pointer-backed native storage. A native module declaration is a different object
origin. Annotated[box, Aliased] lets the wrapper retain that object's native
address and return a live borrowed box wrapper. A plain box module variable
returns the same public wrapper type, backed by typed module-specific bridge
operations. Unsupported live lifetime or module-access policy stops wrapper planning;
the backend must not fall back to a detached object or an invented address.
Public scalar Fortran module variables are emitted directly with their resolved semantic type:
from prik.contracts import Int32, String
counter: Int32
label: String[8]Scalar allocatable and pointer module variables keep their descriptor spelling
in the contract while reads expose copied Python values or None:
from prik.contracts import Allocatable, Float64, Pointer
optional_scale: Allocatable[Float64]
selected_scale: Pointer[Float64]Wrapper generation may synthesize native getter and setter bridge functions to
implement Python attribute reads and writes. Those functions are internal: they
are absent from the .pyi and are not exported as Python-callable procedures.
The post-IR policy stage separately decides the getter result policy, native
setter assignment mode, and Python setter exposure before wrapper planning. A
native value-copy setter can therefore exist for ABI use while Python
replacement is explicitly rejected, as for allocatable or derived fields.
Bridge and binding generation only dispatch those completed accessor decisions.
A mutable scalar module variable may include a literal default in an edited
.pyi contract:
from prik.contracts import Int32
counter: Int32 = 41The default is an import-time native initializer, not a Final constant. When
the extension module is imported, prik applies the value through the completed
native setter policy. Later reads and writes still use the current native module
storage. This initializer form is only for scalar module variables with a
write-through setter; non-scalar or read-only declarations remain explicit
wrapper-planning or code-generation errors instead of falling back to a copied Python
value.
Fortran parameter declarations are emitted as Final[...] constants when
their literal value can be represented in .pyi:
from prik.contracts import Final, Int32
nmax: Final[Int32] = 12If a Fortran parameter initializer is an expression, generated .pyi emits a
default only after prik has resolved that expression to a literal. Unresolved
native expressions are kept out of the active .pyi default:
real, parameter :: c = cos(0.0)from prik.contracts import Final, Float32
c: Final[Float32]The source expression may remain available as native provenance metadata, but it does not become an executable Python default unless a literal value is known.
No setter is generated for parameters. Python module namespaces remain ordinary
Python module namespaces, so assigning to mod.nmax can rebind that Python name
without modifying native Fortran state.
A derived-type parameter remains a Final[DerivedType] constant rather than
mutable module storage. When its fields have a complete value-copy policy, the
wrapper materializes a wrapper-owned copy from the native parameter and exposes
no native setter. Mutating that Python wrapper cannot modify the Fortran
parameter. Derived constants whose fields cannot be copied safely fail policy
completion explicitly instead of being treated as Aliased module variables.
Supported top-level allocatable writable descriptor arguments use
Allocatable[T[...]] handle policy. | None on the annotation means the handle
object itself may be absent for an optional native dummy. That spelling is only
valid on optional callable arguments. Passing a handle to a normal T[...]
argument is separate: the wrapper validates that the handle is allocated and
hands off the native array actual, without implicitly calling h.to_numpy().
Fortran pointer array facts are emitted and loaded with Pointer[T[...]]
handles:
from prik.contracts import Float64, Int32, Pointer
def sum_values(values: Pointer[Float64[:]]) -> Float64: ...
def choose_values(flag: Int32) -> Pointer[Float64[:]]: ...The handle carries association state and descriptor operations:
p.associatedreports whether the pointer currently has a target.p.to_numpy()returns the current target view orNone.p.associate(other)copiesother's association without copying data.p.nullify()is the default descriptor operation.allocate(shape),deallocate(), andresize(shape)are exposed only when completed pointer policy allows them.
Direct pointer-array function results and nonoptional pointer-array
intent(out) outputs return owned PointerArray descriptor handles. Optional
outputs remain visible to preserve native absence, and intent(inout) remains
visible because its incoming association is part of the call.
When descriptor-backed extraction is enabled, to_numpy() builds NumPy shape and
strides from descriptor metadata and can expose strided pointer targets. If that
path is unavailable, the completed policy must choose a contiguous live view or
an explicit wrapper-planning diagnostic. It must not fall back to a copy. Pointer
handle ownership is descriptor or association access by default, not target
ownership.
The generated descriptor-view path establishes portable descriptor storage,
associates an intent(out) pointer dummy with the live target, and decodes the
descriptor synchronously. It does not inspect a compiler-private Fortran
descriptor layout.
@private marks classes, functions and methods private:
from prik.contracts import Int32, private
@private
def helper(x: Int32) -> None: ...private[T] marks a variable or argument private:
from prik.contracts import Float64, Int32, private
hidden_value: private[Float64]
def consume(value: private[Int32]) -> None: ...Generated .pyi files omit ordinary declarations that are private in the
original Fortran source. Privacy written in an edited .pyi is different: it
is a user contract applied to a declaration that was otherwise available to the
wrapper, so the declaration remains printed and loadable as wrapper input.
Names that are not valid Python identifiers are represented with var[...] for
data declarations, or with Annotated[..., SourceName("native-name")] for callable
arguments:
from prik.contracts import Annotated, Int32, SourceName
var["class"]: Int32
def f(class_: Annotated[Int32, SourceName("class")]) -> None: ...@native_call is loaded and printed whenever the Python-visible signature
differs from the exact native signature, whether the projection was generated
from native projected-output behavior or written by the user:
from prik.contracts import Arg, Float64, Return, native_call
@native_call([Arg(0), Arg(0).shape[0], Return("result", 0)])
def normalize(values: Float64[:]) -> Float64: ...Scalar address inputs use a Python-visible value type and an explicit native address projection:
from prik.contracts import Addr, Arg, Int32, native_call
@native_call([Addr(Arg(0))])
def add_one(value: Int32) -> Int32: ...Loaded projection entries:
| Entry | Meaning |
|---|---|
Arg(i) |
native argument is Python argument i's default native representation |
Addr(Arg(i)) |
native argument is the address of Python argument i's call-local native scalar representation |
Value(Arg(i)) |
exact rank-zero monomorphic wrapped derived object is passed to the native value dummy by the typed bridge |
Allocatable(Arg(i)), Pointer(Arg(i)) |
native argument is a nullable call-local scalar descriptor initialized from Python argument i; None means present but unallocated or unassociated |
Return(i) |
native argument is supplied by projected return slot i as hidden writable storage passed by address |
Return("name", i) |
named native argument is supplied by projected return slot i as hidden writable storage passed by address |
Allocatable(Return(...)), Pointer(Return(...)) |
native output dummy is a nullable scalar descriptor copied to the selected Python result slot |
Pass() |
implicit class instance: a method receiver or newly allocated constructor object |
Int32(1), Float64(0.5), Bool(False), String[1]("N") |
hidden native literal with an explicit ABI type |
Len(Arg(i)), Len(Return(i)), Len(Work("name")) |
hidden native length metadata |
Arg(i).shape[d], Return(i).shape[d], Work("name").shape[d] |
hidden native shape metadata |
IsPresent(Arg(i)) |
hidden native optional-presence metadata |
Work("name") |
hidden workspace value |
Hidden native literals must be typed call expressions inside @native_call.
native_call accepts one native-argument list and, when required, one
result=Allocatable(Return(i)) or result=Pointer(Return(i)) keyword. The
result= mapping describes the native function result and is not another native
dummy argument.
For nullable rank-zero outputs, use Allocatable(Return("name", i)) or
Pointer(Return("name", i)) in the native-argument list instead of a direct
allocatable function result.
Allocatable array function results can be direct returns or hidden output
dummies; both forms preserve unallocated handle state.
Bare literals such as 1 or "N" are rejected because they do not declare the
native ABI type. Fixed-length string literals must include their length, for
example String[1]("N"); plain String("N") is not enough.
Generated hidden-output mappings and existing backend-supported projection entries are lowered into runtime calls. General allocation, coercion, validation, and ownership transformations remain unsupported unless the relevant backend explicitly implements them.
Generated .pyi currently covers these exact-contract areas:
| Area | Generated behavior |
|---|---|
| Fortran intrinsic scalars | compiler-aware semantic dtype names |
| Native scope | module-leaf filename, or @external for standalone procedures |
| Functions/subroutines | declaration return shape, optional native rename, ABI argument order, and direct result |
| Hidden Fortran outputs | Python returns plus generated @native_call in native argument order |
| Scalar address inputs | Python-visible T plus Addr(Arg(...)) native-call projection |
| Writable scalar storage | T[()], or visible T plus projected replacement Returns["name", T] |
| Arrays | shaped storage with extents and strided axes; multidimensional order defaults from the selected native language |
| Module variables | direct module-level annotations; native accessors remain internal |
| Native array descriptor handles | Allocatable[T[...]] and Pointer[T[...]] handles for module variables, supported fields, and descriptor arguments; owned allocatable result handles; unallocated or unassociated state remains inside the handle |
| Constants | Final[T] module variables |
| Fortran derived types | classes with fields and methods; @native_type only for irreducible attributes or finalizers |
| Fortran defined operators | Python data-model methods plus explicit named-operator methods |
| Fortran defined assignment | explicit mutating assign(...) overloads |
| Opaque types | Opaque classes and owner-module dependency stubs |
| Imports | retained contract dependencies with aliases; source kind modules are omitted after dtype resolution |
| Callbacks | named @prototype declarations when source interfaces resolve |
Loaded but usually not generated from source today:
| Area | Loaded behavior |
|---|---|
Addr[n](T) for n > 1 |
direct low-level pointer topology |
ORDER_ANY |
edited orientation-independent array contract |
additional @native_call and Returns[...] edits |
projection metadata beyond generated output mappings |
| source-provenance array helpers | compatibility loading for older or edited stubs |
The parser or post-IR policy-completion stage rejects contracts that would be ambiguous, unsafe, or stale before wrapper lowering:
Unknownsemantic types.ConstantorShapeasAnnotatedmetadata.- non-dimensional subscriptions such as
Float64[ORDER_F]. Addr[1](T).- callable
Addr[n](T)forn > 1; deeper pointer topology remains limited to low-level data declarations. - callable
Addr(WrappedType),Addr(String), or raw arrays with unresolved extents. Addr(Arg(i))for anything except a primitive scalar value, and allAddr(Return(i))orAddr(Work("name"))projections.- untyped callable parameters.
- positional-only, keyword-only, vararg or kwarg function parameters, except for the generated derived-type constructor shape.
- nested enum declarations.
- ordinary function bodies instead of
.... - unsupported decorators other than
@private,@bind,@external,@native_call,@native_type,@overload("specific"), the class-operatorgeneric=form,@raises,@nogil, and@staticmethod. - bare
@overloadortyping.overload; overload links require one concrete procedure name. @overload(...)combined with@native_call(...); the linked concrete procedure owns native projection metadata.
Ordered @native_call lowering, typed hidden outputs, array validation, and
completed ownership/action dispatch are implemented parts of the current
Fortran wrapper path. They are not future roadmap items.
Remaining public gaps include broader polymorphic class(...) representation,
pointer-return lifetime cases that still lack a provable owner, and clean
IDE/type-checker stubs that do not lose the semantic wrapper contract. The
language feature matrix is the
authoritative current support summary.