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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/openmpi-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ name: Open MPI Integration

on:
workflow_call:
push:
branches:
- main
- release/*
workflow_dispatch:

permissions:
Expand Down
40 changes: 35 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,42 @@ release tags add a leading `v` to the package version.

## Unreleased

- The README and derived-type contract guides show how to bind a class method
to a module procedure, including when the two share a name.
- Fortran `character(kind=selected_char_kind('ISO_10646'))` (UCS-4) values are
supported through the new `UString` contract type, which takes every form
`String` does and maps storage to NumPy `U<n>` instead of `S<n>`. A
character kind given by `selected_char_kind('ASCII')` or `'DEFAULT'` is now
an ordinary `String` instead of an unsupported kind. A numeric character
kind such as `kind=4` is classified by asking the compiler which character
set it numbers that way.
- **Breaking:** scalar fields of a Fortran derived type read as live rank-zero
NumPy views of the object's storage, as module variables do. A numeric or
logical field returns a writable `T[()]` view instead of a NumPy scalar, and
a fixed-length character field returns a writable fixed-width bytes view
instead of a `str`; writing through the view or assigning the attribute
updates the object. Generated contracts spell these fields `T[()]` and
`String[n][()]`; an edited contract that keeps plain `T` or `String[n]`
still reads a copied value.
- Scalar `allocatable` and `pointer` fields of a derived type, numeric,
logical, complex, or character, are now wrapped like the matching module
variables: reading one returns a live rank-zero NumPy view or `None`, and
assigning to it allocates an allocatable (resizing a deferred-length
character) or writes a pointer's current target. These fields are not
keywords of the default constructor, in the built class and in the
generated `.pyi` alike. Such a type previously failed to build.
- A derived object passed through a `pointer` dummy now reports a field it
cannot reach as a policy diagnostic, as an `allocatable` dummy already did,
instead of failing during wrapper planning.
- Generated extension modules serve their module variables through
descriptors on the module type, so looking up a function or any other
ordinary attribute costs what it costs on a plain module instead of first
being compared with every module variable name. `mpi.comm_rank(comm)` on
the Open MPI tutorial's extension drops from 245 to 164 ns.
- Open MPI integration CI now runs the `mpi_f08` tutorial on Linux and macOS
against Open MPI 4.1 and 5.0 with paired GNU C/Fortran compilers, and
compares its two-rank result with mpi4py built from the same installation.
compares its two-rank result with mpi4py built from the same installation,
on every pull request and on pushes to `main` and release branches.
The tutorial provides a repeatable matched-installation benchmark and a
labeled local results table comparing its wrapped API and mpi4py-style
Python API with mpi4py, including relative timings. The benchmark binds
Expand Down Expand Up @@ -123,10 +151,12 @@ release tags add a leading `v` to the package version.
dummies wider than one byte use integer storage of their own width, as
logical arrays do, so default-logical `intent(inout)` updates reach Python.
- Omitting an optional `intent(inout)` scalar argument returns `None` for it.
- Scalar allocatable and pointer module variables return live read-only
rank-zero NumPy views, or `None` when storage is absent. Assigning to the
attribute allocates an allocatable (resizing a deferred-length character) or
writes a pointer's current target.
- Scalar allocatable and pointer module variables return live writable
rank-zero NumPy views of their current storage, or `None` when storage is
absent. A view is valid until native code reallocates, deallocates, or
reassociates that storage. Assigning to the attribute allocates an
allocatable (resizing a deferred-length character) or writes a pointer's
current target.
- A separate module-level `PARAMETER` statement types an undeclared name by the
module's `IMPLICIT` rules and is rejected under `implicit none`.
- A derived type a module reaches through another module's re-export is
Expand Down
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ end module points
import numpy as np
import geometry.points as points

item = points.point(x=np.float64(3.0), y=np.float64(4.0))
item = points.Point(x=np.float64(3.0), y=np.float64(4.0))
points.move(item, np.float64(1.0), np.float64(-2.0))

print(item.x, item.y) # 4.0 2.0
Expand All @@ -119,25 +119,26 @@ Want a more Pythonic API? Edit `contracts/points.pyi`:
```python
from prik.contracts import Addr, Arg, Float64, Pass, bind, native_call

class point:
x: Float64 = 0.0
y: Float64 = 0.0
class Point:
x: Float64[()] = 0.0
y: Float64[()] = 0.0

def __init__(self, *, x: Float64 = 0.0, y: Float64 = 0.0) -> None: ...

@bind("move")
@native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))])
def translate(self, dx: Float64, dy: Float64) -> None: ...

@bind("norm_squared")
@native_call([Pass()])
def norm_squared(self) -> Float64: ...
```

`@bind("move")` is needed because `translate` has a different Python name.
`norm_squared` needs no `@bind`: matching Python and native names select the
same procedure. `Pass()` supplies the receiver (`self`) to the native call;
`Addr(Arg(...))` passes the remaining arguments by address as required by the
native calling convention.
A method without `@bind` calls the type-bound procedure of its own name. Both
methods here call module procedures instead, so each names one with `@bind`:
`translate` calls `move`, and `norm_squared` calls `norm_squared`. `Pass()`
supplies the receiver (`self`) to the native call; `Addr(Arg(...))` passes the
remaining arguments by address as required by the native calling convention.

Build from the contract:

Expand All @@ -153,7 +154,7 @@ The native Fortran is unchanged, but the Python surface is now:
import numpy as np
import geometry.points as points

item = points.point(x=np.float64(3.0), y=np.float64(4.0))
item = points.Point(x=np.float64(3.0), y=np.float64(4.0))
item.translate(np.float64(1.0), np.float64(-2.0))

print(item.x, item.y) # 4.0 2.0
Expand Down Expand Up @@ -235,21 +236,20 @@ code generation with a diagnostic naming the boundary and the reason.

**Types and arrays**

- arrays of derived types and higher-rank assumed-size `type(*)` arrays;
- parameterized derived types such as `type :: buffer_type(k, n)`;
- character arrays that cannot be represented as a fixed-width NumPy bytes
dtype, and `allocatable` and `pointer` character *fields*.
- real and complex storage wider than the target's `long double`. NumPy's
- Arrays of derived types.
- Parameterized derived types such as `type :: buffer_type(k, n)`.
- `character` kinds other than the default kind and `ISO_10646` (UCS-4).
- Real and complex storage wider than the target's `long double`. NumPy's
`longdouble` is whatever the target C compiler provides, so `real(10)` and C
`long double` are supported while IEEE quad `real(16)` is refused on a target
whose `long double` is x87 extended precision. The diagnostic names the
measured mantissa width on both sides.

**Procedures and polymorphism**

- procedure-pointer module variables, and
callbacks retained after the wrapped call returns;
- polymorphic outputs, mutable polymorphic arguments, polymorphic
- Procedure-pointer module variables, and
callbacks retained after the wrapped call returns.
- Polymorphic outputs, mutable polymorphic arguments, polymorphic
`allocatable` and `pointer` scalars, and unlimited polymorphism (`class(*)`).

The [language feature matrix](https://pynumlab.github.io/prik/user/language-support/feature-matrix/)
Expand Down Expand Up @@ -329,8 +329,8 @@ print(stats.extremes(values)) # (np.float64(1.0), np.float64(5.0))
`count` never appears in the Python signature — the contract derives it from
the array — and the two output pointers come back as a tuple instead of being
passed in. `mean` and `extremes` need no `@bind` because their Python and C
names match; use `@bind("native_name")` only when they differ. The same rule
applies to Fortran contracts.
names match; use `@bind("native_name")` only when they differ. Fortran module
functions follow the same rule.

### What C support covers

Expand Down
8 changes: 4 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ The generated `points.pyi` is:
from prik.contracts import Addr, Arg, Float64, native_call

class Point:
x: Float64 = 0.0
y: Float64 = 0.0
x: Float64[()] = 0.0
y: Float64[()] = 0.0

def __init__(self, *, x: Float64 = 0.0, y: Float64 = 0.0) -> None: ...

Expand Down Expand Up @@ -221,8 +221,8 @@ The edited `points.pyi` is:
from prik.contracts import Addr, Arg, Float64, Pass, bind, native_call

class Point:
x: Float64 = 0.0
y: Float64 = 0.0
x: Float64[()] = 0.0
y: Float64[()] = 0.0

def __init__(self, *, x: Float64 = 0.0, y: Float64 = 0.0) -> None: ...

Expand Down
17 changes: 9 additions & 8 deletions docs/user/guide/allocatables.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ and an array handle gives Python access to that descriptor.
## Key Concepts

- Scalar allocatable dummies and results appear as values or `None`. Reading a
scalar allocatable module variable returns a live read-only rank-zero NumPy
view or `None`; assigning to it allocates when needed. Array allocatables use
`Allocatable[T[...]]` handles.
scalar allocatable module variable or derived-type field returns a live
rank-zero NumPy view or `None`; assigning to it allocates when needed. Array
allocatables use `Allocatable[T[...]]` handles.
- An array handle exposes allocation state and descriptor operations; it is not
itself a NumPy array.
- `allocated` reports whether storage exists; `to_numpy()` returns a live view
Expand Down Expand Up @@ -84,14 +84,15 @@ assert values.allocated is True
The annotation supplies the element dtype and rank. The handle creates its
native storage when first passed to a matching writable argument. It stays the
same Python object after the call.
`Allocatable[Float64]()` is not supported. Reading a scalar module variable
declared `Allocatable[Float64]` returns a live read-only rank-zero `float64`
array when allocated, or `None` otherwise. Assign to the attribute to change
the value: `module.scale = np.float64(2.0)` allocates the variable when it is
`Allocatable[Float64]()` is not supported. Reading a scalar module variable or
derived-type field declared `Allocatable[Float64]` returns a live rank-zero
`float64` array when allocated, or `None` otherwise. Write through the view to
change the current value, or assign to the attribute:
`module.scale = np.float64(2.0)` allocates the variable when it is
unallocated, and a deferred-length character takes the width of the assigned
`str`. Read the attribute again after reallocation; an older view may refer to
storage that is no longer valid. An allocated empty deferred-length character
reads as `b""`.
reads as a detached `b""`.

A returned or attribute array handle remains present even when its descriptor
is unallocated. Reading the Python attribute
Expand Down
14 changes: 8 additions & 6 deletions docs/user/guide/pointers.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ shape, and strides. It does not by itself say who owns that target.
- A pointer descriptor refers to target storage; it does not own that storage
by default.
- Scalar pointer dummies and results appear as values or `None`. Reading a
scalar pointer module variable returns a live read-only rank-zero NumPy view
or `None`; assigning to it writes the current target. Array pointers use live
scalar pointer module variable or derived-type field returns a live
rank-zero NumPy view of its target or `None`; writing through the view or
assigning to it writes the current target. Array pointers use live
`Pointer[T[...]]` handles.
- `associated` describes association, not ownership or target lifetime.
- NumPy arrays returned by `to_numpy()` are live views, not copies.
Expand Down Expand Up @@ -77,10 +78,11 @@ assert target.associated is True
The annotation supplies the element dtype and rank. The handle creates its
native storage when first passed to a matching writable argument. It stays the
same Python object after the call.
`Pointer[Float64]()` is not supported. Reading a scalar module variable
declared `Pointer[Float64]` returns a live read-only rank-zero `float64` array
when associated, or `None` otherwise. Assigning to the attribute copies the
value into the current target; it raises `ValueError` when the pointer is not
`Pointer[Float64]()` is not supported. Reading a scalar module variable or
derived-type field declared `Pointer[Float64]` returns a live rank-zero
`float64` view of its target when associated, or `None` otherwise. Write
through the view, or assign to the attribute to copy the value into the
current target; it raises `ValueError` when the pointer is not
associated, and a character value must encode to the target's width. Assignment
never reassociates the pointer. Read the attribute again after reassociation;
an older view may refer to storage that is no longer valid.
Expand Down
31 changes: 31 additions & 0 deletions docs/user/guide/strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,37 @@ b'Xlpha '
- `String[8][()]` and `String[8][count]` require dtype `S8`.
- A dummy without `intent` uses the conservative `intent(inout)` behavior.

## Unicode Strings

A Fortran `character(kind=selected_char_kind('ISO_10646'))` declaration stores
four bytes per character (UCS-4). Its contract uses `UString` wherever a
default-kind declaration uses `String`, and every form above works the same
way:

| Contract | Python value |
| --- | --- |
| `UString[8]` | `str` or rank-zero NumPy `U8` array |
| `UString[8][()]` | Rank-zero NumPy array with dtype `U8` |
| `UString[8][count]` | NumPy array with dtype `U8` |

```fortran
integer, parameter :: ucs4 = selected_char_kind('ISO_10646')
character(kind=ucs4, len=8) :: title
```

```python
title: UString[8][()]
```

A `UString[8]` length counts characters, not bytes, so it accepts any `str` of
exactly eight characters. A `str` argument is converted into four-byte call
storage; pass NumPy `U<n>` storage to share memory with Fortran instead.
Declare the kind with `selected_char_kind('ISO_10646')`, or with a kind number
the compiler assigns to that set, such as `kind=4` on GNU Fortran; PRIK asks
the compiler which set a number names. The compiler must provide the set: GNU
Fortran and LLVM Flang do, Intel `ifx` does not. A kind that selects
`'ASCII'` or `'DEFAULT'` is an ordinary `String`.

## Allocatable And Pointer Scalar Strings

A scalar `character` dummy may carry the `allocatable` or `pointer` attribute,
Expand Down
43 changes: 28 additions & 15 deletions docs/user/guide/wrapping-derived-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ class Point:
y: Float64 = 0.0
) -> None: ...

x: Float64 = 0.0
y: Float64 = 0.0
x: Float64[()] = 0.0
y: Float64[()] = 0.0

class Holder:
def __init__(self) -> None: ...
Expand Down Expand Up @@ -159,7 +159,7 @@ print(item.x, item.y) # 4.0 6.0
made = points.make_point(np.float64(8.0), np.float64(9.0))

# Nested component
container = points.holder()
container = points.Holder()
points.set_origin(container, made)
container.origin.x = np.float64(12.0)
print(container.origin.x) # 12.0
Expand Down Expand Up @@ -214,7 +214,16 @@ print(points.Point.__init__.__doc__)
instance and do not return it again.
- **Missing intent**: A dummy without `intent` follows the same conservative
in-place rule as `intent(inout)`.
- **Fields**: Public scalar numeric/logical/complex fields become Python attributes.
- **Fields**: Public scalar numeric, logical, complex, and character fields
read as live rank-zero NumPy views of the object's storage, as module
variables do. A numeric or logical field is a writable `T[()]` view, and a
fixed-length character field is a writable fixed-width bytes view: writing
through the view or assigning the attribute updates the object. A scalar
`allocatable` or `pointer` field reads as a view of its current storage, or
`None` when it is unallocated or disassociated; assigning to it allocates an
allocatable field or writes a pointer field's current target. Every view keeps its
parent object alive; read an `allocatable` or `pointer` field again after
its storage changes.
- **Nested types**: Appear as generated objects tied to their parent.
- **Results**: Derived-type function results create new independent objects.
An `allocatable` result must be allocated when the function returns, as
Expand All @@ -223,7 +232,7 @@ print(points.Point.__init__.__doc__)
cannot turn into `None`. A `pointer` result may be disassociated: the
returned object then raises `ReferenceError` when its value is read.
- **Default constructor**: Automatically generated from public, writable
primitive scalar fields.
primitive scalar fields that are not `allocatable` or `pointer`.
- **Constructor fields**: Passed by keyword (`logical`, `integer`, `real`, and
`complex`).

Expand Down Expand Up @@ -292,8 +301,8 @@ In this mapping, `@bind` selects the native initializer,
from prik.contracts import Addr, Arg, Float64, Pass, bind, native_call

class Point:
x: Float64
y: Float64
x: Float64[()]
y: Float64[()]

@bind("initialize_point")
@native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))])
Expand Down Expand Up @@ -334,27 +343,31 @@ end subroutine increment
```

```python
item = counters.counter(value=np.int32(4))
item = counters.Counter(value=np.int32(4))
item.increment(np.int32(3))
print(item.value) # 7
```

The method mutates the existing `counter`; it does not replace the Python
object.
The method mutates the existing `Counter`; it does not replace the Python
object. In a contract, a method without `@bind` calls the type-bound procedure
of its own name, and `@bind("Counter.increment")` names a type-bound procedure
whose name differs from the method's.

### Expose a Module Procedure as a Method

The `move(item, dx, dy)` procedure from this page's example can remain a
module-level function and also become `point.move(dx, dy)`.
module-level function and also become `Point.move(dx, dy)`.

`Pass()` supplies `self` to the native call. `Arg(i)` refers to a visible
Python argument. Add the method to the existing `point` class while keeping
the module declaration:
`@bind("move")` makes the method call the module procedure `move` rather than a
type-bound procedure. `Pass()` supplies `self` to the native call. `Arg(i)`
refers to a visible Python argument. Add the method to the existing `Point`
class while keeping the module declaration:

```python
from prik.contracts import Addr, Arg, Float64, Pass, native_call
from prik.contracts import Addr, Arg, Float64, Pass, bind, native_call

class Point:
@bind("move")
@native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))])
def move(self, dx: Float64, dy: Float64) -> None: ...

Expand Down
6 changes: 3 additions & 3 deletions docs/user/guide/wrapping-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ Mutable fixed-storage scalar module variables expose live rank-zero NumPy
views. Numeric and logical scalars use `T[()]`; fixed-length character scalars
use `String[n][()]` with raw bytes storage. Pass the view itself when a native
dummy needs its original storage. `PARAMETER` declarations remain constants.
- Scalar allocatable and pointer module variables return a live read-only
rank-zero view or `None` on each read. Assign to the attribute to change the
value, and read it again after storage changes.
- Scalar allocatable and pointer module variables return a live rank-zero view
or `None` on each read. Write through the view or assign to the attribute to
change the value, and read it again after storage changes.
- Allocatable module arrays use the `Allocatable[T[...]]` API.
- Allocation, lifetime, NumPy views, and mutation rules are covered in
the storage and objects section.
Expand Down
Loading
Loading