@@ -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
2626itself choose copy, replacement, borrowed-view, or owned-handle behavior. The
@@ -75,7 +75,17 @@ NumPy array when they need independent lifetime.
7575A borrowed view is a NumPy array that points at storage Python does not own.
7676Mutating the view mutates the owner. Deallocating or reallocating the owner can
7777make 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:
259269python3 -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
265275import sys
@@ -270,29 +280,29 @@ import allocations
270280
271281api = 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
282291api.allocate_shared(np.int32(3 ))
283- view = api.shared_values
292+ shared = api.shared_values
293+ view = shared.to_numpy()
284294view[0 ] = np.float64(10.0 )
285295assert api.shared_sum() == np.float64(15.0 )
286296
287297api.allocate_snapshot(np.int32(3 ))
288- snapshot = api.snapshot_values
298+ snapshot = api.snapshot_values.to_numpy()
289299np.testing.assert_array_equal(snapshot, np.array([3.0 , 6.0 , 9.0 ], dtype = np.float64))
290300assert not snapshot.flags.writeable
291301
292302api.scale_snapshot(np.float64(2.0 ))
293303np.testing.assert_array_equal(snapshot, np.array([3.0 , 6.0 , 9.0 ], dtype = np.float64))
294304np.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
333339The 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
341347module character_names
342348 implicit none
349+ character(len=:), allocatable :: stored_names(:)
343350contains
344351 subroutine replace_names(names)
345352 character(len=:), allocatable, intent(inout) :: names(:)
@@ -368,6 +375,8 @@ the native allocation at runtime:
368375``` python
369376from x2py.contracts import Allocatable, Returns, String
370377
378+ stored_names: Allocatable[String[:][:]]
379+
371380def replace_names (
372381 names : Allocatable[String[:][:]]
373382) -> Returns[
@@ -381,31 +390,26 @@ Build the example:
381390python3 -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
387397import sys
388- import numpy as np
389-
390398sys.path.insert(0 , " build/character_allocatables" )
391399import character_allocatables
392400
393401api = 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
404408The ` 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.
425429A supported allocatable component belongs to its containing native derived-type
426430instance. 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
432436Neither owner model can invalidate an already-created NumPy object safely after
433437native 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
461465A borrowed view can become stale after its native owner reallocates or
462466deallocates storage, so copy any data that needs an independent lifetime.
0 commit comments