diff --git a/docs/wrapper_design_notes.md b/docs/wrapper_design_notes.md index 8847b4020..0900f11a7 100644 --- a/docs/wrapper_design_notes.md +++ b/docs/wrapper_design_notes.md @@ -11,6 +11,37 @@ Reference details live in: - `docs/fortran_parser.md` - `docs/semantics.md` +## Known Semantic Gaps To Track + +These are source-language concepts that the parser or semantic layer can often +see today, but that still need a stronger `.pyi`, readiness, or wrapper policy +before generated wrappers should treat them as supported behavior. + +### C Gaps + +| Gap | Current risk | Proposed direction | +| --- | --- | --- | +| Function pointers and callbacks | The parser can capture function-pointer shape, but semantic conversion does not yet preserve a complete callable contract that wrappers can use safely. | Round-trip callback signatures as a first-class semantic callable form, such as a dedicated callback type or `Callable[[...], ...]` plus native callback metadata. Keep wrapper readiness blocked until lifetime, threading, exception, context-pointer, and unregister policy is supplied. | +| Pointer ownership and array extents | Raw pointers, pointer-to-pointer values, unknown extents, output buffers, and arrays of pointers are ambiguous without user policy. | Keep exact pointer topology in semantic IR. Require explicit `.pyi` ownership, borrow, output, shape, nullability, and copy/readback policy before projecting to Python containers or NumPy arrays. | +| Unions | `CUnion` identifies the native type, but it does not say which member is active or whether by-value union ABI is safe. | Continue representing named and anonymous unions explicitly with `CUnion`; require active-member/discriminant policy for high-level access. Prefer a compiled shim or target layout proof for by-value union calls; otherwise keep a readiness blocker. | +| Bitfields | Bit width is parser-visible, but Python field access needs target layout, signedness, padding, and read/write rules. | Preserve bit width, declared base type, containing aggregate, and layout-sensitive attributes. Generate access through a compiled C shim or target layout probe; block direct field projection when layout cannot be proven. | +| ABI and layout attributes | Attributes such as `packed`, `aligned`, `vector_size`, `stdcall`, `ms_abi`, asm labels, and compiler-specific qualifiers can change layout or calls. | Normalize ABI facts into semantic metadata on functions, fields, and classes. Let wrappers accept only the default ABI directly; use generated shims or explicit target support for non-default calling conventions and layout-sensitive attributes. | +| `volatile`, `_Atomic`, and extended scalar types | These require memory-order, side-effect, or target-specific scalar policy that ordinary scalar mapping cannot express. | Add explicit semantic wrappers or metadata for volatile and atomic access, defaulting to blocked wrapper readiness. Extend compiler probing for target scalar spellings such as `_BitInt`, `__int128`, and `_Float128` before assigning stable dtypes. | + +### Fortran Gaps + +| Gap | Current risk | Proposed direction | +| --- | --- | --- | +| Procedure pointers and dummy procedures | A broad `Procedure` type loses enough signature and lifetime information that wrappers cannot safely call or receive callbacks. | Resolve abstract interface signatures into a first-class semantic callable form. Preserve procedure pointer, optional, pass-through, and callback lifetime facts; block wrapper generation until call direction and ownership policy are explicit. | +| `character(len=...)` and character ABI | Mapping all character forms to `String` loses length, kind, hidden length arguments, fixed buffers, and `bind(c)` byte-string behavior. | Represent character storage with length expression, kind, assumed-length status, array shape, and C-interoperability metadata. Require explicit encoding, termination, copy, and hidden-length ABI handling in wrapper policy. | +| Polymorphic `class(...)` and unlimited polymorphism | Declared base types do not fully capture dynamic type, allocation, dispatch, or `select type` behavior. | Distinguish declared type from dynamic type in semantic metadata. Treat polymorphic dummy arguments and allocatable polymorphic results as blocked until wrapper policy defines accepted dynamic types and allocation behavior. | +| Type-bound procedure details | Basic bindings can be discovered, but details such as `pass`, `nopass`, generics, operators, finalizers, and missing binding targets need stronger contracts. | Preserve complete binding metadata on semantic classes. Emit overload-like `.pyi` views for generics when concrete procedures are known; report unresolved binding targets as readiness blockers instead of silently omitting important methods. | +| Derived-type layout and interoperability | `sequence`, `bind(c)`, common ABI expectations, and component layout are wrapper-critical but not yet a complete runtime contract. | Add explicit Fortran derived-type markers and metadata for `bind(c)`, `sequence`, component order, and interoperable layout. Use compiler layout probes or generated Fortran/C shims before passing derived types by value or exposing memory views. | +| Pointer and allocatable ownership | Flags can be preserved, but association, allocation, reallocation, deallocation, and replacement of caller-visible storage are policy decisions. | Keep pointer/allocatable, rank, bounds, `intent`, and contiguity facts in semantic IR. Require wrapper policy for ownership transfer, reassociation, deallocation, and Python object replacement. | +| Assumed-rank, assumed-type, and optional descriptor-heavy arguments | Descriptors such as `dimension(..)` and `type(*)` can accept many native shapes that Python cannot infer safely. | Represent descriptor category, rank constraints, element type availability, optional presence, and contiguity. Generate wrappers only for explicit accepted rank/dtype policies or through backend shims that validate descriptors. | +| Generic interfaces and operators | Concrete procedures may exist, but the exported Python surface needs overload resolution rules. | Preserve overload sets in semantic IR and print `.pyi` overloads when signatures are unambiguous. Keep ambiguous overloads blocked until the wrapper can select a native target deterministically. | +| Coarrays, teams, events, and directive-driven device/offload behavior | These introduce parallel runtime or device-memory semantics outside normal host wrappers. | Treat as out of the initial wrapper scope. Preserve diagnostics where detected and require a separate runtime design before claiming support. | + ## Settled Scope The C frontend is a declaration and signature parser for wrapper-relevant diff --git a/semantics/c2ir.py b/semantics/c2ir.py index 142b9b819..b4fb9aad2 100644 --- a/semantics/c2ir.py +++ b/semantics/c2ir.py @@ -479,12 +479,17 @@ def visit_variable(self, variable: CVariable) -> SemanticArgument: def visit_struct(self, struct: CStruct) -> SemanticClass: name = self._struct_name(struct) metadata: dict[str, Any] = {"c_kind": "struct", "incomplete": struct.is_incomplete} - base_classes = ["Opaque"] if struct.is_incomplete else [] + if struct.name is None: + metadata["c_anonymous"] = True + fields, nested_classes = self._aggregate_fields(struct.members) return SemanticClass( name=name, native_name=struct.reference_name, - fields=[self.visit_variable(member) for member in struct.members if member.name is not None], - base_classes=base_classes, + fields=fields, + classes=nested_classes, + base_classes=self._aggregate_base_classes( + "struct", anonymous=struct.name is None, opaque=struct.is_incomplete + ), metadata=metadata, origin=SemanticOrigin( source_language="c", @@ -496,11 +501,19 @@ def visit_struct(self, struct: CStruct) -> SemanticClass: ) def visit_union(self, union: CUnion) -> SemanticClass: + metadata: dict[str, Any] = {"c_kind": "union", "incomplete": union.is_incomplete} + if union.name is None: + metadata["c_anonymous"] = True + fields, nested_classes = self._aggregate_fields(union.members) return SemanticClass( name=self._union_name(union), native_name=union.reference_name, - fields=[self.visit_variable(member) for member in union.members if member.name is not None], - metadata={"c_kind": "union", "incomplete": union.is_incomplete}, + fields=fields, + classes=nested_classes, + base_classes=self._aggregate_base_classes( + "union", anonymous=union.name is None, opaque=union.is_incomplete + ), + metadata=metadata, origin=SemanticOrigin( source_language="c", native_name=union.reference_name, @@ -510,6 +523,152 @@ def visit_union(self, union: CUnion) -> SemanticClass: ), ) + def _aggregate_fields( + self, + members: list[CVariable], + ) -> tuple[list[SemanticArgument], list[SemanticClass]]: + fields: list[SemanticArgument] = [] + nested_classes: list[SemanticClass] = [] + anonymous_member_counts: dict[str, int] = {"struct": 0, "union": 0} + used_nested_names: set[str] = set() + + for member in members: + if isinstance(member.type, CStruct | CUnion) and member.type.name is None: + kind = "struct" if isinstance(member.type, CStruct) else "union" + field_name = member.name + anonymous_member = field_name is None + if field_name is None: + index = anonymous_member_counts[kind] + anonymous_member_counts[kind] += 1 + field_name = f"_anonymous_{kind}_{index}" + nested_name = self._nested_aggregate_name(field_name, used_nested_names) + used_nested_names.add(nested_name) + nested_classes.append(self._nested_aggregate_class(member.type, name=nested_name)) + fields.append( + self._aggregate_member_argument( + member, + name=field_name, + semantic_type=self._aggregate_reference_type(member.type, name=nested_name), + anonymous_member=anonymous_member, + ) + ) + continue + + if member.name is None: + continue + fields.append(self.visit_variable(member)) + + return fields, nested_classes + + def _nested_aggregate_class(self, aggregate: CStruct | CUnion, *, name: str) -> SemanticClass: + if isinstance(aggregate, CStruct): + fields, nested_classes = self._aggregate_fields(aggregate.members) + return SemanticClass( + name=name, + native_name=aggregate.reference_name, + fields=fields, + classes=nested_classes, + base_classes=self._aggregate_base_classes( + "struct", + anonymous=True, + opaque=aggregate.is_incomplete, + ), + metadata={ + "c_kind": "struct", + "incomplete": aggregate.is_incomplete, + "c_anonymous": True, + }, + origin=SemanticOrigin( + source_language="c", + native_name=aggregate.reference_name, + source_kind="struct", + source_type=aggregate.reference_name, + source_location=self._location_dict(aggregate.source_location), + ), + ) + + fields, nested_classes = self._aggregate_fields(aggregate.members) + return SemanticClass( + name=name, + native_name=aggregate.reference_name, + fields=fields, + classes=nested_classes, + base_classes=self._aggregate_base_classes( + "union", + anonymous=True, + opaque=aggregate.is_incomplete, + ), + metadata={ + "c_kind": "union", + "incomplete": aggregate.is_incomplete, + "c_anonymous": True, + }, + origin=SemanticOrigin( + source_language="c", + native_name=aggregate.reference_name, + source_kind="union", + source_type=aggregate.reference_name, + source_location=self._location_dict(aggregate.source_location), + ), + ) + + @staticmethod + def _aggregate_base_classes(kind: str, *, anonymous: bool, opaque: bool) -> list[str]: + base_classes = ["CStruct" if kind == "struct" else "CUnion"] + if anonymous: + base_classes.append("CAnonymous") + if opaque: + base_classes.append("Opaque") + return base_classes + + def _aggregate_reference_type(self, aggregate: CStruct | CUnion, *, name: str) -> SemanticType: + kind = "struct" if isinstance(aggregate, CStruct) else "union" + semantic_type = SemanticType( + name=name, + dtype=name, + metadata={ + "c_kind": kind, + "incomplete": getattr(aggregate, "is_incomplete", False), + "c_anonymous": True, + }, + origin=self._type_origin(aggregate, native_name=aggregate.reference_name), + ) + if isinstance(aggregate, CUnion): + semantic_type.metadata.setdefault("readiness_blockers", []).append( + self._blocker( + "c_union_unsupported", + "C union arguments and returns require explicit semantic policy before wrapping.", + {"owner": name, "type": aggregate.reference_name}, + ) + ) + return semantic_type + + def _aggregate_member_argument( + self, + member: CVariable, + *, + name: str, + semantic_type: SemanticType, + anonymous_member: bool, + ) -> SemanticArgument: + if anonymous_member: + semantic_type.constraints.append(SemanticConstraint("CAnonymousMember")) + return SemanticArgument( + name=name, + semantic_type=semantic_type, + intent=self._inferred_intent(semantic_type), + visibility="private" if "static" in member.storage else "public", + default_value=member.initializer.source_text if member.initializer is not None else None, + origin=SemanticOrigin( + source_language="c", + native_name=member.name, + source_kind="variable", + source_type=self._type_text(member.type), + source_location=self._location_dict(member.source_location), + metadata={"storage": list(member.storage), "bit_width": member.bit_width}, + ), + ) + def visit_enum(self, enum: CEnum) -> SemanticEnum: enum = self._resolved_enum(enum) name = self._enum_name(enum) @@ -1420,6 +1579,15 @@ def _enum_name(self, enum: CEnum) -> str: alias = self._typedef_alias_for_type(enum) return self._identifier(alias or enum.anonymous_id or "anonymous_enum") + def _nested_aggregate_name(self, field_name: str, used_names: set[str]) -> str: + base = self._identifier(field_name) + candidate = self._identifier(f"{base}_type") + index = 1 + while candidate in used_names: + candidate = self._identifier(f"{base}_type_{index}") + index += 1 + return candidate + def _resolved_enum(self, enum: CEnum) -> CEnum: if enum.name and enum.name in self.enums: return self.enums[enum.name] diff --git a/semantics/models.py b/semantics/models.py index 5ba733fa2..1692f240e 100644 --- a/semantics/models.py +++ b/semantics/models.py @@ -446,6 +446,8 @@ class SemanticClass: methods: list[SemanticMethod] = field(default_factory=list) + classes: list[SemanticClass] = field(default_factory=list) + base_classes: list[str] = field(default_factory=list) contracts: list[SemanticContract] = field(default_factory=list) @@ -522,6 +524,16 @@ def _iter_semantic_type_tree(semantic_type: SemanticType | None): def _iter_module_semantic_types(module: SemanticModule): + def iter_class(declaration: SemanticClass): + for nested in declaration.classes: + yield from iter_class(nested) + for semantic_field in declaration.fields: + yield from _iter_semantic_type_tree(semantic_field.semantic_type) + for method in declaration.methods: + for argument in method.arguments: + yield from _iter_semantic_type_tree(argument.semantic_type) + yield from _iter_semantic_type_tree(method.return_type) + for variable in module.variables: yield from _iter_semantic_type_tree(variable.semantic_type) for declaration in module.classes: @@ -530,12 +542,7 @@ def _iter_module_semantic_types(module: SemanticModule): for enumerator in declaration.enumerators: yield from _iter_semantic_type_tree(enumerator.semantic_type) continue - for semantic_field in declaration.fields: - yield from _iter_semantic_type_tree(semantic_field.semantic_type) - for method in declaration.methods: - for argument in method.arguments: - yield from _iter_semantic_type_tree(argument.semantic_type) - yield from _iter_semantic_type_tree(method.return_type) + yield from iter_class(declaration) for function in module.functions: for argument in function.arguments: yield from _iter_semantic_type_tree(argument.semantic_type) diff --git a/semantics/pyi_parser.py b/semantics/pyi_parser.py index 158768287..127ca61f7 100644 --- a/semantics/pyi_parser.py +++ b/semantics/pyi_parser.py @@ -110,11 +110,25 @@ def class_def(self, node: ast.ClassDef, *, visibility: str) -> SemanticClass: native_name=node.name, fields=body.fields, methods=body.methods, + classes=body.classes, base_classes=base_classes, - metadata={"representation": "opaque"} if "Opaque" in base_classes else {}, + metadata=self._class_metadata(base_classes), visibility=visibility, ) + @staticmethod + def _class_metadata(base_classes: list[str]) -> dict[str, object]: + metadata: dict[str, object] = {} + if "CStruct" in base_classes: + metadata["c_kind"] = "struct" + if "CUnion" in base_classes: + metadata["c_kind"] = "union" + if "CAnonymous" in base_classes: + metadata["c_anonymous"] = True + if "Opaque" in base_classes: + metadata["representation"] = "opaque" + return metadata + def enum_def(self, node: ast.ClassDef, *, visibility: str) -> SemanticEnum: if len(node.bases) != 1 or not self.is_subscript_of(node.bases[0], "Enum"): raise ValueError(f"Enum declaration expects exactly one underlying type: {_node_text(node)!r}") @@ -904,6 +918,7 @@ def __init__(self, parser: _PyiAstParser): self.parser = parser self.fields: list[SemanticArgument] = [] self.methods: list[SemanticMethod] = [] + self.classes: list[SemanticClass] = [] def visit_body(self, nodes: list[ast.stmt]) -> None: for node in nodes: @@ -925,6 +940,14 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: ) ) + def visit_ClassDef(self, node: ast.ClassDef) -> None: + decorators = self.parser.decorators(node.decorator_list, context="class body") + if decorators.has_native_call: + raise ValueError(f"Unsupported class body decorator: {ast.unparse(node.decorator_list[-1])!r}") + if len(node.bases) == 1 and self.parser.is_subscript_of(node.bases[0], "Enum"): + raise ValueError(f"Nested enum declarations are not supported: {_node_text(node)!r}") + self.classes.append(self.parser.class_def(node, visibility=decorators.visibility)) + def generic_visit(self, node: ast.AST) -> None: raise ValueError(f"Unsupported class body node: {_node_text(node)!r}") diff --git a/semantics/pyi_printer.py b/semantics/pyi_printer.py index d8346ac35..12eba4909 100644 --- a/semantics/pyi_printer.py +++ b/semantics/pyi_printer.py @@ -303,6 +303,10 @@ def _emit_callable( def _class_body(self, cls: SemanticClass) -> str: body_parts = [] + nested_classes = "\n\n".join(self._indent_block(self.emit_class(nested), " ") for nested in cls.classes) + if nested_classes: + body_parts.append(nested_classes) + fields = "\n".join(f" {self.emit_data_member(field)}" for field in cls.fields) if fields: body_parts.append(fields) @@ -315,6 +319,10 @@ def _class_body(self, cls: SemanticClass) -> str: return " pass" return "\n\n".join(body_parts) + @staticmethod + def _indent_block(text: str, indent: str) -> str: + return "\n".join(f"{indent}{line}" if line else line for line in text.splitlines()) + def _append_imports(self, sections: list[str], module: SemanticModule) -> None: imports = self._effective_imports(module) for imp in imports: @@ -523,7 +531,7 @@ def opaque_dependency_modules( known_classes = { (module.name, cls.name) for module in known_modules for cls in module.classes if isinstance(cls, SemanticClass) } - dependencies: dict[str, set[str]] = {} + dependencies: dict[str, dict[str, str | None]] = {} for module in source_modules: for semantic_type in _iter_module_semantic_types(module): ref = semantic_type.metadata.get(EXTERNAL_TYPE_REF_METADATA) @@ -535,24 +543,38 @@ def opaque_dependency_modules( continue if (origin_module, type_name) in known_classes: continue - dependencies.setdefault(origin_module, set()).add(type_name) + c_kind = semantic_type.metadata.get("c_kind") + dependencies.setdefault(origin_module, {}).setdefault( + type_name, + c_kind if c_kind in {"struct", "union"} else None, + ) return [ SemanticModule( name=module_name, - classes=[ - SemanticClass( - name=type_name, - native_name=type_name, - base_classes=["Opaque"], - metadata={"representation": "opaque"}, - ) - for type_name in sorted(type_names) - ], + classes=[_opaque_dependency_class(type_name, c_kind) for type_name, c_kind in sorted(type_kinds.items())], ) - for module_name, type_names in sorted(dependencies.items()) + for module_name, type_kinds in sorted(dependencies.items()) ] +def _opaque_dependency_class(type_name: str, c_kind: str | None) -> SemanticClass: + base_classes: list[str] = [] + metadata: dict[str, object] = {"representation": "opaque"} + if c_kind == "struct": + base_classes.append("CStruct") + metadata["c_kind"] = "struct" + elif c_kind == "union": + base_classes.append("CUnion") + metadata["c_kind"] = "union" + base_classes.append("Opaque") + return SemanticClass( + name=type_name, + native_name=type_name, + base_classes=base_classes, + metadata=metadata, + ) + + def emit_module_stubs( modules: SemanticModule | Iterable[SemanticModule], *, diff --git a/semantics/readiness.py b/semantics/readiness.py index a0d67550d..2f708b504 100644 --- a/semantics/readiness.py +++ b/semantics/readiness.py @@ -135,8 +135,11 @@ def _public_api_counts(self) -> dict[str, int]: continue if not _is_public(cls): continue - n_classes += 1 - n_functions += sum(1 for method in cls.methods if _is_public(method)) + public_classes = [cls, *_iter_public_classes(cls)] + n_classes += len(public_classes) + n_functions += sum( + 1 for public_class in public_classes for method in public_class.methods if _is_public(method) + ) return { "n_functions": n_functions, @@ -249,6 +252,16 @@ def _check_class( known_shape_symbols = set(module_constants) | class_symbols constant_names = module_constant_names | _constant_names(cls.fields) + for nested in cls.classes: + if not _is_public(nested): + continue + self._check_class( + nested, + module=module, + module_constants=module_constants, + module_constant_names=module_constant_names, + ) + for field in cls.fields: self._check_argument( field, @@ -569,8 +582,12 @@ def __init__(self, modules: list[SemanticModule]): self.import_aliases_by_module: dict[str, set[str]] = {} for module in modules: - self.known_types.update(declaration.name for declaration in module.classes) - self.known_types.update(f"{module.name}.{declaration.name}" for declaration in module.classes) + for declaration in module.classes: + if isinstance(declaration, SemanticClass): + self.known_types.update(_class_type_names(declaration, module_name=module.name)) + else: + self.known_types.add(declaration.name) + self.known_types.add(f"{module.name}.{declaration.name}") imported_modules, import_aliases, imported_types = _import_index(module.imports) self.imported_modules_by_module[module.name] = imported_modules self.import_aliases_by_module[module.name] = import_aliases @@ -612,6 +629,21 @@ def _import_index(imports: list[str | SemanticImport]) -> tuple[set[str], set[st return imported_modules, import_aliases, imported_types +def _iter_public_classes(cls: SemanticClass): + for nested in cls.classes: + if _is_public(nested): + yield nested + yield from _iter_public_classes(nested) + + +def _class_type_names(cls: SemanticClass, *, module_name: str, prefix: str = "") -> set[str]: + qualified = f"{prefix}.{cls.name}" if prefix else cls.name + names = {cls.name, qualified, f"{module_name}.{qualified}"} + for nested in cls.classes: + names.update(_class_type_names(nested, module_name=module_name, prefix=qualified)) + return names + + def _constant_values(arguments: list[SemanticArgument]) -> dict[str, str]: return { arg.name: str(arg.default_value) diff --git a/tests/_shared/fixture_outputs.py b/tests/_shared/fixture_outputs.py index d78f3f42c..79655294e 100644 --- a/tests/_shared/fixture_outputs.py +++ b/tests/_shared/fixture_outputs.py @@ -83,8 +83,26 @@ def semantic_modules_for_fixture(path: Path): return [fortran_module_to_semantic_module(module) for module in parsed.modules] +def _prune_empty_nested_class_lists(value): + if isinstance(value, list): + return [_prune_empty_nested_class_lists(item) for item in value] + if not isinstance(value, dict): + return value + + is_class_payload = {"fields", "methods", "base_classes"}.issubset(value) + return { + key: _prune_empty_nested_class_lists(item) + for key, item in value.items() + if not (is_class_payload and key == "classes" and item == []) + } + + def semantic_payload_for_fixture(path: Path) -> dict: - return {"semantic_modules": [asdict(module) for module in semantic_modules_for_fixture(path)]} + return { + "semantic_modules": [ + _prune_empty_nested_class_lists(asdict(module)) for module in semantic_modules_for_fixture(path) + ] + } def wrap_readiness_message_payload_for_fixture(path: Path) -> dict: diff --git a/tests/parser/c/test_c_cli_skeleton.py b/tests/parser/c/test_c_cli_skeleton.py index 95db369b6..eefdb0367 100644 --- a/tests/parser/c/test_c_cli_skeleton.py +++ b/tests/parser/c/test_c_cli_skeleton.py @@ -364,7 +364,7 @@ def test_cli_c_pyi_out_writes_explicit_multi_header_owner_stubs(tmp_path: Path): ) assert result.stdout == "" - assert "class state:" in (tmp_path / "types.pyi").read_text(encoding="utf-8") + assert "class state(CStruct):" in (tmp_path / "types.pyi").read_text(encoding="utf-8") api_stub = (tmp_path / "api.pyi").read_text(encoding="utf-8") assert "from types import state" in api_stub assert "class state" not in api_stub diff --git a/tests/pyi/fixtures/c/general/c_richer_features.pyi b/tests/pyi/fixtures/c/general/c_richer_features.pyi index 205b8a6dd..24cf99a8e 100644 --- a/tests/pyi/fixtures/c/general/c_richer_features.pyi +++ b/tests/pyi/fixtures/c/general/c_richer_features.pyi @@ -1,15 +1,15 @@ class x2py_status(Enum[Int]): pass -class x2py_flags: +class x2py_flags(CStruct): ready: UInt32 mode: UInt32 reserved: UInt32 -class x2py_context(Opaque): +class x2py_context(CStruct, Opaque): pass -class x2py_scalar: +class x2py_scalar(CUnion): i32: Int u64: UInt64 f64: Float64 diff --git a/tests/pyi/fixtures/c/general/mesh.pyi b/tests/pyi/fixtures/c/general/mesh.pyi index b17789706..28aa48181 100644 --- a/tests/pyi/fixtures/c/general/mesh.pyi +++ b/tests/pyi/fixtures/c/general/mesh.pyi @@ -1,8 +1,8 @@ -class node: +class node(CStruct): id: Int xyz: Float64[3] -class mesh: +class mesh(CStruct): nnodes: SizeT nodes: Ptr(node) diff --git a/tests/pyi/fixtures/c/general/modern_math_physics.pyi b/tests/pyi/fixtures/c/general/modern_math_physics.pyi index a0a9f89ac..50e5d0ef7 100644 --- a/tests/pyi/fixtures/c/general/modern_math_physics.pyi +++ b/tests/pyi/fixtures/c/general/modern_math_physics.pyi @@ -1,9 +1,9 @@ -class modern_particle: +class modern_particle(CStruct): id: Int mass: Float64 position: Float64[3] -class vector3: +class vector3(CStruct): values: Float64[3] modern_counter: Int diff --git a/tests/pyi/fixtures/c/general/name_reuse.pyi b/tests/pyi/fixtures/c/general/name_reuse.pyi index 6576e7643..67af5cf86 100644 --- a/tests/pyi/fixtures/c/general/name_reuse.pyi +++ b/tests/pyi/fixtures/c/general/name_reuse.pyi @@ -1,4 +1,4 @@ -class same_name: +class same_name(CStruct): payload: Int same_name_i: Int diff --git a/tests/pyi/fixtures/c/general/particles.pyi b/tests/pyi/fixtures/c/general/particles.pyi index bfa5ae513..5a36e0ff0 100644 --- a/tests/pyi/fixtures/c/general/particles.pyi +++ b/tests/pyi/fixtures/c/general/particles.pyi @@ -1,4 +1,4 @@ -class particle: +class particle(CStruct): id: Int x: Float64[3] diff --git a/tests/semantics/test_c2ir.py b/tests/semantics/test_c2ir.py index 50fbc859c..9412c9b72 100644 --- a/tests/semantics/test_c2ir.py +++ b/tests/semantics/test_c2ir.py @@ -378,12 +378,12 @@ def test_c2ir_converts_structs_and_opaque_struct_pointers(): assert point.origin.native_name == "struct point" assert point.origin.source_kind == "struct" assert point.origin.source_type == "struct point" - assert context.base_classes == ["Opaque"] + assert context.base_classes == ["CStruct", "Opaque"] assert context.metadata == {"c_kind": "struct", "incomplete": True} assert scale_point.arguments[0].semantic_type.name == "point" assert context_create.return_type.name == "context" assert context_create.return_type.storage.kind == "reference" - assert "class context(Opaque):" in emit_module(module) + assert "class context(CStruct, Opaque):" in emit_module(module) report = assess_semantic_wrap_readiness(module, source="structs.h") assert report["wrappable"] is True @@ -428,10 +428,52 @@ def test_c2ir_private_include_types_remain_available_as_opaque_handles(): "representation": "opaque", } assert "from private import private_context" in stubs["api"] - assert stubs["private"] == "class private_context(Opaque):\n pass" + assert stubs["private"] == "class private_context(CStruct, Opaque):\n pass" assert assess_semantic_wrap_readiness(module, source="api.h")["wrappable"] is True +def test_c2ir_preserves_anonymous_aggregate_members_as_nested_c_classes(): + parsed = parse_c_file( + "struct flags { union { int integer; float real; }; struct { int code; } meta; int tag; };\n", + filename="flags.h", + ) + + module = c_file_to_semantic_module(parsed) + flags = module.classes[0] + anonymous_union, meta = flags.classes + anonymous_field, meta_field, tag = flags.fields + code = emit_module(module) + reparsed = parse_pyi_text(code, module_name="flags") + reparsed_flags = reparsed.classes[0] + + assert flags.base_classes == ["CStruct"] + assert flags.metadata == {"c_kind": "struct", "incomplete": False} + assert anonymous_union.base_classes == ["CUnion", "CAnonymous"] + assert anonymous_union.metadata == {"c_kind": "union", "incomplete": False, "c_anonymous": True} + assert [field.name for field in anonymous_union.fields] == ["integer", "real"] + assert meta.base_classes == ["CStruct", "CAnonymous"] + assert [field.name for field in meta.fields] == ["code"] + assert anonymous_field.name == "_anonymous_union_0" + assert anonymous_field.semantic_type.name == "anonymous_union_0_type" + assert [constraint.name for constraint in anonymous_field.semantic_type.constraints] == ["CAnonymousMember"] + assert anonymous_field.semantic_type.metadata["c_kind"] == "union" + assert meta_field.name == "meta" + assert meta_field.semantic_type.name == "meta_type" + assert tag.name == "tag" + assert "class flags(CStruct):" in code + assert "class anonymous_union_0_type(CUnion, CAnonymous):" in code + assert "_anonymous_union_0: Annotated[anonymous_union_0_type, CAnonymousMember]" in code + assert [(cls.name, cls.base_classes) for cls in reparsed_flags.classes] == [ + ("anonymous_union_0_type", ["CUnion", "CAnonymous"]), + ("meta_type", ["CStruct", "CAnonymous"]), + ] + assert reparsed_flags.metadata == {"c_kind": "struct"} + assert reparsed_flags.classes[0].metadata == {"c_kind": "union", "c_anonymous": True} + assert [constraint.name for constraint in reparsed_flags.fields[0].semantic_type.constraints] == [ + "CAnonymousMember" + ] + + def test_c2ir_explicit_project_headers_import_types_from_their_owner_module(): project = parse_c_project( {