Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import pytest
from fparser.api import get_reader
from fparser.common.readfortran import FortranStringReader
from fparser.two.utils import FortranSyntaxError
from fparser.two.utils import FortranSyntaxError, NoMatchError
from fparser.two.Fortran2003 import (
Block_Label_Do_Construct,
Block_Nonlabel_Do_Construct,
Expand Down Expand Up @@ -245,3 +245,26 @@ def test_do_construct_missing_end_name(f2003_create, fake_symbol_table):
a = 1
end do"""))
assert exc_info.value.args[0].endswith("Expecting name 'name' but none given")


@pytest.mark.usefixtures("f2003_create")
def test_block_abort_early():
"""Tests that a parsing a blocked do loop will abort early if it detects
a non-blocked loop (which avoids an exponential scaling). This is done
by trying to parse a non-blocked loop as Block_Label_Do_Construct,
and analysing the returned error message. If the parsing does not abort
early, the error location will be line 4 (and an empty line, indicating
the end of file was reached).
If on the other hand the abort happens early, the assignment in line
2 will be returned as the error, indicating that the exponential
behaviour is avoided.
"""

with pytest.raises(NoMatchError) as err:
Block_Label_Do_Construct(get_reader("""\
do 12
12 a = 1
call test()
"""))
assert "at line 2" in str(err.value)
assert "12 a = 1" in str(err.value)
45 changes: 45 additions & 0 deletions src/fparser/two/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,15 @@ def import_now():
Comment,
Include_Stmt,
add_comments_includes_directives,
Continue_Stmt,
End_Do,
End_Do_Stmt,
Label_Do_Stmt,
)
from fparser.two.Fortran2008.label_do_stmt_r816 import (
Label_Do_Stmt as Label_Do_Stmt_2008,
)

from fparser.two import C99Preprocessor

DynamicImport.Else_If_Stmt = Else_If_Stmt
Expand All @@ -356,6 +364,11 @@ def import_now():
DynamicImport.add_comments_includes_directives = (
add_comments_includes_directives
)
DynamicImport.Continue_Stmt = Continue_Stmt
DynamicImport.End_Do = End_Do
DynamicImport.End_Do_Stmt = End_Do_Stmt
DynamicImport.Label_Do_Stmt = Label_Do_Stmt
DynamicImport.Label_Do_Stmt_2008 = Label_Do_Stmt_2008


di = DynamicImport()
Expand Down Expand Up @@ -750,6 +763,38 @@ def match(
i += 1
continue

# The grammar contains an exponential scaling behaviour for
# non-blocked labelled loop statements. The parser will try
# to find a match for a blocked do statement, but will ignore
# the fact that there is a non-blocking label, e.g.:
# do 10 i=1, 10
# 10 a(i) = 1
# It will try to find a `10 enddo` or `10 continue` statement,
# ignoring the fact that the label 10 indicates that it is not
# a blocked loop. Full details in ticket 499.
# In order to avoid that, we identify if we are looking for a
# labelled loop which is blocked (endcls=End_Do), and have
# neither an `End_Do` nor a `Continue`, which has the same
# label: in this case we can abort looking (which will then
# trigger the caller to test for the next rule, which is a
# non-blocked loop). This breaks the exponential behaviour
# in case of non-blocked loops (since the parser won't look
# ahead till the end of the file).
if (
startcls in (di.Label_Do_Stmt, di.Label_Do_Stmt_2008)
and endcls is di.End_Do
and hasattr(obj, "get_end_label")
and (content[start_idx].get_start_label() == obj.get_end_label())
and not isinstance(obj, (di.End_Do_Stmt, di.Continue_Stmt))
):
# We need to put the just read statement back:
obj.restore_reader(reader)
# ... and then also restore all previously read content
for obj in reversed(content):
obj.restore_reader(reader)
# ... before we abort.
return None

# We got a match for this class
had_match = True
content.append(obj)
Expand Down
Loading