| title | Allocatables |
|---|---|
| description | How prik handles Fortran `allocatable` variables, arrays, and descriptors |
| audience | users, advanced users |
| prerequisites | arrays |
| related | arrays.md, pointers.md, memory-management.md |
| status | maintained |
| publication | reviewed |
A Fortran allocatable descriptor records whether storage is allocated and, for arrays, its address, shape, and strides. The descriptor controls the allocation, and an prik handle gives Python access to that descriptor.
- Scalar allocatables appear as
T | None; array allocatables useAllocatable[T[...]]handles. - An array handle exposes allocation state and descriptor operations; it is not itself a NumPy array.
allocatedreports whether storage exists;to_numpy()returns a live view of that storage.- Reallocation or deallocation invalidates existing views.
- Module and derived-field handles expose storage that belongs to their module or parent object. Returned and caller-created handles have their own descriptor storage.
- When available,
deallocate()releases the current allocation but keeps the handle open.close()permanently ends a returned or caller-created handle.
Use Allocatable[T[...]] when the native callable needs the allocatable
descriptor and may inspect or change its allocation state:
from prik.contracts import Allocatable, Float64, Int32
values: Allocatable[Float64[:]]
def resize(values: Allocatable[Float64[:]], n: Int32) -> None: ...Use ordinary T[...] when the callable needs only array data:
def sum_values(values: Float64[:]) -> Float64: ...A plain NumPy array cannot satisfy an Allocatable[T[...]] parameter because
it does not carry native allocation state. Use to_numpy() when Python needs
the current array data held by an allocatable handle.
Allocatable[T[...]] is the type annotation. At runtime, generated Python APIs
use an AllocatableArray. You can also create an unallocated handle when a
routine needs a present descriptor that it will allocate:
import prik.contracts as xc
values = xc.Allocatable[xc.Float64[:]]()
assert values.allocated is False
api.fill_values(values)
assert values.allocated is TrueThe annotation supplies the element dtype and rank. The handle creates its
native descriptor storage when first passed to a matching writable argument.
It stays the same Python object after the call.
Allocatable[Float64]() is not supported because scalar allocatables cross the
Python boundary as values rather than array handles.
A returned or attribute array handle remains present even when its descriptor
is unallocated. Reading the Python attribute
returns an Allocatable[T[...]] handle, not ndarray | None.
A NumPy view reflects current native storage. Access it only while the allocation is present:
h = api.some_allocatable
if h.allocated:
view = h.to_numpy() # live view
view[0] = 42.0
else:
print("Not allocated")| Member | Type | Behavior |
|---|---|---|
allocated |
bool |
Whether native storage is currently allocated. |
shape |
tuple[int, ...] | None |
Current dimensions, or None when unallocated. |
dtype |
numpy.dtype |
Declared array element type. |
rank |
int |
Declared number of dimensions. |
to_numpy() |
numpy.ndarray | None |
A live view of current storage, or None when unallocated. It never creates an automatic detached snapshot. |
deallocate() |
() -> None |
Deallocates current storage when this operation is available for the handle. |
resize(shape) |
(int | Sequence[int]) -> None |
Allocates or resizes storage to shape when this operation is available for the handle. |
close() |
() -> None |
Permanently releases a returned or caller-created descriptor and any remaining allocation. It does nothing on a module or field handle. |
closed |
bool |
Whether a closable handle has been closed. |
Calling deallocate() or resize(shape) when the operation is unavailable
raises NotImplementedError.
| Operation | What it releases | Handle afterward |
|---|---|---|
deallocate() |
The current array allocation. | Open and usable, with allocated == False. |
close() |
This handle's descriptor and any allocation it still contains. | Permanently closed and unusable. |
Returned and caller-created handles close automatically when Python no longer
uses them. Call close() explicitly only when immediate release matters, such
as after using a large allocation.
Calling close() on a module or field handle does nothing: it leaves the
handle and the module's or parent object's storage unchanged. deallocate()
changes that allocation when the operation is available.
A module handle observes the live module allocatable descriptor. A derived-field handle retains its parent wrapper and observes the live allocatable component inside it. Native allocation changes are visible through the same Python handle:
h = api.values
assert not h.allocated
api.allocate_values(3)
assert h.allocated
assert h.shape == (3,)
api.resize_values(5)
assert h.shape == (5,)An allocatable-array function result becomes an AllocatableArray with its own
descriptor storage, which prik releases automatically:
values = api.make_values(3)
print(values.to_numpy())A direct allocatable-array function result is expected to be allocated. Use a zero-sized allocation to represent an empty result. If the native function may instead return an unallocated result, declare that possibility explicitly:
from prik.contracts import Allocatable, Annotated, Float64, Int32, MaybeUnallocated
def make_values(n: Int32) -> Allocatable[Float64[:]]: ...
def maybe_values(
n: Int32,
) -> Annotated[Allocatable[Float64[:]], MaybeUnallocated]: ...The second function still returns a present handle. That handle may have
allocated == False, in which case to_numpy() returns None. Returning an
unallocated direct result without MaybeUnallocated violates the wrapper
contract.
A nonoptional allocatable-array intent(out) does not consume incoming
allocation state, so it is hidden and returned as a new handle. A hidden
output may remain unallocated.
An optional intent(out) remains visible so omission preserves native
present(...) behavior. An intent(inout) descriptor also remains visible
because native code reads and changes its current allocation. When the semantic
contract projects that argument as a result, Python receives the same handle
object:
values = api.make_values(2)
returned = api.replace_values(values)
assert returned is values
print(values.to_numpy())Create storage.f90:
module storage
implicit none
real(8), allocatable :: values(:)
contains
function make_values(n) result(arr)
integer(4), intent(in) :: n
integer(4) :: i
real(8), allocatable :: arr(:)
allocate(arr(max(n, 0)))
if (n > 0) then
arr = [(real(i, 8)*2, i = 1, n)]
end if
end function make_values
subroutine replace_values(arr)
real(8), allocatable, intent(inout) :: arr(:)
if (allocated(arr)) deallocate(arr)
allocate(arr(2))
arr = [10.0_8, 20.0_8]
end subroutine replace_values
end module storageBuild it:
python3 -m prik storage.f90 --out-dir build/storageUse the generated module:
import sys
import numpy as np
sys.path.insert(0, "build/storage")
from storage.storage import make_values, replace_values
values = make_values(np.int32(3))
print(values.to_numpy()) # [2. 4. 6.]
returned = replace_values(values)
assert returned is values # same handle
print(values.to_numpy()) # [10. 20.]Keep these rules in mind when allocation state can change.
view = h.to_numpy()
view[0] = 1.0 # NOT OK: view may be Noneif h.allocated:
view = h.to_numpy()
if view is not None:
view[0] = 1.0This check is required whenever an allocatable may be unallocated, including a
result declared with MaybeUnallocated.
view = h.to_numpy()
saved = None if view is None else view.copy()
h.resize(8)After resize(), deallocate(), or a native call that may reallocate the
descriptor, discard view and call to_numpy() again. The independent
saved copy remains safe.
view = result.to_numpy()
result.close()
result.shape # NOT OK: the descriptor has been released
view[0] # NOT OK: close() released the allocationA view normally retains its handle, but an explicit close() releases a
returned allocatable result immediately. Finish using or copy all views before
closing the handle.
h.deallocate() # may be unavailable for module or field storageNot every module or field handle lets Python resize or deallocate its storage.
An unavailable operation raises NotImplementedError. Use the module's or
parent object's functions to change that storage instead.
Scalar allocatables appear as T | None values at the Python boundary rather
than AllocatableArray handles. An unallocated projected scalar result becomes
None. Scalar values do not expose persistent allocation state, to_numpy(),
or descriptor operations.
For an optional scalar allocatable argument, omission makes the argument absent.
Passing None makes it present but unallocated, while passing a value makes it
present with that value. See Optional Arguments.
- Continue with Pointers for association and target lifetime.
- Then read Memory Management for the rules shared by both kinds of handle.