Skip to content
Merged
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
1 change: 1 addition & 0 deletions c_parser/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ class CMacroDependency:
name: str
context: str = "declaration"
source_location: CSourceLocation | None = None
source_text: str = ""


@dataclass
Expand Down
91 changes: 76 additions & 15 deletions c_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,21 +252,48 @@ def _macro_dependencies(
return dependencies

for segment in split_top_level_c_source(source, filename=filename):
text = segment.text.strip()
if not text:
continue
for macro_name in sorted(macro_names):
if re.search(rf"\b{re.escape(macro_name)}\s*\(", text):
dependencies.append(
CMacroDependency(
name=macro_name,
context="declaration",
source_location=self._source_location(segment),
)
)
break
dependency = self._segment_macro_dependency(segment, macro_names)
if dependency is not None:
dependencies.append(dependency)
return dependencies

def _segment_macro_dependency(
self,
segment: CTopLevelSegment,
macro_names: set[str],
) -> CMacroDependency | None:
text = segment.text.strip()
if not text:
return None
declaration_text, initializer = top_level_partition(text, "=")
scan_text = declaration_text if initializer is not None else text
for macro_name in sorted(macro_names):
if re.search(rf"\b{re.escape(macro_name)}\s*\(", scan_text):
return CMacroDependency(
name=macro_name,
context="declaration",
source_text=text,
source_location=self._source_location(segment),
)
return None

def _macro_dependent_declaration_diagnostic(
self,
segment: CTopLevelSegment,
dependency: CMacroDependency,
) -> CDiagnostic:
return CDiagnostic(
code="C_MACRO_DEPENDENT_DECLARATION",
message=(
f"Declaration depends on function-like macro {dependency.name!r}; "
"provide preprocessed input to parse it."
),
severity="warning",
location=dependency.source_location or self._source_location(segment),
unit_kind="macro_dependent_declaration",
unit_name=dependency.name,
)

def _has_unsupported_declaration_marker(self, text: str) -> bool:
return any(marker in text for marker in _UNSUPPORTED_DECLARATION_MARKERS)

Expand Down Expand Up @@ -1632,6 +1659,18 @@ def _parse_declaration(

return self._declarations_from_declarators(spec_text, declarator_list, segment)

def _is_braced_initializer_declaration(self, segment: CTopLevelSegment) -> bool:
text = segment.text.strip()
if not text:
return False
if "{" in text and "}" in text and "=" in text:
return True
if segment.terminator != "block":
return False

declaration, initializer = top_level_partition(text, "=")
return bool(declaration) and initializer is not None

def _unsupported_declaration_diagnostic(self, segment: CTopLevelSegment) -> CDiagnostic | None:
text = segment.text.strip()
if not text:
Expand All @@ -1640,7 +1679,10 @@ def _unsupported_declaration_diagnostic(self, segment: CTopLevelSegment) -> CDia
kind = "unsupported_declaration"
message = "Unsupported C declaration form."

if text.startswith("struct "):
if self._is_braced_initializer_declaration(segment):
kind = "braced_initializer_declaration"
message = "Braced or designated initializer declarations are not supported yet."
elif text.startswith("struct "):
kind = "struct_definition"
message = "Struct definitions are not supported yet."
elif text.startswith("union "):
Expand All @@ -1661,6 +1703,9 @@ def _unsupported_declaration_diagnostic(self, segment: CTopLevelSegment) -> CDia
elif "_Atomic(" in text:
kind = "atomic_type_declaration"
message = "_Atomic(type) declarations are not supported yet."
elif "{" in text or "}" in text:
kind = "brace_declaration"
message = "Unsupported declaration containing braces."

return CDiagnostic(
code="C_UNSUPPORTED_DECLARATION",
Expand All @@ -1675,6 +1720,8 @@ def _parse_translation_unit(
self,
source: str,
filename: str | None,
*,
function_like_macros: set[str] | None = None,
) -> tuple[
list[CFunction],
list[CStruct],
Expand All @@ -1694,7 +1741,14 @@ def _parse_translation_unit(
variables: list[CVariable] = []
diagnostics: list[CDiagnostic] = []

macro_names = function_like_macros or set()
for segment in split_top_level_c_source(source, filename=filename):
macro_dependency = self._segment_macro_dependency(segment, macro_names)
if macro_dependency is not None:
diagnostics.append(
self._macro_dependent_declaration_diagnostic(segment, macro_dependency)
)
continue
tag_definition = self._parse_tag_definition(segment)
if tag_definition is not None:
aggregate, parsed_functions, parsed_typedefs, parsed_variables, parsed_diagnostics = tag_definition
Expand All @@ -1709,6 +1763,11 @@ def _parse_translation_unit(
variables.extend(parsed_variables)
diagnostics.extend(parsed_diagnostics)
continue
if self._is_braced_initializer_declaration(segment):
unsupported = self._unsupported_declaration_diagnostic(segment)
if unsupported is not None:
diagnostics.append(unsupported)
continue
if segment.terminator != ";":
try:
function = self._parse_function(segment)
Expand Down Expand Up @@ -1957,15 +2016,17 @@ def visit_file(
parsed.includes = metadata.includes
parsed.macros = metadata.macros
parsed.raw_directives = metadata.raw_directives
function_like_macro_names = {macro.name for macro in metadata.macros if macro.function_like}
parsed.macro_dependencies = self._macro_dependencies(
source,
filename,
{macro.name for macro in metadata.macros if macro.function_like},
function_like_macro_names,
)
parsed.diagnostics = metadata.diagnostics
functions, structs, unions, enums, typedefs, variables, parser_diagnostics = self._parse_translation_unit(
source,
filename,
function_like_macros=function_like_macro_names,
)
parsed.functions = functions
parsed.structs = structs
Expand Down
10 changes: 5 additions & 5 deletions docs/c_parser/c_parser_implementation_checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ stable.
## Progress Snapshot

- Last updated: 2026-05-24
- Checklist progress: 586/856 checked (68.5%).
- Checklist progress: 590/856 checked (68.9%).
- Current parser status: partial C parser with raw directive metadata, top-level
source splitting, simple declarations/variables/typedefs, prototype-style
metadata, K&R diagnostics, simple function signatures, and start/end
Expand Down Expand Up @@ -804,7 +804,7 @@ Represented shapes still needing dedicated active regression tests:
- [x] Shared declaration/declarator parser exists.
- [x] It is used by all declaration roles available so far.
- [x] Primitive, pointer, array, typedef-name, and tag references have tests.
- [ ] Unsupported declaration-shaped input raises `CParseError` or structured
- [x] Unsupported declaration-shaped input raises `CParseError` or structured
diagnostics.

### Phase 5 Risks And Open Questions
Expand Down Expand Up @@ -1147,9 +1147,9 @@ Scope:
- [ ] Report function pointer parameters.
- [ ] Report callback typedef parameters.
- [ ] Report missing callback `.pyi` policy.
- [ ] Report macro-dependent declarations.
- [ ] Report unsupported attributes.
- [ ] Report unsupported compiler extensions.
- [x] Report macro-dependent declarations.
- [x] Report unsupported attributes.
- [x] Report unsupported compiler extensions.
- [ ] Report unsupported bitfields.
- [ ] Report pointer ownership ambiguity.
- [ ] Report non-const pointer mutability ambiguity.
Expand Down
21 changes: 21 additions & 0 deletions tests/parser/c/test_c_declarations_and_declarators.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,27 @@ def test_unimplemented_declaration_extensions_are_diagnosed_not_partially_modele
]


def test_braced_initializer_declarations_are_diagnosed_not_declarator_failures():
from c_parser import parse_c_file

parsed = parse_c_file(
"struct config;\n"
"int values[3] = {1, 2, 3};\n"
"struct config cfg = {.enabled = 1};\n"
"int scalar = 1;\n",
filename="braced_initializers.h",
)

assert [variable.name for variable in parsed.variables] == ["scalar"]
assert [
(diagnostic.code, diagnostic.unit_kind, diagnostic.location.line)
for diagnostic in parsed.diagnostics
] == [
("C_UNSUPPORTED_DECLARATION", "braced_initializer_declaration", 2),
("C_UNSUPPORTED_DECLARATION", "braced_initializer_declaration", 3),
]


def test_unconsumed_declarator_suffixes_are_diagnosed_not_silently_discarded():
from c_parser import parse_c_file

Expand Down
27 changes: 27 additions & 0 deletions tests/parser/c/test_c_lexer_preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def test_raw_mode_marks_function_like_macros_as_unsupported_until_expanded():
assert macros["API_DECL"].function_like is True
assert parsed.functions == []
assert any(diag.code == "C_UNSUPPORTED_FUNCTION_LIKE_MACRO" for diag in parsed.diagnostics)
assert any(diag.code == "C_MACRO_DEPENDENT_DECLARATION" for diag in parsed.diagnostics)


def test_raw_mode_fixture_keeps_macro_shaped_declaration_deferred_until_preprocessing():
Expand Down Expand Up @@ -213,7 +214,33 @@ def test_raw_mode_records_macro_dependency_metadata_for_macro_shaped_declaration
assert [(item.name, item.context) for item in parsed.macro_dependencies] == [
("API_DECL", "declaration")
]
assert parsed.macro_dependencies[0].source_text == "API_DECL(int) exported(void)"
assert parsed.macro_dependencies[0].source_location.line == 3
assert [(diag.code, diag.unit_kind, diag.unit_name) for diag in parsed.diagnostics] == [
("C_UNSUPPORTED_FUNCTION_LIKE_MACRO", "macro", "API_DECL"),
("C_MACRO_DEPENDENT_DECLARATION", "macro_dependent_declaration", "API_DECL"),
]


def test_raw_mode_macro_initializers_do_not_hide_parseable_declarations():
from c_parser import parse_c_file

parsed = parse_c_file(
"""
#define INIT(value) value
int answer = INIT(42);
""",
filename="macro_initializer.h",
preprocessing="raw",
)

assert [variable.name for variable in parsed.variables] == ["answer"]
assert parsed.variables[0].initializer is not None
assert parsed.variables[0].initializer.source_text == "INIT(42)"
assert parsed.macro_dependencies == []
assert [diagnostic.code for diagnostic in parsed.diagnostics] == [
"C_UNSUPPORTED_FUNCTION_LIKE_MACRO"
]


@pytest.mark.skip(reason="compiler-preprocessed mode lands after raw metadata collection.")
Expand Down
2 changes: 2 additions & 0 deletions tests/parser/c/test_c_public_api_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,11 @@ def test_model_json_shapes_cover_directive_include_macro_and_diagnostic_fields()
assert payload["macros"][0]["name"] == "API"
assert payload["macros"][0]["function_like"] is True
assert payload["macro_dependencies"][0]["name"] == "API"
assert payload["macro_dependencies"][0]["source_text"] == "API(int) run(void)"
assert {diagnostic["code"] for diagnostic in payload["diagnostics"]} >= {
"C_UNRESOLVED_INCLUDE",
"C_UNSUPPORTED_FUNCTION_LIKE_MACRO",
"C_MACRO_DEPENDENT_DECLARATION",
}


Expand Down
Loading