diff --git a/CHANGELOG.md b/CHANGELOG.md index 746c4224f..456bb8810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,207 @@ release tags add a leading `v` to the package version. ## Unreleased +- Publishing an imported name re-exports it at runtime only where the name is + one Python object to bind. A module publishing an imported callback prototype + states where a signature comes from, and a signature is not an object, so + binding one reached for an attribute of a module that exports nothing and the + build failed outright. Each re-export now records what it publishes, and only + a procedure or a derived type becomes a runtime alias; every other kind keeps + to the semantic and contract-import paths that already carry it. + +- A re-export binds the Python name its declaring module actually published + rather than the Fortran spelling it was written with, so publishing an entity + spelled in capitals no longer looks up an attribute that does not exist. + +- A name a module publishes after a plain `use` is now re-exported. The `use` + carries every public name of the module it reads, and the `public` statement + says which of them this module means to publish; an origin that two such + modules could supply stays unresolved rather than guessed. + +- A generic interface built from several blocks merges within the scope + declaring it. Two procedures of one module may each declare an interface of + the same name, and merging them on the module they share let one procedure's + specifics answer the other's calls. + +- A generated contract writes an overload's target and a prototype import the + way the contract declaring them spells each one. The overload named a source + spelling that matched no declaration it holds, and a prototype's spelling was + kept for every module using that name rather than the one declaring it. + +- A contract can now rename what it declares. `SourceName` states the native + entity a variable or constant reaches, the way `bind` already did for a + callable, instead of replacing the name the declaration states -- editing a + contract to give an entity a Python name exported the source spelling and + dropped the edit. A source name inside `Final[...]` reaches its declaration + as well, where it was previously ignored. A generated contract is affected + too: a Fortran entity Python cannot spell, such as one named `lambda`, is + declared as `lambda_` and now stays reachable under that name. + +- A class can state the native type it reaches through `bind`, so a derived + type can be exported under a different Python name. An imported class + reference resolves through the name its declaring contract states, and a + renamed class keeps its `bind` when the contract is regenerated. + +- A generated Fortran contract no longer records a source spelling that differs + from its Python name only by case. Fortran names entities without regard to + case, so a capitalized `IK` written as `ik` renames nothing and the generated + Fortran reaches it either way; every such declaration nevertheless carried a + `SourceName` or `@bind` stating the capitals back. A name Python cannot hold + as written -- a keyword, an illegal character, one a collision moved aside -- + is a real rename and still keeps its original, as does every name from a + source language that is case-sensitive. + +- A generated contract now imports each name under the spelling the contract + that defines it uses. A source-derived contract declares a Fortran entity + under a Python name, so one spelled in capitals is declared lower case, while + the import kept asking for the source spelling and named nothing the + dependency defines -- loading the package back failed on it. A prototype is + unchanged: it keeps its declared spelling wherever it is written, so an import + binding one keeps it too. + +- An overload declaration whose specific projects an output argument into its + result is now accepted. The check compared the declared result against the + projected one including the write-through the native argument passing states, + and a native scalar descriptor result including the descriptor topology that + only a `native_call` result wrapper can name -- neither of which a declared + result type spells. A generated contract carrying such a generic, for example + one over `intent(out)` allocatable arguments, was rejected on read-back by the + same tool that wrote it. + +- A contract generated from a source whose abstract interface types a dummy + through a kind of its own now resolves that kind. An interface body's + variables reached no target probe, so a kind named only there -- through a + `use` written inside the body -- had no storage fact and `generate --pyi` + failed on a declaration the wrapper build accepted. + +- A derived type building one generic binding from several `generic ::` + statements now collects every specific into that binding. Each statement was + recorded as its own binding of the same name, so only the first reached + dispatch and calling the generic with the argument types of any later + statement raised `no matching overload`. + +- A scope naming the same module in several `use` statements now keeps every + import. Each statement was replacing the previous one, so only the last + survived; a module splitting a long import list across lines silently lost + the names the earlier lines carried, and any kind parameter among them stopped + resolving. + +- A procedure whose outputs have no completed ordering is now reported as an + unsupported wrapper policy instead of raising a comparison error. + +- Generated Fortran module leaves now import sibling contracts relatively, so + building a leaf directly loads the contracts its declarations depend on. + A native derived type exported through several modules shares one set of + generated support procedures. + +- A module that names an imported procedure in a `public` statement now + publishes it, so a facade module reaches Python instead of disappearing. The + declaration is not repeated: the published name binds to the one wrapper its + declaring module exposes, so `facade.proc is home.proc`, and the contract + keeps spelling the re-export as the import it already was. A name public only + because the module default is public states no such intent and is unchanged. + +- A generic interface that repeats a `use`-associated name now extends that + generic instead of replacing it, so the importing module dispatches to the + specifics it inherited as well as its own. Accumulation stays one-directional, + as Fortran requires: the declaring module does not gain what a later module + adds. An inherited specific is reachable only through the generic, because the + import never bound its own name. + +- A generic interface may now be declared across several blocks in one scope, + which Fortran allows and real sources use to add specifics under + preprocessor guards. The blocks become one generic carrying every entry in + declaration order, instead of being rejected as a duplicate declaration. + +- A callback interface reached through renaming re-exports now records the name + its declaring module gives it. The reference followed the module back to the + declaration but kept an alias from partway along the chain, so it named a + symbol that module does not define. + +- The semantic IR now carries a strided axis as `::`, the spelling a contract + uses, instead of a longer internal token. `prik semantics` output changes + accordingly; contracts, docstrings and generated sources are unaffected + because they already printed the contract spelling. + +- Removed the `Strided` contract name and the dimension step that carried it. + `T[::]` already spells a strided axis and `T[:]` a contiguous one, so the + longer `T[::Strided]` and `T[0:n:Strided]` forms are gone rather than kept as + a second way to write the same contract. A value in a dimension's step + position is now rejected with a message naming the spelling to use. + +- A callback interface's result now keeps the declaring module's type identity, + matching its dummies. An imported function interface returning a type its own + module declares previously attributed that type to the consuming module and + failed to build, both from Fortran source and from a generated contract. + +- A renamed callback import keeps the declared interface name beside the local + one, so a contract imports `OBJ as LOCAL_OBJ` rather than a name the declaring + module never defines. A reference that differs from the declaration only in + case is now spelled canonically instead of binding a second name. + +- Following a re-exported callback interface respects Fortran accessibility. A + module that imports an interface privately no longer exposes it to a later + `use`, and the rule applies at every hop of a chain. + +- An abstract interface imported from another module now converts in the scope + of the module that declares it. A derived type the interface names belongs to + that module, so wrapping a consumer that imports only the interface — and not + the types it mentions — no longer fails against a type identity attributed to + the consuming module. + +- Callback interface resolution now covers a `use` inside a single procedure, a + standalone procedure's own imports, and an interface re-exported through any + number of modules. File, project, and `generate --pyi` conversion share one + resolver rather than each carrying its own lookup, and a contract that + re-exports a prototype resolves back to the module that declares it. + +- A contract now imports a prototype it references but never declares, so an + interface named by a procedure-local `use` is bound in the generated `.pyi` + instead of appearing as a free name. + +- Callback docstrings now state each array argument's rank and extents. Every + generated docstring and diagnostic spells a runtime extent with the shorthand + a contract uses (`Float64[::]`) rather than the explicit step the IR stores + (`Float64[::Strided]`); the two are the same contract, while `Float64[:]` + remains the distinct contiguous one. + +- A primitive scalar callback dummy the callee may write now reaches Python as + rank-zero storage (`Out(Float64[()])`) instead of an independent value, so + the value the callback computes reaches the native caller. This covers + `intent(out)` and `intent(inout)`, and also a dummy with no declared + `intent`, which Fortran permits the callee to modify — that case keeps its + missing direction in the contract as a bare `Float64[()]` rather than gaining + a synthesized one. `--assume-intent-in-scalars` elects the input-only default + for it instead. Python has no writable scalar, so the previous `Out(Addr(T))` spelling + silently discarded the write; it is now a policy error naming the replacement. + A prototype still mirrors the native argument list — edit it with + `@native_call` to project an output into the callable's return value instead. + +- Generated docstrings now state a callback's exact callable signature — + arity, per-argument direction and element type, how an output is delivered, + and the lifetime and fatal-error rules — taken from the same completed + prototype the trampoline is generated from. + +- Assumed-shape array arguments are now supported inside a callback prototype. + A `procedure(iface)` dummy whose interface declares `values(:)` lowers to an + assumed-shape bridge dummy and a contiguous call-local copy measured from it, + instead of emitting an invalid array declaration. Array callback *results* + still require an exact shape and now report that directly. + +- A dummy procedure's interface name keeps the spelling it was declared with. + Generated `.pyi` contracts previously annotated `procedure(OBJ)` as `obj` + while importing `OBJ`, so PRIK could not rebuild from the contract it had + just written. + +- `prik generate --pyi` now resolves an abstract interface imported from + another supplied source file, matching multi-file wrapper builds. + +- A `procedure(iface)` dummy whose interface no supplied source declares now + reports the interface by name and asks for the module that declares it, + instead of failing against an opaque placeholder type. Contract extraction + spells that interface name so the generated `.pyi` stays consistent with the + import it already emits. + ## 0.5.0 — 2026-09-13 - Added CMake integration through the packaged `UsePRIK.cmake` helper and a diff --git a/docs/user/guide/callbacks.md b/docs/user/guide/callbacks.md index 475311d1b..24e97b5e6 100644 --- a/docs/user/guide/callbacks.md +++ b/docs/user/guide/callbacks.md @@ -216,6 +216,14 @@ copying an undefined incoming value, and `InOut(...)` copies the incoming value and writes changes back after the callback. Omitting the wrapper preserves an omitted Fortran `intent` rather than inventing one. +An assumed-shape callback dummy is spelled `Float64[::]`, and Python receives +the extent the native caller passed: + +| Fortran callback dummy | Matching prototype | +| --- | --- | +| `real(8), intent(in) :: values(count)` | `values: In(Float64[count])` | +| `real(8), intent(in) :: values(:)` | `values: In(Float64[::])` | + For scalar arguments, choose the spelling from the Fortran callback dummy: | Fortran callback dummy | Matching prototype | @@ -226,6 +234,51 @@ For scalar arguments, choose the spelling from the Fortran callback dummy: Both forms call Python with an independent `np.float64` scalar. The difference is the native calling convention PRIK must match. +A dummy the callee may write is different: PRIK generates rank-zero storage for +it, because Python has no writable scalar. A dummy with no declared `intent` +counts here — Fortran lets the callee both read and modify it, so PRIK is +conservative and the contract records the missing direction by carrying no +wrapper: + +| Fortran callback dummy | Generated prototype | Callback may | +| --- | --- | --- | +| `real(8), intent(in) :: f` | `f: In(Addr(Float64))` | read | +| `real(8), intent(out) :: f` | `f: Out(Float64[()])` | write | +| `real(8), intent(inout) :: f` | `f: InOut(Float64[()])` | read and write | +| `real(8) :: f` | `f: Float64[()]` | read and write | + +Pass `--assume-intent-in-scalars` to treat an undeclared scalar as input-only +instead; the dummy still records no direction, it simply stops being writable. + +Python receives a rank-zero NumPy view of the native storage. Assign through it; +rebinding the name changes nothing the native caller will read: + +```python +def objective(x, f): + f[...] = float(np.sum(x * x)) # delivers the value + f = float(np.sum(x * x)) # rebinds a local name; the caller sees nothing +``` + +To keep an ordinary Python function, write a small adapter and pass that: + +```python +def objective(x): + return float(np.sum(x * x)) + +def objective_prik(x, f): + f[...] = objective(x) +``` + +A prototype keeps the native callback's argument list, so the Python callable +mirrors the Fortran interface. To call a return-style function instead, edit the +prototype to project the output: + +```python +@prototype +@native_call([Arg(0), Return("f", 0)]) +def OBJ(x: In(Float64[::])) -> Float64: ... +``` + `Value(T)` is only for supported non-primitive scalar value dummies, such as a derived-type callback dummy declared with the Fortran `value` attribute. @@ -236,10 +289,10 @@ derived-type callback dummy declared with the Fortran `value` attribute. - The callback is only valid **during** the wrapped native call. - Native code must not store the callback for later use. - Return the exact NumPy scalar type when PRIK expects a scalar callback result. -- Primitive scalar callback arguments arrive as independent NumPy scalar values, - whether the native dummy is `value` or reference. -- Primitive scalar reference writeback is unsupported; return a scalar result - instead. +- Primitive scalar callback arguments projected as values arrive as independent + NumPy scalar values, whether the native dummy is `value` or reference. + Writable reference scalars — `out`, `inout`, or no declared `intent` — arrive + as rank-zero storage you assign through. - Arrays and derived-type arguments can expose live native state; copy data you need after the wrapped call returns. @@ -269,7 +322,7 @@ The current callback contract does not support: or supported scalar derived types. - Arrays passed by Fortran `value`, arrays of derived values, and array callback results without a complete fixed shape. Pass arrays by reference and give array - results an exact primitive shape. + results an exact primitive shape; an array *argument* may be assumed-shape. - Variable-length callback strings. Use a fixed positive `String[n]` length. - Callback execution on a different Python thread. The callback must run on the same thread that entered the wrapper. diff --git a/docs/user/reference/pyi-format.md b/docs/user/reference/pyi-format.md index 071f0e403..3eb62da4b 100644 --- a/docs/user/reference/pyi-format.md +++ b/docs/user/reference/pyi-format.md @@ -114,8 +114,8 @@ The generated forms therefore have these responsibilities: | C `.pyi` | Selected C declarations in one directly buildable contract file. | A contract build receives one entry `.pyi`: the package `__init__.pyi` for the -Fortran layout above, or the C file itself. Relative imports from a package -entry discover its leaf files. +full Fortran package, a Fortran module leaf for that module and its imported +siblings, or the C file itself. Relative imports discover dependent contracts. ### Entry Contract And Extension Identity @@ -141,13 +141,21 @@ contracts/ Building `api.pyi` directly exposes its declarations at the extension root and uses `api` as the default extension name. -Use the entry, not every imported leaf, on the command line: +Use one entry on the command line to build the full package: ```bash python3 -m prik contracts/solver/__init__.pyi \ --native-objects build/solver.o ``` +To build a module leaf directly, pass that leaf as the entry. Its relative +imports load sibling contracts needed by its declarations: + +```bash +python3 -m prik contracts/solver/solver_mod.pyi \ + --native-objects build/solver.o +``` + A source-free C contract also needs its native language selected explicitly: ```bash @@ -748,9 +756,6 @@ and supported pure specification functions. `size(values, 2)`, for example, becomes the second public extent. PRIK rejects expressions it cannot resolve before lowering. -`Strided` is a compatibility spelling for older explicit forms such as -`T[::Strided]`; author the shorter `T[::]` form. - ### Character Length And Shape `String` uses the first subscription for character length and a second @@ -994,7 +999,7 @@ valid and whether it is buildable. | Storage and result types | `Addr`, `Allocatable`, `Pointer`, `Returns`, `private` | | Compatibility/category types | `Matrix`, `Vector`, `OpaqueHandle`, `WrappedType` | | Class and C inspection markers | `CAnonymous`, `CAnonymousMember`, `CStruct`, `CUnion`, `Opaque` | -| Shape and layout markers | `Contiguous`, `COPY_F`, `Flat`, `ORDER_ANY`, `ORDER_C`, `ORDER_F`, `Strided` | +| Shape and layout markers | `Contiguous`, `COPY_F`, `Flat`, `ORDER_ANY`, `ORDER_C`, `ORDER_F` | | General metadata | `Aliased`, `ArrayCategory`, `AssumedType`, `FortranAllocatable`, `Immutable`, `MaybeUnallocated`, `Polymorphic`, `SourceName` | | Constraints and ownership | `Bounded`, `Finite`, `Range`, `Ownership`, `Transfer`, `Destruction`, `PointerAssociation`, `PointerPolicy` | | Prototype direction | `In`, `Out`, `InOut` | diff --git a/prik/cli.py b/prik/cli.py index 0c2f0cd7f..40e57d06b 100644 --- a/prik/cli.py +++ b/prik/cli.py @@ -617,6 +617,9 @@ def _convert_fortran_semantic_sources( refresh=context.refresh_fortran_type_probe, ) converted_files = [] + # A module that imports an abstract interface from another supplied file + # must resolve it here, exactly as a multi-file wrapper build does. + modules_by_file = {id(fobj): list(fobj.modules) for _p, fobj in parsed_files} for p, fobj in parsed_files: compile_time_values = _fortran_compile_time_values(fobj, context.preprocessing, **probe_options) type_facts = _fortran_type_facts( @@ -631,6 +634,9 @@ def _convert_fortran_semantic_sources( compile_time_values=compile_time_values, wrapped_derived_types=wrapped_derived_types, assume_intent_in_scalars=context.assume_intent_in_scalars, + sibling_modules=[ + module for key, modules in modules_by_file.items() if key != id(fobj) for module in modules + ], **({"type_facts": type_facts} if type_facts is not None else {}), ) converted_files.append((p, modules)) diff --git a/prik/codegen/c/binding.py b/prik/codegen/c/binding.py index e52704e4a..875200e66 100644 --- a/prik/codegen/c/binding.py +++ b/prik/codegen/c/binding.py @@ -14,7 +14,11 @@ import re from typing import ClassVar -from prik.utilities.declaration_expressions import declaration_extent_uses_power, render_declaration_extent +from prik.utilities.declaration_expressions import ( + RUNTIME_EXTENT_MARKERS, + declaration_extent_uses_power, + render_declaration_extent, +) from prik.policy.ownership import ( CodegenAction, ObjectKind, @@ -230,7 +234,6 @@ class CBindingGenerator(ClassVisitor): class; unsupported plan actions fail instead of being reinterpreted here. """ - _RUNTIME_EXTENT_MARKERS = frozenset({":", "::Strided", "Flat"}) _SHARED_OUTPUT_CLEANUP_MIN_RESULTS = 4 def require_supported(self, plan: ModulePlan) -> None: @@ -981,6 +984,8 @@ def _callback_python_argument_nodes( match transfer.python_action: case PythonBarrierAction.SCALAR_VALUE: nodes = self._callback_scalar_value_nodes(transfer, target) + case PythonBarrierAction.SCALAR_STORAGE: + nodes = self._callback_scalar_storage_nodes(transfer, target) case PythonBarrierAction.ARRAY_STORAGE: nodes = self._callback_array_nodes(transfer, position, target) case PythonBarrierAction.STRING_STORAGE: @@ -1019,6 +1024,41 @@ def _callback_scalar_value_nodes( ), ) + def _callback_scalar_storage_nodes( + self, + transfer: CallbackTransferPlan, + target: str, + ) -> tuple[CDeclaration, ...]: + """Materialize one completed rank-zero storage projection over native memory. + + The Python callable receives a rank-zero view of the same storage the + adapter hands the native caller, so an ``out`` or ``inout`` dummy is + written through instead of arriving as an independent value. + """ + if transfer.abi is not CallbackABIKind.REFERENCE: + raise ValueError( + f"Unsupported rank-zero storage callback ABI for {transfer.owner_path!r}: {transfer.abi.value}" + ) + scalar = PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name) + parameter = self._callback_parameter_base_name(transfer) + flags = "NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_ALIGNED" + if transfer.adapter_action in { + CallbackTransferAction.COPY_OUT, + CallbackTransferAction.COPY_IN_OUT, + CallbackTransferAction.BORROW_WRITABLE, + }: + flags += " | NPY_ARRAY_WRITEABLE" + return ( + CDeclaration( + target, + "PyObject *", + CodeExpression( + f"PyArray_New(&PyArray_Type, 0, NULL, {scalar.numpy_type_macro}, " + f"NULL, {parameter}_data, 0, {flags}, NULL)" + ), + ), + ) + def _callback_array_nodes( self, transfer: CallbackTransferPlan, @@ -8028,7 +8068,7 @@ def _outlined_array_bind_axis_value( flattened: bool, ) -> str | None: """Lower one axis extent, or None when the axis carries no declared extent.""" - if flattened or expression in self._RUNTIME_EXTENT_MARKERS: + if flattened or expression in RUNTIME_EXTENT_MARKERS: return None if array.extent_evaluation[axis] == "bridge": return None @@ -8199,7 +8239,7 @@ def _native_array_actual_shape_checks( nodes = [] for axis, expression in enumerate(actual.shape): if ( - expression in {":", "::Strided", "Flat"} + expression in RUNTIME_EXTENT_MARKERS or (actual.flatten_storage and axis == actual.flat_axis) or array.extent_evaluation[axis] == "bridge" ): @@ -8334,7 +8374,7 @@ def _array_shape_checks( if handoff is None or handoff.rank is None: return () checks = [] - runtime_markers = {":", "::Strided", "Flat"} + runtime_markers = RUNTIME_EXTENT_MARKERS for axis, expression in enumerate(handoff.shape): if expression in runtime_markers: continue @@ -8365,7 +8405,7 @@ def _descriptor_array_shape_checks( return () checks = [] for axis, expression in enumerate(handoff.shape): - if expression in {":", "::Strided", "Flat"} or handoff.extent_evaluation[axis] == "bridge": + if expression in RUNTIME_EXTENT_MARKERS or handoff.extent_evaluation[axis] == "bridge": continue expected = self._array_extent_expression(handoff, axis, expression, context) checks.append( @@ -15300,10 +15340,41 @@ def _module_init( for namespace in child_namespaces for node in self._child_namespace_import_registration_nodes(plan, namespace) ), + # Aliases bind after every namespace is populated, so the + # callable a re-export names already exists. + *( + node + for namespace in (root_namespace, *child_namespaces) + for node in self._namespace_alias_nodes(plan, namespace) + ), CReturn(CodeExpression("mod")), ), ) + def _namespace_alias_nodes( + self, + plan: ModulePlan, + namespace: NamespacePlan, + ) -> tuple[CExpressionStatement, ...]: + """Bind each re-exported name to the callable its owner already exposes. + + A re-export publishes an existing declaration, so the name is bound to + that one object rather than to a second wrapper for the same procedure. + """ + target = self._namespace_object_name(namespace) + nodes: list[CExpressionStatement] = [] + for alias in namespace.aliases: + source = self._namespace_object_name(self._namespace(plan, alias.source_namespace)) + nodes.append( + CExpressionStatement( + CodeExpression( + f'if (prik_bind_namespace_alias({target}, "{alias.python_name}", ' + f'{source}, "{alias.source_name}") < 0) {{ Py_DECREF(mod); return NULL; }}' + ) + ) + ) + return tuple(nodes) + def _ordered_child_namespaces(self, plan: ModulePlan) -> tuple[NamespacePlan, ...]: """Return parents before descendants regardless of editable tuple order.""" return tuple( diff --git a/prik/codegen/docstrings.py b/prik/codegen/docstrings.py index 50cd5e351..508d41de8 100644 --- a/prik/codegen/docstrings.py +++ b/prik/codegen/docstrings.py @@ -24,6 +24,7 @@ ArrayHandoffPlan, BindingStatusErrorPlan, CallbackHandoffPlan, + CallbackResultPlan, CallbackTransferPlan, ClassMethodPlan, ClassSurfacePlan, @@ -67,7 +68,7 @@ _LOGICAL_ARRAY_NOTE = "Fortran logical elements; compare with .astype(bool) rather than to 1." -_UNKNOWN_EXTENTS = frozenset({"", ":", "::", "*", ".."}) +_UNKNOWN_EXTENTS = frozenset({"", ":", "*", ".."}) class WrapperDocstringBuilder: @@ -748,6 +749,7 @@ def _argument_lines(self, argument: ArgumentTransferPlan) -> tuple[str, ...]: optional = argument.binding.optional_mode not in {OptionalMode.REQUIRED, OptionalMode.REQUIRED_DESCRIPTOR} nullable = optional or argument.binding.nullable lines = [f"{argument.binding.python_name} : {self._type(argument, nullable=nullable, signature=False)}"] + lines.extend(self._callback_signature_lines(argument)) lines.extend(self._array_lines(argument.array)) lines.extend(self._native_c_array_storage_lines(argument)) lines.extend(self._optional_lines(argument)) @@ -967,19 +969,74 @@ def _callback_type(self, callback: CallbackHandoffPlan | None) -> str: @staticmethod def _callback_transfer_type(transfer: CallbackTransferPlan) -> str: - """Render a callback prototype argument or result from completed ABI facts. + """Render one callback prototype dummy as the Python object it receives. - Derived transfers preserve their type identity. Arrays and reference - ABI transfers render as NumPy arrays; other transfers use the scalar - map. The helper is pure and does not inspect outer wrapper policy. + The spelling follows the completed Python projection rather than the + native ABI: a dummy projected as storage arrives as an array the + callable can write through, and one projected as a value does not. """ if transfer.derived_type_identity is not None: return transfer.semantic_type_name scalar = _SCALAR_TYPES.get(transfer.semantic_type_name, transfer.semantic_type_name) - if transfer.array is not None or transfer.abi.value == "reference": + if transfer.python_action in {PythonBarrierAction.ARRAY_STORAGE, PythonBarrierAction.SCALAR_STORAGE}: return f"ndarray[{scalar}]" return scalar + def _callback_signature_lines(self, argument: ArgumentTransferPlan) -> tuple[str, ...]: + """Document the exact callable one callback parameter expects. + + Every fact comes from the completed prototype the trampoline is + generated from, so the documented arity, direction and access cannot + drift from the callable the native caller actually invokes. + """ + callback = argument.callback + if callback is None: + return () + parameters = ", ".join(transfer.name for transfer in callback.arguments) + result = self._callback_result_type(callback.result) + return ( + f" Called as: {argument.binding.python_name}({parameters}) -> {result}", + *(f" {self._callback_parameter_text(transfer)}" for transfer in callback.arguments), + " Valid only during this call; do not retain the callable or its arguments.", + " An exception or an invalid return value terminates the process.", + ) + + @staticmethod + def _callback_parameter_text(transfer: CallbackTransferPlan) -> str: + """Render one prototype dummy with the shape and access it presents.""" + parts = [f"{transfer.name} : {WrapperDocstringBuilder._callback_transfer_type(transfer)}"] + parts.extend(WrapperDocstringBuilder._callback_array_facts(transfer.array)) + if transfer.intent is not None: + parts.append(f"intent({transfer.intent})") + text = ", ".join(parts) + if transfer.python_action is PythonBarrierAction.SCALAR_STORAGE: + text += f"; assign through it ({transfer.name}[...] = value)" + return text + + @staticmethod + def _callback_array_facts(array: ArrayHandoffPlan | None) -> tuple[str, ...]: + """Describe one callback array's rank and extents from its completed plan. + + The callable's ABI depends on both, and extents are spelled the way the + `.pyi` contract spells them so the two descriptions agree. + """ + if array is None or not array.rank: + return () + display = array.display_shape or array.shape + extents = ", ".join(str(extent) for extent in display) + return (f"rank {array.rank}",) + ((f"shape ({extents})",) if extents else ()) + + @staticmethod + def _callback_result_type(result: CallbackResultPlan) -> str: + """Render what the callable must return, or ``None`` for a subroutine.""" + transfer = result.transfer + if transfer is None: + return "None" + if transfer.derived_type_identity is not None: + return transfer.semantic_type_name + scalar = _SCALAR_TYPES.get(transfer.semantic_type_name, transfer.semantic_type_name) + return f"ndarray[{scalar}]" if transfer.array is not None else scalar + @staticmethod def _array_lines(array: ArrayHandoffPlan | None) -> tuple[str, ...]: """Render rank, resolved display shape, and layout notes for one array facet. @@ -993,7 +1050,8 @@ def _array_lines(array: ArrayHandoffPlan | None) -> tuple[str, ...]: lines = [WrapperDocstringBuilder._array_rank_line(array)] display_shape = array.display_shape or array.shape if display_shape and all(str(extent) not in _UNKNOWN_EXTENTS for extent in display_shape): - lines.append(f" Shape: ({', '.join(map(str, display_shape))})") + extents = (str(extent) for extent in display_shape) + lines.append(f" Shape: ({', '.join(extents)})") layout = WrapperDocstringBuilder._array_layout_label(array) if layout is not None: lines.append(f" Layout: {layout}") diff --git a/prik/codegen/fortran/bridge.py b/prik/codegen/fortran/bridge.py index b0ecc0514..e19c7a825 100644 --- a/prik/codegen/fortran/bridge.py +++ b/prik/codegen/fortran/bridge.py @@ -14,7 +14,7 @@ import re from prik.naming.native_symbols import NativeSymbolNames -from prik.utilities.declaration_expressions import render_declaration_extent +from prik.utilities.declaration_expressions import RUNTIME_EXTENT_MARKERS, render_declaration_extent from prik.policy.ownership import ( AssignmentMode, CodegenAction, @@ -858,7 +858,7 @@ def _callback_native_parameter(self, transfer: CallbackTransferPlan) -> FortranP }: attributes.append("target") if transfer.rank: - attributes.append(f"dimension({self._callback_shape(transfer)})") + attributes.append(f"dimension({self._callback_dummy_shape(transfer)})") return FortranParameter( self._callback_parameter_base_name(transfer), self._callback_native_type(transfer), @@ -932,7 +932,7 @@ def _callback_transfer_declarations( }: attributes = ["target"] if transfer.rank: - attributes.append(f"dimension({self._callback_shape(transfer)})") + attributes.append(f"dimension({self._callback_storage_shape(transfer)})") declarations.append( FortranDeclaration( self._callback_storage_name(transfer), @@ -1062,7 +1062,7 @@ def _callback_result_reconstruction( ( CodeExpression("callback_result_data"), CodeExpression("callback_result_view"), - CodeExpression(f"[{self._callback_shape(transfer)}]"), + CodeExpression(f"[{self._callback_result_shape(transfer)}]"), ), ), FortranAssignment("callback_result", CodeExpression("callback_result_view")), @@ -1086,7 +1086,7 @@ def _callback_native_result_type(self, transfer: CallbackTransferPlan | None) -> raise ValueError("Callback function result is missing its transfer plan") result_type = self._callback_native_type(transfer) if transfer.rank: - result_type += f", dimension({self._callback_shape(transfer)})" + result_type += f", dimension({self._callback_result_shape(transfer)})" return result_type def _callback_native_type(self, transfer: CallbackTransferPlan) -> str: @@ -1104,14 +1104,54 @@ def _callback_parameter_base_name(transfer: CallbackTransferPlan) -> str: """Return the base Fortran dummy name reserved for one callback transfer.""" return re.sub(r"\W", "_", transfer.name).casefold() - def _callback_shape(self, transfer: CallbackTransferPlan) -> str: - """Render completed callback extents in native Fortran syntax.""" + def _callback_array_shape(self, transfer: CallbackTransferPlan) -> tuple[str, ...]: + """Return one callback transfer's completed public extent expressions.""" if transfer.array is None or transfer.array.rank is None: raise ValueError(f"Callback array transfer {transfer.owner_path!r} has no shape plan") + return tuple(transfer.array.shape) + + def _callback_dummy_shape(self, transfer: CallbackTransferPlan) -> str: + """Render one callback dummy's extents in native Fortran syntax. + + A runtime extent lowers to an assumed-shape axis, so the dummy takes + the native caller's descriptor. The contiguous call-local copy + declared beside it carries the concrete bounds instead. + """ + return ", ".join( + ":" if expression in RUNTIME_EXTENT_MARKERS else render_declaration_extent(expression, {}, target="fortran") + for expression in self._callback_array_shape(transfer) + ) + + def _callback_storage_shape(self, transfer: CallbackTransferPlan) -> str: + """Render the contiguous call-local copy's extents for one callback dummy. + + An assumed-shape dummy cannot back ``c_loc``, so the copy is an + automatic array measured from the dummy it was declared beside. + """ + base = self._callback_parameter_base_name(transfer) return ", ".join( - render_declaration_extent(expression, {}, target="fortran") for expression in transfer.array.shape + f"size({base}, {axis + 1})" + if expression in RUNTIME_EXTENT_MARKERS + else render_declaration_extent(expression, {}, target="fortran") + for axis, expression in enumerate(self._callback_array_shape(transfer)) ) + def _callback_result_shape(self, transfer: CallbackTransferPlan) -> str: + """Render a callback array result's extents, which must be explicit. + + A function result has no caller descriptor to measure, so a runtime + extent here means policy admitted a form the native result cannot + spell. + """ + shape = self._callback_array_shape(transfer) + runtime = [expression for expression in shape if expression in RUNTIME_EXTENT_MARKERS] + if runtime: + raise ValueError( + f"Callback array result {transfer.owner_path!r} has runtime extents {runtime} " + "and cannot be spelled as a native function result" + ) + return ", ".join(render_declaration_extent(expression, {}, target="fortran") for expression in shape) + def _callback_address_source(self, transfer: CallbackTransferPlan) -> str: """Return the C-address expression that backs one callback transfer.""" if transfer.adapter_action in { @@ -8536,7 +8576,7 @@ def _procedure_prototype_result_type( """Declare one exact function result from the shared prototype plan.""" result_type = self._procedure_prototype_type(result) if result.rank: - result_type += f", dimension({self._procedure_prototype_shape(result.array, result.owner_path)})" + result_type += f", dimension({self._procedure_prototype_result_shape(result.array, result.owner_path)})" return result_type def _procedure_prototype_type( @@ -8554,9 +8594,29 @@ def _procedure_prototype_type( @staticmethod def _procedure_prototype_shape(array: ArrayHandoffPlan | None, owner_path: str) -> str: - """Render an exact prototype array shape without backend role substitution.""" + """Render a prototype dummy's shape without backend role substitution. + + A runtime extent lowers to an assumed-shape axis so the interface body + matches the native declaration it describes. + """ + if array is None or array.rank is None: + raise ValueError(f"Prototype value {owner_path!r} has no concrete shape") + return ", ".join( + ":" if expression in RUNTIME_EXTENT_MARKERS else render_declaration_extent(expression, {}, target="fortran") + for expression in array.shape + ) + + @staticmethod + def _procedure_prototype_result_shape(array: ArrayHandoffPlan | None, owner_path: str) -> str: + """Render a prototype function result's shape, which must be explicit.""" if array is None or array.rank is None: raise ValueError(f"Prototype value {owner_path!r} has no concrete shape") + runtime = [expression for expression in array.shape if expression in RUNTIME_EXTENT_MARKERS] + if runtime: + raise ValueError( + f"Prototype result {owner_path!r} has runtime extents {runtime} " + "and cannot be spelled as a native function result" + ) return ", ".join(render_declaration_extent(expression, {}, target="fortran") for expression in array.shape) def _procedure_prototype_imports( diff --git a/prik/contracts/__init__.py b/prik/contracts/__init__.py index c640da998..3741cac97 100644 --- a/prik/contracts/__init__.py +++ b/prik/contracts/__init__.py @@ -239,7 +239,6 @@ def apply(target): ORDER_F = _ContractExpression() Pointer = _DescriptorContract("pointer") Polymorphic = _ContractExpression() -Strided = _ContractExpression() Arg = _expression ArrayCategory = _expression @@ -411,7 +410,6 @@ def destroy(target): "Returns", "SizeT", "SourceName", - "Strided", "String", "Transfer", "UInt", diff --git a/prik/parsers/fortran/parser.py b/prik/parsers/fortran/parser.py index 9362a07be..9bb1d2147 100644 --- a/prik/parsers/fortran/parser.py +++ b/prik/parsers/fortran/parser.py @@ -1938,10 +1938,15 @@ def _helper_attach_file_interfaces( units: _ParsedFileUnits, ) -> list[FortranInterface]: """Collect interfaces and attach module-owned blocks to their owners.""" - interfaces = [ - self._visit(unit, parent_scope=scope, filename=filename) - for unit, scope in self._collect_interface_source_units(lines, filename) - ] + interfaces = self._merged_generic_interfaces( + [ + ( + self._visit(unit, parent_scope=scope, filename=filename), + self._interface_scope_identity(scope), + ) + for unit, scope in self._collect_interface_source_units(lines, filename) + ] + ) for module in units.modules: module.interfaces = [ iface for iface in interfaces if iface.module and iface.module.lower() == module.name.lower() @@ -1952,6 +1957,50 @@ def _helper_attach_file_interfaces( ] return [iface for iface in interfaces if iface.module is None] + @staticmethod + def _interface_scope_identity(scope: _ParserScope | None) -> tuple[tuple[str, str], ...]: + """Return the lexical scope chain that owns one interface block. + + A generic belongs to the scope declaring it, and a module, a submodule + and each procedure inside them are all separate scopes. The chain names + every enclosing one, so two procedures of the same module never look + like a single owner. + """ + chain: list[tuple[str, str]] = [] + while scope is not None: + chain.append((str(scope.kind), str(scope.name or "").casefold())) + scope = scope.parent + return tuple(reversed(chain)) + + @staticmethod + def _merged_generic_interfaces( + interfaces: list[tuple[FortranInterface, tuple[tuple[str, str], ...]]], + ) -> list[FortranInterface]: + """Combine blocks that extend one generic interface into a single record. + + Fortran lets a generic interface be built from several blocks in the + same scope, each contributing specifics. They name one generic, so the + parser reports one interface carrying every entry in declaration order. + Two scopes that happen to use one name declare two generics, so the + lexical owner is part of the identity rather than the module alone. + Abstract and unnamed blocks are never generics and stay as they are. + """ + merged: dict[tuple[tuple[tuple[str, str], ...], str], FortranInterface] = {} + result: list[FortranInterface] = [] + for interface, scope_identity in interfaces: + if not interface.name or interface.abstract: + result.append(interface) + continue + key = (scope_identity, interface.name.lower()) + existing = merged.get(key) + if existing is None: + merged[key] = interface + result.append(interface) + continue + existing.procedures.extend(interface.procedures) + existing.specific_procedures.extend(interface.specific_procedures) + return result + def _resolve_file_compile_time_facts(self, units: _ParsedFileUnits) -> None: """Apply source-visible compile-time symbols within one parsed file. @@ -2811,7 +2860,12 @@ def _helper_validate_sibling_units( continue if unit.kind == "procedure": key = ("procedure", unit.name.lower()) - elif unit.kind in {"module", "submodule", "program", "block_data", "derived_type", "interface"}: + elif unit.kind == "interface": + # A generic interface may be declared in several blocks, each + # adding specifics to the same name, so a repeat is not a + # duplicate declaration. + continue + elif unit.kind in {"module", "submodule", "program", "block_data", "derived_type"}: key = (unit.kind, unit.name.lower()) else: continue @@ -3419,7 +3473,7 @@ def _parse_module_like_spec_line( parsed_use = self._parse_use_statement(stripped) if parsed_use and hasattr(target, "uses"): module_name, mappings = parsed_use - target.uses[module_name] = mappings + self._record_use_mappings(target.uses, module_name, mappings) return if _REGEX["derived_type"].match(stripped): @@ -3587,8 +3641,8 @@ def _parse_procedure_spec_line( parsed_use = self._parse_use_statement(stripped) if parsed_use: module_name, mappings = parsed_use - proc_state.uses[module_name] = mappings - proc_state.local_uses[module_name] = mappings + self._record_use_mappings(proc_state.uses, module_name, mappings) + self._record_use_mappings(proc_state.local_uses, module_name, mappings) return # This parser is a subset parser focused on wrapper-relevant metadata. # These statements do not affect extracted signature typing/shapes. @@ -3712,6 +3766,23 @@ def _parse_type_spec_line( code="PARSE_UNSUPPORTED_DECLARATION", ) + @staticmethod + def _record_generic_binding(dtype: FortranDerivedType, binding: dict) -> None: + """Record one ``generic ::`` statement on a derived type. + + Fortran lets a type-bound generic be built from several statements in + one type, each contributing specifics. They name one binding, so the + parser reports one record carrying every target in declaration order. + The standard requires every statement for a binding to declare the same + accessibility, so the first statement's attributes stand for the rest. + """ + key = "".join(str(binding["name"]).split()).lower() + for existing in dtype.generic_bindings: + if "".join(str(existing["name"]).split()).lower() == key: + existing["targets"].extend(binding["targets"]) + return + dtype.generic_bindings.append(binding) + @staticmethod def _apply_default_component_visibility( dtype: FortranDerivedType, @@ -3769,13 +3840,14 @@ def _parse_derived_type_contains_line( attrs = [a.strip().lower() for a in split_csv(attr_txt)] if attr_txt else [] lhs, rhs_txt = [x.strip() for x in right.split("=>", 1)] rhs = [r.strip() for r in split_csv(rhs_txt)] - dtype.generic_bindings.append( + self._record_generic_binding( + dtype, { "name": lhs, "targets": rhs, "attrs": attrs, "visibility": _binding_visibility(attrs, dtype.binding_visibility), - } + }, ) return @@ -3941,7 +4013,10 @@ def _parse_declaration_left( return declaration, split_csv((decl.group("attrs") or "").strip().lstrip(", ")) if re.match(r"^procedure\s*\(", left, re.IGNORECASE): procm = _REGEX["procedure_dummy"].match(left) - iface = procm.group("iface").lower() if procm else None + # The interface name is a user-visible symbol that reaches the + # generated .pyi contract, so it keeps its declared spelling; + # every comparison against it normalizes case at the comparison. + iface = procm.group("iface") if procm else None return self._new_declaration("procedure", iface), split_csv( (procm.group("attrs") if procm else "").strip().lstrip(", ") ) @@ -4043,7 +4118,7 @@ def _store_procedure_declaration( filename=filename, code="PARSE_INTERNAL_STATE", ) - if declaration.base_type == "procedure" and declaration.kind in proc_state.imports: + if declaration.base_type == "procedure" and self._scope_key(declaration.kind or "") in proc_state.imports: declaration.kind = "" for normalized_name, shape, _initializer, entity_declaration in self._declaration_entities( right, @@ -5681,6 +5756,30 @@ def _bind_c_name(tail: str) -> str | None: name = match.groupdict().get("name") return name if name else None + @staticmethod + def _record_use_mappings( + uses: dict[str, list[FortranUseMapping]], + module_name: str, + mappings: list[FortranUseMapping], + ) -> None: + """Accumulate one ``use`` statement into a scope's import table. + + A scope may name the same module more than once, each statement adding + what it lists, so a later statement extends the imports rather than + replacing them. A bare ``use`` imports everything, which the empty + mapping list already means, and absorbs any list beside it. + """ + existing = uses.get(module_name) + if existing is None or not mappings: + uses[module_name] = mappings + return + if not existing: + return + known = {(item.source.casefold(), (item.target or item.source).casefold()) for item in existing} + existing.extend( + item for item in mappings if (item.source.casefold(), (item.target or item.source).casefold()) not in known + ) + @staticmethod def _parse_use_statement(line: str) -> tuple[str, list[FortranUseMapping]] | None: """Parse a ``use`` statement into its module and explicit mappings.""" diff --git a/prik/pipeline/build.py b/prik/pipeline/build.py index edc3ac4e5..ac3e2f41d 100644 --- a/prik/pipeline/build.py +++ b/prik/pipeline/build.py @@ -66,6 +66,7 @@ SemanticImport, SemanticModule, SemanticPrototype, + SemanticReexport, SemanticVariable, _module_semantic_types, ) @@ -2063,6 +2064,24 @@ def _apply_pyi_python_exports(entry: Path, modules_by_path: dict[Path, SemanticM tree = _pyi_export_tree(entry, modules_by_path, cache={}, pending=set()) _record_pyi_exports(tree) + for module in modules_by_path.values(): + for declaration in module.classes: + exports = _declaration_exports(declaration) + if len(exports) < 2: + continue + primary = exports[0] + source_namespace = ".".join(primary["namespace"]) + for alias in exports[1:]: + module.reexports.append( + SemanticReexport( + local_name=alias["name"], + origin_module=source_namespace, + source_name=primary["name"], + module=".".join(alias["namespace"]), + entity_kind="derived_type", + ) + ) + exports[:] = [primary] def _pyi_export_tree( @@ -3057,6 +3076,7 @@ def _merge_wrapper_modules(modules: list[SemanticModule], *, name: str | None = functions=[function for module in modules for function in module.functions], prototypes=[prototype for module in modules for prototype in module.prototypes], overload_sets=[overload for module in modules for overload in module.overload_sets], + reexports=[reexport for module in modules for reexport in module.reexports], classes=[semantic_class for module in modules for semantic_class in module.classes], variables=[variable for module in modules for variable in module.variables], metadata=_wrapper_module_metadata(modules), diff --git a/prik/pipeline/pyi.py b/prik/pipeline/pyi.py index ef5f2fb94..68c53f9ec 100644 --- a/prik/pipeline/pyi.py +++ b/prik/pipeline/pyi.py @@ -131,10 +131,26 @@ def emit_module_stubs( target.classes.extend(cls for cls in dependency.classes if cls.name not in existing) complete_semantic_policies(module for module in emitted_modules.values() if module.origin.source_language != "c") + # A prototype keeps the spelling its own contract declares, so every module + # rendered here is told which names those are before any of them writes an + # import binding one. + # A module binds a prototype name by declaring one or by publishing one it + # imported; either way a contract reading from it names it that way. + declared_prototype_names = { + (module_name, str(prototype.name)) + for module_name, module in emitted_modules.items() + for prototype in module.prototypes + } | { + (module_name, str(reexport.local_name)) + for module_name, module in emitted_modules.items() + for reexport in module.reexports + if reexport.entity_kind == "prototype" + } return { module_name: emit_module( module, normalize_fortran_public_names=normalize_fortran_public_names, + declared_prototype_names=declared_prototype_names, ).strip() for module_name, module in emitted_modules.items() } diff --git a/prik/pipeline/wrapper.py b/prik/pipeline/wrapper.py index f5850d744..e7a81ad39 100644 --- a/prik/pipeline/wrapper.py +++ b/prik/pipeline/wrapper.py @@ -20,6 +20,7 @@ from pathlib import Path import time +from prik.utilities.declaration_expressions import RUNTIME_DIMENSION_MARKERS from prik.utilities.stage_values import StageRecord from prik.policy.ownership import ( AssignmentMode, @@ -2329,19 +2330,34 @@ def _callback_scalar_projection_diagnostics( transfer: CallbackTransferPlan, position: int, ) -> tuple[WrapperPlanDiagnostic, ...]: - """Require every primitive scalar callback transfer to use its value projection.""" - if position < 0 or transfer.object_kind is not ObjectKind.SCALAR or transfer.rank != 0: + """Require every primitive scalar callback transfer to use a completed projection. + + A rank-zero primitive dummy is projected either as an independent value + or, when the native caller reads it back, as rank-zero storage the + callable writes through. Any other pairing of projection, ABI and copy + direction means completed policy and the plan disagree. + """ + if position < 0 or transfer.rank != 0: + return () + copies = { + CallbackTransferAction.COPY_IN, + CallbackTransferAction.COPY_OUT, + CallbackTransferAction.COPY_IN_OUT, + } + if transfer.object_kind is ObjectKind.SCALAR: + valid = ( + transfer.python_action is PythonBarrierAction.SCALAR_VALUE + and transfer.abi in {CallbackABIKind.VALUE, CallbackABIKind.REFERENCE} + and transfer.adapter_action in copies + ) + elif transfer.object_kind is ObjectKind.NUMPY_ARRAY: + valid = ( + transfer.python_action is PythonBarrierAction.SCALAR_STORAGE + and transfer.abi is CallbackABIKind.REFERENCE + and transfer.adapter_action in copies + ) + else: return () - valid = ( - transfer.python_action is PythonBarrierAction.SCALAR_VALUE - and transfer.abi in {CallbackABIKind.VALUE, CallbackABIKind.REFERENCE} - and transfer.adapter_action - in { - CallbackTransferAction.COPY_IN, - CallbackTransferAction.COPY_OUT, - CallbackTransferAction.COPY_IN_OUT, - } - ) return ( () if valid @@ -5033,7 +5049,7 @@ def _array_extent_evaluation_is_consistent(array: ArrayHandoffPlan) -> bool: def _array_result_extent_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Reject unresolved ordinary array result extent spellings.""" array = plan.array - if array is not None and any(shape in {":", "::Strided", "...", "Flat"} for shape in array.shape): + if array is not None and any(shape in RUNTIME_DIMENSION_MARKERS for shape in array.shape): return (self._diagnostic(plan.owner_path, "unresolved-array-result-shape", array.shape),) return () diff --git a/prik/planning/entrypoints.py b/prik/planning/entrypoints.py index 9a46916a8..527fa4ebf 100644 --- a/prik/planning/entrypoints.py +++ b/prik/planning/entrypoints.py @@ -133,8 +133,17 @@ def __init__(self, namespaces: tuple[NamespacePlan, ...]) -> None: self.namespaces = namespaces self.functions = tuple(function for namespace in namespaces for function in namespace.functions) self.variables = tuple(variable for namespace in namespaces for variable in namespace.variables) - self.derived_types = tuple(derived for namespace in namespaces for derived in namespace.derived_types) - self.classes = tuple(surface for namespace in namespaces for surface in namespace.classes) + # One native type may be exported through several Python namespaces. + # Its support procedures belong to the native type, not each export. + derived_by_identity = {} + classes_by_identity = {} + for namespace in namespaces: + for derived in namespace.derived_types: + derived_by_identity.setdefault(derived.type_identity, derived) + for surface in namespace.classes: + classes_by_identity.setdefault(surface.type_identity, surface) + self.derived_types = tuple(derived_by_identity.values()) + self.classes = tuple(classes_by_identity.values()) def build(self) -> GeneratedSupportProcedureProjection: """Collect external and binding-local support in declaration order.""" diff --git a/prik/planning/models.py b/prik/planning/models.py index dbce5c7c0..d3a2d1db2 100644 --- a/prik/planning/models.py +++ b/prik/planning/models.py @@ -1387,6 +1387,19 @@ class DeclarationCallablePlan(StageRecord): prototype: ProcedurePrototypePlan | None = None +@dataclass +class NamespaceAliasPlan(StageRecord): + """Bind one name in a namespace to a callable another namespace owns. + + A re-export publishes an existing declaration rather than adding one, so + the alias names where the callable lives instead of repeating its plan. + """ + + python_name: str + source_namespace: tuple[str, ...] + source_name: str + + @dataclass class NamespacePlan(StageRecord): """Represent one Python namespace and its directly exported wrapper owners. @@ -1403,6 +1416,7 @@ class NamespacePlan(StageRecord): derived_types: tuple[DerivedTypePlan, ...] = () classes: tuple[ClassSurfacePlan, ...] = () overloads: tuple[OverloadPlan, ...] = () + aliases: tuple[NamespaceAliasPlan, ...] = () docstring: str | None = None diff --git a/prik/planning/planner.py b/prik/planning/planner.py index ab41dac7a..9baa4491f 100644 --- a/prik/planning/planner.py +++ b/prik/planning/planner.py @@ -72,6 +72,7 @@ completed_module_variable_policy, ) from prik.naming.generated_files import bridge_source_name +from prik.naming.policy import normalize_public_name from prik.policy.exports import PythonExportPolicy from prik.policy.ownership import AssignmentMode, NativeBarrierAction, SetterAction from prik.planning.models import ( @@ -130,6 +131,7 @@ NativeEntrypointParameterPlan, NativeEntrypointProjectedSlotPlan, NativeEntrypointResultPlan, + NamespaceAliasPlan, NamespacePlan, NativeArrayActualPlan, NativeArrayDefaultHandlePlan, @@ -154,6 +156,9 @@ build_generated_support_procedure_projection, ) +# Re-export reaches Python only where the published name is one exported object. +_ALIASABLE_REEXPORT_KINDS = frozenset({"procedure", "derived_type"}) + _DATATYPE_FAMILIES = { **dict.fromkeys(BOOLEAN_SEMANTIC_TYPE_NAMES, DatatypeFamily.BOOL), @@ -371,8 +376,16 @@ def _visit_SemanticModule(self, module: models.SemanticModule) -> ModulePlan: module, class_policies, ) + aliases = self._aliases_by_namespace(module) if not any( - (*functions.values(), *variables.values(), *derived_types.values(), *classes.values(), *overloads.values()) + ( + *functions.values(), + *variables.values(), + *derived_types.values(), + *classes.values(), + *overloads.values(), + *aliases.values(), + ) ): raise ValueError(f"Semantic module {module.name!r} has no public wrapper exports") @@ -381,7 +394,9 @@ def _visit_SemanticModule(self, module: models.SemanticModule) -> ModulePlan: self._attach_overload_functions(functions, overloads) # Complete stable namespace paths, generated symbols, and required headers. - namespaces = self._namespace_plans(module.name, functions, variables, derived_types, classes, overloads) + namespaces = self._namespace_plans( + module.name, functions, variables, derived_types, classes, overloads, aliases + ) support_projection = build_generated_support_procedure_projection(namespaces) support_procedures = support_projection.support_procedures generated_code_groups = self._native_generated_code_groups( @@ -509,10 +524,13 @@ def _namespace_plans( derived_types: dict, classes: dict, overloads: dict, + aliases: dict, ) -> tuple[NamespacePlan, ...]: """Freeze linked namespace members in dependency-safe path order.""" self._complete_generated_symbols(functions, variables) - namespace_paths = self._namespace_paths((*functions, *variables, *derived_types, *classes, *overloads)) + namespace_paths = self._namespace_paths( + (*functions, *variables, *derived_types, *classes, *overloads, *aliases) + ) return tuple( self._namespace_plan( module_name, @@ -522,10 +540,64 @@ def _namespace_plans( tuple(derived_types[path]), tuple(classes[path]), tuple(overloads[path]), + tuple(aliases[path]), ) for path in namespace_paths ) + def _aliases_by_namespace(self, module: models.SemanticModule) -> dict[tuple[str, ...], list[NamespaceAliasPlan]]: + """Group each published re-export under the namespace that publishes it. + + An alias binds one Python object already exported elsewhere, so it is + planned only where the published name reaches Python as exactly that. + The declaration it names supplies the attribute to read, because a + Fortran spelling is not a Python attribute and only the completed export + knows which name the declaring namespace actually bound. + """ + grouped = defaultdict(list) + for reexport in module.reexports: + if reexport.entity_kind not in _ALIASABLE_REEXPORT_KINDS: + continue + source_namespace = tuple(part.casefold() for part in reexport.origin_module.split(".") if part) + source_name = self._exported_declaration_name(module, source_namespace, reexport.source_name) + if source_name is None: + continue + grouped[tuple(part.casefold() for part in reexport.module.split(".") if part)].append( + NamespaceAliasPlan( + python_name=normalize_public_name(reexport.local_name).name, + source_namespace=source_namespace, + source_name=source_name, + ) + ) + return grouped + + @staticmethod + def _exported_declaration_name( + module: models.SemanticModule, + namespace: tuple[str, ...], + source_name: str, + ) -> str | None: + """Return the Python name one namespace bound for a re-exported entity. + + A record reaching here states the entity either the way its source + declares it or the way its own contract already published it, so both + spellings identify the declaration. Finding none means the namespace + exports no such object and there is nothing an alias could bind. + """ + wanted = source_name.casefold() + for declaration in (*module.functions, *module.classes): + if getattr(declaration, "visibility", "public") != "public": + continue + native = str(getattr(declaration, "native_name", "") or declaration.name).casefold() + exports = declaration.metadata.get(models.PYTHON_EXPORTS_METADATA) or () + for export in exports: + name = export.get("name") + if not name or tuple(export.get("namespace") or ()) != namespace: + continue + if native == wanted or str(name).casefold() == wanted: + return str(name) + return None + def _namespace_plan( self, module_name: str, @@ -535,6 +607,7 @@ def _namespace_plan( derived_types: tuple[DerivedTypePlan, ...], classes: tuple[ClassSurfacePlan, ...], overloads: tuple[OverloadPlan, ...], + aliases: tuple[NamespaceAliasPlan, ...] = (), ) -> NamespacePlan: """Create one namespace after its generated symbols are complete.""" return NamespacePlan( @@ -545,6 +618,7 @@ def _namespace_plan( derived_types=derived_types, classes=classes, overloads=overloads, + aliases=aliases, ) def _complete_derived_backend_symbols( diff --git a/prik/policy/construction.py b/prik/policy/construction.py index 82d73340a..4ef4faf4b 100644 --- a/prik/policy/construction.py +++ b/prik/policy/construction.py @@ -173,6 +173,7 @@ FunctionWrapperPolicy, ) from prik.utilities.declaration_expressions import ( + RUNTIME_DIMENSION_MARKERS, declaration_expression_call_sites, declaration_extent_references, resolve_declaration_extent, @@ -744,6 +745,22 @@ def _class_method_blockers(method: ClassMethodPolicy) -> str | None: return None +def _overload_candidate_scope( + procedure: models.SemanticFunction, + owner_path: str, + module_generic: bool, +) -> str: + """Return the scope that addresses one overload candidate. + + A module generic addresses each specific by the module that owns it, so a + specific inherited from an imported generic stays findable. A class-bound + overload is addressed by its class instead, which owns every candidate. + """ + if not module_generic: + return owner_path + return str(procedure.origin.native_scope or owner_path) + + def _overload_policy( owner_path: str, overload: models.ProcedureOverloadSet, @@ -751,13 +768,15 @@ def _overload_policy( python_name: str | None = None, procedures: tuple[models.SemanticFunction, ...] | None = None, python_exports: tuple[PythonExportPolicy, ...] = (), + module_generic: bool = False, ) -> OverloadPolicy: """Complete one overload set from explicit concrete-procedure links.""" selected = tuple(overload.procedures) if procedures is None else procedures public_name = python_name or overload.name candidates = tuple( OverloadCandidatePolicy( - owner_path=f"{owner_path}.{overload.name}.{procedure.name}", + owner_path=f"{_overload_candidate_scope(procedure, owner_path, module_generic)}" + f".{overload.name}.{procedure.name}", arguments=(), passed_object=False, ) @@ -781,13 +800,16 @@ def build_module_overload_policy( ) -> OverloadPolicy: """Complete the stable owner and Python exports for one module generic.""" if not overload.procedures: - return _overload_policy(module.name, overload) + return _overload_policy(overload.native_scope or module.name, overload, module_generic=True) first = overload.procedures[0] - native_scope = str(first.origin.native_scope or module.name) + # A generic extending an imported one holds specifics from another module, + # so the declared scope names the owner rather than the first specific. + native_scope = str(overload.native_scope or first.origin.native_scope or module.name) return _overload_policy( native_scope, overload, python_exports=completed_python_exports(first, overload.name), + module_generic=True, ) @@ -1542,19 +1564,27 @@ def _callback_abi_kind( def _callback_adapter_action( argument: models.SemanticArgument, ) -> CallbackTransferAction: - """Select callback copy direction from the prototype's exact dummy intent.""" + """Select callback copy direction from the prototype's completed dummy contract. + + A declared ``intent`` names the direction outright. With none declared the + callee may both read and modify the dummy, so the direction follows the + completed storage: writable rank-zero storage copies in and out, while a + value projection is input-only. + """ semantic_type = argument.semantic_type intent = argument.origin.metadata.get(models.PROTOTYPE_INTENT_METADATA) if intent == "out": return CallbackTransferAction.COPY_OUT if intent == "inout": return CallbackTransferAction.COPY_IN_OUT - if ( - intent == "in" - or bool(argument.origin.metadata.get("value")) - or (semantic_type.name in _PLAN_PRIMITIVE_SCALAR_TYPES and int(semantic_type.rank or 0) == 0) - ): + if intent == "in" or bool(argument.origin.metadata.get("value")): return CallbackTransferAction.COPY_IN + if semantic_type.name in _PLAN_PRIMITIVE_SCALAR_TYPES and int(semantic_type.rank or 0) == 0: + return ( + CallbackTransferAction.COPY_IN_OUT + if _is_scalar_storage_type(semantic_type) + else CallbackTransferAction.COPY_IN + ) return CallbackTransferAction.COPY_IN_OUT @@ -1574,6 +1604,13 @@ def _callback_transfer_blockers( ) if transfer.passed_by_value and transfer.rank > 0: blockers.append(f"callback argument {argument.name!r} cannot pass an array by value") + if _discards_callback_scalar_writeback(transfer): + # Python has no writable scalar, so a value projection cannot deliver + # anything back to the native caller that reads this dummy after the call. + blockers.append( + f"callback argument {argument.name!r} is intent({transfer.intent}) and cannot use the " + f"value spelling Addr({semantic_type.name}); use {semantic_type.name}[()] for writable storage" + ) if semantic_type.name == "String": if transfer.character_length is None or transfer.character_length <= 0: blockers.append(f"callback argument {argument.name!r} requires a fixed positive character length") @@ -1587,6 +1624,17 @@ def _callback_transfer_blockers( return tuple(blockers) +def _discards_callback_scalar_writeback(transfer: CallbackTransferPolicy) -> bool: + """Report whether a written-back scalar dummy was projected as an unwritable value.""" + return bool( + transfer.rank == 0 + and not transfer.passed_by_value + and transfer.intent is not None + and str(transfer.intent).casefold() in {"out", "inout"} + and transfer.python_action is PythonBarrierAction.SCALAR_VALUE + ) + + def _callback_result_policy( return_type: object, *, @@ -4373,6 +4421,12 @@ def _derived_argument_handoff_blockers( """Require the exact native type definition for a typed value call.""" if derived is None: return () + interface = argument.semantic_type.metadata.get(models.UNRESOLVED_PROCEDURE_INTERFACE_METADATA) + if interface is not None: + return ( + f"argument {argument.name!r} declares procedure interface {str(interface)!r}, " + "which no supplied source declares; add the module that declares it to the build inputs", + ) return _derived_type_definition_blockers(f"argument {argument.name!r}", derived, derived_types) @@ -4766,7 +4820,15 @@ def _resolve_derived_type_policy( if exact is not None: return exact if semantic_type.metadata.get(models.EXTERNAL_TYPE_REF_METADATA) is not None: - return None + # An imported reference names the type the way the module declaring it + # writes it, which is its own name rather than the native type it binds. + # The search stays inside that module, so a type of the same name + # declared elsewhere is never reached. + scope, name = requested_identity + imported_matches = tuple( + policy for policy in derived_types.values() if policy.native_scope == scope and policy.type_name == name + ) + return imported_matches[0] if len(imported_matches) == 1 else None local_matches = tuple(policy for policy in derived_types.values() if policy.type_name == semantic_type.name) return local_matches[0] if len(local_matches) == 1 else None @@ -5713,7 +5775,7 @@ def _ordinary_array_result_blockers( if decision.nullable or decision.descriptor_boundary: blockers.append(f"{label} is descriptor-backed or nullable") array = _array_handoff_policy(semantic_type) - if array is None or array.rank is None or any(shape in {":", "::Strided", "...", "Flat"} for shape in array.shape): + if array is None or array.rank is None or any(shape in RUNTIME_DIMENSION_MARKERS for shape in array.shape): blockers.append(f"{label} ordinary array shape is not fully expressible") elif array.native_order != array.order: blockers.append(f"{label} COPY_F applies only to Python-visible array arguments") @@ -5867,6 +5929,10 @@ def _result_position_blockers( ) if not positions: return () + if any(position is None for position in positions): + # An unplaced output has no position to order, which this check reports + # rather than comparing against the positions that do exist. + return (f"binding result positions are incomplete; received {positions}",) if sorted(positions) == list(range(len(positions))) and len(set(positions)) == len(positions): return () return (f"binding result positions must cover 0..{len(positions) - 1} exactly once; received {positions}",) @@ -7745,7 +7811,7 @@ def _is_phase6_raw_array_address_type(semantic_type: models.SemanticType) -> boo supported_element = _is_plan_primitive_value_type(semantic_type) or ( semantic_type.name == "String" and policy.itemsize is not None ) - return supported_element and all(item not in {":", "::Strided", "...", "Flat"} for item in policy.shape) + return supported_element and all(item not in RUNTIME_DIMENSION_MARKERS for item in policy.shape) def _is_raw_array_address_type(semantic_type: models.SemanticType) -> bool: diff --git a/prik/printers/pyi.py b/prik/printers/pyi.py index b7f066ef9..e0068d39e 100644 --- a/prik/printers/pyi.py +++ b/prik/printers/pyi.py @@ -19,6 +19,7 @@ from prik.codegen.primitive_scalar_types import NumpyDtypeRegistry from prik.contracts import CONTRACT_SYMBOLS, CONTRACT_TYPE_NAMES from prik.naming import NamingPolicy +from prik.naming.policy import normalize_public_name from prik.semantics.scalar_types import SEMANTIC_SCALAR_TYPE_NAMES from prik.semantics.ownership_metadata import ( OWNERSHIP_POLICY_METADATA, @@ -51,6 +52,7 @@ PYTHON_VALUE_MUTABILITY_METADATA, PROTOTYPE_INTENT_METADATA, PROTOTYPE_REF_METADATA, + UNRESOLVED_PROCEDURE_INTERFACE_METADATA, RUNTIME_RELEASE_GIL_METADATA, HIDDEN_NATIVE_OUTPUT_METADATA, RUNTIME_STATUS_ERROR_METADATA, @@ -73,6 +75,7 @@ _module_semantic_types, ) from prik.semantics.native_array_handles import native_array_data_type, native_array_descriptor_kind +from prik.semantics.pyi_metadata import PYI_LOADED_METADATA from prik.utilities.visitor import ClassVisitor _WRAPPED_CALLABLE_TYPE_METADATA = "pyi_wrapped_callable_type" @@ -159,13 +162,26 @@ class PyiPrinter(ClassVisitor): # Public entrypoints and state # ------------------------------------------------------------------ - def __init__(self, *, normalize_fortran_public_names: bool = False): + def __init__( + self, + *, + normalize_fortran_public_names: bool = False, + declared_prototype_names: Iterable[tuple[str, str]] = (), + ): """Configure public-name normalization for independent emissions. Set normalize_fortran_public_names when emitting source-derived Fortran - contracts whose public names need Python normalization. + contracts whose public names need Python normalization. Pass + declared_prototype_names, as ``(module, name)`` pairs, when rendering one + module alongside others, so an import naming a prototype another + contract declares is written under the spelling that contract keeps. + The declaring module is part of that identity because an unrelated + module may spell an ordinary declaration the same way. """ self._normalize_fortran_public_names = normalize_fortran_public_names + self._declared_prototype_names = frozenset( + (str(module).casefold(), str(name)) for module, name in declared_prototype_names + ) def emit(self, node) -> str: """Render one supported semantic model to semantic .pyi text. @@ -229,7 +245,13 @@ def _visit_SemanticType( if semantic_type.name == "Unknown" or semantic_type.dtype == "Unknown": raise ValueError("Cannot emit .pyi with unresolved semantic type 'Unknown'") array_descriptor = native_array_descriptor_kind(semantic_type) - if PROTOTYPE_REF_METADATA in semantic_type.metadata: + unresolved_interface = semantic_type.metadata.get(UNRESOLVED_PROCEDURE_INTERFACE_METADATA) + if unresolved_interface is not None: + # The declaration named an interface no supplied module declares. + # Spelling that name keeps the extracted contract self-consistent + # with the import already emitted for it. + text = str(unresolved_interface) + elif PROTOTYPE_REF_METADATA in semantic_type.metadata: text = semantic_type.name elif array_descriptor is not None: wrapper = "Allocatable" if array_descriptor == "allocatable" else "Pointer" @@ -362,6 +384,20 @@ def _emit_method( parameter_indent=" ", ).rstrip() + @staticmethod + def _overload_target_name(candidate: SemanticFunction, context: _PyiEmissionContext) -> str: + """Return the specific an overload names, as this contract declares it. + + The target names a declaration in the same contract, and a contract + writing its declarations under Python names writes that one the same + way. Naming the source spelling instead points at no declaration the + contract holds. + """ + target = str(candidate.metadata.get(OVERLOAD_TARGET_METADATA) or candidate.native_name or candidate.name) + if not context.normalize_fortran_public_names or candidate.origin.source_language != "fortran": + return target + return normalize_public_name(target).name + def _visit_ProcedureOverloadSet( self, overload_set: ProcedureOverloadSet, @@ -373,7 +409,7 @@ def _visit_ProcedureOverloadSet( definitions = [] for procedure in overload_set.procedures: candidate = deepcopy(procedure) - target = str(candidate.metadata.get(OVERLOAD_TARGET_METADATA) or candidate.native_name or candidate.name) + target = self._overload_target_name(candidate, context) if in_class: candidate = self._overload_method(overload_set, candidate) definition = self._emit_method( @@ -433,6 +469,15 @@ def _visit_SemanticClass( decorators.append(f"@{context.contract('abstract')}") if self._class_uses_c_abi(cls): decorators.append(f'@{context.contract("native_abi")}("c")') + # Only a Fortran type states a separate native name here. A C struct + # keeps its native spelling -- `struct node` for `node` -- through its + # own representation rules, which state it without a decorator. + if ( + cls.origin.source_language == "fortran" + and cls.native_name + and self._renames_native_entity(cls, cls.native_name, cls.name) + ): + decorators.append(f"@{context.contract('bind')}({json.dumps(str(cls.native_name))})") decorator_text = "\n".join(decorators) if decorator_text: decorator_text += "\n" @@ -657,12 +702,7 @@ def _canonical_array_dimension(dimension: object) -> str: @staticmethod def _printed_array_dimension(dimension: object) -> str: """Return the public `.pyi` spelling for an array dimension.""" - text = PyiPrinter._canonical_array_dimension(dimension) - if text == "::Strided": - return "::" - if text.endswith(":Strided"): - return text[: -len("Strided")] - return text + return PyiPrinter._canonical_array_dimension(dimension) @staticmethod def _array_annotation_metadata( @@ -941,7 +981,7 @@ def _emit_data_member( self._annotation_target(name), variable, context, - original_name=variable.name if name != variable.name else None, + original_name=variable.name if self._renames_native_entity(variable, variable.name, name) else None, ) def _emit_module_variable( @@ -955,7 +995,7 @@ def _emit_module_variable( self._annotation_target(name), arg, context, - original_name=arg.name if name != arg.name else None, + original_name=arg.name if self._renames_native_entity(arg, arg.name, name) else None, ) @staticmethod @@ -1332,7 +1372,7 @@ def _constructor_argument( or self._python_literal_text(field.default_value) or "..." ) - if name != field.name: + if self._renames_native_entity(field, field.name, name): type_text = self._annotated_type_text( type_text, [f"{context.contract('SourceName')}({json.dumps(field.name)})"], @@ -1442,8 +1482,16 @@ def _append_imports( if contract_import: sections.append(contract_import) imports = self._effective_imports(module) + verbatim = self._verbatim_import_names(module) for imp in imports: - sections.append(self._emit_import(imp)) + sections.append( + self._emit_import( + imp, + native_source=not module.metadata.get(PYI_LOADED_METADATA), + public_names=context.normalize_fortran_public_names, + verbatim_names=verbatim, + ) + ) if contract_import or imports: sections.append("") @@ -1460,9 +1508,49 @@ def _effective_imports(cls, module: SemanticModule) -> list[str | SemanticImport satisfied_namespaces = cls._satisfied_procedure_namespace_import_names(imports, procedure_namespaces) imports.extend(cls._synthetic_flat_external_type_imports(module, imports, procedure_namespaces)) imports.extend(cls._missing_expression_callable_imports(module, imports)) + imports.extend(cls._missing_prototype_imports(module, imports)) imports.extend(cls._missing_procedure_namespace_imports(procedure_namespaces, satisfied_namespaces)) return imports + @classmethod + def _missing_prototype_imports( + cls, + module: SemanticModule, + imports: list[str | SemanticImport], + ) -> list[SemanticImport]: + """Return imports for prototypes this module references but never declares. + + A ``use`` inside one procedure names an interface without appearing in + the module's own imports, so the annotation would reference a name the + contract never binds. The prototype reference records where it came + from, which is enough to bind it explicitly. + """ + bound = { + (item.target or item.source).casefold() + for imported in imports + if isinstance(imported, SemanticImport) + for item in imported.items + } + bound.update(prototype.name.casefold() for prototype in module.prototypes) + required: dict[str, list[SemanticImportItem]] = {} + for semantic_type in _module_semantic_types(module): + reference = semantic_type.metadata.get(PROTOTYPE_REF_METADATA) + if not isinstance(reference, dict): + continue + local_name = str(reference.get("local_name") or reference.get("name") or "") + origin = str(reference.get("origin_module") or "") + if not local_name or not origin or local_name.casefold() in bound: + continue + native_name = str(reference.get("name") or local_name) + required.setdefault(origin, []).append( + SemanticImportItem( + source=native_name, + target=local_name if local_name != native_name else None, + ) + ) + bound.add(local_name.casefold()) + return [SemanticImport(module=name, items=items) for name, items in required.items()] + @classmethod def _missing_expression_callable_imports( cls, @@ -1694,22 +1782,101 @@ def class_has_overloads(cls: SemanticClass) -> bool: ) @staticmethod - def _emit_import(imp: str | SemanticImport) -> str: + def _emit_import( + imp: str | SemanticImport, + *, + native_source: bool = False, + public_names: bool = False, + verbatim_names: frozenset[tuple[str, str]] = frozenset(), + ) -> str: """Emit import syntax.""" if isinstance(imp, str): return f"import {imp}" if not imp.items: return f"import {imp.module}" - items = ", ".join(PyiPrinter._emit_import_item(item) for item in imp.items) - return f"from {imp.module} import {items}" + source_module = imp.module.lstrip(".").casefold() + items = ", ".join( + PyiPrinter._emit_import_item( + item, + public_names=public_names, + verbatim_names=verbatim_names, + source_module=source_module, + ) + for item in imp.items + ) + module_name = f".{imp.module}" if native_source and not imp.module.startswith(".") else imp.module + return f"from {module_name} import {items}" @staticmethod - def _emit_import_item(item: SemanticImportItem) -> str: - """Emit import item syntax.""" + def _emit_import_item( + item: SemanticImportItem, + *, + public_names: bool = False, + verbatim_names: frozenset[tuple[str, str]] = frozenset(), + source_module: str = "", + ) -> str: + """Emit import item syntax. + + An import names what the module it reads from publishes. Where a + source-derived contract writes its declarations under Python names, the + names it imports are spelled that way too -- a source keeping a Fortran + entity in capitals declares it lower case, and an importer asking for + the source spelling asks for a name no contract defines. A prototype is + the exception: it keeps its declared spelling wherever it is written, + because an annotation naming it is written the same way. + """ + # The source names what the module read from declares and the target + # what this contract calls it; either spelling identifies a prototype + # of that module, and a same-named declaration elsewhere does not. + if (source_module, item.source) in verbatim_names or ( + source_module, + item.target or item.source, + ) in verbatim_names: + return PyiPrinter._verbatim_import_item(item) + source = PyiPrinter._public_import_name(item.source, public_names=public_names) + target = PyiPrinter._public_import_name(item.target, public_names=public_names) + if target and target != source: + return f"{source} as {target}" + return source + + @staticmethod + def _verbatim_import_item(item: SemanticImportItem) -> str: + """Emit one import item under the spelling its declaration keeps.""" if item.target and item.target != item.source: return f"{item.source} as {item.target}" return item.source + @staticmethod + def _public_import_name(name: str | None, *, public_names: bool) -> str | None: + """Return one imported name as the contract that defines it spells it.""" + if not public_names or not name or name == "*": + return name + return normalize_public_name(name).name + + def _verbatim_import_names(self, module: SemanticModule) -> frozenset[tuple[str, str]]: + """Return prototype identities a contract writes under declared spelling. + + A prototype keeps the spelling its own contract declares, and an + annotation naming one is written the same way, so an import binding it + keeps that spelling too. Each identity names the module declaring the + prototype as well as the prototype, because another module may spell an + ordinary declaration the same way and that one follows Python naming. + The modules rendered together with this one supply the identities; a + prototype this module declares itself and one an annotation here already + resolved are known without them. + """ + names = set(self._declared_prototype_names) + names.update((module.name.casefold(), str(prototype.name)) for prototype in module.prototypes) + for semantic_type in _module_semantic_types(module): + reference = semantic_type.metadata.get(PROTOTYPE_REF_METADATA) + if not isinstance(reference, dict): + continue + local_name = reference.get("local_name") or reference.get("name") + origin = reference.get("origin_module") + if local_name and origin: + names.add((str(origin).casefold(), str(local_name))) + return frozenset(names) + def _append_items(self, sections: list[str], items: list, emit_item) -> None: """Append items.""" for item in items: @@ -2104,13 +2271,13 @@ def _bind_target( if bind_target is not None: return bind_target - if isinstance(func, SemanticMethod) and func.name != emitted_name: + if isinstance(func, SemanticMethod) and PyiPrinter._renames_native_entity(func, func.name, emitted_name): if not context.public_namespace: return func.native_name class_name = context.public_namespace[-1] return f"{class_name}.{func.name}" - if func.native_name and func.native_name != emitted_name: + if func.native_name and PyiPrinter._renames_native_entity(func, func.native_name, emitted_name): return func.native_name return None @@ -2520,6 +2687,26 @@ def _is_private(node) -> bool: """Return whether is private.""" return getattr(node, "visibility", "public") == "private" + @staticmethod + def _renames_native_entity(declaration: object, native_name: object, emitted_name: str) -> bool: + """Return whether an emitted name has to record the spelling it came from. + + A Fortran entity is named without regard to case, so writing one under a + lower-case Python name renames nothing and states nothing worth + recording. Any other difference is a real rename -- a Python keyword, a + character an identifier cannot hold, a name a collision moved aside -- + and the declaration keeps the original beside it. Every other source + language names its entities exactly, so there the spellings are compared + as written. + """ + native = str(native_name) + if native == emitted_name: + return False + origin = getattr(declaration, "origin", None) + if getattr(origin, "source_language", None) != "fortran": + return True + return native.casefold() != emitted_name.casefold() + @staticmethod def _annotation_target(name: str) -> str: """Handle annotation target for the current generation context.""" @@ -2545,15 +2732,25 @@ def _parameter_target(name: str) -> str: _DEFAULT_PRINTER = PyiPrinter() -def emit_module(module: SemanticModule, *, normalize_fortran_public_names: bool = False) -> str: +def emit_module( + module: SemanticModule, + *, + normalize_fortran_public_names: bool = False, + declared_prototype_names: Iterable[str] = (), +) -> str: """Render one semantic module through the shared default printer. Use this convenience entrypoint for ordinary one-module emission. Set normalize_fortran_public_names to use a printer configured for normalized - public names. Both paths create a fresh module emission context. + public names, and declared_prototype_names to name the prototypes the + modules rendered alongside this one declare. Both paths create a fresh + module emission context. """ - if normalize_fortran_public_names: - return PyiPrinter(normalize_fortran_public_names=True).emit(module) + if normalize_fortran_public_names or declared_prototype_names: + return PyiPrinter( + normalize_fortran_public_names=normalize_fortran_public_names, + declared_prototype_names=declared_prototype_names, + ).emit(module) return _DEFAULT_PRINTER.emit(module) diff --git a/prik/runtime/native_support/prik_binding.h b/prik/runtime/native_support/prik_binding.h index eea14001f..00c820106 100644 --- a/prik/runtime/native_support/prik_binding.h +++ b/prik/runtime/native_support/prik_binding.h @@ -1406,6 +1406,22 @@ static inline PyObject *prik_float32_to_numpy(const float *value) return result; } +/* Bind one name in a namespace to a callable another namespace owns, so a + * re-exported procedure resolves to the single wrapper that defines it. */ +static inline int prik_bind_namespace_alias(PyObject *target, const char *name, PyObject *source, + const char *source_name) +{ + PyObject *value = PyObject_GetAttrString(source, source_name); + int status; + + if (value == NULL) { + return -1; + } + status = PyObject_SetAttrString(target, name, value); + Py_DECREF(value); + return status; +} + static inline PyObject *prik_float64_to_numpy(const double *value) { PyObject *result = PyArrayScalar_New(Double); diff --git a/prik/semantics/fortran2ir.py b/prik/semantics/fortran2ir.py index 8d269ccfc..7a56fd969 100644 --- a/prik/semantics/fortran2ir.py +++ b/prik/semantics/fortran2ir.py @@ -17,7 +17,7 @@ from collections.abc import Iterable from copy import deepcopy -from dataclasses import dataclass +from dataclasses import dataclass, replace import re from pathlib import Path @@ -28,6 +28,7 @@ FortranEnum, FortranEnumerator, FortranFile, + FortranInterface, FortranModule, FortranProject, FortranProgram, @@ -37,6 +38,7 @@ FortranVariable, ) from prik.utilities.declaration_expressions import ( + is_strided_extent, ArrayExpressionSource, canonicalize_declaration_extent, declaration_expression_calls, @@ -72,6 +74,7 @@ PYTHON_STATIC_METADATA, PROTOTYPE_INTENT_METADATA, PROTOTYPE_REF_METADATA, + UNRESOLVED_PROCEDURE_INTERFACE_METADATA, SemanticArgument, SemanticArrayContract, SemanticClass, @@ -86,6 +89,7 @@ SemanticModule, SemanticOrigin, SemanticPrototype, + SemanticReexport, SemanticStorageContract, SemanticType, SemanticVariable, @@ -172,6 +176,32 @@ # Internal conversion context +@dataclass(frozen=True) +class _CallbackInterface: + """Pair one resolvable callback interface with the module that declares it. + + The declaring module is what makes an imported interface convertible: its + dummies are written in that module's lexical scope, so a derived type the + interface names belongs to the declaring module even when the consuming + module never imports that type. + """ + + signature: FortranProcedureSignature + module: FortranModule | None = None + local_name: str | None = None + """Spelling the importing scope binds, when a ``use`` renamed the interface.""" + + @property + def native_name(self) -> str: + """Return the name the declaring module gives this interface.""" + return self.signature.name + + @property + def visible_name(self) -> str: + """Return the canonical spelling visible where the interface was resolved.""" + return self.local_name or self.signature.name + + @dataclass(frozen=True) class _DerivedTypeContext: """Keep lexical derived-type lookup facts while one parser node is converted. @@ -260,6 +290,10 @@ def replace_symbol(match: re.Match[str]) -> str: return re.sub(r"\b[A-Za-z_][A-Za-z0-9_]*\b", replace_symbol, raw) +# Language-owned modules are contract vocabulary, not sibling contract leaves. +_INTRINSIC_FORTRAN_MODULES = frozenset({"iso_c_binding", "iso_fortran_env"}) + + class FortranToIRConverter(ClassVisitor): """Convert parsed Fortran models into semantic IR models. @@ -344,23 +378,28 @@ def _visit_FortranFile( parsed_file: FortranFile, *, standalone_module_name: str | None = None, + sibling_modules: Iterable[FortranModule] = (), ) -> list[SemanticModule]: """Convert every module and standalone procedure group in one file. The method first expands the wrapped-derived-type lookup from the file, then preserves parser module order. Standalone procedures are emitted - last as the requested synthetic module when present. + last as the requested synthetic module when present. ``sibling_modules`` + supplies modules parsed from other files so that an abstract interface + imported across files resolves the same way it does for a project. """ converter = self._with_additional_wrapped_types(self._wrapped_types_from_file(parsed_file)) converter = converter._with_additional_known_procedures(self._known_procedures_from_file(parsed_file)) converter = converter._with_additional_abstract_types(self._abstract_types_from_file(parsed_file)) - modules = [converter.visit(module) for module in parsed_file.modules] + index = self._callback_module_index(sibling_modules, parsed_file.modules) + modules = [converter.visit(module, module_index=index) for module in parsed_file.modules] if parsed_file.procedures: modules.append( converter.procedures_to_semantic_module( parsed_file.procedures, name=standalone_module_name or self._standalone_module_name(parsed_file), - callback_interfaces=self._callback_interface_lookup(parsed_file), + callback_interfaces=self._declared_callback_interfaces(parsed_file), + module_index=index, ) ) return modules @@ -375,22 +414,21 @@ def _visit_FortranProject(self, project: FortranProject) -> list[SemanticModule] converter = self._with_additional_wrapped_types(self._wrapped_types_from_project(project)) converter = converter._with_additional_known_procedures(self._known_procedures_from_project(project)) converter = converter._with_additional_abstract_types(self._abstract_types_from_project(project)) + index = self._callback_module_index( + project.modules.values(), + (module for parsed_file in project.files for module in parsed_file.modules), + ) semantic_modules = [] for parsed_file in project.files: file_converter = converter._with_additional_wrapped_types(converter._wrapped_types_from_file(parsed_file)) - semantic_modules.extend( - file_converter.visit( - module, - callback_interfaces=self._project_callback_interface_lookup(project, module), - ) - for module in parsed_file.modules - ) + semantic_modules.extend(file_converter.visit(module, module_index=index) for module in parsed_file.modules) if parsed_file.procedures: semantic_modules.append( file_converter.procedures_to_semantic_module( parsed_file.procedures, name=self._standalone_module_name(parsed_file), - callback_interfaces=self._callback_interface_lookup(parsed_file), + callback_interfaces=self._declared_callback_interfaces(parsed_file), + module_index=index, ) ) return semantic_modules @@ -512,7 +550,7 @@ def _visit_FortranArgument( arg: FortranArgument | FortranVariable, *, derived_type_context: _DerivedTypeContext | None = None, - callback_interfaces: dict[str, FortranProcedureSignature] | None = None, + callback_interfaces: dict[str, _CallbackInterface] | None = None, as_data_member: bool = False, as_type: bool = False, binding_cls: type[SemanticVariable] = SemanticVariable, @@ -572,7 +610,7 @@ def _argument_semantic_type( self, arg: FortranArgument | FortranVariable, *, - callback_interfaces: dict[str, FortranProcedureSignature] | None, + callback_interfaces: dict[str, _CallbackInterface] | None, derived_type_context: _DerivedTypeContext | None, declaration_arrays: dict[str, ArrayExpressionSource] | None, ) -> SemanticType: @@ -670,46 +708,122 @@ def _convert_data_member( return binding @staticmethod - def _callback_interface_lookup( - module: FortranModule | FortranFile, - ) -> dict[str, FortranProcedureSignature]: - """Index explicit and abstract interface procedures usable by dummy procedures.""" - lookup: dict[str, FortranProcedureSignature] = {} - for interface in module.interfaces: + def _declared_callback_interfaces( + container: FortranModule | FortranFile, + ) -> dict[str, _CallbackInterface]: + """Index interfaces declared directly in one module or file.""" + owner = container if isinstance(container, FortranModule) else None + lookup: dict[str, _CallbackInterface] = {} + for interface in container.interfaces: for signature in interface.procedures: - lookup.setdefault(signature.name.casefold(), signature) + lookup.setdefault(signature.name.casefold(), _CallbackInterface(signature, owner)) if interface.name and len(interface.procedures) == 1: - lookup.setdefault(interface.name.casefold(), interface.procedures[0]) + lookup.setdefault(interface.name.casefold(), _CallbackInterface(interface.procedures[0], owner)) return lookup + @staticmethod + def _callback_module_index(*containers: Iterable[FortranModule]) -> dict[str, FortranModule]: + """Index every known module by casefolded name for interface resolution.""" + return {module.name.casefold(): module for group in containers for module in group} + @classmethod - def _project_callback_interface_lookup( + def _module_callback_interfaces( cls, - project: FortranProject, + modules: dict[str, FortranModule], module: FortranModule, - ) -> dict[str, FortranProcedureSignature]: - """Resolve abstract interfaces imported from another parsed module.""" - modules = {name.casefold(): item for name, item in project.modules.items()} - modules.update({item.name.casefold(): item for parsed_file in project.files for item in parsed_file.modules}) - imported: dict[str, FortranProcedureSignature] = {} - for module_name, mappings in module.uses.items(): + *, + seen: frozenset[str] = frozenset(), + exported_only: bool = False, + ) -> dict[str, _CallbackInterface]: + """Index every interface name visible in one module. + + Declarations of the module itself take precedence over imported names, + and an import is followed through re-exporting modules so a chain of + ``use`` hops resolves to the module that actually declares it. A module + outside the index leaves its names unresolved, which later stages report + against the ``use`` that named them. + + ``exported_only`` applies the module's accessibility to the result, for + a caller reaching the names from outside through ``use``. A module + still sees its own private interfaces, so it is left off in that case. + """ + key = module.name.casefold() + if key in seen: + return {} + visible = cls._declared_callback_interfaces(module) + cls._merge_imported_callback_interfaces( + visible, + modules, + module.uses, + seen=seen | {key}, + override=False, + ) + if not exported_only: + return visible + return { + name: resolved + for name, resolved in visible.items() + if cls._symbol_visibility(module, resolved.visible_name) == "public" + } + + @classmethod + def _scope_callback_interfaces( + cls, + modules: dict[str, FortranModule], + uses: dict[str, list[FortranUseMapping]], + *, + base: dict[str, _CallbackInterface], + ) -> dict[str, _CallbackInterface]: + """Extend a visible interface set with one inner scope's own imports. + + A procedure-local or standalone-procedure ``use`` names the interface in + that scope, so it takes precedence over anything the enclosing scope + made visible under the same name. + """ + visible = dict(base) + cls._merge_imported_callback_interfaces(visible, modules, uses, seen=frozenset(), override=True) + return visible + + @classmethod + def _merge_imported_callback_interfaces( + cls, + visible: dict[str, _CallbackInterface], + modules: dict[str, FortranModule], + uses: dict[str, list[FortranUseMapping]], + *, + seen: frozenset[str], + override: bool, + ) -> None: + """Merge every interface one ``use`` list makes visible into ``visible``.""" + for module_name, mappings in uses.items(): source_module = modules.get(module_name.casefold()) if source_module is None: continue - source_lookup = cls._callback_interface_lookup(source_module) - if not mappings: - imported.update(source_lookup) - continue - for mapping in mappings: - signature = source_lookup.get(mapping.source.casefold()) - if signature is not None: - imported[mapping.local_name.casefold()] = signature - return imported + source_lookup = cls._module_callback_interfaces( + modules, + source_module, + seen=seen, + exported_only=True, + ) + imported = ( + source_lookup + if not mappings + else { + mapping.local_name.casefold(): replace(resolved, local_name=mapping.local_name) + for mapping in mappings + if (resolved := source_lookup.get(mapping.source.casefold())) is not None + } + ) + for name, resolved in imported.items(): + if override: + visible[name] = resolved + else: + visible.setdefault(name, resolved) def _callback_semantic_type( self, arg: FortranArgument | FortranVariable, - callback_interfaces: dict[str, FortranProcedureSignature], + callback_interfaces: dict[str, _CallbackInterface], *, derived_type_context: _DerivedTypeContext | None, ) -> SemanticType: @@ -723,32 +837,65 @@ def _callback_semantic_type( if getattr(arg, "pointer", False): return self._convert_variable_type(arg, derived_type_context=derived_type_context) interface_name = str(arg.kind or arg.name) - signature = callback_interfaces.get(interface_name.casefold()) + resolved = callback_interfaces.get(interface_name.casefold()) + signature = resolved.signature if resolved is not None else None if signature is None: - return self._convert_variable_type(arg, derived_type_context=derived_type_context) + semantic_type = self._convert_variable_type(arg, derived_type_context=derived_type_context) + if getattr(arg, "kind", None): + # The declaration named an interface that no supplied module + # declares, which later stages report against that name rather + # than against the opaque procedure type used as a placeholder. + semantic_type.metadata[UNRESOLVED_PROCEDURE_INTERFACE_METADATA] = interface_name + return semantic_type - context = self._procedure_derived_type_context(signature, derived_type_context) + # An interface body is written in the scope of the module that declares + # it, so its dummies resolve there rather than in the module that + # imported the interface -- which need not import the types it names. + declaring_context = ( + self._module_derived_type_context(resolved.module) + if resolved is not None and resolved.module is not None + else derived_type_context + ) + context = self._procedure_derived_type_context(signature, declaring_context) projected_arguments = list(signature.arguments) callback_arguments = [self.visit(item, derived_type_context=context) for item in projected_arguments] for source_argument, callback_argument in zip(projected_arguments, callback_arguments, strict=True): self._normalize_callback_reference_storage(callback_argument, source_argument) self._record_prototype_argument_intent(callback_argument, source_argument) + self._record_imported_prototype_type_origin( + callback_argument.semantic_type, + source_argument, + resolved, + derived_type_context, + ) callback_return = ( self.visit(signature.result, derived_type_context=context, as_type=True) if signature.result else SemanticType("None", dtype="None") ) + # A result carries the declaring module's types exactly as a dummy does. + self._record_imported_prototype_type_origin( + callback_return, + signature.result, + resolved, + derived_type_context, + ) prototype_module = str(signature.module or "") + # The declaring module names the interface; the importing scope may bind + # a different spelling. Both are source facts, and a contract needs each + # of them to import the right name under the right alias. + native_name = resolved.native_name if resolved is not None else interface_name + local_name = resolved.visible_name if resolved is not None else interface_name return SemanticType( - interface_name, + local_name, dtype="Prototype", metadata={ "arguments": [item.semantic_type for item in callback_arguments], "callback_arguments": callback_arguments, "return": callback_return, PROTOTYPE_REF_METADATA: { - "name": interface_name, - "local_name": interface_name, + "name": native_name, + "local_name": local_name, "origin_module": prototype_module, }, "native_callback_kind": signature.kind, @@ -774,16 +921,23 @@ def _callback_semantic_type( ), ) - @staticmethod def _normalize_callback_reference_storage( + self, callback_argument: SemanticArgument, source_argument: FortranArgument | FortranVariable, ) -> None: - """Make every non-value callback dummy a permissive reference contract.""" + """Make every non-value callback dummy a permissive reference contract. + + A dummy the callee may write needs storage the Python callable can + write through. Python has no writable scalar, so such a primitive + scalar records rank-zero storage rather than the value contract used + for a dummy the callee only reads. + """ if getattr(source_argument, "pass_by_value", False): return semantic_type = callback_argument.semantic_type - if semantic_type.name == "String" and semantic_type.rank == 0: + written_back = self._is_written_back_callback_scalar(source_argument, semantic_type) + if written_back or (semantic_type.name == "String" and semantic_type.rank == 0): semantic_type.storage = SemanticStorageContract( kind="array", read_only=False, @@ -806,6 +960,59 @@ def _normalize_callback_reference_storage( semantic_type.storage.mutable = True semantic_type.ownership.mutable = True + def _is_written_back_callback_scalar( + self, + source_argument: FortranArgument | FortranVariable, + semantic_type: SemanticType, + ) -> bool: + """Report whether the callee may write one primitive scalar callback dummy. + + Fortran permits a dummy with no declared ``intent`` to be both read and + modified, so an undeclared direction is conservatively writable. Only + ``assume_intent_in_scalars`` elects the input-only default for it; the + declaration itself keeps no intent either way. + """ + if int(semantic_type.rank or 0) != 0 or semantic_type.name not in SEMANTIC_SCALAR_TYPE_NAMES: + return False + intent = getattr(source_argument, "intent", None) + if intent is None: + return not self.assume_intent_in_scalars + return str(intent).casefold() in {"out", "inout"} + + def _record_imported_prototype_type_origin( + self, + semantic_type: SemanticType, + declaration: FortranArgument | FortranVariable | None, + resolved: _CallbackInterface | None, + consuming_context: _DerivedTypeContext | None, + ) -> None: + """Name the declaring module for a derived type an imported interface owns. + + A type declared beside the interface is local to that module, so nothing + in the module that imported the interface identifies it. Recording the + origin keeps the identity with the module that declares the type rather + than the one that happened to import the interface. + """ + if resolved is None or resolved.module is None or declaration is None: + return + if str(getattr(declaration, "base_type", "")).casefold() != "derived": + return + declaring = resolved.module.name + consuming = str(consuming_context.module or "") if consuming_context is not None else "" + if declaring.casefold() == consuming.casefold(): + return + if EXTERNAL_TYPE_REF_METADATA in semantic_type.metadata: + return + name = str(getattr(declaration, "kind", "") or semantic_type.name) + wrapped = (declaring.casefold(), name.casefold()) in self.wrapped_derived_types + semantic_type.metadata[EXTERNAL_TYPE_REF_METADATA] = { + "name": name, + "local_name": name, + "origin_module": declaring, + "wrapped": wrapped, + "representation": "wrapped" if wrapped else "opaque", + } + @staticmethod def _record_prototype_argument_intent( argument: SemanticArgument, @@ -929,7 +1136,7 @@ def _visit_FortranProcedureSignature( visibility: str = "public", *, derived_type_context: _DerivedTypeContext | None = None, - callback_interfaces: dict[str, FortranProcedureSignature] | None = None, + callback_interfaces: dict[str, _CallbackInterface] | None = None, ) -> SemanticFunction: """Convert a parsed procedure signature into its callable semantic contract. @@ -1109,7 +1316,7 @@ def _visit_FortranModule( self, module: FortranModule, *, - callback_interfaces: dict[str, FortranProcedureSignature] | None = None, + module_index: dict[str, FortranModule] | None = None, ) -> SemanticModule: """Assemble the semantic contents of one parsed Fortran module. @@ -1120,10 +1327,8 @@ def _visit_FortranModule( """ context = self._module_derived_type_context(module) self._record_abstract_type_names(module) - callback_interfaces = { - **(callback_interfaces or {}), - **self._callback_interface_lookup(module), - } + index = module_index if module_index is not None else self._callback_module_index([module]) + callback_interfaces = self._module_callback_interfaces(index, module) source_procedures = [ *module.procedures, *self._module_explicit_interface_procedures(module), @@ -1133,7 +1338,9 @@ def _visit_FortranModule( proc, visibility=self._symbol_visibility(module, proc.name), derived_type_context=context, - callback_interfaces=callback_interfaces, + # A procedure-local ``use`` names an interface only inside that + # procedure, so each one resolves against its own imports. + callback_interfaces=self._scope_callback_interfaces(index, proc.uses, base=callback_interfaces), ) for proc in source_procedures ] @@ -1179,12 +1386,17 @@ def _visit_FortranModule( ), ) - overload_sets = self._module_overload_sets( + overload_sets, inherited_functions = self._module_overload_sets( module, procedure_lookup, context, semantic_classes, + module_index=index, ) + # A specific reached through a use-associated generic is callable here, + # so it joins this module's functions. The import never bound its own + # name, so it stays private and is reachable only through the generic. + semantic_functions.extend(inherited_functions) metadata = {} common_variables = {name.casefold() for name in module.common_variables} enum_constants = [ @@ -1224,6 +1436,7 @@ def _visit_FortranModule( classes=semantic_classes, variables=module_variables + enum_constants, imports=self._module_imports(module), + reexports=self._module_reexports(module, index), metadata=metadata, origin=SemanticOrigin( source_language="fortran", @@ -1282,15 +1495,24 @@ def procedures_to_semantic_module( procedures: list[FortranProcedureSignature], *, name: str, - callback_interfaces: dict[str, FortranProcedureSignature] | None = None, + callback_interfaces: dict[str, _CallbackInterface] | None = None, + module_index: dict[str, FortranModule] | None = None, ) -> SemanticModule: """Package standalone procedures as the synthetic semantic module ``name``. This is used by file and project conversion after parser module handling. - Procedure order and optional callback lookup are passed unchanged to the - existing procedure visitor. + Procedure order is preserved, and each procedure resolves interfaces from + its own ``use`` list on top of the supplied file-level lookup. """ - semantic_functions = [self.visit(proc, callback_interfaces=callback_interfaces) for proc in procedures] + index = module_index or {} + base = callback_interfaces or {} + semantic_functions = [ + self.visit( + proc, + callback_interfaces=self._scope_callback_interfaces(index, proc.uses, base=base), + ) + for proc in procedures + ] function_lookup = {function.name.casefold(): function for function in semantic_functions} for procedure, function in zip(procedures, semantic_functions, strict=True): self._record_function_declaration_callables( @@ -1311,6 +1533,108 @@ def procedures_to_semantic_module( ), ) + @classmethod + def _module_reexports( + cls, + module: FortranModule, + module_index: dict[str, FortranModule] | None = None, + ) -> list[SemanticReexport]: + """Return the imported names this module explicitly publishes. + + Naming an imported entity in a ``public`` statement says the module + means it to be part of its own interface, so that name is published + here as well. A name that is public only because the module default is + public carries no such statement and stays where it was declared, and a + ``use`` that publishes nothing explicitly re-exports nothing at all. + Each record also states what the name declares where it comes from, + because only some kinds reach Python as one object to alias. + """ + declared = { + *(procedure.name.casefold() for procedure in module.procedures), + *(derived.name.casefold() for derived in module.derived_types), + *(variable.name.casefold() for variable in getattr(module, "variables", ())), + } + published = {str(name).casefold() for name in getattr(module, "public_symbols", ())} + index = module_index or {} + reexports: list[SemanticReexport] = [] + named: set[str] = set() + for module_name, mappings in module.uses.items(): + for mapping in mappings: + local_name = mapping.local_name + named.add(local_name.casefold()) + if local_name.casefold() in declared or local_name.casefold() not in published: + continue + reexports.append( + SemanticReexport( + local_name, + module_name, + mapping.source, + module.name, + entity_kind=cls._reexported_entity_kind(index.get(module_name.casefold()), mapping.source), + ) + ) + reexports.extend(cls._wildcard_reexports(module, index, declared=declared, published=published, named=named)) + return reexports + + @classmethod + def _wildcard_reexports( + cls, + module: FortranModule, + index: dict[str, FortranModule], + *, + declared: set[str], + published: set[str], + named: set[str], + ) -> list[SemanticReexport]: + """Return published names a plain ``use`` brought into this module. + + A ``use`` naming no list carries every public name of the module it + reads, so a name this module publishes without declaring it is one of + them. The published name says which, and it is resolved only when one + such module declares it: two that do leave the origin genuinely + ambiguous, which is not something to guess at. + """ + wildcard = [ + index[module_name.casefold()] + for module_name, mappings in module.uses.items() + if not mappings and module_name.casefold() in index + ] + if not wildcard: + return [] + reexports: list[SemanticReexport] = [] + for name in sorted(published): + if name in declared or name in named: + continue + origins = [ + (used, kind) for used in wildcard if (kind := cls._reexported_entity_kind(used, name)) != "unknown" + ] + if len(origins) != 1: + continue + used, kind = origins[0] + reexports.append(SemanticReexport(name, used.name, name, module.name, entity_kind=kind)) + return reexports + + @staticmethod + def _reexported_entity_kind(declaring: FortranModule | None, source_name: str) -> str: + """Return what one published name declares in the module it comes from.""" + if declaring is None: + return "unknown" + key = source_name.casefold() + if any(procedure.name.casefold() == key for procedure in declaring.procedures): + return "procedure" + for interface in declaring.interfaces: + if interface.abstract and any(signature.name.casefold() == key for signature in interface.procedures): + return "prototype" + if interface.name and interface.name.casefold() == key: + return "prototype" if interface.abstract else "generic" + if not interface.abstract and any(signature.name.casefold() == key for signature in interface.procedures): + return "procedure" + if any(derived.name.casefold() == key for derived in declaring.derived_types): + return "derived_type" + if any(variable.name.casefold() == key for variable in getattr(declaring, "variables", ())): + return "variable" + return "unknown" + @staticmethod def _module_imports(module: FortranModule) -> list[str | SemanticImport]: """Translate parser ``use`` mappings while preserving parser declaration order.""" @@ -2060,7 +2384,7 @@ def _array_axes( if category == "assumed_rank": return ["..."] if category == "assumed_shape" and not contiguous: - return ["::Strided" for _dim in shape] + return ["::" for _dim in shape] axes: list[str] = [] for dim in shape: @@ -2117,7 +2441,7 @@ def _array_contiguous(category: str, *, contiguous: bool) -> bool | None: @staticmethod def _is_strided_axis(axis: str) -> bool: """Return whether an encoded public axis carries the strided marker.""" - return "Strided" in axis + return is_strided_extent(axis) @staticmethod def _reference_storage_contract(*, writes_argument: bool) -> SemanticStorageContract: @@ -2323,7 +2647,9 @@ def _module_overload_sets( procedure_lookup: dict[str, SemanticFunction], context: _DerivedTypeContext, semantic_classes: list[SemanticClass], - ) -> list[ProcedureOverloadSet]: + *, + module_index: dict[str, FortranModule] | None = None, + ) -> tuple[list[ProcedureOverloadSet], list[SemanticFunction]]: """Convert module generic interfaces into function or class overload sets. Normal procedure generics remain module overloads. Defined operators @@ -2331,6 +2657,7 @@ def _module_overload_sets( constructors preserve the existing descriptive conversion failure. """ overload_sets: list[ProcedureOverloadSet] = [] + inherited_functions: list[SemanticFunction] = [] class_map = {semantic_class.name.casefold(): semantic_class for semantic_class in semantic_classes} for interface in module.interfaces: if not interface.name or interface.abstract: @@ -2343,10 +2670,15 @@ def _module_overload_sets( ) for signature in interface.procedures } - target_names = interface.specific_procedures or [signature.name for signature in interface.procedures] + target_names, inherited_lookup = self._generic_target_names( + module, + interface, + module_index or {}, + inherited_functions, + ) procedures, missing = self._resolve_overload_targets( target_names, - procedure_lookup | inline_lookup, + procedure_lookup | inline_lookup | inherited_lookup, visibility=self._symbol_visibility(module, interface.name), ) if missing or not procedures: @@ -2360,7 +2692,7 @@ def _module_overload_sets( # constructor, so its specifics become the class's own # `__init__` overload set rather than a module generic. constructor_set = self._normal_overload_set("__init__", procedures) - target_lookup = procedure_lookup | inline_lookup + target_lookup = procedure_lookup | inline_lookup | inherited_lookup for target_name, candidate in zip(target_names, constructor_set.procedures, strict=True): if target_lookup[target_name.casefold()].visibility == "private": # A private specific is unreachable by name; the type @@ -2370,8 +2702,14 @@ def _module_overload_sets( self._merge_overload_sets(constructor_class.overload_sets, [constructor_set]) self._mark_constructor_specifics(procedures, procedure_lookup, interface.name) continue - overload_set = self._normal_overload_set(interface.name, procedures) - target_lookup = procedure_lookup | inline_lookup + overload_set = self._normal_overload_set( + interface.name, + procedures, + native_scope=str(module.origin.native_name or module.name) + if hasattr(module, "origin") + else module.name, + ) + target_lookup = procedure_lookup | inline_lookup | inherited_lookup for target_name, candidate in zip(target_names, overload_set.procedures, strict=True): if target_lookup[target_name.casefold()].visibility == "private": candidate.native_name = interface.name @@ -2386,7 +2724,7 @@ def _module_overload_sets( self._apply_assignment_projection_to_originals(interface.name, procedures, procedure_lookup, class_map) for semantic_class, class_sets in defined_sets: self._merge_overload_sets(semantic_class.overload_sets, class_sets) - return overload_sets + return overload_sets, inherited_functions def _bound_overload_sets( self, @@ -2491,7 +2829,12 @@ def _merge_overload_sets( existing.procedures.extend(overload_set.procedures) @staticmethod - def _normal_overload_set(name: str, procedures: list[SemanticFunction]) -> ProcedureOverloadSet: + def _normal_overload_set( + name: str, + procedures: list[SemanticFunction], + *, + native_scope: str | None = None, + ) -> ProcedureOverloadSet: """Copy regular generic candidates and attach generic dispatch metadata. Type-bound methods are projected back to ordinary functions while @@ -2523,7 +2866,7 @@ def _normal_overload_set(name: str, procedures: list[SemanticFunction]) -> Proce candidate.metadata[OVERLOAD_KIND_METADATA] = "generic" candidate.metadata[OVERLOAD_TARGET_METADATA] = candidate.native_name or candidate.name candidates.append(candidate) - return ProcedureOverloadSet(name, candidates) + return ProcedureOverloadSet(name, candidates, native_scope=native_scope) def _defined_overload_sets( self, @@ -2777,6 +3120,87 @@ def _is_procedure_generic_name(name: str) -> bool: """Return whether a generic spelling is an ordinary callable identifier.""" return re.fullmatch(r"[a-z_]\w*", name, re.IGNORECASE) is not None + def _generic_target_names( + self, + module: FortranModule, + interface: FortranInterface, + modules: dict[str, FortranModule], + inherited_functions: list[SemanticFunction], + ) -> tuple[list[str], dict[str, SemanticFunction]]: + """Order one generic's specifics, inherited before locally declared. + + ``inherited_functions`` collects each specific this module gained from + the generic it extends, so the module can carry them for dispatch. + """ + inherited_names, inherited_lookup = self._inherited_generic_specifics(module, interface.name, modules) + known = {item.name.casefold() for item in inherited_functions} + inherited_functions.extend( + inherited_lookup[name.casefold()] for name in inherited_names if name.casefold() not in known + ) + own_names = interface.specific_procedures or [signature.name for signature in interface.procedures] + return [*inherited_names, *own_names], inherited_lookup + + def _inherited_generic_specifics( + self, + module: FortranModule, + generic_name: str, + modules: dict[str, FortranModule], + ) -> tuple[list[str], dict[str, SemanticFunction]]: + """Return the specifics one generic inherits from the generic it extends. + + A local interface block repeating a ``use``-associated generic name + extends that generic rather than replacing it, so this scope resolves + every specific that reached it through the import as well as its own. + Accumulation runs one way: the declaring module never sees what a later + module adds. + """ + source_module, source_generic = self._imported_generic_interface(module, generic_name, modules) + if source_module is None or source_generic is None: + return [], {} + inherited, lookup = self._inherited_generic_specifics(source_module, source_generic.name, modules) + signatures = {procedure.name.casefold(): procedure for procedure in source_module.procedures} + source_context = self._module_derived_type_context(source_module) + names = source_generic.specific_procedures or [item.name for item in source_generic.procedures] + for name in names: + signature = signatures.get(name.casefold()) + if signature is None or name.casefold() in lookup: + continue + function = self.visit(signature, visibility="private", derived_type_context=source_context) + lookup[name.casefold()] = function + inherited.append(name) + return inherited, lookup + + @staticmethod + def _imported_generic_interface( + module: FortranModule, + generic_name: str, + modules: dict[str, FortranModule], + ) -> tuple[FortranModule | None, FortranInterface | None]: + """Find the generic one module imports under ``generic_name``, if any.""" + for module_name, mappings in module.uses.items(): + source_module = modules.get(module_name.casefold()) + if source_module is None: + continue + sources = ( + [generic_name] + if not mappings + else [ + mapping.source for mapping in mappings if mapping.local_name.casefold() == generic_name.casefold() + ] + ) + for source_name in sources: + generic = next( + ( + item + for item in source_module.interfaces + if item.name and not item.abstract and item.name.casefold() == source_name.casefold() + ), + None, + ) + if generic is not None: + return source_module, generic + return None, None + @staticmethod def _resolve_overload_targets( target_names: list[str], @@ -3204,6 +3628,7 @@ def _visit_FortranFile(self, node: FortranFile, **_context): node.block_data_units, node.procedures, node.derived_types, + node.interfaces, ) for collection in collections: for child in collection: @@ -3238,6 +3663,25 @@ def _visit_FortranBlockData(node: FortranBlockData, **_context): for variable in node.variables ) + def _visit_FortranInterface( + self, + node: FortranInterface, + *, + module_name: str | None = None, + **_context, + ): + """Return the variable contexts an interface body declares. + + An interface body types its own dummies, and the kind it names may come + from a ``use`` written inside that body. Those variables reach a target + probe only from here, since no module variable or module procedure + declares them. + """ + contexts = [] + for procedure in node.procedures: + contexts.extend(self._visit(procedure, module_name=module_name or node.module)) + return tuple(contexts) + @staticmethod def _visit_FortranProcedureSignature( node: FortranProcedureSignature, @@ -3299,6 +3743,8 @@ def _module_variable_contexts( contexts.extend(self._visit(procedure, module_name=owner)) for derived_type in node.derived_types: contexts.extend(self._visit(derived_type, module_name=owner)) + for interface in node.interfaces: + contexts.extend(self._visit(interface, module_name=owner)) return tuple(contexts) @@ -3674,12 +4120,15 @@ def fortran_file_to_semantic_modules( wrapped_derived_types: Iterable[tuple[str, str]] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, assume_intent_in_scalars: bool = False, + sibling_modules: Iterable[FortranModule] = (), ) -> list[SemanticModule]: """Convert every module and standalone procedure group in one parsed file. Use this rather than the single-module helper when file-level procedures matter. Parser module ordering is retained, and ``standalone_module_name`` - controls the synthetic module used for top-level procedures. + controls the synthetic module used for top-level procedures. Pass + ``sibling_modules`` when other files were parsed alongside this one so an + abstract interface imported across files resolves. Example: >>> parsed = FortranFile(procedures=[FortranProcedureSignature(name="tick", kind="subroutine")]) @@ -3694,6 +4143,7 @@ def fortran_file_to_semantic_modules( ).visit( parsed_file, standalone_module_name=standalone_module_name, + sibling_modules=sibling_modules, ) diff --git a/prik/semantics/models.py b/prik/semantics/models.py index 05b81507f..aa0d6c237 100644 --- a/prik/semantics/models.py +++ b/prik/semantics/models.py @@ -18,6 +18,7 @@ EXTERNAL_TYPE_REF_METADATA = "external_type_ref" PROTOTYPE_REF_METADATA = "prototype_ref" PROTOTYPE_INTENT_METADATA = "prototype_intent" +UNRESOLVED_PROCEDURE_INTERFACE_METADATA = "unresolved_procedure_interface" INTERNAL_MODULE_VARIABLE_ACCESS_METADATA = "internal_module_variable_access" INTERNAL_MODULE_VARIABLE_NAME_METADATA = "internal_module_variable_name" INTERNAL_NATIVE_ARRAY_HANDLE_OPERATION_METADATA = "internal_native_array_handle_operation" @@ -390,6 +391,8 @@ class SemanticMethod(SemanticFunction): class ProcedureOverloadSet: name: str procedures: list[SemanticFunction] = field(default_factory=list) + native_scope: str | None = None + """Module declaring the generic, which need not own every specific.""" FORTRAN_GENERIC_NAME_METADATA = "fortran_generic_name" @@ -669,12 +672,40 @@ class SemanticImport: items: list[SemanticImportItem] = field(default_factory=list) +@dataclass +class SemanticReexport: + """Record one name a module publishes on behalf of the module it imports. + + A re-export names an existing declaration rather than adding one, so it + carries only where the declaration lives and what this module calls it. + """ + + local_name: str + origin_module: str + source_name: str + module: str = "" + """Module publishing the name, which is not the one declaring it.""" + + entity_kind: str = "unknown" + """What the published name declares where it comes from. + + Re-export reaches Python as a namespace alias only for an entity that is one + Python object, which today means an ordinary procedure. Every other kind -- + a callback prototype, a module variable whose state stays live, a derived + type, a generic -- keeps to the semantic and contract-import paths that + already carry it, and records its kind here rather than an alias that would + misrepresent it. + """ + + @dataclass class SemanticModule: name: str functions: list[SemanticFunction] = field(default_factory=list) + reexports: list[SemanticReexport] = field(default_factory=list) + prototypes: list[SemanticPrototype] = field(default_factory=list) overload_sets: list[ProcedureOverloadSet] = field(default_factory=list) diff --git a/prik/semantics/pyi2ir.py b/prik/semantics/pyi2ir.py index adde496e8..e98a889c1 100644 --- a/prik/semantics/pyi2ir.py +++ b/prik/semantics/pyi2ir.py @@ -254,7 +254,6 @@ def _resolve_local_prototype_references(self) -> None: semantic_type, prototype, origin_module=self.module.name, - source_name=prototype.name, ) def _resolve_declaration_expression_callables(self) -> None: @@ -292,11 +291,14 @@ def _declaration_callable_imports( namespaces: dict[str, str] = {} for imported in self.module.imports: if isinstance(imported, SemanticImport): + # A sibling leaf is imported relatively, but a native scope is + # the module's own name, so the relative marker is dropped. + module_name = imported.module.lstrip(".") if imported.items: for item in imported.items: - explicit[(item.target or item.source).casefold()] = (imported.module, item.source) + explicit[(item.target or item.source).casefold()] = (module_name, item.source) else: - namespaces[imported.module.split(".", 1)[0].casefold()] = imported.module + namespaces[module_name.split(".", 1)[0].casefold()] = module_name continue for item in str(imported).split(","): module_name, _, alias = item.strip().partition(" as ") @@ -449,6 +451,7 @@ def class_def( visibility: str, native_abi: str | None = None, abstract: bool = False, + native_name: str | None = None, ) -> SemanticClass: """Convert one class AST node, its body, and supported native metadata. @@ -479,7 +482,9 @@ def class_def( metadata["fortran_bind_c"] = True semantic_class = SemanticClass( name=node.name, - native_name=node.name, + # A class names its Python type; `bind` states the native type it + # reaches when the two are spelled differently. + native_name=native_name or node.name, fields=body.fields, methods=body.methods, destructors=body.destructors, @@ -537,6 +542,7 @@ def function_def( has_native_call: bool = False, release_gil: bool = False, error_status_policy: dict[str, object] | None = None, + restates_projected_result: bool = False, ) -> SemanticFunction: """Convert a module-level stub into a semantic function declaration. @@ -550,6 +556,7 @@ def function_def( node, projection=actual_projection, native_result=native_result, + restates_projected_result=restates_projected_result, ) metadata = {BIND_TARGET_METADATA: native_name} if native_name is not None else {} if has_native_call: @@ -648,6 +655,7 @@ def method_def( release_gil: bool = False, error_status_policy: dict[str, object] | None = None, deferred: bool = False, + restates_projected_result: bool = False, ) -> SemanticMethod: """Convert a class stub into a semantic method declaration. @@ -662,6 +670,7 @@ def method_def( projection=actual_projection, native_result=native_result, drop_untyped_self=True, + restates_projected_result=restates_projected_result, ) metadata = {BIND_TARGET_METADATA: native_name} if native_name is not None else {} if deferred: @@ -816,8 +825,6 @@ def ann_assign( """ name = self.annotation_target(node.target) visibility, semantic_type, original_name = self.visible_type(node.annotation) - if original_name is not None: - name = original_name self._validate_python_value_policy( semantic_type, writable=self._type_uses_writable_storage(semantic_type), @@ -829,6 +836,11 @@ def ann_assign( visibility=visibility, default_value=self.assignment_default_value(node.value, semantic_type), ) + if original_name is not None: + # A declared name is what Python calls this entity; `SourceName` + # states the entity it reaches, exactly as `bind` does for a + # callable, and leaves the declared name alone. + binding.origin.native_name = original_name if visibility == "private": binding.origin.metadata[USER_PRIVATE_METADATA] = True binding.optional = self.default_marks_optional(node.value) @@ -1276,15 +1288,47 @@ def _validate_overload_signature( @staticmethod def _matches_projected_return(declared, target_return) -> bool: - """Compare a declared result with a target's, ignoring result ownership.""" + """Compare a declared result with a target's, ignoring result ownership. + + A projected output is written through as a native argument and returned + as an ordinary result. Whether the call writes it is a property of that + argument passing, which a declared result type does not state, so the + comparison reads it from the declaration rather than the target. + """ declared_type = _PyiAstParser._visible_overload_type(declared) target_type = _PyiAstParser._visible_overload_type(target_return) if declared_type is None or target_type is None: return declared_type == target_type - expected = deepcopy(target_type) + declared_type = deepcopy(declared_type) + # An unwrapped `| None` leaves a parse marker behind when no projection + # consumes it, which names nothing about the type itself. + declared_type.metadata.pop(_PYI_OPTIONAL_RETURN_METADATA, None) + expected = _PyiAstParser._visible_projected_result(target_type) expected.ownership = deepcopy(declared_type.ownership) + if expected.storage is not None and declared_type.storage is not None: + expected.storage.read_only = declared_type.storage.read_only + expected.storage.mutable = declared_type.storage.mutable return declared_type == expected + @staticmethod + def _visible_projected_result(target_type: SemanticType) -> SemanticType: + """Return the public result form a declaration can spell for a projection. + + A native scalar descriptor result is written as a nullable value plus a + `native_call` result wrapper naming the descriptor, and an overload + declaration carries no `native_call`. Its descriptor topology therefore + has no place in the declared annotation, exactly as the contract printer + emits it. + """ + expected = deepcopy(target_type) + if _PyiAstParser._semantic_scalar_descriptor_kind(expected) is None: + return expected + for key in ("fortran_allocatable", "fortran_pointer", "fortran_pointer_association"): + expected.metadata.pop(key, None) + if expected.storage is not None and expected.storage.kind in {"reference", "pointer", "address"}: + expected.storage = None + return expected + @staticmethod def _projected_overload_arguments( function: SemanticFunction, @@ -1943,6 +1987,18 @@ def semantic_type_annotation( ) semantic_type.metadata[OPTIONAL_ABSENT_HANDLE_METADATA] = True return semantic_type, None + if self.is_subscript_of(node, "Final"): + # `Final` marks the value immutable and wraps the annotation that + # carries any source name, which the declaration still needs. + items = self.subscript_items(node) + if len(items) == 1: + semantic_type, original_name = self.semantic_type_annotation( + items[0], + allow_optional_absent_handle=allow_optional_absent_handle, + ) + if not any(constraint.name == "Constant" for constraint in semantic_type.constraints): + semantic_type.constraints.append(SemanticConstraint("Constant")) + return semantic_type, original_name if not self.is_subscript_of(node, "Annotated"): return self.semantic_type(node), None @@ -2235,7 +2291,7 @@ def _flat_array_dimensions( ) lower_bounds, upper_bounds = _PyiAstParser._bounds_from_source_shape(source_shape) return ( - [dim.replace(_STRIDED_DIMENSION_SENTINEL, "Strided") for dim in dims], + [dim.replace(_STRIDED_DIMENSION_SENTINEL, "") for dim in dims], None, source_shape, lower_bounds, @@ -2658,14 +2714,20 @@ def dimension_text(self, node: ast.expr) -> str: return expression def slice_text(self, node: ast.Slice) -> str: - """Render one dimension slice, preserving the contract's strided marker.""" + """Render one dimension slice as written. + + A dimension carries bounds only. The step position spells nothing the + contract grammar defines, so a value there is rejected rather than read + as an extent expression. + """ + if node.step is not None: + step = ast.unparse(node.step) + raise ValueError( + f"Array dimension step {step!r} is not part of the contract grammar; " + "write 'T[::]' for a strided axis or 'T[:]' for a contiguous one" + ) lower = "" if node.lower is None else ast.unparse(node.lower) upper = "" if node.upper is None else ast.unparse(node.upper) - step = "" - if node.step is not None: - step = _STRIDED_DIMENSION_SENTINEL if self.matches_name(node.step, "Strided") else ast.unparse(node.step) - if step: - return f"{lower}:{upper}:{step}" return f"{lower}:{upper}" # Callback and result conversion @@ -3013,6 +3075,7 @@ def _callable_parts( projection: list[ProjectionMapping], native_result: ProjectionMapping | None = None, drop_untyped_self: bool = False, + restates_projected_result: bool = False, ) -> tuple[list[SemanticArgument], SemanticType | None]: """Build a callable's arguments, results, and native projection metadata. @@ -3030,6 +3093,13 @@ def _callable_parts( # Construct direct and projected outputs from the Python return shape. optional_return_positions = self._optional_native_return_positions(projection, native_result) + if restates_projected_result: + # An overload declaration restates the result its specific projects, + # and the projection that makes a slot nullable lives on that + # specific -- a declaration carrying one is rejected outright. Read + # every slot of such a declaration as nullable so it can spell the + # result the specific already produces. + optional_return_positions = set(range(len(self.return_items(node.returns)))) return_type, returned_args = self.return_projection( node.returns, optional_return_positions=optional_return_positions, @@ -3591,6 +3661,7 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: release_gil=decorators.release_gil, error_status_policy=decorators.error_status_policy, deferred=decorators.abstract_method, + restates_projected_result=decorators.overload_target is not None, ) self.parser._reject_private_constructor(node.name, decorators.visibility) if node.name == "__init__" and decorators.bind_target is not None and decorators.overload_target is None: @@ -3634,7 +3705,6 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: decorators = self.parser.decorators(node.decorator_list, context="class body") if ( decorators.has_native_call - or decorators.bind_target is not None or decorators.overload_target is not None or decorators.is_static or decorators.release_gil @@ -3658,6 +3728,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: visibility=decorators.visibility, native_abi=decorators.native_abi, abstract=decorators.abstract, + native_name=decorators.bind_target, ) ) @@ -3702,7 +3773,6 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: decorators = self.parser.decorators(node.decorator_list, context="class") if ( decorators.has_native_call - or decorators.bind_target is not None or decorators.overload_target is not None or decorators.is_static or decorators.release_gil @@ -3724,6 +3794,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: visibility=decorators.visibility, native_abi=decorators.native_abi, abstract=decorators.abstract, + native_name=decorators.bind_target, ) ) @@ -3752,6 +3823,7 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: has_native_call=decorators.has_native_call, release_gil=decorators.release_gil, error_status_policy=decorators.error_status_policy, + restates_projected_result=decorators.overload_target is not None, ) if decorators.overload_target is not None: self.parser._pending_overloads.append( @@ -3813,7 +3885,7 @@ def _imported_type_refs(module: SemanticModule) -> dict[str, tuple[str, str, str if isinstance(imp, SemanticImport): for item in imp.items: local_name = item.target or item.source - imported[local_name] = (imp.module, item.source, local_name) + imported[local_name] = (imp.module.lstrip("."), item.source, local_name) if imp.module.startswith("."): imported_namespaces[local_name] = _relative_imported_namespace(imp.module, item.source) continue @@ -3842,17 +3914,47 @@ def _relative_imported_namespace(module_name: str, source_name: str) -> str: return f"{module_path}.{source_name}" +def _record_declaring_module_for_prototype_type( + semantic_type: SemanticType, + declaring_module: str, + declared_types: frozenset[str], +) -> None: + """Name the declaring module for a derived type a referenced prototype owns.""" + if not declaring_module or semantic_type.name not in declared_types: + return + if EXTERNAL_TYPE_REF_METADATA in semantic_type.metadata: + return + semantic_type.metadata[EXTERNAL_TYPE_REF_METADATA] = { + "name": semantic_type.name, + "local_name": semantic_type.name, + "origin_module": declaring_module, + } + + def _bind_prototype_reference( semantic_type: SemanticType, prototype: SemanticPrototype, *, origin_module: str, - source_name: str, + declared_types: frozenset[str] = frozenset(), ) -> None: - """Complete one type annotation as a named callback prototype reference.""" + """Complete one type annotation as a named callback prototype reference. + + The declaring prototype names the symbol. A reference reached through + renaming re-exports carries the last alias it passed through, which names + nothing in the module that declares it, so the name is taken from the + declaration rather than from the caller. + """ local_name = semantic_type.name + source_name = prototype.name arguments = deepcopy(prototype.arguments) return_type = deepcopy(prototype.return_type) or SemanticType("None", dtype="None") + # The prototype's own types are written in the declaring module's scope, so + # a type local to that module keeps its origin when the reference is copied + # into a module that only imported the interface. + declaring_module = str(prototype.origin.native_scope or origin_module) + for value in (*(argument.semantic_type for argument in arguments), return_type): + _record_declaring_module_for_prototype_type(value, declaring_module, declared_types) semantic_type.dtype = "Prototype" semantic_type.metadata = { "arguments": [argument.semantic_type for argument in arguments], @@ -3883,6 +3985,79 @@ def _bind_prototype_reference( ) +def _external_module_candidates(module_name: str) -> tuple[str, ...]: + """Return the spellings one import may use to name the same contract module.""" + stripped = module_name.lstrip(".") + return tuple( + dict.fromkeys(candidate for candidate in (module_name, stripped, stripped.rsplit(".", 1)[-1]) if candidate) + ) + + +def _prototypes_with_reexports(modules: list[SemanticModule]) -> dict[tuple[str, str], SemanticPrototype]: + """Index every prototype name a contract module binds, declared or re-exported. + + A module that imports a prototype and publishes it binds that name without + declaring it, so a consumer importing it from there must still resolve to + the declaring module. Repeating to a fixed point follows a chain of any + length. + """ + resolved = {(module.name, prototype.name): prototype for module in modules for prototype in module.prototypes} + changed = True + while changed: + changed = False + for module in modules: + for imported in module.imports: + if not isinstance(imported, SemanticImport): + continue + for item in imported.items: + local_name = item.target or item.source + if (module.name, local_name) in resolved: + continue + prototype = next( + ( + found + for candidate in _external_module_candidates(imported.module) + if (found := resolved.get((candidate, item.source))) is not None + ), + None, + ) + if prototype is not None: + resolved[(module.name, local_name)] = prototype + changed = True + return resolved + + +def _bind_referenced_prototype( + semantic_type: SemanticType, + ref: dict[str, object], + prototypes: dict[tuple[str, str], SemanticPrototype], + declared_class_names: dict[str, frozenset[str]], +) -> bool: + """Complete one external reference as a prototype, reporting whether it matched.""" + origin_module = ref.get("origin_module") + source_name = ref.get("name") + if not isinstance(origin_module, str) or not isinstance(source_name, str): + return False + prototype = next( + ( + found + for candidate in _external_module_candidates(origin_module) + if (found := prototypes.get((candidate, source_name))) is not None + ), + None, + ) + if prototype is None: + return False + declaring_module = str(prototype.origin.native_scope or "") + _bind_prototype_reference( + semantic_type, + prototype, + origin_module=declaring_module or origin_module.lstrip("."), + declared_types=declared_class_names.get(declaring_module, frozenset()), + ) + return True + + def reconcile_external_type_refs(modules: list[SemanticModule]) -> list[SemanticModule]: """Resolve imported class and prototype references across converted modules. @@ -3893,37 +4068,18 @@ def reconcile_external_type_refs(modules: list[SemanticModule]) -> list[Semantic pipeline chaining; absent external definitions remain opaque references. """ definitions = {(module.name, declaration.name): declaration for module in modules for declaration in module.classes} - prototypes = {(module.name, prototype.name): prototype for module in modules for prototype in module.prototypes} + declared_class_names = { + module.name: frozenset(declaration.name for declaration in module.classes) for module in modules + } + prototypes = _prototypes_with_reexports(modules) functions = {(module.name, function.name): function for module in modules for function in module.functions} for module in modules: for semantic_type in _module_semantic_types(module): ref = semantic_type.metadata.get(EXTERNAL_TYPE_REF_METADATA) if not isinstance(ref, dict): continue - origin_module = ref.get("origin_module") - source_name = ref.get("name") - if isinstance(origin_module, str) and isinstance(source_name, str): - module_candidates = ( - origin_module, - origin_module.lstrip("."), - origin_module.lstrip(".").rsplit(".", 1)[-1], - ) - prototype = next( - ( - candidate_prototype - for candidate in module_candidates - if candidate and (candidate_prototype := prototypes.get((candidate, source_name))) is not None - ), - None, - ) - if prototype is not None: - _bind_prototype_reference( - semantic_type, - prototype, - origin_module=str(prototype.origin.native_scope or origin_module.lstrip(".")), - source_name=source_name, - ) - continue + if _bind_referenced_prototype(semantic_type, ref, prototypes, declared_class_names): + continue declaration = definitions.get((ref.get("origin_module"), ref.get("name"))) wrapped = declaration is not None and ( not isinstance(declaration, SemanticClass) or "Opaque" not in declaration.base_classes diff --git a/prik/utilities/declaration_expressions.py b/prik/utilities/declaration_expressions.py index f853262c3..87d069cd5 100644 --- a/prik/utilities/declaration_expressions.py +++ b/prik/utilities/declaration_expressions.py @@ -21,6 +21,8 @@ from dataclasses import dataclass __all__ = ( + "RUNTIME_DIMENSION_MARKERS", + "RUNTIME_EXTENT_MARKERS", "ArrayExpressionSource", "DeclarationExpressionCall", "ResolvedDeclarationExtent", @@ -33,6 +35,7 @@ "fortran_extent_to_python", "is_declaration_expression_helper", "is_public_declaration_expression", + "is_strided_extent", "render_declaration_extent", "resolve_declaration_extent", "split_declaration_assignment", @@ -41,7 +44,25 @@ ) -_RUNTIME_DIMENSIONS = frozenset({":", "::Strided", "...", "Flat"}) +# A runtime extent has a concrete rank but no compile-time bound, so a backend +# spells it from the descriptor it is handed rather than from the expression. +RUNTIME_EXTENT_MARKERS = frozenset({":", "::", "Flat"}) + + +def is_strided_extent(expression: str) -> bool: + """Return whether one extent expression describes a strided axis. + + A trailing empty step marks it, with or without bounds: ``::`` spans the + whole axis and ``lower:upper:`` narrows it. Without that step the axis is + contiguous, so ``:`` and ``lower:upper`` are dense. + """ + parts = str(expression).split(":") + return len(parts) == 3 and parts[2] == "" + + +_ASSUMED_RANK_MARKER = "..." +# Every extent whose value only exists at run time, assumed rank included. +RUNTIME_DIMENSION_MARKERS = RUNTIME_EXTENT_MARKERS | {_ASSUMED_RANK_MARKER} _FORTRAN_RELATIONAL_OPERATORS = { ".eq.": "==", ".ne.": "!=", @@ -360,7 +381,7 @@ def resolve_declaration_extent( stored on completed policy and consumed by backend rendering. """ # Stage 1: preserve caller-owned runtime dimension markers. - if expression in _RUNTIME_DIMENSIONS: + if expression in RUNTIME_DIMENSION_MARKERS: return ResolvedDeclarationExtent(expression) # Stage 2: parse the public expression before binding any producer roles. @@ -395,7 +416,7 @@ def declaration_extent_references(expression: str) -> tuple[str, ...]: known. Array properties and unsupported syntax return ```` so the later policy stage cannot accidentally treat them as scalar values. """ - if expression in _RUNTIME_DIMENSIONS: + if expression in RUNTIME_DIMENSION_MARKERS: return () tree = _parse_expression(expression) if tree is None: @@ -1591,7 +1612,7 @@ def render_declaration_extent( """ if target not in {"c", "fortran"}: raise ValueError(f"unsupported declaration-expression target: {target!r}") - if expression in _RUNTIME_DIMENSIONS: + if expression in RUNTIME_DIMENSION_MARKERS: return expression try: node = ast.parse(expression, mode="eval").body diff --git a/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py b/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py index 7db7f2f2a..0c00b72d8 100644 --- a/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py +++ b/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py @@ -319,7 +319,7 @@ def test_cli_c_pyi_out_writes_explicit_multi_header_owner_stubs(tmp_path: Path): assert result.stdout == "" 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 "from .types import state" in api_stub assert "class state" not in api_stub assert "state: state" in api_stub assert "Addr(state)" not in api_stub diff --git a/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py b/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py index 6c95afa23..044f76178 100644 --- a/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py +++ b/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py @@ -62,7 +62,7 @@ def test_c2ir_explicit_project_headers_import_types_from_their_owner_module(): "representation": "wrapped", } assert "external_type_ref" not in local_state.metadata - assert "from types import state" in stubs["api"] + assert "from .types import state" in stubs["api"] assert "class state" not in stubs["api"] diff --git a/tests/c/records/semantics/test_c_record_semantics.py b/tests/c/records/semantics/test_c_record_semantics.py index c10e26a03..81e8ee38a 100644 --- a/tests/c/records/semantics/test_c_record_semantics.py +++ b/tests/c/records/semantics/test_c_record_semantics.py @@ -113,7 +113,7 @@ def test_c2ir_private_include_types_remain_available_as_opaque_handles(): "wrapped": False, "representation": "opaque", } - assert "from private import private_context" in stubs["api"] + assert "from .private import private_context" in stubs["api"] assert ( stubs["private"] == "from prik.contracts import CStruct, Opaque\n\nclass private_context(CStruct, Opaque):\n pass" diff --git a/tests/fortran/_support/wrapper_build.py b/tests/fortran/_support/wrapper_build.py index 3fe6c0834..3cc11d43d 100644 --- a/tests/fortran/_support/wrapper_build.py +++ b/tests/fortran/_support/wrapper_build.py @@ -210,7 +210,8 @@ def _compile_native_object(source: Path, native_dir: Path) -> Path: return native_object -def _generate_checked_pyi_contract(source: Path, package_dir: Path, expected_package: Path) -> Path: +def _generate_checked_pyi_contract(source: Path, package_dir: Path, expected_package: Path | None) -> Path: + """Generate one contract package, comparing it to a fixture when given.""" _run_captured_command( [ sys.executable, @@ -225,7 +226,8 @@ def _generate_checked_pyi_contract(source: Path, package_dir: Path, expected_pac _compiler(), ], ) - assert_generated_pyi_package_matches_fixture(package_dir, expected_package) + if expected_package is not None: + assert_generated_pyi_package_matches_fixture(package_dir, expected_package) return package_dir / "__init__.pyi" @@ -262,7 +264,12 @@ def _build_inline_pyi_contract_module( return module, result -def _build_generated_pyi_and_import(source_template: Path, workdir: Path, expected_contract_package: Path): +def _build_generated_pyi_and_import( + source_template: Path, + workdir: Path, + expected_contract_package: Path | None = None, +): + """Generate a contract from source, then build and import through that contract.""" source_dir = workdir / "source" source_dir.mkdir(parents=True) source = source_dir / source_template.name diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays/__init__.pyi index 1746077b5..876fd52cc 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays/__init__.pyi @@ -1,6 +1,5 @@ -from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call, standalone +from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, native_call, standalone -@bind("SQUARE_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r4( @@ -9,7 +8,6 @@ def square_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SQUARE_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r8( @@ -18,7 +16,6 @@ def square_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("SQUARE_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_i4( @@ -27,7 +24,6 @@ def square_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c4( @@ -36,7 +32,6 @@ def square_c4( R: Complex64[N] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c8( @@ -45,7 +40,6 @@ def square_c8( R: Complex128[N] ) -> Returns["N", Int32]: ... -@bind("CUBE_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r4( @@ -54,7 +48,6 @@ def cube_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("CUBE_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r8( @@ -63,7 +56,6 @@ def cube_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("CUBE_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_i4( @@ -72,7 +64,6 @@ def cube_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("ADD_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r4( @@ -82,7 +73,6 @@ def add_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ADD_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r8( @@ -92,7 +82,6 @@ def add_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ADD_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_i4( @@ -102,7 +91,6 @@ def add_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("ADD_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c4( @@ -112,7 +100,6 @@ def add_c4( R: Complex64[N] ) -> Returns["N", Int32]: ... -@bind("ADD_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c8( @@ -122,7 +109,6 @@ def add_c8( R: Complex128[N] ) -> Returns["N", Int32]: ... -@bind("SUB_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r4( @@ -132,7 +118,6 @@ def sub_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SUB_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r8( @@ -142,7 +127,6 @@ def sub_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("SUB_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_i4( @@ -152,7 +136,6 @@ def sub_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("MUL_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r4( @@ -162,7 +145,6 @@ def mul_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("MUL_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r8( @@ -172,7 +154,6 @@ def mul_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MUL_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_i4( @@ -182,7 +163,6 @@ def mul_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("DIV_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r4( @@ -192,7 +172,6 @@ def div_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DIV_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r8( @@ -202,7 +181,6 @@ def div_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("POW_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r4( @@ -212,7 +190,6 @@ def pow_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("POW_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r8( @@ -222,7 +199,6 @@ def pow_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ABS_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r4( @@ -231,7 +207,6 @@ def abs_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ABS_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r8( @@ -240,7 +215,6 @@ def abs_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ABS_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_i4( @@ -249,7 +223,6 @@ def abs_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("NEG_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r4( @@ -258,7 +231,6 @@ def neg_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("NEG_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r8( @@ -267,7 +239,6 @@ def neg_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("NEG_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_i4( @@ -276,7 +247,6 @@ def neg_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("SIN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r4( @@ -285,7 +255,6 @@ def sin_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SIN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r8( @@ -294,7 +263,6 @@ def sin_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("COS_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r4( @@ -303,7 +271,6 @@ def cos_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("COS_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r8( @@ -312,7 +279,6 @@ def cos_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("TAN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r4( @@ -321,7 +287,6 @@ def tan_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("TAN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r8( @@ -330,7 +295,6 @@ def tan_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ASIN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r4( @@ -339,7 +303,6 @@ def asin_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ASIN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r8( @@ -348,7 +311,6 @@ def asin_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ACOS_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r4( @@ -357,7 +319,6 @@ def acos_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ACOS_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r8( @@ -366,7 +327,6 @@ def acos_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ATAN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r4( @@ -375,7 +335,6 @@ def atan_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ATAN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r8( @@ -384,7 +343,6 @@ def atan_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r4( @@ -394,7 +352,6 @@ def atan2_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r8( @@ -404,7 +361,6 @@ def atan2_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("EXP_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r4( @@ -413,7 +369,6 @@ def exp_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("EXP_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r8( @@ -422,7 +377,6 @@ def exp_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("LOG_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r4( @@ -431,7 +385,6 @@ def log_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("LOG_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r8( @@ -440,7 +393,6 @@ def log_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("LOG10_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r4( @@ -449,7 +401,6 @@ def log10_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("LOG10_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r8( @@ -458,7 +409,6 @@ def log10_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("SQRT_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r4( @@ -467,7 +417,6 @@ def sqrt_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SQRT_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r8( @@ -476,7 +425,6 @@ def sqrt_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r4( @@ -486,7 +434,6 @@ def hypot_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r8( @@ -496,7 +443,6 @@ def hypot_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MIN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r4( @@ -506,7 +452,6 @@ def min_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("MIN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r8( @@ -516,7 +461,6 @@ def min_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MIN_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_i4( @@ -526,7 +470,6 @@ def min_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("MAX_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r4( @@ -536,7 +479,6 @@ def max_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("MAX_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r8( @@ -546,7 +488,6 @@ def max_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MAX_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_i4( @@ -556,7 +497,6 @@ def max_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("SIGN_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r4( @@ -566,7 +506,6 @@ def sign_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("SIGN_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r8( @@ -576,7 +515,6 @@ def sign_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("MOD_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_i4( @@ -586,7 +524,6 @@ def mod_i4( R: Int32[N] ) -> Returns["N", Int32]: ... -@bind("MOD_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r4( @@ -596,7 +533,6 @@ def mod_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("MOD_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r8( @@ -606,7 +542,6 @@ def mod_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r4( @@ -615,7 +550,6 @@ def deg2rad_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r8( @@ -624,7 +558,6 @@ def deg2rad_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r4( @@ -633,7 +566,6 @@ def rad2deg_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r8( @@ -642,7 +574,6 @@ def rad2deg_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("DIST2_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r4( @@ -652,7 +583,6 @@ def dist2_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DIST2_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r8( @@ -662,7 +592,6 @@ def dist2_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("DOT2_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r4( @@ -674,7 +603,6 @@ def dot2_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DOT2_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r8( @@ -686,7 +614,6 @@ def dot2_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("DOT3_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r4( @@ -700,7 +627,6 @@ def dot3_r4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("DOT3_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r8( @@ -714,7 +640,6 @@ def dot3_r8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("CONJ_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c4( @@ -723,7 +648,6 @@ def conj_c4( R: Complex64[N] ) -> Returns["N", Int32]: ... -@bind("CONJ_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c8( @@ -732,7 +656,6 @@ def conj_c8( R: Complex128[N] ) -> Returns["N", Int32]: ... -@bind("REAL_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c4( @@ -741,7 +664,6 @@ def real_c4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("REAL_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c8( @@ -750,7 +672,6 @@ def real_c8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c4( @@ -759,7 +680,6 @@ def aimag_c4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c8( @@ -768,7 +688,6 @@ def aimag_c8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("ABS_C4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c4( @@ -777,7 +696,6 @@ def abs_c4( R: Float32[N] ) -> Returns["N", Int32]: ... -@bind("ABS_C8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c8( @@ -786,7 +704,6 @@ def abs_c8( R: Float64[N] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r4( @@ -795,7 +712,6 @@ def is_positive_r4( R: Bool8[N] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R8") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r8( @@ -804,7 +720,6 @@ def is_positive_r8( R: Bool8[N] ) -> Returns["N", Int32]: ... -@bind("IS_EVEN_I4") @standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_even_i4( diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi index 2c5a7a922..74a31ac5e 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi @@ -1,6 +1,5 @@ -from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call +from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, native_call -@bind("SQUARE_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r4_contiguous( N: Int32, @@ -8,7 +7,6 @@ def square_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r8_contiguous( N: Int32, @@ -16,7 +14,6 @@ def square_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_i4_contiguous( N: Int32, @@ -24,7 +21,6 @@ def square_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c4_contiguous( N: Int32, @@ -32,7 +28,6 @@ def square_c4_contiguous( R: Complex64[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c8_contiguous( N: Int32, @@ -40,7 +35,6 @@ def square_c8_contiguous( R: Complex128[:] ) -> Returns["N", Int32]: ... -@bind("CUBE_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r4_contiguous( N: Int32, @@ -48,7 +42,6 @@ def cube_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("CUBE_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r8_contiguous( N: Int32, @@ -56,7 +49,6 @@ def cube_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("CUBE_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_i4_contiguous( N: Int32, @@ -64,7 +56,6 @@ def cube_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("ADD_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r4_contiguous( N: Int32, @@ -73,7 +64,6 @@ def add_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ADD_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r8_contiguous( N: Int32, @@ -82,7 +72,6 @@ def add_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ADD_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_i4_contiguous( N: Int32, @@ -91,7 +80,6 @@ def add_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("ADD_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c4_contiguous( N: Int32, @@ -100,7 +88,6 @@ def add_c4_contiguous( R: Complex64[:] ) -> Returns["N", Int32]: ... -@bind("ADD_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c8_contiguous( N: Int32, @@ -109,7 +96,6 @@ def add_c8_contiguous( R: Complex128[:] ) -> Returns["N", Int32]: ... -@bind("SUB_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r4_contiguous( N: Int32, @@ -118,7 +104,6 @@ def sub_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SUB_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r8_contiguous( N: Int32, @@ -127,7 +112,6 @@ def sub_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("SUB_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_i4_contiguous( N: Int32, @@ -136,7 +120,6 @@ def sub_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("MUL_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r4_contiguous( N: Int32, @@ -145,7 +128,6 @@ def mul_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("MUL_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r8_contiguous( N: Int32, @@ -154,7 +136,6 @@ def mul_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MUL_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_i4_contiguous( N: Int32, @@ -163,7 +144,6 @@ def mul_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("DIV_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r4_contiguous( N: Int32, @@ -172,7 +152,6 @@ def div_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DIV_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r8_contiguous( N: Int32, @@ -181,7 +160,6 @@ def div_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("POW_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r4_contiguous( N: Int32, @@ -190,7 +168,6 @@ def pow_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("POW_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r8_contiguous( N: Int32, @@ -199,7 +176,6 @@ def pow_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ABS_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r4_contiguous( N: Int32, @@ -207,7 +183,6 @@ def abs_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ABS_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r8_contiguous( N: Int32, @@ -215,7 +190,6 @@ def abs_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ABS_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_i4_contiguous( N: Int32, @@ -223,7 +197,6 @@ def abs_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("NEG_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r4_contiguous( N: Int32, @@ -231,7 +204,6 @@ def neg_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("NEG_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r8_contiguous( N: Int32, @@ -239,7 +211,6 @@ def neg_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("NEG_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_i4_contiguous( N: Int32, @@ -247,7 +218,6 @@ def neg_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("SIN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r4_contiguous( N: Int32, @@ -255,7 +225,6 @@ def sin_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SIN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r8_contiguous( N: Int32, @@ -263,7 +232,6 @@ def sin_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("COS_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r4_contiguous( N: Int32, @@ -271,7 +239,6 @@ def cos_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("COS_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r8_contiguous( N: Int32, @@ -279,7 +246,6 @@ def cos_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("TAN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r4_contiguous( N: Int32, @@ -287,7 +253,6 @@ def tan_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("TAN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r8_contiguous( N: Int32, @@ -295,7 +260,6 @@ def tan_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ASIN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r4_contiguous( N: Int32, @@ -303,7 +267,6 @@ def asin_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ASIN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r8_contiguous( N: Int32, @@ -311,7 +274,6 @@ def asin_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ACOS_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r4_contiguous( N: Int32, @@ -319,7 +281,6 @@ def acos_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ACOS_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r8_contiguous( N: Int32, @@ -327,7 +288,6 @@ def acos_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ATAN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r4_contiguous( N: Int32, @@ -335,7 +295,6 @@ def atan_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ATAN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r8_contiguous( N: Int32, @@ -343,7 +302,6 @@ def atan_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r4_contiguous( N: Int32, @@ -352,7 +310,6 @@ def atan2_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r8_contiguous( N: Int32, @@ -361,7 +318,6 @@ def atan2_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("EXP_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r4_contiguous( N: Int32, @@ -369,7 +325,6 @@ def exp_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("EXP_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r8_contiguous( N: Int32, @@ -377,7 +332,6 @@ def exp_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("LOG_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r4_contiguous( N: Int32, @@ -385,7 +339,6 @@ def log_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("LOG_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r8_contiguous( N: Int32, @@ -393,7 +346,6 @@ def log_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("LOG10_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r4_contiguous( N: Int32, @@ -401,7 +353,6 @@ def log10_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("LOG10_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r8_contiguous( N: Int32, @@ -409,7 +360,6 @@ def log10_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("SQRT_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r4_contiguous( N: Int32, @@ -417,7 +367,6 @@ def sqrt_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SQRT_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r8_contiguous( N: Int32, @@ -425,7 +374,6 @@ def sqrt_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r4_contiguous( N: Int32, @@ -434,7 +382,6 @@ def hypot_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r8_contiguous( N: Int32, @@ -443,7 +390,6 @@ def hypot_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MIN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r4_contiguous( N: Int32, @@ -452,7 +398,6 @@ def min_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("MIN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r8_contiguous( N: Int32, @@ -461,7 +406,6 @@ def min_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MIN_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_i4_contiguous( N: Int32, @@ -470,7 +414,6 @@ def min_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("MAX_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r4_contiguous( N: Int32, @@ -479,7 +422,6 @@ def max_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("MAX_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r8_contiguous( N: Int32, @@ -488,7 +430,6 @@ def max_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MAX_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_i4_contiguous( N: Int32, @@ -497,7 +438,6 @@ def max_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("SIGN_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r4_contiguous( N: Int32, @@ -506,7 +446,6 @@ def sign_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("SIGN_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r8_contiguous( N: Int32, @@ -515,7 +454,6 @@ def sign_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("MOD_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_i4_contiguous( N: Int32, @@ -524,7 +462,6 @@ def mod_i4_contiguous( R: Int32[:] ) -> Returns["N", Int32]: ... -@bind("MOD_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r4_contiguous( N: Int32, @@ -533,7 +470,6 @@ def mod_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("MOD_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r8_contiguous( N: Int32, @@ -542,7 +478,6 @@ def mod_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r4_contiguous( N: Int32, @@ -550,7 +485,6 @@ def deg2rad_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r8_contiguous( N: Int32, @@ -558,7 +492,6 @@ def deg2rad_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r4_contiguous( N: Int32, @@ -566,7 +499,6 @@ def rad2deg_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r8_contiguous( N: Int32, @@ -574,7 +506,6 @@ def rad2deg_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("DIST2_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r4_contiguous( N: Int32, @@ -583,7 +514,6 @@ def dist2_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DIST2_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r8_contiguous( N: Int32, @@ -592,7 +522,6 @@ def dist2_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("DOT2_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r4_contiguous( N: Int32, @@ -603,7 +532,6 @@ def dot2_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DOT2_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r8_contiguous( N: Int32, @@ -614,7 +542,6 @@ def dot2_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("DOT3_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r4_contiguous( N: Int32, @@ -627,7 +554,6 @@ def dot3_r4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("DOT3_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r8_contiguous( N: Int32, @@ -640,7 +566,6 @@ def dot3_r8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("CONJ_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c4_contiguous( N: Int32, @@ -648,7 +573,6 @@ def conj_c4_contiguous( R: Complex64[:] ) -> Returns["N", Int32]: ... -@bind("CONJ_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c8_contiguous( N: Int32, @@ -656,7 +580,6 @@ def conj_c8_contiguous( R: Complex128[:] ) -> Returns["N", Int32]: ... -@bind("REAL_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c4_contiguous( N: Int32, @@ -664,7 +587,6 @@ def real_c4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("REAL_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c8_contiguous( N: Int32, @@ -672,7 +594,6 @@ def real_c8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c4_contiguous( N: Int32, @@ -680,7 +601,6 @@ def aimag_c4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c8_contiguous( N: Int32, @@ -688,7 +608,6 @@ def aimag_c8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("ABS_C4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c4_contiguous( N: Int32, @@ -696,7 +615,6 @@ def abs_c4_contiguous( R: Float32[:] ) -> Returns["N", Int32]: ... -@bind("ABS_C8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c8_contiguous( N: Int32, @@ -704,7 +622,6 @@ def abs_c8_contiguous( R: Float64[:] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r4_contiguous( N: Int32, @@ -712,7 +629,6 @@ def is_positive_r4_contiguous( R: Bool8[:] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R8_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r8_contiguous( N: Int32, @@ -720,7 +636,6 @@ def is_positive_r8_contiguous( R: Bool8[:] ) -> Returns["N", Int32]: ... -@bind("IS_EVEN_I4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_even_i4_contiguous( N: Int32, @@ -728,7 +643,6 @@ def is_even_i4_contiguous( R: Bool8[:] ) -> Returns["N", Int32]: ... -@bind("SQUARE_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r4_strided( N: Int32, @@ -736,7 +650,6 @@ def square_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SQUARE_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r8_strided( N: Int32, @@ -744,7 +657,6 @@ def square_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("SQUARE_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_i4_strided( N: Int32, @@ -752,7 +664,6 @@ def square_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c4_strided( N: Int32, @@ -760,7 +671,6 @@ def square_c4_strided( R: Complex64[::] ) -> Returns["N", Int32]: ... -@bind("SQUARE_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c8_strided( N: Int32, @@ -768,7 +678,6 @@ def square_c8_strided( R: Complex128[::] ) -> Returns["N", Int32]: ... -@bind("CUBE_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r4_strided( N: Int32, @@ -776,7 +685,6 @@ def cube_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("CUBE_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r8_strided( N: Int32, @@ -784,7 +692,6 @@ def cube_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("CUBE_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_i4_strided( N: Int32, @@ -792,7 +699,6 @@ def cube_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("ADD_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r4_strided( N: Int32, @@ -801,7 +707,6 @@ def add_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ADD_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r8_strided( N: Int32, @@ -810,7 +715,6 @@ def add_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ADD_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_i4_strided( N: Int32, @@ -819,7 +723,6 @@ def add_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("ADD_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c4_strided( N: Int32, @@ -828,7 +731,6 @@ def add_c4_strided( R: Complex64[::] ) -> Returns["N", Int32]: ... -@bind("ADD_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c8_strided( N: Int32, @@ -837,7 +739,6 @@ def add_c8_strided( R: Complex128[::] ) -> Returns["N", Int32]: ... -@bind("SUB_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r4_strided( N: Int32, @@ -846,7 +747,6 @@ def sub_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SUB_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r8_strided( N: Int32, @@ -855,7 +755,6 @@ def sub_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("SUB_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_i4_strided( N: Int32, @@ -864,7 +763,6 @@ def sub_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("MUL_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r4_strided( N: Int32, @@ -873,7 +771,6 @@ def mul_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("MUL_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r8_strided( N: Int32, @@ -882,7 +779,6 @@ def mul_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MUL_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_i4_strided( N: Int32, @@ -891,7 +787,6 @@ def mul_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("DIV_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r4_strided( N: Int32, @@ -900,7 +795,6 @@ def div_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DIV_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r8_strided( N: Int32, @@ -909,7 +803,6 @@ def div_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("POW_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r4_strided( N: Int32, @@ -918,7 +811,6 @@ def pow_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("POW_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r8_strided( N: Int32, @@ -927,7 +819,6 @@ def pow_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ABS_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r4_strided( N: Int32, @@ -935,7 +826,6 @@ def abs_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ABS_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r8_strided( N: Int32, @@ -943,7 +833,6 @@ def abs_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ABS_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_i4_strided( N: Int32, @@ -951,7 +840,6 @@ def abs_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("NEG_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r4_strided( N: Int32, @@ -959,7 +847,6 @@ def neg_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("NEG_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r8_strided( N: Int32, @@ -967,7 +854,6 @@ def neg_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("NEG_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_i4_strided( N: Int32, @@ -975,7 +861,6 @@ def neg_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("SIN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r4_strided( N: Int32, @@ -983,7 +868,6 @@ def sin_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SIN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r8_strided( N: Int32, @@ -991,7 +875,6 @@ def sin_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("COS_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r4_strided( N: Int32, @@ -999,7 +882,6 @@ def cos_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("COS_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r8_strided( N: Int32, @@ -1007,7 +889,6 @@ def cos_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("TAN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r4_strided( N: Int32, @@ -1015,7 +896,6 @@ def tan_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("TAN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r8_strided( N: Int32, @@ -1023,7 +903,6 @@ def tan_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ASIN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r4_strided( N: Int32, @@ -1031,7 +910,6 @@ def asin_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ASIN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r8_strided( N: Int32, @@ -1039,7 +917,6 @@ def asin_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ACOS_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r4_strided( N: Int32, @@ -1047,7 +924,6 @@ def acos_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ACOS_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r8_strided( N: Int32, @@ -1055,7 +931,6 @@ def acos_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ATAN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r4_strided( N: Int32, @@ -1063,7 +938,6 @@ def atan_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ATAN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r8_strided( N: Int32, @@ -1071,7 +945,6 @@ def atan_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r4_strided( N: Int32, @@ -1080,7 +953,6 @@ def atan2_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ATAN2_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r8_strided( N: Int32, @@ -1089,7 +961,6 @@ def atan2_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("EXP_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r4_strided( N: Int32, @@ -1097,7 +968,6 @@ def exp_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("EXP_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r8_strided( N: Int32, @@ -1105,7 +975,6 @@ def exp_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("LOG_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r4_strided( N: Int32, @@ -1113,7 +982,6 @@ def log_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("LOG_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r8_strided( N: Int32, @@ -1121,7 +989,6 @@ def log_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("LOG10_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r4_strided( N: Int32, @@ -1129,7 +996,6 @@ def log10_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("LOG10_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r8_strided( N: Int32, @@ -1137,7 +1003,6 @@ def log10_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("SQRT_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r4_strided( N: Int32, @@ -1145,7 +1010,6 @@ def sqrt_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SQRT_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r8_strided( N: Int32, @@ -1153,7 +1017,6 @@ def sqrt_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r4_strided( N: Int32, @@ -1162,7 +1025,6 @@ def hypot_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("HYPOT_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r8_strided( N: Int32, @@ -1171,7 +1033,6 @@ def hypot_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MIN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r4_strided( N: Int32, @@ -1180,7 +1041,6 @@ def min_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("MIN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r8_strided( N: Int32, @@ -1189,7 +1049,6 @@ def min_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MIN_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_i4_strided( N: Int32, @@ -1198,7 +1057,6 @@ def min_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("MAX_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r4_strided( N: Int32, @@ -1207,7 +1065,6 @@ def max_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("MAX_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r8_strided( N: Int32, @@ -1216,7 +1073,6 @@ def max_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MAX_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_i4_strided( N: Int32, @@ -1225,7 +1081,6 @@ def max_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("SIGN_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r4_strided( N: Int32, @@ -1234,7 +1089,6 @@ def sign_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("SIGN_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r8_strided( N: Int32, @@ -1243,7 +1097,6 @@ def sign_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("MOD_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_i4_strided( N: Int32, @@ -1252,7 +1105,6 @@ def mod_i4_strided( R: Int32[::] ) -> Returns["N", Int32]: ... -@bind("MOD_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r4_strided( N: Int32, @@ -1261,7 +1113,6 @@ def mod_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("MOD_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r8_strided( N: Int32, @@ -1270,7 +1121,6 @@ def mod_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r4_strided( N: Int32, @@ -1278,7 +1128,6 @@ def deg2rad_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DEG2RAD_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r8_strided( N: Int32, @@ -1286,7 +1135,6 @@ def deg2rad_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r4_strided( N: Int32, @@ -1294,7 +1142,6 @@ def rad2deg_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("RAD2DEG_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r8_strided( N: Int32, @@ -1302,7 +1149,6 @@ def rad2deg_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("DIST2_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r4_strided( N: Int32, @@ -1311,7 +1157,6 @@ def dist2_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DIST2_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r8_strided( N: Int32, @@ -1320,7 +1165,6 @@ def dist2_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("DOT2_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r4_strided( N: Int32, @@ -1331,7 +1175,6 @@ def dot2_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DOT2_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r8_strided( N: Int32, @@ -1342,7 +1185,6 @@ def dot2_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("DOT3_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r4_strided( N: Int32, @@ -1355,7 +1197,6 @@ def dot3_r4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("DOT3_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r8_strided( N: Int32, @@ -1368,7 +1209,6 @@ def dot3_r8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("CONJ_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c4_strided( N: Int32, @@ -1376,7 +1216,6 @@ def conj_c4_strided( R: Complex64[::] ) -> Returns["N", Int32]: ... -@bind("CONJ_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c8_strided( N: Int32, @@ -1384,7 +1223,6 @@ def conj_c8_strided( R: Complex128[::] ) -> Returns["N", Int32]: ... -@bind("REAL_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c4_strided( N: Int32, @@ -1392,7 +1230,6 @@ def real_c4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("REAL_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c8_strided( N: Int32, @@ -1400,7 +1237,6 @@ def real_c8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c4_strided( N: Int32, @@ -1408,7 +1244,6 @@ def aimag_c4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("AIMAG_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c8_strided( N: Int32, @@ -1416,7 +1251,6 @@ def aimag_c8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("ABS_C4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c4_strided( N: Int32, @@ -1424,7 +1258,6 @@ def abs_c4_strided( R: Float32[::] ) -> Returns["N", Int32]: ... -@bind("ABS_C8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c8_strided( N: Int32, @@ -1432,7 +1265,6 @@ def abs_c8_strided( R: Float64[::] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r4_strided( N: Int32, @@ -1440,7 +1272,6 @@ def is_positive_r4_strided( R: Bool8[::] ) -> Returns["N", Int32]: ... -@bind("IS_POSITIVE_R8_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r8_strided( N: Int32, @@ -1448,7 +1279,6 @@ def is_positive_r8_strided( R: Bool8[::] ) -> Returns["N", Int32]: ... -@bind("IS_EVEN_I4_STRIDED") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_even_i4_strided( N: Int32, diff --git a/tests/fortran/arrays/semantics/test_array_semantics.py b/tests/fortran/arrays/semantics/test_array_semantics.py index 75e96b557..901068c55 100644 --- a/tests/fortran/arrays/semantics/test_array_semantics.py +++ b/tests/fortran/arrays/semantics/test_array_semantics.py @@ -43,7 +43,7 @@ def test_array_constraints(): contract = array_contract(x.semantic_type) assert contract.category == "assumed_shape" - assert contract.shape == ["::Strided"] + assert contract.shape == ["::"] assert contract.source_shape == [":"] assert contract.order is None @@ -76,7 +76,7 @@ def test_matrix_semantics(): assert A.semantic_type.rank == 2 contract = array_contract(A.semantic_type) - assert A.semantic_type.shape == ["::Strided", "::Strided"] + assert A.semantic_type.shape == ["::", "::"] assert contract.source_shape == [":", ":"] assert contract.category == "assumed_shape" assert contract.order == "ORDER_F" @@ -230,7 +230,7 @@ def test_specification_function_calls_keep_local_and_imported_native_identity(): reloaded = parse_pyi_text(generated, module_name="expression_owner") reloaded_array = get_function(reloaded, "values").return_type.storage.array - assert "from extent_helpers import extent_for as imported_extent" in generated + assert "from .extent_helpers import extent_for as imported_extent" in generated assert "Float64[imported_extent(n), local_extent(n)]" in generated assert reloaded_array.expression_callables == array.expression_callables @@ -271,7 +271,7 @@ def test_wildcard_specification_function_origin_round_trips_unambiguously(): reloaded = parse_pyi_text(generated, module_name="expression_owner") reloaded_array = get_function(reloaded, "values").return_type.storage.array - assert "from extent_helpers import extent_for" in generated + assert "from .extent_helpers import extent_for" in generated assert reloaded_array.expression_callables == array.expression_callables diff --git a/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py b/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py index 416a96a53..14390ede1 100644 --- a/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py +++ b/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py @@ -34,7 +34,7 @@ def test_source_helpers_keep_nested_syntax_intact() -> None: "[third, fourth]", ] assert split_top_level_expression("'first''part', second", ",") == ["'first''part'", "second"] - assert split_top_level_expression("first::Strided:upper", ":") == ["first", "", "Strided", "upper"] + assert split_top_level_expression("first::middle:upper", ":") == ["first", "", "middle", "upper"] with pytest.raises(ValueError, match="one character"): split_top_level_expression("value", "::") @@ -111,7 +111,7 @@ def test_normalization_and_inspection_preserve_expression_provenance() -> None: assert declaration_extent_references("n + max(m, 1)") == ("n", "m") assert declaration_extent_references("values.shape[0]") == ("",) assert declaration_extent_references("not valid (") == ("",) - assert declaration_extent_references("::Strided") == () + assert declaration_extent_references("::") == () assert declaration_extent_uses_power("n ** 2") assert not declaration_extent_uses_power("not valid (") assert is_declaration_expression_helper("SUM") @@ -192,7 +192,7 @@ def test_role_resolution_reuses_completed_roles_and_names_blockers() -> None: array_roles = {"values": ("values", ("value_role_0", "value_role_1"))} callable_roles = {"extent_for": ("prik_extent_for", "extent_role")} - assert resolve_declaration_extent("::Strided", scalar_roles, array_roles) == ResolvedDeclarationExtent("::Strided") + assert resolve_declaration_extent("::", scalar_roles, array_roles) == ResolvedDeclarationExtent("::") assert resolve_declaration_extent("n + values.shape[1]", scalar_roles, array_roles) == ResolvedDeclarationExtent( "n + __prik_extent_values_1", ("n", "__prik_extent_values_1"), diff --git a/tests/fortran/callbacks/codegen/test_callback_planning.py b/tests/fortran/callbacks/codegen/test_callback_planning.py index 527aa537d..740b36bb0 100644 --- a/tests/fortran/callbacks/codegen/test_callback_planning.py +++ b/tests/fortran/callbacks/codegen/test_callback_planning.py @@ -64,12 +64,15 @@ def test_callback_policy_completes_value_default_and_explicit_reference_before_p assert scalar.thread_action is CallbackThreadAction.REQUIRE_ENTERING_THREAD assert scalar.gil_actions == (CallbackGILAction.ACQUIRE_GIL, CallbackGILAction.RELEASE_GIL) assert tuple(transfer.abi for transfer in scalar.arguments) == (CallbackABIKind.REFERENCE,) * 3 + # An undeclared intent permits the callee to read and modify the dummy, so + # it copies both ways rather than defaulting to copy-in. assert tuple(transfer.adapter_action for transfer in scalar.arguments) == ( CallbackTransferAction.COPY_IN_OUT, CallbackTransferAction.COPY_OUT, - CallbackTransferAction.COPY_IN, + CallbackTransferAction.COPY_IN_OUT, ) - assert tuple(transfer.python_action for transfer in scalar.arguments) == (PythonBarrierAction.SCALAR_VALUE,) * 3 + # Every dummy the callee may write needs storage Python can write through. + assert tuple(transfer.python_action for transfer in scalar.arguments) == (PythonBarrierAction.SCALAR_STORAGE,) * 3 array = policies["apply_array_storage_callback"].arguments[0].callback assert array.arguments[0].abi is CallbackABIKind.REFERENCE @@ -157,7 +160,9 @@ def test_callback_plan_edits_fail_central_validation_before_backend_emission(edi callback.arguments[1].extent_roles = () elif edit == "scalar_projection": callback = _callback_argument(plan, "apply_scalar_storage_callback").callback - callback.arguments[0].python_action = PythonBarrierAction.SCALAR_STORAGE + # A rank-zero storage transfer cannot claim the value projection: an + # immutable value cannot deliver a write back to the native caller. + callback.arguments[0].python_action = PythonBarrierAction.SCALAR_VALUE elif edit == "result": callback = _callback_argument(plan, "apply_value_callback").callback callback.result.action = CallbackResultAction.RETURN_VOID @@ -245,3 +250,136 @@ def test_optional_callback_retains_one_exact_policy_blocker(): with pytest.raises(ValueError, match="unsupported optional callback"): WrapperPlanner().build(module) + + +def test_runtime_callback_extents_lower_to_assumed_shape_dummies_and_measured_copies(): + """Codegen spells a runtime extent instead of leaking the plan's marker. + + A runtime extent reaches the bridge as a public marker rather than an + expression, so the dummy takes the caller's descriptor and the contiguous + copy that backs ``c_loc`` is measured from that dummy. + """ + module = pyi_file_to_semantic_module(ARRAY_CONTRACT, module_name="fcallback_array_f90") + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + + callback = _callback_argument(plan, "apply_assumed_shape").callback + assert [transfer.array.shape for transfer in callback.arguments] == [("::",), ("::",)] + + _, bridge = _sources(plan) + assert "real(c_double), intent(in), dimension(:) :: values" in bridge + assert "real(c_double), target, dimension(size(values, 1)) :: values_callback_storage" in bridge + assert "real(c_double), intent(out), dimension(:) :: doubled" in bridge + assert "real(c_double), target, dimension(size(doubled, 1)) :: doubled_callback_storage" in bridge + + +def test_rank_zero_callback_storage_lowers_to_a_direction_correct_native_view(): + """Rank-zero storage aliases native memory instead of copying a value. + + Writeability follows the completed transfer direction, so only an ``out`` + or ``inout`` dummy can be written through. + """ + module = pyi_text_to_semantic_module( + """ +from prik.contracts import Float64, In, InOut, Out, prototype + +@prototype +def directions_callback( + read_value: In(Float64[()]), + update_value: InOut(Float64[()]), + write_value: Out(Float64[()]) +) -> None: ... + +def apply_directions(callback: directions_callback) -> None: ... +""", + module_name="callback_scalar_storage", + ) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + + callback = _callback_argument(plan, "apply_directions").callback + assert [transfer.python_action for transfer in callback.arguments] == [PythonBarrierAction.SCALAR_STORAGE] * 3 + assert [transfer.abi for transfer in callback.arguments] == [CallbackABIKind.REFERENCE] * 3 + + c_source, _bridge = _sources(plan) + read_only = "PyArray_New(&PyArray_Type, 0, NULL, NPY_FLOAT64, NULL, read_value_data, 0, " + assert f"{read_only}NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_ALIGNED, NULL)" in c_source + for parameter in ("update_value", "write_value"): + writable = f"PyArray_New(&PyArray_Type, 0, NULL, NPY_FLOAT64, NULL, {parameter}_data, 0, " + assert f"{writable}NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_ALIGNED | NPY_ARRAY_WRITEABLE, NULL)" in c_source + + +MATRIX_CONTRACT = """ +from prik.contracts import Float64, In, Out, prototype + +@prototype +def matrix_callback( + input: In(Float64[::, ::]), + output: Out(Float64[::, ::]) +) -> None: ... + +def apply_matrix(callback: matrix_callback) -> None: ... +""" + + +def _matrix_plan(): + module = pyi_text_to_semantic_module(MATRIX_CONTRACT, module_name="callback_matrix") + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + +def test_multidimensional_runtime_extents_measure_every_axis_from_the_dummy(): + """Each axis of an assumed-shape callback array is lowered independently. + + A rank-one fix can silently ignore later axes, so the copy that backs + ``c_loc`` must be measured on every axis of the dummy it sits beside. + """ + plan = _matrix_plan() + callback = _callback_argument(plan, "apply_matrix").callback + assert [transfer.array.rank for transfer in callback.arguments] == [2, 2] + + _, bridge = _sources(plan) + assert "real(c_double), intent(in), dimension(:, :) :: input" in bridge + assert "real(c_double), target, dimension(size(input, 1), size(input, 2)) :: input_callback_storage" in bridge + assert "real(c_double), intent(out), dimension(:, :) :: output" in bridge + assert "real(c_double), target, dimension(size(output, 1), size(output, 2)) :: output_callback_storage" in bridge + + +def test_callback_docstrings_carry_array_rank_and_public_extents(): + """A callable's ABI depends on rank and shape, so both are documented. + + Extents use the spelling the `.pyi` contract uses, so the two descriptions + of the same array agree and no internal marker reaches the reader. + """ + plan = _matrix_plan() + c_source, _bridge = _sources(plan) + documentation = c_source.encode().decode("unicode_escape") + + assert "Called as: callback(input, output) -> None" in documentation + assert "input : ndarray[float64], rank 2, shape (::, ::), intent(in)" in documentation + assert "output : ndarray[float64], rank 2, shape (::, ::), intent(out)" in documentation + + +def test_callback_array_result_diagnostic_uses_the_contract_spelling(): + """A rejected shape is reported the way a contract would spell it. + + A function result has no caller descriptor to measure, so a runtime extent + there is refused; the message names the extent the author wrote rather than + the explicit step the IR stores. + """ + module = pyi_text_to_semantic_module( + """ +from prik.contracts import Float64, In, prototype + +@prototype +def strided_result(x: In(Float64)) -> Float64[::]: ... + +def apply(callback: strided_result) -> None: ... +""", + module_name="callback_strided_result", + ) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + + with pytest.raises(ValueError, match=r"runtime extents \['::'\]"): + _sources(plan) diff --git a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/fcallback_all_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/fcallback_all_f90.pyi index b6895f49a..863dfa6fc 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/fcallback_all_f90.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_all_f90/fcallback_all_f90.pyi @@ -18,9 +18,9 @@ def value_callback( @prototype def scalar_storage_callback( - value: InOut(Addr(Float64)), - output: Out(Addr(Float64)), - missing: Addr(Float64) + value: InOut(Float64[()]), + output: Out(Float64[()]), + missing: Float64[()] ) -> None: ... @prototype diff --git a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/fcallback_array_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/fcallback_array_f90.pyi index 5623f4aad..c2751e0af 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/fcallback_array_f90.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_array_f90/fcallback_array_f90.pyi @@ -1,4 +1,4 @@ -from prik.contracts import Addr, Arg, Float64, In, Int32, native_call, prototype +from prik.contracts import Addr, Arg, Float64, In, Int32, Out, native_call, prototype @prototype def reduce_callback( @@ -12,6 +12,12 @@ def transform_callback( values: In(Float64[count]) ) -> Float64[count]: ... +@prototype +def assumed_shape_callback( + values: In(Float64[::]), + doubled: Out(Float64[::]) +) -> None: ... + @native_call([Arg(0), Addr(Arg(1)), Arg(2)]) def apply_reduce( callback: reduce_callback, @@ -26,3 +32,9 @@ def apply_transform( values: Float64[count], output: Float64[count] ) -> None: ... + +def apply_assumed_shape( + callback: assumed_shape_callback, + values: Float64[::], + doubled: Float64[::] +) -> None: ... diff --git a/tests/fortran/callbacks/end_to_end/fixtures/native/fcallback_array_f90.f90 b/tests/fortran/callbacks/end_to_end/fixtures/native/fcallback_array_f90.f90 index 28e673c31..a3b003125 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/native/fcallback_array_f90.f90 +++ b/tests/fortran/callbacks/end_to_end/fixtures/native/fcallback_array_f90.f90 @@ -14,6 +14,11 @@ function transform_callback(count, values) result(output) real(8), intent(in) :: values(count) real(8) :: output(count) end function transform_callback + + subroutine assumed_shape_callback(values, doubled) + real(8), intent(in) :: values(:) + real(8), intent(out) :: doubled(:) + end subroutine assumed_shape_callback end interface contains @@ -33,4 +38,12 @@ subroutine apply_transform(callback, count, values, output) output = callback(count, values) end subroutine apply_transform + + subroutine apply_assumed_shape(callback, values, doubled) + procedure(assumed_shape_callback) :: callback + real(8), intent(in) :: values(:) + real(8), intent(out) :: doubled(:) + + call callback(values, doubled) + end subroutine apply_assumed_shape end module fcallback_array_f90 diff --git a/tests/fortran/callbacks/end_to_end/test_array_callbacks.py b/tests/fortran/callbacks/end_to_end/test_array_callbacks.py index ac7ee8677..fd1147177 100644 --- a/tests/fortran/callbacks/end_to_end/test_array_callbacks.py +++ b/tests/fortran/callbacks/end_to_end/test_array_callbacks.py @@ -5,7 +5,10 @@ import numpy as np import pytest -from tests.fortran._support.wrapper_build import _build_source_or_generated_pyi_and_import +from tests.fortran._support.wrapper_build import ( + _build_source_and_import, + _build_source_or_generated_pyi_and_import, +) FIXTURES = Path(__file__).parent / "fixtures" CALLBACK_ARRAY_F90_SOURCE = FIXTURES / "native" / "fcallback_array_f90.f90" @@ -40,3 +43,87 @@ def test_immediate_dummy_procedure_converts_array_arguments_and_results( ) assert result is None np.testing.assert_array_equal(transformed, np.array([2.0, 4.0, 6.0], dtype=np.float64)) + + +def test_assumed_shape_callback_arrays_cross_the_boundary_as_contiguous_copies( + pyi_parity_build_mode: str, + tmp_path: Path, +): + """An assumed-shape callback dummy carries its extent from the native descriptor.""" + module = _build_source_or_generated_pyi_and_import( + CALLBACK_ARRAY_F90_SOURCE, + tmp_path, + { + "bind_c_fcallback_array_f90_wrapper.f90", + "fcallback_array_f90_wrapper.c", + "fcallback_array_f90_wrapper.h", + }, + CONTRACT_FIXTURES / "fcallback_array_f90", + pyi_parity_build_mode, + ) + values = np.asfortranarray(np.array([1.5, 2.5, 3.5, 4.5], dtype=np.float64)) + doubled = np.zeros(4, dtype=np.float64) + seen = [] + + def double(data, output): + seen.append(np.array(data)) + output[...] = data * 2.0 + + assert module.apply_assumed_shape(double, values, doubled) is None + np.testing.assert_array_equal(seen[0], values) + np.testing.assert_array_equal(doubled, values * 2.0) + + +MATRIX_SOURCE = """ +module fcallback_matrix_f90 + implicit none + + abstract interface + subroutine matrix_callback(input, output) + real(8), intent(in) :: input(:,:) + real(8), intent(out) :: output(:,:) + end subroutine matrix_callback + end interface + +contains + subroutine apply_matrix(callback, input, output) + procedure(matrix_callback) :: callback + real(8), intent(in) :: input(:,:) + real(8), intent(out) :: output(:,:) + + call callback(input, output) + end subroutine apply_matrix +end module fcallback_matrix_f90 +""" + + +def test_rank_two_assumed_shape_callback_arrays_cross_both_directions(tmp_path: Path): + """Every axis of a multidimensional assumed-shape dummy must survive. + + A rank-one lowering can look correct while dropping later axes, so this + checks the extents the callable observes and the data written back. + """ + source = tmp_path / "fcallback_matrix_f90.f90" + source.write_text(MATRIX_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_matrix_f90_wrapper.f90", + "fcallback_matrix_f90_wrapper.c", + "fcallback_matrix_f90_wrapper.h", + }, + ) + incoming = np.asfortranarray(np.arange(6, dtype=np.float64).reshape(2, 3)) + written = np.asfortranarray(np.zeros((2, 3), dtype=np.float64)) + observed = {} + + def double(input_values, output_values): + observed["shape"] = input_values.shape + observed["values"] = np.array(input_values) + output_values[...] = input_values * 2.0 + + assert module.apply_matrix(double, incoming, written) is None + assert observed["shape"] == (2, 3) + np.testing.assert_array_equal(observed["values"], incoming) + np.testing.assert_array_equal(written, incoming * 2.0) diff --git a/tests/fortran/callbacks/end_to_end/test_callback_scalar_storage.py b/tests/fortran/callbacks/end_to_end/test_callback_scalar_storage.py new file mode 100644 index 000000000..42719594f --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/test_callback_scalar_storage.py @@ -0,0 +1,288 @@ +"""Rank-zero callback storage: writable scalar dummies reach native memory.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_generated_pyi_and_import, + _build_inline_pyi_contract_module, + _build_source_and_import, +) + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = """ +module fcallback_scalar_storage_f90 + implicit none + + abstract interface + subroutine directions_callback(read_value, update_value, write_value) + real(8), intent(in) :: read_value + real(8), intent(inout) :: update_value + real(8), intent(out) :: write_value + end subroutine directions_callback + end interface + +contains + subroutine apply_directions(callback, read_value, update_value, write_value) + procedure(directions_callback) :: callback + real(8), intent(in) :: read_value + real(8), intent(inout) :: update_value + real(8), intent(out) :: write_value + + call callback(read_value, update_value, write_value) + end subroutine apply_directions +end module fcallback_scalar_storage_f90 +""" + +CONTRACT = """ +from prik.contracts import Addr, Arg, Float64, In, InOut, Out, Return, Returns, native_call, prototype + +@prototype +def directions_callback( + read_value: In(Float64[()]), + update_value: InOut(Float64[()]), + write_value: Out(Float64[()]) +) -> None: ... + +@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2)), Return('write_value', 1)]) +def apply_directions( + callback: directions_callback, + read_value: Float64, + update_value: Float64 +) -> tuple[Returns["update_value", Float64], Float64]: ... +""" + + +def test_rank_zero_callback_storage_writes_through_to_the_native_caller(tmp_path: Path): + """A rank-zero storage dummy exposes native memory with direction-correct access. + + The default `Addr(T)` spelling hands Python an independent value, so a + contract that needs an `out` or `inout` callback dummy to reach the native + caller asks for storage instead. + """ + module, _result = _build_inline_pyi_contract_module( + tmp_path, + module_name="fcallback_scalar_storage_f90", + source_text=SOURCE, + contract_text=CONTRACT, + ) + observed = {} + + def callback(read_value, update_value, write_value): + observed["read_writeable"] = read_value.flags.writeable + observed["update_writeable"] = update_value.flags.writeable + observed["write_writeable"] = write_value.flags.writeable + observed["read"] = float(read_value) + observed["update_in"] = float(update_value) + update_value[...] = float(update_value) * 10.0 + write_value[...] = float(read_value) + float(update_value) + + updated, written = module.apply_directions(callback, np.float64(3.0), np.float64(4.0)) + + assert observed == { + "read_writeable": False, + "update_writeable": True, + "write_writeable": True, + "read": 3.0, + "update_in": 4.0, + } + assert updated == np.float64(40.0) + assert written == np.float64(43.0) + + +SOURCE_DEFAULT = """ +module fcallback_default_storage_f90 + implicit none + + abstract interface + subroutine objective_callback(x, f) + real(8), intent(in) :: x(:) + real(8), intent(out) :: f + end subroutine objective_callback + end interface + +contains + subroutine evaluate(calfun, x, total) + procedure(objective_callback) :: calfun + real(8), intent(in) :: x(:) + real(8), intent(out) :: total + + call calfun(x, total) + end subroutine evaluate +end module fcallback_default_storage_f90 +""" + + +def test_out_scalar_callback_writes_back_without_editing_the_contract(tmp_path: Path): + """Wrapping Fortran source directly produces a callback that can answer. + + The generated default must be the spelling that works: an `intent(out)` + scalar reaches Python as writable storage, so the value the callable + computes reaches the native caller with no contract edit. + """ + source = tmp_path / "fcallback_default_storage_f90.f90" + source.write_text(SOURCE_DEFAULT, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_default_storage_f90_wrapper.f90", + "fcallback_default_storage_f90_wrapper.c", + "fcallback_default_storage_f90_wrapper.h", + }, + ) + + def objective(x): + return float(np.sum(x * x)) + + def objective_prik(x, f): + f[...] = objective(x) + + assert module.evaluate(objective_prik, np.array([1.0, 2.0, 3.0])) == np.float64(14.0) + + +def test_callback_docstring_states_the_callable_signature_and_write_through(tmp_path: Path): + """The docstring is the only callback description in the source-only workflow. + + Guessing a callback signature wrong is fatal at the callback boundary, so + `help()` must state the arity, direction, and how an output is delivered. + """ + source = tmp_path / "fcallback_default_storage_f90.f90" + source.write_text(SOURCE_DEFAULT, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_default_storage_f90_wrapper.f90", + "fcallback_default_storage_f90_wrapper.c", + "fcallback_default_storage_f90_wrapper.h", + }, + ) + documentation = module.evaluate.__doc__ + + assert "Called as: calfun(x, f) -> None" in documentation + assert "x : ndarray[float64], rank 1, shape (::), intent(in)" in documentation + assert "f : ndarray[float64], intent(out); assign through it (f[...] = value)" in documentation + assert "An exception or an invalid return value terminates the process." in documentation + + +SOURCE_UNDECLARED = """ +module fcallback_undeclared_intent_f90 + implicit none + + abstract interface + subroutine tweak_callback(value) + real(8) :: value + end subroutine tweak_callback + end interface + +contains + subroutine drive(callback, seed, result) + procedure(tweak_callback) :: callback + real(8), intent(in) :: seed + real(8), intent(out) :: result + + result = seed + call callback(result) + end subroutine drive +end module fcallback_undeclared_intent_f90 +""" + + +def _undeclared_intent_module(tmp_path: Path): + source = tmp_path / "fcallback_undeclared_intent_f90.f90" + source.write_text(SOURCE_UNDECLARED, encoding="utf-8") + return _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_undeclared_intent_f90_wrapper.f90", + "fcallback_undeclared_intent_f90_wrapper.c", + "fcallback_undeclared_intent_f90_wrapper.h", + }, + ) + + +def test_callback_scalar_without_declared_intent_is_read_and_written(tmp_path: Path): + """An undeclared ``intent`` is conservatively both read and written. + + Fortran permits the callee to modify such a dummy, so the callable must + observe the incoming value and see its own write reach the native caller. + """ + module = _undeclared_intent_module(tmp_path) + observed = [] + + def tweak(value): + observed.append(float(value)) + assert value.flags.writeable + value[...] = float(value) * 3.0 + + assert module.drive(tweak, np.float64(7.0)) == np.float64(21.0) + assert observed == [7.0] + + +def test_undeclared_intent_survives_the_generated_contract_round_trip(tmp_path: Path): + """The absent ``intent`` must survive source, contract, codegen and runtime. + + Building through PRIK's own generated contract proves the bare + ``Float64[()]`` spelling carries the conservative read/write transfer all + the way to the trampoline, rather than only appearing in the contract text. + """ + source = tmp_path / "fcallback_undeclared_intent_f90.f90" + source.write_text(SOURCE_UNDECLARED, encoding="utf-8") + workdir = tmp_path / "round_trip" + module = _build_generated_pyi_and_import(source, workdir) + + contract = (workdir / "contracts" / source.stem / f"{source.stem}.pyi").read_text(encoding="utf-8") + assert "value: Float64[()]" in contract + assert "In(" not in contract and "Out(" not in contract and "InOut(" not in contract + + bridge = next((workdir / "pyi_build").glob("bind_c_*_wrapper.f90")).read_text(encoding="utf-8") + assert "real(c_double) :: value" in bridge + assert not any(f"intent({direction}) :: value" in bridge for direction in ("in", "out", "inout")) + assert "value = value_callback_storage" in bridge + + def tweak(value): + value[...] = float(value) * 3.0 + + assert module.drive(tweak, np.float64(7.0)) == np.float64(21.0) + + +def test_assume_intent_in_scalars_makes_an_undeclared_callback_scalar_input_only(tmp_path: Path): + """The flag narrows the default without declaring a direction. + + The contract still carries no direction wrapper, because the source still + declares none; only the projection and the copy direction change. + """ + source = tmp_path / "fcallback_undeclared_intent_f90.f90" + source.write_text(SOURCE_UNDECLARED, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_fcallback_undeclared_intent_f90_wrapper.f90", + "fcallback_undeclared_intent_f90_wrapper.c", + "fcallback_undeclared_intent_f90_wrapper.h", + }, + assume_intent_in_scalars=True, + ) + contract = (tmp_path / "build" / "contracts" / "fcallback_undeclared_intent_f90.pyi").read_text(encoding="utf-8") + assert "value: Addr(Float64)" in contract + assert "In(" not in contract and "Out(" not in contract and "InOut(" not in contract + + bridge = (tmp_path / "build" / "bind_c_fcallback_undeclared_intent_f90_wrapper.f90").read_text(encoding="utf-8") + assert "real(c_double) :: value" in bridge + assert not any(f"intent({direction}) :: value" in bridge for direction in ("in", "out", "inout")) + # Input-only: nothing is copied back out of the call-local storage. + assert "value = value_callback_storage" not in bridge + + observed = [] + + def tweak(value): + observed.append(float(value)) + + assert module.drive(tweak, np.float64(7.0)) == np.float64(7.0) + assert observed == [7.0] diff --git a/tests/fortran/callbacks/end_to_end/test_direct_bind_c_callback_storage.py b/tests/fortran/callbacks/end_to_end/test_direct_bind_c_callback_storage.py new file mode 100644 index 000000000..7b706b257 --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/test_direct_bind_c_callback_storage.py @@ -0,0 +1,90 @@ +"""Writable scalar callback storage on the bridge-free direct `bind(C)` route.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = """ +module fcallback_direct_storage_f90 + use iso_c_binding + implicit none + + abstract interface + subroutine update_callback(value) bind(C) + import :: c_double + real(c_double), intent(inout) :: value + end subroutine update_callback + + subroutine emit_callback(value) bind(C) + import :: c_double + real(c_double), intent(out) :: value + end subroutine emit_callback + end interface + +contains + real(c_double) function drive_update(callback, seed) bind(C) result(output) + procedure(update_callback) :: callback + real(c_double), value, intent(in) :: seed + + output = seed + call callback(output) + end function drive_update + + real(c_double) function drive_emit(callback) bind(C) result(output) + procedure(emit_callback) :: callback + + call callback(output) + end function drive_emit +end module fcallback_direct_storage_f90 +""" + + +def _direct_module(tmp_path: Path): + source = tmp_path / "fcallback_direct_storage_f90.f90" + source.write_text(SOURCE, encoding="utf-8") + # A bind(C) entry point needs no generated Fortran adapter, so the expected + # source set is exactly the binding pair. + return _build_source_and_import( + source, + tmp_path / "build", + { + "fcallback_direct_storage_f90_wrapper.c", + "fcallback_direct_storage_f90_wrapper.h", + }, + ) + + +def test_direct_bind_c_callbacks_receive_writable_rank_zero_storage(tmp_path: Path): + """The projection must work where no Fortran bridge exists at all. + + A direct entry point calls the trampoline as a plain C function pointer, so + writable storage has to be the binding's doing rather than an adapter's. + """ + module = _direct_module(tmp_path) + observed = {} + + def update(value): + observed["writeable"] = value.flags.writeable + observed["incoming"] = float(value) + value[...] *= 2 + + def emit(value): + observed["emit_writeable"] = value.flags.writeable + value[...] = 42.0 + + assert module.drive_update(update, np.float64(5.0)) == np.float64(10.0) + assert module.drive_emit(emit) == np.float64(42.0) + assert observed == {"writeable": True, "incoming": 5.0, "emit_writeable": True} + + +def test_direct_bind_c_callback_storage_adds_no_fortran_bridge(tmp_path: Path): + """Scalar callback storage must not drag a bridge onto the direct route.""" + _direct_module(tmp_path) + generated = {path.name for path in (tmp_path / "build").glob("*_wrapper.f90")} + + assert generated == set() diff --git a/tests/fortran/callbacks/end_to_end/test_multi_file_contract_generation.py b/tests/fortran/callbacks/end_to_end/test_multi_file_contract_generation.py new file mode 100644 index 000000000..98c14bccb --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/test_multi_file_contract_generation.py @@ -0,0 +1,364 @@ +"""Multi-file `generate --pyi`: imported interfaces reach a buildable contract.""" + +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _compiler, _import_from_build_dir +from prik import build_pyi_extension +from prik.pipeline.pyi import pyi_text_to_semantic_module +from prik.semantics.native_contract import native_contract_issues + +pytestmark = pytest.mark.fortran_end_to_end + +PINTRF_SOURCE = """ +module pintrf_mod + implicit none + private + public :: OBJ + + abstract interface + subroutine OBJ(x, f) + implicit none + real(8), intent(in) :: x + real(8), intent(out) :: f + end subroutine OBJ + end interface +end module pintrf_mod +""" + +SOLVER_SOURCE = """ +module solver_mod + use, non_intrinsic :: pintrf_mod, only : OBJ + implicit none +contains + subroutine minimize(calfun, x, f) + procedure(OBJ) :: calfun + real(8), intent(in) :: x + real(8), intent(out) :: f + + call calfun(x, f) + end subroutine minimize +end module solver_mod +""" + +RENAMED_SOURCE = """ +module renamed_mod + use, non_intrinsic :: pintrf_mod, only : LOCAL_OBJ => OBJ + implicit none +contains + subroutine minimize_renamed(calfun, x, f) + procedure(LOCAL_OBJ) :: calfun + real(8), intent(in) :: x + real(8), intent(out) :: f + + call calfun(x, f) + end subroutine minimize_renamed +end module renamed_mod + +module scoped_rename_mod + implicit none +contains + subroutine minimize_scoped(calfun, x, f) + use, non_intrinsic :: pintrf_mod, only : SCOPED_OBJ => OBJ + implicit none + procedure(SCOPED_OBJ) :: calfun + real(8), intent(in) :: x + real(8), intent(out) :: f + + call calfun(x, f) + end subroutine minimize_scoped +end module scoped_rename_mod +""" + + +def _generate_contracts(tmp_path: Path) -> tuple[Path, list[Path]]: + sources = [] + for name, text in ( + ("pintrf.f90", PINTRF_SOURCE), + ("solver.f90", SOLVER_SOURCE), + ("renamed.f90", RENAMED_SOURCE), + ): + path = tmp_path / name + path.write_text(text, encoding="utf-8") + sources.append(path) + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + *[str(path) for path in sources], + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + return contracts, sources + + +def test_multi_file_generation_places_the_prototype_with_its_declaring_module(tmp_path: Path): + """Each module's contract records what that module declares or imports. + + The per-file CLI conversion path is where an imported interface previously + degraded to an opaque placeholder, so this exercises that workflow rather + than whole-project conversion. + """ + contracts, _sources = _generate_contracts(tmp_path) + + declaring = (contracts / "pintrf_mod.pyi").read_text(encoding="utf-8") + assert "@prototype\ndef OBJ(" in declaring + + consuming = (contracts / "solver_mod.pyi").read_text(encoding="utf-8") + assert "from .pintrf_mod import OBJ" in consuming + assert "calfun: OBJ" in consuming + + renamed = (contracts / "renamed_mod.pyi").read_text(encoding="utf-8") + assert "from .pintrf_mod import OBJ as LOCAL_OBJ" in renamed + assert "calfun: LOCAL_OBJ" in renamed + + # A procedure-local rename reaches the contract through the synthetic + # prototype import rather than the module's own import list. + scoped = (contracts / "scoped_rename_mod.pyi").read_text(encoding="utf-8") + assert "from .pintrf_mod import OBJ as SCOPED_OBJ" in scoped + assert "calfun: SCOPED_OBJ" in scoped + assert "import SCOPED_OBJ" not in scoped.replace("OBJ as SCOPED_OBJ", "") + + +def test_generated_multi_file_contracts_parse_without_native_contract_issues(tmp_path: Path): + """PRIK must be able to read back every contract it just wrote.""" + contracts, _sources = _generate_contracts(tmp_path) + + for contract in sorted(contracts.glob("*.pyi")): + if contract.name == "__init__.pyi": + continue + module = pyi_text_to_semantic_module(contract.read_text(encoding="utf-8"), module_name=contract.stem) + assert native_contract_issues(module) == [], contract.name + + +def test_building_from_generated_multi_file_contracts_runs_the_callback(tmp_path: Path): + """The whole route must survive: source, contract, parse, build, call.""" + contracts, sources = _generate_contracts(tmp_path) + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(path) for path in sources], + output_dir=tmp_path / "build", + output_name="multi_file_callbacks", + ) + module = _import_from_build_dir(result.module_name, result.output_dir) + + def objective(x, f): + f[...] = float(x) ** 2 + + assert module.solver_mod.minimize(objective, np.float64(3.0)) == np.float64(9.0) + assert module.renamed_mod.minimize_renamed(objective, np.float64(4.0)) == np.float64(16.0) + assert module.scoped_rename_mod.minimize_scoped(objective, np.float64(5.0)) == np.float64(25.0) + + +CALLBACK_RESULT_TYPES_SOURCE = """ +module cbresult_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + function make_point(x) result(p) + import :: point_t + implicit none + real(8), intent(in) :: x + type(point_t) :: p + end function make_point + end interface +end module cbresult_types +""" + +CALLBACK_RESULT_CONSUMER_SOURCE = """ +module cbresult_consumer + use, non_intrinsic :: cbresult_types, only : make_point, point_t + implicit none +contains + subroutine run(f, seed, out_x) + procedure(make_point) :: f + real(8), intent(in) :: seed + real(8), intent(out) :: out_x + type(point_t) :: made + + made = f(seed) + out_x = made%x + end subroutine run +end module cbresult_consumer +""" + + +def test_imported_callback_returning_a_module_owned_type_builds(tmp_path: Path): + """A callback result type belongs to the module that declares the interface. + + Attributing it to the consuming module produced an identity no wrapper + definition could satisfy, so the build failed outright. The generated + contract must name the declaring module and the extension must build. + + The built extension is not called here: resolving a cross-module derived + type through the runtime namespace is a separate, pre-existing gap that + also affects ordinary functions returning an imported type. + """ + sources = [] + for name, text in ( + ("cbresult_types.f90", CALLBACK_RESULT_TYPES_SOURCE), + ("cbresult_consumer.f90", CALLBACK_RESULT_CONSUMER_SOURCE), + ): + path = tmp_path / name + path.write_text(text, encoding="utf-8") + sources.append(path) + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + *[str(path) for path in sources], + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + + declaring = (contracts / "cbresult_types.pyi").read_text(encoding="utf-8") + assert "def make_point(" in declaring + assert "-> point_t: ..." in declaring + + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(path) for path in sources], + output_dir=tmp_path / "build", + output_name="callback_result_types", + ) + assert result.shared_library.exists() + + +RENAMED_CHAIN_SOURCE = """ +module chain_declares_mod + implicit none + abstract interface + subroutine OBJ(x, f) + implicit none + real(8), intent(in) :: x + real(8), intent(out) :: f + end subroutine OBJ + end interface +end module chain_declares_mod + +module chain_middle_mod + use, non_intrinsic :: chain_declares_mod, only : MID => OBJ + implicit none + public :: MID +end module chain_middle_mod + +module chain_consumer_mod + use, non_intrinsic :: chain_middle_mod, only : LOCAL => MID + implicit none +contains + subroutine run_chain(calfun, x, f) + procedure(LOCAL) :: calfun + real(8), intent(in) :: x + real(8), intent(out) :: f + + call calfun(x, f) + end subroutine run_chain +end module chain_consumer_mod +""" + + +def test_renamed_reexport_chain_builds_through_its_generated_contracts(tmp_path: Path): + """Each hop renames the interface, so only the declaring module names it. + + A rename and a re-export are covered separately elsewhere; combining them + is what exposes a reference that followed the module back to the declaration + while keeping an alias from somewhere along the way. + """ + source = tmp_path / "chain.f90" + source.write_text(RENAMED_CHAIN_SOURCE, encoding="utf-8") + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + str(source), + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + + # Each contract mirrors the `use` its own module wrote. + assert "from .chain_declares_mod import OBJ as MID" in (contracts / "chain_middle_mod.pyi").read_text( + encoding="utf-8" + ) + consuming = (contracts / "chain_consumer_mod.pyi").read_text(encoding="utf-8") + assert "from .chain_middle_mod import MID as LOCAL" in consuming + assert "calfun: LOCAL" in consuming + + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(source)], + output_dir=tmp_path / "build", + output_name="renamed_chain_callbacks", + ) + module = _import_from_build_dir(result.module_name, result.output_dir) + + def objective(x, f): + f[...] = float(x) * 7.0 + + assert module.chain_consumer_mod.run_chain(objective, np.float64(6.0)) == np.float64(42.0) + + +def test_renamed_reexport_chain_builds_directly_from_its_fortran_source(tmp_path: Path): + """Publishing an imported interface adds no runtime name to alias. + + A module publishing an imported prototype states where a callback signature + comes from, and a signature is not an object Python holds. Binding one at + runtime reaches for an attribute of a module that exports nothing at all, + so the chain has to reach the build through prototype resolution alone. + """ + from tests.fortran._support.wrapper_build import _build_source_and_import + + source = tmp_path / "chain.f90" + source.write_text(RENAMED_CHAIN_SOURCE, encoding="utf-8") + + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_chain_wrapper.f90", "chain_wrapper.c", "chain_wrapper.h"}, + ) + + def calfun(x, f): + f[()] = x * 3.0 + + assert module.run_chain(calfun, np.float64(4.0)) == pytest.approx(12.0) + # The consuming module is the only namespace with a runtime name, so the + # declaring and publishing modules contributed nothing to alias. + extension = sys.modules[module.__name__.split(".", 1)[0]] + assert not hasattr(extension, "chain_declares_mod") + assert not hasattr(extension, "chain_middle_mod") diff --git a/tests/fortran/callbacks/policy/test_callback_policy.py b/tests/fortran/callbacks/policy/test_callback_policy.py index b069f2936..3689016b0 100644 --- a/tests/fortran/callbacks/policy/test_callback_policy.py +++ b/tests/fortran/callbacks/policy/test_callback_policy.py @@ -11,6 +11,7 @@ RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA, ) from prik.policy.completion import complete_semantic_policies +from prik.policy.ownership import PythonBarrierAction from prik.policy.models import ( CallbackABIKind, CallbackTransferAction, @@ -21,10 +22,10 @@ FIXTURES = Path(__file__).parents[1] / "end_to_end" / "fixtures" -def _source_semantic_module(filename: str, *, module_name: str): +def _source_semantic_module(filename: str, *, module_name: str, assume_intent_in_scalars: bool = False): source = FIXTURES / "native" / filename parsed = parse_fortran_project({str(source): _fortran_source_for_pipeline(source, PreprocessingConfig())}) - modules = fortran_project_to_semantic_modules(parsed) + modules = fortran_project_to_semantic_modules(parsed, assume_intent_in_scalars=assume_intent_in_scalars) _apply_source_python_exports(modules) module = _merge_wrapper_modules(modules, name=module_name) complete_semantic_policies(module) @@ -83,3 +84,222 @@ def apply(callback: callback_shape) -> None: ... assert isinstance(policy, FunctionWrapperPolicy) assert policy.supported is False assert blocker in policy.blockers + + +def test_procedure_interface_from_an_unsupplied_module_is_blocked_by_name(): + """A named interface no input declares is reported against that name. + + Without the module that declares it the dummy has no signature, so the + diagnostic must name the interface the declaration asked for rather than + the opaque placeholder type it fell back to. + """ + source = """ +module solver_mod + use, non_intrinsic :: pintrf_mod, only : OBJ + implicit none +contains + subroutine minimize(calfun, x) + procedure(OBJ) :: calfun + real(8), intent(in) :: x + end subroutine minimize +end module solver_mod +""" + parsed = parse_fortran_project({"solver.f90": source}) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name="solver_mod") + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert policy.supported is False + assert ( + "argument 'calfun' declares procedure interface 'OBJ', which no supplied source declares; " + "add the module that declares it to the build inputs" in policy.blockers + ) + + +def test_writable_callback_scalars_use_rank_zero_storage_without_synthesizing_intent(): + """Every dummy the callee may write is projected as writable storage. + + Python has no writable scalar, so a dummy the native caller reads back must + reach the callable as rank-zero storage. An undeclared ``intent`` is + conservatively writable because Fortran permits the callee to modify it, + and the declaration keeps no intent of its own either way. + """ + module = _source_semantic_module("fcallback_all_f90.f90", module_name="fcallback_all_f90") + function = next(item for item in module.functions if item.name == "apply_scalar_storage_callback") + policy = completed_function_wrapper_policy(function) + transfers = policy.arguments[0].callback.arguments + + assert [transfer.intent for transfer in transfers] == ["inout", "out", None] + assert [transfer.python_action for transfer in transfers] == [PythonBarrierAction.SCALAR_STORAGE] * 3 + assert [transfer.adapter_action for transfer in transfers] == [ + CallbackTransferAction.COPY_IN_OUT, + CallbackTransferAction.COPY_OUT, + CallbackTransferAction.COPY_IN_OUT, + ] + assert policy.supported is True + + +def test_assume_intent_in_scalars_elects_the_input_only_default_for_an_undeclared_intent(): + """The flag chooses which default an undeclared ``intent`` receives. + + It narrows the conservative read/write default to input-only; it does not + give the dummy a declared direction, so the contract still carries none. + """ + module = _source_semantic_module( + "fcallback_all_f90.f90", + module_name="fcallback_all_f90", + assume_intent_in_scalars=True, + ) + function = next(item for item in module.functions if item.name == "apply_scalar_storage_callback") + transfers = completed_function_wrapper_policy(function).arguments[0].callback.arguments + + undeclared = transfers[2] + assert undeclared.intent is None + assert undeclared.python_action is PythonBarrierAction.SCALAR_VALUE + assert undeclared.adapter_action is CallbackTransferAction.COPY_IN + + +@pytest.mark.parametrize( + ("prototype", "blocker"), + [ + ( + "def callback_shape(value: Out(Addr(Float64))) -> None: ...", + "callback argument 'value' is intent(out) and cannot use the value spelling " + "Addr(Float64); use Float64[()] for writable storage", + ), + ( + "def callback_shape(value: InOut(Addr(Int32))) -> None: ...", + "callback argument 'value' is intent(inout) and cannot use the value spelling " + "Addr(Int32); use Int32[()] for writable storage", + ), + ], +) +def test_value_spelling_is_blocked_for_written_back_callback_scalars(prototype: str, blocker: str): + """An out or inout dummy spelled as a value would silently discard the write.""" + module = parse_pyi_text( + f""" +@prototype +{prototype} + +def apply(callback: callback_shape) -> None: ... +""", + module_name="discarded_callback_writeback", + ) + + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert policy.supported is False + assert blocker in policy.blockers + + +def test_read_only_callback_scalars_keep_the_value_spelling(): + """An in dummy is never read back, so the value projection stays valid.""" + module = parse_pyi_text( + """ +@prototype +def callback_shape(value: In(Addr(Float64))) -> None: ... + +def apply(callback: callback_shape) -> None: ... +""", + module_name="read_only_callback_scalar", + ) + + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert policy.supported is True + assert policy.arguments[0].callback.arguments[0].python_action is PythonBarrierAction.SCALAR_VALUE + + +def test_imported_interface_keeps_its_declaring_module_in_the_completed_identity(): + """A type an imported interface owns must not be attributed to the consumer. + + The consuming module never imports ``point_t``, so an identity taken from + the consuming scope names a type that module does not define and no wrapper + definition can satisfy it. + """ + sources = { + "callback_types.f90": """ +module callback_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + subroutine move_point(p) + import :: point_t + implicit none + type(point_t), intent(inout) :: p + end subroutine move_point + end interface +end module callback_types +""", + "consumer.f90": """ +module consumer + use callback_types, only : move_point + implicit none +contains + subroutine run(f) + procedure(move_point) :: f + end subroutine run +end module consumer +""", + } + parsed = parse_fortran_project(sources) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name="merged") + complete_semantic_policies(module) + + function = next(item for item in module.functions if item.name == "run") + policy = completed_function_wrapper_policy(function) + + assert policy.supported is True + assert policy.arguments[0].callback.arguments[0].derived_type_identity == ("callback_types", "point_t") + + +def test_imported_interface_result_keeps_its_declaring_module_in_the_completed_identity(): + """A callback result's type identity must name the module that declares it.""" + sources = { + "callback_types.f90": """ +module callback_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + function make_point(x) result(p) + import :: point_t + implicit none + real(8), intent(in) :: x + type(point_t) :: p + end function make_point + end interface +end module callback_types +""", + "consumer.f90": """ +module consumer + use callback_types, only : make_point + implicit none +contains + subroutine run(f) + procedure(make_point) :: f + end subroutine run +end module consumer +""", + } + parsed = parse_fortran_project(sources) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name="merged") + complete_semantic_policies(module) + + policy = completed_function_wrapper_policy(next(item for item in module.functions if item.name == "run")) + + assert policy.supported is True + assert policy.arguments[0].callback.result.transfer.derived_type_identity == ("callback_types", "point_t") diff --git a/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py b/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py index 30de6fe85..3df05bff3 100644 --- a/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py +++ b/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py @@ -3,6 +3,11 @@ from prik.parsers.fortran import parse_fortran_project from prik.printers import emit_module from prik.semantics.fortran2ir import FortranToIRConverter +from prik.semantics.models import ( + EXTERNAL_TYPE_REF_METADATA, + PROTOTYPE_REF_METADATA, + UNRESOLVED_PROCEDURE_INTERFACE_METADATA, +) from prik.semantics.native_contract import native_contract_issues from tests.fortran._support.semantic_conversion import get_function from prik.parsers.fortran import parse_fortran_file as parse_fortran_source @@ -118,7 +123,9 @@ def test_dummy_procedure_interfaces_become_complete_callable_contracts(): assert "callback: transform_iface" in emitted assert "@prototype\ndef value_iface(" in emitted assert "value: In(Int32)" in emitted - assert "ref: Addr(Float64)" in emitted + # A dummy with no declared intent keeps that absence in the contract while + # carrying storage the callee may write through. + assert "ref: Float64[()]" in emitted assert "@prototype\ndef string_iface(" in emitted assert "read_label: In(String[8])" in emitted assert native_contract_issues(parse_pyi_text(emitted, module_name=module.name)) == [] @@ -194,3 +201,368 @@ def test_duplicate_interface_signatures_emit_one_named_callback_prototype(): module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) assert [prototype.name for prototype in module.prototypes] == ["callback"] + + +def test_imported_abstract_interface_resolves_across_files_and_keeps_its_declared_name(): + """A `procedure(OBJ)` dummy resolves against the module that declares OBJ. + + The interface name reaches the generated contract as a public symbol, so + the declaration keeps the spelling the interface was declared with rather + than the casefolded key used to match it. + """ + interface_source = """ +module pintrf_mod + implicit none + private + public :: OBJ + + abstract interface + subroutine OBJ(x, f) + implicit none + real(8), intent(in) :: x(:) + real(8), intent(out) :: f + end subroutine OBJ + end interface +end module pintrf_mod +""" + solver_source = """ +module solver_mod + use, non_intrinsic :: pintrf_mod, only : OBJ + implicit none +contains + subroutine minimize(calfun, x, f) + procedure(OBJ) :: calfun + real(8), intent(in) :: x(:) + real(8), intent(out) :: f + call calfun(x, f) + end subroutine minimize +end module solver_mod +""" + project = parse_fortran_project({"pintrf.f90": interface_source, "solver.f90": solver_source}) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["solver_mod"], "minimize").arguments[0].semantic_type + assert callback.name == "OBJ" + assert callback.storage is not None and callback.storage.kind == "callback" + assert [argument.name for argument in callback.metadata["callback_arguments"]] == ["x", "f"] + assert callback.metadata["arguments"][0].shape == ["::"] + assert callback.metadata["return"].name == "None" + + +def test_named_but_undeclared_procedure_interface_is_recorded_for_diagnosis(): + """An unresolved `procedure(OBJ)` keeps the name so later stages can report it.""" + source = """ +module solver_mod + use, non_intrinsic :: pintrf_mod, only : OBJ + implicit none +contains + subroutine minimize(calfun, x) + procedure(OBJ) :: calfun + real(8), intent(in) :: x + end subroutine minimize +end module solver_mod +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + + callback = get_function(module, "minimize").arguments[0].semantic_type + assert callback.metadata[UNRESOLVED_PROCEDURE_INTERFACE_METADATA] == "OBJ" + + +CALLBACK_TYPES_SOURCE = """ +module callback_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + subroutine move_point(p) + import :: point_t + implicit none + type(point_t), intent(inout) :: p + end subroutine move_point + end interface +end module callback_types +""" + + +def test_imported_interface_resolves_its_types_in_the_declaring_module(): + """An interface body is written in the scope of the module that declares it. + + The consuming module need not import the types the interface names, so + those types must keep the declaring module's identity rather than being + attributed to whichever module imported the interface. + """ + consumer_source = """ +module consumer + use callback_types, only : move_point + implicit none +contains + subroutine run(f) + procedure(move_point) :: f + end subroutine run +end module consumer +""" + project = parse_fortran_project({"callback_types.f90": CALLBACK_TYPES_SOURCE, "consumer.f90": consumer_source}) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["consumer"], "run").arguments[0].semantic_type + point = callback.metadata["callback_arguments"][0].semantic_type + assert point.name == "point_t" + assert point.metadata[EXTERNAL_TYPE_REF_METADATA]["origin_module"] == "callback_types" + + +def test_procedure_local_use_resolves_an_imported_interface(): + """A ``use`` inside one procedure names the interface only in that scope.""" + source = """ +module proclocal_mod + implicit none +contains + subroutine run_local(callback) + use callback_types, only : move_point + implicit none + procedure(move_point) :: callback + end subroutine run_local +end module proclocal_mod +""" + project = parse_fortran_project({"callback_types.f90": CALLBACK_TYPES_SOURCE, "users.f90": source}) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["proclocal_mod"], "run_local").arguments[0].semantic_type + assert callback.name == "move_point" + assert callback.storage is not None and callback.storage.kind == "callback" + + +def test_reexported_interface_resolves_through_every_import_hop(): + """An interface published by a re-exporting module resolves to its declarer.""" + reexport_source = """ +module reexport_mod + use callback_types, only : move_point + implicit none + public :: move_point +end module reexport_mod +""" + chain_source = """ +module chain_mod + use reexport_mod, only : move_point + implicit none +contains + subroutine run_chain(callback) + procedure(move_point) :: callback + end subroutine run_chain +end module chain_mod +""" + project = parse_fortran_project( + { + "callback_types.f90": CALLBACK_TYPES_SOURCE, + "reexport.f90": reexport_source, + "chain.f90": chain_source, + } + ) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["chain_mod"], "run_chain").arguments[0].semantic_type + assert callback.name == "move_point" + assert callback.storage is not None and callback.storage.kind == "callback" + point = callback.metadata["callback_arguments"][0].semantic_type + assert point.metadata[EXTERNAL_TYPE_REF_METADATA]["origin_module"] == "callback_types" + + +MAKE_POINT_SOURCE = """ +module callback_types + implicit none + type :: point_t + real(8) :: x + end type point_t + + abstract interface + function make_point(x) result(p) + import :: point_t + implicit none + real(8), intent(in) :: x + type(point_t) :: p + end function make_point + end interface +end module callback_types +""" + + +def test_imported_interface_result_keeps_the_declaring_module(): + """A callback result carries the declaring module's types like a dummy does. + + Ownership was recorded only while iterating dummies, so a function + interface returning a module-owned type attributed it to the consumer. + """ + consumer_source = """ +module consumer + use callback_types, only : make_point + implicit none +contains + subroutine run(f) + procedure(make_point) :: f + end subroutine run +end module consumer +""" + project = parse_fortran_project({"callback_types.f90": MAKE_POINT_SOURCE, "consumer.f90": consumer_source}) + modules = {module.name: module for module in FortranToIRConverter().visit(project)} + + callback = get_function(modules["consumer"], "run").arguments[0].semantic_type + result = callback.metadata["return"] + assert result.name == "point_t" + assert result.metadata[EXTERNAL_TYPE_REF_METADATA]["origin_module"] == "callback_types" + + +def test_procedure_local_rename_keeps_both_the_declared_and_local_names(): + """A renamed import binds a new name without changing the declared one. + + The contract must import the declaring name under the local alias, which + requires keeping the two spellings apart as separate source facts. + """ + source = """ +module ren_types + implicit none + abstract interface + subroutine OBJ(x) + implicit none + real(8), intent(in) :: x + end subroutine OBJ + end interface +end module ren_types + +module ren_consumer + implicit none +contains + subroutine run_ren(callback) + use ren_types, only : LOCAL_OBJ => OBJ + implicit none + procedure(LOCAL_OBJ) :: callback + end subroutine run_ren +end module ren_consumer +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source))[1] + + callback = get_function(module, "run_ren").arguments[0].semantic_type + assert callback.name == "LOCAL_OBJ" + assert callback.metadata[PROTOTYPE_REF_METADATA] == { + "name": "OBJ", + "local_name": "LOCAL_OBJ", + "origin_module": "ren_types", + } + + +def test_interface_reference_uses_the_declared_spelling(): + """Fortran matches names case-insensitively; Python contracts do not. + + A reference spelled in another case is the same interface, so the contract + keeps the declared spelling instead of binding a second name. + """ + source = """ +module cas_mod + implicit none + abstract interface + subroutine OBJ(x) + implicit none + real(8), intent(in) :: x + end subroutine OBJ + end interface +contains + subroutine run_cas(callback) + procedure(obj) :: callback + end subroutine run_cas +end module cas_mod +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + + assert get_function(module, "run_cas").arguments[0].semantic_type.name == "OBJ" + + +ACCESSIBILITY_SOURCE = """ +module acc_a + implicit none + abstract interface + subroutine OBJ(x) + implicit none + real(8), intent(in) :: x + end subroutine OBJ + end interface +end module acc_a + +module acc_b_public + use acc_a, only : OBJ + implicit none + private + public :: OBJ +end module acc_b_public + +module acc_b_private + use acc_a, only : OBJ + implicit none + private +end module acc_b_private + +module acc_ok + use acc_b_public, only : OBJ + implicit none +contains + subroutine run_ok(callback) + procedure(OBJ) :: callback + end subroutine run_ok +end module acc_ok + +module acc_bad + use acc_b_private, only : OBJ + implicit none +contains + subroutine run_bad(callback) + procedure(OBJ) :: callback + end subroutine run_bad +end module acc_bad +""" + + +def _is_resolved_callback(module, function_name: str) -> bool: + semantic_type = get_function(module, function_name).arguments[0].semantic_type + return semantic_type.storage is not None and semantic_type.storage.kind == "callback" + + +def test_reexported_interface_resolves_only_when_the_module_publishes_it(): + """Following a re-export must respect the module's own accessibility. + + A name a module imports privately is not part of its interface, so reaching + it through ``use`` must not resolve even though the chain exists. + """ + modules = { + module.name: module for module in FortranToIRConverter().visit(parse_fortran_source(ACCESSIBILITY_SOURCE)) + } + + assert _is_resolved_callback(modules["acc_ok"], "run_ok") + assert not _is_resolved_callback(modules["acc_bad"], "run_bad") + + +def test_accessibility_is_enforced_at_every_re_export_hop(): + """A private hop anywhere in the chain stops the name from travelling.""" + source = ( + ACCESSIBILITY_SOURCE + + """ +module acc_mid + use acc_b_public, only : OBJ + implicit none + private +end module acc_mid + +module acc_far + use acc_mid, only : OBJ + implicit none +contains + subroutine run_far(callback) + procedure(OBJ) :: callback + end subroutine run_far +end module acc_far +""" + ) + modules = {module.name: module for module in FortranToIRConverter().visit(parse_fortran_source(source))} + + assert not _is_resolved_callback(modules["acc_far"], "run_far") diff --git a/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py b/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py index a32446cc8..4da816d3e 100644 --- a/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py +++ b/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py @@ -1,7 +1,9 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" import pytest +from prik.pipeline.pyi import pyi_paths_to_semantic_modules from prik.policy.completion import complete_semantic_policies +from prik.semantics.models import PROTOTYPE_REF_METADATA from tests.fortran._support.pyi_conversion import parse_pyi_text @@ -215,3 +217,31 @@ def test_convert_pyi_to_ir_rejects_redundant_or_invalid_prototype_value_wrappers f"@prototype\ndef callback(value: {annotation}) -> None: ...", module_name="callbacks", ) + + +def test_renamed_reexport_chain_resolves_to_the_declaring_name(tmp_path): + """A reference follows both module and symbol provenance to the declaration. + + Each hop of a renaming chain binds a new alias, and only the module that + declares the prototype knows the name it declared. Recording an alias from + somewhere along the chain would name a symbol the declaring module does not + define. + """ + for name, text in ( + ( + "mod_a.pyi", + "from prik.contracts import Float64, In, prototype\n\n@prototype\ndef OBJ(x: In(Float64)) -> None: ...\n", + ), + ("mod_b.pyi", "from mod_a import OBJ as MID\n"), + ("mod_c.pyi", "from mod_b import MID as LOCAL\n\ndef run(callback: LOCAL) -> None: ...\n"), + ): + (tmp_path / name).write_text(text, encoding="utf-8") + + modules = {module.name: module for module in pyi_paths_to_semantic_modules(sorted(tmp_path.glob("*.pyi")))} + + callback = next(item for item in modules["mod_c"].functions if item.name == "run").arguments[0].semantic_type + assert callback.metadata[PROTOTYPE_REF_METADATA] == { + "name": "OBJ", + "local_name": "LOCAL", + "origin_module": "mod_a", + } diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath/__init__.pyi index d7a7d8642..26cc2cc80 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath/__init__.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath/__init__.pyi @@ -1,62 +1,53 @@ -from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call, standalone +from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, native_call, standalone -@bind("SQUARE_R4") @standalone @native_call([Addr(Arg(0))]) def square_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SQUARE_R8") @standalone @native_call([Addr(Arg(0))]) def square_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("SQUARE_I4") @standalone @native_call([Addr(Arg(0))]) def square_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("SQUARE_C4") @standalone @native_call([Addr(Arg(0))]) def square_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... -@bind("SQUARE_C8") @standalone @native_call([Addr(Arg(0))]) def square_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... -@bind("CUBE_R4") @standalone @native_call([Addr(Arg(0))]) def cube_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("CUBE_R8") @standalone @native_call([Addr(Arg(0))]) def cube_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("CUBE_I4") @standalone @native_call([Addr(Arg(0))]) def cube_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("ADD_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r4( @@ -64,7 +55,6 @@ def add_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("ADD_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r8( @@ -72,7 +62,6 @@ def add_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("ADD_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_i4( @@ -80,7 +69,6 @@ def add_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("ADD_C4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c4( @@ -88,7 +76,6 @@ def add_c4( Y: Complex64 ) -> tuple[Complex64, Returns["X", Complex64], Returns["Y", Complex64]]: ... -@bind("ADD_C8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c8( @@ -96,7 +83,6 @@ def add_c8( Y: Complex128 ) -> tuple[Complex128, Returns["X", Complex128], Returns["Y", Complex128]]: ... -@bind("SUB_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r4( @@ -104,7 +90,6 @@ def sub_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("SUB_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r8( @@ -112,7 +97,6 @@ def sub_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("SUB_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_i4( @@ -120,7 +104,6 @@ def sub_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MUL_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r4( @@ -128,7 +111,6 @@ def mul_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MUL_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r8( @@ -136,7 +118,6 @@ def mul_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MUL_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_i4( @@ -144,7 +125,6 @@ def mul_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("DIV_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r4( @@ -152,7 +132,6 @@ def div_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("DIV_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r8( @@ -160,7 +139,6 @@ def div_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("POW_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r4( @@ -168,7 +146,6 @@ def pow_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("POW_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r8( @@ -176,133 +153,114 @@ def pow_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("ABS_R4") @standalone @native_call([Addr(Arg(0))]) def abs_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ABS_R8") @standalone @native_call([Addr(Arg(0))]) def abs_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ABS_I4") @standalone @native_call([Addr(Arg(0))]) def abs_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("NEG_R4") @standalone @native_call([Addr(Arg(0))]) def neg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("NEG_R8") @standalone @native_call([Addr(Arg(0))]) def neg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("NEG_I4") @standalone @native_call([Addr(Arg(0))]) def neg_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("SIN_R4") @standalone @native_call([Addr(Arg(0))]) def sin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SIN_R8") @standalone @native_call([Addr(Arg(0))]) def sin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("COS_R4") @standalone @native_call([Addr(Arg(0))]) def cos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("COS_R8") @standalone @native_call([Addr(Arg(0))]) def cos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("TAN_R4") @standalone @native_call([Addr(Arg(0))]) def tan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("TAN_R8") @standalone @native_call([Addr(Arg(0))]) def tan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ASIN_R4") @standalone @native_call([Addr(Arg(0))]) def asin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ASIN_R8") @standalone @native_call([Addr(Arg(0))]) def asin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ACOS_R4") @standalone @native_call([Addr(Arg(0))]) def acos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ACOS_R8") @standalone @native_call([Addr(Arg(0))]) def acos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ATAN_R4") @standalone @native_call([Addr(Arg(0))]) def atan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ATAN_R8") @standalone @native_call([Addr(Arg(0))]) def atan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ATAN2_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r4( @@ -310,7 +268,6 @@ def atan2_r4( X: Float32 ) -> tuple[Float32, Returns["Y", Float32], Returns["X", Float32]]: ... -@bind("ATAN2_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r8( @@ -318,63 +275,54 @@ def atan2_r8( X: Float64 ) -> tuple[Float64, Returns["Y", Float64], Returns["X", Float64]]: ... -@bind("EXP_R4") @standalone @native_call([Addr(Arg(0))]) def exp_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("EXP_R8") @standalone @native_call([Addr(Arg(0))]) def exp_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("LOG_R4") @standalone @native_call([Addr(Arg(0))]) def log_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("LOG_R8") @standalone @native_call([Addr(Arg(0))]) def log_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("LOG10_R4") @standalone @native_call([Addr(Arg(0))]) def log10_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("LOG10_R8") @standalone @native_call([Addr(Arg(0))]) def log10_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("SQRT_R4") @standalone @native_call([Addr(Arg(0))]) def sqrt_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SQRT_R8") @standalone @native_call([Addr(Arg(0))]) def sqrt_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("HYPOT_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r4( @@ -382,7 +330,6 @@ def hypot_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("HYPOT_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r8( @@ -390,7 +337,6 @@ def hypot_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MIN_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r4( @@ -398,7 +344,6 @@ def min_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MIN_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r8( @@ -406,7 +351,6 @@ def min_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MIN_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_i4( @@ -414,7 +358,6 @@ def min_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MAX_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r4( @@ -422,7 +365,6 @@ def max_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MAX_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r8( @@ -430,7 +372,6 @@ def max_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MAX_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_i4( @@ -438,7 +379,6 @@ def max_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("SIGN_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r4( @@ -446,7 +386,6 @@ def sign_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("SIGN_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r8( @@ -454,7 +393,6 @@ def sign_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MOD_I4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_i4( @@ -462,7 +400,6 @@ def mod_i4( Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MOD_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r4( @@ -470,7 +407,6 @@ def mod_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MOD_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r8( @@ -478,35 +414,30 @@ def mod_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("DEG2RAD_R4") @standalone @native_call([Addr(Arg(0))]) def deg2rad_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("DEG2RAD_R8") @standalone @native_call([Addr(Arg(0))]) def deg2rad_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("RAD2DEG_R4") @standalone @native_call([Addr(Arg(0))]) def rad2deg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("RAD2DEG_R8") @standalone @native_call([Addr(Arg(0))]) def rad2deg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("DIST2_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r4( @@ -514,7 +445,6 @@ def dist2_r4( Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("DIST2_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r8( @@ -522,7 +452,6 @@ def dist2_r8( Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("DOT2_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r4( @@ -532,7 +461,6 @@ def dot2_r4( Y2: Float32 ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["Y1", Float32], Returns["Y2", Float32]]: ... -@bind("DOT2_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r8( @@ -542,7 +470,6 @@ def dot2_r8( Y2: Float64 ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["Y1", Float64], Returns["Y2", Float64]]: ... -@bind("DOT3_R4") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5))]) def dot3_r4( @@ -554,7 +481,6 @@ def dot3_r4( Y3: Float32 ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["X3", Float32], Returns["Y1", Float32], Returns["Y2", Float32], Returns["Y3", Float32]]: ... -@bind("DOT3_R8") @standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5))]) def dot3_r8( @@ -566,77 +492,66 @@ def dot3_r8( Y3: Float64 ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["X3", Float64], Returns["Y1", Float64], Returns["Y2", Float64], Returns["Y3", Float64]]: ... -@bind("CONJ_C4") @standalone @native_call([Addr(Arg(0))]) def conj_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... -@bind("CONJ_C8") @standalone @native_call([Addr(Arg(0))]) def conj_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... -@bind("REAL_C4") @standalone @native_call([Addr(Arg(0))]) def real_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("REAL_C8") @standalone @native_call([Addr(Arg(0))]) def real_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("AIMAG_C4") @standalone @native_call([Addr(Arg(0))]) def aimag_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("AIMAG_C8") @standalone @native_call([Addr(Arg(0))]) def aimag_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("ABS_C4") @standalone @native_call([Addr(Arg(0))]) def abs_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("ABS_C8") @standalone @native_call([Addr(Arg(0))]) def abs_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("IS_POSITIVE_R4") @standalone @native_call([Addr(Arg(0))]) def is_positive_r4( X: Float32 ) -> tuple[Bool32, Returns["X", Float32]]: ... -@bind("IS_POSITIVE_R8") @standalone @native_call([Addr(Arg(0))]) def is_positive_r8( X: Float64 ) -> tuple[Bool32, Returns["X", Float64]]: ... -@bind("IS_EVEN_I4") @standalone @native_call([Addr(Arg(0))]) def is_even_i4( diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/fmath_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/fmath_f90.pyi index 6b8daf07d..8cd058552 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/fmath_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fmath_f90/fmath_f90.pyi @@ -1,458 +1,387 @@ -from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call +from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, native_call -@bind("SQUARE_R4") @native_call([Addr(Arg(0))]) def square_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SQUARE_R8") @native_call([Addr(Arg(0))]) def square_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("SQUARE_I4") @native_call([Addr(Arg(0))]) def square_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("SQUARE_C4") @native_call([Addr(Arg(0))]) def square_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... -@bind("SQUARE_C8") @native_call([Addr(Arg(0))]) def square_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... -@bind("CUBE_R4") @native_call([Addr(Arg(0))]) def cube_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("CUBE_R8") @native_call([Addr(Arg(0))]) def cube_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("CUBE_I4") @native_call([Addr(Arg(0))]) def cube_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("ADD_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("ADD_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("ADD_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("ADD_C4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c4( X: Complex64, Y: Complex64 ) -> tuple[Complex64, Returns["X", Complex64], Returns["Y", Complex64]]: ... -@bind("ADD_C8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c8( X: Complex128, Y: Complex128 ) -> tuple[Complex128, Returns["X", Complex128], Returns["Y", Complex128]]: ... -@bind("SUB_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("SUB_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("SUB_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MUL_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MUL_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MUL_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("DIV_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("DIV_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("POW_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("POW_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("ABS_R4") @native_call([Addr(Arg(0))]) def abs_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ABS_R8") @native_call([Addr(Arg(0))]) def abs_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ABS_I4") @native_call([Addr(Arg(0))]) def abs_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("NEG_R4") @native_call([Addr(Arg(0))]) def neg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("NEG_R8") @native_call([Addr(Arg(0))]) def neg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("NEG_I4") @native_call([Addr(Arg(0))]) def neg_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... -@bind("SIN_R4") @native_call([Addr(Arg(0))]) def sin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SIN_R8") @native_call([Addr(Arg(0))]) def sin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("COS_R4") @native_call([Addr(Arg(0))]) def cos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("COS_R8") @native_call([Addr(Arg(0))]) def cos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("TAN_R4") @native_call([Addr(Arg(0))]) def tan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("TAN_R8") @native_call([Addr(Arg(0))]) def tan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ASIN_R4") @native_call([Addr(Arg(0))]) def asin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ASIN_R8") @native_call([Addr(Arg(0))]) def asin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ACOS_R4") @native_call([Addr(Arg(0))]) def acos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ACOS_R8") @native_call([Addr(Arg(0))]) def acos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ATAN_R4") @native_call([Addr(Arg(0))]) def atan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("ATAN_R8") @native_call([Addr(Arg(0))]) def atan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("ATAN2_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r4( Y: Float32, X: Float32 ) -> tuple[Float32, Returns["Y", Float32], Returns["X", Float32]]: ... -@bind("ATAN2_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r8( Y: Float64, X: Float64 ) -> tuple[Float64, Returns["Y", Float64], Returns["X", Float64]]: ... -@bind("EXP_R4") @native_call([Addr(Arg(0))]) def exp_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("EXP_R8") @native_call([Addr(Arg(0))]) def exp_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("LOG_R4") @native_call([Addr(Arg(0))]) def log_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("LOG_R8") @native_call([Addr(Arg(0))]) def log_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("LOG10_R4") @native_call([Addr(Arg(0))]) def log10_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("LOG10_R8") @native_call([Addr(Arg(0))]) def log10_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("SQRT_R4") @native_call([Addr(Arg(0))]) def sqrt_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("SQRT_R8") @native_call([Addr(Arg(0))]) def sqrt_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("HYPOT_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("HYPOT_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MIN_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MIN_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MIN_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MAX_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MAX_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MAX_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("SIGN_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("SIGN_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("MOD_I4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_i4( X: Int32, Y: Int32 ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... -@bind("MOD_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("MOD_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("DEG2RAD_R4") @native_call([Addr(Arg(0))]) def deg2rad_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("DEG2RAD_R8") @native_call([Addr(Arg(0))]) def deg2rad_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("RAD2DEG_R4") @native_call([Addr(Arg(0))]) def rad2deg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... -@bind("RAD2DEG_R8") @native_call([Addr(Arg(0))]) def rad2deg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... -@bind("DIST2_R4") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r4( X: Float32, Y: Float32 ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... -@bind("DIST2_R8") @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r8( X: Float64, Y: Float64 ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... -@bind("DOT2_R4") @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r4( X1: Float32, @@ -461,7 +390,6 @@ def dot2_r4( Y2: Float32 ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["Y1", Float32], Returns["Y2", Float32]]: ... -@bind("DOT2_R8") @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r8( X1: Float64, @@ -470,7 +398,6 @@ def dot2_r8( Y2: Float64 ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["Y1", Float64], Returns["Y2", Float64]]: ... -@bind("DOT3_R4") @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5))]) def dot3_r4( X1: Float32, @@ -481,7 +408,6 @@ def dot3_r4( Y3: Float32 ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["X3", Float32], Returns["Y1", Float32], Returns["Y2", Float32], Returns["Y3", Float32]]: ... -@bind("DOT3_R8") @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5))]) def dot3_r8( X1: Float64, @@ -492,67 +418,56 @@ def dot3_r8( Y3: Float64 ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["X3", Float64], Returns["Y1", Float64], Returns["Y2", Float64], Returns["Y3", Float64]]: ... -@bind("CONJ_C4") @native_call([Addr(Arg(0))]) def conj_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... -@bind("CONJ_C8") @native_call([Addr(Arg(0))]) def conj_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... -@bind("REAL_C4") @native_call([Addr(Arg(0))]) def real_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("REAL_C8") @native_call([Addr(Arg(0))]) def real_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("AIMAG_C4") @native_call([Addr(Arg(0))]) def aimag_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("AIMAG_C8") @native_call([Addr(Arg(0))]) def aimag_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("ABS_C4") @native_call([Addr(Arg(0))]) def abs_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... -@bind("ABS_C8") @native_call([Addr(Arg(0))]) def abs_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... -@bind("IS_POSITIVE_R4") @native_call([Addr(Arg(0))]) def is_positive_r4( X: Float32 ) -> tuple[Bool32, Returns["X", Float32]]: ... -@bind("IS_POSITIVE_R8") @native_call([Addr(Arg(0))]) def is_positive_r8( X: Float64 ) -> tuple[Bool32, Returns["X", Float64]]: ... -@bind("IS_EVEN_I4") @native_call([Addr(Arg(0))]) def is_even_i4( X: Int32 diff --git a/tests/fortran/data_types/end_to_end/test_project_kind_alias_chain.py b/tests/fortran/data_types/end_to_end/test_project_kind_alias_chain.py new file mode 100644 index 000000000..86189e2f9 --- /dev/null +++ b/tests/fortran/data_types/end_to_end/test_project_kind_alias_chain.py @@ -0,0 +1,106 @@ +"""Project kind aliases resolve to intrinsics before any compiler probe runs.""" + +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import, _compiler, _import_from_build_dir +from prik import build_pyi_extension +from prik.parsers.fortran import parse_fortran_file +from prik.semantics.fortran2ir import collect_fortran_type_storage_requirements + +pytestmark = pytest.mark.fortran_end_to_end + +KIND_ALIAS_SOURCE = """ +module consts_mod + use iso_fortran_env, only : REAL64 + use iso_fortran_env, only : INT32 + implicit none + + integer, parameter :: DP = REAL64 + integer, parameter :: IK_DFT = INT32 + integer, parameter :: RP = DP + integer, parameter :: IK = IK_DFT +end module consts_mod + +module consumer_mod + use consts_mod, only : RP, IK + implicit none +contains + subroutine work(x, n) + real(RP), intent(inout) :: x + integer(IK), intent(in) :: n + x = x * real(n, RP) + end subroutine work +end module consumer_mod +""" + + +def test_kind_alias_chain_reaches_the_probe_as_intrinsic_expressions(tmp_path: Path): + """A project names its kinds through its own parameters, and they resolve. + + Each `use` of one module adds to what the scope imported, and a parameter + may name another, so `RP` reaches `REAL64` through `DP`. The probe measures + target storage and is given expressions a compiler understands, never a + project name it has no way to evaluate. + """ + source = tmp_path / "kinds.f90" + source.write_text(KIND_ALIAS_SOURCE, encoding="utf-8") + + parsed = parse_fortran_file(source) + consumer = next(module for module in parsed.modules if module.name == "consumer_mod") + assert [(argument.name, argument.kind) for argument in consumer.procedures[0].arguments] == [ + ("x", "REAL64"), + ("n", "INT32"), + ] + assert all( + "RP" not in str(requirement["expression"]) and "IK" not in str(requirement["expression"]) + for requirement in collect_fortran_type_storage_requirements(parsed) + ) + + module = _build_source_and_import( + source, + tmp_path / "source_build", + {"bind_c_kinds_wrapper.f90", "kinds_wrapper.c", "kinds_wrapper.h"}, + ) + assert module.consumer_mod.work(np.float64(2.5), np.int32(4)) == pytest.approx(10.0) + + +def test_kind_alias_chain_survives_its_generated_contract(tmp_path: Path): + """The contract states resolved types, and rebuilding keeps the behavior.""" + source = tmp_path / "kinds.f90" + source.write_text(KIND_ALIAS_SOURCE, encoding="utf-8") + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + str(source), + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + + contract = (contracts / "consumer_mod.pyi").read_text(encoding="utf-8") + assert "x: Float64" in contract + assert "n: Int32" in contract + + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(source)], + output_dir=tmp_path / "contract_build", + output_name="kinds_contract", + ) + rebuilt = _import_from_build_dir(result.module_name, result.output_dir) + assert rebuilt.consumer_mod.work(np.float64(2.5), np.int32(4)) == pytest.approx(10.0) diff --git a/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py b/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py index 1add7b3ef..bed918970 100644 --- a/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py +++ b/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py @@ -203,3 +203,40 @@ def test_legacy_fortran_storage_uses_fixed_star_widths_and_probes_double_types() ("real", "kind(1.0d0)", "storage_size(real(0.0,kind=kind(1.0d0)))"), ("complex", "kind(1.0d0)", "storage_size(cmplx(0.0,kind=kind(1.0d0)))"), } + + +def test_interface_body_dummies_require_target_storage_facts(): + """An interface body's dummies reach the target probe like any other variable. + + An abstract interface names its own kinds, often through a ``use`` written + inside the body, and no module variable or module procedure declares them. + Collecting nothing for such a body leaves the conversion without the storage + fact it later demands. + """ + source = """ +module callback_mod + implicit none + private + public :: reporter + abstract interface + subroutine reporter(x, nf) + use kind_mod, only : rp, ik + implicit none + real(rp), intent(in) :: x + integer(ik), intent(in) :: nf + end subroutine reporter + end interface +end module callback_mod +""" + + parsed = parse_fortran_source(source) + + requirements = collect_fortran_type_storage_requirements( + parsed, + compile_time_values={"rp": "kind(0.0d0)", "ik": "kind(0)"}, + ) + + assert [requirement["expression"] for requirement in requirements] == [ + "storage_size(real(0.0,kind=kind(0.0d0)))", + "storage_size(int(0,kind=kind(0)))", + ] diff --git a/tests/fortran/data_types/semantics/test_types_and_storage.py b/tests/fortran/data_types/semantics/test_types_and_storage.py index 893b27c7b..054f1f10c 100644 --- a/tests/fortran/data_types/semantics/test_types_and_storage.py +++ b/tests/fortran/data_types/semantics/test_types_and_storage.py @@ -157,7 +157,7 @@ def test_fortran_native_storage_contracts_cover_array_categories_and_scalars(): assumed = array_contract(args["assumed"].semantic_type) assert assumed.category == "assumed_shape" - assert assumed.shape == ["::Strided", "::Strided"] + assert assumed.shape == ["::", "::"] assert assumed.order == "ORDER_F" contig = array_contract(args["contig"].semantic_type) @@ -268,7 +268,7 @@ def test_fortran_native_storage_contracts_preserve_exact_bounds_and_member_flags assert semantic_member.semantic_type.storage.array.pointer is True assert plain_member.optional is False assert plain_member.visibility == "public" - assert plain_member.semantic_type.storage.array.shape == ["::Strided"] + assert plain_member.semantic_type.storage.array.shape == ["::"] assert plain_member.semantic_type.storage.array.allocatable is False assert plain_member.semantic_type.storage.array.pointer is False assert plain_member.origin.source_language == "fortran" diff --git a/tests/fortran/functions/end_to_end/test_bind_c_label_case.py b/tests/fortran/functions/end_to_end/test_bind_c_label_case.py new file mode 100644 index 000000000..ba15aa520 --- /dev/null +++ b/tests/fortran/functions/end_to_end/test_bind_c_label_case.py @@ -0,0 +1,75 @@ +"""A `bind(C)` label is an external symbol, not a Fortran identifier.""" + +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import, _compiler, _import_from_build_dir +from prik import build_pyi_extension + +pytestmark = pytest.mark.fortran_end_to_end + +BIND_C_LABEL_SOURCE = """ +module label_mod + use iso_c_binding, only : c_int + implicit none +contains + subroutine scale(x) bind(C, name="SCALE") + integer(c_int), intent(inout) :: x + x = x * 3 + end subroutine scale +end module label_mod +""" + + +def test_bind_c_label_keeps_its_exact_spelling_through_a_generated_contract(tmp_path: Path): + """A C binding label differing only in case from its procedure survives. + + Fortran names `scale` without regard to case, so nothing about that name + needs recording. The label `SCALE` is a C external symbol instead, which is + spelled exactly, and the wrapper links against it rather than the Fortran + identifier it happens to resemble. + """ + source = tmp_path / "label.f90" + source.write_text(BIND_C_LABEL_SOURCE, encoding="utf-8") + + source_module = _build_source_and_import( + source, + tmp_path / "source_build", + {"label_wrapper.c", "label_wrapper.h"}, + ) + assert source_module.scale(np.int32(5)) == np.int32(15) + + contracts = tmp_path / "contracts" + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + str(source), + "--out", + str(contracts), + "--compiler", + _compiler(), + ], + check=True, + capture_output=True, + ) + contract = (contracts / "label_mod.pyi").read_text(encoding="utf-8") + assert '@bind("SCALE")' in contract + assert "def scale(" in contract + + result = build_pyi_extension( + contracts / "__init__.pyi", + input_compiler=_compiler(), + native_fortran_sources=[str(source)], + output_dir=tmp_path / "contract_build", + output_name="label_contract", + ) + rebuilt = _import_from_build_dir(result.module_name, result.output_dir) + assert rebuilt.label_mod.scale(np.int32(5)) == np.int32(15) diff --git a/tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py b/tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py index de7d6ad26..79d17fe79 100644 --- a/tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py +++ b/tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py @@ -6,6 +6,7 @@ import pytest from tests.fortran._support.wrapper_build import ( + _build_source_and_import, _build_source_or_generated_pyi_and_import, _build_sources_and_import, ) @@ -123,3 +124,63 @@ def test_public_generic_dispatches_to_private_inline_submodule_specifics(tmp_pat assert "native__prik_overload_shift_1 => shift" in bridge assert "=> shift_integer" not in bridge assert "=> shift_real" not in bridge + + +EXTENDED_GENERIC_SOURCE = """ +module gen_base_mod + implicit none + interface report + module procedure report_int + end interface report +contains + subroutine report_int(value, seen) + integer, intent(in) :: value + integer, intent(out) :: seen + seen = value + end subroutine report_int +end module gen_base_mod + +module gen_extended_mod + use gen_base_mod, only : report + implicit none + interface report + module procedure report_real + end interface report +contains + subroutine report_real(value, seen) + real(8), intent(in) :: value + integer, intent(out) :: seen + seen = int(value) * 10 + end subroutine report_real +end module gen_extended_mod +""" + + +def test_generic_extended_across_modules_dispatches_to_every_specific(tmp_path: Path): + """A local interface block extends the generic it imports, not replaces it. + + The extending module resolves both the specific it declares and the one + that reached it through the import, while the declaring module keeps only + its own: a generic accumulates along the `use` chain in one direction. + """ + source = tmp_path / "gen_extended.f90" + source.write_text(EXTENDED_GENERIC_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + { + "bind_c_gen_extended_wrapper.f90", + "gen_extended_wrapper.c", + "gen_extended_wrapper.h", + }, + ) + + assert module.gen_extended_mod.report(np.int32(3)) == np.int32(3) + assert module.gen_extended_mod.report(np.float64(4.0)) == np.int32(40) + assert module.gen_base_mod.report(np.int32(3)) == np.int32(3) + + # The inherited specific is reachable only through the generic, because + # `use gen_base_mod, only : report` never bound its own name. + assert "report_int" not in dir(module.gen_extended_mod) + with pytest.raises(TypeError, match="no matching overload"): + module.gen_base_mod.report(np.float64(4.0)) diff --git a/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py b/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py index a2b909b6a..c3f0898fc 100644 --- a/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py +++ b/tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py @@ -8,6 +8,7 @@ from tests.fortran._support.parser_procedures import ( parse_fortran_interfaces, parse_fortran_module, + parse_fortran_modules, ) from prik.parsers.fortran.models import FortranParseError @@ -105,3 +106,193 @@ def test_assumed_type_generic_candidate_is_rejected_at_parsing(): parse_fortran_file(source, filename="unsupported_generic.f90") assert exc_info.value.code == "PARSE_UNSUPPORTED_DECLARATION" + + +def test_generic_interface_declared_in_several_blocks_becomes_one_generic(): + """Fortran builds one generic from as many blocks as a scope declares. + + Real sources split a generic across preprocessor-guarded blocks, adding + specifics only for the kinds a build supports, so repeated blocks name one + generic rather than redeclaring it. + """ + source = """ +module huge_mod + implicit none + private + public :: huge_value + + interface huge_value + module procedure huge_value_sp, huge_value_dp + end interface huge_value + + interface huge_value + module procedure huge_value_qp + end interface huge_value +contains + real function huge_value_sp(x) + real, intent(in) :: x + huge_value_sp = huge(x) + end function huge_value_sp + real(8) function huge_value_dp(x) + real(8), intent(in) :: x + huge_value_dp = huge(x) + end function huge_value_dp + real(16) function huge_value_qp(x) + real(16), intent(in) :: x + huge_value_qp = huge(x) + end function huge_value_qp +end module huge_mod +""" + + module = parse_fortran_module(source) + + generics = [interface for interface in module.interfaces if interface.name] + assert len(generics) == 1 + assert generics[0].name == "huge_value" + assert generics[0].specific_procedures == ["huge_value_sp", "huge_value_dp", "huge_value_qp"] + + +def test_repeated_generic_names_stay_separate_per_module(): + """Two modules in one file each own their generic of the same name.""" + source = """ +module first_mod + implicit none + interface report + module procedure report_first + end interface report +contains + subroutine report_first() + end subroutine report_first +end module first_mod + +module second_mod + implicit none + interface report + module procedure report_second + end interface report +contains + subroutine report_second() + end subroutine report_second +end module second_mod +""" + + modules = {module.name: module for module in parse_fortran_modules(source)} + + assert [item.specific_procedures for item in modules["first_mod"].interfaces if item.name] == [["report_first"]] + assert [item.specific_procedures for item in modules["second_mod"].interfaces if item.name] == [["report_second"]] + + +def test_type_bound_generic_declared_in_several_statements_becomes_one_binding(): + """A type-bound generic collects specifics from as many statements as it takes. + + A derived type may name one generic binding over several ``generic ::`` + statements, and every statement contributes specifics to that one binding + rather than declaring another of the same name. + """ + source = """ +module shape_mod + implicit none + type :: shape_t + real(8) :: v + contains + procedure :: area_int + procedure :: area_real + generic :: area => area_int + generic :: area => area_real + end type shape_t +contains + real(8) function area_int(self, k) + class(shape_t), intent(in) :: self + integer, intent(in) :: k + area_int = self%v * k + end function area_int + real(8) function area_real(self, k) + class(shape_t), intent(in) :: self + real(8), intent(in) :: k + area_real = self%v * k + end function area_real +end module shape_mod +""" + + module = parse_fortran_module(source) + + assert [binding["name"] for binding in module.derived_types[0].generic_bindings] == ["area"] + assert module.derived_types[0].generic_bindings[0]["targets"] == ["area_int", "area_real"] + + +def test_type_bound_operator_generic_merges_across_statements_and_spacing(): + """One defined operator binding survives being split across statements.""" + source = """ +module vec_mod + implicit none + type :: vec_t + real(8) :: v + contains + procedure :: add_int + procedure :: add_real + generic :: operator(+) => add_int + generic :: operator (+) => add_real + end type vec_t +contains + type(vec_t) function add_int(self, k) + class(vec_t), intent(in) :: self + integer, intent(in) :: k + add_int%v = self%v + k + end function add_int + type(vec_t) function add_real(self, k) + class(vec_t), intent(in) :: self + real(8), intent(in) :: k + add_real%v = self%v + k + end function add_real +end module vec_mod +""" + + module = parse_fortran_module(source) + + assert [binding["name"] for binding in module.derived_types[0].generic_bindings] == ["operator(+)"] + assert module.derived_types[0].generic_bindings[0]["targets"] == ["add_int", "add_real"] + + +def test_same_generic_name_in_two_procedures_declares_two_generics(): + """A generic belongs to the scope declaring it, and procedures are scopes. + + Two procedures of one module may each declare an interface of the same + name, and they name different generics. Merging them on the module they + share would let one procedure's specifics answer the other's calls. + """ + source = """ +module scoped_mod + implicit none +contains + subroutine first(x) + real(8), intent(in) :: x + interface local_generic + subroutine first_impl(a) + real(8), intent(in) :: a + end subroutine first_impl + end interface + call local_generic(x) + end subroutine first + + subroutine second(n) + integer, intent(in) :: n + interface local_generic + subroutine second_impl(b) + integer, intent(in) :: b + end subroutine second_impl + end interface + call local_generic(n) + end subroutine second +end module scoped_mod +""" + + module = parse_fortran_module(source) + + assert [ + (interface.name, [signature.name for signature in interface.procedures]) + for interface in module.interfaces + if interface.name + ] == [ + ("local_generic", ["first_impl"]), + ("local_generic", ["second_impl"]), + ] diff --git a/tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py b/tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py index ad19772eb..1e7f56dbc 100644 --- a/tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py +++ b/tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py @@ -6,6 +6,9 @@ import pytest +from prik.parsers.fortran import parse_fortran_file as parse_fortran_source +from prik.printers import emit_module +from prik.semantics.fortran2ir import fortran_module_to_semantic_module from tests.fortran._support.generated_contracts import ( GeneratedContractCase, assert_generated_contract_matches_fixture, @@ -39,3 +42,35 @@ def test_generated_generic_contract_matches_fixture( tmp_path: Path, ): assert_generated_contract_matches_fixture(case, tmp_path) + + +def test_overload_names_its_specific_as_the_contract_declares_it(): + """An overload target names a declaration this contract holds. + + A specific whose Fortran spelling carries capitals is declared under its + Python name, so the overload naming it is written the same way; the source + spelling would name no declaration in the contract at all. + """ + source = """ +module powalg_mod +implicit none +private +public :: qradd +interface qradd +module procedure qradd_Rdiag +end interface qradd +contains +subroutine qradd_Rdiag(x) +real(8), intent(inout) :: x +end subroutine qradd_Rdiag +end module powalg_mod +""" + + code = emit_module( + fortran_module_to_semantic_module(parse_fortran_source(source)), + normalize_fortran_public_names=True, + ) + + assert "def qradd_rdiag(" in code + assert '@overload("qradd_rdiag")' in code + assert "qradd_Rdiag" not in code diff --git a/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py b/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py index dd1f0b5b1..bf4e15567 100644 --- a/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py +++ b/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py @@ -169,3 +169,44 @@ def test_converter_preserves_defined_operators_assignment_and_type_bound_operato assert [ (item.name, [procedure.name for procedure in item.procedures]) for item in classes["counter"].overload_sets ] == [("__add__", ["counter_add_integer"])] + + +def test_type_bound_generic_split_across_statements_reaches_one_overload_set(): + """Every specific a split generic binding names stays reachable. + + A type-bound generic built from several ``generic ::`` statements means one + binding, so the class carries a single overload set holding every specific + -- not one set per statement, which leaves all but the first unreachable at + dispatch. + """ + source = """ +module shape_mod + implicit none + type :: shape_t + real(8) :: v + contains + procedure :: area_integer + procedure :: area_real + generic :: area => area_integer + generic :: area => area_real + end type shape_t +contains + real(8) function area_integer(self, scale) + class(shape_t), intent(in) :: self + integer, intent(in) :: scale + area_integer = self%v * scale + end function area_integer + real(8) function area_real(self, scale) + class(shape_t), intent(in) :: self + real(8), intent(in) :: scale + area_real = self%v * scale + end function area_real +end module shape_mod +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + + shape = module.classes[0] + assert [(item.name, [proc.name for proc in item.procedures]) for item in shape.overload_sets] == [ + ("area", ["area_integer", "area_real"]) + ] diff --git a/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py b/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py index c610f0129..8cc96bf54 100644 --- a/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py +++ b/tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py @@ -108,3 +108,88 @@ def set(self, value: Int32) -> None: ... def test_convert_pyi_to_ir_rejects_invalid_prik_overload_links(source: str, message: str): with pytest.raises(ValueError, match=message): parse_pyi_text(source, module_name="generic_mod") + + +def test_overload_accepts_a_specific_that_projects_an_output_array_to_its_result(): + """A projected array output matches a declared result that states no mutability. + + An `intent(out)` allocatable dummy is written through as an argument, and a + projection returns it as an ordinary result. That write-through belongs to + the argument passing, so a declared result type -- which states no such + thing -- still names the same value. + """ + module = parse_pyi_text( + """ +@native_call([Return('x', 0), Addr(Arg(0))]) +def alloc_vector(n: Int32) -> Allocatable[Int32[:]]: ... + +@bind("safealloc") +@overload("alloc_vector") +def safealloc(n: Int32) -> Allocatable[Int32[:]]: ... +""", + module_name="memory_mod", + ) + + assert [(item.name, [procedure.name for procedure in item.procedures]) for item in module.overload_sets] == [ + ("safealloc", ["alloc_vector"]) + ] + + +@pytest.mark.parametrize( + "declared_result", + ["Allocatable[Float64[:]]", "Allocatable[Int32[:, :]]", "Int32"], +) +def test_overload_still_rejects_a_projected_result_of_another_type(declared_result: str): + """Neutralizing write-through leaves every other result difference compared.""" + source = f""" +@native_call([Return('x', 0), Addr(Arg(0))]) +def alloc_vector(n: Int32) -> Allocatable[Int32[:]]: ... + +@bind("safealloc") +@overload("alloc_vector") +def safealloc(n: Int32) -> {declared_result}: ... +""" + + with pytest.raises(ValueError, match="declaration 'safealloc' is incompatible"): + parse_pyi_text(source, module_name="memory_mod") + + +def test_overload_accepts_a_specific_that_projects_a_scalar_descriptor_to_its_result(): + """A nullable descriptor result is the only form an overload can restate. + + A native scalar descriptor result is written as a nullable value plus a + `native_call` result wrapper, and an overload declaration may carry no + `native_call`. The declaration therefore spells the visible value alone, as + the contract printer emits it. + """ + module = parse_pyi_text( + """ +@native_call([Allocatable(Return('x', 0)), Addr(Arg(0))]) +def alloc_character(n: Int32) -> String[:] | None: ... + +@bind("safealloc") +@overload("alloc_character") +def safealloc(n: Int32) -> String[:] | None: ... +""", + module_name="memory_mod", + ) + + assert [(item.name, [procedure.name for procedure in item.procedures]) for item in module.overload_sets] == [ + ("safealloc", ["alloc_character"]) + ] + + +@pytest.mark.parametrize("declared_result", ["String", "Int32[:] | None", "Float64[:] | None"]) +def test_overload_still_rejects_a_projected_descriptor_of_another_type(declared_result: str): + """Reading a descriptor result as nullable leaves every other difference compared.""" + source = f""" +@native_call([Allocatable(Return('x', 0)), Addr(Arg(0))]) +def alloc_character(n: Int32) -> String[:] | None: ... + +@bind("safealloc") +@overload("alloc_character") +def safealloc(n: Int32) -> {declared_result}: ... +""" + + with pytest.raises(ValueError, match="declaration 'safealloc' is incompatible"): + parse_pyi_text(source, module_name="memory_mod") diff --git a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi index ffc6ff07d..5fcc92471 100644 --- a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi +++ b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi @@ -1,5 +1,5 @@ from prik.contracts import Int32 -from shared_types import box +from .shared_types import box def box_value( item: box diff --git a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi index bb8c307a4..bcb952886 100644 --- a/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi +++ b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi @@ -1,5 +1,5 @@ from prik.contracts import Addr, Arg, Int32, native_call -from first_math import add_one +from .first_math import add_one @native_call([Addr(Arg(0))]) def double_after_add( diff --git a/tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py b/tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py index 9142b41c7..48d110b1f 100644 --- a/tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py +++ b/tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py @@ -298,7 +298,8 @@ def test_multi_source_pyi_out_writes_one_flat_combined_package(tmp_path: Path): assert entry.read_text(encoding="utf-8") == ( "from . import first_math\nfrom . import shared_types\nfrom . import second_math\nfrom . import box_ops\n" ) - assert "shared_types" in (package / "box_ops.pyi").read_text(encoding="utf-8") + assert "from .shared_types import box" in (package / "box_ops.pyi").read_text(encoding="utf-8") + assert "from .first_math import add_one" in (package / "second_math.pyi").read_text(encoding="utf-8") def test_multi_source_generated_contract_build_matches_source_runtime_and_link_order(tmp_path: Path): @@ -331,6 +332,28 @@ def test_multi_source_generated_contract_build_matches_source_runtime_and_link_o ] _assert_combined_runtime(source_module) _assert_combined_runtime(generated_module) + assert generated_module.box_ops.box is generated_module.shared_types.box + + +def test_generated_module_leaf_loads_sibling_type_contract(tmp_path: Path): + sources = _write_combined_sources(tmp_path) + entry = _generate_combined_contract(sources, tmp_path / "contracts") + native_objects = _compile_native_objects(sources, tmp_path / "native") + + module, payload = _build_contract( + entry.parent / "box_ops.pyi", + native_objects, + tmp_path / "leaf_build", + output_name="box_leaf", + ) + + assert payload["sources"] == [ + str(entry.parent / "box_ops.pyi"), + str(entry.parent / "shared_types.pyi"), + ] + box = module.box() + box.value = np.int32(7) + assert module.box_value(box) == np.int32(7) def test_multi_source_modified_entry_preserves_modules_and_adds_documented_alias(tmp_path: Path): diff --git a/tests/fortran/infrastructure/policy/test_wrapper_policy.py b/tests/fortran/infrastructure/policy/test_wrapper_policy.py index 3018f220f..c4781ed10 100644 --- a/tests/fortran/infrastructure/policy/test_wrapper_policy.py +++ b/tests/fortran/infrastructure/policy/test_wrapper_policy.py @@ -243,7 +243,9 @@ def test_fmath_scalar_policy_records_address_projected_call_slots(): assert policy.owner_path == "fmath.add_r8" assert [(export.namespace, export.name) for export in policy.python_exports] == [((), "add_r8")] - assert policy.native_name == "ADD_R8" + # The contract states no separate native name: `add_r8` reaches Fortran's + # `ADD_R8`, which is named without regard to case. + assert policy.native_name == "add_r8" assert policy.standalone is True assert [argument.name for argument in policy.arguments] == ["X", "Y"] diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json index 14299dc23..57e8c192f 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json @@ -234,6 +234,7 @@ } } ], + "reexports": [], "prototypes": [], "overload_sets": [], "classes": [], diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json index 6cc74d754..cd59851b8 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json @@ -1128,6 +1128,7 @@ } } ], + "reexports": [], "prototypes": [], "overload_sets": [], "classes": [], diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json index 468c95111..20f253b50 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json @@ -274,6 +274,7 @@ } } ], + "reexports": [], "prototypes": [], "overload_sets": [], "classes": [], diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json index f87a1fca7..af9bcfa97 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json @@ -114,6 +114,7 @@ } } ], + "reexports": [], "prototypes": [], "overload_sets": [], "classes": [ diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json index afd9fb1a0..27fab4266 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json @@ -3,6 +3,7 @@ { "name": "mesh_mod", "functions": [], + "reexports": [], "prototypes": [], "overload_sets": [], "classes": [ diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json index b6dcc3b14..876c6f9f9 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json @@ -958,7 +958,7 @@ "rank": 1, "dtype": "Float64", "shape": [ - "::Strided" + "::" ], "constraints": [], "coercions": [], @@ -977,7 +977,7 @@ "array": { "rank": 1, "shape": [ - "::Strided" + "::" ], "lower_bounds": [], "upper_bounds": [], @@ -1852,6 +1852,7 @@ } } ], + "reexports": [], "prototypes": [], "overload_sets": [], "classes": [ diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json index 22dbd792a..716355eb7 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json @@ -3,6 +3,7 @@ { "name": "constants_mod", "functions": [], + "reexports": [], "prototypes": [], "overload_sets": [], "classes": [], diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json index 638bca564..acaf83ecd 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json @@ -14,7 +14,7 @@ "rank": 1, "dtype": "Float64", "shape": [ - "::Strided" + "::" ], "constraints": [], "coercions": [], @@ -33,7 +33,7 @@ "array": { "rank": 1, "shape": [ - "::Strided" + "::" ], "lower_bounds": [], "upper_bounds": [], @@ -269,7 +269,7 @@ "rank": 1, "dtype": "Float64", "shape": [ - "::Strided" + "::" ], "constraints": [], "coercions": [], @@ -288,7 +288,7 @@ "array": { "rank": 1, "shape": [ - "::Strided" + "::" ], "lower_bounds": [], "upper_bounds": [], @@ -414,6 +414,7 @@ } } ], + "reexports": [], "prototypes": [], "overload_sets": [], "classes": [], diff --git a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json index 6dcbbac10..904d7b475 100644 --- a/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json @@ -1002,6 +1002,7 @@ } } ], + "reexports": [], "prototypes": [], "overload_sets": [ { @@ -1351,7 +1352,8 @@ "metadata": {} } } - ] + ], + "native_scope": "scope_name_reuse_combinations" } ], "classes": [ diff --git a/tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py b/tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py index 7700ef28a..2398a87eb 100644 --- a/tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py +++ b/tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py @@ -79,7 +79,8 @@ def test_pyi_parser_preserves_generic_constraints_as_annotation_metadata(): SemanticConstraint("Bounded", [1, 8]), SemanticConstraint("Finite"), ] - assert module.variables[1].name == "native_alias" + assert module.variables[1].name == "alias" + assert module.variables[1].origin.native_name == "native_alias" assert module.variables[1].semantic_type.constraints == [SemanticConstraint("Finite")] emitted = emit_module(SemanticModule(name="constraints", variables=[module.variables[0]])) assert "value: Annotated[Int32, Bounded(1, 8), Finite]" in emitted diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py index eb989adc3..0929d4e81 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py @@ -97,6 +97,15 @@ def test_pyi_emission_context_isolates_modules_and_shares_nested_imports(): assert second.contract_import() == "" +def test_printing_loaded_contract_preserves_absolute_support_imports(): + module = _parse_pyi_text( + "from typing import Any\nfrom prik.contracts import Int32\n\ndef identity(value: Int32) -> Int32: ...\n", + module_name="identity", + ) + + assert "from typing import Any" in emit_module(module) + + def test_printer_validation_and_opaque_dependency_edge_cases(): printer = PyiPrinter() @@ -283,7 +292,7 @@ def test_emit_import_renames(): code = generate_pyi(source) - assert "from list_input import delete_input_list as delete_input" in code + assert "from .list_input import delete_input_list as delete_input" in code def test_emit_imported_derived_type_reference_without_reexporting_class(): @@ -302,7 +311,7 @@ def test_emit_imported_derived_type_reference_without_reexporting_class(): stubs = emit_module_stubs(module) code = stubs["physics"] - assert "from types_mod import particle" in code + assert "from .types_mod import particle" in code assert "from . import types_mod" not in code assert "p: particle" in code assert "Addr(particle)" not in code @@ -416,7 +425,7 @@ def test_emit_bare_use_adds_import_for_opaque_dependency_type(): stubs = emit_module_stubs(fortran_module_to_semantic_module(parsed)) assert "import types_mod" in stubs["physics"] - assert "from types_mod import particle" in stubs["physics"] + assert "from .types_mod import particle" in stubs["physics"] assert stubs["types_mod"].endswith("class particle(Opaque):\n pass") @@ -505,3 +514,260 @@ def test_emit_module_aliases_standalone_only_for_actual_name_collisions(): assert "@prik_standalone\ndef standalone() -> Int32: ..." in colliding assert "standalone as prik_standalone_2" in twice_colliding.splitlines()[0] assert "@prik_standalone_2\ndef standalone() -> Int32: ..." in twice_colliding + + +def test_generated_contract_imports_a_name_under_the_spelling_its_definition_uses(): + """An import binds the name the module it reads from actually defines. + + A source-derived contract writes its declarations under Python names, so a + Fortran entity spelled in capitals is declared lower case. An import asking + for the source spelling names nothing the dependency contract defines, and + loading the package back fails on it. + """ + consts = parse_fortran_source(""" +module consts_mod +implicit none +integer, parameter :: IK = 4 +end module consts_mod +""") + infos = parse_fortran_source(""" +module infos_mod +use consts_mod, only : IK +implicit none +end module infos_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(consts), fortran_module_to_semantic_module(infos)], + normalize_fortran_public_names=True, + ) + + assert "ik: Final[Int32]" in stubs["consts_mod"] + assert "from .consts_mod import ik" in stubs["infos_mod"] + assert "import IK" not in stubs["infos_mod"] + + +def test_generated_contract_renames_an_imported_name_under_both_spellings(): + """A renamed import binds the defined name to this contract's own name.""" + consts = parse_fortran_source(""" +module consts_mod +implicit none +integer, parameter :: IK = 4 +end module consts_mod +""") + renaming = parse_fortran_source(""" +module renaming_mod +use consts_mod, only : MY_IK => IK +implicit none +end module renaming_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(consts), fortran_module_to_semantic_module(renaming)], + normalize_fortran_public_names=True, + ) + + assert "from .consts_mod import ik as my_ik" in stubs["renaming_mod"] + + +def test_generated_contract_imports_a_prototype_under_its_declared_spelling(): + """A prototype keeps its spelling, so the import that binds it keeps it too. + + A contract writes a prototype under the name its own declaration states, and + an annotation naming that prototype is written the same way, so normalizing + the import would bind a name no declaration defines. + """ + declares = parse_fortran_source(""" +module pintrf_mod +implicit none +private +public :: OBJ +abstract interface +subroutine OBJ(x) +implicit none +real(8), intent(in) :: x(:) +end subroutine OBJ +end interface +end module pintrf_mod +""") + solver = parse_fortran_source(""" +module solver_mod +use pintrf_mod, only : OBJ +implicit none +contains +subroutine solve(calfun, x) +procedure(OBJ) :: calfun +real(8), intent(inout) :: x(:) +end subroutine solve +end module solver_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(declares), fortran_module_to_semantic_module(solver)], + normalize_fortran_public_names=True, + ) + + assert "def OBJ(" in stubs["pintrf_mod"] + assert "from .pintrf_mod import OBJ" in stubs["solver_mod"] + assert "calfun: OBJ" in stubs["solver_mod"] + + +def test_fortran_contract_records_no_source_name_for_a_case_only_python_name(): + """Writing a Fortran entity in lower case renames nothing worth recording. + + Fortran names entities without regard to case, so a capitalized source + spelling and the lower-case Python name are the same entity and the + generated Fortran reaches it either way. + """ + source = """ +module consts_mod +implicit none +integer, parameter :: IK = 4 +contains +subroutine SCALE_VALUE(x) +integer, intent(in) :: x +end subroutine SCALE_VALUE +end module consts_mod +""" + + code = emit_module( + fortran_module_to_semantic_module(parse_fortran_source(source)), + normalize_fortran_public_names=True, + ) + + assert "ik: Final[Int32]" in code + assert "def scale_value(" in code + assert "SourceName" not in code + assert "@bind(" not in code + + +def test_fortran_contract_records_a_source_name_python_cannot_spell(): + """A name Python cannot hold as written keeps the spelling it came from.""" + source = """ +module naming_mod +implicit none +integer :: lambda +integer :: LAMBDA_ +contains +subroutine ASSERT(x) +integer, intent(in) :: x +end subroutine ASSERT +end module naming_mod +""" + + code = emit_module( + fortran_module_to_semantic_module(parse_fortran_source(source)), + normalize_fortran_public_names=True, + ) + + assert 'lambda_: Annotated[Int32, SourceName("lambda")]' in code + assert 'lambda__2: Annotated[Int32, SourceName("LAMBDA_")]' in code + assert '@bind("ASSERT")\n@native_call([Addr(Arg(0))])\ndef assert_(' in code + + +def test_non_fortran_declaration_compares_its_native_spelling_exactly(): + """Every other source language names its entities exactly, case included.""" + origin = SemanticOrigin(source_language="c", native_scope="c_mod") + module = SemanticModule( + name="c_mod", + functions=[ + SemanticFunction( + "scale_value", + native_name="ScaleValue", + return_type=SemanticType("Int32"), + origin=origin, + ) + ], + origin=origin, + ) + + code = emit_module(module, normalize_fortran_public_names=True) + + assert '@bind("ScaleValue")' in code + + +def test_generated_contract_binds_a_class_whose_python_name_renames_its_type(): + """A renamed class states its native type so the contract reads back.""" + origin = SemanticOrigin(source_language="fortran", native_scope="shapes_mod") + module = SemanticModule( + name="shapes_mod", + classes=[ + SemanticClass( + name="PointType", + native_name="POINT_T", + fields=[SemanticField("x", SemanticType("Float64"))], + origin=origin, + ) + ], + origin=origin, + ) + + code = emit_module(module, normalize_fortran_public_names=True) + + assert '@bind("POINT_T")\nclass PointType:' in code + + +def test_generated_contract_omits_a_class_bind_for_a_case_only_python_name(): + """A class named without regard to case states no separate native type.""" + origin = SemanticOrigin(source_language="fortran", native_scope="shapes_mod") + module = SemanticModule( + name="shapes_mod", + classes=[ + SemanticClass( + name="point_t", + native_name="POINT_T", + fields=[SemanticField("x", SemanticType("Float64"))], + origin=origin, + ) + ], + origin=origin, + ) + + code = emit_module(module, normalize_fortran_public_names=True) + + assert "class point_t:" in code + assert "@bind(" not in code + + +def test_prototype_spelling_is_kept_only_for_the_module_that_declares_one(): + """A prototype identity names its module, not a spelling used anywhere. + + One module may declare a prototype while another spells an ordinary + declaration the same way. The second follows Python naming, so an import + reading from it asks for the name that module actually defines. + """ + callbacks = parse_fortran_source(""" +module callback_mod +implicit none +private +public :: OBJ +abstract interface +subroutine OBJ(x) +implicit none +real(8), intent(in) :: x +end subroutine OBJ +end interface +end module callback_mod +""") + values = parse_fortran_source(""" +module values_mod +implicit none +integer, parameter :: OBJ = 1 +end module values_mod +""") + consumer = parse_fortran_source(""" +module consumer_mod +use values_mod, only : OBJ +implicit none +end module consumer_mod +""") + + stubs = emit_module_stubs( + [fortran_module_to_semantic_module(item) for item in (callbacks, values, consumer)], + normalize_fortran_public_names=True, + ) + + assert "def OBJ(" in stubs["callback_mod"] + assert "obj: Final[Int32]" in stubs["values_mod"] + assert "from .values_mod import obj" in stubs["consumer_mod"] + assert "import OBJ" not in stubs["consumer_mod"] diff --git a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py index d3ea0d06b..76c8a0fb9 100644 --- a/tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py @@ -55,7 +55,12 @@ def test_emit_basic_scalar_function(): assert ") -> Float64: ..." in code -def test_fortran_generated_contracts_emit_python_name_and_bind_original_name(): +def test_fortran_generated_contracts_emit_python_name_without_binding_the_same_name(): + """A capitalized Fortran procedure is written lower case and binds nothing. + + Fortran reaches a procedure without regard to case, so the lower-case + Python name already names it and no original spelling has to be recorded. + """ module = SemanticModule( name="math_mod", functions=[ @@ -72,7 +77,8 @@ def test_fortran_generated_contracts_emit_python_name_and_bind_original_name(): code = emit_module(module, normalize_fortran_public_names=True) - assert '@bind("SQUARE_R4")\ndef square_r4(' in code + assert "def square_r4(" in code + assert "@bind(" not in code def test_emit_rejects_unknown_semantic_type(): diff --git a/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py index e6ba0bc49..74b3b17fa 100644 --- a/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py +++ b/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py @@ -872,7 +872,7 @@ def test_convert_pyi_to_ir_handles_pointer_and_array_storage_variants(): assert rank_any.storage.array.category == "assumed_rank" assert rank_any.storage.array.source_shape == [".."] assert rank_any.rank == 1 - assert strided.shape == ["0:n:Strided"] + assert strided.shape == ["0:n:"] assert strided.storage.array.contiguous is False assert computed.shape == ["xl.size"] assert bounded.constraints == [ diff --git a/tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py index 13cae8693..bdc595c62 100644 --- a/tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py +++ b/tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py @@ -73,7 +73,7 @@ def import_lines(names): ) return [line for line in emit_module(module).splitlines() if line.startswith("from ")] - expected = [f"from types import {', '.join(sorted(type_names))}"] + expected = [f"from .types import {', '.join(sorted(type_names))}"] assert import_lines(type_names) == expected assert import_lines(reversed(type_names)) == expected diff --git a/tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py index 8a3cbba51..b002003de 100644 --- a/tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py +++ b/tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py @@ -194,7 +194,10 @@ def f() -> tuple[F64, Gives["y", F64]]: ... module_name="edited", ) - assert module.variables[0].name == "native_alias" + # The declared name stays the Python name; SourceName states the native + # entity it reaches, as bind does for a callable. + assert module.variables[0].name == "alias" + assert module.variables[0].origin.native_name == "native_alias" assert module.variables[0].semantic_type.shape == ["1:n"] assert module.functions[0].return_type is not None assert module.functions[0].return_type.name == "Float64" @@ -242,7 +245,7 @@ def apply( assert args["A"].source_shape == ["LDA", "N"] assert args["A"].lower_bounds == [None, None] assert args["A"].upper_bounds == [None, None] - assert args["work"].shape == ["::Strided"] + assert args["work"].shape == ["::"] assert args["work"].axes == ["strided"] assert args["work"].contiguous is False assert args["work"].source_shape == [] @@ -252,21 +255,31 @@ def apply( assert args["scratch"].source_shape == [] -def test_convert_pyi_to_ir_accepts_explicit_strided_marker_for_edited_contracts(): +def test_convert_pyi_to_ir_reads_a_strided_axis_from_its_empty_step(): + """An empty step marks a strided axis; a bounded axis keeps its bounds.""" module = parse_pyi_text( """ -current: Float64[::] -explicit: Float64[::Strided] +unbounded: Float64[::] bounded: Float64[0:n:] -explicit_bounded: Float64[0:n:Strided] """, module_name="strided_axes", ) arrays = [variable.semantic_type.storage.array for variable in module.variables] - assert [array.shape for array in arrays] == [["::Strided"], ["::Strided"], ["0:n:Strided"], ["0:n:Strided"]] - assert [array.axes for array in arrays] == [["strided"], ["strided"], ["strided"], ["strided"]] - assert [array.contiguous for array in arrays] == [False, False, False, False] + assert [array.shape for array in arrays] == [["::"], ["0:n:"]] + assert [array.axes for array in arrays] == [["strided"], ["strided"]] + assert [array.contiguous for array in arrays] == [False, False] + + +@pytest.mark.parametrize("dimension", ["Float64[::Strided]", "Float64[0:n:Strided]", "Float64[::2]"]) +def test_convert_pyi_to_ir_rejects_a_dimension_step(dimension: str): + """A dimension carries bounds only, so the step position spells nothing. + + `T[::]` already says strided, so the longer explicit form it replaced is + refused rather than kept as a second way to write the same contract. + """ + with pytest.raises(ValueError, match="not part of the contract grammar"): + parse_pyi_text(f"x: {dimension}\n", module_name="rejected_step") def test_convert_pyi_to_ir_uses_fortran_native_array_defaults(): @@ -585,3 +598,44 @@ def test_native_contract_structurally_accepts_declared_type_and_constraint_edits assert native_contract_issues(parse_pyi_text(constrained, module_name="solver_mod")) == [] assert native_contract_issues(parse_pyi_text(changed_abi, module_name="solver_mod")) == [] + + +def test_source_name_binds_a_native_entity_without_taking_the_declared_name(): + """`SourceName` states what a declaration reaches, like `bind` on a callable. + + A contract is edited to give an entity the name Python should call it, and + that name has to survive. Reading the source spelling as the declaration's + own name discards the edit and exports the native spelling instead. + """ + module = pyi_text_to_semantic_module( + """ +from prik.contracts import Annotated, Final, Int32, SourceName + +tally: Annotated[Int32, SourceName("COUNTER")] + +limit: Final[Annotated[Int32, SourceName("MAXFUN")]] +""", + module_name="edited", + ) + + assert [(item.name, item.origin.native_name) for item in module.variables] == [ + ("tally", "COUNTER"), + ("limit", "MAXFUN"), + ] + assert [constraint.name for constraint in module.variables[1].semantic_type.constraints] == ["Constant"] + + +def test_class_binds_a_native_type_under_its_own_python_name(): + """A class states the native type it reaches when the two names differ.""" + module = pyi_text_to_semantic_module( + """ +from prik.contracts import Float64, bind + +@bind("POINT_T") +class PointType: + x: Float64 +""", + module_name="edited", + ) + + assert (module.classes[0].name, module.classes[0].native_name) == ("PointType", "POINT_T") diff --git a/tests/fortran/modules/end_to_end/test_module_variables_and_state.py b/tests/fortran/modules/end_to_end/test_module_variables_and_state.py index 04268fe8c..3085a7d36 100644 --- a/tests/fortran/modules/end_to_end/test_module_variables_and_state.py +++ b/tests/fortran/modules/end_to_end/test_module_variables_and_state.py @@ -7,6 +7,7 @@ import numpy as np import pytest from tests.fortran._support.wrapper_build import ( + _build_source_and_import, _build_source_or_generated_pyi_and_import, _build_text_and_import, _sole_native_module, @@ -534,3 +535,139 @@ def test_declared_length_character_module_arrays_compile_and_expose_their_width( module.deferred_ptr.deallocate() assert module.deferred_ptr.associated is False assert module.deferred_ptr.shape is None + + +REEXPORT_SOURCE = """ +module reexport_home_mod + implicit none +contains + subroutine scale_value(value, scaled) + integer, intent(in) :: value + integer, intent(out) :: scaled + scaled = value * 2 + end subroutine scale_value +end module reexport_home_mod + +module reexport_facade_mod + use reexport_home_mod, only : scale_value + implicit none + private + public :: scale_value +end module reexport_facade_mod + +module reexport_default_mod + use reexport_home_mod + implicit none +end module reexport_default_mod + +module reexport_shout_mod + implicit none +contains + subroutine SCALE_LOUD(value, scaled) + integer, intent(in) :: value + integer, intent(out) :: scaled + scaled = value * 3 + end subroutine SCALE_LOUD +end module reexport_shout_mod + +module reexport_case_mod + use reexport_shout_mod, only : SCALE_LOUD + implicit none + private + public :: SCALE_LOUD +end module reexport_case_mod + +module reexport_renamed_mod + use reexport_home_mod, only : public_scale => scale_value + implicit none + private + public :: public_scale +end module reexport_renamed_mod + +module reexport_wildcard_mod + use reexport_home_mod + implicit none + private + public :: scale_value +end module reexport_wildcard_mod +""" + + +def test_explicitly_published_import_is_reachable_without_a_second_wrapper(tmp_path: Path): + """Naming an imported procedure in a `public` statement publishes it here. + + The declaration is not repeated: the published name binds to the one + wrapper its own module exposes, so both namespaces share a single callable. + A module that merely imports without publishing adds no name of its own. + """ + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_facade_mod.scale_value is module.reexport_home_mod.scale_value + assert module.reexport_facade_mod.scale_value(np.int32(4)) == np.int32(8) + + # A plain `use` states no intent to publish, so it adds nothing. + assert not hasattr(module, "reexport_default_mod") or "scale_value" not in dir(module.reexport_default_mod) + + # One wrapper defines the procedure; the facade only names it again. + generated = (tmp_path / "build" / "reexport_wrapper.c").read_text(encoding="utf-8") + assert generated.count("static PyObject * wrap_scale_value") == 1 + + +def test_published_import_resolves_the_python_name_its_declaring_module_bound(tmp_path: Path): + """A re-export binds a Python attribute, which is not a Fortran spelling. + + A Fortran entity written in capitals is exported under its Python name, so + the module publishing it has to reach for that name rather than the source + spelling, which names no attribute at all. + """ + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_case_mod.scale_loud is module.reexport_shout_mod.scale_loud + assert module.reexport_case_mod.scale_loud(np.int32(4)) == np.int32(12) + assert not hasattr(module.reexport_case_mod, "SCALE_LOUD") + + +def test_renamed_published_import_shares_the_wrapper_it_renames(tmp_path: Path): + """A renamed re-export states a new name for one existing callable.""" + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_renamed_mod.public_scale is module.reexport_home_mod.scale_value + assert module.reexport_renamed_mod.public_scale(np.int32(6)) == np.int32(12) + + +def test_publishing_a_name_a_plain_use_brought_in_republishes_only_that_name(tmp_path: Path): + """A plain `use` publishes nothing until a name is named in `public`. + + Such a `use` carries every public name of the module it reads, so the + `public` statement is what says which of them this module means to publish. + """ + source = tmp_path / "reexport.f90" + source.write_text(REEXPORT_SOURCE, encoding="utf-8") + module = _build_source_and_import( + source, + tmp_path / "build", + {"bind_c_reexport_wrapper.f90", "reexport_wrapper.c", "reexport_wrapper.h"}, + ) + + assert module.reexport_wildcard_mod.scale_value is module.reexport_home_mod.scale_value + assert module.reexport_wildcard_mod.scale_value(np.int32(5)) == np.int32(10) + # The same plain `use` without a `public` statement publishes nothing. + assert not hasattr(module, "reexport_default_mod") or "scale_value" not in dir(module.reexport_default_mod) diff --git a/tests/fortran/modules/parsing/test_scope_handling.py b/tests/fortran/modules/parsing/test_scope_handling.py index 3e5867c6c..8f926e1d5 100644 --- a/tests/fortran/modules/parsing/test_scope_handling.py +++ b/tests/fortran/modules/parsing/test_scope_handling.py @@ -188,3 +188,45 @@ def test_fortran_parser_class_entrypoint(): assert len(signatures) == 1 assert signatures[0].name == "touch" + + +def test_repeated_use_of_one_module_accumulates_its_imports(): + """A scope may name the same module in several `use` statements. + + Each statement adds what it lists, so a later one extends the imports + rather than replacing them; real sources split long import lists this way, + and dropping the earlier statements loses the names they carried. + """ + module = parse_fortran_file( + """ +module consumer_mod + use, intrinsic :: iso_fortran_env, only : INT32, SP => REAL32, DP => REAL64 + use, intrinsic :: iso_fortran_env, only : QP => REAL128 + use, intrinsic :: iso_fortran_env, only : STDOUT => OUTPUT_UNIT + implicit none +end module consumer_mod +""" + ).modules[0] + + assert [(item.source, item.target) for item in module.uses["iso_fortran_env"]] == [ + ("INT32", None), + ("REAL32", "SP"), + ("REAL64", "DP"), + ("REAL128", "QP"), + ("OUTPUT_UNIT", "STDOUT"), + ] + + +def test_a_bare_use_absorbs_the_named_imports_of_the_same_module(): + """Importing everything subsumes any list beside it.""" + module = parse_fortran_file( + """ +module wide_mod + use kinds_mod, only : rk + use kinds_mod + implicit none +end module wide_mod +""" + ).modules[0] + + assert module.uses["kinds_mod"] == [] diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi index 9248c87c6..b4724f0f6 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi @@ -1,47 +1,38 @@ -from prik.contracts import Int32, Returns, String, bind, standalone +from prik.contracts import Int32, Returns, String, standalone -@bind("CHAR_CODE_DEFAULT") @standalone def char_code_default( C: String[1] ) -> tuple[Int32, Returns["C", String[1]]]: ... -@bind("CHAR_CODE_STAR1") @standalone def char_code_star1( C: String[1] ) -> tuple[Int32, Returns["C", String[1]]]: ... -@bind("STRING_LEN_STAR8") @standalone def string_len_star8( TEXT: String[8] ) -> tuple[Int32, Returns["TEXT", String[8]]]: ... -@bind("STRING_LEN_ASSUMED") @standalone def string_len_assumed( TEXT: String ) -> tuple[Int32, Returns["TEXT", String]]: ... -@bind("STRING_LEN_ENTITY") @standalone def string_len_entity( TEXT: String[6] ) -> tuple[Int32, Returns["TEXT", String[6]]]: ... -@bind("CHAR_RESULT_DEFAULT") @standalone def char_result_default() -> String[1]: ... -@bind("STRING_RESULT_STAR8") @standalone def string_result_star8() -> String[8]: ... -@bind("STRING_RESULT_PADDED") @standalone def string_result_padded() -> String[8]: ... -@bind("STRING_RESULT_DECLARED") @standalone def string_result_declared() -> String[6]: ... diff --git a/tests/fortran/strings/semantics/test_string_pyi_semantics.py b/tests/fortran/strings/semantics/test_string_pyi_semantics.py index 8de38d5c5..7f9ddb587 100644 --- a/tests/fortran/strings/semantics/test_string_pyi_semantics.py +++ b/tests/fortran/strings/semantics/test_string_pyi_semantics.py @@ -79,7 +79,7 @@ def array_assumed_strided(values: String[...][::]) -> None: ... assert assumed_type.metadata["fortran_character_length"] == "*" assert assumed_type.rank == 1 assert assumed_type.shape == [":"] - assert array_assumed_strided.arguments[0].semantic_type.shape == ["::Strided"] + assert array_assumed_strided.arguments[0].semantic_type.shape == ["::"] emitted = emit_module(module) assert "value: String" in emitted