diff --git a/c_parser/models.py b/c_parser/models.py index 0a9be3dd9..a7913c44b 100644 --- a/c_parser/models.py +++ b/c_parser/models.py @@ -223,6 +223,7 @@ class CFunction: specifiers: list[str] = field(default_factory=list) variadic: bool = False is_definition: bool = False + prototype_style: str | None = None source_location: CSourceLocation | None = None @@ -284,6 +285,7 @@ class CMacro: name: str value: str | None = None function_like: bool = False + directive: str = "define" source_location: CSourceLocation | None = None diff --git a/c_parser/parser.py b/c_parser/parser.py index 6ca9d6691..b871e1d86 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -8,6 +8,7 @@ from .lexer import ( CTopLevelSegment, split_top_level_c_source, + strip_c_comments, top_level_partition, top_level_split, ) @@ -304,6 +305,70 @@ def _is_knr_definition(self, segment: CTopLevelSegment, parameters_text: str) -> return False return True + def _raise_for_unsupported_old_style_definitions( + self, + source: str, + filename: str | None, + ) -> None: + source_lines = source.splitlines() + stripped_lines = strip_c_comments(source).splitlines() + + for index, line in enumerate(stripped_lines): + text = line.strip() + parameter_bounds = self._find_parameter_list(text) + if parameter_bounds is None: + continue + open_index, close_index = parameter_bounds + before_parameters = text[:open_index].strip() + name_match = self._last_identifier(before_parameters) + if name_match is None: + continue + return_spec = before_parameters[: name_match.start()].strip() + if not return_spec or "(" in return_spec or ")" in return_spec: + continue + + parameters_text = text[open_index + 1 : close_index].strip() + if not parameters_text or parameters_text == "void": + continue + + parameters = [part.strip() for part in parameters_text.split(",")] + if not parameters or not all(re.fullmatch(r"[A-Za-z_]\w*", part) for part in parameters): + continue + + saw_old_style_declaration = False + for follow in stripped_lines[index + 1 :]: + stripped = follow.strip() + if not stripped: + continue + if stripped.startswith("{"): + source_line = source_lines[index] if index < len(source_lines) else line + raise CParseError( + "K&R style function definitions are not supported", + filename=filename, + line_number=index + 1, + column=max(line.find(name_match.group(0)) + 1, 1), + source_line=source_line, + code="CPARSE002", + ) + if stripped.endswith(";"): + saw_old_style_declaration = True + continue + break + + if saw_old_style_declaration: + source_line = source_lines[index] if index < len(source_lines) else line + raise CParseError( + "K&R style function definitions are not supported", + filename=filename, + line_number=index + 1, + column=max(line.find(name_match.group(0)) + 1, 1), + source_line=source_line, + code="CPARSE002", + ) + + def _prototype_style(self, parameters_text: str) -> str: + return "unspecified" if not parameters_text.strip() else "prototype" + def _parse_function(self, segment: CTopLevelSegment) -> CFunction | None: text = segment.text.strip() if text.startswith(("typedef ", "struct ", "union ", "enum ")): @@ -347,6 +412,7 @@ def _parse_function(self, segment: CTopLevelSegment) -> CFunction | None: specifiers=function_specifiers, variadic=variadic, is_definition=segment.terminator == "block", + prototype_style=self._prototype_style(parameters_text), source_location=self._source_location(segment), ) @@ -387,6 +453,8 @@ def _parse_translation_unit( source: str, filename: str | None, ) -> tuple[list[CFunction], list[CTypedef], list[CGlobal]]: + self._raise_for_unsupported_old_style_definitions(source, filename) + functions: list[CFunction] = [] typedefs: list[CTypedef] = [] globals_: list[CGlobal] = [] diff --git a/c_parser/preprocessor.py b/c_parser/preprocessor.py index 3d9184d62..d4c3bdbe0 100644 --- a/c_parser/preprocessor.py +++ b/c_parser/preprocessor.py @@ -12,6 +12,7 @@ _INCLUDE_RE = re.compile(r'^\s*#\s*include\s*(?:"([^"]+)"|<([^>]+)>)') _DEFINE_RE = re.compile(r"^\s*#\s*define\s+([A-Za-z_]\w*)(\([^)]*\))?(?:\s+(.*))?$") +_UNDEF_RE = re.compile(r"^\s*#\s*undef\s+([A-Za-z_]\w*)\s*$") @dataclass @@ -121,6 +122,18 @@ def collect_preprocessor_metadata( unit_name=name, ) ) + continue + + undef_match = _UNDEF_RE.match(record.text) + if undef_match: + name = undef_match.group(1) + metadata.macros.append( + CMacro( + name=name, + directive="undef", + source_location=_record_location(record), + ) + ) return metadata diff --git a/docs/c_parser/c_parser_architecture.md b/docs/c_parser/c_parser_architecture.md index 02d3d91a9..889acceb9 100644 --- a/docs/c_parser/c_parser_architecture.md +++ b/docs/c_parser/c_parser_architecture.md @@ -2,9 +2,9 @@ Status: partial parser plus raw directive metadata implemented. The `c_parser` package, typed parser models, public entrypoints, explicit -`x2py --language c --parse` CLI path, raw include/macro metadata collection, -top-level source splitting, and a first simple declaration/function subset -exist. +`x2py --language c --parse` CLI path, raw include/macro/undef metadata +collection, top-level source splitting, and a first simple +declaration/function subset exist. This document records the target architecture for the C parser frontend in x2py. The initial skeleton has grown into a partial parser, and the remaining @@ -27,16 +27,22 @@ Implemented now: records, exposes lightweight token records, and provides top-level splitting helpers that track braces, parentheses, brackets, and literals. - `c_parser.preprocessor` records raw `#include` directives, simple object-like - macros, and unsupported function-like macro diagnostics without expanding - macros. + macros, `#undef` directives, and unsupported function-like macro diagnostics + without expanding macros. - `c_parser.parser` parses simple globals, typedefs, function prototypes, and - function-definition signatures while skipping bodies. + function-definition signatures while skipping bodies. Function models include + `prototype_style`, and K&R-style function definitions raise focused + diagnostics. - `c_parser.cli` provides C-specific partial report formatting. - `x2py.cli` dispatches `--language c --parse` to the C parser path. - `--language c --semantics`, `--language c --pyi`, and C wrap-readiness are rejected until semantic conversion exists. -- Focused partial CLI/API, declaration/function, and raw lexer/directive tests are - unskipped while broader roadmap tests remain skipped. +- Focused partial CLI/API, declaration/function, diagnostic color, and raw + lexer/directive tests are unskipped while broader roadmap tests remain + skipped. +- `tests/data/c/` contains C fixture scaffolding and general fixtures modeled + after the Fortran general fixture themes, with additional C-specific API + shapes. Deferred: @@ -185,8 +191,8 @@ Current and planned responsibilities: them. - `c_parser/preprocessor.py` - Implemented: lightweight raw directive metadata for includes, - object-like macros, function-like macro diagnostics, and local include - resolution when a matching file is available. + object-like macros, `#undef` directives, function-like macro diagnostics, + and local include resolution when a matching file is available. - Planned: compiler-assisted preprocessing metadata and `#line`/linemarker source mapping for preprocessed input. - `c_parser/parser.py` @@ -194,7 +200,9 @@ Current and planned responsibilities: translation-unit visiting, simple declaration/function visitors, simple declaration-specifier handling, and simple pointer/array declarator extraction. Helper methods live on `CParser` rather than as broad - module-level functions. + module-level functions. Current function models record prototype-style + versus unspecified empty parameter lists, and K&R-style definitions are + rejected with `CParseError`. - Planned: recursive declarator/function/composite-type visitors and a richer shared declaration/declarator backend. - `c_parser/project.py` @@ -452,6 +460,7 @@ Raw-source mode target: - Fold backslash-newline continuations. - Record `#include` directives as structured include dependencies. - Record `#define` object-like macros for simple constants. +- Record `#undef` directives as macro provenance. - Record function-like macros as unsupported or deferred metadata. - Record conditional directive presence as metadata only when needed for provenance. diff --git a/docs/c_parser/c_parser_cli_workflow.md b/docs/c_parser/c_parser_cli_workflow.md index d94c77bd9..af364c631 100644 --- a/docs/c_parser/c_parser_cli_workflow.md +++ b/docs/c_parser/c_parser_cli_workflow.md @@ -2,8 +2,9 @@ Status: C parser partial subset plus raw directive metadata implemented. The CLI command shape exists and parse reports can include raw includes, simple -macros, metadata diagnostics, simple globals, typedefs, function prototypes, -and function-definition signatures. +macros, `#undef` provenance, metadata diagnostics, simple globals, typedefs, +function prototypes, prototype-style metadata, and function-definition +signatures. The C parser CLI workflow should be designed before parser implementation so future parser work lands behind a stable command shape, output schema, and @@ -30,7 +31,8 @@ top-level sections: `functions`, `structs`, `unions`, `enums`, `typedefs`, can populate `functions`, `typedefs`, and `globals` for the supported subset, while composite type sections remain empty. Raw `includes`, `macros`, and metadata `diagnostics` can also be populated. The parser reports -`parser_status: "partial"`. +`parser_status: "partial"`. C parse diagnostics, currently including +unsupported K&R-style function definitions, honor `--no-color` and `NO_COLOR=1`. Unsupported C stages: @@ -202,7 +204,8 @@ JSON output for a file without raw directives: "storage": [], "specifiers": [], "variadic": false, - "is_definition": false + "is_definition": false, + "prototype_style": "prototype" } ], "structs": [], diff --git a/docs/c_parser/c_parser_implementation_checklist.md b/docs/c_parser/c_parser_implementation_checklist.md index 0589dc747..47da7165b 100644 --- a/docs/c_parser/c_parser_implementation_checklist.md +++ b/docs/c_parser/c_parser_implementation_checklist.md @@ -1,10 +1,11 @@ # C Parser Implementation Checklist -Status: implementation checklist with Phase 1 skeleton, selected Phase 3 -model work, Phase 4 raw lexer/directive metadata, and a first Phase 5/6 -partial declaration/function subset complete. The `c_parser` package and -explicit C parse path exist, and simple globals, typedefs, function prototypes, -and function-definition signatures are now parsed. +Status: implementation checklist with Phase 1 skeleton, selected Phase 2 +fixture scaffolding, selected Phase 3 model/error work, Phase 4 raw +lexer/directive metadata, and a first Phase 5/6 partial +declaration/function subset complete. The `c_parser` package and explicit C +parse path exist, and simple globals, typedefs, function prototypes, and +function-definition signatures are now parsed. 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 @@ -14,10 +15,10 @@ stable. ## Progress Snapshot - Last updated: 2026-05-22 -- Checklist progress: 415/848 checked (48.9%). +- Checklist progress: 443/848 checked (52.2%). - Current parser status: partial C parser with raw directive metadata, top-level - source splitting, simple declarations/globals/typedefs, and simple function - signatures. + source splitting, simple declarations/globals/typedefs, prototype-style + metadata, K&R diagnostics, and simple function signatures. ## Global Rules @@ -250,7 +251,7 @@ Scope: - [x] Test `--parse` without `--language` remains Fortran behavior. - [ ] If `--parse-c` is added, test it maps to C parse mode. - [x] Test `--no-color` is accepted in C mode. -- [ ] Test `NO_COLOR=1` is honored once C diagnostics exist. +- [x] Test `NO_COLOR=1` is honored once C diagnostics exist. - [x] Test `--debug-traceback` is accepted in C mode. ### Phase 1 Definition Of Done @@ -286,18 +287,18 @@ Scope: ### Test Layout Tasks -- [ ] Create a dedicated C parser test area. -- [ ] Choose between `tests/c_parser/` and `tests/parser/c/` for focused C +- [x] Create a dedicated C parser test area. +- [x] Choose between `tests/c_parser/` and `tests/parser/c/` for focused C parser tests. -- [ ] Create `tests/data/c/general/`. -- [ ] Create `tests/data/c/errors/parser/`. -- [ ] Create `tests/data/c/corpus/`. -- [ ] Create `tests/data/c/scientific/`. +- [x] Create `tests/data/c/general/`. +- [x] Create `tests/data/c/errors/parser/`. +- [x] Create `tests/data/c/corpus/`. +- [x] Create `tests/data/c/scientific/`. - [ ] Create `tests/parser/c/fixtures/general/`. - [ ] Create `tests/parser/c/fixtures/errors/`. -- [ ] Keep C fixture data separate from Fortran fixture data. -- [ ] Add README files explaining each C fixture directory. -- [ ] Add small placeholder `.h` and `.c` fixture files only if tests need them. +- [x] Keep C fixture data separate from Fortran fixture data. +- [x] Add README files explaining each C fixture directory. +- [x] Add small `.h` and `.c` fixture files for C fixture coverage. ### Skipped Roadmap Test Policy @@ -372,7 +373,7 @@ Scope: ### Phase 2 Definition Of Done - [x] C test directory structure is present. -- [ ] C fixture directory structure is present. +- [x] C fixture directory structure is present. - [ ] C golden update workflow is documented. - [x] Partial parser and metadata tests pass against current behavior. - [ ] Fortran tests still pass. @@ -444,7 +445,7 @@ Scope: - [x] Add optional C debug env var. - [x] Test C parse error attributes. - [x] Test compiler-style diagnostic rendering. -- [ ] Test color and no-color behavior. +- [x] Test color and no-color behavior. - [x] Test debug note behavior. ### Model Tasks @@ -567,7 +568,7 @@ Scope: - [x] Recognize object-like `#define NAME value`. - [x] Recognize function-like `#define NAME(...) body`. - [x] Store function-like macros as unsupported/deferred metadata. -- [ ] Recognize `#undef`. +- [x] Recognize `#undef`. - [ ] Record conditional directive presence (`#ifdef`, `#ifndef`, `#if`, `#elif`, `#else`, `#endif`) as provenance metadata if needed. - [x] Do not select active branches in raw mode. @@ -712,15 +713,15 @@ Scope: - [x] Preserve typedef references before project resolution. - [ ] Add tests for each declaration role. - [x] Add tests for declarations with multiple variables. -- [ ] Add tests for declarations with initializers. -- [ ] Add tests that local executable statements are not parsed as declarations. -- [ ] Add exhaustive tests for every supported storage class. +- [x] Add tests for declarations with initializers. +- [x] Add tests that local executable statements are not parsed as declarations. +- [x] Add exhaustive tests for every supported storage class. - [ ] Add exhaustive tests for every supported type qualifier. - [ ] Add exhaustive tests for every supported primitive spelling. -- [ ] Add tests for typedef-name references outside `size_t`-style examples. -- [ ] Add tests for `struct name`, `union name`, and `enum name` references in +- [x] Add tests for typedef-name references outside `size_t`-style examples. +- [x] Add tests for `struct name`, `union name`, and `enum name` references in globals and parameters. -- [ ] Add tests for multidimensional arrays. +- [x] Add tests for multidimensional arrays. - [ ] Add diagnostics for declarations ignored by the current partial parser. - [ ] Add structured source facts for declarations that depend on macros. @@ -772,15 +773,15 @@ Scope: - [x] Parse storage class `static`. - [x] Add source locations. - [x] Add tests for simple prototypes. -- [ ] Add tests for no-argument prototypes. -- [ ] Add tests for `void` arguments. +- [x] Add tests for no-argument prototypes. +- [x] Add tests for `void` arguments. - [x] Add tests for pointer and array parameters. - [x] Add tests for const pointer variants. - [x] Add tests for variadic prototypes. - [ ] Add tests for function pointer parameters. -- [ ] Add model field for prototype style so `int f(void)` and `int f()` can +- [x] Add model field for prototype style so `int f(void)` and `int f()` can be distinguished. -- [ ] Add tests that distinguish explicit `void` parameter lists from +- [x] Add tests that distinguish explicit `void` parameter lists from unspecified empty parameter lists. - [ ] Add parser source facts or diagnostics for variadic functions. - [ ] Add parser source facts for callback candidates once function pointer @@ -797,12 +798,12 @@ Scope: - [x] Skip body contents for wrapper metadata. - [x] Balance braces while respecting strings, chars, and comments. - [x] Ignore local declarations for exported signatures in v1. -- [ ] Reject or diagnose K&R style function definitions initially. +- [x] Reject or diagnose K&R style function definitions initially. - [x] Add tests for simple definitions. - [x] Add tests for nested braces in function body. - [x] Add tests for strings containing braces. -- [ ] Add tests for K&R unsupported diagnostics. -- [ ] Detect K&R definitions before body skipping hides the old-style +- [x] Add tests for K&R unsupported diagnostics. +- [x] Detect K&R definitions before body skipping hides the old-style declaration list. ### Function Deduplication Tasks @@ -897,8 +898,8 @@ Scope: ### Typedef Tasks - [x] Parse primitive typedefs. -- [ ] Parse pointer typedefs. -- [ ] Parse array typedefs. +- [x] Parse pointer typedefs. +- [x] Parse array typedefs. - [ ] Parse function pointer typedefs. - [ ] Parse struct/union/enum typedefs. - [ ] Preserve alias chains before resolution. diff --git a/docs/c_parser/c_parser_reference.md b/docs/c_parser/c_parser_reference.md index 2b8443b4d..b1c032d38 100644 --- a/docs/c_parser/c_parser_reference.md +++ b/docs/c_parser/c_parser_reference.md @@ -61,10 +61,15 @@ Implemented: - raw `#include` collection for quoted and system includes - simple object-like `#define` macro collection - function-like macro metadata with unsupported diagnostics +- raw `#undef` directive provenance in macro metadata - simple primitive, pointer, array, and qualifier type extraction - simple global variable and `typedef` extraction - simple function prototype extraction +- prototype-style metadata distinguishing `int f(void)` from `int f()` - simple function-definition signature extraction with body skipping +- unsupported K&R function-definition diagnostics +- C fixture inputs under `tests/data/c/general/` plus C fixture directory + scaffolding for errors, corpus, and scientific APIs Placeholder only: @@ -133,6 +138,7 @@ Raw-source mode means source normalization plus directive metadata: source locations - record `#include` directives as structured include dependencies - record simple object-like `#define` directives as macro metadata +- record `#undef` directives as macro provenance - record function-like macros as metadata with unsupported/deferred diagnostics - parse only declarations that are already visible as ordinary C without macro expansion @@ -193,8 +199,10 @@ These return typed parser models analogous to the Fortran parser API. The current partial phase can populate `functions`, `typedefs`, `globals`, `includes`, `macros`, and metadata `diagnostics`. Composite-type lists such as `structs`, `unions`, and `enums` remain empty until their dedicated parser -phase lands. Re-export from `x2py` is still deferred; users should import from -`c_parser`. +phase lands. Functions include `prototype_style`, currently `"prototype"` for +typed or explicit `void` parameter lists and `"unspecified"` for empty +parameter lists such as `int f()`. Re-export from `x2py` is still deferred; +users should import from `c_parser`. `macro_defines` is reserved for future compiler-assisted preprocessing configuration. It must not mean that raw mode evaluates C preprocessor @@ -243,6 +251,7 @@ Per-file shape: "specifiers": [], "variadic": false, "is_definition": false, + "prototype_style": "prototype", "source_location": {"filename": "", "line": 1, "...": "..."} } ], @@ -308,8 +317,10 @@ The parser defines `CParseError` with: The CLI should print compiler-style diagnostics without tracebacks by default. 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. The current grammar -subset is intentionally tolerant for unsupported declaration forms; more hard +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. The current grammar subset +is otherwise intentionally tolerant for unsupported declaration forms; more hard syntax errors should be added only with focused tests. ## Planned Testing Workflow diff --git a/tests/data/c/README.md b/tests/data/c/README.md new file mode 100644 index 000000000..c3ec634b6 --- /dev/null +++ b/tests/data/c/README.md @@ -0,0 +1,18 @@ +C fixture data +============== + +This tree holds C-only parser fixtures. Keep these files separate from the +Fortran fixture data so C parser tests can grow without changing the existing +Fortran workflows. + +Directories: + +- `general/`: small hand-written C headers and sources that mirror the themes + in `tests/data/fortran/general/`, plus C-specific API shapes. +- `errors/parser/`: future invalid C snippets for parser diagnostic goldens. +- `corpus/`: future pinned third-party C corpus fixtures with license + provenance. +- `scientific/`: future scientific C API fixtures. + +The C parser is still partial. Fixtures may contain constructs that are not +fully parsed yet when they are useful roadmap examples. diff --git a/tests/data/c/corpus/README.md b/tests/data/c/corpus/README.md new file mode 100644 index 000000000..c7a62fb2e --- /dev/null +++ b/tests/data/c/corpus/README.md @@ -0,0 +1,6 @@ +C corpus fixtures +================= + +Store pinned third-party C corpus fixtures here once corpus testing starts. +Each corpus target should include source provenance, an exact upstream tag or +commit, and the relevant license text next to the vendored files. diff --git a/tests/data/c/errors/parser/README.md b/tests/data/c/errors/parser/README.md new file mode 100644 index 000000000..db384b657 --- /dev/null +++ b/tests/data/c/errors/parser/README.md @@ -0,0 +1,5 @@ +C parser error fixtures +======================= + +Future C parser diagnostic fixtures belong here. Expected outputs should be +generated by the C error golden workflow once that workflow exists. diff --git a/tests/data/c/general/README.md b/tests/data/c/general/README.md new file mode 100644 index 000000000..55d342545 --- /dev/null +++ b/tests/data/c/general/README.md @@ -0,0 +1,22 @@ +General C fixtures +================== + +These fixtures mirror the themes of `tests/data/fortran/general/` using C API +shapes: + +- `basic_array_update.*`: equivalent to a simple subroutine mutating an array. +- `math_api.*`: scalar functions plus vector input/output arrays. +- `particles.*`: derived-type-like particle records and handle typedefs. +- `mesh.h`: nested record and pointer ownership shapes. +- `constants.h`: module variables, constants, enums, and macros. +- `shape_exprs.h`: compile-time expression bounds and multidimensional arrays. +- `modern_math_physics.*`: a compact public API with structs, globals, and + function definitions. +- `name_reuse.h`: C tag namespace and ordinary identifier reuse examples. +- `c_richer_features.h`: C-specific callbacks, opaque handles, unions, + bitfields, conditionals, and a macro-shaped declaration. The macro-shaped + declaration is deferred in raw mode and is intended to become supported only + through compiler-preprocessed input that exposes an ordinary C declaration. + +These are fixture inputs, not generated goldens. Some constructs are richer +than the current partial parser can model. diff --git a/tests/data/c/general/basic_array_update.c b/tests/data/c/general/basic_array_update.c new file mode 100644 index 000000000..4608df7f5 --- /dev/null +++ b/tests/data/c/general/basic_array_update.c @@ -0,0 +1,15 @@ +#include "basic_array_update.h" + +void add1(int n, double x[static 1]) +{ + for (int i = 0; i < n; ++i) { + x[i] += 1.0; + } +} + +void add1_strided(int n, double *restrict x, int incx) +{ + for (int i = 0; i < n; ++i) { + x[i * incx] += 1.0; + } +} diff --git a/tests/data/c/general/basic_array_update.h b/tests/data/c/general/basic_array_update.h new file mode 100644 index 000000000..d9b7bcf95 --- /dev/null +++ b/tests/data/c/general/basic_array_update.h @@ -0,0 +1,7 @@ +#ifndef X2PY_GENERAL_BASIC_ARRAY_UPDATE_H +#define X2PY_GENERAL_BASIC_ARRAY_UPDATE_H + +void add1(int n, double x[static 1]); +void add1_strided(int n, double *restrict x, int incx); + +#endif diff --git a/tests/data/c/general/c_richer_features.h b/tests/data/c/general/c_richer_features.h new file mode 100644 index 000000000..22d1eb1ec --- /dev/null +++ b/tests/data/c/general/c_richer_features.h @@ -0,0 +1,58 @@ +#ifndef X2PY_GENERAL_C_RICHER_FEATURES_H +#define X2PY_GENERAL_C_RICHER_FEATURES_H + +#include + +#define X2PY_API(ret) ret +#define X2PY_STRINGIFY(value) #value + +#ifdef X2PY_ENABLE_FAST_PATH +int x2py_fast_path(void); +#else +int x2py_slow_path(void); +#endif + +typedef int (*x2py_compare_fn)(const void *left, const void *right); + +enum x2py_status { + X2PY_STATUS_OK = 0, + X2PY_STATUS_RETRY = 1, + X2PY_STATUS_ERROR = -1 +}; + +union x2py_scalar { + int i32; + unsigned long u64; + double f64; +}; + +struct x2py_flags { + unsigned ready : 1; + unsigned mode : 3; + unsigned reserved : 28; +}; + +struct x2py_context; +typedef struct x2py_context *x2py_context_handle; + +/* Raw mode must defer this macro-shaped declaration. It is a future supported + case only after compiler preprocessing expands X2PY_API into ordinary C. */ +X2PY_API(int) x2py_sort( + void *items, + size_t count, + size_t item_size, + x2py_compare_fn compare +); + +int x2py_register_callback( + x2py_context_handle context, + void (*callback)(void *userdata, enum x2py_status status), + void *userdata +); + +const char *x2py_status_message(enum x2py_status status); +void x2py_fill_matrix(size_t rows, size_t cols, double matrix[static rows][cols]); + +#undef X2PY_API + +#endif diff --git a/tests/data/c/general/constants.h b/tests/data/c/general/constants.h new file mode 100644 index 000000000..7dc642ec1 --- /dev/null +++ b/tests/data/c/general/constants.h @@ -0,0 +1,23 @@ +#ifndef X2PY_GENERAL_CONSTANTS_H +#define X2PY_GENERAL_CONSTANTS_H + +#include + +#define X2PY_GENERAL_NMAX 100 +#define X2PY_GENERAL_ORIGIN_RANK 3 + +typedef int c_int_like; +typedef double c_double_like; + +enum coordinate_axis { + COORD_X = 0, + COORD_Y = 1, + COORD_Z = 2 +}; + +extern c_int_like nmax; +extern c_double_like origin[3]; +extern const char *coordinate_axis_name(enum coordinate_axis axis); +size_t coordinate_axis_count(void); + +#endif diff --git a/tests/data/c/general/math_api.c b/tests/data/c/general/math_api.c new file mode 100644 index 000000000..c2616c8fa --- /dev/null +++ b/tests/data/c/general/math_api.c @@ -0,0 +1,37 @@ +#include "math_api.h" + +#include + +double norm2(int n, const double x[static 1]) +{ + double accum = 0.0; + for (int i = 0; i < n; ++i) { + accum += x[i] * x[i]; + } + return sqrt(accum); +} + +void scale(int n, double alpha, double x[static 1]) +{ + for (int i = 0; i < n; ++i) { + x[i] *= alpha; + } +} + +double dot(int n, const double *restrict x, const double *restrict y) +{ + double accum = 0.0; + for (int i = 0; i < n; ++i) { + accum += x[i] * y[i]; + } + return accum; +} + +void fill_identity3(double a[static 3][3]) +{ + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 3; ++col) { + a[row][col] = row == col ? 1.0 : 0.0; + } + } +} diff --git a/tests/data/c/general/math_api.h b/tests/data/c/general/math_api.h new file mode 100644 index 000000000..5d4ff6686 --- /dev/null +++ b/tests/data/c/general/math_api.h @@ -0,0 +1,9 @@ +#ifndef X2PY_GENERAL_MATH_API_H +#define X2PY_GENERAL_MATH_API_H + +double norm2(int n, const double x[static 1]); +void scale(int n, double alpha, double x[static 1]); +double dot(int n, const double *restrict x, const double *restrict y); +void fill_identity3(double a[static 3][3]); + +#endif diff --git a/tests/data/c/general/mesh.h b/tests/data/c/general/mesh.h new file mode 100644 index 000000000..9279fe96b --- /dev/null +++ b/tests/data/c/general/mesh.h @@ -0,0 +1,24 @@ +#ifndef X2PY_GENERAL_MESH_H +#define X2PY_GENERAL_MESH_H + +#include + +struct node { + int id; + double xyz[3]; +}; + +struct mesh { + size_t nnodes; + struct node *nodes; +}; + +typedef struct node node; +typedef struct mesh mesh; + +void node_move(struct node *node, const double delta[static 3]); +int mesh_init(struct mesh *mesh, size_t nnodes); +void mesh_clear(struct mesh *mesh); +struct node *mesh_node_at(struct mesh *mesh, size_t index); + +#endif diff --git a/tests/data/c/general/modern_math_physics.c b/tests/data/c/general/modern_math_physics.c new file mode 100644 index 000000000..d9cda6c5d --- /dev/null +++ b/tests/data/c/general/modern_math_physics.c @@ -0,0 +1,52 @@ +#include "modern_math_physics.h" + +#include + +int modern_counter = 0; +static double hidden_scale = 1.0; + +void init_particle(modern_particle *p, int pid, double mass, double x, double y, double z) +{ + p->id = pid; + p->mass = mass; + p->position[0] = x; + p->position[1] = y; + p->position[2] = z; + modern_counter += 1; +} + +double kinetic_energy(const modern_particle *p, double vx, double vy, double vz) +{ + return 0.5 * p->mass * (vx * vx + vy * vy + vz * vz) * hidden_scale; +} + +void scale_vector(int n, double v[static 1], double alpha) +{ + for (int i = 0; i < n; ++i) { + v[i] = alpha * v[i]; + } +} + +double dot3(const double a[static 3], const double b[static 3]) +{ + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +void fill_identity3_modern(double a[static 3][3]) +{ + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 3; ++col) { + a[row][col] = row == col ? 1.0 : 0.0; + } + } +} + +void normalize_particle(modern_particle *p) +{ + double n = sqrt(dot3(p->position, p->position)); + if (n > 0.0) { + p->position[0] /= n; + p->position[1] /= n; + p->position[2] /= n; + } +} diff --git a/tests/data/c/general/modern_math_physics.h b/tests/data/c/general/modern_math_physics.h new file mode 100644 index 000000000..ee5515615 --- /dev/null +++ b/tests/data/c/general/modern_math_physics.h @@ -0,0 +1,26 @@ +#ifndef X2PY_GENERAL_MODERN_MATH_PHYSICS_H +#define X2PY_GENERAL_MODERN_MATH_PHYSICS_H + +extern int modern_counter; + +struct modern_particle { + int id; + double mass; + double position[3]; +}; + +struct vector3 { + double values[3]; +}; + +typedef struct modern_particle modern_particle; +typedef struct vector3 vector3; + +void init_particle(modern_particle *p, int pid, double mass, double x, double y, double z); +double kinetic_energy(const modern_particle *p, double vx, double vy, double vz); +void scale_vector(int n, double v[static 1], double alpha); +double dot3(const double a[static 3], const double b[static 3]); +void fill_identity3_modern(double a[static 3][3]); +void normalize_particle(modern_particle *p); + +#endif diff --git a/tests/data/c/general/name_reuse.h b/tests/data/c/general/name_reuse.h new file mode 100644 index 000000000..93ff48562 --- /dev/null +++ b/tests/data/c/general/name_reuse.h @@ -0,0 +1,23 @@ +#ifndef X2PY_GENERAL_NAME_REUSE_H +#define X2PY_GENERAL_NAME_REUSE_H + +#include + +struct same_name { + int payload; +}; + +extern int same_name_i; +extern float same_name_r; +extern bool same_name_l; +extern double _Complex same_name_c; +extern char same_name_s[8]; + +void do_work_i(int *same_name); +void do_work_r(float same_name); +void do_work_l(bool same_name, struct same_name *shared); +double _Complex convert_to_complex(int same_name); +int convert_to_string(float same_name, char shared[static 16]); +bool convert_to_logical(const char *same_name); + +#endif diff --git a/tests/data/c/general/particles.c b/tests/data/c/general/particles.c new file mode 100644 index 000000000..1566e7c7f --- /dev/null +++ b/tests/data/c/general/particles.c @@ -0,0 +1,34 @@ +#include "particles.h" + +static struct particle current_particle; + +void particle_touch(struct particle *p) +{ + if (p != 0) { + current_particle = *p; + } +} + +void particle_reset(particle *p) +{ + if (p != 0) { + p->id = 0; + p->x[0] = 0.0; + p->x[1] = 0.0; + p->x[2] = 0.0; + } +} + +void particle_move(particle *p, const double delta[static 3]) +{ + if (p != 0) { + p->x[0] += delta[0]; + p->x[1] += delta[1]; + p->x[2] += delta[2]; + } +} + +const struct particle *particle_current(void) +{ + return ¤t_particle; +} diff --git a/tests/data/c/general/particles.h b/tests/data/c/general/particles.h new file mode 100644 index 000000000..cc093d52e --- /dev/null +++ b/tests/data/c/general/particles.h @@ -0,0 +1,17 @@ +#ifndef X2PY_GENERAL_PARTICLES_H +#define X2PY_GENERAL_PARTICLES_H + +struct particle { + int id; + double x[3]; +}; + +typedef struct particle particle; +typedef struct particle *particle_handle; + +void particle_touch(struct particle *p); +void particle_reset(particle *p); +void particle_move(particle *p, const double delta[static 3]); +const struct particle *particle_current(void); + +#endif diff --git a/tests/data/c/general/shape_exprs.h b/tests/data/c/general/shape_exprs.h new file mode 100644 index 000000000..17c21c20e --- /dev/null +++ b/tests/data/c/general/shape_exprs.h @@ -0,0 +1,30 @@ +#ifndef X2PY_GENERAL_SHAPE_EXPRS_H +#define X2PY_GENERAL_SHAPE_EXPRS_H + +#define X2PY_EXPR_N0 4 +#define X2PY_EXPR_N1 (X2PY_EXPR_N0 + 2) +#define X2PY_EXPR_A 8 +#define X2PY_EXPR_B 3 +#define X2PY_EXPR_C 2 + +void fill_grid(int x[static 1][X2PY_EXPR_N1]); +void update_plane(int n, float x[static 1][n]); + +void use_expr( + int x[static X2PY_EXPR_N1], + float y[static X2PY_EXPR_N0 * 2] +); + +void all_exprs( + int x1[static X2PY_EXPR_A + X2PY_EXPR_B], + int x2[static X2PY_EXPR_A - X2PY_EXPR_B], + int x3[static X2PY_EXPR_B * X2PY_EXPR_C], + int x4[static X2PY_EXPR_A / X2PY_EXPR_C], + int x5[static 1 << X2PY_EXPR_B], + int x6[static (X2PY_EXPR_A + X2PY_EXPR_B) * X2PY_EXPR_C - 1], + int x7[static -(-X2PY_EXPR_A + X2PY_EXPR_B)], + int x8[static (X2PY_EXPR_A + X2PY_EXPR_B) * (X2PY_EXPR_C + 1) - 1], + int x9[static (X2PY_EXPR_A - X2PY_EXPR_B) * (X2PY_EXPR_A - X2PY_EXPR_C)] +); + +#endif diff --git a/tests/data/c/scientific/README.md b/tests/data/c/scientific/README.md new file mode 100644 index 000000000..5538bc557 --- /dev/null +++ b/tests/data/c/scientific/README.md @@ -0,0 +1,6 @@ +C scientific fixtures +===================== + +Future scientific C API fixtures belong here. Prefer small headers and sources +that exercise wrapper-relevant patterns such as array extents, work buffers, +opaque solver handles, callback hooks, and status codes. diff --git a/tests/parser/c/test_c_cli_skeleton.py b/tests/parser/c/test_c_cli_skeleton.py index 0f8099513..0637685ce 100644 --- a/tests/parser/c/test_c_cli_skeleton.py +++ b/tests/parser/c/test_c_cli_skeleton.py @@ -2,6 +2,7 @@ """C parser CLI coverage for the current partial subset.""" import json +import os import subprocess import sys from pathlib import Path @@ -166,6 +167,37 @@ def test_cli_c_no_color_and_debug_traceback_flags_are_accepted(tmp_path: Path): assert "Parser status: partial" in res.stdout +def test_cli_c_no_color_and_no_color_env_format_parse_errors_without_ansi(tmp_path: Path): + source = tmp_path / "old_style.c" + source.write_text( + """ +int add(a, b) +int a; +int b; +{ + return a + b; +} +""", + encoding="utf-8", + ) + + base_cmd = [sys.executable, "-m", "x2py", str(source), "--language", "c", "--parse"] + no_color_res = subprocess.run( + [*base_cmd, "--no-color"], + capture_output=True, + text=True, + ) + env = {**os.environ, "NO_COLOR": "1"} + env_res = subprocess.run(base_cmd, capture_output=True, text=True, env=env) + + assert no_color_res.returncode == 1 + assert "K&R style function definitions are not supported" in no_color_res.stderr + assert "\x1b[" not in no_color_res.stderr + assert env_res.returncode == 1 + assert "K&R style function definitions are not supported" in env_res.stderr + assert "\x1b[" not in env_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 7ffc2bcbb..0467c57be 100644 --- a/tests/parser/c/test_c_declarations_and_declarators.py +++ b/tests/parser/c/test_c_declarations_and_declarators.py @@ -75,6 +75,112 @@ def test_typedef_declaration_preserves_alias_and_underlying_type_text(): assert typedef.type.storage_class == ["typedef"] +def test_pointer_array_typedefs_and_typedef_name_references_are_preserved(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +struct state; +typedef const struct state *state_ref; +typedef double vector3[3]; +typedef vector3 basis3[3]; +state_ref current_state(void); +void set_basis(basis3 basis); +""", + filename="typedef_layers.h", + ) + + typedefs = {typedef.name: typedef for typedef in parsed.typedefs} + assert typedefs["state_ref"].type.tag_kind == "struct" + assert typedefs["state_ref"].type.tag_name == "state" + assert typedefs["state_ref"].type.pointers + assert typedefs["vector3"].type.arrays[0].size == "3" + assert typedefs["basis3"].type.typedef_name == "vector3" + assert typedefs["basis3"].type.arrays[0].size == "3" + assert parsed.functions[1].parameters[0].type.typedef_name == "basis3" + + +def test_globals_with_initializers_multidimensional_arrays_and_tag_refs_parse(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +const struct state *global_state = 0; +volatile union scalar *global_scalar; +const enum status last_status = STATUS_OK; +double matrix[3][4]; +int answer = 42; +""", + filename="globals_richer.h", + ) + + globals_by_name = {glob.name: glob for glob in parsed.globals} + assert globals_by_name["global_state"].type.tag_kind == "struct" + assert globals_by_name["global_state"].type.tag_name == "state" + assert globals_by_name["global_state"].type.pointers + assert globals_by_name["global_scalar"].type.tag_kind == "union" + assert globals_by_name["last_status"].type.tag_kind == "enum" + assert [array.size for array in globals_by_name["matrix"].type.arrays] == ["3", "4"] + assert globals_by_name["answer"].type.base == "int" + + +def test_parameters_preserve_struct_union_and_enum_references(): + from c_parser import parse_c_file + + parsed = parse_c_file( + "void consume(const struct state *s, union scalar *u, enum status status);\n", + filename="tag_params.h", + ) + + params = {param.name: param for param in parsed.functions[0].parameters} + assert params["s"].type.tag_kind == "struct" + assert params["s"].type.tag_name == "state" + assert params["u"].type.tag_kind == "union" + assert params["u"].type.tag_name == "scalar" + assert params["status"].type.tag_kind == "enum" + assert params["status"].type.tag_name == "status" + + +def test_storage_classes_and_qualifiers_are_recorded_for_globals(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +extern int api_errno; +static const double scale_factor = 1.0; +_Thread_local unsigned long tls_counter; +register volatile int scratch; +""", + filename="storage_globals.h", + ) + + globals_by_name = {glob.name: glob for glob in parsed.globals} + assert globals_by_name["api_errno"].type.storage_class == ["extern"] + assert globals_by_name["scale_factor"].type.storage_class == ["static"] + assert globals_by_name["scale_factor"].type.qualifiers == ["const"] + assert globals_by_name["tls_counter"].type.storage_class == ["_Thread_local"] + assert globals_by_name["scratch"].type.storage_class == ["register"] + assert globals_by_name["scratch"].type.qualifiers == ["volatile"] + + +def test_function_bodies_do_not_contribute_local_declarations_to_globals(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +int compute(int x) +{ + int local_value = x + 1; + return local_value; +} +extern int exported_value; +""", + filename="locals.c", + ) + + assert [global_.name for global_ in parsed.globals] == ["exported_value"] + + @pytest.mark.skip(reason="recursive pointer/array type layers are not implemented yet.") def test_parenthesized_declarators_distinguish_pointer_arrays_from_array_pointers(): from c_parser import parse_c_file diff --git a/tests/parser/c/test_c_functions.py b/tests/parser/c/test_c_functions.py index 0bab21d28..2460eec8e 100644 --- a/tests/parser/c/test_c_functions.py +++ b/tests/parser/c/test_c_functions.py @@ -39,7 +39,6 @@ def test_function_definitions_skip_bodies_but_preserve_source_span(): assert fn.source_span.end.line == 5 -@pytest.mark.skip(reason="prototype-style classification is not modeled yet.") def test_void_parameter_list_and_empty_parameter_list_are_distinguished(): from c_parser import parse_c_file @@ -65,7 +64,6 @@ def test_variadic_functions_are_parsed_as_source_facts(): assert parsed.functions[0].variadic is True -@pytest.mark.skip(reason="K&R function definition diagnostics need declaration-region slicing.") def test_old_style_knr_function_definition_raises_or_records_unsupported_diagnostic(): from c_parser import CParseError, parse_c_file diff --git a/tests/parser/c/test_c_lexer_preprocessor.py b/tests/parser/c/test_c_lexer_preprocessor.py index 2b28c5e46..efa5d8589 100644 --- a/tests/parser/c/test_c_lexer_preprocessor.py +++ b/tests/parser/c/test_c_lexer_preprocessor.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """C lexer and lightweight preprocessing coverage.""" +from pathlib import Path + import pytest @@ -115,6 +117,24 @@ def test_raw_mode_records_simple_object_like_macros_as_constants(): assert macros["API_VERSION"].function_like is False +def test_raw_mode_records_undef_directives_as_macro_provenance(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +#define API_FEATURE 1 +#undef API_FEATURE +""", + filename="undefs.h", + preprocessing="raw", + ) + + assert [(macro.name, macro.directive, macro.value) for macro in parsed.macros] == [ + ("API_FEATURE", "define", "1"), + ("API_FEATURE", "undef", None), + ] + + def test_raw_mode_marks_function_like_macros_as_unsupported_until_expanded(): from c_parser import parse_c_file @@ -133,6 +153,28 @@ def test_raw_mode_marks_function_like_macros_as_unsupported_until_expanded(): assert any(diag.code == "C_UNSUPPORTED_FUNCTION_LIKE_MACRO" for diag in parsed.diagnostics) +def test_raw_mode_fixture_keeps_macro_shaped_declaration_deferred_until_preprocessing(): + from c_parser import parse_c_file + + fixture = ( + Path(__file__).resolve().parents[2] + / "data" + / "c" + / "general" + / "c_richer_features.h" + ) + + parsed = parse_c_file(fixture) + + function_names = {fn.name for fn in parsed.functions} + assert "x2py_sort" not in function_names + assert any( + diag.code == "C_UNSUPPORTED_FUNCTION_LIKE_MACRO" and diag.unit_name == "X2PY_API" + for diag in parsed.diagnostics + ) + assert any(macro.name == "X2PY_API" and macro.directive == "undef" for macro in parsed.macros) + + def test_raw_conditional_directives_do_not_select_active_branches(): from c_parser import parse_c_file diff --git a/tests/parser/c/test_c_public_api_skeleton.py b/tests/parser/c/test_c_public_api_skeleton.py index 5ea764d91..3df083cd2 100644 --- a/tests/parser/c/test_c_public_api_skeleton.py +++ b/tests/parser/c/test_c_public_api_skeleton.py @@ -117,3 +117,21 @@ def test_c_parse_error_attributes_and_diagnostic_formatting(): assert "bad.h:2:5: error[CPARSE001]: unexpected token" in diagnostic assert "2 | int broken(;" in diagnostic assert "note: parser raised at" in diagnostic + + +def test_c_parse_error_color_and_no_color_formatting(): + from c_parser import CParseError + + err = CParseError( + "unexpected token", + filename="bad.h", + line_number=2, + column=5, + source_line="int broken(;", + ) + + plain = err.format_diagnostic(color=False) + colored = err.format_diagnostic(color=True) + + assert "\x1b[" not in plain + assert "\x1b[" in colored