diff --git a/c_parser/parser.py b/c_parser/parser.py index 299ac15e6..2f2b1c122 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -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 @@ -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() @@ -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] = [] @@ -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( @@ -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) @@ -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 @@ -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 diff --git a/docs/c_parser/c_parser_architecture.md b/docs/c_parser/c_parser_architecture.md index 41ee78267..c0a1fbaa5 100644 --- a/docs/c_parser/c_parser_architecture.md +++ b/docs/c_parser/c_parser_architecture.md @@ -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. @@ -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` @@ -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: diff --git a/docs/c_parser/c_parser_cli_workflow.md b/docs/c_parser/c_parser_cli_workflow.md index 7d30a3449..3aca190d1 100644 --- a/docs/c_parser/c_parser_cli_workflow.md +++ b/docs/c_parser/c_parser_cli_workflow.md @@ -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. @@ -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: @@ -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: @@ -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 @@ -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 diff --git a/docs/c_parser/c_parser_implementation_checklist.md b/docs/c_parser/c_parser_implementation_checklist.md index c7d0be2d0..336484264 100644 --- a/docs/c_parser/c_parser_implementation_checklist.md +++ b/docs/c_parser/c_parser_implementation_checklist.md @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/docs/c_parser/c_parser_reference.md b/docs/c_parser/c_parser_reference.md index 83eedf961..182afefac 100644 --- a/docs/c_parser/c_parser_reference.md +++ b/docs/c_parser/c_parser_reference.md @@ -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 @@ -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: @@ -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. @@ -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 @@ -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. diff --git a/tests/parser/c/test_c_cli_skeleton.py b/tests/parser/c/test_c_cli_skeleton.py index ed0349512..4fa6e7b5c 100644 --- a/tests/parser/c/test_c_cli_skeleton.py +++ b/tests/parser/c/test_c_cli_skeleton.py @@ -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"] diff --git a/tests/parser/c/test_c_declarations_and_declarators.py b/tests/parser/c/test_c_declarations_and_declarators.py index 239549059..c53d5c08e 100644 --- a/tests/parser/c/test_c_declarations_and_declarators.py +++ b/tests/parser/c/test_c_declarations_and_declarators.py @@ -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