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
32 changes: 24 additions & 8 deletions c_parser/lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class CTopLevelSegment:
original_end_column: int = 1
original_source_line: str | None = None
original_end_source_line: str | None = None
original_source_lines: tuple[str, ...] = field(default_factory=tuple)


_TWO_CHAR_OPERATORS = {
Expand Down Expand Up @@ -82,6 +83,10 @@ def _source_line(lines: list[str], line_number: int) -> str | None:
return None


def _source_lines(lines: list[str], start_line: int, end_line: int) -> tuple[str, ...]:
return tuple(lines[start_line - 1 : end_line])


def _advance_position(char: str, line: int, column: int) -> tuple[int, int]:
if char == "\n":
return line + 1, 1
Expand Down Expand Up @@ -138,26 +143,33 @@ def _scan_code_states(text: str):
stack.pop()


def top_level_split(text: str, delimiter: str = ",") -> list[str]:
"""Split on a delimiter that appears outside brackets and literals."""
def top_level_split_with_offsets(text: str, delimiter: str = ",") -> list[tuple[str, int]]:
"""Split outside nested syntax and preserve each trimmed fragment offset."""
if len(delimiter) != 1:
raise ValueError("top_level_split delimiter must be a single character")
raise ValueError("top_level_split_with_offsets delimiter must be a single character")

parts: list[str] = []
parts: list[tuple[str, int]] = []
start = 0
for index, char, stack, state in _scan_code_states(text):
if state == "normal" and not stack and char == delimiter:
part = text[start:index].strip()
raw_part = text[start:index]
part = raw_part.strip()
if part:
parts.append(part)
parts.append((part, start + len(raw_part) - len(raw_part.lstrip())))
start = index + 1

tail = text[start:].strip()
raw_tail = text[start:]
tail = raw_tail.strip()
if tail:
parts.append(tail)
parts.append((tail, start + len(raw_tail) - len(raw_tail.lstrip())))
return parts


def top_level_split(text: str, delimiter: str = ",") -> list[str]:
"""Split on a delimiter that appears outside brackets and literals."""
return [part for part, _offset in top_level_split_with_offsets(text, delimiter)]


def top_level_partition(text: str, delimiter: str = "=") -> tuple[str, str | None]:
"""Partition once on a top-level delimiter outside brackets and literals."""
if len(delimiter) != 1:
Expand Down Expand Up @@ -281,6 +293,7 @@ def split_top_level_c_source(
original_end_column=column,
original_source_line=block_source_line,
original_end_source_line=_source_line(source_lines, line),
original_source_lines=_source_lines(source_lines, block_start_line, line),
)
)
block_header = None
Expand All @@ -305,6 +318,7 @@ def split_top_level_c_source(
original_end_column=column,
original_source_line=_source_line(source_lines, start_line),
original_end_source_line=_source_line(source_lines, line),
original_source_lines=_source_lines(source_lines, start_line, line),
)
)
start_index = None
Expand All @@ -329,6 +343,7 @@ def split_top_level_c_source(
original_end_column=column,
original_source_line=_source_line(source_lines, start_line),
original_end_source_line=_source_line(source_lines, line),
original_source_lines=_source_lines(source_lines, start_line, line),
)
)

Expand Down Expand Up @@ -573,4 +588,5 @@ def lex_c_source(source: str, filename: str | None = None) -> list[CToken]:
"strip_c_comments",
"top_level_partition",
"top_level_split",
"top_level_split_with_offsets",
)
116 changes: 100 additions & 16 deletions c_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
strip_c_comments,
top_level_partition,
top_level_split,
top_level_split_with_offsets,
)
from .models import (
CArray,
Expand Down Expand Up @@ -198,14 +199,27 @@ class CParser:
declarators, aggregate declarations, typedefs, and function signatures.
"""

def _source_location(self, segment: CTopLevelSegment) -> CSourceLocation:
def _source_location_at(self, segment: CTopLevelSegment, offset: int) -> CSourceLocation:
prefix = segment.text[:offset]
line_offset = prefix.count("\n")
line = segment.original_start_line + line_offset
if line_offset:
column = len(prefix.rsplit("\n", 1)[-1]) + 1
else:
column = segment.original_start_column + len(prefix)
source_line = segment.original_source_line
if line_offset and line_offset < len(segment.original_source_lines):
source_line = segment.original_source_lines[line_offset]
return CSourceLocation(
filename=segment.filename,
line=segment.original_start_line,
column=segment.original_start_column,
source_line=segment.original_source_line,
line=line,
column=column,
source_line=source_line,
)

def _source_location(self, segment: CTopLevelSegment) -> CSourceLocation:
return self._source_location_at(segment, 0)

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

Expand Down Expand Up @@ -248,13 +262,16 @@ def _invalid_specifier_error(
self,
segment: CTopLevelSegment,
message: str,
*,
offset: int = 0,
) -> CParseError:
location = self._source_location_at(segment, offset)
return CParseError(
message,
filename=segment.filename,
line_number=segment.original_start_line,
column=segment.original_start_column,
source_line=segment.original_source_line,
filename=location.filename,
line_number=location.line,
column=location.column,
source_line=location.source_line,
code="CPARSE003",
)

Expand Down Expand Up @@ -932,31 +949,79 @@ def _field_diagnostic(
segment: CTopLevelSegment,
owner_kind: str,
message: str,
*,
offset: int = 0,
) -> CDiagnostic:
return CDiagnostic(
code="C_UNSUPPORTED_FIELD_DECLARATION",
message=message,
severity="warning",
location=self._source_location(segment),
location=self._source_location_at(segment, offset),
unit_kind=f"{owner_kind}_field",
unit_name=None,
)

def _incomplete_array_component(self, type_: CType) -> CArray | None:
components = type_.components if isinstance(type_, CComposedType) else [type_]
if not components or not isinstance(components[0], CArray):
return None
array = components[0]
if array.bound is None and not array.is_variable_length:
return array
return None

def _validate_flexible_members(
self,
members: list[CVariable],
owner_kind: str,
) -> list[CDiagnostic]:
diagnostics: list[CDiagnostic] = []
named_members = sum(member.name is not None for member in members)
for index, member in enumerate(members):
array = self._incomplete_array_component(member.type)
if array is None:
continue
if owner_kind == "struct" and index == len(members) - 1 and named_members > 1:
array.is_flexible = True
continue
if owner_kind == "union":
message = "A union member cannot be a flexible array member."
elif index != len(members) - 1:
message = "A flexible array member must be the final member of a struct."
else:
message = "A flexible array member requires a preceding named struct member."
diagnostics.append(
CDiagnostic(
code="C_INVALID_FLEXIBLE_ARRAY_MEMBER",
message=message,
severity="error",
location=member.source_location,
unit_kind=f"{owner_kind}_field",
unit_name=member.name,
)
)
return diagnostics

def _parse_fields(
self,
body: str,
segment: CTopLevelSegment,
owner_kind: str,
*,
body_offset: int,
) -> tuple[list[CVariable], list[CDiagnostic]]:
members: list[CVariable] = []
diagnostics: list[CDiagnostic] = []
for text in top_level_split(body, ";"):
for text, field_offset in top_level_split_with_offsets(body, ";"):
member_offset = body_offset + field_offset
member_location = self._source_location_at(segment, member_offset)
if self._has_unsupported_declaration_marker(text):
diagnostics.append(
self._field_diagnostic(
segment,
owner_kind,
"Declaration attributes and alignment specifiers are not supported in fields yet.",
offset=member_offset,
)
)
continue
Expand All @@ -966,13 +1031,19 @@ def _parse_fields(
segment,
owner_kind,
"Nested aggregate field definitions are not supported yet.",
offset=member_offset,
)
)
continue
spec_text, declarator_list = self._split_declaration_specifiers(text)
if not spec_text or not declarator_list:
diagnostics.append(
self._field_diagnostic(segment, owner_kind, "Unsupported field declaration.")
self._field_diagnostic(
segment,
owner_kind,
"Unsupported field declaration.",
offset=member_offset,
)
)
continue
for declarator in top_level_split(declarator_list, ","):
Expand All @@ -984,23 +1055,31 @@ def _parse_fields(
declaration,
)
except _InvalidSpecifierSequence as error:
raise self._invalid_specifier_error(segment, str(error)) from None
raise self._invalid_specifier_error(segment, str(error), offset=member_offset) from None
except _UnsupportedDeclaratorSyntax as error:
diagnostics.append(self._field_diagnostic(segment, owner_kind, str(error)))
diagnostics.append(
self._field_diagnostic(segment, owner_kind, str(error), offset=member_offset)
)
continue
if name is None and bit_width is None:
diagnostics.append(
self._field_diagnostic(segment, owner_kind, "Unnamed field type is not supported.")
self._field_diagnostic(
segment,
owner_kind,
"Unnamed field type is not supported.",
offset=member_offset,
)
)
continue
members.append(
CVariable(
name=name,
type=type_,
source_location=self._source_location(segment),
source_location=member_location,
bit_width=bit_width,
)
)
diagnostics.extend(self._validate_flexible_members(members, owner_kind))
return members, diagnostics

def _parse_enumerators(self, body: str, segment: CTopLevelSegment) -> list[CEnumerator]:
Expand Down Expand Up @@ -1060,7 +1139,12 @@ def _parse_tag_definition(
source_location=location,
)
else:
members, diagnostics = self._parse_fields(body, segment, kind)
members, diagnostics = self._parse_fields(
body,
segment,
kind,
body_offset=open_index + 1,
)
if kind == "struct":
aggregate = CStruct(
name=tag_name,
Expand Down
16 changes: 9 additions & 7 deletions docs/c_parser/c_parser_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ Implemented now:
Declaration types are represented by concrete `CType` subclasses:
primitives, `CPointer`, `CArray`, `CFunctionType`, and
`CComposedType`. Aggregate members are `CVariable` objects using that same
type path and preserve arrays, callback candidates, and bit-width text.
type path and preserve arrays, callback candidates, bit-width text, and
member-level source locations. Supported flexible final struct members set
`CArray.is_flexible=True`; invalid flexible-member placement and union use
produce `C_INVALID_FLEXIBLE_ARRAY_MEMBER` diagnostics.
Inline tag definitions followed by aliases or objects produce concrete
`CTypedef` or `CVariable` records linked to the aggregate object. Function
models expose `result_type` and named `parameters`; their derived
Expand Down Expand Up @@ -73,9 +76,7 @@ Deferred:
- typedef/tag resolution beyond an inline aggregate declaration and callback
policy metadata, for example resolving `size_t count(void);` to a prior
`typedef unsigned long size_t;`
- parameter adjustment and flexible array member classification, for example
`void process(int values[4]);` and
`struct packet { unsigned size; unsigned char data[]; };`
- parameter adjustment, for example `void process(int values[4]);`
- nested aggregate member definitions, braced initializers, compiler
attributes, alignment specifiers, and `_Atomic(type)` declarations, for
example `struct outer { struct { int x; } inner; };` and
Expand Down Expand Up @@ -302,8 +303,8 @@ Derived and named `CType` subclasses are:

- `CPointer`, whose qualifiers apply to that pointer component
- `CArray`, with `bound`, `is_static_minimum`, `is_variable_length`, and
`is_flexible`; bound/static/VLA metadata is populated now, while
flexible-array-member parsing and validation are deferred
`is_flexible`; supported final flexible struct members are classified now,
and invalid placement or union use produces a parser diagnostic
- `CFunctionType`, the nameless callable signature with `result_type`,
`parameter_types`, `is_variadic`, and `prototype_style`
- `CComposedType`, whose `components` are read from the declared name outward
Expand All @@ -326,7 +327,8 @@ Declaration objects are separate from the type components:

- `CVariable` has `name`, `type`, `storage`, optional `initializer`, optional
`bit_width`, and source/callback metadata. Struct and union `members` are
also `CVariable` objects; there is no separate field class.
also `CVariable` objects with per-member locations; there is no separate
field class.
- `CFunction` has `name`, `result_type`, named `parameters`, storage and
function specifiers, `is_variadic`, prototype style, and source/definition
locations. Its `type` property builds the corresponding nameless
Expand Down
12 changes: 10 additions & 2 deletions docs/c_parser/c_parser_cli_workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ top-level sections: `functions`, `structs`, `unions`, `enums`, `typedefs`,
can populate `functions`, `typedefs`, `variables`, `structs`, `unions`, and
`enums` in the supported subset. Typedefs, variables, parameters, and aggregate
members can include concrete composed types for pointer/array/function forms,
including function pointers and functions returning function pointers. Raw
including function pointers, functions returning function pointers, and
legal final flexible struct members marked with `is_flexible=True`. Raw
`includes`, `macros`, and metadata `diagnostics` can also be
populated. The object class distinguishes declarations (`CFunction`,
`CVariable`, `CTypedef`, `CStruct`, `CUnion`, or `CEnum`), and incomplete tag
declarations set `is_incomplete=True`.
Known unsupported declaration forms such as declaration attributes, alignment
specifiers, `_Atomic(type)`, nested aggregate member definitions, and static assertions are
reported in diagnostics with explicit `unit_kind` values; unconsumed declarator
suffixes are diagnosed instead of silently omitted. The parser reports
suffixes are diagnosed instead of silently omitted. Invalid flexible array
member placement and flexible union members produce
`C_INVALID_FLEXIBLE_ARRAY_MEMBER` error diagnostics at the field location. The parser reports
`parser_status: "partial"`. C parse diagnostics, currently including
unsupported K&R-style function definitions and invalid primitive-specifier
combinations such as `unsigned float`, honor `--no-color` and `NO_COLOR=1`.
Expand Down Expand Up @@ -399,6 +402,9 @@ The active CLI/parser tests cover the current partial subset:
signatures with definition start/end locations are covered by focused C tests.
- valid reordered primitive specifiers and fatal invalid primitive-specifier
combinations are covered by focused C tests.
- flexible array member classification/validation, per-member source
locations, and named/unnamed/zero-width bit-field source facts are covered
by focused C tests.
- `--show-vars` and `--print-limit` are rejected in C mode until C-specific
display controls exist.
- `--semantics` with `--language c` is rejected until C semantic conversion is
Expand Down Expand Up @@ -436,6 +442,8 @@ Completed order:
13. Added order-insensitive primitive specifier validation and `CPARSE003`
errors for invalid primitive combinations while retaining unresolved
typedef-name references for later resolution.
14. Added field-level source locations, flexible array member classification
and invalid-use diagnostics, plus explicit bit-field regression coverage.

Next implementation work should continue with tag/typedef resolution,
preprocessed-input line mapping, compiler extension policy, and project
Expand Down
Loading
Loading