From bd83fb3be89790ecd244107ddeb87b2735a4c82e Mon Sep 17 00:00:00 2001 From: said Date: Tue, 26 May 2026 00:46:54 +0100 Subject: [PATCH 1/8] raise errors if it not part of the language --- README.md | 8 +- c_parser/parser.py | 85 ++++++++++++++++++ docs/c_parser/c_parser_cli_workflow.md | 5 +- docs/c_parser/c_parser_reference.md | 3 + docs/fortran/fortran_parser.md | 4 + fortran_parser/parser.py | 117 +++++++++++++++++++++++++ tests/parser/c/test_c_cli_skeleton.py | 17 ++++ tests/parser/c/test_c_functions.py | 52 +++++++++++ tests/parser/test_cli.py | 17 ++++ tests/parser/test_error_handling.py | 47 ++++++++++ 10 files changed, 351 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 74fec8581..2c081cbcf 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ The C frontend is currently parse-only. It supports: - Raw mutually exclusive function alternatives preserved for later semantic selection rather than collapsed into one signature. -C semantic IR conversion, C `.pyi` generation, and C wrap-readiness are still -intentionally disabled until the C semantic layer is implemented. +The supported C subset continues through semantic IR conversion, `.pyi` +generation, and wrap-readiness. ## Public APIs @@ -112,7 +112,9 @@ path when `--language` is omitted. C source/header files require explicit `--language fortran` or `--language c`. C parsing, semantic IR, `.pyi` generation, and wrap-readiness are available in explicit C mode. Selecting a frontend that conflicts with a recognized C or Fortran source suffix is an -error. +error. Once selected, a frontend also rejects unmistakable declarations or +program-unit syntax from the other language outside ignored execution/function +bodies rather than silently dropping it. For parse output, `--show-vars` expands scope-level variables that are normally summarized as `vars=N`. Use `--print-limit N` to keep large repeated sections diff --git a/c_parser/parser.py b/c_parser/parser.py index 220afe1dc..f45703ca7 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -120,6 +120,42 @@ _RAW_CONDITIONAL_DIRECTIVE_RE = re.compile( r"^\s*#\s*(?Pif|ifdef|ifndef|elif|else|endif)\b" ) +_FOREIGN_FORTRAN_LINES = ( + re.compile( + r"^\s*(?:pure\s+|elemental\s+|recursive\s+|module\s+)*" + r"subroutine\s+\w+\s*(?:\([^;{}]*\))?" + r"(?:\s+bind\s*\([^;{}]*\))?\s*$", + re.IGNORECASE, + ), + re.compile( + r"^\s*(?:pure\s+|elemental\s+|recursive\s+|module\s+)*" + r"(?:(?:integer|real|logical|complex|character)(?:\s*\([^;{}]*\))?|" + r"double\s+precision|type\s*\([^)]*\)|class\s*\([^)]*\))?" + r"\s*function\s+\w+\s*(?:\([^;{}]*\))?" + r"(?:\s+result\s*\([^;{}]*\))?" + r"(?:\s+bind\s*\([^;{}]*\))?\s*$", + re.IGNORECASE, + ), + re.compile( + r"^\s*end\s+(?:subroutine|function|module|submodule|program|interface|type|block\s+data)\b", + re.IGNORECASE, + ), + re.compile( + r"^\s*(?:module|program|block\s+data)\s+[A-Za-z_]\w*\s*$", + re.IGNORECASE, + ), + re.compile(r"^\s*submodule\s*\([^)]*\)\s+[A-Za-z_]\w*\s*$", re.IGNORECASE), + re.compile(r"^\s*(?:implicit\s+none|contains)\s*$", re.IGNORECASE), + re.compile( + r"^\s*use\s+(?:,\s*(?:intrinsic|non_intrinsic)\s*)?(?:::)?\s*[A-Za-z_]\w*" + r"(?:\s*,\s*only\s*:.*)?\s*$", + re.IGNORECASE, + ), + re.compile( + r"^\s*(?:(?:integer|real|logical|complex|character)\b|(?:type|class)\s*\([^)]*\)).*::", + re.IGNORECASE, + ), +) _PRIMITIVE_WORDS = { "void", "char", @@ -337,6 +373,11 @@ def visit_file( if preprocessing == "raw" and inferred_preprocessed_path is not None: preprocessing = "preprocessed" + self._raise_for_foreign_fortran_syntax( + source, + filename, + use_linemarkers=preprocessing in {"compiler", "preprocessed"}, + ) parsed = CFile(filename=filename, parser_status="partial", preprocessing=preprocessing) if preprocessing == "raw": effective_include_dirs = list(include_dirs or ()) @@ -575,6 +616,50 @@ def _source_location(self, segment: CTopLevelSegment) -> CSourceLocation: """Return the original start location for a top-level segment.""" return self._source_location_at(segment, 0) + @staticmethod + def _raise_for_foreign_fortran_syntax( + source: str, + filename: str | None, + *, + use_linemarkers: bool = False, + ) -> None: + """Reject unmistakable Fortran statements outside ignored C bodies.""" + for segment in split_top_level_c_source( + source, + filename=filename, + use_linemarkers=use_linemarkers, + ): + CParser._raise_for_foreign_fortran_segment(segment) + + @staticmethod + def _raise_for_foreign_fortran_segment(segment: CTopLevelSegment) -> None: + for line_offset, line in enumerate(segment.text.splitlines()): + if not any(pattern.match(line) for pattern in _FOREIGN_FORTRAN_LINES): + continue + filename = ( + segment.original_filenames[line_offset] + if line_offset < len(segment.original_filenames) + else segment.filename + ) + line_number = ( + segment.original_line_numbers[line_offset] + if line_offset < len(segment.original_line_numbers) + else segment.original_start_line + line_offset + ) + source_line = ( + segment.original_source_lines[line_offset] + if line_offset < len(segment.original_source_lines) + else segment.original_source_line + ) + raise CParseError( + "Fortran syntax is not valid C input; parse Fortran source with --language fortran.", + filename=filename, + line_number=line_number, + column=1, + source_line=source_line, + code="CPARSE_FOREIGN_FORTRAN_SYNTAX", + ) + def _macro_dependencies( self, source: str, diff --git a/docs/c_parser/c_parser_cli_workflow.md b/docs/c_parser/c_parser_cli_workflow.md index e1ac2a14a..d0086a9ca 100644 --- a/docs/c_parser/c_parser_cli_workflow.md +++ b/docs/c_parser/c_parser_cli_workflow.md @@ -44,7 +44,10 @@ recognizable Fortran files and `.pyi` readiness inputs, but a `.c`, `.h`, or `.i` path fails with guidance to pass `--language c`; directory and unknown-suffix source inputs require an explicit frontend selection. A known C path explicitly passed with `--language fortran` is rejected before parsing, -so it cannot silently produce an empty Fortran interface. +so it cannot silently produce an empty Fortran interface. The parser also +rejects unmistakable Fortran unit or declaration syntax found in C input, and +the Fortran parser rejects unmistakable C declarations outside execution +regions that are intentionally not modeled. The C parser output differs from Fortran parser output by using C-specific top-level sections: `functions`, `structs`, `unions`, `enums`, `typedefs`, diff --git a/docs/c_parser/c_parser_reference.md b/docs/c_parser/c_parser_reference.md index e1d501e46..537d656ef 100644 --- a/docs/c_parser/c_parser_reference.md +++ b/docs/c_parser/c_parser_reference.md @@ -490,6 +490,9 @@ x2py path/to/api.h --language c --parse --out report.json There is no separate `--parse-c` alias: `--language c --parse` is the shared language-selection form. Auto-detection remains deferred: a `.c`, `.h`, or `.i` input without `--language c` exits with language-selection guidance. +Explicit C input containing unmistakable top-level Fortran unit or declaration +syntax raises a fatal parser diagnostic instead of emitting a partial C +interface. ## Current JSON Output diff --git a/docs/fortran/fortran_parser.md b/docs/fortran/fortran_parser.md index b3b6cb5fb..a02b29ad9 100644 --- a/docs/fortran/fortran_parser.md +++ b/docs/fortran/fortran_parser.md @@ -210,6 +210,10 @@ python -m x2py path/to/fortran_src --language fortran --parse Fortran directories are recursively scanned for `.f`, `.for`, `.ftn`, `.f90`, `.f95`, `.f03`, `.f08`. +The Fortran frontend rejects unmistakable C declaration syntax before +wrapper-focused parsing when it appears outside executable procedure/program +bodies, which are intentionally not represented in the extracted interface. + The human-readable parse tree keeps scope variables compact by default as `vars=N`. Add `--show-vars` to print the variables, or `--print-limit N` to print only the first `N` items in each repeated section. diff --git a/fortran_parser/parser.py b/fortran_parser/parser.py index d56cd7280..00e89923c 100644 --- a/fortran_parser/parser.py +++ b/fortran_parser/parser.py @@ -118,6 +118,37 @@ "unsupported_procedure_pointer", "unsupported_c_ptr", ) +_FOREIGN_C_DECLARATION = re.compile( + r""" + ^\s* + (?:(?:typedef|extern|static|register|inline|const|volatile|restrict|_Atomic)\s+)* + (?: + (?:signed|unsigned)(?:\s+(?:char|short|int|long)(?:\s+long)?)? + |(?:void|char|short|int|long|float|double)(?:\s+(?:int|long|_Complex))? + |(?:struct|union|enum)\s+[A-Za-z_]\w* + ) + \s+(?:\*+\s*)?[A-Za-z_]\w*\s*(?:\(|\[|=|;|\{) + """, + re.IGNORECASE | re.VERBOSE, +) +_FOREIGN_C_ALIAS_DECLARATION = re.compile( + r""" + ^\s* + (?! + (?:allocate|associate|backspace|block|call|case|close|continue|cycle| + data|deallocate|do|else|elseif|end|error|exit|external|format| + goto|go|if|implicit|include|inquire|intrinsic|nullify|open| + parameter|pause|print|read|return|rewind|save|select|stop|use| + wait|where|write|integer|real|complex|logical|character|double| + type|class|procedure)\b + ) + (?:(?:typedef|extern|static|register|inline|const|volatile|restrict|_Atomic)\s+)* + [A-Za-z_]\w*\s+(?:\*+\s*)?[A-Za-z_]\w* + \s*(?:\([^;{}]*\)|\[[^\]]*\])? + \s*(?:=[^;{}]*)?[;{]\s*$ + """, + re.IGNORECASE | re.VERBOSE, +) _PreprocessedLines = list[tuple[str, int | None, str | None]] @@ -900,6 +931,7 @@ def _helper_prepare_source_units( self._helper_validate_unit_headers(lines, filename) root_scope = _ParserScope(kind="file", name=None) units = self._helper_slice_child_units(lines, parent_scope=root_scope, filename=filename) + self._helper_validate_file_scope_unparsed_lines(lines, filename) self._helper_validate_sibling_units(units, parent_scope=root_scope, filename=filename) return lines, root_scope, units @@ -1136,6 +1168,79 @@ def _helper_validate_unit_headers(self, lines: _PreprocessedLines, filename: str source_line=source_line, ) + def _helper_validate_file_scope_unparsed_lines(self, lines: _PreprocessedLines, filename: str | None) -> None: + """Reject foreign C declarations not owned by a Fortran source unit.""" + index = 0 + pp_condition_stack: list[tuple[int, int]] = [] + pp_active_stack: list[bool] = [] + pp_group_counter = 0 + while index < len(lines): + line, lineno, source_line = lines[index] + stripped = line.strip() + if not stripped: + index += 1 + continue + handled_pp, pp_group_counter = self._handle_procedure_preprocessor_line( + stripped, + macro_selection_enabled=False, + macro_names=set(), + pp_condition_stack=pp_condition_stack, + pp_active_stack=pp_active_stack, + pp_group_counter=pp_group_counter, + ) + if handled_pp: + index += 1 + continue + start = self._helper_classify_unit_start(stripped) + if start is not None: + end_index = self._helper_find_unit_end(lines, index, start[0], filename=filename) + if end_index is not None: + index = end_index + 1 + continue + if self._is_allowed_unparsed_file_scope_line(stripped): + index += 1 + continue + self._raise_if_foreign_c_syntax_line( + stripped, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + index += 1 + + @staticmethod + def _is_allowed_unparsed_file_scope_line(line: str) -> bool: + stripped = line.strip() + lowered = stripped.lower() + return ( + stripped.startswith("#") + or lowered == "contains" + or lowered.startswith("end ") + or FortranParser._is_ignored_spec_statement(stripped) + or FortranParser._is_openmp_directive(stripped) + ) + + @staticmethod + def _raise_if_foreign_c_syntax_line( + line: str, + *, + filename: str | None, + lineno: int | None, + source_line: str | None, + ) -> None: + if not ( + _FOREIGN_C_DECLARATION.match(line) + or _FOREIGN_C_ALIAS_DECLARATION.match(line) + ): + return + raise FortranParseError( + "C declaration syntax is not valid Fortran input; parse C source with --language c.", + filename=filename, + line_number=lineno, + source_line=source_line, + code="PARSE_FOREIGN_C_SYNTAX", + ) + def _helper_slice_child_units( self, lines: _PreprocessedLines, @@ -2047,6 +2152,12 @@ def _helper_visit_spec_part( stripped = line.strip() if not stripped: continue + self._raise_if_foreign_c_syntax_line( + stripped, + filename=filename, + lineno=lineno, + source_line=source_line, + ) if scope.kind == "procedure": self._helper_visit_procedure_spec_line( stripped, @@ -2306,6 +2417,12 @@ def _parse_derived_type_contains_line( lineno: int | None = None, source_line: str | None = None, ) -> None: + self._raise_if_foreign_c_syntax_line( + line, + filename=filename, + lineno=lineno, + source_line=source_line, + ) proc_binding = _REGEX["procedure_binding"].match(line) if proc_binding: binding_names = split_csv(proc_binding.group("names")) diff --git a/tests/parser/c/test_c_cli_skeleton.py b/tests/parser/c/test_c_cli_skeleton.py index 40242cdf0..8234c28d6 100644 --- a/tests/parser/c/test_c_cli_skeleton.py +++ b/tests/parser/c/test_c_cli_skeleton.py @@ -208,6 +208,23 @@ def test_cli_c_input_rejects_explicit_fortran_frontend(tmp_path: Path): assert not output.exists() +def test_cli_c_pyi_rejects_embedded_fortran_syntax(tmp_path: Path): + header = tmp_path / "api.h" + header.write_text( + "int add(int a, int b);\nsubroutine solve()\nend subroutine solve\n", + encoding="utf-8", + ) + result = subprocess.run( + [sys.executable, "-m", "x2py", str(header), "--language", "c", "--pyi"], + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert "CPARSE_FOREIGN_FORTRAN_SYNTAX" in result.stderr + assert "Fortran syntax is not valid C input" in result.stderr + + def test_cli_c_rejects_fortran_only_parse_flags(tmp_path: Path): header = tmp_path / "api.h" header.write_text("int add(int a, int b);\n", encoding="utf-8") diff --git a/tests/parser/c/test_c_functions.py b/tests/parser/c/test_c_functions.py index 4167e8bb3..bf6fda258 100644 --- a/tests/parser/c/test_c_functions.py +++ b/tests/parser/c/test_c_functions.py @@ -78,6 +78,58 @@ def test_old_style_knr_function_definition_raises_unsupported_diagnostic(): parse_c_file(source, filename="knr.c") +@pytest.mark.parametrize( + "source", + [ + "subroutine solve()\nend subroutine solve\n", + "integer function answer()\nend function answer\n", + "int add(int a, int b);\ninteger :: state\n", + "int add(int a, int b);\ntype(c_ptr) :: handle\n", + ], +) +def test_c_parser_rejects_foreign_fortran_syntax(source): + from c_parser import CParseError, parse_c_file + + with pytest.raises(CParseError, match="Fortran syntax is not valid C input") as exc_info: + parse_c_file(source, filename="mixed.h") + + assert exc_info.value.code == "CPARSE_FOREIGN_FORTRAN_SYNTAX" + + +def test_c_parser_foreign_fortran_error_maps_preprocessed_source_location(): + from c_parser import CParseError, parse_c_file + + with pytest.raises(CParseError) as exc_info: + parse_c_file( + '# 80 "generated.f90"\ninteger :: state\n', + filename="translation.i", + preprocessing="preprocessed", + ) + + assert exc_info.value.code == "CPARSE_FOREIGN_FORTRAN_SYNTAX" + assert exc_info.value.filename == "generated.f90" + assert exc_info.value.line_number == 80 + + +def test_c_parser_ignores_foreign_fortran_syntax_inside_function_body(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +int run(void) +{ + subroutine solve() + integer :: state + end subroutine solve + return 0; +} +""", + filename="mixed_body.c", + ) + + assert [function.name for function in parsed.functions] == ["run"] + + def test_control_flow_conditions_inside_function_body_do_not_look_like_knr_definitions(): from c_parser import parse_c_file diff --git a/tests/parser/test_cli.py b/tests/parser/test_cli.py index a82249203..d1bb95995 100644 --- a/tests/parser/test_cli.py +++ b/tests/parser/test_cli.py @@ -550,6 +550,23 @@ def test_cli_rejects_fortran_file_with_explicit_c_frontend(tmp_path: Path): assert "pass --language fortran" in result.stderr +def test_cli_fortran_rejects_embedded_c_declaration_outside_execution_body(tmp_path: Path): + source = tmp_path / "solver.f90" + source.write_text( + "subroutine solve()\n int add(int a, int b);\nend subroutine solve\n", + encoding="utf-8", + ) + result = subprocess.run( + [sys.executable, "-m", "x2py", str(source), "--pyi"], + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert "PARSE_FOREIGN_C_SYNTAX" in result.stderr + assert "C declaration syntax is not valid Fortran input" in result.stderr + + def test_cli_parse_shows_module_derived_types_and_derived_arg_kinds(): fixture = Path(__file__).parent.parent / "data" / "fortran" / "general" / "modern_pyi_example.f90" cmd = [sys.executable, "-m", "x2py", str(fixture), "--parse"] diff --git a/tests/parser/test_error_handling.py b/tests/parser/test_error_handling.py index bdb67e0b2..621268cb4 100644 --- a/tests/parser/test_error_handling.py +++ b/tests/parser/test_error_handling.py @@ -746,3 +746,50 @@ def test_slicer_reports_missing_end_unit(): """ with pytest.raises(FortranParseError, match="Missing end module for module 'missing_end'"): parse_fortran_file(code, filename="missing_end_module.f90") + + +@pytest.mark.parametrize( + "code", + [ + "int add(int a, int b);\n", + "api_size count(void);\n", + """ +subroutine mixed_spec() + api_size count(void); +end subroutine mixed_spec +""", + ], +) +def test_fortran_parser_rejects_foreign_c_declarations_outside_execution_bodies(code): + with pytest.raises(FortranParseError, match="C declaration syntax is not valid Fortran input") as exc_info: + parse_fortran_file(code, filename="mixed.f90") + + assert exc_info.value.code == "PARSE_FOREIGN_C_SYNTAX" + + +def test_fortran_parser_ignores_foreign_c_declarations_after_execution_boundary(): + parsed = parse_fortran_file( + """ +subroutine mixed_body() + call noop() + api_size count(void); +end subroutine mixed_body +""", + filename="mixed_body.f90", + ) + + assert parsed.procedures[0].name == "mixed_body" + + +def test_foreign_c_check_preserves_valid_semicolon_separated_fortran_statements(): + parsed = parse_fortran_file( + """ +subroutine valid_body(x) + real :: x + call update(x); write(*,*) x +end subroutine valid_body +""", + filename="valid_body.f90", + ) + + assert parsed.procedures[0].name == "valid_body" From 3eb572f9209a1dfffb7d27fb99ecc8371d83ddec Mon Sep 17 00:00:00 2001 From: said Date: Tue, 26 May 2026 16:25:50 +0100 Subject: [PATCH 2/8] errors --- c_parser/parser.py | 20 ++++ fortran_parser/parser.py | 98 ++++++++++++++++--- tests/parser/c/test_c_functions.py | 27 +++++ tests/parser/test_error_handling.py | 37 +++++++ .../parser/test_parser_public_entrypoints.py | 4 +- 5 files changed, 173 insertions(+), 13 deletions(-) diff --git a/c_parser/parser.py b/c_parser/parser.py index f45703ca7..bb16956e4 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -660,6 +660,25 @@ def _raise_for_foreign_fortran_segment(segment: CTopLevelSegment) -> None: code="CPARSE_FOREIGN_FORTRAN_SYNTAX", ) + @staticmethod + def _could_start_c_external_declaration(text: str) -> bool: + stripped = text.lstrip() + return bool(stripped) and (stripped[0].isalpha() or stripped[0] == "_") + + @staticmethod + def _raise_for_invalid_top_level_syntax(segment: CTopLevelSegment) -> None: + text = segment.text.strip() + if not text or CParser._could_start_c_external_declaration(text): + return + raise CParseError( + f"Invalid C syntax at top level: {text}", + filename=segment.filename, + line_number=segment.original_start_line, + column=segment.original_start_column, + source_line=segment.original_source_line, + code="CPARSE_INVALID_SYNTAX", + ) + def _macro_dependencies( self, source: str, @@ -2560,6 +2579,7 @@ def _parse_translation_unit( filename=filename, use_linemarkers=use_linemarkers, ): + self._raise_for_invalid_top_level_syntax(segment) macro_dependency = self._segment_macro_dependency( segment, function_like_names, diff --git a/fortran_parser/parser.py b/fortran_parser/parser.py index 00e89923c..08e4f11b6 100644 --- a/fortran_parser/parser.py +++ b/fortran_parser/parser.py @@ -1200,13 +1200,22 @@ def _helper_validate_file_scope_unparsed_lines(self, lines: _PreprocessedLines, if self._is_allowed_unparsed_file_scope_line(stripped): index += 1 continue + if self._is_executable_statement_start(stripped): + index += 1 + continue self._raise_if_foreign_c_syntax_line( stripped, filename=filename, lineno=lineno, source_line=source_line, ) - index += 1 + self._raise_invalid_fortran_syntax_line( + stripped, + context="file scope", + filename=filename, + lineno=lineno, + source_line=source_line, + ) @staticmethod def _is_allowed_unparsed_file_scope_line(line: str) -> bool: @@ -1216,6 +1225,9 @@ def _is_allowed_unparsed_file_scope_line(line: str) -> bool: stripped.startswith("#") or lowered == "contains" or lowered.startswith("end ") + or lowered.startswith(("endif", "enddo")) + or lowered == "else" + or lowered.startswith(("elseif", "else if")) or FortranParser._is_ignored_spec_statement(stripped) or FortranParser._is_openmp_directive(stripped) ) @@ -1241,6 +1253,29 @@ def _raise_if_foreign_c_syntax_line( code="PARSE_FOREIGN_C_SYNTAX", ) + @staticmethod + def _raise_invalid_fortran_syntax_line( + line: str, + *, + context: str, + filename: str | None, + lineno: int | None, + source_line: str | None, + ) -> None: + FortranParser._raise_if_foreign_c_syntax_line( + line, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + raise FortranParseError( + f"Invalid Fortran syntax in {context}: {line.strip()}", + filename=filename, + line_number=lineno, + source_line=source_line, + code="PARSE_INVALID_SYNTAX", + ) + def _helper_slice_child_units( self, lines: _PreprocessedLines, @@ -1483,16 +1518,10 @@ def _helper_split_unit_parts( index = child_end + 1 continue - is_spec_statement = ( - self._parse_use_statement(stripped) is not None - or self._is_ignored_spec_statement(stripped) - or self._looks_like_declaration_or_spec(stripped) - ) if ( region == "specification" and grammar.has_execution_part and self._is_executable_statement_start(stripped) - and not is_spec_statement ): region = "execution" @@ -2152,6 +2181,8 @@ def _helper_visit_spec_part( stripped = line.strip() if not stripped: continue + if stripped.startswith("#"): + continue self._raise_if_foreign_c_syntax_line( stripped, filename=filename, @@ -2278,9 +2309,15 @@ def _helper_visit_module_like_spec_line( ) if parsed: return - if "::" not in stripped and not self._looks_like_declaration_or_spec(stripped): - return owner_kind, owner_name = self._variable_scope_label(target) + if "::" not in stripped and not self._looks_like_declaration_or_spec(stripped): + self._raise_invalid_fortran_syntax_line( + stripped, + context=f"{owner_kind} '{owner_name or ''}' specification part", + filename=filename, + lineno=lineno, + source_line=source_line, + ) raise FortranParseError( f"Unknown or unsupported datatype declaration in {owner_kind} '{owner_name or ''}': {stripped}", filename=filename, @@ -2334,6 +2371,8 @@ def _helper_visit_procedure_spec_line(self, line: str, proc_state: dict, filenam source_line=source_line, ): return + if self._is_statement_function_statement(stripped): + return parsed = self._helper_parse_declaration_line( stripped, @@ -2400,7 +2439,15 @@ def _helper_visit_type_spec_line(self, line: str, scope: _ParserScope, filename: if parsed: return if "::" not in stripped and not self._looks_like_declaration_or_spec(stripped): - return + if self._is_executable_statement_start(stripped): + return + self._raise_invalid_fortran_syntax_line( + stripped, + context=f"type '{dtype.name}' specification part", + filename=filename, + lineno=lineno, + source_line=source_line, + ) raise FortranParseError( f"Unknown or unsupported datatype declaration in type '{dtype.name}': {line.strip()}", filename=filename, @@ -3007,7 +3054,13 @@ def _handle_unknown_proc_declaration( as an unsupported datatype declaration for the active procedure. """ if not self._looks_like_unknown_proc_declaration(line): - return + self._raise_invalid_fortran_syntax_line( + line, + context=f"procedure '{proc_state['signature'].name}' specification part", + filename=filename, + lineno=lineno, + source_line=source_line, + ) if any(_REGEX[pattern_key].search(line) for pattern_key in _UNSUPPORTED_PATTERN_KEYS): return raise FortranParseError( @@ -3895,6 +3948,17 @@ def _looks_like_declaration_or_spec(line: str) -> bool: return True return bool(re.match(r"^[A-Za-z_]\w+\s+[A-Za-z_]\w*", stripped)) + @staticmethod + def _is_statement_function_statement(line: str) -> bool: + stripped = line.strip() + return bool( + re.match( + r"^[A-Za-z_]\w*\s*\([^()]*\)\s*=", + stripped, + flags=re.IGNORECASE, + ) + ) + @staticmethod def _is_ignored_spec_statement(line: str) -> bool: return bool( @@ -3936,9 +4000,19 @@ def _is_executable_statement_start(line: str) -> bool: stripped = line.strip() if not stripped: # pragma: no cover - callers skip blank lines before executable checks. return False + labeled = re.match(r"^\d+\s+(?P.*)$", stripped) + if labeled: + stripped = labeled.group("body").strip() + if not stripped: + return False lowered = stripped.lower() if FortranParser._is_openmp_directive(stripped): return not FortranParser._is_openmp_declarative_directive(stripped) + if ( + FortranParser._parse_use_statement(stripped) is not None + or FortranParser._is_ignored_spec_statement(stripped) + ): + return False first_match = re.match(r"([a-z_][a-z0-9_]*)", lowered) first = first_match.group(1) if first_match else lowered.split(None, 1)[0] if first.isdigit(): @@ -3953,6 +4027,8 @@ def _is_executable_statement_start(line: str) -> bool: return True if _REGEX["legacy_parameter"].match(stripped): return False + if FortranParser._is_statement_function_statement(stripped): + return False if "=" in stripped and "::" not in stripped: # Distinguish assignment/statements from declaration lines carrying # type specs with named arguments, e.g.: diff --git a/tests/parser/c/test_c_functions.py b/tests/parser/c/test_c_functions.py index bf6fda258..9dbea3953 100644 --- a/tests/parser/c/test_c_functions.py +++ b/tests/parser/c/test_c_functions.py @@ -130,6 +130,33 @@ def test_c_parser_ignores_foreign_fortran_syntax_inside_function_body(): assert [function.name for function in parsed.functions] == ["run"] +@pytest.mark.parametrize("source", ["@@@\n", "int run(void);\n@@@;\n"]) +def test_c_parser_rejects_invalid_top_level_syntax(source): + from c_parser import CParseError, parse_c_file + + with pytest.raises(CParseError, match="Invalid C syntax") as exc_info: + parse_c_file(source, filename="invalid.c") + + assert exc_info.value.code == "CPARSE_INVALID_SYNTAX" + + +def test_c_parser_ignores_invalid_syntax_inside_function_body(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +int run(void) +{ + @@@ + return 0; +} +""", + filename="invalid_body.c", + ) + + assert [function.name for function in parsed.functions] == ["run"] + + def test_control_flow_conditions_inside_function_body_do_not_look_like_knr_definitions(): from c_parser import parse_c_file diff --git a/tests/parser/test_error_handling.py b/tests/parser/test_error_handling.py index 621268cb4..1689149f5 100644 --- a/tests/parser/test_error_handling.py +++ b/tests/parser/test_error_handling.py @@ -781,6 +781,43 @@ def test_fortran_parser_ignores_foreign_c_declarations_after_execution_boundary( assert parsed.procedures[0].name == "mixed_body" +@pytest.mark.parametrize( + "code", + [ + "@@@\n", + """ +module bad_spec + @@@ +end module bad_spec +""", + """ +subroutine bad_spec() + @@@ +end subroutine bad_spec +""", + ], +) +def test_fortran_parser_rejects_invalid_syntax_outside_execution_bodies(code): + with pytest.raises(FortranParseError, match="Invalid Fortran syntax") as exc_info: + parse_fortran_file(code, filename="invalid_syntax.f90") + + assert exc_info.value.code == "PARSE_INVALID_SYNTAX" + + +def test_fortran_parser_ignores_invalid_syntax_after_execution_boundary(): + parsed = parse_fortran_file( + """ +subroutine ignored_body() + call noop() + @@@ +end subroutine ignored_body +""", + filename="invalid_body.f90", + ) + + assert parsed.procedures[0].name == "ignored_body" + + def test_foreign_c_check_preserves_valid_semicolon_separated_fortran_statements(): parsed = parse_fortran_file( """ diff --git a/tests/parser/test_parser_public_entrypoints.py b/tests/parser/test_parser_public_entrypoints.py index 4de59daba..d9892418d 100644 --- a/tests/parser/test_parser_public_entrypoints.py +++ b/tests/parser/test_parser_public_entrypoints.py @@ -73,13 +73,13 @@ def test_file_path_and_unknown_filename_public_parse_paths(tmp_path): """, filename="from_unknown.src", ) - parsed_literal = parse_fortran_file(12345) + with pytest.raises(FortranParseError, match="Invalid Fortran syntax"): + parse_fortran_file(12345) assert parsed_from_path.filename == str(source_path) assert parsed_from_path.format == "modern" assert parsed_from_path.procedures[0].name == "from_path" assert parsed_unknown_suffix.format == "unknown" - assert parsed_literal.procedures == [] def test_public_instance_visitor_entrypoints_use_source_strings(): parser = FortranParser() From e91fc6ba40105bc2dd40489823297bc1b540bddc Mon Sep 17 00:00:00 2001 From: said Date: Tue, 26 May 2026 17:12:47 +0100 Subject: [PATCH 3/8] errors --- c_parser/parser.py | 99 ++++----------------------- tests/parser/c/test_c_cli_skeleton.py | 8 +-- tests/parser/c/test_c_functions.py | 34 +++++---- 3 files changed, 38 insertions(+), 103 deletions(-) diff --git a/c_parser/parser.py b/c_parser/parser.py index bb16956e4..eaf8da429 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -46,6 +46,7 @@ from .lexer import ( CTopLevelSegment, + lex_c_source, line_mappings_for_source, split_top_level_c_source, strip_c_comments, @@ -120,42 +121,6 @@ _RAW_CONDITIONAL_DIRECTIVE_RE = re.compile( r"^\s*#\s*(?Pif|ifdef|ifndef|elif|else|endif)\b" ) -_FOREIGN_FORTRAN_LINES = ( - re.compile( - r"^\s*(?:pure\s+|elemental\s+|recursive\s+|module\s+)*" - r"subroutine\s+\w+\s*(?:\([^;{}]*\))?" - r"(?:\s+bind\s*\([^;{}]*\))?\s*$", - re.IGNORECASE, - ), - re.compile( - r"^\s*(?:pure\s+|elemental\s+|recursive\s+|module\s+)*" - r"(?:(?:integer|real|logical|complex|character)(?:\s*\([^;{}]*\))?|" - r"double\s+precision|type\s*\([^)]*\)|class\s*\([^)]*\))?" - r"\s*function\s+\w+\s*(?:\([^;{}]*\))?" - r"(?:\s+result\s*\([^;{}]*\))?" - r"(?:\s+bind\s*\([^;{}]*\))?\s*$", - re.IGNORECASE, - ), - re.compile( - r"^\s*end\s+(?:subroutine|function|module|submodule|program|interface|type|block\s+data)\b", - re.IGNORECASE, - ), - re.compile( - r"^\s*(?:module|program|block\s+data)\s+[A-Za-z_]\w*\s*$", - re.IGNORECASE, - ), - re.compile(r"^\s*submodule\s*\([^)]*\)\s+[A-Za-z_]\w*\s*$", re.IGNORECASE), - re.compile(r"^\s*(?:implicit\s+none|contains)\s*$", re.IGNORECASE), - re.compile( - r"^\s*use\s+(?:,\s*(?:intrinsic|non_intrinsic)\s*)?(?:::)?\s*[A-Za-z_]\w*" - r"(?:\s*,\s*only\s*:.*)?\s*$", - re.IGNORECASE, - ), - re.compile( - r"^\s*(?:(?:integer|real|logical|complex|character)\b|(?:type|class)\s*\([^)]*\)).*::", - re.IGNORECASE, - ), -) _PRIMITIVE_WORDS = { "void", "char", @@ -373,11 +338,6 @@ def visit_file( if preprocessing == "raw" and inferred_preprocessed_path is not None: preprocessing = "preprocessed" - self._raise_for_foreign_fortran_syntax( - source, - filename, - use_linemarkers=preprocessing in {"compiler", "preprocessed"}, - ) parsed = CFile(filename=filename, parser_status="partial", preprocessing=preprocessing) if preprocessing == "raw": effective_include_dirs = list(include_dirs or ()) @@ -616,50 +576,6 @@ def _source_location(self, segment: CTopLevelSegment) -> CSourceLocation: """Return the original start location for a top-level segment.""" return self._source_location_at(segment, 0) - @staticmethod - def _raise_for_foreign_fortran_syntax( - source: str, - filename: str | None, - *, - use_linemarkers: bool = False, - ) -> None: - """Reject unmistakable Fortran statements outside ignored C bodies.""" - for segment in split_top_level_c_source( - source, - filename=filename, - use_linemarkers=use_linemarkers, - ): - CParser._raise_for_foreign_fortran_segment(segment) - - @staticmethod - def _raise_for_foreign_fortran_segment(segment: CTopLevelSegment) -> None: - for line_offset, line in enumerate(segment.text.splitlines()): - if not any(pattern.match(line) for pattern in _FOREIGN_FORTRAN_LINES): - continue - filename = ( - segment.original_filenames[line_offset] - if line_offset < len(segment.original_filenames) - else segment.filename - ) - line_number = ( - segment.original_line_numbers[line_offset] - if line_offset < len(segment.original_line_numbers) - else segment.original_start_line + line_offset - ) - source_line = ( - segment.original_source_lines[line_offset] - if line_offset < len(segment.original_source_lines) - else segment.original_source_line - ) - raise CParseError( - "Fortran syntax is not valid C input; parse Fortran source with --language fortran.", - filename=filename, - line_number=line_number, - column=1, - source_line=source_line, - code="CPARSE_FOREIGN_FORTRAN_SYNTAX", - ) - @staticmethod def _could_start_c_external_declaration(text: str) -> bool: stripped = text.lstrip() @@ -668,7 +584,18 @@ def _could_start_c_external_declaration(text: str) -> bool: @staticmethod def _raise_for_invalid_top_level_syntax(segment: CTopLevelSegment) -> None: text = segment.text.strip() - if not text or CParser._could_start_c_external_declaration(text): + if not text or _looks_like_cxx_declaration(text): + return + tokens = lex_c_source(text) + has_scope_operator = any( + left.text == ":" and right.text == ":" + for left, right in zip(tokens, tokens[1:]) + ) + if ( + segment.terminator != "eof" + and not has_scope_operator + and CParser._could_start_c_external_declaration(text) + ): return raise CParseError( f"Invalid C syntax at top level: {text}", diff --git a/tests/parser/c/test_c_cli_skeleton.py b/tests/parser/c/test_c_cli_skeleton.py index 8234c28d6..2a9b25e6b 100644 --- a/tests/parser/c/test_c_cli_skeleton.py +++ b/tests/parser/c/test_c_cli_skeleton.py @@ -208,10 +208,10 @@ def test_cli_c_input_rejects_explicit_fortran_frontend(tmp_path: Path): assert not output.exists() -def test_cli_c_pyi_rejects_embedded_fortran_syntax(tmp_path: Path): +def test_cli_c_pyi_rejects_invalid_c_syntax(tmp_path: Path): header = tmp_path / "api.h" header.write_text( - "int add(int a, int b);\nsubroutine solve()\nend subroutine solve\n", + "int add(int a, int b);\nvalue_type :: state;\n", encoding="utf-8", ) result = subprocess.run( @@ -221,8 +221,8 @@ def test_cli_c_pyi_rejects_embedded_fortran_syntax(tmp_path: Path): ) assert result.returncode == 1 - assert "CPARSE_FOREIGN_FORTRAN_SYNTAX" in result.stderr - assert "Fortran syntax is not valid C input" in result.stderr + assert "CPARSE_INVALID_SYNTAX" in result.stderr + assert "Invalid C syntax" in result.stderr def test_cli_c_rejects_fortran_only_parse_flags(tmp_path: Path): diff --git a/tests/parser/c/test_c_functions.py b/tests/parser/c/test_c_functions.py index 9dbea3953..4fcb619f2 100644 --- a/tests/parser/c/test_c_functions.py +++ b/tests/parser/c/test_c_functions.py @@ -83,44 +83,42 @@ def test_old_style_knr_function_definition_raises_unsupported_diagnostic(): [ "subroutine solve()\nend subroutine solve\n", "integer function answer()\nend function answer\n", - "int add(int a, int b);\ninteger :: state\n", - "int add(int a, int b);\ntype(c_ptr) :: handle\n", + "int add(int a, int b);\ninteger :: state;\n", + "int add(int a, int b);\ntype(c_ptr) :: handle;\n", ], ) -def test_c_parser_rejects_foreign_fortran_syntax(source): +def test_c_parser_rejects_non_c_top_level_syntax(source): from c_parser import CParseError, parse_c_file - with pytest.raises(CParseError, match="Fortran syntax is not valid C input") as exc_info: + with pytest.raises(CParseError, match="Invalid C syntax") as exc_info: parse_c_file(source, filename="mixed.h") - assert exc_info.value.code == "CPARSE_FOREIGN_FORTRAN_SYNTAX" + assert exc_info.value.code == "CPARSE_INVALID_SYNTAX" -def test_c_parser_foreign_fortran_error_maps_preprocessed_source_location(): +def test_c_parser_invalid_syntax_error_maps_preprocessed_source_location(): from c_parser import CParseError, parse_c_file with pytest.raises(CParseError) as exc_info: parse_c_file( - '# 80 "generated.f90"\ninteger :: state\n', + '# 80 "generated.input"\nvalue_type :: state;\n', filename="translation.i", preprocessing="preprocessed", ) - assert exc_info.value.code == "CPARSE_FOREIGN_FORTRAN_SYNTAX" - assert exc_info.value.filename == "generated.f90" + assert exc_info.value.code == "CPARSE_INVALID_SYNTAX" + assert exc_info.value.filename == "generated.input" assert exc_info.value.line_number == 80 -def test_c_parser_ignores_foreign_fortran_syntax_inside_function_body(): +def test_c_parser_skips_non_c_tokens_inside_function_body(): from c_parser import parse_c_file parsed = parse_c_file( """ int run(void) { - subroutine solve() - integer :: state - end subroutine solve + value_type :: state; return 0; } """, @@ -130,6 +128,16 @@ def test_c_parser_ignores_foreign_fortran_syntax_inside_function_body(): assert [function.name for function in parsed.functions] == ["run"] +def test_c_parser_does_not_classify_valid_c_from_typedef_identifier_spelling(): + from c_parser import CTypedef, parse_c_file + + parsed = parse_c_file("subroutine solve(void);\n", filename="identifier_spelling.h") + + assert [function.name for function in parsed.functions] == ["solve"] + assert isinstance(parsed.functions[0].result_type, CTypedef) + assert parsed.functions[0].result_type.name == "subroutine" + + @pytest.mark.parametrize("source", ["@@@\n", "int run(void);\n@@@;\n"]) def test_c_parser_rejects_invalid_top_level_syntax(source): from c_parser import CParseError, parse_c_file From e9fbb19df4b62c4c046ab276155dadb4eddeb68e Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Wed, 27 May 2026 02:54:52 +0100 Subject: [PATCH 4/8] codex: clarify file-scope syntax guard as language-agnostic --- fortran_parser/parser.py | 83 +++-------------------------- tests/parser/c/test_c_functions.py | 4 +- tests/parser/test_cli.py | 4 +- tests/parser/test_error_handling.py | 13 +++-- 4 files changed, 18 insertions(+), 86 deletions(-) diff --git a/fortran_parser/parser.py b/fortran_parser/parser.py index 08e4f11b6..e2500b581 100644 --- a/fortran_parser/parser.py +++ b/fortran_parser/parser.py @@ -118,37 +118,6 @@ "unsupported_procedure_pointer", "unsupported_c_ptr", ) -_FOREIGN_C_DECLARATION = re.compile( - r""" - ^\s* - (?:(?:typedef|extern|static|register|inline|const|volatile|restrict|_Atomic)\s+)* - (?: - (?:signed|unsigned)(?:\s+(?:char|short|int|long)(?:\s+long)?)? - |(?:void|char|short|int|long|float|double)(?:\s+(?:int|long|_Complex))? - |(?:struct|union|enum)\s+[A-Za-z_]\w* - ) - \s+(?:\*+\s*)?[A-Za-z_]\w*\s*(?:\(|\[|=|;|\{) - """, - re.IGNORECASE | re.VERBOSE, -) -_FOREIGN_C_ALIAS_DECLARATION = re.compile( - r""" - ^\s* - (?! - (?:allocate|associate|backspace|block|call|case|close|continue|cycle| - data|deallocate|do|else|elseif|end|error|exit|external|format| - goto|go|if|implicit|include|inquire|intrinsic|nullify|open| - parameter|pause|print|read|return|rewind|save|select|stop|use| - wait|where|write|integer|real|complex|logical|character|double| - type|class|procedure)\b - ) - (?:(?:typedef|extern|static|register|inline|const|volatile|restrict|_Atomic)\s+)* - [A-Za-z_]\w*\s+(?:\*+\s*)?[A-Za-z_]\w* - \s*(?:\([^;{}]*\)|\[[^\]]*\])? - \s*(?:=[^;{}]*)?[;{]\s*$ - """, - re.IGNORECASE | re.VERBOSE, -) _PreprocessedLines = list[tuple[str, int | None, str | None]] @@ -1169,7 +1138,12 @@ def _helper_validate_unit_headers(self, lines: _PreprocessedLines, filename: str ) def _helper_validate_file_scope_unparsed_lines(self, lines: _PreprocessedLines, filename: str | None) -> None: - """Reject foreign C declarations not owned by a Fortran source unit.""" + """Reject any non-Fortran syntax outside recognized unit bodies. + + This guard is intentionally language-agnostic: lines that are neither + valid file-scope Fortran constructs nor part of a recognized unit are + rejected via the generic invalid-syntax diagnostic path. + """ index = 0 pp_condition_stack: list[tuple[int, int]] = [] pp_active_stack: list[bool] = [] @@ -1203,12 +1177,6 @@ def _helper_validate_file_scope_unparsed_lines(self, lines: _PreprocessedLines, if self._is_executable_statement_start(stripped): index += 1 continue - self._raise_if_foreign_c_syntax_line( - stripped, - filename=filename, - lineno=lineno, - source_line=source_line, - ) self._raise_invalid_fortran_syntax_line( stripped, context="file scope", @@ -1232,27 +1200,6 @@ def _is_allowed_unparsed_file_scope_line(line: str) -> bool: or FortranParser._is_openmp_directive(stripped) ) - @staticmethod - def _raise_if_foreign_c_syntax_line( - line: str, - *, - filename: str | None, - lineno: int | None, - source_line: str | None, - ) -> None: - if not ( - _FOREIGN_C_DECLARATION.match(line) - or _FOREIGN_C_ALIAS_DECLARATION.match(line) - ): - return - raise FortranParseError( - "C declaration syntax is not valid Fortran input; parse C source with --language c.", - filename=filename, - line_number=lineno, - source_line=source_line, - code="PARSE_FOREIGN_C_SYNTAX", - ) - @staticmethod def _raise_invalid_fortran_syntax_line( line: str, @@ -1262,12 +1209,6 @@ def _raise_invalid_fortran_syntax_line( lineno: int | None, source_line: str | None, ) -> None: - FortranParser._raise_if_foreign_c_syntax_line( - line, - filename=filename, - lineno=lineno, - source_line=source_line, - ) raise FortranParseError( f"Invalid Fortran syntax in {context}: {line.strip()}", filename=filename, @@ -2183,12 +2124,6 @@ def _helper_visit_spec_part( continue if stripped.startswith("#"): continue - self._raise_if_foreign_c_syntax_line( - stripped, - filename=filename, - lineno=lineno, - source_line=source_line, - ) if scope.kind == "procedure": self._helper_visit_procedure_spec_line( stripped, @@ -2464,12 +2399,6 @@ def _parse_derived_type_contains_line( lineno: int | None = None, source_line: str | None = None, ) -> None: - self._raise_if_foreign_c_syntax_line( - line, - filename=filename, - lineno=lineno, - source_line=source_line, - ) proc_binding = _REGEX["procedure_binding"].match(line) if proc_binding: binding_names = split_csv(proc_binding.group("names")) diff --git a/tests/parser/c/test_c_functions.py b/tests/parser/c/test_c_functions.py index 4fcb619f2..98f5d8932 100644 --- a/tests/parser/c/test_c_functions.py +++ b/tests/parser/c/test_c_functions.py @@ -81,8 +81,8 @@ def test_old_style_knr_function_definition_raises_unsupported_diagnostic(): @pytest.mark.parametrize( "source", [ - "subroutine solve()\nend subroutine solve\n", - "integer function answer()\nend function answer\n", + "def solve():\n return 0\n", + "lambda x: x\n", "int add(int a, int b);\ninteger :: state;\n", "int add(int a, int b);\ntype(c_ptr) :: handle;\n", ], diff --git a/tests/parser/test_cli.py b/tests/parser/test_cli.py index d1bb95995..1a3193098 100644 --- a/tests/parser/test_cli.py +++ b/tests/parser/test_cli.py @@ -563,8 +563,8 @@ def test_cli_fortran_rejects_embedded_c_declaration_outside_execution_body(tmp_p ) assert result.returncode == 1 - assert "PARSE_FOREIGN_C_SYNTAX" in result.stderr - assert "C declaration syntax is not valid Fortran input" in result.stderr + assert "PARSE001" in result.stderr + assert "Unknown or unsupported datatype declaration" in result.stderr def test_cli_parse_shows_module_derived_types_and_derived_arg_kinds(): diff --git a/tests/parser/test_error_handling.py b/tests/parser/test_error_handling.py index 1689149f5..68f621586 100644 --- a/tests/parser/test_error_handling.py +++ b/tests/parser/test_error_handling.py @@ -760,14 +760,17 @@ def test_slicer_reports_missing_end_unit(): """, ], ) -def test_fortran_parser_rejects_foreign_c_declarations_outside_execution_bodies(code): - with pytest.raises(FortranParseError, match="C declaration syntax is not valid Fortran input") as exc_info: +def test_fortran_parser_rejects_invalid_non_fortran_syntax_outside_execution_bodies(code): + with pytest.raises( + FortranParseError, + match=r"Invalid Fortran syntax|Unknown or unsupported datatype declaration", + ) as exc_info: parse_fortran_file(code, filename="mixed.f90") - assert exc_info.value.code == "PARSE_FOREIGN_C_SYNTAX" + assert exc_info.value.code in {"PARSE_INVALID_SYNTAX", "PARSE001"} -def test_fortran_parser_ignores_foreign_c_declarations_after_execution_boundary(): +def test_fortran_parser_ignores_non_fortran_syntax_after_execution_boundary(): parsed = parse_fortran_file( """ subroutine mixed_body() @@ -818,7 +821,7 @@ def test_fortran_parser_ignores_invalid_syntax_after_execution_boundary(): assert parsed.procedures[0].name == "ignored_body" -def test_foreign_c_check_preserves_valid_semicolon_separated_fortran_statements(): +def test_invalid_syntax_guard_preserves_valid_semicolon_separated_fortran_statements(): parsed = parse_fortran_file( """ subroutine valid_body(x) From 5f8ef246ee50bf3dbdaf6a0ced1041acb8c91368 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Wed, 27 May 2026 02:56:32 +0100 Subject: [PATCH 5/8] codex: generalize syntax-error wording in parser docs --- docs/c_parser/c_parser_cli_workflow.md | 2 +- docs/fortran/fortran_parser.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/c_parser/c_parser_cli_workflow.md b/docs/c_parser/c_parser_cli_workflow.md index d0086a9ca..8a9811532 100644 --- a/docs/c_parser/c_parser_cli_workflow.md +++ b/docs/c_parser/c_parser_cli_workflow.md @@ -46,7 +46,7 @@ unknown-suffix source inputs require an explicit frontend selection. A known C path explicitly passed with `--language fortran` is rejected before parsing, so it cannot silently produce an empty Fortran interface. The parser also rejects unmistakable Fortran unit or declaration syntax found in C input, and -the Fortran parser rejects unmistakable C declarations outside execution +the Fortran parser rejects unsupported non-Fortran syntax outside execution regions that are intentionally not modeled. The C parser output differs from Fortran parser output by using C-specific diff --git a/docs/fortran/fortran_parser.md b/docs/fortran/fortran_parser.md index a02b29ad9..7a03b2046 100644 --- a/docs/fortran/fortran_parser.md +++ b/docs/fortran/fortran_parser.md @@ -210,7 +210,7 @@ python -m x2py path/to/fortran_src --language fortran --parse Fortran directories are recursively scanned for `.f`, `.for`, `.ftn`, `.f90`, `.f95`, `.f03`, `.f08`. -The Fortran frontend rejects unmistakable C declaration syntax before +The Fortran frontend rejects unsupported non-Fortran syntax before wrapper-focused parsing when it appears outside executable procedure/program bodies, which are intentionally not represented in the extracted interface. From 65bb167c709434316fa5c69e091626e373664dfa Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Wed, 27 May 2026 03:07:56 +0100 Subject: [PATCH 6/8] Update c_parser_cli_workflow.md --- docs/c_parser/c_parser_cli_workflow.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/c_parser/c_parser_cli_workflow.md b/docs/c_parser/c_parser_cli_workflow.md index 8a9811532..717b66706 100644 --- a/docs/c_parser/c_parser_cli_workflow.md +++ b/docs/c_parser/c_parser_cli_workflow.md @@ -45,8 +45,8 @@ recognizable Fortran files and `.pyi` readiness inputs, but a `.c`, `.h`, or unknown-suffix source inputs require an explicit frontend selection. A known C path explicitly passed with `--language fortran` is rejected before parsing, so it cannot silently produce an empty Fortran interface. The parser also -rejects unmistakable Fortran unit or declaration syntax found in C input, and -the Fortran parser rejects unsupported non-Fortran syntax outside execution +rejects foreign syntax found in C input, and the Fortran parser +rejects unsupported non-Fortran syntax outside execution regions that are intentionally not modeled. The C parser output differs from Fortran parser output by using C-specific From df998ef26c8ce9d0fa524d1be571484fb57b9f4f Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Wed, 27 May 2026 03:09:39 +0100 Subject: [PATCH 7/8] Update c_parser_reference.md --- docs/c_parser/c_parser_reference.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/c_parser/c_parser_reference.md b/docs/c_parser/c_parser_reference.md index 537d656ef..feeffdd62 100644 --- a/docs/c_parser/c_parser_reference.md +++ b/docs/c_parser/c_parser_reference.md @@ -490,8 +490,7 @@ x2py path/to/api.h --language c --parse --out report.json There is no separate `--parse-c` alias: `--language c --parse` is the shared language-selection form. Auto-detection remains deferred: a `.c`, `.h`, or `.i` input without `--language c` exits with language-selection guidance. -Explicit C input containing unmistakable top-level Fortran unit or declaration -syntax raises a fatal parser diagnostic instead of emitting a partial C +Explicit C input containing unmistakable non-C syntax raises a fatal parser diagnostic instead of emitting a partial C interface. ## Current JSON Output From a3920bfcf991f14dbfa74e66ca711aa9ba708f17 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Wed, 27 May 2026 03:14:01 +0100 Subject: [PATCH 8/8] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2c081cbcf..828fb1863 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,8 @@ path when `--language` is omitted. C source/header files require explicit generation, and wrap-readiness are available in explicit C mode. Selecting a frontend that conflicts with a recognized C or Fortran source suffix is an error. Once selected, a frontend also rejects unmistakable declarations or -program-unit syntax from the other language outside ignored execution/function -bodies rather than silently dropping it. +program-unit syntax that are not from the selected language outside ignored execution/function +bodies, rather than silently dropping them. For parse output, `--show-vars` expands scope-level variables that are normally summarized as `vars=N`. Use `--print-limit N` to keep large repeated sections