Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions examples/issue306_allocatable_realloc/ANALYSIS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Issue #306 Analysis: Module-level allocatable arrays fail after reallocation

## Root Cause

The issue is a **mismatch between `sizeof_fortran_t` at wrapper generation time vs runtime**.

### How it happens

1. When f90wrap generates Fortran wrappers, it hardcodes `dummy_this(N)` where N is `sizeof_fortran_t` computed on the **generation** system
2. At runtime, f90wrap computes `sizeof_fortran_t` again and creates `empty_handle = [0]*sizeof_fortran_t`
3. If these values differ, f2py throws: "0-th dimension must be fixed to X but got Y"

### Reporter's Environment

Error message: "0-th dimension must be fixed to 2 but got 4"

This indicates:
- GCC 15.2.1 produces `sizeof_fortran_t = 2`
- The wrappers were generated on a system with `sizeof_fortran_t = 4`

### Reproduction

On a system with `sizeof_fortran_t = 4`, passing a size-2 handle reproduces the error:

```python
>>> import _alloc_mod
>>> _alloc_mod.f90wrap_alloc_mod__array__data_array([0, 0]) # size 2 handle
ValueError: 0-th dimension must be fixed to 4 but got 2
```

### Why GCC 15?

`sizeof_fortran_t` is computed in `f90wrap/sizeoffortran.f90` by measuring the size of a Fortran derived type pointer using `transfer()`. GCC 15 may have changed the internal representation of type/class pointers, reducing their size from 4 integers to 2.

### Key Observation

For **module-level** arrays, `dummy_this` is actually **unused**! The compiler warns:

```
Warning: Unused dummy argument 'dummy_this' at (1) [-Wunused-dummy-argument]
```

Looking at the generated Fortran wrapper:
```fortran
subroutine f90wrap_alloc_mod__array__data_array(dummy_this, nd, dtype, dshape, dloc)
integer, intent(in) :: dummy_this(4) ! UNUSED - never referenced
! ... dummy_this is never used in the body
end subroutine
```

## Proposed Fix

For module-level array accessors, we should either:

1. **Remove `dummy_this` entirely** - since it's unused for modules
2. **Make the size dynamic** - use assumed-size array `dummy_this(*)`

Option 1 is cleaner but requires changes to both:
- `f90wrap/f90wrapgen.py` - Fortran wrapper generation
- `f90wrap/pywrapgen.py` - Python wrapper generation

Option 2 is simpler but may have f2py compatibility issues.

## Files to Modify

- `f90wrap/f90wrapgen.py` - generates the Fortran `f90wrap_*__array__*` subroutines
- `f90wrap/pywrapgen.py` - generates the Python property that calls these subroutines

## Test Case

The test case in this directory (`alloc_mod.f90`, `tests.py`) passes on systems where `sizeof_fortran_t` matches between generation and runtime (GCC 13/14). It would fail on GCC 15 due to the size mismatch.

## Questions for Reporter

1. Confirm `sizeof_fortran_t` value:
```python
from f90wrap.sizeof_fortran_t import sizeof_fortran_t
print(sizeof_fortran_t())
```

2. How was f90wrap installed? (PyPI wheels vs source build)
32 changes: 32 additions & 0 deletions examples/issue306_allocatable_realloc/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#=======================================================================
# define the compiler names
#=======================================================================
include ../make.inc

PY_MOD = alloc_mod
F90_SRC = alloc_mod.f90
OBJ = $(F90_SRC:.f90=.o)
F90WRAP_SRC = $(addprefix f90wrap_,${F90_SRC})
WRAPFLAGS = -v
F2PY = f2py-f90wrap
.PHONY: all clean

all: test

clean:
rm -rf *.mod *.smod *.o f90wrap*.f90 ${PY_MOD}.py _${PY_MOD}*.so __pycache__/ .f2py_f2cmap build ${PY_MOD}/

alloc_mod.o: ${F90_SRC}
${F90} ${F90FLAGS} -c $< -o $@

%.o: %.f90
${F90} ${F90FLAGS} -c $< -o $@

${F90WRAP_SRC}: ${OBJ}
${F90WRAP} -m ${PY_MOD} ${WRAPFLAGS} ${F90_SRC}

f2py: ${F90WRAP_SRC}
CFLAGS="${CFLAGS}" ${F2PY} -c -m _${PY_MOD} ${F2PYFLAGS} f90wrap_*.f90 *.o

test: f2py
${PYTHON} tests.py
6 changes: 6 additions & 0 deletions examples/issue306_allocatable_realloc/Makefile.meson
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
include ../make.meson.inc

NAME := alloc_mod

test: build
$(PYTHON) tests.py
34 changes: 34 additions & 0 deletions examples/issue306_allocatable_realloc/alloc_mod.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module alloc_mod
implicit none

! Module-level allocatable array
real, allocatable, dimension(:,:) :: data_array

contains

subroutine allocate_array(n, m)
integer, intent(in) :: n, m

if (allocated(data_array)) deallocate(data_array)
allocate(data_array(n, m))
data_array = 0.0
end subroutine allocate_array

subroutine fill_array(val)
real, intent(in) :: val

if (allocated(data_array)) then
data_array = val
end if
end subroutine fill_array

subroutine reallocate_array(n, m)
! Reallocate with different dimensions
integer, intent(in) :: n, m

if (allocated(data_array)) deallocate(data_array)
allocate(data_array(n, m))
data_array = 0.0
end subroutine reallocate_array

end module alloc_mod
79 changes: 79 additions & 0 deletions examples/issue306_allocatable_realloc/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Test for issue #306: Module-level allocatable arrays fail after reallocation

This test verifies that module-level allocatable arrays can be:
1. Allocated and accessed from Python
2. Reallocated with different dimensions and still accessed
"""
import numpy as np
# Import the module instance (lowercase), not the class
from alloc_mod import alloc_mod

def test_allocate_and_access():
"""Test basic allocation and access."""
print("Test 1: Basic allocation and access")

# Allocate 3x4 array
alloc_mod.allocate_array(3, 4)
alloc_mod.fill_array(1.5)

# Access the array
arr = alloc_mod.data_array
print(f" Array shape: {arr.shape}")
print(f" Array values: {arr[0, 0]}")

assert arr.shape == (3, 4), f"Expected (3, 4), got {arr.shape}"
assert np.allclose(arr, 1.5), f"Expected all 1.5, got {arr}"
print(" PASS")

def test_reallocate_different_size():
"""Test reallocation with different dimensions - this is the issue."""
print("\nTest 2: Reallocation with different dimensions")

# First allocation: 2x2
alloc_mod.allocate_array(2, 2)
alloc_mod.fill_array(1.0)
arr1 = alloc_mod.data_array
print(f" Initial shape: {arr1.shape}")
assert arr1.shape == (2, 2), f"Expected (2, 2), got {arr1.shape}"

# Reallocate to 4x4 (different size!)
alloc_mod.reallocate_array(4, 4)
alloc_mod.fill_array(2.0)

# This is where issue #306 occurs - accessing the array after reallocation
try:
arr2 = alloc_mod.data_array
print(f" After reallocation shape: {arr2.shape}")
assert arr2.shape == (4, 4), f"Expected (4, 4), got {arr2.shape}"
assert np.allclose(arr2, 2.0), f"Expected all 2.0, got {arr2}"
print(" PASS")
except ValueError as e:
print(f" FAIL: {e}")
raise

def test_multiple_reallocations():
"""Test multiple reallocations."""
print("\nTest 3: Multiple reallocations")

sizes = [(2, 3), (5, 5), (1, 10), (3, 3)]

for i, (n, m) in enumerate(sizes):
alloc_mod.reallocate_array(n, m)
alloc_mod.fill_array(float(i))
arr = alloc_mod.data_array
print(f" Iteration {i}: shape = {arr.shape}")
assert arr.shape == (n, m), f"Expected ({n}, {m}), got {arr.shape}"

print(" PASS")

if __name__ == "__main__":
print("Testing issue #306: Module-level allocatable arrays")
print("=" * 60)

test_allocate_and_access()
test_reallocate_different_size()
test_multiple_reallocations()

print("\n" + "=" * 60)
print("All tests passed!")
Loading