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
47 changes: 37 additions & 10 deletions c_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@
"double _Complex": CDoubleComplex,
"long double _Complex": CLongDoubleComplex,
}
_PRIMITIVE_TYPE_SIGNATURES = {
tuple(sorted(spelling.split())): type_class
for spelling, type_class in _PRIMITIVE_TYPES.items()
}


@dataclass
Expand Down Expand Up @@ -164,6 +168,10 @@ class _UnsupportedDeclaratorSyntax(ValueError):
pass


class _InvalidSpecifierSequence(ValueError):
pass


def _looks_like_existing_source_path(value: object) -> bool:
if isinstance(value, Path):
return value.is_file()
Expand Down Expand Up @@ -236,6 +244,20 @@ def _specifier_words(self, spec_text: str) -> list[str]:
def _qualifiers(self, spellings: list[str]) -> list:
return [_QUALIFIER_CLASSES[spelling]() for spelling in spellings]

def _invalid_specifier_error(
self,
segment: CTopLevelSegment,
message: str,
) -> CParseError:
return CParseError(
message,
filename=segment.filename,
line_number=segment.original_start_line,
column=segment.original_start_column,
source_line=segment.original_source_line,
code="CPARSE003",
)

def _parse_specifiers(self, spec_text: str) -> tuple[CType, list[str], list[str]]:
words = self._specifier_words(spec_text)
storage: list[str] = []
Expand Down Expand Up @@ -266,23 +288,21 @@ def _parse_specifiers(self, spec_text: str) -> tuple[CType, list[str], list[str]
type_: CType = tag_type(**tag_kwargs)
elif type_words:
spelling = " ".join(type_words)
primitive = _PRIMITIVE_TYPES.get(spelling)
primitive = _PRIMITIVE_TYPE_SIGNATURES.get(tuple(sorted(type_words)))
if primitive is not None:
type_ = primitive(
qualifiers=self._qualifiers(qualifiers),
source_text=" ".join([*qualifiers, *type_words]),
)
elif len(type_words) == 1:
elif len(type_words) == 1 and type_words[0] not in _PRIMITIVE_WORDS:
type_ = CTypedef(
name=type_words[0],
qualifiers=self._qualifiers(qualifiers),
source_text=" ".join([*qualifiers, *type_words]),
)
else:
type_ = CUnknownType(
spelling=spelling,
qualifiers=self._qualifiers(qualifiers),
source_text=" ".join([*qualifiers, *type_words]),
raise _InvalidSpecifierSequence(
f"Invalid type specifier sequence {spelling!r}."
)
else:
type_ = CUnknownType(
Expand Down Expand Up @@ -739,10 +759,13 @@ def _parse_function(self, segment: CTopLevelSegment) -> CFunction | None:
spec_text, declarator = self._split_declaration_specifiers(text)
if not spec_text or not declarator:
return None
name, function_type, storage, function_specifiers, direct_function = self._build_declared_type(
spec_text,
declarator,
)
try:
name, function_type, storage, function_specifiers, direct_function = self._build_declared_type(
spec_text,
declarator,
)
except _InvalidSpecifierSequence as error:
raise self._invalid_specifier_error(segment, str(error)) from None
if name is None or not isinstance(function_type, CFunctionType) or direct_function is None:
return None
parameter_bounds = self._find_parameter_list(text)
Expand Down Expand Up @@ -858,6 +881,8 @@ def _declarations_from_declarators(
spec_text,
declaration,
)
except _InvalidSpecifierSequence as error:
raise self._invalid_specifier_error(segment, str(error)) from None
except _UnsupportedDeclaratorSyntax as error:
diagnostics.append(self._declarator_diagnostic(segment, str(error)))
continue
Expand Down Expand Up @@ -958,6 +983,8 @@ def _parse_fields(
spec_text,
declaration,
)
except _InvalidSpecifierSequence as error:
raise self._invalid_specifier_error(segment, str(error)) from None
except _UnsupportedDeclaratorSyntax as error:
diagnostics.append(self._field_diagnostic(segment, owner_kind, str(error)))
continue
Expand Down
11 changes: 8 additions & 3 deletions docs/c_parser/c_parser_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ Implemented now:
`_Atomic(type)`, nested aggregate member definitions, and static assertions,
are reported as diagnostics with
explicit `unit_kind` values. A declarator must be fully consumed before a
concrete object is returned; unknown suffixes become diagnostics. Definitions preserve direct
concrete object is returned; unknown suffixes become diagnostics. Primitive
specifier order is normalized, and invalid combinations such as
`unsigned float` raise `CParseError` with code `CPARSE003` while a single
unresolved typedef-like name remains deferred. Definitions preserve direct
`start` and `end` locations from the signature start through the closing
brace; and K&R-style function definitions raise focused diagnostics.
- `c_parser.cli` provides C-specific partial report formatting.
Expand Down Expand Up @@ -231,7 +234,8 @@ Current and planned responsibilities:
member extraction. Helper methods live on `CParser` rather than as broad
module-level functions. Function models record prototype-style versus
unspecified empty parameter lists, function definitions preserve start/end
locations, and K&R-style definitions are rejected with `CParseError`.
locations, K&R-style definitions are rejected with `CParseError`, and
invalid primitive-specifier combinations are rejected with `CPARSE003`.
- Planned: symbol resolution, parameter array/function adjustment, and
additional declaration-specifier and extension coverage.
- `c_parser/project.py`
Expand Down Expand Up @@ -307,7 +311,8 @@ Derived and named `CType` subclasses are:
declaration objects
- `CTypedef`, which represents either a declared alias with its underlying
`type`, or an unresolved typedef-name use until symbol resolution is added
- `CUnknownType`, which preserves an unrecognized type spelling
- `CUnknownType`, retained for type states that cannot yet be modeled; invalid
primitive-specifier combinations no longer become `CUnknownType`

For example, composition order distinguishes the following declarations:

Expand Down
22 changes: 19 additions & 3 deletions docs/c_parser/c_parser_cli_workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ specifiers, `_Atomic(type)`, nested aggregate member definitions, and static ass
reported in diagnostics with explicit `unit_kind` values; unconsumed declarator
suffixes are diagnosed instead of silently omitted. The parser reports
`parser_status: "partial"`. C parse diagnostics, currently including
unsupported K&R-style function definitions, honor `--no-color` and `NO_COLOR=1`.
unsupported K&R-style function definitions and invalid primitive-specifier
combinations such as `unsigned float`, honor `--no-color` and `NO_COLOR=1`.
Function definitions do not store executable body text; they preserve a
direct `start` location and `end` location from the signature start through the
closing brace.
Expand Down Expand Up @@ -142,8 +143,10 @@ Initial flags:

Current C behavior also accepts `--no-color`. `CParseError` supports
compiler-style diagnostic formatting and the `C_PARSER_DEBUG` environment
variable. The current grammar subset is tolerant for unsupported declaration
forms; targeted syntax diagnostics should be added alongside focused tests.
variable. The current grammar subset is tolerant for recoverable unsupported
declaration forms, but invalid primitive-specifier combinations raise
`CPARSE003`; unresolved single typedef-name uses are deferred until type
resolution can determine whether a declaration exists.

C-specific flags to add only when needed:

Expand Down Expand Up @@ -352,6 +355,14 @@ Default CLI behavior:

Unsupported but recoverable declarations and raw preprocessor limitations are
stored as non-fatal `CDiagnostic` entries in the parse report instead.
Invalid primitive-specifier combinations that are independent of symbol
resolution are fatal:

```text
src/api.h:12:1: error[CPARSE003]: Invalid type specifier sequence 'unsigned float'.
12 | unsigned float value;
| ^
```

Debug behavior:

Expand Down Expand Up @@ -386,6 +397,8 @@ The active CLI/parser tests cover the current partial subset:
declarator combinations, concrete declaration objects, aggregate
definitions/members/enumerators, incomplete struct/union tags, and function
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.
- `--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 @@ -420,6 +433,9 @@ Completed order:
12. Replaced generic type references and declaration-kind tags with concrete
`CType` subclasses, `CComposedType` components, and concrete declaration
objects.
13. Added order-insensitive primitive specifier validation and `CPARSE003`
errors for invalid primitive combinations while retaining unresolved
typedef-name references for later resolution.

Next implementation work should continue with tag/typedef resolution,
preprocessed-input line mapping, compiler extension policy, and project
Expand Down
15 changes: 9 additions & 6 deletions docs/c_parser/c_parser_implementation_checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ now parsed. Declarators use a recursive grammar-style parser for pointer,
array, function, and parenthesized combinations. Declaration types are concrete
`CType` subclasses combined by `CComposedType`; aggregate members are
`CVariable` objects using the same declared-type path. Selected unsupported
extensions are diagnosed.
extensions are diagnosed, and invalid primitive-specifier combinations raise
`CParseError` without treating unresolved single typedef-name uses as invalid.

This checklist is intentionally detailed so future work can proceed one branch,
one checklist item, and one tested capability at a time. The C parser initiative
Expand All @@ -20,8 +21,8 @@ stable.

## Progress Snapshot

- Last updated: 2026-05-23
- Checklist progress: 512/848 checked (60.4%).
- Last updated: 2026-05-24
- Checklist progress: 513/848 checked (60.5%).
- 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 All @@ -31,8 +32,9 @@ stable.
typedefs/parameters, callback members, and functions returning function
pointers are represented with concrete `CType` objects and
`CComposedType.components`. Primitive specifiers have concrete type classes,
and functions, variables, typedefs, and aggregates are distinguished by
their concrete declaration objects rather than a kind field.
valid spelling permutations are normalized, invalid combinations raise
`CPARSE003`, and functions, variables, typedefs, and aggregates are
distinguished by their concrete declaration objects rather than a kind field.

## Global Rules

Expand Down Expand Up @@ -697,7 +699,8 @@ Scope:
- [x] Parse `enum name`.
- [x] Parse typedef-name references.
- [x] Preserve original declaration specifier text.
- [ ] Diagnose unknown specifier sequences.
- [x] Diagnose invalid primitive specifier sequences while deferring unresolved
single typedef-name references to project/type resolution.

### Declarator Tasks

Expand Down
17 changes: 14 additions & 3 deletions docs/c_parser/c_parser_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ Implemented:
- raw `#undef` directive provenance in macro metadata
- concrete primitive `CType` objects, pointer/array composition, and concrete
qualifier objects
- order-insensitive primitive specifier matching with `CPARSE003` errors for
invalid combinations such as `unsigned float`
- recursive declarator extraction for parenthesized pointer/array precedence
- nameless `CFunctionType` signatures for function pointer typedefs and
parameter source facts
Expand Down Expand Up @@ -234,7 +236,11 @@ All types inherit from `CType`. Implemented primitive type classes are
`CConst`, `CVolatile`, `CRestrict`, and `CAtomic`, attached to the precise
type component they qualify. `_Atomic int value;` is stored with a `CAtomic`
qualifier; the distinct `_Atomic(int) value;` type-specifier form remains
diagnosed as unsupported.
diagnosed as unsupported. Equivalent primitive orderings, such as
`int unsigned` and `double long`, map to the same concrete type while
invalid combinations, such as `unsigned float`, raise `CParseError` with
code `CPARSE003`. A single unresolved typedef-name use remains a `CTypedef`
until resolution can establish whether a matching declaration exists.

Nested declarators are `CComposedType` objects whose `components` are read
from the declared name outward:
Expand Down Expand Up @@ -392,7 +398,9 @@ The parser has the error type and formatter. Raw directive collection can emit
non-fatal metadata diagnostics, such as unresolved local includes or
function-like macros that were recorded but not expanded. K&R-style function
definitions now raise `CParseError` because the current function parser only
models prototype-style declarations and definitions. Known unsupported
models prototype-style declarations and definitions. Invalid primitive
specifier combinations also raise `CParseError` (`CPARSE003`) because their
invalidity does not depend on later typedef resolution. Known unsupported
declaration extensions are diagnosed rather than partially modeled; additional
syntax diagnostics should be added only with focused tests.

Expand Down Expand Up @@ -434,7 +442,8 @@ merge those branches back into `c-parser/main`.

Active declaration tests currently cover:

- every implemented primitive spelling mapped to its concrete `CType`
- every implemented primitive spelling and selected reordered equivalent
spellings mapped to their concrete `CType`
- all qualifier objects, storage metadata, simple expression initializers, and
multiple declarators
- pointer/array precedence, multidimensional arrays, parameter VLA/static
Expand All @@ -448,6 +457,8 @@ Active declaration tests currently cover:
- diagnostics for selected unsupported attributes, alignment, `_Atomic(type)`,
nested aggregate definitions, K&R definitions, and trailing declarator
extensions
- fatal diagnostics for invalid primitive-specifier combinations while
unresolved single typedef-name uses remain deferred

This is enough coverage for the currently implemented subset, not for all C
declarations.
Expand Down
12 changes: 12 additions & 0 deletions tests/parser/c/test_c_cli_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ def test_cli_c_no_color_and_no_color_env_format_parse_errors_without_ansi(tmp_pa
assert "\x1b[" not in env_res.stderr


def test_cli_c_invalid_primitive_specifier_sequence_is_fatal(tmp_path: Path):
header = tmp_path / "invalid_specifiers.h"
header.write_text("unsigned float value;\n", encoding="utf-8")
cmd = [sys.executable, "-m", "x2py", str(header), "--language", "c", "--parse", "--no-color"]

res = subprocess.run(cmd, capture_output=True, text=True)

assert res.returncode == 1
assert "error[CPARSE003]: Invalid type specifier sequence 'unsigned float'." in res.stderr
assert "\x1b[" not in res.stderr


def test_cli_without_language_keeps_fortran_default_behavior():
fixture = Path(__file__).resolve().parents[2] / "data" / "fortran" / "general" / "basic_subroutine.f90"
cmd = [sys.executable, "-m", "x2py", str(fixture), "--parse"]
Expand Down
48 changes: 48 additions & 0 deletions tests/parser/c/test_c_declarations_and_declarators.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,54 @@ def test_every_supported_primitive_spelling_creates_a_concrete_ctype(spelling, e
assert isinstance(function.result_type, CType)


@pytest.mark.parametrize(
("spelling", "expected_name"),
[
("int unsigned", "CUnsignedInt"),
("int long unsigned", "CUnsignedLong"),
("double long", "CLongDouble"),
("_Complex float", "CFloatComplex"),
],
)
def test_valid_reordered_primitive_specifiers_are_normalized(spelling, expected_name):
import c_parser
from c_parser import parse_c_file

function = parse_c_file(f"{spelling} primitive(void);\n", filename="reordered_primitives.h").functions[0]

assert isinstance(function.result_type, getattr(c_parser, expected_name))
assert function.result_type.source_text == spelling


@pytest.mark.parametrize(
"source",
[
"unsigned float value;\n",
"void bad(long char value);\n",
"struct bad { signed unsigned value; };\n",
"unsigned float bad(void) { return 0; }\n",
],
)
def test_invalid_primitive_specifier_sequences_raise_parse_errors(source):
from c_parser import CParseError, parse_c_file

with pytest.raises(CParseError, match="Invalid type specifier sequence") as error:
parse_c_file(source, filename="invalid_specifiers.h")

assert error.value.code == "CPARSE003"
assert "invalid_specifiers.h:1:1: error[CPARSE003]" in error.value.format_diagnostic(color=False)


def test_unresolved_single_typedef_name_is_preserved_until_resolution():
from c_parser import CTypedef, parse_c_file

parsed = parse_c_file("external_type value;\n", filename="deferred_typedef.h")

assert isinstance(parsed.variables[0].type, CTypedef)
assert parsed.variables[0].type.name == "external_type"
assert parsed.diagnostics == []


def test_pointer_qualifiers_belong_to_the_component_they_qualify():
from c_parser import CComposedType, CConst, CDouble, CPointer, CRestrict, parse_c_file

Expand Down
Loading