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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ release tags add a leading `v` to the package version.

## Unreleased

- Forwardable Fortran optional arguments use a linear number of contained
procedures and converge on one native call site instead of enumerating
presence combinations. Descriptor categories that cannot be forwarded,
including optional assumed-rank arrays, preserve `PRESENT()` through direct
present/absent call leaves.

- Optional Fortran callbacks and optional reference dummies inside callback
interfaces preserve `PRESENT()` through source and generated-contract builds.

Expand Down
18 changes: 18 additions & 0 deletions docs/developer/packages/codegen/fortran-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ them.
3. It runs the selected writeback and cleanup finalizers, wrapping derived
result or carrier lifecycles when the plan requires them.

Forwardable optional arguments use a linear number of contained procedures and
converge on one native call site instead of enumerating presence combinations.
Each procedure introduces one planned optional actual and forwards the optional
dummies already introduced. Fortran therefore propagates absence through its
own optional-dummy rules. When the binding has already entered Fortran-owned
descriptors through its inverted consumer chain, that outer chain remains
active until this inner native call returns.
Mutable deferred-length character descriptors remain on direct present/absent
call leaves because compiler descriptor updates do not propagate reliably
through another optional dummy. Optional assumed-rank arrays also remain on
direct leaves because Fortran cannot declare the local assumed-rank pointer
that descriptor transport would require. Their completed entrypoint ABI carries
an explicit presence value beside the caller's descriptor or a valid rank-zero
ordinary placeholder. This avoids compiler-dependent rank loss at an optional
assumed-rank `bind(C)` dummy; the placeholder is never passed to the native
procedure. Other optionals in the same procedure still use the forwarding
chain.

The entrypoint record exposes a `bind(C)` name shared with the C binding. For a
standalone native procedure, the bridge record explicitly selects its external
declaration; for a module procedure, it supplies the native module use. Those
Expand Down
2 changes: 2 additions & 0 deletions docs/user/guide/optional-arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ Result:
- Providing a concrete value makes the argument **present**.
- Use **keyword arguments** when skipping earlier optional parameters.
- Optional arrays and derived types also accept `None` to indicate absence.
- Optional assumed-rank arrays accept ranks 1 through 15 when present; omission
and `None` preserve `present(...) == .false.`.
- Optional `intent(out)` / `intent(inout)` arguments remain visible in Python
so you can control `present(...)`.
- An optional argument without `intent` uses the same conservative
Expand Down
75 changes: 73 additions & 2 deletions prik/codegen/c/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
NativeDescriptorHandoffABI,
EntrypointProjectionAction,
EntrypointPassingConvention,
EntrypointOptionalityAction,
OptionalMode,
OverloadMatchKind,
PythonExceptionKind,
Expand Down Expand Up @@ -7807,6 +7808,8 @@ def _descriptor_array_argument_declarations(
CDeclaration(f"{names.value_name}_extents_out", self.ARRAY_EXTENTS_RECORD),
*(CDeclaration(name, "int64_t", CodeExpression("0")) for name in names.extent_names),
]
if plan.entrypoint.pass_descriptor_presence:
declarations.append(CDeclaration(names.present_name, "void *", CodeExpression("NULL")))
if plan.transformations:
declarations.append(
CDeclaration(
Expand Down Expand Up @@ -10457,7 +10460,12 @@ def _lower_entrypoint_call(self, plan: FunctionPlan, context: _CFunctionContext)
CExpressionStatement(CodeExpression(f"prik_native_array_backend_release_call({backend})"))
for backend in reversed(leased)
)
return (*acquire_nodes, *body, *release_nodes)
return (
*self._explicit_descriptor_presence_nodes(plan, context),
*acquire_nodes,
*body,
*release_nodes,
)

def _lower_entrypoint_call_with_live_descriptors(
self,
Expand Down Expand Up @@ -10495,6 +10503,66 @@ def _lower_entrypoint_call_with_live_descriptors(
),
)

def _explicit_descriptor_presence_nodes(
self,
plan: FunctionPlan,
context: _CFunctionContext,
) -> tuple:
"""Supply a valid rank-zero ordinary descriptor beside explicit presence."""
nodes = []
for owner_path in context.inverted_descriptors:
argument = self._argument_by_owner(plan, owner_path)
if (
argument.entrypoint.optionality
is not EntrypointOptionalityAction.EXPLICIT_PRESENCE_WITH_PLACEHOLDER_DESCRIPTOR
):
continue
if argument.array is None:
raise ValueError(f"Placeholder descriptor {argument.owner_path!r} has no array handoff")
names = context.arguments[owner_path]
placeholder = f"{names.value_name}_placeholder"
placeholder_type = (
"char"
if argument.datatype_family is DatatypeFamily.STRING
else PrimitiveScalarTypeRegistry.type_for(argument.semantic_type_name).array_c_spelling
)
nodes.extend(
(
CDeclaration(placeholder, placeholder_type, CodeExpression("0")),
CExpressionStatement(
CodeExpression(
f"{names.present_name} = {names.object_name} != Py_None ? "
f"(void *){names.object_name} : NULL"
)
),
CIf(
CodeExpression(f"{names.value_name} == NULL"),
body=(
CIf(
CodeExpression(
f"CFI_establish((CFI_cdesc_t *)&{names.value_name}_section, &{placeholder}, "
f"CFI_attribute_other, {self._native_array_cfi_type(argument)}, "
f"{self._native_array_expected_element_size(argument)}, 0, NULL) != CFI_SUCCESS"
),
body=(
CExpressionStatement(
CodeExpression(
'PyErr_SetString(PyExc_RuntimeError, "Could not create an absent '
f'descriptor for argument {argument.binding.python_name}")'
)
),
CReturn(CodeExpression("NULL")),
),
),
CExpressionStatement(
CodeExpression(f"{names.value_name} = (CFI_cdesc_t *)&{names.value_name}_section")
),
),
),
)
)
return tuple(nodes)

@staticmethod
def _array_crosses_as_descriptor(argument: ArgumentTransferPlan) -> bool:
"""Report whether completed policy hands this array over as a descriptor."""
Expand Down Expand Up @@ -14366,7 +14434,10 @@ def _ordinary_entrypoint_argument_parameters(
if self._array_crosses_as_descriptor(argument):
# Extents and strides travel inside the descriptor, so the
# address and the fields beside it are not needed.
return (CParameter(name, "CFI_cdesc_t *"),)
parameters = [CParameter(name, "CFI_cdesc_t *")]
if argument.entrypoint.pass_descriptor_presence:
parameters.append(CParameter(f"{name}_present", "void *"))
return tuple(parameters)
return self._array_entrypoint_argument_parameters(argument, name)
if argument.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR:
handle = argument.native_array_handle
Expand Down
Loading
Loading