Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3c57801
Add notes
znicholls Aug 31, 2025
9402f91
Add ResultInt and clean up suggested Result implemenations
znicholls Sep 1, 2025
300694f
class(results) fun
mzecc Sep 2, 2025
844d29e
Playing around
mzecc Sep 3, 2025
89a8c79
Result Integer 0D
mzecc Sep 9, 2025
2f4ce95
Result Integer 0D
mzecc Sep 9, 2025
f23cf17
Result-type : 1D integer
mzecc Sep 9, 2025
a5fba54
Merge branch 'main' into result-type
mzecc Sep 9, 2025
53b1910
Fortitude CI
mzecc Sep 9, 2025
02b5392
Result type
mzecc Sep 11, 2025
e2145a0
Get fortran compiling
znicholls Sep 11, 2025
7937334
Add square root for easier illustration
znicholls Sep 11, 2025
4b6f728
Add failing tests
znicholls Sep 11, 2025
053fafd
Up to writing wrapper
znicholls Sep 11, 2025
61ba5dc
Get test failing
znicholls Sep 11, 2025
9dc7b3f
Get one test passing
znicholls Sep 11, 2025
31ebe4c
Pass error raising test
znicholls Sep 11, 2025
0174330
Skip test
mzecc Sep 12, 2025
f60b408
Corrected create_errors small mistake
mzecc Sep 12, 2025
999f35f
Corrected error
mzecc Sep 12, 2025
3c2a6a4
Mypy
mzecc Sep 12, 2025
4322463
Merge pull request #34 from openscm/result-type-zn
mzecc Sep 12, 2025
1c249f9
Errors bubble-up:1
mzecc Sep 24, 2025
fbec11d
Improvements and bubbling up errors
mzecc Oct 13, 2025
989bfa4
Removed '(type, external)' from 'implicit none'
mzecc Oct 13, 2025
2c1f383
Removed '(type, external)' from 'implicit none':2
mzecc Oct 13, 2025
a773f9c
Advancements and bubble-up working (message only)
mzecc Nov 12, 2025
daeb786
Corrected bug
mzecc Nov 14, 2025
c42bb2d
Tests Passing
mzecc Nov 17, 2025
a567cd5
Unified result container
mzecc Nov 25, 2025
da8421d
Tests passing
mzecc Nov 25, 2025
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
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ test: ## run the tests (re-installs the package every time so you might want to
uv run --no-sync python scripts/inject-srcs-into-meson-build.py
uv run --no-sync python -c 'from pathlib import Path; import example_fgen_basic' || ( echo "Run make virtual-environment first" && false )
COV_DIR=$$(uv run --no-sync python -c 'from pathlib import Path; import example_fgen_basic; print(Path(example_fgen_basic.__file__).parent)'); \
uv run --no-editable --reinstall-package example-fgen-basic pytest -r a -v tests src --doctest-modules --doctest-report ndiff --cov=$$COV_DIR
uv run --no-editable --reinstall-package example-fgen-basic pytest -s -r a -v tests src --doctest-modules --doctest-report ndiff --cov=$$COV_DIR
# uv run --no-editable --reinstall-package example-fgen-basic pytest -s -r a -v tests/unit/test_get_square_root.py src --doctest-modules --doctest-report ndiff --cov=$$COV_DIR

# Note on code coverage and testing:
# You must specify cov=src.
Expand Down Expand Up @@ -113,7 +114,7 @@ test-fortran: build-fortran ## run the Fortran tests

.PHONY: install-fortran
install-fortran: build-fortran ## install the Fortran (including the extension module)
uv run meson install -C build -v
uv run meson install -C build # -v
# # Can also do this to see where things go without making a mess
# uv run meson install -C build --destdir ../install-example

Expand Down
10 changes: 7 additions & 3 deletions fortitude.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
[check]
# TODO: think about adding other rules
select = [ "C", "E", "S" ]
ignore = [ ]
# Fortitude rules (https://fortitude.readthedocs.io/en/stable/rules/):
# Error (E), Correctness (C), Obsolescent (OB), Modernisation (MOD),
# Style (S), Portability (PORT), Fortitude (FORT)
select = [ "C", "E", "S", "PORT" ]
#Ignoring:
# C003: 'implicit none' missing 'external' [f2py does not recognize the syntax implicit none(type, external)]
ignore = ["C003","C072","S221"]
line-length = 120
15 changes: 15 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ if pyprojectwheelbuild_enabled
'src/example_fgen_basic/error_v/creation_wrapper.f90',
'src/example_fgen_basic/error_v/error_v_wrapper.f90',
'src/example_fgen_basic/error_v/passing_wrapper.f90',
'src/example_fgen_basic/get_square_root_wrapper.f90',
'src/example_fgen_basic/get_wavelength_wrapper.f90',
'src/example_fgen_basic/result/result_wrapper.f90',
)

# Specify all the other source Fortran files (original files and managers)
Expand All @@ -66,8 +68,18 @@ if pyprojectwheelbuild_enabled
'src/example_fgen_basic/error_v/passing.f90',
'src/example_fgen_basic/fpyfgen/base_finalisable.f90',
'src/example_fgen_basic/fpyfgen/derived_type_manager_helpers.f90',
'src/example_fgen_basic/get_square_root.f90',
'src/example_fgen_basic/get_wavelength.f90',
'src/example_fgen_basic/kind_parameters.f90',
'src/example_fgen_basic/result/result_gen.f90',
'src/example_fgen_basic/result/result_manager.f90',
# 'src/example_fgen_basic/result/result.f90',
# 'src/example_fgen_basic/result/result_none.f90',
# 'src/example_fgen_basic/result/result_dp.f90',
# 'src/example_fgen_basic/result/result_dp_manager.f90',
# 'src/example_fgen_basic/result/result_int.f90',
# 'src/example_fgen_basic/result/result_int_manager.f90',
# 'src/example_fgen_basic/result/result_int1D.f90',
)

# All Python files (wrappers and otherwise)
Expand All @@ -79,9 +91,12 @@ if pyprojectwheelbuild_enabled
'src/example_fgen_basic/error_v/error_v.py',
'src/example_fgen_basic/error_v/passing.py',
'src/example_fgen_basic/exceptions.py',
'src/example_fgen_basic/get_square_root.py',
'src/example_fgen_basic/get_wavelength.py',
'src/example_fgen_basic/pyfgen_runtime/__init__.py',
'src/example_fgen_basic/pyfgen_runtime/exceptions.py',
'src/example_fgen_basic/result/__init__.py',
'src/example_fgen_basic/result/result_gen.py',
'src/example_fgen_basic/typing.py',
)

Expand Down
1 change: 1 addition & 0 deletions scripts/inject-srcs-into-meson-build.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def main():
meson_variable, sorted(src_paths), REPO_ROOT
)

# TODO: something wrong in here
meson_build_out = re.sub(pattern, substitution, meson_build_out)

with open(REPO_ROOT / "meson.build", "w") as fh:
Expand Down
2 changes: 1 addition & 1 deletion src/example_fgen_basic/error_v/creation.f90
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module m_error_v_creation

use m_error_v, only: ErrorV, NO_ERROR_CODE

implicit none (type, external)
implicit none
private

public :: create_error, create_errors
Expand Down
5 changes: 3 additions & 2 deletions src/example_fgen_basic/error_v/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def create_error(inv: int) -> ErrorV:

# Initialise the result from the received index
res = ErrorV.from_instance_index(instance_index)

# Tell Fortran to finalise the object on the Fortran side
# (all data has been copied to Python now)
m_error_v_w.finalise_instance(instance_index)
Expand All @@ -80,7 +79,9 @@ def create_errors(invs: NP_ARRAY_OF_INT) -> tuple[ErrorV, ...]:
Created errors
"""
# Get the result, but receiving an instance index rather than the object itself
instance_indexes: NP_ARRAY_OF_INT = m_error_v_creation_w.create_errors(invs)
instance_indexes: NP_ARRAY_OF_INT = m_error_v_creation_w.create_errors(
invs, len(invs)
)

# Initialise the result from the received index
res = tuple(ErrorV.from_instance_index(i) for i in instance_indexes)
Expand Down
20 changes: 14 additions & 6 deletions src/example_fgen_basic/error_v/creation_wrapper.f90
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module m_error_v_creation_w
error_v_manager_set_instance_index_to => set_instance_index_to, &
error_v_manager_ensure_instance_array_size_is_at_least => ensure_instance_array_size_is_at_least

implicit none (type, external)
implicit none
private

public :: create_error, create_errors
Expand All @@ -39,7 +39,7 @@ function create_error(inv) result(res_instance_index)
! This is the major trick for wrapping.
! We return instance indexes (integers) to Python rather than the instance itself.

type(ErrorV) :: res
type(ErrorV) :: res, err

! Do the Fortran call
res = o_create_error(inv)
Expand All @@ -51,7 +51,8 @@ function create_error(inv) result(res_instance_index)

! Set the derived type value in the manager's array,
! ready for its attributes to be retrieved from Python.
call error_v_manager_set_instance_index_to(res_instance_index, res)
err = error_v_manager_set_instance_index_to(res_instance_index, res)
!MZ: check for errors ?

end function create_error

Expand All @@ -72,8 +73,8 @@ function create_errors(invs, n) result(res_instance_indexes)
!
! This is the major trick for wrapping.
! We return instance indexes (integers) to Python rather than the instance itself.

type(ErrorV), dimension(n) :: res
type(ErrorV) :: err
type(ErrorV), allocatable, dimension(:) :: res

integer :: i, tmp

Expand All @@ -82,7 +83,13 @@ function create_errors(invs, n) result(res_instance_indexes)
! Just do something stupid for now to see the pattern.
call error_v_manager_ensure_instance_array_size_is_at_least(n)

allocate(res(n))
! Do the Fortran call
! MZ: somenthing funny happens wheb res is an automatic array and
! not an allocatable one. LLMs and internet resorces I found are not
! completely clear to me. What seems to happen is that returning an array of derived types with allocatable
! components may generate hidden temporary arrays whose allocatable components
! become undefined (or the heap address gets corrupted) after the function returns.
res = o_create_errors(invs, n)

do i = 1, n
Expand All @@ -91,7 +98,8 @@ function create_errors(invs, n) result(res_instance_indexes)
call error_v_manager_get_available_instance_index(tmp)
! Set the derived type value in the manager's array,
! ready for its attributes to be retrieved from Python.
call error_v_manager_set_instance_index_to(tmp, res(i))
err = error_v_manager_set_instance_index_to(tmp, res(i))
!MZ: check for errors ?
! Set the result in the output array
res_instance_indexes(i) = tmp

Expand Down
106 changes: 92 additions & 14 deletions src/example_fgen_basic/error_v/error_v.f90
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,111 @@
!>
!> Fortran doesn't have a null value.
!> As a result, we introduce this derived type
!> with the convention that a code of 0 indicates no error.
!> with the convention that a code of `NO_ERROR_CODE` (0)
!> indicates no error (i.e. is our equivalent of a null value).
module m_error_v

implicit none (type, external)
implicit none
private

integer, parameter, public :: NO_ERROR_CODE = 0
!! Code that indicates no error

type, public :: ErrorV
!! Error value
!! Error value

integer :: code = 1
!! Error code

character(len=128) :: message = ""
character(len=:), allocatable :: message

!! Error message
! TODO: think about making the message allocatable to handle long messages

! TODO: think about adding idea of critical
! (means you can stop but also unwind errors and traceback along the way)

! TODO: think about adding trace (might be simpler than compiling with traceback)
! type(ErrorV), allocatable, dimension(:) :: causes
! class(ErrorV), allocatable :: cause
type(ErrorV), pointer :: cause => null()

contains

private

procedure, public :: build, finalise
procedure, public :: build
procedure, public :: finalise
! procedure, public :: get_error_message
final :: finalise_auto
! get_res sort of not needed (?)
! get_err sort of not needed (?)

end type ErrorV

interface ErrorV
!! Constructor interface - see build (TODO: figure out cross-ref syntax) for details
!! Constructor interface - see build (TODO: figure out cross-ref syntax) for details
module procedure :: constructor
end interface ErrorV

contains

function constructor(code, message) result(self)
! pure recursive function get_error_message(self) result(full_msg)
!
! class(ErrorV), target, intent(in) :: self
!
! character(len=:), allocatable :: full_msg
! character(len=:), allocatable :: cause_msg
!
! full_msg = self%message
! if (associated(self%cause)) then
! cause_msg = self%cause%get_error_message()
! full_msg = trim(full_msg) // ' Previous error: ' // trim(cause_msg)
! end if
!
! end function
! function get_error_message(self) result(full_msg)
!
! class(ErrorV), target, intent(in) :: self
! class(ErrorV), pointer :: p_errorv
!
! character(len=:), allocatable :: full_msg
!
! full_msg = ""
!
! if (allocated(self%message)) full_msg = trim(self%message)
! p_errorv => self
!
! do while (associated(p_errorv))
!
! if(len(full_msg)>0)then
! full_msg = trim(full_msg) // " --> Cause: " // p_errorv % message
! else
! full_msg = p_errorv % message
! end if
!
! p_errorv => p_errorv % cause
!
! end do
!
! end function

function constructor(code, message, cause) result(self)
!! Constructor - see build (TODO: figure out cross-ref syntax) for details

integer, intent(in) :: code
character(len=*), optional, intent(in) :: message
type(ErrorV), target, optional, intent(in) :: cause

type(ErrorV) :: self

call self % build(code, message)
call self % build(code, message, cause)

end function constructor

subroutine build(self, code, message)
subroutine build(self, code, message, cause)
!! Build instance

class(ErrorV), intent(inout) :: self
class(ErrorV), intent(out) :: self
! Hopefully can leave without docstring (like Python)

integer, intent(in) :: code
Expand All @@ -72,10 +119,25 @@ subroutine build(self, code, message)

character(len=*), optional, intent(in) :: message
!! Error message
type(ErrorV), target, optional, intent(in) :: cause

self % code = code
if (present(message)) then
self % message = message

if (present(cause)) then
! self % cause => cause
! allocate(self % cause)
! call self%cause%build(cause%code, cause%message, cause%cause)
! self%cause = cause
if (present(message)) then
self % message = adjustl(trim(message)) // " --> Cause: " // cause % message
else
self % message = " --> Cause: " // cause % message
end if

else
if (present(message)) then
self % message = adjustl(trim(message))
end if
end if

end subroutine build
Expand All @@ -88,8 +150,24 @@ subroutine finalise(self)

! If we make message allocatable, deallocate here
self % code = 1
self % message = ""
if (allocated(self%message)) deallocate(self%message)
! MZ when the object is finalized or goes out of scope, its pointer components are destroyed.
! Hopefully no shared ownership??
if (associated(self%cause)) nullify(self%cause)

end subroutine finalise

subroutine finalise_auto(self)
!! Finalise the instance (i.e. free/deallocate)
!!
!! This method is expected to be called automatically
!! by clever clean up, which is why it differs from [TODO x-ref] `finalise`

type(ErrorV), intent(inout) :: self
! Hopefully can leave without docstring (like Python)

call self % finalise()

end subroutine finalise_auto

end module m_error_v
2 changes: 0 additions & 2 deletions src/example_fgen_basic/error_v/error_v.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,8 @@ def from_instance_index(cls, instance_index: int) -> ErrorV:

# Integer is very simple
code = m_error_v_w.get_code(instance_index)

# String requires decode
message = m_error_v_w.get_message(instance_index).decode()

res = cls(code=code, message=message)

return res
Expand Down
Loading
Loading