diff --git a/src/fparser/two/tests/fortran2003/test_block_do_construct_r826.py b/src/fparser/two/tests/fortran2003/test_block_do_construct_r826.py index 812b0ee7..d47109cc 100644 --- a/src/fparser/two/tests/fortran2003/test_block_do_construct_r826.py +++ b/src/fparser/two/tests/fortran2003/test_block_do_construct_r826.py @@ -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, @@ -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) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 7a179d2e..9d639f1e 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -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 @@ -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() @@ -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)