Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/wrapper_design_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
178 changes: 173 additions & 5 deletions semantics/c2ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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]
Expand Down
19 changes: 13 additions & 6 deletions semantics/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
25 changes: 24 additions & 1 deletion semantics/pyi_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down Expand Up @@ -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:
Expand All @@ -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}")

Expand Down
Loading
Loading