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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Modifications by (in alphabetical order):
* P. Vitt, University of Siegen, Germany
* A. Voysey, UK Met Office

18/08/2026 PR #526 for #484. Add the 'format-missing-comma' extension: accept a
missing comma between a character-string edit descriptor and a
neighbouring format item, as many compilers do.

16/07/2026 PR #520 for #519. Fix truncation of a character length expression that
contains a comma (e.g. the arguments to an intrinsic such as
MAX) when the kind appears before the length in a char-selector.
Expand Down
17 changes: 17 additions & 0 deletions doc/source/fparser2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,23 @@ omitted, the value is implicitly assumed to be one. For example::
For more information see
https://gcc.gnu.org/onlinedocs/gfortran/X-format-descriptor-without-count-field.html

Missing Comma in Format Specifications
++++++++++++++++++++++++++++++++++++++

Standard Fortran only permits the comma separating format items to be
omitted in a small number of situations (constraint C1002). However,
many compilers (e.g. gfortran, ifort, ifx) additionally accept a
missing comma between a character-string edit descriptor and a
neighbouring format item. For example::

100 format('a' 1x,'b')
200 format(15x'a')
300 format('a' 'b')

The 'format-missing-comma' extension adds support in fparser for this
relaxation. Note that when such a format specification is re-generated
from the parse tree the omitted commas are re-introduced.

Hollerith Constant
++++++++++++++++++

Expand Down
111 changes: 111 additions & 0 deletions src/fparser/two/Fortran2003.py
Original file line number Diff line number Diff line change
Expand Up @@ -10039,6 +10039,35 @@ def skip_digits(string):
return found, index


def split_leading_char_literal(string):
"""Splits a string that starts with a character literal into the
literal and the remainder, honouring doubled (escaped) quotes
inside the literal.

:param str string: the string to split.

:returns: a 2-tuple containing the character literal and the \
remainder of the string, or None if the string does not start \
with a complete character literal.
:rtype: Optional[Tuple[str, str]]

"""
if not string or string[0] not in "'\"":
return None
quote = string[0]
index = 1
while index < len(string):
if string[index] == quote:
if index + 1 < len(string) and string[index + 1] == quote:
# A doubled quote is an escaped quote, still inside the
# literal.
index += 2
continue
return string[: index + 1], string[index + 1 :]
index += 1
return None


class Format_Item_C1002(Base): # pylint: disable=invalid-name
"""
Fortran 2003 constraint C1002::
Expand All @@ -10063,13 +10092,49 @@ class Format_Item_C1002(Base): # pylint: disable=invalid-name

(4) Before or after a colon edit descriptor.

If 'format-missing-comma' is specified in the EXTENSIONS list then
the comma may additionally be omitted between a character-string
edit descriptor and any neighbouring format item, e.g.
FORMAT('a' 1x,'b'), FORMAT(15x'a') or FORMAT('a' 'b'), as accepted
by many compilers (e.g. gfortran, ifort, ifx).

"""

subclass_names = []
use_names = ["K", "W", "D", "E", "Format_Item", "R"]

@staticmethod
def match(string):
"""Implements the matching for the C1002 Format Item constraint,
optionally relaxed by the 'format-missing-comma' extension.

:param str string: The string to check for conformance with a \
C1002 format item constraint.
:return: `None` if there is no match, otherwise a tuple of \
size 2 containing a mixture of Control_Edit_Descriptor and \
Format_Item classes depending on what has been matched.

:rtype: `NoneType` or ( \
:py:class:`fparser.two.Control_Edit_Desc`, \
:py:class:`fparser.two.Format_Item` ) or \
(:py:class:`fparser.two.Format_Item`, \
:py:class:`fparser.two.Control_Edit_Desc`) or \
(:py:class:`fparser.two.Format_Item`, \
:py:class:`fparser.two.Format_Item`)

"""
try:
result = Format_Item_C1002._standard_match(string)
except NoMatchError:
result = None
if result:
return result
if "format-missing-comma" not in EXTENSIONS():
return None
return Format_Item_C1002._extension_match(string)

@staticmethod
def _standard_match(string):
"""Implements the matching for the C1002 Format Item constraint. The
constraints specify certain combinations of format items that
do not need a comma to separate them. Rather than sorting this
Expand Down Expand Up @@ -10171,6 +10236,52 @@ def match(string):
Format_Item(option + repmap(right.lstrip())),
)

return None

@staticmethod
def _extension_match(string):
"""Implements the matching for the 'format-missing-comma'
extension. Various compilers (e.g. gfortran, ifort, ifx) accept
a missing comma between a character-string edit descriptor and
the neighbouring format item, e.g. FORMAT('a' 1x,'b'),
FORMAT(15x'a') or FORMAT('a' 'b'). The item is split at the
boundary of the first character literal and both sides are
matched separately.

:param str string: The string to check for conformance with the \
'format-missing-comma' extension.
:return: `None` if there is no match, otherwise a tuple of \
size 2 containing two Format_Item classes.
:rtype: `NoneType` or \
(:py:class:`fparser.two.Format_Item`, \
:py:class:`fparser.two.Format_Item`)

"""
if not string:
return None
strip_string = string.strip()
split = split_leading_char_literal(strip_string)
if split:
# The item starts with a character literal, e.g. "'a' 1x".
literal, rest = split
rest = rest.lstrip()
if not rest or rest.startswith(","):
# Nothing follows the literal, or standard syntax.
return None
return (Format_Item(literal), Format_Item(rest))
indices = [
strip_string.find(quote) for quote in "'\"" if strip_string.find(quote) > 0
]
if indices:
# The item contains a character literal preceded by another
# format item, e.g. "15x'a'". Split at the earliest quote.
index = min(indices)
return (
Format_Item(strip_string[:index].rstrip()),
Format_Item(strip_string[index:]),
)
return None

def tostr(self):
"""
:return: Parsed representation of two format items
Expand Down
56 changes: 56 additions & 0 deletions src/fparser/two/tests/fortran2003/test_format_item_c1002.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

import pytest
from fparser.two.Fortran2003 import Format_Item_C1002
from fparser.two import utils
from fparser.two.utils import InternalError, NoMatchError


Expand Down Expand Up @@ -106,3 +107,58 @@ def test_internal_error3(f2003_create, monkeypatch):
assert (
"items entry 1 should contain a format items object but it " "is empty or None"
) in str(excinfo.value)


@pytest.mark.parametrize(
"my_input,expected",
[
("'a' 1x", "'a', 1X"),
("'a' 'b'", "'a', 'b'"),
("'a''b' 'c'", "'a''b', 'c'"),
("15x'a'", "15X, 'a'"),
("21x ' if it is the case '", "21X, ' if it is the case '"),
('1x"don\'t"', '1X, "don\'t"'),
("'a' 1x 'b'", "'a', 1X, 'b'"),
],
)
def test_format_missing_comma_extension(f2003_create, my_input, expected):
"""Check that a missing comma between a character-string edit
descriptor and a neighbouring format item is matched when the
'format-missing-comma' extension is enabled (which it is by
default).

"""
ast = Format_Item_C1002(my_input)
assert str(ast) == expected


@pytest.mark.parametrize("my_input", ["'a' 1x", "15x'a'", "'a' 'b'"])
def test_format_missing_comma_disabled(f2003_create, monkeypatch, my_input):
"""Check that a missing comma between a character-string edit
descriptor and a neighbouring format item is not matched when the
'format-missing-comma' extension is not enabled.

"""
monkeypatch.setattr(
utils,
"_EXTENSIONS",
[ext for ext in utils.EXTENSIONS() if ext != "format-missing-comma"],
)
with pytest.raises(NoMatchError):
_ = Format_Item_C1002(my_input)


def test_format_missing_comma_format_statement(f2003_create):
"""Check that format statements making use of the
'format-missing-comma' extension are parsed (see issue #484 and
the MODFLOW-2005 codebase).

"""
from fparser.two.Fortran2003 import Format_Stmt

ast = Format_Stmt("FORMAT(//1X,'SWR PROCESS REQUIRES LAYCON'1X,'FOR',//)")
assert (
str(ast) == "FORMAT(/, /, 1X, 'SWR PROCESS REQUIRES LAYCON', 1X, 'FOR', /, /)"
)
ast = Format_Stmt("FORMAT(/1x,'unable to find:',1x,A,/15x' in file:')")
assert str(ast) == "FORMAT(/, 1X, 'unable to find:', 1X, A, /, 15X, ' in file:')"
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"""

import pytest
from fparser.two import utils
from fparser.two.Fortran2003 import Format_Specification
from fparser.two.utils import NoMatchError

Expand Down Expand Up @@ -247,7 +248,7 @@ def test_syntaxerror(f2003_create):
_ = Format_Specification(my_input)


def test_syntaxerror_c1002(f2003_create):
def test_syntaxerror_c1002(f2003_create, monkeypatch):
"""Test that we get an exception in situations where no comma is
supplied and the C1002 constraints for optional commas do not
apply.
Expand All @@ -259,9 +260,25 @@ def test_syntaxerror_c1002(f2003_create):
for my_input in ["(2P, 2/)", "('hello', 2/)", "(2E2.2, 3/)"]:
ast = Format_Specification(my_input)
# invalid syntax
for my_input in ["(2P 2/)", "(2P2/)", "('hello' 2/)", "('hello'2/)"]:
for my_input in ["(2P 2/)", "(2P2/)"]:
with pytest.raises(NoMatchError):
_ = Format_Specification(my_input)
# A missing comma after a character-string edit descriptor is
# accepted by the 'format-missing-comma' extension (enabled by
# default) ...
for my_input in ["('hello' 2/)", "('hello'2/)"]:
ast = Format_Specification(my_input)
assert str(ast) == "('hello', 2/)"
# ... and is invalid syntax without the extension.
with monkeypatch.context() as mpatch:
mpatch.setattr(
utils,
"_EXTENSIONS",
[ext for ext in utils.EXTENSIONS() if ext != "format-missing-comma"],
)
for my_input in ["('hello' 2/)", "('hello'2/)"]:
with pytest.raises(NoMatchError):
_ = Format_Specification(my_input)
# Comma is mandatory after a P descriptor if not one of ['F', 'E',
# 'EN', 'ES', 'D', 'G'] or not a '/' or a ':'
# Test valid syntax.
Expand All @@ -285,7 +302,6 @@ def test_syntaxerror_c1002(f2003_create):
_ = Format_Specification(my_input)
# Comma is mandatory if C1002 is not relevant
for my_input in [
"('hello' 'hello')",
"(2P 2P)",
"(2P2P)",
"(F2.2 F2.2)",
Expand All @@ -299,3 +315,17 @@ def test_syntaxerror_c1002(f2003_create):
]:
with pytest.raises(NoMatchError):
_ = Format_Specification(my_input)
# A missing comma between two character-string edit descriptors is
# accepted by the 'format-missing-comma' extension (enabled by
# default) ...
ast = Format_Specification("('hello' 'hello')")
assert str(ast) == "('hello', 'hello')"
# ... and is invalid syntax without the extension.
with monkeypatch.context() as mpatch:
mpatch.setattr(
utils,
"_EXTENSIONS",
[ext for ext in utils.EXTENSIONS() if ext != "format-missing-comma"],
)
with pytest.raises(NoMatchError):
_ = Format_Specification("('hello' 'hello')")
7 changes: 7 additions & 0 deletions src/fparser/two/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@
# With this extension, these statements will be allowed.
_EXTENSIONS += ["extended-stop-args"]

# Many compilers (e.g. gfortran, ifort, ifx) accept a missing comma
# between a character-string edit descriptor and a neighbouring format
# item in a format specification, e.g. FORMAT('a' 1x,'b'), FORMAT(15x'a')
# or FORMAT('a' 'b'). This is supported by fparser if
# 'format-missing-comma' is specified in the EXTENSIONS list.
_EXTENSIONS += ["format-missing-comma"]


def EXTENSIONS():
"""
Expand Down
Loading