Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1b75d53
Fix imported abstract interfaces and callback scalar outputs
saidctb Sep 14, 2026
195e32b
Treat an undeclared callback scalar intent as conservatively writable
saidctb Sep 14, 2026
85c5893
Prove the undeclared callback intent through a contract round trip
saidctb Sep 15, 2026
ec7002a
Resolve imported callback interfaces in their declaring module
saidctb Sep 15, 2026
b0364b8
Carry interface provenance through results, renames, and accessibility
saidctb Sep 15, 2026
9623e8c
Spell runtime extents the way a contract spells them in diagnostics
saidctb Sep 15, 2026
e0f9a19
Extract prototype binding from external reference reconciliation
saidctb Sep 15, 2026
eed7e66
Remove the Strided contract name and the dimension step
saidctb Sep 15, 2026
d362f6d
Carry a strided axis as the spelling a contract uses
saidctb Sep 15, 2026
0b9d450
Record the declaring name for a renamed re-export chain
saidctb Sep 15, 2026
f0b3c14
Build one generic interface from every block that declares it
saidctb Sep 15, 2026
ac314f3
Extend a generic interface across the modules that build it
saidctb Sep 15, 2026
dfd955f
Publish an imported procedure a module explicitly makes public
saidctb Sep 15, 2026
841575d
codex: generate relative sibling imports in Fortran leaf contracts
saidctb Sep 15, 2026
2a1dd8f
Keep native scopes bare under relative sibling imports
saidctb Sep 15, 2026
51de26f
Keep every import when a scope uses one module repeatedly
saidctb Sep 15, 2026
bf935a6
Remove an empty test module left by a probe
saidctb Sep 15, 2026
0a040df
Collect every specific a split generic binding names
saidctb Sep 15, 2026
3f7955a
Resolve a kind an interface body names for itself
saidctb Sep 15, 2026
586e4e6
Accept an overload declaration that restates a projected result
saidctb Sep 15, 2026
6ec888d
Import each name as the contract defining it spells it
saidctb Sep 15, 2026
6f439d3
Let a contract rename the entity a declaration reaches
saidctb Sep 15, 2026
fc4a32d
Record a source spelling only where Python cannot hold the name
saidctb Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,180 @@ release tags add a leading `v` to the package version.

## Unreleased

- 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
Expand Down
63 changes: 58 additions & 5 deletions docs/user/guide/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
19 changes: 12 additions & 7 deletions docs/user/reference/pyi-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ The generated forms therefore have these responsibilities:
| C `<name>.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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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` |
Expand Down
6 changes: 6 additions & 0 deletions prik/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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))
Expand Down
Loading
Loading