Skip to content

Commit 358b872

Browse files
authored
Merge pull request #37 from PyNumLab/codex/review-fortran-parser-behavior
Treat executable statements at file scope as syntax errors
2 parents 03d2d0b + 612f642 commit 358b872

40 files changed

Lines changed: 1197 additions & 443 deletions

‎README.md‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,18 @@ path when `--language` is omitted. C source/header files require explicit
112112
`--language fortran` or `--language c`. C parsing, semantic IR, `.pyi`
113113
generation, and wrap-readiness are available in explicit C mode. Selecting a
114114
frontend that conflicts with a recognized C or Fortran source suffix is an
115-
error. Once selected, a frontend also rejects unmistakable declarations or
116-
program-unit syntax that are not from the selected language outside ignored execution/function
117-
bodies, rather than silently dropping them.
115+
error. Once selected, a frontend validates the grammar regions it models and
116+
rejects unparsed syntax outside intentionally ignored execution/function
117+
bodies, rather than guessing another language from keyword spellings or
118+
silently dropping malformed input.
119+
120+
Parse failures print a compiler-style diagnostic without a Python traceback.
121+
Use `--debug` to re-raise the parser error and print the traceback;
122+
`--debug-traceback` remains accepted as a compatibility alias. Diagnostic codes
123+
such as `PARSE_UNSUPPORTED_DECLARATION`, `CPARSE_INVALID_SPECIFIER_SEQUENCE`,
124+
and `CPARSE_INVALID_SYNTAX` are stable, explicit error-category identifiers for
125+
tests, tools, and documentation. The current categories are listed in
126+
[`docs/diagnostic_codes.md`](docs/diagnostic_codes.md).
118127

119128
For parse output, `--show-vars` expands scope-level variables that are normally
120129
summarized as `vars=N`. Use `--print-limit N` to keep large repeated sections
@@ -718,8 +727,12 @@ visitor then parses only its own substring, splits it into header,
718727
specification, optional execution, and optional `contains` regions, and recurses
719728
into direct child units where that grammar allows children. Shared declaration
720729
helpers parse variables, procedure arguments/results, and type fields, then
721-
push them into the active scope. Procedure execution bodies and internal
722-
subprograms are ignored for wrapper metadata; procedure-local interfaces are
730+
push them into the active scope. Nested unit boundaries and placement outside
731+
execution regions are checked even when they do not produce wrapper metadata.
732+
Internal procedures inside a host procedure's `contains` block are
733+
structurally sliced, then their declarations and bodies are skipped. After an
734+
execution boundary is detected, procedure bodies and standalone included
735+
execution fragments are intentionally skipped. Procedure-local interfaces are
723736
retained for callback typing.
724737
Parameter variables keep both `value` and serialized `symbolic_value` when the
725738
parser has that information. `value` is literal/evaluated only; if an

‎c_parser/cli.py‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,26 @@
33

44
import argparse
55
import json
6+
import os
7+
import sys
68
from collections.abc import Callable, Sequence
79
from pathlib import Path
810
from typing import Any
911

10-
from .models import CFile, c_model_to_dict
12+
from .models import CFile, CParseError, c_model_to_dict
1113
from .parser import CParser
1214

1315

1416
_C_SOURCE_SUFFIXES = {".c", ".h", ".i"}
17+
_TRUE_VALUES = {"1", "true", "yes", "on"}
18+
19+
20+
def _env_flag(name: str) -> bool:
21+
return os.getenv(name, "").strip().lower() in _TRUE_VALUES
22+
23+
24+
def _diagnostic_color_enabled(*, disabled: bool) -> bool:
25+
return not disabled and "NO_COLOR" not in os.environ
1526

1627

1728
def _collect_c_extensions(path: Path) -> list[Path]:
@@ -88,9 +99,23 @@ def main(argv: list[str] | None = None) -> int:
8899
parser.add_argument("paths", nargs="+", help="C source/header file(s) or directory path(s)")
89100
parser.add_argument("--json", action="store_true", help="Print JSON to stdout")
90101
parser.add_argument("--out", type=str, help="Write parser JSON to a file")
102+
parser.add_argument("--no-color", action="store_true", help="Disable ANSI color in parse diagnostics")
103+
parser.add_argument(
104+
"--debug",
105+
"--debug-traceback",
106+
dest="debug",
107+
action="store_true",
108+
help="Re-raise parser errors so Python prints a traceback for parser debugging.",
109+
)
91110
args = parser.parse_args(argv)
92111

93-
payload = parse_c_report(args.paths)
112+
try:
113+
payload = parse_c_report(args.paths)
114+
except CParseError as exc:
115+
if args.debug or _env_flag("C_PARSER_DEBUG"):
116+
raise
117+
print(exc.format_diagnostic(color=_diagnostic_color_enabled(disabled=args.no_color), debug=False), file=sys.stderr)
118+
return 1
94119
if args.out:
95120
Path(args.out).write_text(json.dumps(payload, indent=2), encoding="utf-8")
96121
return 0

‎c_parser/models.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def c_model_to_dict(obj: Any, _seen: set[int] | None = None) -> Any:
106106
class CParseError(ValueError):
107107
"""C parser error with compiler-style diagnostic rendering support."""
108108

109-
default_code = "CPARSE001"
109+
default_code = "CPARSE_ERROR"
110110

111111
def __init__(
112112
self,

‎c_parser/parser.py‎

Lines changed: 80 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,6 @@
116116
"_Alignas",
117117
"alignas",
118118
)
119-
_CXX_DECLARATION_KEYWORDS = {"using", "namespace", "template", "class"}
120-
_CXX_ACCESS_SPECIFIERS = {"public", "private", "protected"}
121119
_RAW_CONDITIONAL_DIRECTIVE_RE = re.compile(
122120
r"^\s*#\s*(?P<directive>if|ifdef|ifndef|elif|else|endif)\b"
123121
)
@@ -230,6 +228,12 @@ class _InvalidSpecifierSequence(ValueError):
230228
pass
231229

232230

231+
class _InvalidCGrammarSyntax(ValueError):
232+
"""Raised internally when a nested C grammar region is malformed."""
233+
234+
pass
235+
236+
233237
def _looks_like_existing_source_path(value: object) -> bool:
234238
"""Return whether `value` can safely be treated as an existing source path."""
235239
if isinstance(value, Path):
@@ -274,21 +278,6 @@ def _is_source_key(key: str) -> bool:
274278
return PurePosixPath(key).suffix.lower() == ".c"
275279

276280

277-
def _looks_like_cxx_declaration(text: str) -> bool:
278-
"""Detect obvious C++ declarations so they become explicit diagnostics."""
279-
stripped = text.lstrip()
280-
identifier = _IDENTIFIER_RE.match(stripped)
281-
if identifier is None:
282-
return False
283-
284-
word = identifier.group(0)
285-
if word in _CXX_DECLARATION_KEYWORDS:
286-
return True
287-
if word in _CXX_ACCESS_SPECIFIERS:
288-
return stripped[identifier.end() :].lstrip().startswith(":")
289-
return False
290-
291-
292281
class CParser:
293282
"""Parser orchestration object for the partial typed C model.
294283
@@ -584,7 +573,7 @@ def _could_start_c_external_declaration(text: str) -> bool:
584573
@staticmethod
585574
def _raise_for_invalid_top_level_syntax(segment: CTopLevelSegment) -> None:
586575
text = segment.text.strip()
587-
if not text or _looks_like_cxx_declaration(text):
576+
if not text:
588577
return
589578
tokens = lex_c_source(text)
590579
has_scope_operator = any(
@@ -606,6 +595,25 @@ def _raise_for_invalid_top_level_syntax(segment: CTopLevelSegment) -> None:
606595
code="CPARSE_INVALID_SYNTAX",
607596
)
608597

598+
def _invalid_syntax_error(
599+
self,
600+
segment: CTopLevelSegment,
601+
text: str,
602+
*,
603+
context: str,
604+
offset: int = 0,
605+
) -> CParseError:
606+
"""Build the fatal diagnostic used when a C grammar region is invalid."""
607+
location = self._source_location_at(segment, offset)
608+
return CParseError(
609+
f"Invalid C syntax in {context}: {text.strip()}",
610+
filename=location.filename,
611+
line_number=location.line,
612+
column=location.column,
613+
source_line=location.source_line,
614+
code="CPARSE_INVALID_SYNTAX",
615+
)
616+
609617
def _macro_dependencies(
610618
self,
611619
source: str,
@@ -1183,7 +1191,7 @@ def _invalid_specifier_error(
11831191
line_number=location.line,
11841192
column=location.column,
11851193
source_line=location.source_line,
1186-
code="CPARSE003",
1194+
code="CPARSE_INVALID_SPECIFIER_SEQUENCE",
11871195
)
11881196

11891197
def _atomic_type_specifier_parts(self, spec_text: str) -> tuple[str, str] | None:
@@ -1669,7 +1677,7 @@ def _parse_parameter(self, text: str) -> CParameter | None:
16691677
return None
16701678
spec_text, declarator = self._split_declaration_specifiers(stripped)
16711679
if not spec_text:
1672-
return None
1680+
raise _InvalidCGrammarSyntax(f"Invalid parameter declaration: {stripped}")
16731681
name, type_, _storage, _function_specifiers, _direct_function = self._build_declared_type(
16741682
spec_text,
16751683
declarator,
@@ -1724,13 +1732,17 @@ def _parse_parameters(self, parameters_text: str) -> tuple[list[CParameter], boo
17241732

17251733
parameters: list[CParameter] = []
17261734
variadic = False
1727-
for item in top_level_split(stripped, ","):
1735+
items = top_level_split(stripped, ",")
1736+
for index, item in enumerate(items):
17281737
if item == "...":
1738+
if variadic or index != len(items) - 1:
1739+
raise _InvalidCGrammarSyntax("The variadic marker must be the final function parameter.")
17291740
variadic = True
17301741
continue
17311742
parameter = self._parse_parameter(item)
1732-
if parameter is not None:
1733-
parameters.append(parameter)
1743+
if parameter is None:
1744+
raise _InvalidCGrammarSyntax(f"Invalid parameter declaration: {item}")
1745+
parameters.append(parameter)
17341746
return parameters, variadic
17351747

17361748
def _is_knr_definition(self, segment: CTopLevelSegment, parameters_text: str) -> bool:
@@ -1803,7 +1815,7 @@ def _raise_for_unsupported_old_style_definitions(
18031815
line_number=mapping.line if mapping is not None else index + 1,
18041816
column=max(line.find(name_match.group(0)) + 1, 1),
18051817
source_line=source_line,
1806-
code="CPARSE002",
1818+
code="CPARSE_UNSUPPORTED_KNR_DEFINITION",
18071819
)
18081820
if stripped.endswith(";"):
18091821
saw_old_style_declaration = True
@@ -1821,7 +1833,7 @@ def _raise_for_unsupported_old_style_definitions(
18211833
line_number=mapping.line if mapping is not None else index + 1,
18221834
column=max(line.find(name_match.group(0)) + 1, 1),
18231835
source_line=source_line,
1824-
code="CPARSE002",
1836+
code="CPARSE_UNSUPPORTED_KNR_DEFINITION",
18251837
)
18261838

18271839
def _prototype_style(self, parameters_text: str) -> str:
@@ -1857,7 +1869,7 @@ def _parse_function(self, segment: CTopLevelSegment) -> CFunction | None:
18571869
line_number=segment.original_start_line,
18581870
column=segment.original_start_column,
18591871
source_line=segment.original_source_line,
1860-
code="CPARSE002",
1872+
code="CPARSE_UNSUPPORTED_KNR_DEFINITION",
18611873
)
18621874
return self._function_from_type(
18631875
name,
@@ -2196,6 +2208,13 @@ def _parse_fields(
21962208
"""Parse struct/union member declarations through the shared backend."""
21972209
members: list[CVariable] = []
21982210
diagnostics: list[CDiagnostic] = []
2211+
if body.strip() and not body.rstrip().endswith(";"):
2212+
raise self._invalid_syntax_error(
2213+
segment,
2214+
body,
2215+
context=f"{owner_kind} field declaration",
2216+
offset=body_offset,
2217+
)
21992218
for text, field_offset in top_level_split_with_offsets(body, ";"):
22002219
member_offset = body_offset + field_offset
22012220
member_location = self._source_location_at(segment, member_offset)
@@ -2239,17 +2258,21 @@ def _parse_fields(
22392258
)
22402259
)
22412260
continue
2261+
if "::" in text:
2262+
raise self._invalid_syntax_error(
2263+
segment,
2264+
text,
2265+
context=f"{owner_kind} field declaration",
2266+
offset=member_offset,
2267+
)
22422268
spec_text, declarator_list = self._split_declaration_specifiers(text)
22432269
if not spec_text or not declarator_list:
2244-
diagnostics.append(
2245-
self._field_diagnostic(
2246-
segment,
2247-
owner_kind,
2248-
"Unsupported field declaration.",
2249-
offset=member_offset,
2250-
)
2270+
raise self._invalid_syntax_error(
2271+
segment,
2272+
text,
2273+
context=f"{owner_kind} field declaration",
2274+
offset=member_offset,
22512275
)
2252-
continue
22532276
for declarator in top_level_split(declarator_list, ","):
22542277
declaration, _initializer = top_level_partition(declarator, "=")
22552278
declaration, bit_width = top_level_partition(declaration, ":")
@@ -2293,10 +2316,10 @@ def _parse_enumerators(self, body: str, segment: CTopLevelSegment) -> list[CEnum
22932316
name_text, value = top_level_partition(item, "=")
22942317
identifier = self._read_identifier(name_text.strip(), 0)
22952318
if identifier is None:
2296-
continue
2319+
raise self._invalid_syntax_error(segment, item, context="enum member")
22972320
name, end = identifier
22982321
if name_text[end:].strip():
2299-
continue
2322+
raise self._invalid_syntax_error(segment, item, context="enum member")
23002323
constants.append(
23012324
CEnumerator(
23022325
name=name,
@@ -2402,7 +2425,6 @@ def _parse_declaration(
24022425
not text
24032426
or text.startswith("_Static_assert")
24042427
or self._has_unsupported_declaration_marker(text)
2405-
or _looks_like_cxx_declaration(text)
24062428
):
24072429
return [], [], [], []
24082430

@@ -2418,13 +2440,10 @@ def _unsupported_declaration_diagnostic(self, segment: CTopLevelSegment) -> CDia
24182440
if not text:
24192441
return None
24202442

2421-
kind = "unsupported_declaration"
2422-
message = "Unsupported C declaration form."
2443+
kind = ""
2444+
message = ""
24232445

2424-
if _looks_like_cxx_declaration(text):
2425-
kind = "cxx_declaration"
2426-
message = "C++ declaration syntax is not supported by the C parser."
2427-
elif text.startswith("struct "):
2446+
if text.startswith("struct "):
24282447
kind = "struct_definition"
24292448
message = "Struct definitions are not supported yet."
24302449
elif text.startswith("union "):
@@ -2445,6 +2464,8 @@ def _unsupported_declaration_diagnostic(self, segment: CTopLevelSegment) -> CDia
24452464
elif "{" in text or "}" in text:
24462465
kind = "brace_declaration"
24472466
message = "Unsupported declaration containing braces."
2467+
else:
2468+
return None
24482469

24492470
return CDiagnostic(
24502471
code="C_UNSUPPORTED_DECLARATION",
@@ -2521,12 +2542,10 @@ def _parse_translation_unit(
25212542
)
25222543
)
25232544
continue
2524-
if _looks_like_cxx_declaration(segment.text):
2525-
unsupported = self._unsupported_declaration_diagnostic(segment)
2526-
if unsupported is not None:
2527-
diagnostics.append(unsupported)
2528-
continue
2529-
tag_definition = self._parse_tag_definition(segment)
2545+
try:
2546+
tag_definition = self._parse_tag_definition(segment)
2547+
except _InvalidCGrammarSyntax as error:
2548+
raise self._invalid_syntax_error(segment, str(error), context="nested declaration") from None
25302549
if tag_definition is not None:
25312550
aggregate, parsed_functions, parsed_typedefs, parsed_variables, parsed_diagnostics = tag_definition
25322551
if isinstance(aggregate, CStruct):
@@ -2551,6 +2570,8 @@ def _parse_translation_unit(
25512570
except _UnsupportedDeclaratorSyntax as error:
25522571
diagnostics.append(self._declarator_diagnostic(segment, str(error)))
25532572
continue
2573+
except _InvalidCGrammarSyntax as error:
2574+
raise self._invalid_syntax_error(segment, str(error), context="function declaration") from None
25542575
if function is not None:
25552576
function.condition_set = condition_sets.get(
25562577
segment.original_start_line,
@@ -2562,17 +2583,21 @@ def _parse_translation_unit(
25622583
unsupported = self._unsupported_declaration_diagnostic(segment)
25632584
if unsupported is not None:
25642585
diagnostics.append(unsupported)
2565-
continue
2586+
continue
2587+
raise self._invalid_syntax_error(segment, segment.text, context="top level")
25662588
forward_tag = self._forward_tag(segment)
25672589
if isinstance(forward_tag, CStruct):
25682590
structs.append(forward_tag)
25692591
continue
25702592
if isinstance(forward_tag, CUnion):
25712593
unions.append(forward_tag)
25722594
continue
2573-
parsed_functions, parsed_typedefs, parsed_variables, declarator_diagnostics = self._parse_declaration(
2574-
segment
2575-
)
2595+
try:
2596+
parsed_functions, parsed_typedefs, parsed_variables, declarator_diagnostics = self._parse_declaration(
2597+
segment
2598+
)
2599+
except _InvalidCGrammarSyntax as error:
2600+
raise self._invalid_syntax_error(segment, str(error), context="declaration") from None
25762601
functions.extend(parsed_functions)
25772602
for function in parsed_functions:
25782603
function.condition_set = condition_sets.get(
@@ -2593,6 +2618,8 @@ def _parse_translation_unit(
25932618
unsupported = self._unsupported_declaration_diagnostic(segment)
25942619
if unsupported is not None:
25952620
diagnostics.append(unsupported)
2621+
else:
2622+
raise self._invalid_syntax_error(segment, segment.text, context="top level")
25962623

25972624
return functions, structs, unions, enums, typedefs, variables, diagnostics
25982625

0 commit comments

Comments
 (0)