From ce12157a266750afb931899e48de3cb97ea4286b Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Mon, 20 Apr 2026 17:53:00 +1000 Subject: [PATCH 1/7] #499 Added initial try to avoid the look-ahead and break the exponential scaling. --- src/fparser/two/utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 7a179d2e..0259c1e3 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -325,6 +325,7 @@ def import_now(): Else_If_Stmt, Else_Stmt, End_If_Stmt, + Label_Do_Stmt, Masked_Elsewhere_Stmt, Elsewhere_Stmt, End_Where_Stmt, @@ -750,6 +751,21 @@ def match( i += 1 continue + from fparser.two.Fortran2003 import Continue_Stmt, End_Do, End_Do_Stmt, Label_Do_Stmt + if (startcls and + isinstance(content[start_idx], Label_Do_Stmt) and + hasattr(obj, "get_end_label") and + content[start_idx].get_start_label() == obj.get_end_label() + and endcls is End_Do and + not isinstance(obj, (End_Do_Stmt, Continue_Stmt))): + if table_name: + SYMBOL_TABLES.exit_scope() + obj.restore_reader(reader) + for obj in reversed(content): + obj.restore_reader(reader) + return None + + # We got a match for this class had_match = True content.append(obj) From 9ae435c8cd729e7bb598b21fe12a94e30e8a6f8e Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 21 Apr 2026 15:21:52 +1000 Subject: [PATCH 2/7] #499 Simplified and documented code. --- src/fparser/two/utils.py | 43 +++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 0259c1e3..18847908 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -325,7 +325,6 @@ def import_now(): Else_If_Stmt, Else_Stmt, End_If_Stmt, - Label_Do_Stmt, Masked_Elsewhere_Stmt, Elsewhere_Stmt, End_Where_Stmt, @@ -337,7 +336,12 @@ def import_now(): Comment, Include_Stmt, add_comments_includes_directives, + Continue_Stmt, + End_Do, + End_Do_Stmt, + Label_Do_Stmt, ) + from fparser.two import C99Preprocessor DynamicImport.Else_If_Stmt = Else_If_Stmt @@ -357,6 +361,10 @@ 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 di = DynamicImport() @@ -751,21 +759,38 @@ def match( i += 1 continue - from fparser.two.Fortran2003 import Continue_Stmt, End_Do, End_Do_Stmt, Label_Do_Stmt - if (startcls and - isinstance(content[start_idx], Label_Do_Stmt) and - hasattr(obj, "get_end_label") and - content[start_idx].get_start_label() == obj.get_end_label() - and endcls is End_Do and - not isinstance(obj, (End_Do_Stmt, Continue_Stmt))): + # 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 is di.Label_Do_Stmt 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))): if table_name: SYMBOL_TABLES.exit_scope() + # 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) From f60f1258199b0605b26591f2ac83843574cb9699 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 21 Apr 2026 16:49:03 +1000 Subject: [PATCH 3/7] #499 Fix typo in di.ContinueStatement, fixed black issues. --- src/fparser/two/utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 18847908..9be75e4c 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -361,7 +361,7 @@ def import_now(): DynamicImport.add_comments_includes_directives = ( add_comments_includes_directives ) - DynamicImport.Continue_Stmt = Continue_Stmt, + DynamicImport.Continue_Stmt = Continue_Stmt DynamicImport.End_Do = End_Do DynamicImport.End_Do_Stmt = End_Do_Stmt DynamicImport.Label_Do_Stmt = Label_Do_Stmt @@ -776,11 +776,13 @@ def match( # 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 is di.Label_Do_Stmt 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))): + if ( + startcls is di.Label_Do_Stmt + 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)) + ): if table_name: SYMBOL_TABLES.exit_scope() # We need to put the just read statement back: From b5b6e406ed6417c1bd8816d2047819ca863ce4a1 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 21 Apr 2026 17:01:58 +1000 Subject: [PATCH 4/7] #499 Removing exit_scope, since it's not a scoping region. --- src/fparser/two/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 9be75e4c..a285178b 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -783,8 +783,6 @@ def match( and (content[start_idx].get_start_label() == obj.get_end_label()) and not isinstance(obj, (di.End_Do_Stmt, di.Continue_Stmt)) ): - if table_name: - SYMBOL_TABLES.exit_scope() # We need to put the just read statement back: obj.restore_reader(reader) # ... and then also restore all previously read content From ba5ee95e4395e4296d607b6d06542ad39562ec53 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 21 Apr 2026 17:32:33 +1000 Subject: [PATCH 5/7] #499 Also support F2008's version of Label_Do_Stmt. --- src/fparser/two/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index a285178b..47dfabd9 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -341,6 +341,7 @@ def import_now(): 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 @@ -365,6 +366,7 @@ def import_now(): 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() @@ -777,7 +779,7 @@ def match( # in case of non-blocked loops (since the parser won't look # ahead till the end of the file). if ( - startcls is di.Label_Do_Stmt + 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()) From fbcb3f670a423504c60ca6c8b5e121cfc1d16096 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 21 Apr 2026 17:35:08 +1000 Subject: [PATCH 6/7] #499 Fix black issue. --- src/fparser/two/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py index 47dfabd9..9d639f1e 100644 --- a/src/fparser/two/utils.py +++ b/src/fparser/two/utils.py @@ -341,7 +341,9 @@ def import_now(): 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.Fortran2008.label_do_stmt_r816 import ( + Label_Do_Stmt as Label_Do_Stmt_2008, + ) from fparser.two import C99Preprocessor From 869c01ca4c19c1e1466b54506d5a26bc21c13494 Mon Sep 17 00:00:00 2001 From: Joerg Henrichs Date: Tue, 21 Apr 2026 23:15:20 +1000 Subject: [PATCH 7/7] #499 Add tests to verify that early aborting for parsing blocked loops in case of a non-blocking loop works. --- .../test_block_do_construct_r826.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) 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)