Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
f7e65e5
Expose non-target module arrays as views and interoperable logicals
saidctb Sep 2, 2026
bbee891
Borrow the runtime descriptor for an allocatable actual
saidctb Sep 3, 2026
92af0d2
Publish a native entry-point table for module array handles
saidctb Sep 3, 2026
c3b272d
Take the published entry-point table when placing a descriptor argument
saidctb Sep 3, 2026
bde7120
Publish entry-point tables for field and owned result handles
saidctb Sep 3, 2026
3cc23e4
Detach a borrowed descriptor from the capsule that carried it
saidctb Sep 3, 2026
ab2b6b7
Refuse a PROTECTED module array before generating its accessors
saidctb Sep 3, 2026
24a01f9
Let a descriptor consumer define the module array it receives
saidctb Sep 3, 2026
248ff1e
Call inside the descriptor consumer for a writable allocatable dummy
saidctb Sep 4, 2026
d742fec
Place every descriptor argument inside its consumer
saidctb Sep 4, 2026
941b901
Remove the machinery for handing a copied descriptor to a binding
saidctb Sep 4, 2026
c15013f
Reach a pointer dummy through its consumer, as an allocatable one is
saidctb Sep 4, 2026
ff8d32d
Place a present optional descriptor argument like any other
saidctb Sep 4, 2026
a73b08b
Delete the machinery for rebuilding a descriptor in C
saidctb Sep 4, 2026
97ff88f
Publish an entry-point table for wrapper-owned descriptor storage
saidctb Sep 4, 2026
f6d29bf
Build an allocatable handle's NumPy view where its descriptor is
saidctb Sep 4, 2026
eb48fda
Read an array handle's storage from its table for an ordinary dummy
saidctb Sep 4, 2026
7ec432e
Normalize an empty dimension when a handle reaches an ordinary dummy
saidctb Sep 4, 2026
00e9bf6
Answer an owned handle's inquiries from the descriptor it holds
saidctb Sep 4, 2026
16d92b0
Reach an array handle's storage from C for more dummy forms
saidctb Sep 4, 2026
c19ade5
Report a refused array handle from the binding, and cover characters
saidctb Sep 4, 2026
79e7652
Publish one versioned backend for every array handle
saidctb Sep 5, 2026
abc7b3a
codex: Answer every handle inquiry from the descriptor it already has
saidctb Sep 5, 2026
b76447a
codex: Delete the lowering the descriptor route replaced, and cover t…
saidctb Sep 5, 2026
e10f5f2
Dispatch every array handle through one backend and one call
saidctb Sep 5, 2026
a2cf473
Refuse a borrowed descriptor a call cannot hold open
saidctb Sep 5, 2026
9d02d1b
Make the call inside the descriptors, always
saidctb Sep 5, 2026
3479b32
codex: Keep native array documentation current
saidctb Sep 5, 2026
3e586a4
Say which storage an owned handle operation needs
saidctb Sep 5, 2026
127e3c4
Record the capsule ABI break for extension builders
saidctb Sep 5, 2026
daa8f29
codex: Complete native array backend audit
saidctb Sep 5, 2026
b9e2d3f
Prove the two things the capsule name and an absent optional promise
saidctb Sep 5, 2026
7feba1c
Hold the record still while the version name does
saidctb Sep 5, 2026
e7a3a52
Name the capsule after the record it carries
saidctb Sep 5, 2026
ab6e467
Drop the version number the tag made redundant
saidctb Sep 5, 2026
72c1031
Describe the capsule name the code actually publishes
saidctb Sep 5, 2026
7a812cb
codex: Version native array backend semantics
saidctb Sep 6, 2026
78ef2cd
codex: Classify how each array dummy is reached, and say why a layout…
saidctb Sep 6, 2026
3ab611f
codex: Describe a NumPy array to a Fortran descriptor dummy
saidctb Sep 6, 2026
75717c9
codex: Format the descriptor constructor
saidctb Sep 6, 2026
6e431b5
codex: Reach a descriptor dummy from a handle and from NumPy alike
saidctb Sep 6, 2026
2aa5423
codex: Hand every descriptor-capable array over as a descriptor
saidctb Sep 6, 2026
ebad154
codex: Cover every source and shape a signed-stride handoff accepts
saidctb Sep 6, 2026
20d4f74
codex: Complete and harden descriptor array handoff
saidctb Sep 6, 2026
795717c
codex: Take a fixed-width character array by the only dummy that can …
saidctb Sep 6, 2026
9b46574
codex: Distinguish native entities from descriptor projections
saidctb Sep 6, 2026
22a2c81
codex: Take a default-kind LOGICAL array at its real width in the LAP…
saidctb Sep 6, 2026
d76bc7a
update docs
saidctb Sep 6, 2026
a7b491a
codex: Unify character-array handles on Fortran-owned storage
saidctb Sep 8, 2026
12a9d68
codex: complete signed stride array handling
saidctb Sep 8, 2026
a71e627
codex: Add zero-copy deferred character pointer views
saidctb Sep 8, 2026
8254533
codex: Leave deferred pointer compatibility to compiler tests
saidctb Sep 9, 2026
47ca904
remove dependency : mkdocs-material
saidctb Sep 9, 2026
ce73fd1
codex: Support extended native array handle dtypes
saidctb Sep 9, 2026
5c89d9c
codex: Restore efficient generated array dispatch
saidctb Sep 9, 2026
be91987
codex: Preserve character array widths in shared dispatch
saidctb Sep 9, 2026
63d605a
use int64_t instead of CFI_index_t
saidctb Sep 9, 2026
b8bff86
improve performance
saidctb Sep 9, 2026
f083bbf
codex: Keep native-array documentation task-focused
saidctb Sep 9, 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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ compilation should use the focused owners under
`tests/fortran/infrastructure/building/compiling/` as applicable. Include the
relevant end-to-end feature tests whenever a generated or compiled mechanism
changes; run a broader suite when behavior spans multiple stages.
Run pytest with at most `-n 2`. Never `-n 4`, `-n 8`, or `-n auto`. The development machine has 12 cores but only about 7 GB of RAM, and every xdist worker loads NumPy while the Fortran end-to-end tests fork gfortran and cc per test on top of `pytest-monitor` profiling each one. Higher parallelism exhausts memory and thrashes swap, which has hard-frozen the machine and forced a reboot. Prefer the narrowest owning test path over a full suite run, and commit verified work promptly rather than batching it behind a long run.
Do not run LAPACK wrapper tests locally unless the user explicitly asks for them. Local verification may run everything else, including BLAS-only real-library tests; leave LAPACK coverage to GitHub Actions by default.
Do not run the full coverage workflow for routine changes. Run focused tests plus the required static-analysis suite. Reserve the complete CI-style coverage workflow for explicit pre-merge or pull-request verification, or when the user specifically requests it.
When investigating coverage failures, mirror the GitHub Actions workflow before deciding the fix: run coverage with `COVERAGE_PROCESS_START=pyproject.toml`, combine parallel data with `python3 -m coverage combine`, then run `python3 -m coverage report`. Do not assume a plain local coverage run matches CI, especially when subprocess tests are involved.
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ release tags add a leading `v` to the package version.

## Unreleased

- Array handles support allocatable and pointer arguments, results, module
variables, derived fields, optional arguments, and matching ordinary-array
parameters. Numeric and character arrays accept supported forward and
reversed Fortran sections without copying.

- Character array handles support caller-created and returned storage.
Deferred-length allocation and resizing use `element_length=...`.

- Wider Fortran logical arrays use the matching-width NumPy integer dtype;
`logical(c_bool)` arrays use `numpy.bool_`. Handle storage also supports
`longdouble`, `clongdouble`, and `uintp` where the target exposes them.

- Improved generated-wrapper build time and contiguous-array call overhead.

- **Breaking (native ABI):** native array handles use
`prik.native_array_backend.v2`. Rebuild extensions that exchange handles.

## 0.4.3 — 2026-08-31

- Republishes 0.4.2. That tag carried the previous package version, so the
Expand Down
183 changes: 183 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

PRIK (Python Runtime Interop Kit) generates native Python bindings from Fortran
projects: importable CPython extensions plus editable `.pyi` contracts for
reshaping the generated Python API. The active codebase is entirely Python
(`prik/`); Fortran/C fixture and source files (`*.f90`, `*.f95`, `*.for`, `*.c`,
`*.h`) are inputs/outputs of the tool, not implementation — don't spend
analysis effort on them unless explicitly asked to.

This repo also has an `AGENTS.md` with detailed, load-bearing contributor
policy (test philosophy, changelog rules, policy-completion boundary, QA
gating). Read it — the summary below pulls out what matters most day to day,
but AGENTS.md is authoritative for edge cases.

## Commands

Install with dev/QA extras:

```bash
python3 -m pip install -e ".[qa]"
```

Run the full suite:

```bash
PYTHONPATH=. python3 -m pytest -q
```

Run a single test / focused owner (always prefer the narrowest owning path over the full suite):

```bash
python3 -m pytest -q path/to/tests
python3 -m pytest -q path/to/test_file.py::test_name
```

The five independently-collecting suite roots:

```bash
python3 -m pytest -q tests/fortran -m "not real_library"
python3 -m pytest -q tests/c
python3 -m pytest -q tests/docs
python3 -m pytest -q tests/tools
python3 -m pytest -q tests/workflows
```

For documentation-only changes, run only:

```bash
python3 -m pytest -q tests/docs
git diff --check
```

Static analysis suite (blocking for any code/test/build/tooling change; run before pushing):

```bash
python3 -m ruff check .
python3 -m ruff format --check .
python3 tools/check_static_analysis_versions.py
python3 tools/check_codegen_complexity.py # advisory
python3 -m bandit -c pyproject.toml -r prik --severity-level medium --confidence-level medium
python3 -m vulture
python3 tools/check_radon_policy.py --base-ref auto # blocking (changed code)
python3 -m radon cc prik -n C -s --total-average # advisory, still run
python3 -m radon mi prik -s # advisory, still run
```

Enable the tracked pre-push hook once per clone (`git config core.hooksPath .githooks`) — it runs the static-analysis gate plus focused doc/wrapper/tools/workflows smoke tests.

Full CI-style coverage (only for explicit pre-merge/PR verification or investigating a coverage failure — mirror this exactly, a plain local `coverage run` does not match CI):

```bash
COVERAGE_PROCESS_START=pyproject.toml PYTHONPATH=. python3 -m coverage run -m pytest -q --randomly-seed=1
python3 -m coverage combine
python3 -m coverage report # fail_under = 90
```

Run pytest with at most `-n 2` — never `-n 4`, `-n 8`, or `-n auto`. This
machine has 12 cores but only ~7 GB of RAM; each xdist worker loads NumPy
while the Fortran end-to-end tests fork gfortran and cc per test, so higher
parallelism exhausts memory, thrashes swap, and has hard-frozen the machine.
Prefer the narrowest owning test path, and commit verified work promptly
rather than batching it behind a long run.

Don't run LAPACK wrapper tests locally unless explicitly asked (leave to GitHub Actions); ordinary local runs also exclude `real_library` generally. `examples/blas`, `examples/lapack`, `examples/fftpack`, `examples/minpack` are full-library correctness projects with their own workflows.

Alternate-compiler toolchain lane (when changing compiler portability/native generation):

```bash
python3 tools/run_fortran_toolchain_lane.py --compiler=/path/to/compiler
```

## Architecture

PRIK is a strict pipeline: source facts flow forward through owned stages, and
**meaning moves forward only** — a downstream stage implements an upstream
decision, it never reinterprets or overrides it.

```
preprocessing/ -> parsers/ -> semantics/ -> policy/ -> planning/ -> codegen/ -> printers/ -> compiler/ -> runtime/
```

| Package | Owns |
| --- | --- |
| `preprocessing/` | Source prep, provenance, includes, compiler-derived target/type probes |
| `parsers/` | Syntax facts only (`fortran/`, `pyi/` — a deferred `c/` frontend exists but isn't the published contributor architecture) |
| `semantics/` | Language-neutral semantic IR (`SemanticModule`); shared meaning, not a Python API or emitted code |
| `policy/` | **Every** interoperability decision: object kind, ownership, transfer, destruction, mutability/writeback, nullability, output projection, release responsibility, storage mode (stack/heap/alias), getter/setter behavior, support |
| `planning/` | Projects policy-complete IR into a deterministic, backend-neutral `ModulePlan` (`WrapperPlanner.build()`) — orders/names/validates, invents nothing |
| `codegen/` | Dispatches the plan into named C-binding and Fortran-bridge lowering mechanisms; backend scalar projection |
| `printers/` | Serializes formed nodes to C/Fortran/`.pyi` text — no behavior decisions |
| `compiler/` | Compiler commands, compile objects, native-support install, linking |
| `runtime/` | Python runtime objects + bundled native support used by generated extensions |
| `pipeline/` | End-to-end build orchestration (`build.py`, `wrapper.py`, `pyi.py`) tying the stages together |
| `contracts/` | Public names usable in semantic `.pyi` contracts (deliberately public — its import path is part of `.pyi` syntax) |
| `naming/` | Shared public-name and generated-symbol policy |
| `utilities/` | Stage-neutral helpers only (parsing/normalization/rendering/evaluation/visitor) |

Two input routes converge at `SemanticModule` and share everything after it:
Fortran source (`preprocessing` → `parsers/fortran` → `semantics/fortran2ir.py`)
and semantic `.pyi` contracts (`parsers/pyi` → `semantics/pyi2ir.py`).

**The hard boundary is before `WrapperPlanner.build()`.** By that point policy
must be fully decided. Binding/bridge generators (`codegen/`) must never infer
or override policy from datatype, Fortran `intent`, alias shape, storage
layout, or a local memory check, and must not add a silent fallback — if a
decision is missing, that's a bug in `policy/`, not something to patch around
in codegen. When changing behavior, prefer expressing it in completed policy
or the shared wrapper plan; touch binding/bridge lowering only when the plan
already requires a genuinely new emitted-code mechanism.

**Deciding binding vs. bridge, or any ABI question:** ask how it would work for
a `bind(C)` procedure, where there is no bridge. A direct entrypoint has only
the binding and the user's C ABI symbol, so whatever the direct route must do
is binding-owned; the bridge owns exactly the remainder that makes an ordinary
non-`bind(C)` procedure reachable through the same completed plan. When a form
*cannot* be `bind(C)` at all — e.g. a deferred-length `character(len=:)` dummy,
which the standard rejects there because `bind(C)` character dummies must have
length 1 — that proves a generated Fortran adapter is mandatory and names what
it must construct. See AGENTS.md for the full rule.

Array declaration expressions specifically cross packages in a fixed order:
`utilities/declaration_expressions.py` (parse/normalize text) → `semantics/`
(record native callable provenance) → `policy/` (complete support) →
`codegen/` (consume the completed plan only).

Root entry points: `prik.__init__` exposes `build_fortran_extension`,
`build_pyi_extension`, and `__version__` only; `prik/cli.py` is the `python3 -m prik`
dispatcher into the same stage owners. Deeper docs: `docs/developer/architecture.md`,
`docs/developer/codebase-map.md`, `docs/developer/packages/*.md`.

## Test tree

Tests mirror the pipeline and are organized `tests/<language>/<documented-feature>/<owning-stage>/`
(stage names: `parsing/`, `probes/`, `preprocessing/`, `semantics/`, `policy/`,
`codegen/`, `printers/`, `compiling/`, `pipeline/`, `runtime/`, `end_to_end/`).
Give each test one primary invariant, placed at the earliest stage that can
prove it; add `end_to_end/` only when generation/compilation/import/runtime
behavior contributes a distinct claim. Genuinely internal (non-public-behavior)
mechanisms live under `tests/fortran/infrastructure/<production-package>/`,
mirroring the production module. See `tests/README.md` for the full stage
table and markers (`fortran_end_to_end`, `real_library`, `toolchain_smoke`, `property`, `regression`, `slow`, `benchmark`).

## Working conventions (see AGENTS.md for full detail)

- Update `CHANGELOG.md` under **Unreleased** for any user/maintainer-visible
change (public APIs, features, examples, CI/build workflow, benchmark
methodology, documented limitations). Skip it for invisible internal cleanup.
- When asked to move/change an API, import path, command, or behavior, remove
the old path — do not add compatibility shims, aliases, or fallbacks unless
explicitly asked to keep them.
- Tests are evidence for a named invariant (observable behavior, a public API,
a documented diagnostic/serialized format, ABI/ownership/lifetime/build
behavior, a stage boundary, or tooling-consumed structure) — not a freeze on
prose, private names, file inventories, or incidental layout. Remove tests
that only pin removed behavior; don't add tests that only prevent refactors.
- After finishing an implementation task, summarize which pipeline stages
actually changed (parsing / semantic IR / policy / planning / codegen /
bridge / compilation / docs) and what changed there, plus the tests
touched and how they were verified.
34 changes: 17 additions & 17 deletions docs/developer/packages/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ select a different view behavior from local descriptor facts.
## A Native Array Handle At Runtime

```text
generated operation dictionary + dtype, rank, ownership, and view policy
generated dispatcher + completed capabilities + native backend
+ dtype, rank, ownership, and view policy
-> NativeArrayHandleBase validation and owner retention
-> AllocatableArray or PointerArray
-> state, lifecycle, association, and to_numpy() operations
```

The operation dictionary is the boundary between generated extension code and
the stable Python handle API. An operation exists only when the completed plan
allows the generator to expose it. Missing operations fail explicitly rather
than being inferred from `allocatable` or `pointer` alone.
The dispatcher and capabilities come from the completed wrapper plan. The
runtime validates them, retains the owners required for a live NumPy view, and
uses the generated native backend for descriptor and lifecycle work. It never
infers an operation or ownership rule from the declaration alone.

## Local Structure

Expand All @@ -45,21 +46,19 @@ prik/runtime/
```

- [`handles.py`](../../../prik/runtime/handles.py) contains the Python runtime.
`NativeArrayHandleBase` validates common metadata and operations.
`NativeArrayHandleBase` validates common metadata and completed capabilities.
`AllocatableArray` adds allocation state, resize, and deallocation;
`PointerArray` adds association, nullification, allocation, resize, and
deallocation when supplied. Internal adapters translate generated call
signatures and descriptor handoffs.
deallocation when supplied.
- `native_support/prik_binding.h` contains header-only CPython/NumPy
conversion, descriptor, validation, capsule, and release support. Change it
only with its generated C users and `prik/compiler/native_support.py`.
- `native_support/LICENSE` is distributed with the native payload.

`to_numpy()` returns `None` for an absent allocatable or pointer and otherwise
validates the completed view policy, dtype, rank, and any required contiguity.
Native argument handoff performs the additional expected shape, layout,
alignment, byte-order, and writeability checks. A returned NumPy array is a
view of native storage; a caller that needs independent storage must copy it.
`to_numpy()` returns `None` for absent storage and otherwise applies the
completed view policy. Native argument handoff is performed in the binding
against the planned array contract. A returned NumPy array is a view of native
storage; a caller that needs independent storage must copy it.

## Run The Handle Demonstration

Expand All @@ -75,17 +74,18 @@ Resized shape: (4,)
Generated resize received NumPy extents: True
```

The example supplies the same operation-dictionary shape as generated code.
It creates an allocatable handle, reads its live NumPy view, and routes a
resize through the adapter. The native header has no standalone Python route;
the compiler installs it into a generated `binding_support/` directory.
The example creates an allocatable handle, reads its live NumPy view, and
resizes it. The compiler installs the native header into a generated
`binding_support/` directory.

## Change Routes And Evidence

- Change handle protocol, validation, retention, views, or adapters in
`handles.py`.
- Change the native payload together with its generated users and
`prik/compiler/native_support.py`.
- Update the native backend ABI version when its callback contract or record
meaning changes.
- Complete new ownership, lifecycle, operation, or view policy before planning
rather than selecting it in runtime code.

Expand Down
21 changes: 14 additions & 7 deletions docs/user/guide/allocatables.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ and a PRIK handle gives Python access to that descriptor.
- Reallocation or deallocation invalidates existing views.
- Module and derived-field handles expose storage that belongs to their module
or parent object. Returned and caller-created handles have their own
descriptor storage.
native handle storage.
- When available, `deallocate()` releases the current allocation but keeps the
handle open. `close()` permanently ends a returned or caller-created handle.

Expand All @@ -50,10 +50,17 @@ Use ordinary `T[...]` when the callable needs only array data:
def sum_values(values: Float64[:]) -> Float64: ...
```

An allocated handle may satisfy a matching ordinary array parameter.

A plain NumPy array cannot satisfy an `Allocatable[T[...]]` parameter because
it does not carry native allocation state. Use `to_numpy()` when Python needs
the current array data held by an allocatable handle.

Character allocatable parameters use the same handle form. A fixed-width
contract includes the width, for example `Allocatable[String[8][:]]`. A
deferred-width contract uses `Allocatable[String[:][:]]`; its `resize()` call
must supply `element_length`.

---

## Allocatable Array Handle API
Expand All @@ -73,8 +80,8 @@ assert values.allocated is True
```

The annotation supplies the element dtype and rank. The handle creates its
native descriptor storage when first passed to a matching writable argument.
It stays the same Python object after the call.
native storage when first passed to a matching writable argument. It stays the
same Python object after the call.
`Allocatable[Float64]()` is not supported because scalar allocatables cross the
Python boundary as values rather than array handles.

Expand Down Expand Up @@ -102,8 +109,8 @@ else:
| `rank` | `int` | Declared number of dimensions. |
| `to_numpy()` | `numpy.ndarray \| None` | A live view of current storage, or `None` when unallocated. It never creates an automatic detached snapshot. |
| `deallocate()` | `() -> None` | Deallocates current storage when this operation is available for the handle. |
| `resize(shape)` | `(int \| Sequence[int]) -> None` | Allocates or resizes storage to `shape` when this operation is available for the handle. |
| `close()` | `() -> None` | Permanently releases a returned or caller-created descriptor and any remaining allocation. It does nothing on a module or field handle. |
| `resize(shape, *, element_length=None)` | `(...) -> None` | Allocates or resizes storage to `shape` when available. Pass `element_length` only for deferred-length character arrays. |
| `close()` | `() -> None` | Permanently releases a returned or caller-created handle and any remaining allocation. It does nothing on a module or field handle. |
| `closed` | `bool` | Whether a closable handle has been closed. |

Calling `deallocate()` or `resize(shape)` when the operation is unavailable
Expand Down Expand Up @@ -151,8 +158,8 @@ assert h.shape == (5,)

### Function Results

An allocatable-array function result becomes an `AllocatableArray` with its own
descriptor storage, which PRIK releases automatically:
An allocatable-array function result becomes an `AllocatableArray` that owns
the returned allocation, which PRIK releases automatically:

```python
values = api.make_values(3)
Expand Down
Loading
Loading