From ad3bddf01a7254d8f01639927bbe91226d838a53 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 9 Mar 2026 10:12:44 +0000 Subject: [PATCH 01/28] #2612 add length property to ScalarType --- src/psyclone/psyir/backend/fortran.py | 11 +- src/psyclone/psyir/symbols/datatypes.py | 131 +++++++++++++++++++++--- 2 files changed, 123 insertions(+), 19 deletions(-) diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index acac8c384f..575f84a5fe 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -342,16 +342,23 @@ def gen_datatype(self, datatype, name): f"ScalarType.Precision.DOUBLE is not supported for " f"datatypes other than floating point numbers in " f"Fortran, found '{fortrantype}'") + if datatype.intrinsic == ScalarType.Intrinsic.CHARACTER: + # Include length information. + return f"{fortrantype}(len={self._visit(datatype.length)})" return fortrantype if isinstance(precision, DataNode): - if fortrantype not in ["real", "integer", "logical"]: + if fortrantype not in ["real", "integer", "logical", "character"]: raise VisitorError( f"kind not supported for datatype '{fortrantype}' in " f"symbol '{name}' in Fortran backend.") + len_str = "" + if datatype.intrinsic == ScalarType.Intrinsic.CHARACTER: + # Include length information. + len_str = f", len={self._visit(datatype.length)}" # The precision information is provided by a parameter, # so use KIND. - return f"{fortrantype}(kind={self._visit(precision)})" + return f"{fortrantype}(kind={self._visit(precision)}{len_str})" raise VisitorError( f"Unsupported precision type '{type(precision).__name__}' found " diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index eea60425e1..f0b496ae11 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -45,10 +45,12 @@ from collections import OrderedDict from dataclasses import dataclass from enum import Enum -from typing import Any, Optional, Union +from typing import Any, Optional, Union, TYPE_CHECKING from psyclone.configuration import Config from psyclone.errors import InternalError +if TYPE_CHECKING: + from psyclone.psyir.nodes.datanode import DataNode from psyclone.psyir.commentable_mixin import CommentableMixin from psyclone.psyir.symbols.datasymbol import DataSymbol from psyclone.psyir.symbols.data_type_symbol import DataTypeSymbol @@ -401,7 +403,8 @@ def copy(self): Intrinsic.BOOLEAN: bool, Intrinsic.REAL: float} - def __init__(self, intrinsic, precision): + def __init__(self, intrinsic, precision, + length: Optional[Union[int, str, "DataNode"]] = None): if not isinstance(intrinsic, ScalarType.Intrinsic): raise TypeError( f"ScalarType expected 'intrinsic' argument to be of type " @@ -436,11 +439,85 @@ def __init__(self, intrinsic, precision): # possible due to circular imports. self._precision = precision + # The 'length' setter includes validation checks. + self._length = None + self.length = length + @property - def intrinsic(self): + def length(self) -> "DataNode": + ''' + :returns: the length of a character type. + + :raises TypeError: if this ScalarType instance is not of + character type. + ''' + if self._intrinsic != ScalarType.Intrinsic.CHARACTER: + raise TypeError( + f"A ScalarType of intrinsic type '{self._intrinsic}' does not " + f"have the 'length' property.") + return self._length + + @length.setter + def length(self, value: Union[int, str, "DataNode"]): + ''' + Setter for the length of a character string. If the new value + is supplied as an int or str then this is converted into a Literal. + + :value: the new length to assign. + + :raises ValueError: if the supplied value is a str but is not ":" + or "*". + :raises ValueError: if the supplied value is an int with value < 0. + :raises TypeError: if the supplied value is of the wrong type. + + ''' + if value is None: + if self._intrinsic == ScalarType.Intrinsic.CHARACTER: + # pylint: disable=import-outside-toplevel + from psyclone.psyir.nodes.literal import Literal + # Default length of a character string is 1. + self._length = Literal("1", INTEGER_TYPE) + self._length = None + return + + if self._intrinsic != ScalarType.Intrinsic.CHARACTER: + raise TypeError( + f"Only ScalarTypes of CHARACTER type support the length " + f"property but length '{value}' was supplied to an intrinsic" + f" type of '{self._intrinsic}'") + + # pylint: disable=import-outside-toplevel + from psyclone.psyir.nodes.datanode import DataNode + if isinstance(value, str): + if value not in [":", "*"]: + raise ValueError( + f"If the length of a CharacterType is specified as a " + f"str then it must contain only ':' or '*' but got: " + f"'{value}'") + from psyclone.psyir.nodes.literal import Literal + # We could have an Enum to record ':' and '*' but I don't think + # that buys us anything over just storing the strings. + self._length = Literal( + value, ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, 1)) + elif isinstance(value, int) and not isinstance(value, bool): + if value < 0: + raise ValueError( + f"If the length of a character ScalarType is specified " + f"using an int then it must be >= 0 but got: {value}") + from psyclone.psyir.nodes import Literal + self._length = Literal(str(value), INTEGER_TYPE) + elif isinstance(value, DataNode): + self._length = value + else: + raise TypeError( + f"The length property of a CharacterType must be an int, str " + f"or DataNode but got '{type(value).__name__}'") + + @property + def intrinsic(self) -> ScalarType.Intrinsic: ''' :returns: the intrinsic used by this scalar type. - :rtype: :py:class:`pyclone.psyir.datatypes.ScalarType.Intrinsic` ''' return self._intrinsic @@ -453,28 +530,36 @@ def precision(self): ''' return self._precision - def __str__(self): + def __str__(self) -> str: ''' :returns: a description of this scalar datatype. - :rtype: str ''' if isinstance(self.precision, ScalarType.Precision): precision_str = self.precision.name else: precision_str = str(self.precision) - return f"Scalar<{self.intrinsic.name}, {precision_str}>" - def __eq__(self, other): + if self._length: + len_str = f", len:{self._length}" + else: + len_str = "" + + return f"Scalar<{self.intrinsic.name}, {precision_str}{len_str}>" + + def __eq__(self, other: Any) -> bool: ''' - :param Any other: the object to check equality to. + :param other: the object to check equality to. :returns: whether this type is equal to the 'other' type. - :rtype: bool + ''' if not super().__eq__(other): return False + if self.intrinsic != other.intrinsic: + return False + # TODO #2659 - the following should be sufficient but isn't because # currently, each new instance of an LFRicIntegerScalarDataType ends # up with a brand new instance of a precision symbol. @@ -491,7 +576,14 @@ def __eq__(self, other): ) else: precision_match = self.precision == other.precision - return precision_match and self.intrinsic == other.intrinsic + + if self.intrinsic == ScalarType.Intrinsic.CHARACTER: + # We've already checked that the two are of the same intrinsic type + length_match = self._length == other.length + else: + length_match = True + + return precision_match and length_match def replace_symbols_using(self, table_or_symbol): ''' @@ -510,6 +602,8 @@ def replace_symbols_using(self, table_or_symbol): from psyclone.psyir.nodes.datanode import DataNode if isinstance(self.precision, DataNode): self._precision.replace_symbols_using(table_or_symbol) + if self._length: + self._length.replace_symbols_using(table_or_symbol) def get_all_accessed_symbols(self) -> set[Symbol]: ''' @@ -522,20 +616,23 @@ def get_all_accessed_symbols(self) -> set[Symbol]: from psyclone.psyir.nodes.datanode import DataNode if isinstance(self.precision, DataNode): symbols.update(self.precision.get_all_accessed_symbols()) - + if self._length: + symbols.update(self._length.get_all_accessed_symbols()) return symbols - def copy(self): + def copy(self) -> ScalarType: ''' :returns: a copy of self. - :rtype: :py:class:`psyclone.psyir.symbols.DatatTypes.ScalarType` ''' # TODO #3135 After the precision is always either a Precision or # a DataNode this hasattr check can be removed. if hasattr(self.precision, "copy"): - return ScalarType(self.intrinsic, self.precision.copy()) + precision = self.precision.copy() else: - return ScalarType(self.intrinsic, self.precision) + precision = self.precision + if self._length: + return ScalarType(self.intrinsic, precision, self._length.copy()) + return ScalarType(self.intrinsic, precision) class ArrayType(DataType): @@ -1332,7 +1429,7 @@ def get_all_accessed_symbols(self) -> set[Symbol]: BOOLEAN_TYPE = ScalarType(ScalarType.Intrinsic.BOOLEAN, ScalarType.Precision.UNDEFINED) CHARACTER_TYPE = ScalarType(ScalarType.Intrinsic.CHARACTER, - ScalarType.Precision.UNDEFINED) + ScalarType.Precision.UNDEFINED, 1) # For automatic documentation generation From 5bb616cb3f9d6d9745792a485c1347bec83169e5 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 16 Mar 2026 17:11:30 +0000 Subject: [PATCH 02/28] #2612 add routine to handle char-length specification [skip ci] --- src/psyclone/psyir/backend/fortran.py | 37 +++++++------ src/psyclone/psyir/frontend/fparser2.py | 55 +++++++++++++++---- src/psyclone/psyir/nodes/literal.py | 3 +- src/psyclone/psyir/symbols/datatypes.py | 38 +++++++------ .../tests/psyir/backend/fortran_test.py | 11 ++-- .../frontend/fparser2_char_decln_test.py | 43 +++++++++++++++ .../fparser2_subroutine_handler_test.py | 2 +- .../tests/psyir/symbols/datasymbol_test.py | 3 +- 8 files changed, 138 insertions(+), 54 deletions(-) create mode 100644 src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index 575f84a5fe..bb1bb4124d 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -40,6 +40,8 @@ from a PSyIR tree. ''' # pylint: disable=too-many-lines +from typing import Union + from psyclone.configuration import Config from psyclone.errors import InternalError from psyclone.psyir.backend.language_writer import LanguageWriter @@ -51,10 +53,10 @@ Literal, Node, OMPDependClause, OMPReductionClause, Operation, Range, Routine, Schedule, UnaryOperation) from psyclone.psyir.symbols import ( - ArgumentInterface, ArrayType, ContainerSymbol, DataSymbol, DataTypeSymbol, - GenericInterfaceSymbol, IntrinsicSymbol, PreprocessorInterface, - RoutineSymbol, ScalarType, StructureType, Symbol, SymbolTable, - UnresolvedInterface, UnresolvedType, UnsupportedFortranType, + ArgumentInterface, ArrayType, ContainerSymbol, DataSymbol, DataType, + DataTypeSymbol, GenericInterfaceSymbol, IntrinsicSymbol, + PreprocessorInterface, RoutineSymbol, ScalarType, StructureType, Symbol, + SymbolTable, UnresolvedInterface, UnresolvedType, UnsupportedFortranType, UnsupportedType, TypedSymbol) @@ -263,20 +265,19 @@ def _reverse_map(reverse_dict, op_map): if mapping_key not in reverse_dict: reverse_dict[mapping_key] = mapping_value.upper() - def gen_datatype(self, datatype, name): + def gen_datatype(self, + datatype: Union[DataType, DataTypeSymbol], + name: str) -> str: '''Given a DataType instance as input, return the Fortran datatype of the symbol including any specific precision properties. :param datatype: the DataType or DataTypeSymbol describing the type of the declaration. - :type datatype: :py:class:`psyclone.psyir.symbols.DataType` or - :py:class:`psyclone.psyir.symbols.DataTypeSymbol` - :param str name: the name of the symbol being declared (only used for - error messages). + :param name: the name of the symbol being declared (only used for + error messages). :returns: the Fortran representation of the symbol's datatype including any precision properties. - :rtype: str :raises NotImplementedError: if the symbol has an unsupported datatype. @@ -296,9 +297,9 @@ def gen_datatype(self, datatype, name): return f"type({datatype.name})" if (isinstance(datatype, ArrayType) and - isinstance(datatype.intrinsic, DataTypeSymbol)): + isinstance(datatype.elemental_type, DataTypeSymbol)): # Symbol is an array of derived types - return f"type({datatype.intrinsic.name})" + return f"type({datatype.elemental_type.name})" try: fortrantype = TYPE_MAP_TO_FORTRAN[datatype.intrinsic] @@ -308,6 +309,10 @@ def gen_datatype(self, datatype, name): f"'{name}' found in gen_datatype().") from error precision = datatype.precision + if isinstance(datatype, ArrayType): + scalar_type = datatype.elemental_type + else: + scalar_type = datatype if isinstance(precision, int): if fortrantype not in ['real', 'integer', 'logical']: @@ -342,9 +347,9 @@ def gen_datatype(self, datatype, name): f"ScalarType.Precision.DOUBLE is not supported for " f"datatypes other than floating point numbers in " f"Fortran, found '{fortrantype}'") - if datatype.intrinsic == ScalarType.Intrinsic.CHARACTER: + if scalar_type.intrinsic == ScalarType.Intrinsic.CHARACTER: # Include length information. - return f"{fortrantype}(len={self._visit(datatype.length)})" + return f"{fortrantype}(len={self._visit(scalar_type.length)})" return fortrantype if isinstance(precision, DataNode): @@ -353,9 +358,9 @@ def gen_datatype(self, datatype, name): f"kind not supported for datatype '{fortrantype}' in " f"symbol '{name}' in Fortran backend.") len_str = "" - if datatype.intrinsic == ScalarType.Intrinsic.CHARACTER: + if scalar_type.intrinsic == ScalarType.Intrinsic.CHARACTER: # Include length information. - len_str = f", len={self._visit(datatype.length)}" + len_str = f", len={self._visit(scalar_type.length)}" # The precision information is provided by a parameter, # so use KIND. return f"{fortrantype}(kind={self._visit(precision)}{len_str})" diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index f90a6588bd..b19c8a5b6a 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -1632,12 +1632,12 @@ def _process_type_spec(self, parent, type_spec): precision = self._process_precision(type_spec, parent) if not precision: precision = default_precision(data_name) - # We don't support len or kind specifiers for character variables - if fort_type == "character" and type_spec.children[1]: - raise NotImplementedError( - f"Length or kind attributes not supported on a character " - f"variable: '{type_spec}'") - base_type = ScalarType(data_name, precision) + + if fort_type == "character": + # Character types can have a length + char_len = self._process_char_length(type_spec, parent) + + base_type = ScalarType(data_name, precision, length=char_len) elif isinstance(type_spec, Fortran2003.Declaration_Type_Spec): # This is a variable of derived type @@ -2827,12 +2827,14 @@ def _process_precision(self, type_spec, psyir_parent): ''' symbol_table = psyir_parent.scope.symbol_table - if not isinstance(type_spec.items[1], Fortran2003.Kind_Selector): + for child in type_spec.children: + if isinstance(child, Fortran2003.Kind_Selector): + kind_selector = child + break + else: # No precision is specified return None - kind_selector = type_spec.items[1] - if (isinstance(kind_selector.children[0], str) and kind_selector.children[0] == "*"): # Precision is provided in the form *N @@ -2889,6 +2891,37 @@ def _process_precision(self, type_spec, psyir_parent): ) return kind_expression + def _process_char_length(self, + type_spec, + psyir_parent: Node) -> Optional[DataNode]: + ''' + ''' + for child in type_spec.children: + if isinstance(child, Fortran2003.Length_Selector): + len_selector = child + break + else: + # No length is specified + return None + + # Children 0 holds '(' for a '(len=xxx)' + # or '*' for a '* char-length' + if isinstance(len_selector.children[1], + Fortran2003.Char_Length): + char_len = len_selector.children[1].children[1] + else: + char_len = len_selector.children[1] + + if isinstance(char_len, Fortran2003.Type_Param_Value): + if char_len.string == ":": + return ScalarType.CharLengthParameter.COLON + return ScalarType.CharLengthParameter.ASTERISK + + dummy = Assignment(parent=psyir_parent) + dummy.addchild(Reference(Symbol("a"))) + self.process_nodes(parent=dummy, nodes=[char_len]) + return dummy.rhs.detach() + def _add_comments_to_tree(self, parent: Node, preceding_comments, psy_child: Node) -> None: ''' @@ -5577,10 +5610,10 @@ def _subroutine_handler(self, node, parent): if routine_node.name.lower() == name.lower(): routine = routine_node break - if routine is None: + else: routine = Routine.create(name) # We add this to the parent so the finally of the next block - # can safe call detach on the routine. This handles the case + # can safely call detach on the routine. This handles the case # where an error occurs which should result in a codeblock, but # we had forward declared the Routine and we need to ensure the # empty Routine is detached from the tree. diff --git a/src/psyclone/psyir/nodes/literal.py b/src/psyclone/psyir/nodes/literal.py index f72bfe522a..17b07ee1da 100644 --- a/src/psyclone/psyir/nodes/literal.py +++ b/src/psyclone/psyir/nodes/literal.py @@ -45,7 +45,8 @@ from psyclone.core import VariablesAccessMap, Signature, AccessType from psyclone.psyir.nodes.datanode import DataNode -from psyclone.psyir.symbols import ScalarType, ArrayType, Symbol +from psyclone.psyir.symbols.symbol import Symbol +from psyclone.psyir.symbols.datatypes import ScalarType, ArrayType class Literal(DataNode): diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index f0b496ae11..cba0157e48 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -388,13 +388,25 @@ class Precision(Enum): DOUBLE = 2 UNDEFINED = 3 - def copy(self): + def copy(self) -> Precision: + ''' + :returns: a copy of self. + ''' + return copy.copy(self) + + class CharLengthParameter(Enum): + ASTERISK = 1 + COLON = 2 + + def copy(self) -> CharLengthParameter: ''' :returns: a copy of self. - :rtype: :py:class:`psyclone.psyir.symbols.ScalarType.Precision` ''' return copy.copy(self) + def debug_string(self) -> str: + return self.name + #: Mapping from PSyIR scalar data types to intrinsic Python types #: ignoring precision. TYPE_MAP_TO_PYTHON = { @@ -477,7 +489,8 @@ def length(self, value: Union[int, str, "DataNode"]): from psyclone.psyir.nodes.literal import Literal # Default length of a character string is 1. self._length = Literal("1", INTEGER_TYPE) - self._length = None + else: + self._length = None return if self._intrinsic != ScalarType.Intrinsic.CHARACTER: @@ -488,30 +501,21 @@ def length(self, value: Union[int, str, "DataNode"]): # pylint: disable=import-outside-toplevel from psyclone.psyir.nodes.datanode import DataNode - if isinstance(value, str): - if value not in [":", "*"]: - raise ValueError( - f"If the length of a CharacterType is specified as a " - f"str then it must contain only ':' or '*' but got: " - f"'{value}'") - from psyclone.psyir.nodes.literal import Literal - # We could have an Enum to record ':' and '*' but I don't think - # that buys us anything over just storing the strings. - self._length = Literal( - value, ScalarType(ScalarType.Intrinsic.CHARACTER, - ScalarType.Precision.UNDEFINED, 1)) + if isinstance(value, ScalarType.CharLengthParameter): + self._length = value elif isinstance(value, int) and not isinstance(value, bool): if value < 0: raise ValueError( f"If the length of a character ScalarType is specified " f"using an int then it must be >= 0 but got: {value}") - from psyclone.psyir.nodes import Literal + from psyclone.psyir.nodes.literal import Literal self._length = Literal(str(value), INTEGER_TYPE) elif isinstance(value, DataNode): self._length = value else: raise TypeError( - f"The length property of a CharacterType must be an int, str " + f"The length property of a CharacterType must be an int, " + f"ScalarType.CharLengthParameter " f"or DataNode but got '{type(value).__name__}'") @property diff --git a/src/psyclone/tests/psyir/backend/fortran_test.py b/src/psyclone/tests/psyir/backend/fortran_test.py index 8aeced553c..17a5d3c2de 100644 --- a/src/psyclone/tests/psyir/backend/fortran_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_test.py @@ -141,7 +141,7 @@ def test_gen_indices_error(fortran_writer): "type_name,result", [(ScalarType.Intrinsic.REAL, "real"), (ScalarType.Intrinsic.INTEGER, "integer"), - (ScalarType.Intrinsic.CHARACTER, "character"), + (ScalarType.Intrinsic.CHARACTER, "character(len=1)"), (ScalarType.Intrinsic.BOOLEAN, "logical")]) def test_gen_datatype_default_precision(fortran_writer, type_name, result): '''Check for all supported datatype names that the gen_datatype @@ -167,7 +167,7 @@ def test_gen_datatype_default_precision(fortran_writer, type_name, result): "double precision"), (ScalarType.Intrinsic.INTEGER, ScalarType.Precision.SINGLE, "integer"), (ScalarType.Intrinsic.CHARACTER, ScalarType.Precision.SINGLE, - "character"), + "character(len=1)"), (ScalarType.Intrinsic.BOOLEAN, ScalarType.Precision.SINGLE, "logical"),]) def test_gen_datatype_relative_precision(fortran_writer, type_name, precision, result): @@ -299,11 +299,8 @@ def test_gen_datatype_kind_precision(fortran_writer, type_name, result): array_type = ArrayType(scalar_type, [10, 10]) for my_type in [scalar_type, array_type]: if type_name == ScalarType.Intrinsic.CHARACTER: - with pytest.raises(VisitorError) as excinfo: - fortran_writer.gen_datatype(my_type, symbol_name) - assert (f"kind not supported for datatype 'character' in symbol " - f"'{symbol_name}' in Fortran backend." - in str(excinfo.value)) + assert (fortran_writer.gen_datatype(my_type, symbol_name) == + f"{result}(kind={precision_name}, len=1)") else: assert (fortran_writer.gen_datatype(my_type, symbol_name) == f"{result}(kind={precision_name})") diff --git a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py new file mode 100644 index 0000000000..1127fc63d1 --- /dev/null +++ b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py @@ -0,0 +1,43 @@ +import pytest + +from fparser.common.readfortran import FortranStringReader +from fparser.common.sourceinfo import FortranFormat +from fparser.two import Fortran2003 + +from psyclone.psyir.frontend.fparser2 import ( + Fparser2Reader) +from psyclone.psyir.nodes import Literal, Routine +from psyclone.psyir.symbols import INTEGER_TYPE, ScalarType + + +@pytest.mark.usefixtures("f2008_parser") +@pytest.mark.parametrize("len_expr,length", + [("", "1"), + ("(len=3)", "3"), + ("(3)", "3"), + ("*3", "3"), + ("*(3)", "3"), + ("(len=2*max_len)", "2 * max_len"), + ("*(2*max_len)", "2 * max_len"), + ("(len=:)", "COLON"), + ("(:)", "COLON"), + ("*(:)", "COLON"), + ("(len=*)", "ASTERISK"), + ("(*)", "ASTERISK"), + ("*(*)", "ASTERISK")]) +def test_char_decln_length_handling(len_expr, length): + ''' + ''' + fake_parent = Routine.create("dummy_schedule") + symtab = fake_parent.symbol_table + processor = Fparser2Reader() + + # Test simple declarations + reader = FortranStringReader(f"character{len_expr} :: l1") + # Set reader to free format (otherwise this is a comment in fixed format) + reader.set_format(FortranFormat(True, True)) + fparser2spec = Fortran2003.Specification_Part(reader).content[0] + processor.process_declarations(fake_parent, [fparser2spec], []) + l1_var = symtab.lookup("l1") + assert l1_var.datatype.intrinsic == ScalarType.Intrinsic.CHARACTER + assert l1_var.datatype.length.debug_string() == length diff --git a/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py b/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py index a633d769c0..234612e1f4 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py @@ -187,7 +187,7 @@ def test_function_handler(fortran_reader, fortran_writer): @pytest.mark.parametrize("basic_type, rhs_val", [("real", "1.0"), ("integer", "1"), ("logical", ".false."), - ("character", "'b'")]) + ("character(len=1)", "'b'")]) def test_function_type_prefix(fortran_reader, fortran_writer, basic_type, rhs_val): ''' diff --git a/src/psyclone/tests/psyir/symbols/datasymbol_test.py b/src/psyclone/tests/psyir/symbols/datasymbol_test.py index 4a227e0e1e..2858035c72 100644 --- a/src/psyclone/tests/psyir/symbols/datasymbol_test.py +++ b/src/psyclone/tests/psyir/symbols/datasymbol_test.py @@ -305,7 +305,8 @@ def test_datasymbol_initial_value_setter_invalid(): with pytest.raises(ValueError) as error: DataSymbol('a', CHARACTER_TYPE, initial_value=42) assert ("Error setting initial value for symbol 'a'. This DataSymbol " - "instance datatype is 'Scalar' meaning " + "instance datatype is 'Scalar]>' meaning " "the initial value should be") in str(error.value) assert "'str'>' but found " in str(error.value) assert "'int'>'." in str(error.value) From 4deeb2bca63f2e3c5f502dca7fdc95106e5e64d2 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Mar 2026 10:08:29 +0000 Subject: [PATCH 03/28] #2612 WIP fixing tests [skip ci] --- src/psyclone/psyir/backend/fortran.py | 18 +++++++- src/psyclone/psyir/frontend/fparser2.py | 22 ++++++---- src/psyclone/psyir/symbols/datatypes.py | 4 +- .../fortran_unsupported_declns_test.py | 6 +-- src/psyclone/tests/psyir/backend/sir_test.py | 6 +-- .../frontend/fparser2_char_decln_test.py | 34 ++++++++------- .../frontend/fparser2_parameter_stmts_test.py | 20 ++++----- .../frontend/fparser2_select_case_test.py | 4 +- .../frontend/fparser2_select_type_test.py | 42 +++++++++---------- 9 files changed, 90 insertions(+), 66 deletions(-) diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index bb1bb4124d..b7edbe1eee 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -349,7 +349,14 @@ def gen_datatype(self, f"Fortran, found '{fortrantype}'") if scalar_type.intrinsic == ScalarType.Intrinsic.CHARACTER: # Include length information. - return f"{fortrantype}(len={self._visit(scalar_type.length)})" + if (scalar_type.length == + ScalarType.CharLengthParameter.ASTERISK): + len_str = "*" + elif scalar_type.length == ScalarType.CharLengthParameter.COLON: + len_str = ":" + else: + len_str = self._visit(scalar_type.length) + return f"{fortrantype}(len={len_str})" return fortrantype if isinstance(precision, DataNode): @@ -360,7 +367,14 @@ def gen_datatype(self, len_str = "" if scalar_type.intrinsic == ScalarType.Intrinsic.CHARACTER: # Include length information. - len_str = f", len={self._visit(scalar_type.length)}" + if (scalar_type.length == + ScalarType.CharLengthParameter.ASTERISK): + len_str = "*" + elif scalar_type.length == ScalarType.CharLengthParameter.COLON: + len_str = ":" + else: + len_str = self._visit(scalar_type.length) + len_str = f", len={len_str}" # The precision information is provided by a parameter, # so use KIND. return f"{fortrantype}(kind={self._visit(precision)}{len_str})" diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index b19c8a5b6a..813b9868fb 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -1633,6 +1633,7 @@ def _process_type_spec(self, parent, type_spec): if not precision: precision = default_precision(data_name) + char_len = None if fort_type == "character": # Character types can have a length char_len = self._process_char_length(type_spec, parent) @@ -2892,9 +2893,16 @@ def _process_precision(self, type_spec, psyir_parent): return kind_expression def _process_char_length(self, - type_spec, + type_spec: Fortran2003.Intrinsic_Type_Spec, psyir_parent: Node) -> Optional[DataNode]: ''' + Process any length and precision attributes on a CHARACTER declaration. + + :param type_spec: the fparser2 parse tree describing the type. + :param psyir_parent: the parent node in the PSyIR tree. + + :returns: TODO + ''' for child in type_spec.children: if isinstance(child, Fortran2003.Length_Selector): @@ -3785,18 +3793,16 @@ def _create_select_type( ''' pointer_symbols = [] - # Create a symbol from the supplied base name. Store as an - # UnsupportedFortranType in the symbol table as we do not natively - # support character strings (as opposed to scalars) in the PSyIR at - # the moment. + # Create a symbol from the supplied base name. # TODO #2550 will improve this by using an integer instead. type_string_name = parent.scope.symbol_table.next_available_name( type_string_name) # Length is hardcoded here so could potentially be too short. # TODO #2550 will improve this by using an integer instead. - type_string_type = UnsupportedFortranType( - f"character(256) :: {type_string_name}") - type_string_symbol = DataSymbol(type_string_name, type_string_type) + type_string_symbol = DataSymbol( + type_string_name, + ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, length=256)) parent.scope.symbol_table.add(type_string_symbol) # Create text for a select type construct using the information diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index cba0157e48..0f150b7b18 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -606,7 +606,7 @@ def replace_symbols_using(self, table_or_symbol): from psyclone.psyir.nodes.datanode import DataNode if isinstance(self.precision, DataNode): self._precision.replace_symbols_using(table_or_symbol) - if self._length: + if isinstance(self._length, DataNode): self._length.replace_symbols_using(table_or_symbol) def get_all_accessed_symbols(self) -> set[Symbol]: @@ -620,7 +620,7 @@ def get_all_accessed_symbols(self) -> set[Symbol]: from psyclone.psyir.nodes.datanode import DataNode if isinstance(self.precision, DataNode): symbols.update(self.precision.get_all_accessed_symbols()) - if self._length: + if isinstance(self._length, DataNode): symbols.update(self._length.get_all_accessed_symbols()) return symbols diff --git a/src/psyclone/tests/psyir/backend/fortran_unsupported_declns_test.py b/src/psyclone/tests/psyir/backend/fortran_unsupported_declns_test.py index 9a2e6a6ad1..0c7f66e471 100644 --- a/src/psyclone/tests/psyir/backend/fortran_unsupported_declns_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_unsupported_declns_test.py @@ -253,8 +253,8 @@ def test_generating_unsupportedtype_routine_imports( module.write(''' module a_mod contains - character(len=3) function unsupported_type_symbol() - unsupported_type_symbol = 'a' + complex function unsupported_type_symbol() + unsupported_type_symbol = (1.0, 1.0) end function unsupported_type_symbol end module a_mod ''') @@ -264,7 +264,7 @@ def test_generating_unsupportedtype_routine_imports( contains subroutine test() integer :: a - a = unsupported_type_symbol() + a = INT(REAL(unsupported_type_symbol())) end subroutine test end module test_mod ''') diff --git a/src/psyclone/tests/psyir/backend/sir_test.py b/src/psyclone/tests/psyir/backend/sir_test.py index 9793a956ed..6240a6c848 100644 --- a/src/psyclone/tests/psyir/backend/sir_test.py +++ b/src/psyclone/tests/psyir/backend/sir_test.py @@ -653,9 +653,9 @@ def test_sirwriter_literal_node_error(sir_writer, value, datatype): rhs = get_rhs(code) with pytest.raises(VisitorError) as excinfo: sir_writer.literal_node(rhs) - assert ( - f"PSyIR type 'Scalar<{datatype}, UNDEFINED>' has no representation in " - f"the SIR backend." in str(excinfo.value)) + err_msg = str(excinfo.value) + assert f"PSyIR type 'Scalar<{datatype}, " in err_msg + assert ">' has no representation in the SIR backend" in err_msg # (1/5) Method unaryoperation_node diff --git a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py index 1127fc63d1..1f1007b488 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py @@ -11,21 +11,24 @@ @pytest.mark.usefixtures("f2008_parser") -@pytest.mark.parametrize("len_expr,length", - [("", "1"), - ("(len=3)", "3"), - ("(3)", "3"), - ("*3", "3"), - ("*(3)", "3"), - ("(len=2*max_len)", "2 * max_len"), - ("*(2*max_len)", "2 * max_len"), - ("(len=:)", "COLON"), - ("(:)", "COLON"), - ("*(:)", "COLON"), - ("(len=*)", "ASTERISK"), - ("(*)", "ASTERISK"), - ("*(*)", "ASTERISK")]) -def test_char_decln_length_handling(len_expr, length): +@pytest.mark.parametrize("len_expr,length,kind", + [("", "1", ScalarType.Precision.UNDEFINED), + ("(len=3)", "3", ScalarType.Precision.UNDEFINED), + ("(3)", "3", ScalarType.Precision.UNDEFINED), + ("*3", "3", ScalarType.Precision.UNDEFINED), + ("*(3)", "3", ScalarType.Precision.UNDEFINED), + ("(len=2*max_len)", "2 * max_len", + ScalarType.Precision.UNDEFINED), + ("*(2*max_len)", "2 * max_len", + ScalarType.Precision.UNDEFINED), + ("(len=:)", "COLON", ScalarType.Precision.UNDEFINED), + ("(:)", "COLON", ScalarType.Precision.UNDEFINED), + ("*(:)", "COLON", ScalarType.Precision.UNDEFINED), + ("(len=*)", "ASTERISK", + ScalarType.Precision.UNDEFINED), + ("(*)", "ASTERISK", ScalarType.Precision.UNDEFINED), + ("*(*)", "ASTERISK", ScalarType.Precision.UNDEFINED)]) +def test_char_decln_length_handling(len_expr, length, kind): ''' ''' fake_parent = Routine.create("dummy_schedule") @@ -40,4 +43,5 @@ def test_char_decln_length_handling(len_expr, length): processor.process_declarations(fake_parent, [fparser2spec], []) l1_var = symtab.lookup("l1") assert l1_var.datatype.intrinsic == ScalarType.Intrinsic.CHARACTER + assert l1_var.datatype.precision == kind assert l1_var.datatype.length.debug_string() == length diff --git a/src/psyclone/tests/psyir/frontend/fparser2_parameter_stmts_test.py b/src/psyclone/tests/psyir/frontend/fparser2_parameter_stmts_test.py index c432da13dc..83dc8e0141 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_parameter_stmts_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_parameter_stmts_test.py @@ -154,13 +154,13 @@ def test_parameter_statements_with_unsupported_symbols(): # Test with a UnsupportedType declaration reader = FortranStringReader(''' - character*5 :: var1 - parameter (var1='hello')''') + complex :: var1 + parameter (var1=(1.0,1.0))''') fparser2spec = Specification_Part(reader) with pytest.raises(NotImplementedError) as error: processor.process_declarations(routine, fparser2spec.content, []) - assert ("Could not process 'PARAMETER(var1 = 'hello')' because 'var1' has " + assert ("Could not process 'PARAMETER(var1 = (1.0, 1.0))' because 'var1' has " "an UnsupportedType." in str(error.value)) # Test with a symbol which is not a DataSymbol @@ -196,8 +196,8 @@ def test_unsupported_parameter_statements_produce_codeblocks(fortran_reader, contains subroutine my_sub() integer :: var - character*5 :: var1 - parameter (var=3, var1='hello') + complex :: var1 + parameter (var=3, var1=(1.0, 1.0)) end subroutine my_sub end module my_mod ''') @@ -206,8 +206,8 @@ def test_unsupported_parameter_statements_produce_codeblocks(fortran_reader, psyir = fortran_reader.psyir_from_source(''' module my_mod - character*5 :: var1 - parameter (var1='hello') + complex :: var1 + parameter (var1=(1.0, 1.0)) contains subroutine my_sub() integer :: var @@ -222,11 +222,11 @@ def test_unsupported_parameter_statements_produce_codeblocks(fortran_reader, code = fortran_writer(psyir) assert code == '''\ ! PSyclone CodeBlock (unsupported code) reason: -! - Could not process 'PARAMETER(var1 = 'hello')' because 'var1' has an \ +! - Could not process 'PARAMETER(var1 = (1.0, 1.0))' because 'var1' has an \ UnsupportedType. MODULE my_mod - CHARACTER*5 :: var1 - PARAMETER(var1 = 'hello') + COMPLEX :: var1 + PARAMETER(var1 = (1.0, 1.0)) CONTAINS SUBROUTINE my_sub INTEGER :: var diff --git a/src/psyclone/tests/psyir/frontend/fparser2_select_case_test.py b/src/psyclone/tests/psyir/frontend/fparser2_select_case_test.py index 6d9a8ad491..5505c51a0c 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_select_case_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_select_case_test.py @@ -512,8 +512,8 @@ def has_cmp_interface(code): # Check that the char implementation is in the code assert '''function test_psyclone_internal_cmp_char(op1, op2) - CHARACTER(LEN = *), INTENT(IN) :: op1 - CHARACTER(LEN = *), INTENT(IN) :: op2 + character(len=*), intent(in) :: op1 + character(len=*), intent(in) :: op2 logical :: test_psyclone_internal_cmp_char test_psyclone_internal_cmp_char = op1 == op2 diff --git a/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py b/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py index e72162024f..69b16ebd86 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py @@ -74,7 +74,7 @@ def test_type(fortran_reader, fortran_writer, tmpdir): "end module\n") expected1 = "CLASS(*), TARGET :: type_selector" expected2 = ( - " character(256) :: type_string\n" + " character(len=256) :: type_string\n" " INTEGER, pointer :: ptr_INTEGER => null()\n" " REAL, pointer :: ptr_REAL => null()\n\n" " type_string = ''\n" @@ -135,7 +135,7 @@ def test_default(fortran_reader, fortran_writer, tmpdir): "end subroutine\n" "end module\n") expected = ( - " character(256) :: type_string\n" + " character(len=256) :: type_string\n" " INTEGER, pointer :: ptr_INTEGER => null()\n" " REAL, pointer :: ptr_REAL => null()\n\n" " type_string = ''\n" @@ -207,7 +207,7 @@ def test_class(fortran_reader, fortran_writer, tmpdir): "end module\n") expected1 = "class(*), pointer :: type" expected2 = ( - " character(256) :: type_string\n" + " character(len=256) :: type_string\n" " type(type2), pointer :: ptr_type2 => null()\n" " INTEGER, pointer :: ptr_INTEGER => null()\n" " type(type3), pointer :: ptr_type3 => null()\n" @@ -382,7 +382,7 @@ def test_kind(fortran_reader, fortran_writer, tmpdir): " integer :: branch2\n" " REAL(KIND=4) :: rinfo1\n" " REAL(KIND=8) :: rinfo2\n" - " character(256) :: type_string\n" + " character(len=256) :: type_string\n" " REAL(KIND = 4), pointer :: ptr_REAL_4 => null()\n" " REAL(KIND = 8), pointer :: ptr_REAL_8 => null()\n").lower() expected2 = ( @@ -441,7 +441,7 @@ def test_derived(fortran_reader, fortran_writer, tmpdir): " end type field_type\n" " type(field_type) :: field_type_info\n" " integer :: branch1\n" - " character(256) :: type_string\n" + " character(len=256) :: type_string\n" " type(field_type), pointer :: ptr_field_type => null()\n") expected2 = ( " type_string = ''\n" @@ -497,11 +497,11 @@ def test_datatype(fortran_reader, fortran_writer, tmpdir): " integer :: branch2\n" " integer :: branch3\n" " logical :: logical_type\n" - " CHARACTER(LEN = 256) :: character_type\n" + " character(len=256) :: character_type\n" " COMPLEX :: complex_type\n" - " character(256) :: type_string\n" + " character(len=256) :: type_string\n" " LOGICAL, pointer :: ptr_LOGICAL => null()\n" - " CHARACTER(LEN=256), pointer :: ptr_CHARACTER_star => null()\n" + " character(len=256), pointer :: ptr_CHARACTER_star => null()\n" " COMPLEX, pointer :: ptr_COMPLEX => null()\n").lower() expected2 = ( " type_string = ''\n" @@ -540,8 +540,8 @@ def test_datatype(fortran_reader, fortran_writer, tmpdir): @pytest.mark.parametrize( "char_type_in, char_type_out", - (["*256", "*256"], ["(256)", "(LEN = 256)"], - ["(LEN = 256)", "(LEN = 256)"])) + (["*256", "(len=256)"], ["(256)", "(len=256)"], + ["(LEN = 256)", "(len=256)"])) def test_character(fortran_reader, fortran_writer, tmpdir, char_type_in, char_type_out): '''Check that the correct code is output with literal and implicit @@ -566,7 +566,7 @@ def test_character(fortran_reader, fortran_writer, tmpdir, char_type_in, f" subroutine select_type(type_selector)\n" f" CLASS(*), TARGET :: type_selector\n" f" CHARACTER{char_type_out} :: character_type\n" - f" character(256) :: type_string\n" + f" character(len=256) :: type_string\n" f" CHARACTER(LEN=256), pointer :: ptr_CHARACTER_star => " f"null()\n").lower() expected2 = ( @@ -611,7 +611,7 @@ def test_character_assumed_len(fortran_reader, fortran_writer, tmpdir, f" subroutine select_type(type_selector)\n" f" CLASS(*), TARGET :: type_selector\n" f" CHARACTER{char_type_out}, POINTER :: character_type => null()\n" - f" character(256) :: type_string\n" + f" character(len=256) :: type_string\n" f" CHARACTER(LEN=256), pointer :: ptr_CHARACTER_star => " f"null()\n").lower() expected2 = ( @@ -633,13 +633,13 @@ def test_character_assumed_len(fortran_reader, fortran_writer, tmpdir, @pytest.mark.parametrize( "char_type_in, char_type_out, pointer", - (["(LEN=*, KIND=1)", "(LEN = *, KIND = 1)", ""], - ["(LEN=*, KIND=1*1)", "(LEN = *, KIND = 1 * 1)", ""], - ["(LEN=1*2, KIND=1*1)", "(LEN = 1 * 2, KIND = 1 * 1)", ""], - ["(*, KIND=1*1)", "(LEN = *, KIND = 1 * 1)", ""], - ["(256*1, KIND=1*1)", "(LEN = 256 * 1, KIND = 1 * 1)", ""], - ["(*, 1*1)", "(LEN = *, KIND = 1 * 1)", ""], - ["(256*1, 1*1)", "(LEN = 256 * 1, KIND = 1 * 1)", ""], + (["(LEN=*, KIND=1)", "(LEN=*, KIND=1)", ""], + ["(LEN=*, KIND=1*1)", "(LEN=*, KIND=1 * 1)", ""], + ["(LEN=1*2, KIND=1*1)", "(LEN=1 * 2, KIND=1 * 1)", ""], + ["(*, KIND=1*1)", "(LEN=*, KIND=1 * 1)", ""], + ["(256*1, KIND=1*1)", "(LEN=256 * 1, KIND=1 * 1)", ""], + ["(*, 1*1)", "(LEN=*, KIND=1 * 1)", ""], + ["(256*1, 1*1)", "(LEN=256*1, KIND=1 * 1)", ""], ["(KIND=1*1, LEN=*)", "(LEN = *, KIND = 1 * 1)", ""], ["(KIND=1*1, LEN=256*1)", "(LEN = 256 * 1, KIND = 1 * 1)", ""], ["(KIND=1*1)", "(KIND = 1 * 1)", ""], @@ -674,7 +674,7 @@ def test_character_kind( f" subroutine select_type(type_selector, character_type)\n" f" class(*), target :: type_selector\n" f" character{char_type_out}{pointer} :: character_type\n" - f" character(256) :: type_string\n" + f" character(len=256) :: type_string\n" f" character(len=256), pointer :: ptr_character_star => null()\n\n" f" type_string = ''\n" f" select type(type_selector)\n" @@ -725,7 +725,7 @@ def test_class_target( f" subroutine select_type(type_selector, character_type)\n" f" CLASS(*), {post_attribute} :: type_selector\n" f" CHARACTER(LEN = *) :: character_type\n" - f" character(256) :: type_string\n" + f" character(len=256) :: type_string\n" f" CHARACTER(LEN=256), pointer :: ptr_CHARACTER_star => null()\n\n" f" type_string = ''\n" f" SELECT TYPE(type_selector)\n" From e11294e9f9f7b7b0ca57a3387367c4c6bae6f0a3 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Mar 2026 14:17:08 +0000 Subject: [PATCH 04/28] #2612 fix len and kind handling for characters [skip ci] --- src/psyclone/psyir/frontend/fparser2.py | 66 +++++++++++-------- .../frontend/fparser2_char_decln_test.py | 31 ++++++++- 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index 813b9868fb..cbf43e5391 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -2802,7 +2802,10 @@ def _process_common_blocks(nodes, psyir_parent): f"The symbol interface of a common block variable " f"could not be updated because of {error}.") from error - def _process_precision(self, type_spec, psyir_parent): + def _process_precision(self, + type_spec: Fortran2003.Intrinsic_Type_Spec, + psyir_parent: Node) -> Optional[ + Union[ScalarType.Precision, DataNode]]: '''Processes the fparser2 parse tree of the type specification of a variable declaration in order to extract precision information. Two formats for specifying precision are @@ -2810,40 +2813,42 @@ def _process_precision(self, type_spec, psyir_parent): kind=KIND(x). :param type_spec: the fparser2 parse tree of the type specification. - :type type_spec: \ - :py:class:`fparser.two.Fortran2003.Intrinsic_Type_Spec` - :param psyir_parent: the parent PSyIR node where the new node \ + :param psyir_parent: the parent PSyIR node where the new node will be attached. - :type psyir_parent: :py:class:`psyclone.psyir.nodes.Node` :returns: the precision associated with the type specification. - :rtype: :py:class:`psyclone.psyir.symbols.DataSymbol.Precision` or \ - :py:class:`psyclone.psyir.nodes.DataNode` or int or NoneType - :raises NotImplementedError: if a KIND intrinsic is found with an \ + :raises NotImplementedError: if a KIND intrinsic is found with an argument other than a real or integer literal. - :raises NotImplementedError: if we have `kind=xxx` but cannot find \ + :raises NotImplementedError: if we have `kind=xxx` but cannot find a valid variable name. ''' symbol_table = psyir_parent.scope.symbol_table + is_char = False for child in type_spec.children: if isinstance(child, Fortran2003.Kind_Selector): kind_selector = child break + if isinstance(child, Fortran2003.Char_Selector): + # A CHARACTER declaration can be of Char_Selector type. + # The second child of Char_Selector holds the precision. + is_char = True + kind_selector = child.children[1] + break else: # No precision is specified return None - if (isinstance(kind_selector.children[0], str) and - kind_selector.children[0] == "*"): + if not is_char and (isinstance(kind_selector.children[0], str) and + kind_selector.children[0] == "*"): # Precision is provided in the form *N precision = int(str(kind_selector.children[1])) return precision # Precision is supplied in the form "kind=..." - intrinsics = walk(kind_selector.items, + intrinsics = walk(kind_selector, Fortran2003.Intrinsic_Function_Reference) if intrinsics and isinstance(intrinsics[0].items[0], Fortran2003.Intrinsic_Name) and \ @@ -2868,8 +2873,10 @@ def _process_precision(self, type_spec, psyir_parent): # Create a dummy Routine and Assignment to capture the kind=... # so we can capture expressions such as 2*wp. - # The input from fparser2 is ['(', kind, ')'] - kind_items = kind_selector.items[1] + # The input from fparser2 is ['(', kind, ')'] if it is not a + # Char_Selector, otherwise kind_selector already holds the kind + # expression. + kind_items = kind_selector.items[1] if not is_char else kind_selector fake_routine = Routine(RoutineSymbol("dummy")) # Create a dummy assignment "a = " to place the kind statement on # the rhs of. @@ -2894,37 +2901,44 @@ def _process_precision(self, type_spec, psyir_parent): def _process_char_length(self, type_spec: Fortran2003.Intrinsic_Type_Spec, - psyir_parent: Node) -> Optional[DataNode]: + psyir_parent: Node) -> Optional[ + Union[ScalarType.CharLengthParameter, + DataNode]]: ''' - Process any length and precision attributes on a CHARACTER declaration. + Process any length attribute on a CHARACTER declaration. :param type_spec: the fparser2 parse tree describing the type. :param psyir_parent: the parent node in the PSyIR tree. - :returns: TODO + :returns: the length of the character string or None if it is + unspecified. ''' for child in type_spec.children: if isinstance(child, Fortran2003.Length_Selector): - len_selector = child + # Child 0 holds '(' for a '(len=xxx)' or '*' for a + # '* char-length'. Either way, child 1 holds the length. + if isinstance(child.children[1], Fortran2003.Char_Length): + char_len = child.children[1].children[1] + else: + char_len = child.children[1] + break + + if isinstance(child, Fortran2003.Char_Selector): + # A CHARACTER declaration can be of Char_Selector type. + # The first child of Char_Selector holds the length. + char_len = child.children[0] break else: # No length is specified return None - # Children 0 holds '(' for a '(len=xxx)' - # or '*' for a '* char-length' - if isinstance(len_selector.children[1], - Fortran2003.Char_Length): - char_len = len_selector.children[1].children[1] - else: - char_len = len_selector.children[1] - if isinstance(char_len, Fortran2003.Type_Param_Value): if char_len.string == ":": return ScalarType.CharLengthParameter.COLON return ScalarType.CharLengthParameter.ASTERISK + # Create a dummy assignment so we can process the length expression. dummy = Assignment(parent=psyir_parent) dummy.addchild(Reference(Symbol("a"))) self.process_nodes(parent=dummy, nodes=[char_len]) diff --git a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py index 1f1007b488..bfe840bf4f 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py @@ -6,8 +6,9 @@ from psyclone.psyir.frontend.fparser2 import ( Fparser2Reader) -from psyclone.psyir.nodes import Literal, Routine -from psyclone.psyir.symbols import INTEGER_TYPE, ScalarType +from psyclone.psyir.nodes import Reference, Routine +from psyclone.psyir.symbols import ( + ScalarType, Symbol, UnsupportedFortranType) @pytest.mark.usefixtures("f2008_parser") @@ -27,9 +28,14 @@ ("(len=*)", "ASTERISK", ScalarType.Precision.UNDEFINED), ("(*)", "ASTERISK", ScalarType.Precision.UNDEFINED), - ("*(*)", "ASTERISK", ScalarType.Precision.UNDEFINED)]) + ("*(*)", "ASTERISK", ScalarType.Precision.UNDEFINED), + ("(len=3, kind=ckind)", "3", + Reference(Symbol("ckind"))), + ("(len=*, kind=ckind)", "ASTERISK", + Reference(Symbol("ckind")))]) def test_char_decln_length_handling(len_expr, length, kind): ''' + Test the handling of kind and length specifiers. ''' fake_parent = Routine.create("dummy_schedule") symtab = fake_parent.symbol_table @@ -45,3 +51,22 @@ def test_char_decln_length_handling(len_expr, length, kind): assert l1_var.datatype.intrinsic == ScalarType.Intrinsic.CHARACTER assert l1_var.datatype.precision == kind assert l1_var.datatype.length.debug_string() == length + + +@pytest.mark.usefixtures("f2008_parser") +def test_char_decln_with_char_kind(): + ''' + Check that we get the expected UnsupportedFortranType if the kind is + specified using a character literal. + + ''' + fake_parent = Routine.create("dummy_schedule") + symtab = fake_parent.symbol_table + processor = Fparser2Reader() + reader = FortranStringReader(f"character(len=3, kind=KIND('h')) :: l1") + # Set reader to free format (otherwise this is a comment in fixed format) + reader.set_format(FortranFormat(True, True)) + fparser2spec = Fortran2003.Specification_Part(reader).content[0] + processor.process_declarations(fake_parent, [fparser2spec], []) + l1_var = symtab.lookup("l1") + assert isinstance(l1_var.datatype, UnsupportedFortranType) From a4e1f407ccbc237909d19688e48f38751e9dff6f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Mar 2026 14:39:29 +0000 Subject: [PATCH 05/28] #2612 fix select-type tests [skip ci] --- src/psyclone/psyir/frontend/fparser2.py | 5 +++- .../frontend/fparser2_select_type_test.py | 26 +++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index cbf43e5391..b1f7601db5 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -2926,8 +2926,11 @@ def _process_char_length(self, if isinstance(child, Fortran2003.Char_Selector): # A CHARACTER declaration can be of Char_Selector type. - # The first child of Char_Selector holds the length. + # The first child of Char_Selector holds the length (which + # may be None if it is unspecified). char_len = child.children[0] + if not char_len: + return None break else: # No length is specified diff --git a/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py b/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py index 69b16ebd86..51066c4a22 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py @@ -633,24 +633,24 @@ def test_character_assumed_len(fortran_reader, fortran_writer, tmpdir, @pytest.mark.parametrize( "char_type_in, char_type_out, pointer", - (["(LEN=*, KIND=1)", "(LEN=*, KIND=1)", ""], - ["(LEN=*, KIND=1*1)", "(LEN=*, KIND=1 * 1)", ""], - ["(LEN=1*2, KIND=1*1)", "(LEN=1 * 2, KIND=1 * 1)", ""], - ["(*, KIND=1*1)", "(LEN=*, KIND=1 * 1)", ""], - ["(256*1, KIND=1*1)", "(LEN=256 * 1, KIND=1 * 1)", ""], - ["(*, 1*1)", "(LEN=*, KIND=1 * 1)", ""], - ["(256*1, 1*1)", "(LEN=256*1, KIND=1 * 1)", ""], - ["(KIND=1*1, LEN=*)", "(LEN = *, KIND = 1 * 1)", ""], - ["(KIND=1*1, LEN=256*1)", "(LEN = 256 * 1, KIND = 1 * 1)", ""], - ["(KIND=1*1)", "(KIND = 1 * 1)", ""], + (["(LEN=*, KIND=1)", "(KIND=1, LEN=*)", ""], + ["(LEN=*, KIND=1*1)", "(KIND=1 * 1, LEN=*)", ""], + ["(LEN=1*2, KIND=1*1)", "(KIND=1 * 1, LEN=1 * 2)", ""], + ["(*, KIND=1*1)", "(KIND=1 * 1, LEN=*)", ""], + ["(256*1, KIND=1*1)", "(KIND=1 * 1, LEN=256 * 1)", ""], + ["(*, 1*1)", "(KIND=1 * 1, LEN=*)", ""], + ["(256*1, 1*1)", "(KIND=1 * 1, LEN=256 * 1)", ""], + ["(KIND=1*1, LEN=*)", "(KIND=1 * 1, LEN=*)", ""], + ["(KIND=1*1, LEN=256*1)", "(KIND=1 * 1, LEN=256 * 1)", ""], + ["(KIND=1*1)", "(KIND=1 * 1, LEN=1)", ""], ["(LEN=:, KIND=1*1)", "(LEN = :, KIND = 1 * 1)", ", POINTER"], - ["(:, KIND=1*1)", "(LEN = :, KIND = 1 * 1)", ", POINTER"], + ["(*, KIND=1*1)", "(LEN = *, KIND = 1 * 1)", ", POINTER"], ["(:, 1*1)", "(LEN = :, KIND = 1 * 1)", ", POINTER"], ["(KIND=1*1, LEN=:)", "(LEN = :, KIND = 1 * 1)", ", POINTER"])) def test_character_kind( fortran_reader, fortran_writer, tmpdir, char_type_in, char_type_out, pointer): - '''Test that characters with kind clauses in various formats are + '''Test that characters with kind and len clauses in various formats are supported. ''' @@ -724,7 +724,7 @@ def test_class_target( f" contains\n" f" subroutine select_type(type_selector, character_type)\n" f" CLASS(*), {post_attribute} :: type_selector\n" - f" CHARACTER(LEN = *) :: character_type\n" + f" CHARACTER(LEN=*) :: character_type\n" f" character(len=256) :: type_string\n" f" CHARACTER(LEN=256), pointer :: ptr_CHARACTER_star => null()\n\n" f" type_string = ''\n" From 153277618d7048b06f53a1c12dcd5062dcf0aed4 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 17 Mar 2026 16:50:03 +0000 Subject: [PATCH 06/28] #2612 fix more psyir tests [skip ci] --- src/psyclone/psyir/nodes/array_reference.py | 5 +++- src/psyclone/psyir/nodes/datanode.py | 17 +++++++------- .../transformations/acc_kernels_trans.py | 23 +++++++------------ .../arrayassignment2loops_trans.py | 18 +++++++-------- .../frontend/fparser2_char_decln_test.py | 16 +++++++++++++ .../fparser2_subroutine_handler_test.py | 21 +++++++++-------- .../tests/psyir/frontend/fparser2_test.py | 5 ---- .../arrayassignment2loops_trans_test.py | 3 ++- .../datanode_to_temp_trans_test.py | 2 +- ...replace_reference_by_literal_trans_test.py | 15 ++++++------ 10 files changed, 65 insertions(+), 60 deletions(-) diff --git a/src/psyclone/psyir/nodes/array_reference.py b/src/psyclone/psyir/nodes/array_reference.py index 07831516a4..4338804713 100644 --- a/src/psyclone/psyir/nodes/array_reference.py +++ b/src/psyclone/psyir/nodes/array_reference.py @@ -178,7 +178,10 @@ def datatype(self) -> DataType: base_type = UnresolvedType() else: # Create a copy of the base datatype. - base_type = self.symbol.datatype.elemental_type.copy() + if isinstance(self.symbol.datatype, ArrayType): + base_type = self.symbol.datatype.elemental_type.copy() + else: + base_type = self.symbol.datatype.copy() return ArrayType(base_type, shape) # Otherwise, we're accessing a single element of the array. if type(self.symbol) is Symbol: diff --git a/src/psyclone/psyir/nodes/datanode.py b/src/psyclone/psyir/nodes/datanode.py index 1e2c75488c..157564fd9f 100644 --- a/src/psyclone/psyir/nodes/datanode.py +++ b/src/psyclone/psyir/nodes/datanode.py @@ -37,6 +37,8 @@ ''' This module contains the DataNode abstract node implementation.''' +from typing import Optional + from psyclone.psyir.nodes.node import Node @@ -64,28 +66,25 @@ def datatype(self): return INTEGER_TYPE return UnresolvedType() - def is_character(self, unknown_as=None): + def is_character(self, unknown_as: Optional[bool] = None) -> bool: ''' :param unknown_as: Determines behaviour in the case where it cannot be determined whether the DataNode is a character. Defaults to None, in which case an exception is raised. - :type unknown_as: Optional[bool] :returns: True if this DataNode is a character, otherwise False. - :rtype: bool :raises ValueError: if the intrinsic type cannot be determined. ''' - # pylint: disable=import-outside-toplevel - from psyclone.psyir.symbols.datatypes import ScalarType - if not hasattr(self.datatype, "intrinsic"): + dtype = self.datatype + if not hasattr(dtype, "intrinsic"): if unknown_as is None: raise ValueError( "is_character could not resolve whether the expression" f" '{self.debug_string()}' operates on characters." ) return unknown_as - return ( - self.datatype.intrinsic == ScalarType.Intrinsic.CHARACTER - ) + # pylint: disable=import-outside-toplevel + from psyclone.psyir.symbols.datatypes import ScalarType + return dtype.intrinsic == ScalarType.Intrinsic.CHARACTER diff --git a/src/psyclone/psyir/transformations/acc_kernels_trans.py b/src/psyclone/psyir/transformations/acc_kernels_trans.py index 01bd76533b..e25109aa4b 100644 --- a/src/psyclone/psyir/transformations/acc_kernels_trans.py +++ b/src/psyclone/psyir/transformations/acc_kernels_trans.py @@ -49,7 +49,7 @@ ACCEnterDataDirective, ACCKernelsDirective, Assignment, Call, CodeBlock, Literal, Loop, Node, PSyDataNode, Reference, Return, Routine, Statement, WhileLoop) -from psyclone.psyir.symbols import INTEGER_TYPE, UnsupportedFortranType +from psyclone.psyir.symbols import DataTypeSymbol, INTEGER_TYPE, ScalarType from psyclone.psyir.transformations.arrayassignment2loops_trans import ( ArrayAssignment2LoopsTrans) from psyclone.psyir.transformations.region_trans import RegionTrans @@ -250,12 +250,6 @@ def validate( "GOcean InvokeSchedules") super().validate(node_list, options) - # The regex we use to determine whether a character declaration is - # of assumed size ('LEN=*' or '*(*)'). - # TODO #2612 - improve the fparser2 frontend support for character - # declarations. - assumed_size = re.compile(r"\(\s*len\s*=\s*\*\s*\)|\*\s*\(\s*\*\s*\)") - # Construct a list of any symbols that correspond to assumed-size # character strings. These can only be routine arguments. char_syms = [] @@ -263,14 +257,13 @@ def validate( if parent_routine: arg_syms = parent_routine.symbol_table.argument_datasymbols for sym in arg_syms: - # Currently the fparser2 frontend does not support any type - # of LEN= specification on a character variable so we resort - # to a regex to check whether it is assumed-size. - if isinstance(sym.datatype, UnsupportedFortranType): - type_txt = sym.datatype.type_text.lower() - if (type_txt.startswith("character") and - assumed_size.search(type_txt)): - char_syms.append(sym) + if isinstance(sym.datatype, DataTypeSymbol): + continue + if sym.datatype.intrinsic != ScalarType.Intrinsic.CHARACTER: + continue + if isinstance(sym.datatype.length, + ScalarType.CharLengthParameter): + char_syms.append(sym) for node in node_list: # Check that there are no assumed-size character variables as these diff --git a/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py b/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py index bb0c90c578..53e585065e 100644 --- a/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py +++ b/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py @@ -215,11 +215,9 @@ def validate( not have Range specifying the access to at least one of its dimensions. :raises TransformationError: if two or more of the loop ranges - in the assignment are different or are not known to be the - same. + in the assignment are different or are not known to be the same. :raises TransformationError: if node contains a character type - child and the allow_strings option is - not set. + child and the allow_strings option is not set. ''' super().validate(node, **kwargs) @@ -424,19 +422,19 @@ def validate_no_char(node: Node, message: str, verbose: bool) -> None: ''' for child in node.walk((Literal, Reference)): - try: + if 1: #try: forbidden = ScalarType.Intrinsic.CHARACTER - if (child.is_character(unknown_as=False) or - (child.symbol.datatype.intrinsic == forbidden)): + if child.is_character(unknown_as=False): # or + #(child.symbol.datatype.intrinsic == forbidden)): if verbose: node.append_preceding_comment(message) # pylint: disable=cell-var-from-loop raise TransformationError(LazyString( lambda: f"{message}, but found:" f"\n{node.debug_string()}")) - except (NotImplementedError, AttributeError): - # We cannot always get the datatype, we ignore this for now - pass + #except (NotImplementedError, AttributeError): + # # We cannot always get the datatype, we ignore this for now + # pass def _walk_until_non_elemental_call( diff --git a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py index bfe840bf4f..a02525ad61 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py @@ -70,3 +70,19 @@ def test_char_decln_with_char_kind(): processor.process_declarations(fake_parent, [fparser2spec], []) l1_var = symtab.lookup("l1") assert isinstance(l1_var.datatype, UnsupportedFortranType) + + +@pytest.mark.usefixtures("f2008_parser") +def test_char_len_inline(): + ''' + ''' + fake_parent = Routine.create("dummy_schedule") + symtab = fake_parent.symbol_table + processor = Fparser2Reader() + reader = FortranStringReader(f"character :: l1*3") + # Set reader to free format (otherwise this is a comment in fixed format) + reader.set_format(FortranFormat(True, True)) + fparser2spec = Fortran2003.Specification_Part(reader).content[0] + processor.process_declarations(fake_parent, [fparser2spec], []) + l1_var = symtab.lookup("l1") + assert l1_var.datatype.length.value == 3 diff --git a/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py b/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py index 234612e1f4..cbb464f542 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_subroutine_handler_test.py @@ -223,7 +223,9 @@ def test_function_type_prefix(fortran_reader, fortran_writer, assert isinstance(routine, Routine) return_sym = routine.return_symbol assert isinstance(return_sym, DataSymbol) - assert return_sym.datatype.intrinsic == TYPE_MAP_FROM_FORTRAN[basic_type] + # Allow for the "(len=...)" on the end of the character case. + type_name = basic_type.split("(")[0] + assert return_sym.datatype.intrinsic == TYPE_MAP_FROM_FORTRAN[type_name] result = fortran_writer(psyir) assert result == expected @@ -327,8 +329,8 @@ def test_function_unsupported_type(fortran_reader): " my_func = CMPLX(1.0, 1.0)\n" " end function my_func\n" "\n" - " character(len=3) function Agrif_CFixed()\n" - " Agrif_CFixed = '0'\n" + " complex function Agrif_CFixed()\n" + " Agrif_CFixed = (0.0, 1.0)\n" " end function Agrif_CFixed\n" "end module\n") psyir = fortran_reader.psyir_from_source(code) @@ -426,9 +428,9 @@ def test_unsupported_routine_prefix(fortran_reader, fn_prefix, routine_type): assert isinstance(fsym.datatype, UnresolvedType) -def test_unsupported_char_len_function(fortran_reader): - ''' Check that we get a CodeBlock if a Fortran function is of character - type with a specified length. ''' +def test_char_len_function(fortran_reader): + ''' Check that a Fortran function of character type with a specified length + is handled correctly. ''' code = ("module a\n" "contains\n" " character(len=2) function my_func()\n" @@ -437,12 +439,11 @@ def test_unsupported_char_len_function(fortran_reader): " end function my_func\n" "end module\n") psyir = fortran_reader.psyir_from_source(code) - cblock = psyir.children[0].children[0] - assert isinstance(cblock, CodeBlock) - assert "LEN = 2" in str(cblock.get_ast_nodes[0]) + my_func = psyir.children[0].children[0] + assert isinstance(my_func, Routine) fsym = psyir.children[0].symbol_table.lookup("my_func") assert isinstance(fsym, RoutineSymbol) - assert isinstance(fsym.datatype, UnresolvedType) + assert fsym.datatype.length.value == "2" def test_unsupported_contains_subroutine(fortran_reader): diff --git a/src/psyclone/tests/psyir/frontend/fparser2_test.py b/src/psyclone/tests/psyir/frontend/fparser2_test.py index d0fb5621b2..23a63ffce8 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_test.py @@ -844,11 +844,6 @@ def test_process_unsupported_declarations(fortran_reader): "end program") assert isinstance(psyir.children[0].symbol_table.lookup("l").datatype, UnsupportedFortranType) - psyir = fortran_reader.psyir_from_source("program dummy\n" - "character(len=4) :: l\n" - "end program") - assert isinstance(psyir.children[0].symbol_table.lookup("l").datatype, - UnsupportedFortranType) # Test that CodeBlocks and references to variables initialised with a # CodeBlock are handled correctly diff --git a/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py b/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py index 5c6d2b8daa..b495c90d17 100644 --- a/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py @@ -640,8 +640,9 @@ def test_unsupported_type_character(fortran_reader): end subroutine test''' psyir = fortran_reader.psyir_from_source(code) + trans = ArrayAssignment2LoopsTrans() + for assign in psyir.walk(Assignment): - trans = ArrayAssignment2LoopsTrans() with pytest.raises(TransformationError) as info: trans.validate(assign) assert ( diff --git a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py index f362b7c4ec..f40b56e832 100644 --- a/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/datanode_to_temp_trans_test.py @@ -83,7 +83,7 @@ def test_datanodetotemptrans_validate(fortran_reader, tmp_path): "may enable resolution of these symbols." in str(err.value)) code = """subroutine test - character(len=25) :: a, b + complex :: a, b b = a end subroutine test""" diff --git a/src/psyclone/tests/psyir/transformations/replace_reference_by_literal_trans_test.py b/src/psyclone/tests/psyir/transformations/replace_reference_by_literal_trans_test.py index 315745b009..a5fac5a5c8 100644 --- a/src/psyclone/tests/psyir/transformations/replace_reference_by_literal_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/replace_reference_by_literal_trans_test.py @@ -363,14 +363,14 @@ def test_rrbl_code_not_transformed_because_involves_more_than_just_literal( assert "x = b" in written_code -def test_rrbl_annotating_fortran_code_because_str_not_literal( +def test_rrbl_annotating_fortran_code_because_complex_not_literal( fortran_reader, fortran_writer ): """test fortran code annotation with transformation warning""" source = """subroutine foo() - character(len=4), parameter :: a = "toto" - character(len=4):: x + complex, parameter :: a = (1.0, 1.0) + complex :: x x = a end subroutine""" psyir = fortran_reader.psyir_from_source(source) @@ -382,13 +382,12 @@ def test_rrbl_annotating_fortran_code_because_str_not_literal( rbbl.apply(routine_foo) written_code = fortran_writer(routine_foo.ancestor(Container)) assert "x = a" in written_code - assert 'x = "toto"' not in written_code - toto_var_name = '"toto"' + assert written_code.count("x = ") == 1 assert ( f"{rbbl.name}: only " - + "support constant (parameter) but UnsupportedFortranType" - + f"('CHARACTER(LEN = 4), PARAMETER :: a = {toto_var_name}') " - + "is not seen by Psyclone as a constant." + f"support constant (parameter) but UnsupportedFortranType" + f"('COMPLEX, PARAMETER :: a = (1.0, 1.0)') " + f"is not seen by Psyclone as a constant." in written_code ) From 37fda472589c2486479da49f919a4cdfd01e0b6a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 18 Mar 2026 14:54:03 +0000 Subject: [PATCH 07/28] #2612 add support for var*len type declarations [skip ci] --- src/psyclone/psyir/frontend/fparser2.py | 48 +++++++++++-------- .../frontend/fparser2_char_decln_test.py | 12 +++-- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index b1f7601db5..5eb11fddeb 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -45,7 +45,7 @@ import re import os import sys -from typing import Iterable, Optional, Union +from typing import Iterable, Optional, Tuple, Union from fparser.common.readfortran import FortranStringReader from fparser.two import C99Preprocessor, Fortran2003, utils @@ -1586,28 +1586,27 @@ def _process_use_stmts(self, parent, nodes, visibility_map=None): if symbol.name.lower() in visibility_map: symbol.visibility = visibility_map[symbol.name.lower()] - def _process_type_spec(self, parent, type_spec): + def _process_type_spec( + self, + parent: Node, + type_spec: Union[Fortran2003.Intrinsic_Type_Spec, + Fortran2003.Declaration_Type_Spec] + ) -> Tuple[ + Union[ScalarType, DataTypeSymbol], + Union[ScalarType.Precision, DataSymbol, int, None]]: ''' Processes the fparser2 parse tree of a type specification in order to extract the type and precision that are specified. :param parent: the parent of the current PSyIR node under construction. - :type parent: :py:class:`psyclone.psyir.nodes.Node` :param type_spec: the fparser2 parse tree of the type specification. - :type type_spec: \ - :py:class:`fparser.two.Fortran2003.Intrinsic_Type_Spec` or \ - :py:class:`fparser.two.Fortran2003.Declaration_Type_Spec` :returns: the type and precision specified by the type-spec. - :rtype: 2-tuple of :py:class:`psyclone.psyir.symbols.ScalarType` or \ - :py:class:`psyclone.psyir.symbols.DataTypeSymbol` and \ - :py:class:`psyclone.psyir.symbols.DataSymbol.Precision` or \ - :py:class:`psyclone.psyir.symbols.DataSymbol` or int or NoneType :raises NotImplementedError: if an unsupported intrinsic type is found. - :raises SymbolError: if a symbol already exists for the name of a \ + :raises SymbolError: if a symbol already exists for the name of a derived type but is not a DataTypeSymbol. - :raises NotImplementedError: if the supplied type specification is \ + :raises NotImplementedError: if the supplied type specification is not for an intrinsic type or a derived type. ''' @@ -1844,6 +1843,7 @@ def _process_decln( decln_access_spec = None # 6) Whether this declaration has the SAVE attribute. has_save_attr = False + if attr_specs: for attr in attr_specs.items: if isinstance(attr, (Fortran2003.Attr_Spec, @@ -1928,11 +1928,14 @@ def _process_decln( (name, array_spec, char_len, initialisation) = entity.items init_expr = None + # Since specifiers on an individual entity can override those in + # the general declaration, we have to take a copy. + this_type = base_type.copy() + # If the entity has an array-spec shape, it has priority. # Otherwise use the declaration attribute shape. if array_spec is not None: - entity_shape = \ - self._parse_dimensions(array_spec, symbol_table) + entity_shape = self._parse_dimensions(array_spec, symbol_table) else: entity_shape = attribute_shape @@ -1966,9 +1969,15 @@ def _process_decln( init_expr = dummynode.children[0].detach() if char_len is not None: - raise NotImplementedError( - f"Could not process {decl.items}. Character length " - f"specifications are not supported.") + # Handle any character length specification. This takes + # precedence over anything in the declaration attributes + # handled earlier. + if isinstance(char_len, Fortran2003.Char_Length): + # e.g. Char_Length('(', Name('MAX_LEN'), ')') + char_len = char_len.children[1] + dummynode = Assignment(parent=scope) + self.process_nodes(parent=dummynode, nodes=[char_len]) + this_type.length = dummynode.children[0].detach() sym_name = str(name).lower() @@ -2002,10 +2011,10 @@ def _process_decln( if entity_shape: # array - datatype = ArrayType(base_type, entity_shape) + datatype = ArrayType(this_type, entity_shape) else: # scalar - datatype = base_type + datatype = this_type # Make sure the declared symbol exists in the SymbolTable. tag = None @@ -5573,7 +5582,6 @@ def _subroutine_handler(self, node, parent): :returns: PSyIR representation of node. :rtype: :py:class:`psyclone.psyir.nodes.Routine` - :raises NotImplementedError: if the node contains a Contains clause. :raises NotImplementedError: if the node contains an ENTRY statement. :raises NotImplementedError: if an unsupported prefix is found. diff --git a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py index a02525ad61..2a3193c6c1 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py @@ -63,7 +63,7 @@ def test_char_decln_with_char_kind(): fake_parent = Routine.create("dummy_schedule") symtab = fake_parent.symbol_table processor = Fparser2Reader() - reader = FortranStringReader(f"character(len=3, kind=KIND('h')) :: l1") + reader = FortranStringReader("character(len=3, kind=KIND('h')) :: l1") # Set reader to free format (otherwise this is a comment in fixed format) reader.set_format(FortranFormat(True, True)) fparser2spec = Fortran2003.Specification_Part(reader).content[0] @@ -75,14 +75,20 @@ def test_char_decln_with_char_kind(): @pytest.mark.usefixtures("f2008_parser") def test_char_len_inline(): ''' + Check that specifying the character length of an individual entity is + supported and correctly overrides any length in the base declaration. + ''' fake_parent = Routine.create("dummy_schedule") symtab = fake_parent.symbol_table processor = Fparser2Reader() - reader = FortranStringReader(f"character :: l1*3") + reader = FortranStringReader("character*5 :: l1*3, l2*(MAX_LEN), l3") # Set reader to free format (otherwise this is a comment in fixed format) reader.set_format(FortranFormat(True, True)) fparser2spec = Fortran2003.Specification_Part(reader).content[0] processor.process_declarations(fake_parent, [fparser2spec], []) l1_var = symtab.lookup("l1") - assert l1_var.datatype.length.value == 3 + assert l1_var.datatype.length.value == "3" + l2_var = symtab.lookup("l2") + assert l2_var.datatype.length.symbol is symtab.lookup("MAX_LEN") + assert symtab.lookup("l3").datatype.length.value == "5" From 58b9d0b72cfeabf42b943bbc79436bbec4b89aff Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 18 Mar 2026 16:30:34 +0000 Subject: [PATCH 08/28] #2612 allow for colon/asterisk in entity decln [skip ci] --- src/psyclone/psyir/frontend/fparser2.py | 65 ++++++++++--------- .../frontend/fparser2_char_decln_test.py | 5 +- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index 5eb11fddeb..0c3e3a5014 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -1972,12 +1972,9 @@ def _process_decln( # Handle any character length specification. This takes # precedence over anything in the declaration attributes # handled earlier. - if isinstance(char_len, Fortran2003.Char_Length): - # e.g. Char_Length('(', Name('MAX_LEN'), ')') - char_len = char_len.children[1] - dummynode = Assignment(parent=scope) - self.process_nodes(parent=dummynode, nodes=[char_len]) - this_type.length = dummynode.children[0].detach() + clen = self._process_char_length(char_len, scope) + if clen: + this_type.length = clen sym_name = str(name).lower() @@ -2908,11 +2905,13 @@ def _process_precision(self, ) return kind_expression - def _process_char_length(self, - type_spec: Fortran2003.Intrinsic_Type_Spec, - psyir_parent: Node) -> Optional[ - Union[ScalarType.CharLengthParameter, - DataNode]]: + def _process_char_length( + self, + type_spec: Union[Fortran2003.Intrinsic_Type_Spec, + Fortran2003.Int_Literal_Constant, + Fortran2003.Char_Length], + psyir_parent: Node) -> Optional[ + Union[ScalarType.CharLengthParameter, DataNode]]: ''' Process any length attribute on a CHARACTER declaration. @@ -2923,27 +2922,33 @@ def _process_char_length(self, unspecified. ''' - for child in type_spec.children: - if isinstance(child, Fortran2003.Length_Selector): - # Child 0 holds '(' for a '(len=xxx)' or '*' for a - # '* char-length'. Either way, child 1 holds the length. - if isinstance(child.children[1], Fortran2003.Char_Length): - char_len = child.children[1].children[1] - else: - char_len = child.children[1] - break + if isinstance(type_spec, Fortran2003.Intrinsic_Type_Spec): + for child in type_spec.children: + if isinstance(child, Fortran2003.Length_Selector): + # Child 0 holds '(' for a '(len=xxx)' or '*' for a + # '* char-length'. Either way, child 1 holds the length. + if isinstance(child.children[1], Fortran2003.Char_Length): + char_len = child.children[1].children[1] + else: + char_len = child.children[1] + break - if isinstance(child, Fortran2003.Char_Selector): - # A CHARACTER declaration can be of Char_Selector type. - # The first child of Char_Selector holds the length (which - # may be None if it is unspecified). - char_len = child.children[0] - if not char_len: - return None - break + if isinstance(child, Fortran2003.Char_Selector): + # A CHARACTER declaration can be of Char_Selector type. + # The first child of Char_Selector holds the length (which + # may be None if it is unspecified). + char_len = child.children[0] + if not char_len: + return None + break + else: + # No length is specified + return None + elif isinstance(type_spec, Fortran2003.Char_Length): + # e.g. Char_Length('(', Name('MAX_LEN'), ')') + char_len = type_spec.children[1] else: - # No length is specified - return None + char_len = type_spec if isinstance(char_len, Fortran2003.Type_Param_Value): if char_len.string == ":": diff --git a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py index 2a3193c6c1..b14e0f864f 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py @@ -82,7 +82,8 @@ def test_char_len_inline(): fake_parent = Routine.create("dummy_schedule") symtab = fake_parent.symbol_table processor = Fparser2Reader() - reader = FortranStringReader("character*5 :: l1*3, l2*(MAX_LEN), l3") + reader = FortranStringReader( + "character*5 :: l1*3, l2*(MAX_LEN), l3, l4*(:)") # Set reader to free format (otherwise this is a comment in fixed format) reader.set_format(FortranFormat(True, True)) fparser2spec = Fortran2003.Specification_Part(reader).content[0] @@ -92,3 +93,5 @@ def test_char_len_inline(): l2_var = symtab.lookup("l2") assert l2_var.datatype.length.symbol is symtab.lookup("MAX_LEN") assert symtab.lookup("l3").datatype.length.value == "5" + assert (symtab.lookup("l4").datatype.length == + ScalarType.CharLengthParameter.COLON) From e7f9cce47027b0878d005f025cff43546f1e04d9 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Wed, 18 Mar 2026 17:01:11 +0000 Subject: [PATCH 09/28] #2612 fix tests and linting --- src/psyclone/psyir/backend/fortran.py | 37 ++++++++----------- src/psyclone/psyir/frontend/fparser2.py | 8 +++- src/psyclone/psyir/symbols/datatypes.py | 4 +- .../transformations/acc_kernels_trans.py | 1 - .../arrayassignment2loops_trans.py | 20 ++++------ .../transformations/globalstoargs_test.py | 2 +- .../gocean_opencl_trans_test.py | 6 +-- .../frontend/fparser2_char_decln_test.py | 2 +- .../frontend/fparser2_parameter_stmts_test.py | 4 +- .../tests/psyir/frontend/fparser2_test.py | 7 ---- 10 files changed, 37 insertions(+), 54 deletions(-) diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index b7edbe1eee..319717ba92 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -335,6 +335,16 @@ def gen_datatype(self, # ISO_FORTRAN_ENV; type(type64) :: MyType. return f"{fortrantype}*{precision}" + len_str = "" + if scalar_type.intrinsic == ScalarType.Intrinsic.CHARACTER: + # Include length information for a character type. + if scalar_type.length == ScalarType.CharLengthParameter.ASTERISK: + len_str = "*" + elif scalar_type.length == ScalarType.CharLengthParameter.COLON: + len_str = ":" + else: + len_str = self._visit(scalar_type.length) + if isinstance(precision, ScalarType.Precision): # The precision information is not absolute so is either # machine specific or is specified via the compiler. Fortran @@ -347,16 +357,8 @@ def gen_datatype(self, f"ScalarType.Precision.DOUBLE is not supported for " f"datatypes other than floating point numbers in " f"Fortran, found '{fortrantype}'") - if scalar_type.intrinsic == ScalarType.Intrinsic.CHARACTER: - # Include length information. - if (scalar_type.length == - ScalarType.CharLengthParameter.ASTERISK): - len_str = "*" - elif scalar_type.length == ScalarType.CharLengthParameter.COLON: - len_str = ":" - else: - len_str = self._visit(scalar_type.length) - return f"{fortrantype}(len={len_str})" + if len_str: + return f"{fortrantype}(len={len_str})" return fortrantype if isinstance(precision, DataNode): @@ -364,20 +366,11 @@ def gen_datatype(self, raise VisitorError( f"kind not supported for datatype '{fortrantype}' in " f"symbol '{name}' in Fortran backend.") - len_str = "" - if scalar_type.intrinsic == ScalarType.Intrinsic.CHARACTER: - # Include length information. - if (scalar_type.length == - ScalarType.CharLengthParameter.ASTERISK): - len_str = "*" - elif scalar_type.length == ScalarType.CharLengthParameter.COLON: - len_str = ":" - else: - len_str = self._visit(scalar_type.length) - len_str = f", len={len_str}" + if len_str: + len_txt = f", len={len_str}" # The precision information is provided by a parameter, # so use KIND. - return f"{fortrantype}(kind={self._visit(precision)}{len_str})" + return f"{fortrantype}(kind={self._visit(precision)}{len_txt})" raise VisitorError( f"Unsupported precision type '{type(precision).__name__}' found " diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index 0c3e3a5014..9c0f3b5aef 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -1929,8 +1929,8 @@ def _process_decln( init_expr = None # Since specifiers on an individual entity can override those in - # the general declaration, we have to take a copy. - this_type = base_type.copy() + # the general declaration, we may have to take a copy. + this_type = base_type # If the entity has an array-spec shape, it has priority. # Otherwise use the declaration attribute shape. @@ -1974,6 +1974,10 @@ def _process_decln( # handled earlier. clen = self._process_char_length(char_len, scope) if clen: + # copy() does a deep copy but we need to preserve any + # symbols referred to within the type. + this_type = base_type.copy() + this_type.replace_symbols_using(scope.symbol_table) this_type.length = clen sym_name = str(name).lower() diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index 0f150b7b18..4281925ba5 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -388,7 +388,7 @@ class Precision(Enum): DOUBLE = 2 UNDEFINED = 3 - def copy(self) -> Precision: + def copy(self) -> ScalarType.Precision: ''' :returns: a copy of self. ''' @@ -398,7 +398,7 @@ class CharLengthParameter(Enum): ASTERISK = 1 COLON = 2 - def copy(self) -> CharLengthParameter: + def copy(self) -> ScalarType.CharLengthParameter: ''' :returns: a copy of self. ''' diff --git a/src/psyclone/psyir/transformations/acc_kernels_trans.py b/src/psyclone/psyir/transformations/acc_kernels_trans.py index 67d37a34f0..e90ba7e311 100644 --- a/src/psyclone/psyir/transformations/acc_kernels_trans.py +++ b/src/psyclone/psyir/transformations/acc_kernels_trans.py @@ -40,7 +40,6 @@ ''' This module provides the ACCKernelsTrans transformation. ''' -import re from typing import Any, Dict, Union import warnings diff --git a/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py b/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py index 53e585065e..2ac4d26fc6 100644 --- a/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py +++ b/src/psyclone/psyir/transformations/arrayassignment2loops_trans.py @@ -422,19 +422,13 @@ def validate_no_char(node: Node, message: str, verbose: bool) -> None: ''' for child in node.walk((Literal, Reference)): - if 1: #try: - forbidden = ScalarType.Intrinsic.CHARACTER - if child.is_character(unknown_as=False): # or - #(child.symbol.datatype.intrinsic == forbidden)): - if verbose: - node.append_preceding_comment(message) - # pylint: disable=cell-var-from-loop - raise TransformationError(LazyString( - lambda: f"{message}, but found:" - f"\n{node.debug_string()}")) - #except (NotImplementedError, AttributeError): - # # We cannot always get the datatype, we ignore this for now - # pass + if child.is_character(unknown_as=False): + if verbose: + node.append_preceding_comment(message) + # pylint: disable=cell-var-from-loop + raise TransformationError(LazyString( + lambda: f"{message}, but found:" + f"\n{node.debug_string()}")) def _walk_until_non_elemental_call( diff --git a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py index 3f7984f218..f5a15a8ae7 100644 --- a/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/globalstoargs_test.py @@ -270,7 +270,7 @@ def create_data_symbol(arg): trans.apply(kernel) assert ("The imported variable 'rdt' could not be promoted to an argument " "because the GOcean infrastructure does not have any scalar type " - "equivalent to the PSyIR Scalar type." + "equivalent to the PSyIR Scalar Date: Wed, 18 Mar 2026 17:27:13 +0000 Subject: [PATCH 10/28] #2361 really fix the test --- src/psyclone/psyir/backend/fortran.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index 319717ba92..89dc874de6 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -357,8 +357,8 @@ def gen_datatype(self, f"ScalarType.Precision.DOUBLE is not supported for " f"datatypes other than floating point numbers in " f"Fortran, found '{fortrantype}'") - if len_str: - return f"{fortrantype}(len={len_str})" + if len_str: + return f"{fortrantype}(len={len_str})" return fortrantype if isinstance(precision, DataNode): @@ -366,6 +366,8 @@ def gen_datatype(self, raise VisitorError( f"kind not supported for datatype '{fortrantype}' in " f"symbol '{name}' in Fortran backend.") + + len_txt = "" if len_str: len_txt = f", len={len_str}" # The precision information is provided by a parameter, From 3409df4ffd9e8ea1ccf10ab6ad23ead5fa1daa56 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Thu, 19 Mar 2026 18:13:10 +0000 Subject: [PATCH 11/28] #2612 improve test coverage --- src/psyclone/psyir/backend/fortran.py | 8 ---- src/psyclone/psyir/symbols/datatypes.py | 38 +++++++++------- .../psyir/backend/fortran_gen_decls_test.py | 33 ++++++++++++++ .../tests/psyir/symbols/datatype_test.py | 45 +++++++++++++++++++ 4 files changed, 99 insertions(+), 25 deletions(-) diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index 89dc874de6..9ef673b4a0 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -285,9 +285,6 @@ def gen_datatype(self, and this is not supported for the datatype. :raises VisitorError: if the size of the explicit precision is not supported for the datatype. - :raises VisitorError: if the size of the symbol is specified by - another variable and the datatype is not one that supports the - Fortran KIND option. :raises NotImplementedError: if the type of the precision object is an unsupported type. @@ -362,11 +359,6 @@ def gen_datatype(self, return fortrantype if isinstance(precision, DataNode): - if fortrantype not in ["real", "integer", "logical", "character"]: - raise VisitorError( - f"kind not supported for datatype '{fortrantype}' in " - f"symbol '{name}' in Fortran backend.") - len_txt = "" if len_str: len_txt = f", len={len_str}" diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index 4281925ba5..105d8f9257 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -470,35 +470,39 @@ def length(self) -> "DataNode": return self._length @length.setter - def length(self, value: Union[int, str, "DataNode"]): + def length(self, value: Union[int, "DataNode", None]): ''' Setter for the length of a character string. If the new value - is supplied as an int or str then this is converted into a Literal. + is supplied as an int then this is converted into a Literal. + + If this type is a character string and the `value` is None then + the length is set to the Fortran default of 1. :value: the new length to assign. - :raises ValueError: if the supplied value is a str but is not ":" - or "*". + :raises TypeError: if value is not None and this is not a + character type. :raises ValueError: if the supplied value is an int with value < 0. :raises TypeError: if the supplied value is of the wrong type. ''' - if value is None: - if self._intrinsic == ScalarType.Intrinsic.CHARACTER: - # pylint: disable=import-outside-toplevel - from psyclone.psyir.nodes.literal import Literal - # Default length of a character string is 1. - self._length = Literal("1", INTEGER_TYPE) - else: - self._length = None - return - if self._intrinsic != ScalarType.Intrinsic.CHARACTER: + if value is None: + self._length = None + return raise TypeError( - f"Only ScalarTypes of CHARACTER type support the length " + f"Only ScalarTypes of character type support the length " f"property but length '{value}' was supplied to an intrinsic" f" type of '{self._intrinsic}'") + # This is a character type. + if value is None: + # pylint: disable=import-outside-toplevel + from psyclone.psyir.nodes.literal import Literal + # Default length of a character string is 1. + self._length = Literal("1", INTEGER_TYPE) + return + # pylint: disable=import-outside-toplevel from psyclone.psyir.nodes.datanode import DataNode if isinstance(value, ScalarType.CharLengthParameter): @@ -514,8 +518,8 @@ def length(self, value: Union[int, str, "DataNode"]): self._length = value else: raise TypeError( - f"The length property of a CharacterType must be an int, " - f"ScalarType.CharLengthParameter " + f"The length property of a character ScalarType must be an " + f"int, ScalarType.CharLengthParameter " f"or DataNode but got '{type(value).__name__}'") @property diff --git a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py index 9720e7a8e9..fe0760718a 100644 --- a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py @@ -255,6 +255,39 @@ def test_gen_decls(fortran_writer): "'unknown'" in str(excinfo.value)) +def test_gen_decls_char(fortran_writer): + ''' + Test that various forms of character declaration are handled OK. + ''' + table = SymbolTable() + sym1 = DataSymbol("amo", ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, + 4)) + table.add(sym1) + sym2 = DataSymbol("amos", + ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, + ScalarType.CharLengthParameter.COLON)) + table.add(sym2) + sym3 = DataSymbol("amat", + ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, + ScalarType.CharLengthParameter.ASTERISK)) + table.add(sym3) + char_kind = DataSymbol("ckind", INTEGER_TYPE) + table.add(char_kind) + sym4 = DataSymbol("amore", + ScalarType(ScalarType.Intrinsic.CHARACTER, + Reference(char_kind), + ScalarType.CharLengthParameter.ASTERISK)) + table.add(sym4) + result = fortran_writer.gen_decls(table) + assert "character(len=4) :: amo" in result + assert "character(len=:) :: amos" in result + assert "character(len=*) :: amat" in result + assert "character(kind=ckind, len=*) :: amore" in result + + def test_gen_decls_array(fortran_writer): ''' Test that various forms of array declaration are created correctly. diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py index 1a0bf3c2b8..7fe75329b4 100644 --- a/src/psyclone/tests/psyir/symbols/datatype_test.py +++ b/src/psyclone/tests/psyir/symbols/datatype_test.py @@ -196,6 +196,45 @@ def test_scalartype_datasymbol_precision(intrinsic): assert scalar_type == scalar_type2 +def test_scalartype_character_length(): + ''' + Test the length getter and setter of ScalarType. + ''' + data_type = ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, + length=Literal("5", INTEGER_TYPE)) + assert data_type.length.value == "5" + data_type.length = Reference(Symbol("MAX_LEN")) + assert data_type.length.symbol.name == "MAX_LEN" + data_type.length = ScalarType.CharLengthParameter.COLON + assert data_type.length == ScalarType.CharLengthParameter.COLON + + with pytest.raises(ValueError) as err: + data_type.length = -1 + assert ("specified using an int then it must be >= 0 but got: -1" + in str(err.value)) + with pytest.raises(TypeError) as err: + data_type.length = "yes" + assert ("must be an int, ScalarType.CharLengthParameter or DataNode but " + "got 'str'" in str(err.value)) + + # Now test with a non-character type. + non_char = INTEGER_TYPE + # The getter raises an error. + with pytest.raises(TypeError) as err: + _ = non_char.length + assert ("ScalarType of intrinsic type 'Intrinsic.INTEGER' does not have " + "the 'length' property" in str(err.value)) + # The setter does permit a value of None. + non_char.length = None + # The setter rejects a value that is not None. + with pytest.raises(TypeError) as err: + non_char.length = 10 + assert ("character type support the length property but length '10' was " + "supplied to an intrinsic type of 'Intrinsic.INTEGER'" + in str(err.value)) + + def test_scalartype_equal(): ''' Check that ScalarType instances with different precision or intrinsic type @@ -287,6 +326,12 @@ def test_scalartype_str(): data_type = ScalarType(ScalarType.Intrinsic.BOOLEAN, ScalarType.Precision.UNDEFINED) assert str(data_type) == "Scalar" + str_type = ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, + 4) + assert str(str_type) == ( + "Scalar]>") def test_scalartype_immutable(): From 60a92f0333d61598c4790a3006a1eb3a7622d6d2 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Mar 2026 15:15:40 +0000 Subject: [PATCH 12/28] #2612 tidy implementation and improve cov --- src/psyclone/psyir/symbols/datatypes.py | 69 +++++++++---------- .../tests/psyir/symbols/datatype_test.py | 32 +++++++++ 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index 105d8f9257..8254879ced 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -49,12 +49,13 @@ from psyclone.configuration import Config from psyclone.errors import InternalError -if TYPE_CHECKING: - from psyclone.psyir.nodes.datanode import DataNode from psyclone.psyir.commentable_mixin import CommentableMixin from psyclone.psyir.symbols.datasymbol import DataSymbol from psyclone.psyir.symbols.data_type_symbol import DataTypeSymbol from psyclone.psyir.symbols.symbol import Symbol +if TYPE_CHECKING: + from psyclone.psyir.nodes.datanode import DataNode + from psyclone.psyir.symbols import SymbolTable class DataType(metaclass=abc.ABCMeta): @@ -359,17 +360,29 @@ class ScalarType(DataType): '''Describes a scalar datatype (and its precision). :param intrinsic: the intrinsic of this scalar type. - :type intrinsic: :py:class:`pyclone.psyir.datatypes.ScalarType.Intrinsic` :param precision: the precision of this scalar type. - :type precision: :py:class:`psyclone.psyir.symbols.ScalarType.Precision` | - int | :py:class:`psyclone.psyir.nodes.DataNode` + :param length: optionally, the length of a character type. :raises TypeError: if any of the arguments are of the wrong type. :raises ValueError: if any of the argument have unexpected values. ''' - class Intrinsic(Enum): + class ScalarTypeAttribute: + ''' + Provides some common functionality to the various classes that + describe attributes of a ScalarType. + + ''' + def copy(self) -> ScalarType.ScalarTypeAttribute: + ''':returns: a copy of self.''' + return copy.copy(self) + + def debug_string(self) -> str: + ''':returns: the name of the Enum item.''' + return self.name + + class Intrinsic(ScalarTypeAttribute, Enum): '''Enumeration of the different intrinsic scalar datatypes that are supported by the PSyIR. @@ -379,7 +392,7 @@ class Intrinsic(Enum): BOOLEAN = 3 CHARACTER = 4 - class Precision(Enum): + class Precision(ScalarTypeAttribute, Enum): '''Enumeration of the different types of 'default' precision that may be specified for a scalar datatype. @@ -388,25 +401,10 @@ class Precision(Enum): DOUBLE = 2 UNDEFINED = 3 - def copy(self) -> ScalarType.Precision: - ''' - :returns: a copy of self. - ''' - return copy.copy(self) - - class CharLengthParameter(Enum): + class CharLengthParameter(ScalarTypeAttribute, Enum): ASTERISK = 1 COLON = 2 - def copy(self) -> ScalarType.CharLengthParameter: - ''' - :returns: a copy of self. - ''' - return copy.copy(self) - - def debug_string(self) -> str: - return self.name - #: Mapping from PSyIR scalar data types to intrinsic Python types #: ignoring precision. TYPE_MAP_TO_PYTHON = { @@ -415,8 +413,13 @@ def debug_string(self) -> str: Intrinsic.BOOLEAN: bool, Intrinsic.REAL: float} - def __init__(self, intrinsic, precision, - length: Optional[Union[int, str, "DataNode"]] = None): + def __init__( + self, + intrinsic: ScalarType.Intrinsic, + precision: Union[int, ScalarType.Precision, "DataNode"], + length: Optional[ + Union[int, ScalarType.CharLengthParam, "DataNode"]] = None + ): if not isinstance(intrinsic, ScalarType.Intrinsic): raise TypeError( f"ScalarType expected 'intrinsic' argument to be of type " @@ -593,7 +596,9 @@ def __eq__(self, other: Any) -> bool: return precision_match and length_match - def replace_symbols_using(self, table_or_symbol): + def replace_symbols_using( + self, + table_or_symbol: Union[SymbolTable, Symbol]) -> None: ''' Replace any Symbols referred to by this object with those in the supplied SymbolTable (or just the supplied Symbol instance) if they @@ -601,10 +606,7 @@ def replace_symbols_using(self, table_or_symbol): left unchanged. :param table_or_symbol: the symbol table from which to get replacement - symbols or a single, replacement Symbol. - :type table_or_symbol: :py:class:`psyclone.psyir.symbols.SymbolTable` | - :py:class:`psyclone.psyir.symbols.Symbol` - + symbols or a single, replacement Symbol. ''' # pylint: disable=import-outside-toplevel from psyclone.psyir.nodes.datanode import DataNode @@ -632,12 +634,7 @@ def copy(self) -> ScalarType: ''' :returns: a copy of self. ''' - # TODO #3135 After the precision is always either a Precision or - # a DataNode this hasattr check can be removed. - if hasattr(self.precision, "copy"): - precision = self.precision.copy() - else: - precision = self.precision + precision = self.precision.copy() if self._length: return ScalarType(self.intrinsic, precision, self._length.copy()) return ScalarType(self.intrinsic, precision) diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py index 7fe75329b4..b1aeba2e6c 100644 --- a/src/psyclone/tests/psyir/symbols/datatype_test.py +++ b/src/psyclone/tests/psyir/symbols/datatype_test.py @@ -153,6 +153,17 @@ def test_scalartype_enum_precision(intrinsic, precision): assert scalar_type.is_allocatable is False +@pytest.mark.parametrize("attribute", [ScalarType.Precision.DOUBLE, + ScalarType.Intrinsic.BOOLEAN, + ScalarType.CharLengthParameter.COLON]) +def test_scalartypeattribute(attribute): + ''' + Test the debug_string() and copy() methods provided by ScalarTypeAttribute. + ''' + assert attribute.copy() == attribute + assert attribute.debug_string() == attribute.name + + @pytest.mark.parametrize("precision", [1, 8, 16]) @pytest.mark.parametrize("intrinsic", [ScalarType.Intrinsic.INTEGER, ScalarType.Intrinsic.REAL, @@ -208,6 +219,7 @@ def test_scalartype_character_length(): assert data_type.length.symbol.name == "MAX_LEN" data_type.length = ScalarType.CharLengthParameter.COLON assert data_type.length == ScalarType.CharLengthParameter.COLON + assert data_type.length.debug_string() == "COLON" with pytest.raises(ValueError) as err: data_type.length = -1 @@ -367,6 +379,12 @@ def test_scalartype_replace_symbols(): stype2.replace_symbols_using(table) # Precision symbol should have been updated. assert stype2.precision.symbol is rdef2 + # Repeat but for a Symbol used to define the length of a character string + chartype = ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, + Reference(rdef)) + chartype.replace_symbols_using(table) + assert chartype.length.symbol is rdef2 def test_scalartype_get_all_accessed_symbols(): @@ -376,6 +394,11 @@ def test_scalartype_get_all_accessed_symbols(): Reference(rdef)) dependent_symbols = stype2.get_all_accessed_symbols() assert rdef in dependent_symbols + chartype = ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, + Reference(rdef)) + dependent_symbols2 = chartype.get_all_accessed_symbols() + assert rdef in dependent_symbols2 def test_scalartype_copy(): @@ -402,6 +425,15 @@ def test_scalartype_copy(): assert rcopy.precision == stype2.precision assert rcopy.precision is not stype2.precision + chartype = ScalarType(ScalarType.Intrinsic.CHARACTER, + ScalarType.Precision.UNDEFINED, + Reference(rdef)) + ccopy = chartype.copy() + # Length expression has been copied. + assert ccopy.length is not chartype.length + # Referenced Symbol is unchanged. + assert ccopy.length.symbol is rdef + # ArrayType class From c76ec8f3c3067717a47929218cf5990694017e99 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Mar 2026 15:50:22 +0000 Subject: [PATCH 13/28] #2612 allow for int precision --- src/psyclone/psyir/symbols/datatypes.py | 7 ++++++- src/psyclone/tests/psyir/symbols/datatype_test.py | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index 8254879ced..988fd0b6f5 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -634,7 +634,12 @@ def copy(self) -> ScalarType: ''' :returns: a copy of self. ''' - precision = self.precision.copy() + if isinstance(self.precision, int): + # TODO #3135 - ideally precision will always be stored as a + # DataNode and this branch of the `if` won't be necessary. + precision = self.precision + else: + precision = self.precision.copy() if self._length: return ScalarType(self.intrinsic, precision, self._length.copy()) return ScalarType(self.intrinsic, precision) diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py index b1aeba2e6c..d0279a9041 100644 --- a/src/psyclone/tests/psyir/symbols/datatype_test.py +++ b/src/psyclone/tests/psyir/symbols/datatype_test.py @@ -425,6 +425,14 @@ def test_scalartype_copy(): assert rcopy.precision == stype2.precision assert rcopy.precision is not stype2.precision + # Repeat but with precision as an int. + # TODO #3135 - once precision is always stored as a DataNode this separate + # test won't be necessary. + itype = ScalarType(ScalarType.Intrinsic.INTEGER, 4) + icopy = itype.copy() + assert icopy.precision == 4 + + # Test a character type with a length. chartype = ScalarType(ScalarType.Intrinsic.CHARACTER, ScalarType.Precision.UNDEFINED, Reference(rdef)) From 43ffeb17b3fd45dc9510371588ecef8c2c562010 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Mar 2026 16:35:49 +0000 Subject: [PATCH 14/28] #2612 rm unnecessary refinement to copy --- src/psyclone/psyir/frontend/fparser2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index 9c0f3b5aef..2b6f7c5ef1 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -1974,10 +1974,7 @@ def _process_decln( # handled earlier. clen = self._process_char_length(char_len, scope) if clen: - # copy() does a deep copy but we need to preserve any - # symbols referred to within the type. this_type = base_type.copy() - this_type.replace_symbols_using(scope.symbol_table) this_type.length = clen sym_name = str(name).lower() From 866ec47b018f6ee23143025b6d769bc67ecd0ae5 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Mar 2026 16:36:22 +0000 Subject: [PATCH 15/28] #2612 add comment --- src/psyclone/psyir/nodes/array_reference.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/psyclone/psyir/nodes/array_reference.py b/src/psyclone/psyir/nodes/array_reference.py index 4338804713..18180ee7ca 100644 --- a/src/psyclone/psyir/nodes/array_reference.py +++ b/src/psyclone/psyir/nodes/array_reference.py @@ -181,6 +181,8 @@ def datatype(self) -> DataType: if isinstance(self.symbol.datatype, ArrayType): base_type = self.symbol.datatype.elemental_type.copy() else: + # TODO #3240 - sometimes we have an ArrayReference that is + # actually a character sub-string. base_type = self.symbol.datatype.copy() return ArrayType(base_type, shape) # Otherwise, we're accessing a single element of the array. From 3c4b39224d432f9c49dfe74268b51535fb166b04 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Mar 2026 21:23:50 +0000 Subject: [PATCH 16/28] #2612 fix for expressions --- src/psyclone/psyir/backend/fortran.py | 18 +++++++++--------- .../psyir/backend/fortran_gen_decls_test.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index 9ef673b4a0..cb41cec116 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -50,8 +50,8 @@ Fparser2Reader, TYPE_MAP_FROM_FORTRAN) from psyclone.psyir.nodes import ( BinaryOperation, Call, Container, CodeBlock, DataNode, IntrinsicCall, - Literal, Node, OMPDependClause, OMPReductionClause, Operation, Range, - Routine, Schedule, UnaryOperation) + Literal, Member, Node, OMPDependClause, OMPReductionClause, Operation, + Range, Routine, Schedule, UnaryOperation) from psyclone.psyir.symbols import ( ArgumentInterface, ArrayType, ContainerSymbol, DataSymbol, DataType, DataTypeSymbol, GenericInterfaceSymbol, IntrinsicSymbol, @@ -340,7 +340,7 @@ def gen_datatype(self, elif scalar_type.length == ScalarType.CharLengthParameter.COLON: len_str = ":" else: - len_str = self._visit(scalar_type.length) + len_str = self._visit(scalar_type.length).strip() if isinstance(precision, ScalarType.Precision): # The precision information is not absolute so is either @@ -364,7 +364,8 @@ def gen_datatype(self, len_txt = f", len={len_str}" # The precision information is provided by a parameter, # so use KIND. - return f"{fortrantype}(kind={self._visit(precision)}{len_txt})" + return (f"{fortrantype}(kind={self._visit(precision).strip()}" + f"{len_txt})") raise VisitorError( f"Unsupported precision type '{type(precision).__name__}' found " @@ -512,18 +513,17 @@ def gen_use(self, symbol, symbol_table): f"{renames}\n") return f"{self._nindent}use{intrinsic_str}{symbol.name}\n" - def gen_vardecl(self, symbol, include_visibility=False): + def gen_vardecl(self, + symbol: Union[DataSymbol, Member], + include_visibility: bool = False) -> str: '''Create and return the Fortran variable declaration for this Symbol or derived-type member. :param symbol: the symbol or member instance. - :type symbol: :py:class:`psyclone.psyir.symbols.DataSymbol` or - :py:class:`psyclone.psyir.nodes.MemberReference` - :param bool include_visibility: whether to include the visibility of + :param include_visibility: whether to include the visibility of the symbol in the generated declaration (default False). :returns: the Fortran variable declaration as a string. - :rtype: str :raises VisitorError: if the symbol is not typed. :raises VisitorError: if the symbol is of UnresolvedType. diff --git a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py index fe0760718a..ace5a4e39e 100644 --- a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py @@ -281,11 +281,21 @@ def test_gen_decls_char(fortran_writer): Reference(char_kind), ScalarType.CharLengthParameter.ASTERISK)) table.add(sym4) + # When both len and kind are given by expressions. + sym5 = DataSymbol( + "philemon", + ScalarType(ScalarType.Intrinsic.CHARACTER, + IntrinsicCall.create(IntrinsicCall.Intrinsic.KIND, + [Reference(sym4)]), + IntrinsicCall.create(IntrinsicCall.Intrinsic.LEN, + [Reference(sym4)]))) + table.add(sym5) result = fortran_writer.gen_decls(table) assert "character(len=4) :: amo" in result assert "character(len=:) :: amos" in result assert "character(len=*) :: amat" in result assert "character(kind=ckind, len=*) :: amore" in result + assert "character(kind=KIND(amore), len=LEN(amore)) :: philemon" in result def test_gen_decls_array(fortran_writer): From c1bbb7fb5ef1b8c1ae171f9ffe2d009d5d0ebcee Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Mon, 23 Mar 2026 21:32:12 +0000 Subject: [PATCH 17/28] #2612 add missing copyright/license --- .../frontend/fparser2_char_decln_test.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py index bc1e00e24f..6425c372b8 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py @@ -1,3 +1,42 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2026, Science and Technology Facilities Council. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------------------------- +# Author: A. R. Porter +# ----------------------------------------------------------------------------- + +''' Performs pytest tests on the handling of character declarations in the + fparser2 PSyIR frontend. ''' + import pytest from fparser.common.readfortran import FortranStringReader From a2c4c743dc6ab91fdc25af80bf8e3e784c66c8f9 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 24 Mar 2026 11:21:50 +0000 Subject: [PATCH 18/28] #2612 fix bug in handling of UnsupportedFortranType --- .../psyir/transformations/acc_kernels_trans.py | 9 ++++++--- .../transformations/acc_kernels_trans_test.py | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/psyclone/psyir/transformations/acc_kernels_trans.py b/src/psyclone/psyir/transformations/acc_kernels_trans.py index e90ba7e311..39f3cb960a 100644 --- a/src/psyclone/psyir/transformations/acc_kernels_trans.py +++ b/src/psyclone/psyir/transformations/acc_kernels_trans.py @@ -49,7 +49,8 @@ ACCEnterDataDirective, ACCKernelsDirective, Assignment, Call, CodeBlock, Literal, Loop, Node, PSyDataNode, Reference, Return, Routine, Statement, WhileLoop) -from psyclone.psyir.symbols import DataTypeSymbol, INTEGER_TYPE, ScalarType +from psyclone.psyir.symbols import ( + DataTypeSymbol, INTEGER_TYPE, ScalarType, UnsupportedFortranType) from psyclone.psyir.transformations.arrayassignment2loops_trans import ( ArrayAssignment2LoopsTrans) from psyclone.psyir.transformations.region_trans import RegionTrans @@ -271,8 +272,10 @@ def validate( continue if sym.datatype.intrinsic != ScalarType.Intrinsic.CHARACTER: continue - if isinstance(sym.datatype.length, - ScalarType.CharLengthParameter): + dtype = sym.datatype + if isinstance(sym.datatype, UnsupportedFortranType): + dtype = sym.datatype.partial_datatype + if isinstance(dtype.length, ScalarType.CharLengthParameter): char_syms.append(sym) for node in node_list: diff --git a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py index 81a4019069..dce23213e3 100644 --- a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py @@ -46,6 +46,7 @@ from psyclone.psyir.nodes import ( Assignment, ACCKernelsDirective, Loop, Routine ) +from psyclone.psyir.symbols import UnsupportedFortranType from psyclone.psyir.transformations import ( ACCKernelsTrans, TransformationError, ProfileTrans) from psyclone.transformations import ACCEnterDataTrans, ACCLoopTrans @@ -443,10 +444,11 @@ def test_no_assumed_size_char_in_kernels(fortran_reader): ''' code = '''\ -subroutine ice(assumed_size_char, assumed2) +subroutine ice(assumed_size_char, assumed2, assumed3) implicit none character(len = *), intent(in) :: assumed_size_char character*(*) :: assumed2 + character(len=*), optional :: assumed3 character(len=10) :: explicit_size_char real, dimension(10,10) :: my_var @@ -467,7 +469,8 @@ def test_no_assumed_size_char_in_kernels(fortran_reader): explicit_size_char = assumed2 -end + assumed3(:) = '' +end subroutine ice ''' psyir = fortran_reader.psyir_from_source(code) sub = psyir.walk(Routine)[0] @@ -514,6 +517,13 @@ def test_no_assumed_size_char_in_kernels(fortran_reader): assert ("Assumed-size character variables cannot be enclosed in an OpenACC" " region but found 'explicit_size_char = assumed2" in str(err.value)) + # Assumed-size within an UnsupportedFortranType + assert isinstance(sub.symbol_table.lookup("assumed3").datatype, + UnsupportedFortranType) + with pytest.raises(TransformationError) as err: + acc_trans.validate(sub.children[6]) + assert ("Assumed-size character variables cannot be enclosed in an OpenACC" + " region but found 'assumed3(:) = ''" in str(err.value)) def test_check_async_queue_with_enter_data(fortran_reader): From 5c368e1c2813d2ae9982d36861df25fb1e44006c Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 24 Mar 2026 14:13:24 +0000 Subject: [PATCH 19/28] #2612 allow for array of char in acc_kernels_trans --- src/psyclone/psyir/transformations/acc_kernels_trans.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/psyclone/psyir/transformations/acc_kernels_trans.py b/src/psyclone/psyir/transformations/acc_kernels_trans.py index 39f3cb960a..0a6878b79c 100644 --- a/src/psyclone/psyir/transformations/acc_kernels_trans.py +++ b/src/psyclone/psyir/transformations/acc_kernels_trans.py @@ -50,7 +50,7 @@ Call, CodeBlock, Literal, Loop, Node, PSyDataNode, Reference, Return, Routine, Statement, WhileLoop) from psyclone.psyir.symbols import ( - DataTypeSymbol, INTEGER_TYPE, ScalarType, UnsupportedFortranType) + ArrayType, DataTypeSymbol, INTEGER_TYPE, ScalarType, UnsupportedFortranType) from psyclone.psyir.transformations.arrayassignment2loops_trans import ( ArrayAssignment2LoopsTrans) from psyclone.psyir.transformations.region_trans import RegionTrans @@ -275,6 +275,8 @@ def validate( dtype = sym.datatype if isinstance(sym.datatype, UnsupportedFortranType): dtype = sym.datatype.partial_datatype + if isinstance(sym.datatype, ArrayType): + dtype = sym.datatype.elemental_type if isinstance(dtype.length, ScalarType.CharLengthParameter): char_syms.append(sym) From ef200da6cd7c3267db533889715b0e227fe295bf Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 24 Mar 2026 14:31:00 +0000 Subject: [PATCH 20/28] #2612 add test for an argument that is an array of char strings --- .../psyir/transformations/acc_kernels_trans_test.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py index dce23213e3..26cdc69f4e 100644 --- a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py @@ -444,13 +444,14 @@ def test_no_assumed_size_char_in_kernels(fortran_reader): ''' code = '''\ -subroutine ice(assumed_size_char, assumed2, assumed3) +subroutine ice(assumed_size_char, assumed2, assumed3, assumed4) implicit none character(len = *), intent(in) :: assumed_size_char character*(*) :: assumed2 character(len=*), optional :: assumed3 character(len=10) :: explicit_size_char real, dimension(10,10) :: my_var + character(len=*), dimension(:) :: assumed4 if (assumed_size_char == 'literal') then my_var(:UBOUND(my_var)) = 0.0 @@ -470,6 +471,9 @@ def test_no_assumed_size_char_in_kernels(fortran_reader): explicit_size_char = assumed2 assumed3(:) = '' + + assumed4 = '' + end subroutine ice ''' psyir = fortran_reader.psyir_from_source(code) @@ -524,6 +528,11 @@ def test_no_assumed_size_char_in_kernels(fortran_reader): acc_trans.validate(sub.children[6]) assert ("Assumed-size character variables cannot be enclosed in an OpenACC" " region but found 'assumed3(:) = ''" in str(err.value)) + # Array of character strings + with pytest.raises(TransformationError) as err: + acc_trans.validate(sub.children[7]) + assert ("Assumed-size character variables cannot be enclosed in an OpenACC" + " region but found 'assumed4 = ''" in str(err.value)) def test_check_async_queue_with_enter_data(fortran_reader): From e7d6ce14d4919e647ddd6f2b6443a9a994ed230f Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 24 Mar 2026 14:31:57 +0000 Subject: [PATCH 21/28] #2612 linting --- src/psyclone/psyir/transformations/acc_kernels_trans.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/psyclone/psyir/transformations/acc_kernels_trans.py b/src/psyclone/psyir/transformations/acc_kernels_trans.py index 0a6878b79c..7f77f44b77 100644 --- a/src/psyclone/psyir/transformations/acc_kernels_trans.py +++ b/src/psyclone/psyir/transformations/acc_kernels_trans.py @@ -50,7 +50,8 @@ Call, CodeBlock, Literal, Loop, Node, PSyDataNode, Reference, Return, Routine, Statement, WhileLoop) from psyclone.psyir.symbols import ( - ArrayType, DataTypeSymbol, INTEGER_TYPE, ScalarType, UnsupportedFortranType) + ArrayType, DataTypeSymbol, INTEGER_TYPE, ScalarType, + UnsupportedFortranType) from psyclone.psyir.transformations.arrayassignment2loops_trans import ( ArrayAssignment2LoopsTrans) from psyclone.psyir.transformations.region_trans import RegionTrans From b05dbb44a839fff54ed6a756414786e9d721ee37 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 31 Mar 2026 13:39:10 +0100 Subject: [PATCH 22/28] #2612 attempt to fix bug in check for char length --- .../psyir/transformations/acc_kernels_trans.py | 14 +++++++------- .../transformations/acc_kernels_trans_test.py | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/psyclone/psyir/transformations/acc_kernels_trans.py b/src/psyclone/psyir/transformations/acc_kernels_trans.py index 7f77f44b77..192f4691e1 100644 --- a/src/psyclone/psyir/transformations/acc_kernels_trans.py +++ b/src/psyclone/psyir/transformations/acc_kernels_trans.py @@ -269,15 +269,15 @@ def validate( if parent_routine: arg_syms = parent_routine.symbol_table.argument_datasymbols for sym in arg_syms: - if isinstance(sym.datatype, DataTypeSymbol): + dtype = sym.datatype + if isinstance(dtype, UnsupportedFortranType): + dtype = dtype.partial_datatype + if isinstance(dtype, DataTypeSymbol): continue - if sym.datatype.intrinsic != ScalarType.Intrinsic.CHARACTER: + if dtype.intrinsic != ScalarType.Intrinsic.CHARACTER: continue - dtype = sym.datatype - if isinstance(sym.datatype, UnsupportedFortranType): - dtype = sym.datatype.partial_datatype - if isinstance(sym.datatype, ArrayType): - dtype = sym.datatype.elemental_type + if isinstance(dtype, ArrayType): + dtype = dtype.elemental_type if isinstance(dtype.length, ScalarType.CharLengthParameter): char_syms.append(sym) diff --git a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py index 26cdc69f4e..558b0a5154 100644 --- a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py @@ -44,7 +44,7 @@ from psyclone.errors import GenerationError from psyclone.psyir.nodes import ( - Assignment, ACCKernelsDirective, Loop, Routine + Assignment, ACCKernelsDirective, Loop, Reference, Routine ) from psyclone.psyir.symbols import UnsupportedFortranType from psyclone.psyir.transformations import ( @@ -443,9 +443,17 @@ def test_no_assumed_size_char_in_kernels(fortran_reader): or intrinsics that aren't available on GPU. ''' + # A routine with some quite complex argument types to check the various + # branches of the code that finds out whether there's a character length + # specified. code = '''\ -subroutine ice(assumed_size_char, assumed2, assumed3, assumed4) +subroutine ice(dtype, dtype_ptr, type_list, assumed_size_char, assumed2, & + assumed3, assumed4) + use some_mod, only: a_type implicit none + type(a_type) :: dtype + type(d_type), pointer :: dtype_ptr + type(a_type), dimension(10) :: type_list character(len = *), intent(in) :: assumed_size_char character*(*) :: assumed2 character(len=*), optional :: assumed3 @@ -545,13 +553,17 @@ def test_check_async_queue_with_enter_data(fortran_reader): "or bool, got : 3.5" in str(err.value)) psyir = fortran_reader.psyir_from_source( "program two_loops\n" - " integer :: ji\n" + " integer :: ji, aqueue\n" " real :: array(10,10)\n" " do ji = 1, 5\n" " array(ji,1) = 2.0*array(ji,2)\n" " end do\n" "end program two_loops\n") prog = psyir.walk(Routine)[0] + # Check that we can supply a bool or a Reference to specify the queue. + acc_trans.check_async_queue(prog.walk(Loop), True) + acc_trans.check_async_queue(prog.walk(Loop), + Reference(prog.symbol_table.lookup("aqueue"))) # TODO #2668 deprecate options coverage. This test is left for options # coverage acc_edata_trans.apply(prog, options={"async_queue": 1}) From 15b53312c83e6a8c24f039db29553929c2bc2f5e Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Tue, 31 Mar 2026 14:19:54 +0100 Subject: [PATCH 23/28] #2612 update dev guide on supported PSyIR types --- doc/developer_guide/psyir_symbols.rst | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/doc/developer_guide/psyir_symbols.rst b/doc/developer_guide/psyir_symbols.rst index 2a5f56254e..22a3cefbeb 100644 --- a/doc/developer_guide/psyir_symbols.rst +++ b/doc/developer_guide/psyir_symbols.rst @@ -80,8 +80,8 @@ explicitly listed may be assumed to be unsupported): +======================+====================+====================+ |Variables |ALLOCATABLE |CLASS | +----------------------+--------------------+--------------------+ -| |CHARACTER, DOUBLE |COMPLEX, CHARACTER | -| |PRECISION, INTEGER, |with LEN or KIND | +| |CHARACTER, DOUBLE |COMPLEX | +| |PRECISION, INTEGER, | | | |LOGICAL, REAL | | +----------------------+--------------------+--------------------+ | |Derived Types |'extends', | @@ -89,10 +89,7 @@ explicitly listed may be assumed to be unsupported): | | |CONTAINS; Operator | | | |overloading | +----------------------+--------------------+--------------------+ -| |DIMENSION |Array extents | -| | |specified using | -| | |expressions; | -| | |Assumed-size arrays | +| |DIMENSION |Assumed-size arrays | +----------------------+--------------------+--------------------+ | |INTENT, PARAMETER, |VOLATILE, VALUE, | | |SAVE |POINTER | @@ -102,8 +99,8 @@ explicitly listed may be assumed to be unsupported): +----------------------+--------------------+--------------------+ | |PUBLIC, PRIVATE | | +----------------------+--------------------+--------------------+ -|Initialisation |Explicit | | -|expressions |initialisation | | +|Initialisation |Explicit | Implicit loops, | +|expressions |initialisation | array constructors | +----------------------+--------------------+--------------------+ | |Data statements | | | |(limited) | | From 46f2c0f10cbfea4dba5d7b9cbb4bb7663708b457 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 17 Apr 2026 14:31:44 +0100 Subject: [PATCH 24/28] #2612 updates for review --- src/psyclone/psyir/nodes/intrinsic_call.py | 6 ++++-- .../tests/psyir/nodes/array_reference_test.py | 15 ++++++++++++--- .../transformations/acc_kernels_trans_test.py | 5 +---- .../arrayassignment2loops_trans_test.py | 4 ++-- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/psyclone/psyir/nodes/intrinsic_call.py b/src/psyclone/psyir/nodes/intrinsic_call.py index cea33cea06..9b132ca149 100644 --- a/src/psyclone/psyir/nodes/intrinsic_call.py +++ b/src/psyclone/psyir/nodes/intrinsic_call.py @@ -791,7 +791,8 @@ class Intrinsic(IAttr, Enum): types=DataNode, arg_names=(("string",),)), optional_args={}, - # TODO 2612 This may be more complex if we support character len + # Returned string is of the same length as the input (trailing + # spaces are added as needed). return_type=lambda node: _type_of_named_argument(node, "string"), reference_accesses=lambda node: ( _compute_reference_accesses( @@ -810,7 +811,8 @@ class Intrinsic(IAttr, Enum): types=DataNode, arg_names=(("string",),)), optional_args={}, - # TODO 2612 This may be more complex if we support character len + # Returned string is of the same length as the input (leading + # spaces are added as needed). return_type=lambda node: _type_of_named_argument(node, "string"), reference_accesses=lambda node: ( _compute_reference_accesses( diff --git a/src/psyclone/tests/psyir/nodes/array_reference_test.py b/src/psyclone/tests/psyir/nodes/array_reference_test.py index 66a9fbf5ac..c7e0f9f911 100644 --- a/src/psyclone/tests/psyir/nodes/array_reference_test.py +++ b/src/psyclone/tests/psyir/nodes/array_reference_test.py @@ -47,9 +47,9 @@ Reference, ArrayReference, Assignment, Literal, BinaryOperation, Range, KernelSchedule, IntrinsicCall) from psyclone.psyir.symbols import ( - ArrayType, DataSymbol, DataTypeSymbol, UnresolvedType, ScalarType, + ArrayType, DataSymbol, DataTypeSymbol, ScalarType, REAL_SINGLE_TYPE, INTEGER_SINGLE_TYPE, REAL_TYPE, Symbol, INTEGER_TYPE, - UnsupportedFortranType, StructureType) + UnsupportedFortranType, StructureType, UnresolvedType) from psyclone.tests.utilities import check_links @@ -479,6 +479,7 @@ def test_array_datatype(fortran_reader): real, dimension(10) :: test real, dimension(10, 8) :: test_2d real, dimension(3:) :: test3 + character(len=10) :: my_string, string2 real :: thing thing = test(1) @@ -486,7 +487,7 @@ def test_array_datatype(fortran_reader): thing = test_2d(2, 2:6:2) thing = test3(:) thing = test_2d(:, 1) - + string2 = my_string(1:10) end subroutine code""" psyir = fortran_reader.psyir_from_source(code) @@ -540,6 +541,14 @@ def test_array_datatype(fortran_reader): assert dtype.shape[0].lower.value == "1" assert dtype.shape[0].upper.value == "10" + # Character sub-strings are currently mis-identified as array + # ranges (TODO #3240): + # my_string(1:10) + dref = refs[5] + dtype = dref.datatype + assert isinstance(dtype, ArrayType) + assert dtype.intrinsic is ScalarType.Intrinsic.CHARACTER + # Reference to a single element of an array of structures. one = Literal("1", INTEGER_TYPE) two = Literal("2", INTEGER_TYPE) diff --git a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py index 80664b51b3..e8b8281650 100644 --- a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py @@ -447,13 +447,10 @@ def test_no_assumed_size_char_in_kernels(fortran_reader): # branches of the code that finds out whether there's a character length # specified. code = '''\ -subroutine ice(dtype, dtype_ptr, type_list, assumed_size_char, assumed2, & - assumed3, assumed4) +subroutine ice(dtype, assumed_size_char, assumed2, assumed3, assumed4) use some_mod, only: a_type implicit none type(a_type) :: dtype - type(d_type), pointer :: dtype_ptr - type(a_type), dimension(10) :: type_list character(len = *), intent(in) :: assumed_size_char character*(*) :: assumed2 character(len=*), optional :: assumed3 diff --git a/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py b/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py index b495c90d17..5c3154ffd1 100644 --- a/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/arrayassignment2loops_trans_test.py @@ -627,8 +627,8 @@ def test_character_validation(fortran_reader): def test_unsupported_type_character(fortran_reader): ''' Test that the check for character references inside the assignment - being transformed also works with 'unsupported characters arrays' (see - issue #2612). + being transformed also works with character substrings. + ''' code = '''subroutine test() character(LEN=100) :: a From 6a1a9a2697f6748830001e5556ca56caf58e7d5a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 17 Apr 2026 14:36:19 +0100 Subject: [PATCH 25/28] #2612 use more meaningful names for character length parameters --- src/psyclone/psyir/backend/fortran.py | 4 +-- src/psyclone/psyir/frontend/fparser2.py | 4 +-- src/psyclone/psyir/symbols/datatypes.py | 24 +++++++++----- .../psyir/backend/fortran_gen_decls_test.py | 6 ++-- .../fortran_unsupported_declns_test.py | 6 ++-- .../frontend/fparser2_char_decln_test.py | 17 +++++----- .../tests/psyir/frontend/fparser2_test.py | 33 +++++++++++++++++++ .../tests/psyir/symbols/datatype_test.py | 13 ++++---- 8 files changed, 76 insertions(+), 31 deletions(-) diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index 2a4b94507f..70f0da66b5 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -335,9 +335,9 @@ def gen_datatype(self, len_str = "" if scalar_type.intrinsic == ScalarType.Intrinsic.CHARACTER: # Include length information for a character type. - if scalar_type.length == ScalarType.CharLengthParameter.ASTERISK: + if scalar_type.length == ScalarType.CharLengthParameter.ASSUMED: len_str = "*" - elif scalar_type.length == ScalarType.CharLengthParameter.COLON: + elif scalar_type.length == ScalarType.CharLengthParameter.DEFERRED: len_str = ":" else: len_str = self._visit(scalar_type.length).strip() diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index cb887ddb96..f5660a854e 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -3072,8 +3072,8 @@ def _process_char_length( if isinstance(char_len, Fortran2003.Type_Param_Value): if char_len.string == ":": - return ScalarType.CharLengthParameter.COLON - return ScalarType.CharLengthParameter.ASTERISK + return ScalarType.CharLengthParameter.DEFERRED + return ScalarType.CharLengthParameter.ASSUMED # Create a dummy assignment so we can process the length expression. dummy = Assignment(parent=psyir_parent) diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index 988fd0b6f5..02eb0162e5 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -368,7 +368,7 @@ class ScalarType(DataType): ''' - class ScalarTypeAttribute: + class ScalarTypeAttribute(Enum): ''' Provides some common functionality to the various classes that describe attributes of a ScalarType. @@ -382,7 +382,7 @@ def debug_string(self) -> str: ''':returns: the name of the Enum item.''' return self.name - class Intrinsic(ScalarTypeAttribute, Enum): + class Intrinsic(ScalarTypeAttribute): '''Enumeration of the different intrinsic scalar datatypes that are supported by the PSyIR. @@ -392,7 +392,7 @@ class Intrinsic(ScalarTypeAttribute, Enum): BOOLEAN = 3 CHARACTER = 4 - class Precision(ScalarTypeAttribute, Enum): + class Precision(ScalarTypeAttribute): '''Enumeration of the different types of 'default' precision that may be specified for a scalar datatype. @@ -401,9 +401,17 @@ class Precision(ScalarTypeAttribute, Enum): DOUBLE = 2 UNDEFINED = 3 - class CharLengthParameter(ScalarTypeAttribute, Enum): - ASTERISK = 1 - COLON = 2 + class CharLengthParameter(ScalarTypeAttribute): + '''Enumeration of different length characteristics that a character + type may have. + + ''' + #: The length is defined by some other variable. In Fortran + ## this is indicated with an asterisk. + ASSUMED = 1 + #: The length can change during program execution. In Fortran this + ## is indicated with a colon. + DEFERRED = 2 #: Mapping from PSyIR scalar data types to intrinsic Python types #: ignoring precision. @@ -521,8 +529,8 @@ def length(self, value: Union[int, "DataNode", None]): self._length = value else: raise TypeError( - f"The length property of a character ScalarType must be an " - f"int, ScalarType.CharLengthParameter " + f"The length property of a character ScalarType must be a non-" + f"negative int, ScalarType.CharLengthParameter " f"or DataNode but got '{type(value).__name__}'") @property diff --git a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py index ace5a4e39e..2eed1ca3f5 100644 --- a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py @@ -267,19 +267,19 @@ def test_gen_decls_char(fortran_writer): sym2 = DataSymbol("amos", ScalarType(ScalarType.Intrinsic.CHARACTER, ScalarType.Precision.UNDEFINED, - ScalarType.CharLengthParameter.COLON)) + ScalarType.CharLengthParameter.DEFERRED)) table.add(sym2) sym3 = DataSymbol("amat", ScalarType(ScalarType.Intrinsic.CHARACTER, ScalarType.Precision.UNDEFINED, - ScalarType.CharLengthParameter.ASTERISK)) + ScalarType.CharLengthParameter.ASSUMED)) table.add(sym3) char_kind = DataSymbol("ckind", INTEGER_TYPE) table.add(char_kind) sym4 = DataSymbol("amore", ScalarType(ScalarType.Intrinsic.CHARACTER, Reference(char_kind), - ScalarType.CharLengthParameter.ASTERISK)) + ScalarType.CharLengthParameter.ASSUMED)) table.add(sym4) # When both len and kind are given by expressions. sym5 = DataSymbol( diff --git a/src/psyclone/tests/psyir/backend/fortran_unsupported_declns_test.py b/src/psyclone/tests/psyir/backend/fortran_unsupported_declns_test.py index 0c7f66e471..6de4d55ae7 100644 --- a/src/psyclone/tests/psyir/backend/fortran_unsupported_declns_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_unsupported_declns_test.py @@ -263,8 +263,10 @@ def test_generating_unsupportedtype_routine_imports( use a_mod, only: unsupported_type_symbol contains subroutine test() - integer :: a - a = INT(REAL(unsupported_type_symbol())) + real :: a + ! The function call returns a complex number and we want + ! to store a simple scalar. + a = REAL(unsupported_type_symbol()) end subroutine test end module test_mod ''') diff --git a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py index 6425c372b8..85cdae2c69 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_char_decln_test.py @@ -61,16 +61,17 @@ ScalarType.Precision.UNDEFINED), ("*(2*max_len)", "2 * max_len", ScalarType.Precision.UNDEFINED), - ("(len=:)", "COLON", ScalarType.Precision.UNDEFINED), - ("(:)", "COLON", ScalarType.Precision.UNDEFINED), - ("*(:)", "COLON", ScalarType.Precision.UNDEFINED), - ("(len=*)", "ASTERISK", + ("(len=:)", "DEFERRED", ScalarType.Precision.UNDEFINED), - ("(*)", "ASTERISK", ScalarType.Precision.UNDEFINED), - ("*(*)", "ASTERISK", ScalarType.Precision.UNDEFINED), + ("(:)", "DEFERRED", ScalarType.Precision.UNDEFINED), + ("*(:)", "DEFERRED", ScalarType.Precision.UNDEFINED), + ("(len=*)", "ASSUMED", + ScalarType.Precision.UNDEFINED), + ("(*)", "ASSUMED", ScalarType.Precision.UNDEFINED), + ("*(*)", "ASSUMED", ScalarType.Precision.UNDEFINED), ("(len=3, kind=ckind)", "3", Reference(Symbol("ckind"))), - ("(len=*, kind=ckind)", "ASTERISK", + ("(len=*, kind=ckind)", "ASSUMED", Reference(Symbol("ckind")))]) def test_char_decln_length_handling(len_expr, length, kind): ''' @@ -133,4 +134,4 @@ def test_char_len_inline(): assert l2_var.datatype.length.symbol is symtab.lookup("MAX_LEN") assert symtab.lookup("l3").datatype.length.value == "5" assert (symtab.lookup("l4").datatype.length == - ScalarType.CharLengthParameter.COLON) + ScalarType.CharLengthParameter.DEFERRED) diff --git a/src/psyclone/tests/psyir/frontend/fparser2_test.py b/src/psyclone/tests/psyir/frontend/fparser2_test.py index 124a370c73..7dd3b84eff 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_test.py @@ -707,9 +707,11 @@ def test_declarations_with_initialisations(fortran_reader): psyir = fortran_reader.psyir_from_source( """ module test + implicit none integer :: a = 1, aa = 4 integer, save :: b = 1 integer, parameter :: c = 1 + integer, parameter :: MAXDIM = 4 contains subroutine mysub() integer :: d = 1 @@ -749,6 +751,37 @@ def test_declarations_with_initialisations(fortran_reader): assert fsym.is_constant is True +@pytest.mark.usefixtures("f2008_parser") +def test_array_declarations_with_initialisations(fortran_reader): + '''Test that Fparser2Reader keeps variable initialisation + expressions for arrays. + + ''' + psyir = fortran_reader.psyir_from_source( + """ + module test + implicit none + integer, parameter :: MAXDIM = 4 + contains + subroutine mysub() + integer, dimension(3) :: g = (/1, 2, 3/) + integer :: i + integer, dimension(MAXDIM) :: h = (/ (i, i=1,MAXDIM) /) + integer, dimension(2,2) :: l = MAXDIM + end subroutine mysub + end module test + """) + inner_st = psyir.walk(Routine)[0].symbol_table + gsym = inner_st.lookup('g') + hsym = inner_st.lookup('h') + all_syms = [gsym, hsym] + + assert all(isinstance(sym, DataSymbol) for sym in all_syms) + assert all(isinstance(sym.initial_value, CodeBlock) for sym in all_syms) + lsym = inner_st.lookup('l') + assert isinstance(lsym.initial_value, Reference) + + @pytest.mark.usefixtures("f2008_parser") def test_process_declarations_accessibility(): ''' Check that process_declarations behaves as expected when a visibility diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py index d0279a9041..05153f7386 100644 --- a/src/psyclone/tests/psyir/symbols/datatype_test.py +++ b/src/psyclone/tests/psyir/symbols/datatype_test.py @@ -153,9 +153,10 @@ def test_scalartype_enum_precision(intrinsic, precision): assert scalar_type.is_allocatable is False -@pytest.mark.parametrize("attribute", [ScalarType.Precision.DOUBLE, - ScalarType.Intrinsic.BOOLEAN, - ScalarType.CharLengthParameter.COLON]) +@pytest.mark.parametrize("attribute", + [ScalarType.Precision.DOUBLE, + ScalarType.Intrinsic.BOOLEAN, + ScalarType.CharLengthParameter.DEFERRED]) def test_scalartypeattribute(attribute): ''' Test the debug_string() and copy() methods provided by ScalarTypeAttribute. @@ -217,9 +218,9 @@ def test_scalartype_character_length(): assert data_type.length.value == "5" data_type.length = Reference(Symbol("MAX_LEN")) assert data_type.length.symbol.name == "MAX_LEN" - data_type.length = ScalarType.CharLengthParameter.COLON - assert data_type.length == ScalarType.CharLengthParameter.COLON - assert data_type.length.debug_string() == "COLON" + data_type.length = ScalarType.CharLengthParameter.DEFERRED + assert data_type.length == ScalarType.CharLengthParameter.DEFERRED + assert data_type.length.debug_string() == "DEFERRED" with pytest.raises(ValueError) as err: data_type.length = -1 From ba915a591bb6c0c56a5448e5ff22063a59cd603a Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 17 Apr 2026 14:58:50 +0100 Subject: [PATCH 26/28] #2612 fix failing test --- src/psyclone/tests/psyir/symbols/datatype_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py index 05153f7386..468a059809 100644 --- a/src/psyclone/tests/psyir/symbols/datatype_test.py +++ b/src/psyclone/tests/psyir/symbols/datatype_test.py @@ -228,8 +228,8 @@ def test_scalartype_character_length(): in str(err.value)) with pytest.raises(TypeError) as err: data_type.length = "yes" - assert ("must be an int, ScalarType.CharLengthParameter or DataNode but " - "got 'str'" in str(err.value)) + assert ("must be a non-negative int, ScalarType.CharLengthParameter or " + "DataNode but got 'str'" in str(err.value)) # Now test with a non-character type. non_char = INTEGER_TYPE From b9bb6a69095b9eb6edc9c46aba2b5b1b38bc75e6 Mon Sep 17 00:00:00 2001 From: Andrew Porter Date: Fri, 17 Apr 2026 16:04:46 +0100 Subject: [PATCH 27/28] #2612 fix failure when partial_datatype is None and re-instate ACCKernelsTrans tests for coverage --- .../psyir/transformations/acc_kernels_trans.py | 2 ++ .../transformations/acc_kernels_trans_test.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/psyclone/psyir/transformations/acc_kernels_trans.py b/src/psyclone/psyir/transformations/acc_kernels_trans.py index 192f4691e1..cb84e5d56f 100644 --- a/src/psyclone/psyir/transformations/acc_kernels_trans.py +++ b/src/psyclone/psyir/transformations/acc_kernels_trans.py @@ -272,6 +272,8 @@ def validate( dtype = sym.datatype if isinstance(dtype, UnsupportedFortranType): dtype = dtype.partial_datatype + if not dtype: + continue if isinstance(dtype, DataTypeSymbol): continue if dtype.intrinsic != ScalarType.Intrinsic.CHARACTER: diff --git a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py index e8b8281650..04692773be 100644 --- a/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/acc_kernels_trans_test.py @@ -445,18 +445,28 @@ def test_no_assumed_size_char_in_kernels(fortran_reader): ''' # A routine with some quite complex argument types to check the various # branches of the code that finds out whether there's a character length - # specified. + # specified. Although some of these arguments aren't actually used in + # the subroutine, they are needed for test coverage. code = '''\ -subroutine ice(dtype, assumed_size_char, assumed2, assumed3, assumed4) +subroutine ice(dtype, dtype_ptr, type_list, assumed_size_char, assumed2, & + assumed3, assumed4, ctype) use some_mod, only: a_type implicit none type(a_type) :: dtype + ! An unsupported datatype which has a partial_datatype that is a + ! DataTypeSymbol. + type(d_type), pointer :: dtype_ptr + ! An unsupported datatype which has a partial_datatype that is an + ! array of DataTypeSymbol. + type(a_type), dimension(10) :: type_list character(len = *), intent(in) :: assumed_size_char character*(*) :: assumed2 character(len=*), optional :: assumed3 character(len=10) :: explicit_size_char real, dimension(10,10) :: my_var character(len=*), dimension(:) :: assumed4 + ! An unsupported declaration for which we have no partial_datatype + complex :: ctype if (assumed_size_char == 'literal') then my_var(:UBOUND(my_var)) = 0.0 From b3553c8ee7aa3f714aca4787604d39f607ce43e2 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Mon, 20 Apr 2026 14:06:05 +0100 Subject: [PATCH 28/28] #2612 Update changelog --- changelog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog b/changelog index eff9ed4a04..3f43a5e0c0 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,5 @@ + 5) PR #3377 for #2612. Add support for Fortran character length. + 4) PR #3388 for #3334. Remove the functionality to write (and rename) kernel files from the invoke. Now to modify kernels they must be inlinded first or modified directy in their file (e.g. with lfric transmute pass).