From dc2c16ba83ec072e19cbb50d69835dccdec19d51 Mon Sep 17 00:00:00 2001 From: Christopher Albert Date: Thu, 14 Nov 2024 19:33:46 +0000 Subject: [PATCH] Abstract classes --- .../issue206_subroutine_oldstyle/Makefile | 3 +- examples/issue41_abstract_classes/Makefile | 26 ++ .../issue41_abstract_classes/Makefile.meson | 6 + examples/issue41_abstract_classes/main.f90 | 24 ++ .../issue41_abstract_classes/myclass_base.f90 | 17 ++ .../myclass_factory.f90 | 26 ++ .../issue41_abstract_classes/myclass_impl.f90 | 27 ++ .../myclass_impl2.f90 | 27 ++ examples/issue41_abstract_classes/run.py | 19 ++ .../test_parser_abstract_iface.py | 50 ++++ f90wrap/f90wrapgen.py | 3 + f90wrap/parser.py | 237 ++++++++++++------ f90wrap/pywrapgen.py | 3 + f90wrap/transform.py | 23 +- 14 files changed, 417 insertions(+), 74 deletions(-) create mode 100644 examples/issue41_abstract_classes/Makefile create mode 100644 examples/issue41_abstract_classes/Makefile.meson create mode 100644 examples/issue41_abstract_classes/main.f90 create mode 100644 examples/issue41_abstract_classes/myclass_base.f90 create mode 100644 examples/issue41_abstract_classes/myclass_factory.f90 create mode 100644 examples/issue41_abstract_classes/myclass_impl.f90 create mode 100644 examples/issue41_abstract_classes/myclass_impl2.f90 create mode 100644 examples/issue41_abstract_classes/run.py create mode 100644 examples/issue41_abstract_classes/test_parser_abstract_iface.py diff --git a/examples/issue206_subroutine_oldstyle/Makefile b/examples/issue206_subroutine_oldstyle/Makefile index 1d5c4693..a67587ab 100644 --- a/examples/issue206_subroutine_oldstyle/Makefile +++ b/examples/issue206_subroutine_oldstyle/Makefile @@ -8,8 +8,7 @@ PYTHON = python all: subroutine_oldstyle.o f90wrap -m itest -P subroutine_oldstyle.f -v - f2py-f90wrap --fcompiler=$(FC) --build-dir . -c -m _itest f90wrap_toplevel.f90 \ - subroutine_oldstyle.o + f2py-f90wrap --build-dir . -c -m _itest f90wrap_toplevel.f90 subroutine_oldstyle.o test: all $(PYTHON) run.py diff --git a/examples/issue41_abstract_classes/Makefile b/examples/issue41_abstract_classes/Makefile new file mode 100644 index 00000000..2d0d0e8f --- /dev/null +++ b/examples/issue41_abstract_classes/Makefile @@ -0,0 +1,26 @@ +FC = gfortran +FCFLAGS = -fPIC +PYTHON = python + +all: wrapper test_abstract_classes.x + +test: wrapper + $(PYTHON) run.py + +test_abstract_classes.x: main.f90 myclass_base.o myclass_impl.o myclass_impl2.o myclass_factory.o + $(FC) $(FCFLAGS) -o $@ $^ + +wrapper: myclass_base.o myclass_impl.o myclass_impl2.o myclass_factory.o + f90wrap -m itest -P myclass_base.f90 myclass_impl.f90 myclass_impl2.f90 myclass_factory.f90 -v + f2py-f90wrap --build-dir . -c -m _itest --opt="-O0 -g" \ + f90wrap_myclass_base.f90 f90wrap_myclass_impl.f90 f90wrap_myclass_impl2.f90 f90wrap_myclass_factory.f90 \ + myclass_base.o myclass_impl.o myclass_impl2.o myclass_factory.o + +%.o : %.f90 + $(FC) $(FCFLAGS) -c -g -O0 $< -o $@ + +clean: + rm -f *.o f90wrap*.f90 *.so *.mod *.x + rm -rf src.*/ + rm -rf itest/ + -rm -rf src.*/ .f2py_f2cmap .libs/ __pycache__/ diff --git a/examples/issue41_abstract_classes/Makefile.meson b/examples/issue41_abstract_classes/Makefile.meson new file mode 100644 index 00000000..1fe6c182 --- /dev/null +++ b/examples/issue41_abstract_classes/Makefile.meson @@ -0,0 +1,6 @@ +include ../make.meson.inc + +NAME := itest + +test: build + $(PYTHON) run.py diff --git a/examples/issue41_abstract_classes/main.f90 b/examples/issue41_abstract_classes/main.f90 new file mode 100644 index 00000000..1713b4de --- /dev/null +++ b/examples/issue41_abstract_classes/main.f90 @@ -0,0 +1,24 @@ +program main + use myclass_factory, only: create_myclass + use myclass_base, only: myclass_t + implicit none + + print *, "Start" + + call test() + + print *, "Done" + +contains + +subroutine test + real :: x + class(myclass_t), allocatable :: myobject + + myobject = create_myclass("impl") + call myobject%get_value(x) + + print *, "Value: ", x +end subroutine test + +end program main diff --git a/examples/issue41_abstract_classes/myclass_base.f90 b/examples/issue41_abstract_classes/myclass_base.f90 new file mode 100644 index 00000000..e8929688 --- /dev/null +++ b/examples/issue41_abstract_classes/myclass_base.f90 @@ -0,0 +1,17 @@ +module myclass_base +implicit none + +type, abstract :: myclass_t +contains + procedure(get_value_i), deferred :: get_value +end type myclass_t + +abstract interface + subroutine get_value_i(self, value) + import myclass_t + class(myclass_t), intent(in) :: self + real, intent(out) :: value + end subroutine get_value_i +end interface + +end module myclass_base diff --git a/examples/issue41_abstract_classes/myclass_factory.f90 b/examples/issue41_abstract_classes/myclass_factory.f90 new file mode 100644 index 00000000..7c04bf2e --- /dev/null +++ b/examples/issue41_abstract_classes/myclass_factory.f90 @@ -0,0 +1,26 @@ +module myclass_factory + +use myclass_base, only: myclass_t +use myclass_impl, only: myclass_impl_t +use myclass_impl2, only: myclass_impl2_t +implicit none + +contains + +function create_myclass(impl_type) result(myobject) + class(myclass_t), allocatable :: myobject + + character(*), intent(in) :: impl_type + + select case(impl_type) + case("impl") + allocate(myclass_impl_t :: myobject) + case("impl2") + allocate(myclass_impl2_t :: myobject) + case default + print *, "create_field_can: Unknown implementation: ", impl_type + error stop + end select +end function create_myclass + +end module myclass_factory diff --git a/examples/issue41_abstract_classes/myclass_impl.f90 b/examples/issue41_abstract_classes/myclass_impl.f90 new file mode 100644 index 00000000..e5fea8f9 --- /dev/null +++ b/examples/issue41_abstract_classes/myclass_impl.f90 @@ -0,0 +1,27 @@ +module myclass_impl + +use myclass_base, only: myclass_t +implicit none + +type, extends(myclass_t) :: myclass_impl_t +contains + procedure :: get_value => get_value_impl + final :: myclass_impl_destroy +end type myclass_impl_t + +contains + +subroutine get_value_impl(self, value) + class(myclass_impl_t), intent(in) :: self + real, intent(out) :: value + + value = 1.0 +end subroutine get_value_impl + +subroutine myclass_impl_destroy(self) + type(myclass_impl_t), intent(inout) :: self + + print *, "Finalising myclass_impl_t" +end subroutine myclass_impl_destroy + +end module myclass_impl diff --git a/examples/issue41_abstract_classes/myclass_impl2.f90 b/examples/issue41_abstract_classes/myclass_impl2.f90 new file mode 100644 index 00000000..6f81151c --- /dev/null +++ b/examples/issue41_abstract_classes/myclass_impl2.f90 @@ -0,0 +1,27 @@ +module myclass_impl2 + +use myclass_base, only: myclass_t +implicit none + +type, extends(myclass_t) :: myclass_impl2_t +contains + procedure :: get_value => get_value_impl2 + final :: myclass_impl2_destroy +end type myclass_impl2_t + +contains + +subroutine get_value_impl2(self, value) + class(myclass_impl2_t), intent(in) :: self + real, intent(out) :: value + + value = 2.0 +end subroutine get_value_impl2 + +subroutine myclass_impl2_destroy(self) + type(myclass_impl2_t), intent(inout) :: self + + print *, "Finalising myclass_impl2_t" +end subroutine myclass_impl2_destroy + +end module myclass_impl2 diff --git a/examples/issue41_abstract_classes/run.py b/examples/issue41_abstract_classes/run.py new file mode 100644 index 00000000..219315fc --- /dev/null +++ b/examples/issue41_abstract_classes/run.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +import itest + +REF = 1.0 +TOL = 1.0e-6 + +obj = itest.myclass_factory.create_myclass("impl") + +output = obj.get_value() +assert(abs(output-REF)') + if m == None: + return check_deferred_binding(cl, file) + + type = m.group(1).strip().lower() + attrs = m.group(4) + bindings = m.group(6) + if attrs: + attrs = [a.strip().lower() for a in attrs.split(',')] + out = [] + if type == 'generic': + name, targets = bindings.split('=>') + name = name.strip().lower() + log.debug('found generic binding %s => %s', name, targets) + out.append(Binding( + name=name, + lineno=file.lineno, + filename=file.filename, + type=type, + attributes=attrs, + procedures=[ + Prototype( + name=t.strip().lower(), + lineno=file.lineno, + filename=file.filename + ) + for t in targets.split(',') + ], + )) + else: + for b in bindings.split(','): + name, *target = [ word.strip().lower() for word in b.split('=>')] name = name.strip().lower() - log.debug('found generic binding %s => %s', name, targets) + target = target[0] if target else name + log.debug('found %s binding %s => %s', type, name, target) out.append(Binding( name=name, lineno=file.lineno, @@ -1288,38 +1374,47 @@ def check_binding(cl, file): attributes=attrs, procedures=[ Prototype( - name=t.strip().lower(), + name=target.strip().lower(), lineno=file.lineno, - filename=file.filename - ) - for t in targets.split(',') + filename=file.filename, + ), ], )) - else: - for b in bindings.split(','): - name, *target = [ word.strip().lower() for word in b.split('=>')] - name = name.strip().lower() - target = target[0] if target else name - log.debug('found %s binding %s => %s', type, name, target) - out.append(Binding( - name=name, - lineno=file.lineno, - filename=file.filename, - type=type, - attributes=attrs, - procedures=[ - Prototype( - name=target.strip().lower(), - lineno=file.lineno, - filename=file.filename, - ), - ], - )) - cl = file.next() - return [out, cl] - else: + cl = file.next() + return [out, cl] + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def check_deferred_binding(cl, file): + m = deferred_binding.match(cl) + if m == None: return [None, cl] + type = m.group(1).strip().lower() + interface = m.group(2).strip().lower() + attrs = m.group(5) + name = m.group(7) + if attrs: + attrs = [a.strip().lower() for a in attrs.split(',')] + out = [] + log.debug('found deferred %s(%s) binding %s', type, interface, name) + out.append(Binding( + name=name, + lineno=file.lineno, + filename=file.filename, + type=type, + attributes=attrs, + procedures=[ + Prototype( + name=interface, + lineno=file.lineno, + filename=file.filename + ), + ], + )) + cl = file.next() + return [out, cl] # +++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/f90wrap/pywrapgen.py b/f90wrap/pywrapgen.py index 410a3c06..3c3fa693 100644 --- a/f90wrap/pywrapgen.py +++ b/f90wrap/pywrapgen.py @@ -462,6 +462,8 @@ def write_destructor(self, node): def visit_Procedure(self, node): log.info("PythonWrapperGenerator visiting routine %s" % node.name) + if "abstract" in node.attributes and not "method" in node.attributes: + return if "classmethod" in node.attributes: self.write_classmethod(node) elif "constructor" in node.attributes: @@ -499,6 +501,7 @@ def visit_Procedure(self, node): ): # procedures outside of derived types become static methods self.write("@staticmethod") + self.write("def %(method_name)s(%(py_arg_names)s):" % dct) self.indent() self.write(format_doc_string(node)) diff --git a/f90wrap/transform.py b/f90wrap/transform.py index 1f584a6b..89ac97c5 100644 --- a/f90wrap/transform.py +++ b/f90wrap/transform.py @@ -822,6 +822,9 @@ def add_missing_constructors(tree): for node in ft.walk(tree): if not isinstance(node, ft.Type): continue + if "abstract" in node.attributes: + log.info('Skipping constructor for abstract type %s', node.name) + continue for child in ft.iter_child_nodes(node): if 'constructor' in child.attributes: log.info('found constructor %s', child.name) @@ -1183,6 +1186,7 @@ class ResolveBindingPrototypes(ft.FortranTransformer): """ def visit_Module(self, node): procedure_map = { p.name:p for p in node.procedures } + proc_interf_map = procedures_and_abstract_interfaces(procedure_map, node) for type in node.types: # Pass 1: Associate module procedures with specific bindings @@ -1190,7 +1194,7 @@ def visit_Module(self, node): if binding.type == 'generic': continue proto = binding.procedures[0] # Only generics have multiple procedures - proc = procedure_map[proto.name] + proc = proc_interf_map[proto.name] proc = copy.deepcopy(proc) log.debug('Creating method for %s from procedure %s.', type.name, proc.name) proc.type_name = type.name @@ -1218,6 +1222,23 @@ def visit_Module(self, node): return node +def procedures_and_abstract_interfaces(procedure_map, node): + """ + Add procedures from abstract interfaces to the procedure map + + This is needed because abstract interfaces are not included in the module + interfaces list, and so are not resolved by ResolveInterfacePrototypes. + """ + extended_procedure_map = procedure_map.copy() + for int in node.interfaces: + if not 'abstract' in int.attributes: + continue + for proc in int.procedures: + if proc.name not in extended_procedure_map: + extended_procedure_map[proc.name] = proc + return extended_procedure_map + + class BindConstructorInterfaces(ft.FortranTransformer): """ Moves interfaces named after a type into that type and marks as constructor.