Skip to content
Merged
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 @@ -23,6 +23,10 @@ Modifications by (in alphabetical order):
* P. Vitt, University of Siegen, Germany
* A. Voysey, UK Met Office

26/08/2026 PR #526 for #484. Adds a 'format-missing-comma' extension which
permits commas to be omitted between character-string edit
descriptors and neighbouring format items.

24/08/2026 PR #525 for #524. Bug fix for Io_implied_Do rejecting a minimal-
length io-implied-do.

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
117 changes: 103 additions & 14 deletions src/fparser/two/Fortran2003.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@
# Original author: Pearu Peterson <pearu@cens.ioc.ee>
# First version created: Oct 2006

from __future__ import annotations
import inspect
import re
import sys

from typing import Union
from typing import Optional, Union

from fparser.common.splitline import string_replace_map
from fparser.two import pattern_tools as pattern
Expand Down Expand Up @@ -10078,33 +10079,67 @@ 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):
def match(
string: str,
) -> Optional[
tuple[
Union[Control_Edit_Desc, Format_Item], Union[Control_Edit_Desc, Format_Item]
]
]:
"""Implements the matching for the C1002 Format Item constraint,
optionally relaxed by the 'format-missing-comma' extension.

:param string: the string to check for conformance with a
C1002 format item constraint.

:returns: `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.

"""
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: str,
) -> Optional[
tuple[
Union[Control_Edit_Desc, Format_Item], Union[Control_Edit_Desc, Format_Item]
]
]:
"""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
out when parsing the list, it was decided to treat these
separately and match them in this class. As a result the
generated class hierarchy is a little more complicated.

: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.
:param string: the string to check for conformance with a
C1002 format item constraint.

: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`)
:returns: `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.

"""
if not string:
Expand Down Expand Up @@ -10186,6 +10221,60 @@ def match(string):
Format_Item(option + repmap(right.lstrip())),
)

return None

@staticmethod
def _extension_match(string: str) -> Optional[tuple[Format_Item, Format_Item]]:
"""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.

Quote/escape handling is delegated to string_replace_map()
(the same helper _standard_match() uses to locate the '/' and
':' edit descriptors), rather than re-implementing it here: it
masks character literals - simple ones are left untouched and
complex ones (e.g. containing doubled quotes) are replaced
with a placeholder - so the masked line can never contain the
literal's own quote character before its closing quote.

:param string: the string to check for conformance with the
'format-missing-comma' extension.

:returns: `None` if there is no match, otherwise a tuple of
size 2 containing two Format_Item classes.

"""
if not string:
return None
strip_string = string.strip()
line, repmap = string_replace_map(strip_string)
if line and line[0] in "'\"":
# The item starts with a character literal, e.g. "'a' 1x".
quote = line[0]
end = line.find(quote, 1)
if end == -1:
return None
literal = repmap(line[: end + 1])
rest = repmap(line[end + 1 :]).lstrip()
if not rest or rest.startswith(","):
# Nothing follows the literal, or standard syntax.
return None
return (Format_Item(literal), Format_Item(rest))
indices = [line.find(quote) for quote in "'\"" if line.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(repmap(line[:index]).rstrip()),
Format_Item(repmap(line[index:])),
)
return None

def tostr(self):
"""
:return: Parsed representation of two format items
Expand Down
51 changes: 51 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,53 @@ 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", [])
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.

"""
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,21 @@ 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", [])
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 +298,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 +311,13 @@ 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:
Comment thread
arporter marked this conversation as resolved.
Resolved
mpatch.setattr(utils, "_EXTENSIONS", [])
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 @@ -122,6 +122,13 @@
# with a directory, using e.g. INQUIRE (DIRECTORY=".", DIRSPEC=C_DIRSPEC, ...)
_EXTENSIONS += ["inquire-directory"]

# 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