Skip to content

Commit 97c4387

Browse files
committed
fully implement Pointer and Allocatable and update the docs
1 parent 57af600 commit 97c4387

49 files changed

Lines changed: 8414 additions & 1077 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/maintainer/roadmap/native-array-handle-checklist.md‎

Lines changed: 314 additions & 88 deletions
Large diffs are not rendered by default.

‎docs/user/guide/allocatables.md‎

Lines changed: 63 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ descriptor, not a NumPy array.
2020
| Allocatable descriptor argument | `Allocatable[T[...]]` handle | the handle passes the native allocatable descriptor |
2121
| Module allocatable array | `Allocatable[T[...]]` handle | the Fortran module owns allocation and release |
2222
| Derived allocatable field | `Allocatable[T[...]]` handle | the containing generated wrapper owns the native instance |
23-
| Owned allocatable result | `Allocatable[T[...]]` handle when stable owner storage is available | x2py-owned storage releases the native allocation with the handle |
23+
| Owned allocatable result | `Allocatable[T[...]]` handle | x2py-owned descriptor storage releases the native allocation with the handle |
2424

2525
`Allocatable` is the dynamic-storage fact shared by all rows. It does not by
2626
itself choose copy, replacement, borrowed-view, or owned-handle behavior. The
@@ -75,7 +75,17 @@ NumPy array when they need independent lifetime.
7575
A borrowed view is a NumPy array that points at storage Python does not own.
7676
Mutating the view mutates the owner. Deallocating or reallocating the owner can
7777
make existing views stale, so copy the view when Python needs an independent
78-
lifetime.
78+
lifetime. Each fresh extraction starts at the array's current native lower
79+
bounds; changing lower bounds during native reallocation must not offset the
80+
first element exposed to NumPy.
81+
82+
An allocatable array returned by a function or hidden output is different from
83+
a borrowed module or field handle. x2py transfers the result into persistent
84+
descriptor storage owned by the returned handle. The handle remains
85+
usable after the native call returns, and `close()` or finalization releases the
86+
native allocation. A NumPy view extracted from that handle retains the handle;
87+
as with every live view, explicitly closing or resizing the handle makes older
88+
views stale.
7989

8090
## Scalar Allocatable Projections
8191

@@ -259,7 +269,7 @@ Build it:
259269
python3 -m x2py allocations.f90 --out-dir build/allocations
260270
```
261271

262-
Then exercise copy, replacement, and borrowed-view behavior:
272+
Then exercise owned-result, descriptor-argument, and module-handle behavior:
263273

264274
```python
265275
import sys
@@ -270,29 +280,29 @@ import allocations
270280

271281
api = allocations.storage
272282

273-
copy = api.make_values(np.int32(3))
274-
np.testing.assert_array_equal(copy, np.array([2.0, 4.0, 6.0], dtype=np.float64))
275-
assert api.make_values(np.int32(0)) is None
283+
values = api.make_values(np.int32(3))
284+
np.testing.assert_array_equal(values.to_numpy(), np.array([2.0, 4.0, 6.0], dtype=np.float64))
285+
assert api.make_values(np.int32(0)).allocated is False
276286

277-
original = np.array([1.0, 2.0], dtype=np.float64)
278-
replacement = api.replace_values(original)
279-
np.testing.assert_array_equal(original, np.array([1.0, 2.0], dtype=np.float64))
280-
np.testing.assert_array_equal(replacement, np.array([10.0, 20.0], dtype=np.float64))
287+
returned = api.replace_values(values)
288+
assert returned is values
289+
np.testing.assert_array_equal(values.to_numpy(), np.array([10.0, 20.0], dtype=np.float64))
281290

282291
api.allocate_shared(np.int32(3))
283-
view = api.shared_values
292+
shared = api.shared_values
293+
view = shared.to_numpy()
284294
view[0] = np.float64(10.0)
285295
assert api.shared_sum() == np.float64(15.0)
286296

287297
api.allocate_snapshot(np.int32(3))
288-
snapshot = api.snapshot_values
298+
snapshot = api.snapshot_values.to_numpy()
289299
np.testing.assert_array_equal(snapshot, np.array([3.0, 6.0, 9.0], dtype=np.float64))
290300
assert not snapshot.flags.writeable
291301

292302
api.scale_snapshot(np.float64(2.0))
293303
np.testing.assert_array_equal(snapshot, np.array([3.0, 6.0, 9.0], dtype=np.float64))
294304
np.testing.assert_array_equal(
295-
api.snapshot_values,
305+
api.snapshot_values.to_numpy(),
296306
np.array([6.0, 12.0, 18.0], dtype=np.float64),
297307
)
298308
```
@@ -302,32 +312,28 @@ the previous borrowed view stale.
302312

303313
## Output And Function Results
304314

305-
Allocated top-level results and non-optional hidden allocatable outputs use
306-
copy-return: x2py copies the native allocation into a new Python-owned NumPy
307-
array and then releases the native temporary. Unallocated storage becomes
308-
`None`, while allocated zero-sized storage remains a zero-sized array.
309-
Optional allocatable outputs remain visible so the caller can omit them and make
310-
native `present(...)` false.
315+
Allocated top-level results and non-optional hidden allocatable outputs return
316+
wrapper-owned `AllocatableHandle` objects. The generated binding transfers the
317+
result into persistent descriptor storage; the handle releases that storage on
318+
`close()` or finalization. Unallocated storage is represented by a present
319+
handle whose `allocated` property is false and whose `to_numpy()` result is
320+
`None`. Optional allocatable outputs remain visible so the caller can omit them
321+
and make native `present(...)` false.
311322

312-
Changing the returned NumPy array does not mutate later native results or module
313-
state.
323+
A NumPy view returned by `to_numpy()` retains its handle owner. Changing that
324+
view changes the handle's current allocation, but does not affect later,
325+
independent result handles.
314326

315327
## Inout Replacement
316328

317-
An allocatable `intent(inout)` argument is annotated with `| None` because
318-
`None` means initially unallocated native storage. When the value is an exact
319-
matching NumPy array, x2py copies that value into temporary native allocatable
320-
storage. The Python argument remains Python-owned and is not mutated. The NumPy
321-
array is only an initializer for a bridge-owned native allocatable temporary;
322-
Python is not passing that NumPy buffer as native allocatable storage.
323-
324-
After the call, x2py copies the final native allocation into a new Python-owned
325-
return value, or returns `None` if native storage is unallocated. This is
326-
replacement behavior, not ordinary in-place array mutation. Assign the return
327-
value:
329+
An allocatable `intent(inout)` descriptor argument accepts an
330+
`AllocatableHandle`, not a plain NumPy array. A matching `Returns[...]`
331+
projection records that the same caller handle is the Python result. Policy
332+
completion marks that descriptor boundary read-write before lowering; generated
333+
binding code does not manufacture a replacement ndarray or a second handle.
328334

329335
```python
330-
values = api.replace_values(values)
336+
assert api.replace_values(values) is values
331337
```
332338

333339
The source for this call is already shown in the complete example above.
@@ -340,6 +346,7 @@ Allocatable character arrays use fixed-width NumPy bytes storage. Create
340346
```fortran
341347
module character_names
342348
implicit none
349+
character(len=:), allocatable :: stored_names(:)
343350
contains
344351
subroutine replace_names(names)
345352
character(len=:), allocatable, intent(inout) :: names(:)
@@ -368,6 +375,8 @@ the native allocation at runtime:
368375
```python
369376
from x2py.contracts import Allocatable, Returns, String
370377

378+
stored_names: Allocatable[String[:][:]]
379+
371380
def replace_names(
372381
names: Allocatable[String[:][:]]
373382
) -> Returns[
@@ -381,31 +390,26 @@ Build the example:
381390
python3 -m x2py character_allocatables.f90 --out-dir build/character_allocatables
382391
```
383392

384-
Pass a NumPy bytes array and assign the returned replacement:
393+
Pass an existing compatible allocatable character handle. The projected result
394+
is that same handle, and extraction remains explicit:
385395

386396
```python
387397
import sys
388-
import numpy as np
389-
390398
sys.path.insert(0, "build/character_allocatables")
391399
import character_allocatables
392400

393401
api = character_allocatables.character_names
394-
original = np.array([b"aa", b"bbb"], dtype="S3")
395-
replacement = api.replace_names(original)
396-
397-
assert original.dtype == np.dtype("S3")
398-
assert original.tolist() == [b"aa", b"bbb"]
399-
assert replacement.dtype == np.dtype("S5")
400-
assert replacement.tolist() == [b"red ", b"blue "]
401-
assert replacement is not original
402+
names = api.stored_names
403+
assert api.replace_names(names) is names
404+
assert names.to_numpy().dtype.itemsize == 5
405+
assert names.to_numpy().tolist() == [b"red ", b"blue "]
402406
```
403407

404408
The `S5` itemsize comes from `allocate(character(len=5) :: names(count))`.
405-
x2py copies the final native allocation into the returned Python-owned array
406-
and releases the native temporary. Python inputs must use NumPy bytes dtype
407-
`S`; Unicode (`U`) and object (`O`) arrays are rejected. When the Fortran
408-
element length is fixed, the input dtype itemsize must match that length.
409+
Plain NumPy arrays are not allocatable descriptors and are rejected for this
410+
handle-typed parameter. When extracting character storage, x2py uses NumPy
411+
bytes dtype `S`; Unicode (`U`) and object (`O`) arrays are not descriptor-handle
412+
substitutes.
409413

410414
## Module Handles And Views
411415

@@ -425,9 +429,9 @@ readiness blocks with a clear diagnostic.
425429
A supported allocatable component belongs to its containing native derived-type
426430
instance. The generated wrapper owns that native instance. The field exposes an
427431
`Allocatable[T[...]]` handle that retains the parent wrapper. Any borrowed NumPy
428-
view produced by `to_numpy()` uses the wrapper object as `view.base`, which
429-
keeps the owner alive. Assigning a replacement array directly to such a field is
430-
rejected when native reallocation must go through an explicit method.
432+
view produced by `to_numpy()` retains the field handle, and the field handle
433+
retains the parent wrapper. Assigning a replacement array directly to such a
434+
field is rejected when native reallocation must go through an explicit method.
431435

432436
Neither owner model can invalidate an already-created NumPy object safely after
433437
native reallocation. Copy before any operation that may reallocate or
@@ -443,20 +447,20 @@ independent = view.copy()
443447
- Mutable scalar deferred-length character storage is blocked.
444448
- Borrowed views require a proved native or wrapper owner and `Aliased`
445449
storage when the owner is a module variable.
446-
- An edited `.pyi` cannot relabel a native-owned allocation as Python-owned
447-
without choosing an implemented owner-storage or copy-return path.
450+
- An edited `.pyi` cannot relabel a native-owned descriptor as Python-owned.
451+
Use an implemented owned-result handle or copy an extracted NumPy value when
452+
Python needs independent storage.
448453
- `Annotated[T[...], Allocatable]` is no longer the active public spelling for
449454
allocatable array descriptors; use `Allocatable[T[...]]`.
450455

451456
## Evidence And Troubleshooting
452457

453-
Results, module views, component views, `None`, and owner retention are exercised
454-
by
458+
Owned results, module and component handles, unallocated state, extraction, and
459+
owner retention are exercised by
455460
[`test_allocatable_views.py`](../../../tests/wrapper/fortran/module_state/test_allocatable_views.py).
456-
Replacement behavior and invalid dtype/rank calls are exercised by
457-
[`test_allocatable_replacement.py`](../../../tests/wrapper/fortran/module_state/test_allocatable_replacement.py).
458-
Character array replacement and generated-`.pyi` builds are exercised by
459-
[`test_character_edge_cases.py`](../../../tests/wrapper/fortran/strings/test_character_edge_cases.py).
461+
Character descriptor generation in source and generated-`.pyi` modes is
462+
exercised by
463+
[`test_character_arguments.py`](../../../tests/wrapper/fortran/strings/test_character_arguments.py).
460464

461465
A borrowed view can become stale after its native owner reallocates or
462466
deallocates storage, so copy any data that needs an independent lifetime.

‎docs/user/guide/arrays.md‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ status: maintained
88

99
# Arrays
1010

11-
Numeric Fortran arrays cross the Python boundary as NumPy arrays. The semantic
12-
contract records element dtype, rank, known extents, layout, allowed strides,
13-
mutability, and storage category. The wrapper validates these facts before the
14-
native call and does not silently repair an incompatible array.
11+
Ordinary numeric Fortran arrays cross the Python boundary as NumPy arrays.
12+
Native allocatable and pointer array descriptors instead cross as
13+
`Allocatable[T[...]]` and `Pointer[T[...]]` handles. In both cases, the
14+
semantic contract records element dtype, rank, known extents, layout, allowed
15+
strides, mutability, and storage category. The wrapper validates these facts
16+
before the native call and does not silently repair an incompatible value.
1517

1618
## Complete Array Example
1719

@@ -145,14 +147,17 @@ movement do not by themselves make the layout invalid.
145147
- Input arrays remain caller-owned and may be read-only.
146148
- Ordinary output arrays remain visible; the caller allocates writable storage.
147149
- Inout arrays remain visible and mutate in place.
148-
- Array function results and non-optional hidden allocatable outputs are
149-
Python-owned copies.
150+
- Ordinary array function results are Python-owned NumPy copies.
151+
- Non-optional hidden allocatable outputs are wrapper-owned
152+
`Allocatable[T[...]]` handles. Unallocated state remains inside the present
153+
handle.
150154
- Optional allocatable outputs remain visible so the caller controls native
151155
`present(...)`.
152156
- Pointer-array handle results remain blocked until owner storage, target
153157
lifetime, descriptor extraction, and destroy behavior are implemented.
154-
- Borrowed allocatable module views are native-owned; borrowed component views
155-
are owned through the containing wrapper. Both require lifetime care.
158+
- Allocatable and pointer module variables and supported components are handle
159+
objects. NumPy views are obtained explicitly with `to_numpy()` and require
160+
lifetime care after native descriptor changes.
156161

157162
Caller-provided output storage is demonstrated with complete source in
158163
[Wrapping Subroutines](wrapping-subroutines.md#complete-output-example).
@@ -177,19 +182,26 @@ its own runtime rank. Rank-zero values and ranks above 15 are rejected.
177182

178183
## Array Results
179184

180-
Supported numeric and fixed-width character array results preserve dtype, rank,
181-
and Fortran-oriented multidimensional data. Character arrays use NumPy bytes
182-
dtypes such as `S5`, where the dtype itemsize is the Fortran element length.
183-
Allocated zero-sized results are arrays; unallocated allocatable or
184-
unassociated pointer results are `None`.
185+
Supported ordinary numeric and fixed-width character array results preserve
186+
dtype, rank, and Fortran-oriented multidimensional data as NumPy arrays.
187+
Character arrays use NumPy bytes dtypes such as `S5`, where the dtype itemsize
188+
is the Fortran element length. Ordinary zero-sized results remain zero-sized
189+
arrays.
190+
191+
An allocatable array result instead returns an `Allocatable[T[...]]` handle.
192+
An allocated zero-sized result is a handle whose shape contains a zero extent;
193+
an unallocated result is a present handle with `allocated is False` and
194+
`to_numpy() is None`. Pointer-array results remain blocked until their owner
195+
and target lifetime can be represented safely.
185196

186197
## Unsupported Forms
187198

188199
- assumed type `type(*)`;
189200
- character arrays that cannot be represented as fixed-width NumPy bytes
190201
storage;
191202
- arrays of derived types;
192-
- general borrowed pointer array views and reassociation; and
203+
- pointer-array results and reassociation without completed owner, lifetime,
204+
and operation policy; and
193205
- any kind or rank whose portable NumPy storage contract cannot be proved.
194206

195207
## Evidence And Troubleshooting

0 commit comments

Comments
 (0)