From 375821970d9b47d0e76b2b612d4dbb2dff2c7260 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Dec 2025 11:35:19 +0000 Subject: [PATCH 1/2] Add test case for issue #306: module-level allocatable arrays This test case demonstrates the reported issue where module-level allocatable arrays may fail after reallocation. Investigation findings: - Tests pass on this system (sizeof_fortran_t = 4) - The generated wrapper uses `dummy_this(4)` but this parameter is unused for module-level arrays (only for derived types) - Issue may be platform-specific when sizeof_fortran_t differs between wrapper generation time and runtime Test files: - alloc_mod.f90: Fortran module with allocatable array - tests.py: Tests for allocation and reallocation - Makefile/Makefile.meson: Build files Related to #306 --- .../issue306_allocatable_realloc/Makefile | 32 ++++++++ .../Makefile.meson | 6 ++ .../alloc_mod.f90 | 34 ++++++++ .../issue306_allocatable_realloc/tests.py | 79 +++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 examples/issue306_allocatable_realloc/Makefile create mode 100644 examples/issue306_allocatable_realloc/Makefile.meson create mode 100644 examples/issue306_allocatable_realloc/alloc_mod.f90 create mode 100644 examples/issue306_allocatable_realloc/tests.py diff --git a/examples/issue306_allocatable_realloc/Makefile b/examples/issue306_allocatable_realloc/Makefile new file mode 100644 index 00000000..5dcf7fb1 --- /dev/null +++ b/examples/issue306_allocatable_realloc/Makefile @@ -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 diff --git a/examples/issue306_allocatable_realloc/Makefile.meson b/examples/issue306_allocatable_realloc/Makefile.meson new file mode 100644 index 00000000..1160c28d --- /dev/null +++ b/examples/issue306_allocatable_realloc/Makefile.meson @@ -0,0 +1,6 @@ +include ../make.meson.inc + +NAME := alloc_mod + +test: build + $(PYTHON) tests.py diff --git a/examples/issue306_allocatable_realloc/alloc_mod.f90 b/examples/issue306_allocatable_realloc/alloc_mod.f90 new file mode 100644 index 00000000..fa4b95e2 --- /dev/null +++ b/examples/issue306_allocatable_realloc/alloc_mod.f90 @@ -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 diff --git a/examples/issue306_allocatable_realloc/tests.py b/examples/issue306_allocatable_realloc/tests.py new file mode 100644 index 00000000..81ab65df --- /dev/null +++ b/examples/issue306_allocatable_realloc/tests.py @@ -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!") From 961444a19836f30d46b907a2b5c7f03f7a6161ff Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Dec 2025 11:56:07 +0000 Subject: [PATCH 2/2] Add analysis of issue #306 root cause Root cause identified: sizeof_fortran_t mismatch between wrapper generation time (4 on GCC 13/14) and runtime (2 on GCC 15). The generated Fortran wrappers hardcode dummy_this(4) but this parameter is unused for module-level arrays. Proposed fix is to remove dummy_this from module-level array accessors entirely. --- .../issue306_allocatable_realloc/ANALYSIS.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 examples/issue306_allocatable_realloc/ANALYSIS.md diff --git a/examples/issue306_allocatable_realloc/ANALYSIS.md b/examples/issue306_allocatable_realloc/ANALYSIS.md new file mode 100644 index 00000000..edeab146 --- /dev/null +++ b/examples/issue306_allocatable_realloc/ANALYSIS.md @@ -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)